Грабли двух WebSocket-каналов в WikiMind
В WikiMind развели WebSocket для нотификаций и CRDT-редактирования. Рассказываем, где легко связать независимые каналы обратно и потерять предсказуемость.

«Если колокольчик отвалится, редактор документа тоже перестанет работать?» — этот вопрос быстро снял романтику с идеи “один WebSocket на всё”.
В WikiMind у нас были две разные real-time-задачи. Первая — нотификации для колокольчика. Вторая — совместное CRDT-редактирование документов. Обе удобно гонять через WebSocket, но требования у них разные.
Для CRDT-редактора канал влияет на рабочий сценарий: пользователь печатает, получает операции других участников, клиент должен понимать состояние соединения. Для нотификаций всё мягче: WebSocket работает как push-only сигнал. Если пользователь offline, сообщение через сокет не догоняем. Оно остаётся в БД и читается через REST.
Развели это на два независимых канала. Ниже — грабли, на которые почти наступили.
Грабля 1: один сокет кажется проще, пока не появляется общий отказ
На старте самый очевидный вариант — держать один WebSocket и различать сообщения по типу: notification, crdt_operation, presence и так далее. Меньше соединений, один reconnect, один обработчик ошибок.
Проблема в том, что так мы получаем общую точку отказа для двух разных функций. Если канал падает из-за истории с нотификациями, совместное редактирование тоже теряет транспорт. Если редактору нужно переподключиться или пересинхронизировать CRDT-состояние, колокольчик оказывается втянут в этот же цикл.
Это особенно неприятно для UI. Пользователь может спокойно читать документ, но из-за проблемы на нотификационном сценарии редактор начинает выглядеть “сломавшимся”. Или наоборот: активная работа с документом влияет на доставку обычных push-событий.
Мы разделили WebSocket-каналы физически и по жизненному циклу:
- канал нотификаций живёт отдельно;
- канал CRDT-редактирования живёт отдельно;
- падение одного не закрывает второй;
- reconnect и обработка ошибок не общие.
Живая деталь: больше всего спорили не про протокол, а про “а не перебор ли два соединения на одно приложение”. В итоге аргумент был простой: соединений больше на одно, зато отказ перестал распространяться между сценариями.
Грабля 2: нотификационный WebSocket легко случайно сделать очередью
С нотификациями была другая ловушка. Как только появляется WebSocket, очень хочется начать относиться к нему как к гарантированной доставке: пользователь был offline — значит, при reconnect надо дослать всё пропущенное через тот же канал.
Но в WikiMind мы не закладывали такую семантику. Нотификационный WebSocket у нас push-only. Он сообщает о событии, если пользователь сейчас online. Если нет — push теряется. Это нормально, потому что источник правды не сокет, а БД.
Прочитать накопленные уведомления можно через REST. Поэтому после открытия интерфейса или возврата пользователя в приложение клиент не должен ждать “историю” из WebSocket. Он забирает актуальное состояние обычным запросом.
Так мы не смешали два режима доставки:
- WebSocket — быстрый сигнал для online-пользователя;
- БД — хранение уведомлений;
- REST — чтение текущего списка и пропущенных сообщений.
Важный момент: потеря push-события не равна потере уведомления. Теряется только мгновенный сигнал в колокольчик, запись остаётся доступной через REST.
Грабля 3: независимые каналы можно случайно связать на клиенте
Даже если на сервере каналы разделены, их легко снова склеить во фронтенде. Например, сделать один общий connection manager, один глобальный статус “WebSocket disconnected” и одну кнопку “переподключиться”.
Для WikiMind это было бы вредно. Если упал нотификационный канал, редактор не должен получать состояние ошибки. Если CRDT-канал переподключается, колокольчик не обязан мигать тревогой.
Поэтому состояния разделили так же, как соединения. У каждого канала свои события, свои ошибки и свой reconnect. Общим остаётся только то, что действительно общее для приложения: пользовательская сессия и возможность сходить в REST за данными.
Это убрало странные пограничные состояния. Например, пользователь может временно не получать push-нотификации, но продолжать редактировать документ. Или открыть колокольчик и через REST прочитать уведомления, пока CRDT-канал восстанавливается после обрыва.
Где проходит граница
Критерий получился практический: если отказ одного real-time-сценария не должен ломать другой, им не место в одном WebSocket-канале.
В WikiMind нотификации и CRDT-редактирование отличаются по критичности, поведению при offline и способу восстановления данных. Поэтому два независимых канала оказались проще в эксплуатации, чем один универсальный транспорт с кучей условий.
Следующий шаг для таких схем — явно описывать контракт каждого канала: что он доставляет, что не гарантирует, где хранится состояние и каким запросом клиент восстанавливается после offline.