Перейти к содержанию

Участники и awareness

Общий текст уже можно передать другой реплике, но пользователю недостаточно видеть только итоговую строку. Во время совместной работы полезно знать, кто находится в редакторе. Такое состояние быстро меняется: участник открыл вкладку, переместил курсор, ушёл. В этом уроке вводим awareness — отдельный механизм эфемерного присутствия, который не превращает список открытых вкладок в часть истории Markdown-документа.

В состоянии lesson-05 появляется объект Awareness, связанный с текущим Y.Doc, и строка участников над текстовым полем. Пока это локальное состояние. Наличие двух имён в собственных полях документа не означало бы двух подключённых участников; список формируется из состояний awareness. Сетевую доставку этого канала реализуем следующим шагом вместе с provider.

Присутствие и содержимое

Представьте, что Анна закрыла браузер после добавления пункта повестки. Её пункт должен остаться в документе, а отметка «Анна сейчас здесь» должна исчезнуть. Если хранить присутствие в обычной общей карте без отдельного жизненного цикла, отметка может сохраняться вместе с документом и после аварийного завершения. Нам нужна разная семантика для содержимого и временного состояния клиента.

Awareness использует идентификатор реплики и обновляемое состояние. В него можно положить имя для отображения, выбранный цвет или данные выделения. Значение должно быть небольшим и пригодным для передачи; не стоит добавлять сюда целый документ, историю ввода или секреты сессии. Состояние присутствия будет регулярно обновляться, поэтому лишние данные увеличивают расход сети даже без содержательной правки текста.

Ниже показан точный фрагмент src/app.js, который создаёт локальный awareness и обновляет строку участников. Переменная doc уже существует в openEditor, а класс импортирован из y-protocols/awareness. Полный файл состояния также снимает подписку и уничтожает awareness при закрытии редактора.

 const awareness=new Awareness(doc);awareness.setLocalStateField('user',{name:displayName});
 const roster=()=>{$('people').textContent=[...awareness.getStates().values()].map(x=>x.user?.name || 'Участник').join(', ');};awareness.on('change',roster);roster();
 let disposed=false;

После самостоятельного открытия страницы строка участников должна содержать имя локальной реплики. Пока сеть не подключена, появление одного имени не доказывает присутствие второго пользователя. Событие change означает изменение состава или значений состояний; список читается через getStates. Здесь имена вставляются через textContent, поэтому клиентское поле не становится HTML-шаблоном.

Изменение одного поля

Метод setLocalStateField меняет конкретное поле локального состояния. Это удобно, когда слой курсора и слой профиля работают независимо. Если каждый слой присваивал бы целиком весь объект через setLocalState, он мог бы случайно стереть поля другого слоя. Но такая независимость не отменяет соглашение о схеме: имена user и будущего cursor должны одинаково интерпретироваться всеми клиентами.

Для явного ухода локальное состояние устанавливают в null. В результате остальные участники должны удалить его из своего списка после доставки уведомления. При аварийном обрыве доставить последнее сообщение невозможно; тогда удаление выполняется по тайм-ауту протокола или на стороне собственного provider при закрытии соединения. Поэтому нельзя обещать мгновенное исчезновение каждой потерянной вкладки при любом виде сбоя.

У awareness различаются события изменения состояния и его обновления. Для интерфейса обычно нужен change, чтобы перерисовать реальные изменения. Для транспорта нужен update, поскольку даже обновление без нового видимого значения может поддерживать актуальность присутствия. Если отправлять только изменения имени, неподвижный участник выглядел бы давно исчезнувшим, хотя его соединение продолжает работать.

Пользователь и вкладка

В списке awareness одна запись соответствует реплике, а не обязательно аккаунту. Анна может открывать две вкладки и получить две записи. Если продукт хочет показывать «два человека», необходимо сгруппировать подтверждённые аккаунты и отдельно решить, как отображать несколько активных вкладок одного участника. Наивное значение размера getStates представляет количество известных состояний, а не точный подсчёт уникальных людей.

Имя в клиентском awareness не является доказательством личности. Браузер способен написать «Администратор» или скопировать имя другого участника. В собственном серверном протоколе мы заменим отображаемые данные значениями из проверенной сессии. Сервер также свяжет состояние с разрешённым идентификатором клиента, чтобы соединение не могло удалять присутствие чужой реплики. Такая проверка находится вне задачи CRDT-слияния.

В учебном интерфейсе сведения о ролях также придут с сервера. Надпись «редактор» в произвольном клиентском JSON не должна разрешать отправку update. У прав доступа другой источник истины: серверная таблица участников комнаты. Awareness лишь сообщает интерфейсу данные для отображения. Даже если злоумышленник подменит локальную надпись, серверный обработчик изменения документа должен отклонить запрещённый пакет.

Курсор без обещаний привязки

Данные курсора часто считают естественным продолжением присутствия, но обычный числовой индекс быстро устаревает при чужих вставках. Если Борис добавил строку перед курсором Анны, индекс старого положения уже не описывает прежнее место текста. Для совместных курсоров можно применять относительные позиции Yjs и превращать их обратно в абсолютные относительно текущей реплики, учитывая возможность отсутствующего результата.

Мы пока не рисуем чужие курсоры. Учебный сервер в следующем состоянии оставит поле курсора пустым и покажет только подтверждённые имена. Это конкретное ограничение примера: список участников реализован отдельно от полноценной binding-интеграции выделений. Нельзя представить такую строку имён как законченную систему совместного редактирования курсоров, особенно при сложном IME и удалении диапазонов.

Нельзя также использовать наличие awareness как блокировку редактирования. Два участника могут одновременно видеть одно и то же временное состояние с небольшой задержкой. Если бизнес-процесс требует исключительного владения документом на определённом этапе, нужна серверная блокировка или иной контракт с проверкой срока и владельца. Отметка «Анна печатает» сообщает наблюдение интерфейса, но не резервирует ресурс.

В самостоятельном варианте измените только поле имени и проследите предполагаемое обновление строки. Затем установите локальное состояние в null и объясните, почему текст повестки должен сохраниться. Это и есть результат урока: содержимое документа и присутствие имеют разные сроки жизни. Следующий шаг соединит оба канала с собственным сервером, сохранив различия между ними.

Оглавление серии · Исходные состояния проекта.

API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.