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

Серверное сохранение и подтверждение записи

В восьмом состоянии «Черновик» умеет восстановить браузерную реплику из IndexedDB. Но сервер всё ещё теряет общий документ после остановки процесса. Теперь добавим однопроцессное серверное хранение: текущую структуру Yjs будем сохранять в файл и отправлять подтверждение только после успешной записи. Этот урок посвящён долговечности общей копии, а не изменению механизма слияния.

Каталог lesson-09 продолжает собственный provider и добавляет функции чтения и сохранения в server.mjs. Хранилище содержит base64 двоичного состояния и место для будущих ревизий. Base64 выбран ради JSON-контейнера учебного файла; он не является защитой данных. Файл расположен в серверном каталоге storage, который не входит в разрешённую карту статических ресурсов.

Загрузка перед обслуживанием комнаты

Сервер сначала проверяет существование файла. Если запись есть, он читает контейнер и применяет update к новому Y.Doc. Только после этого сервер начинает обслуживать соединения. Начальный план вставляется исключительно при отсутствии серверного состояния. Такой порядок предотвращает повторную серверную инициализацию поверх восстановленной истории.

Ошибочный файл нельзя автоматически считать отсутствующим. Если JSON повреждён или двоичный пакет не читается, запуск должен остановиться с диагностикой, чтобы владелец решил, какую копию восстанавливать. Молчаливое создание пустого документа могло бы привести к тому, что клиенты начнут согласовывать рабочую историю с новым шаблоном. В коде чтение не скрывается успешной ветвью catch.

Ниже дан полный блок persist из server.mjs. Переменные doc, revisions и file определены выше в том же файле. Входящий update уже прошёл проверку сессии и роли. Здесь кодируетcя текущее серверное состояние; подписки клиентов или строковый предварительный просмотр в этой функции не участвуют.

function persist() {
  if (LESSON < 9) return;
  mkdirSync(new URL('./storage/',import.meta.url),{recursive:true});
  const temp = new URL('./storage/draft-42.tmp',import.meta.url);
  const fd=openSync(temp,'w');
  try { writeFileSync(fd,JSON.stringify({update:Buffer.from(Y.encodeStateAsUpdate(doc)).toString('base64'),revisions})); fsyncSync(fd); } finally { closeSync(fd); }
  renameSync(temp,file);
  const directory=openSync(new URL('./storage/',import.meta.url),'r');
  try { fsyncSync(directory); } finally { closeSync(directory); }
}
if (LESSON >= 9 && !existsSync(file)) persist();

Временный файл и замена

Запись сначала направляется во временный файл. Затем его содержимое синхронизируется, дескриптор закрывается и временный путь заменяет основной. Дополнительно синхронизируется каталог. Такой порядок уменьшает вероятность получить частично записанный основной JSON после сбоя. Но конкретные гарантии зависят от файловой системы, ОС и устройства; код не доказывает устойчивость к любому аппаратному отказу.

Оба пути находятся в одном каталоге. Это существенно для операции переименования: перенос между разными устройствами имеет другое поведение. Учебный сервер работает в одном процессе и сохраняет состояние последовательно синхронными операциями. Если несколько процессов одновременно записывают один файл, данная схема не обеспечивает координацию. В распределённом сервисе нужно выбрать хранилище и контракт конкурентной записи отдельно.

После самостоятельной проверки перезапуск сервера должен восстановить последний успешно записанный документ без повторной вставки исходного плана. Если запись update завершилась успешно, клиент должен получить сообщение ack с номером пакета. Эти эффекты ожидаются по устройству примера; сервер, файловые отказы и перезапуски при авторстве не проверялись. Не следует выдавать выбранную последовательность файловых вызовов за измеренную надёжность.

Что подтверждает ack

Номер seq создаётся provider для исходящего пакета текущей транспортной работы. Сервер возвращает его после persist. Это подтверждает принятую запись в границах учебного хранилища. Номер не является глобальной версией документа: два клиента имеют собственные счётчики, а после нового объекта provider отсчёт начинается заново. Для истории продукта нужен отдельный идентификатор ревизии.

Надпись «Сервер сохранил пакет» точнее общей надписи «Сохранено навсегда». Подтверждение относится к конкретной операции и серверной копии. Оно не говорит, что создан независимый backup, что данные перенесены в другое место или что восстановление уже проверено. Резервное копирование требует отдельного объекта хранения, политики срока и процедуры проверки восстановления.

Если клиент не получил подтверждение, это ещё не доказывает, что запись не произошла. Ответ мог потеряться после успешного сохранения. При reconnect двоичный обмен снова согласует состояния; повторная доставка известного update допустима. Но UI может временно оставаться неопределённым относительно последнего подтверждения. Такой промежуточный статус нужно объяснять пользователю, а не трактовать как обязательную потерю данных.

Ошибка сохранения

Сначала сервер применяет update в памяти, затем сохраняет результат. Если файловая операция падает, откат внутренней CRDT-структуры автоматически не выполняется. Учебный сервер поэтому переходит в состояние отказа, закрывает соединения и не допускает новые upgrade. Это предотвращает продолжение работы с памятью, которая расходится с последней подтверждённой дисковой копией.

При восстановлении после такого отказа владельцу процесса нужно исследовать причину: права каталога, заполненный диск, ошибку устройства или повреждённый путь. Копирование работающей памяти в произвольный файл без анализа не заменяет процедуру восстановления. Клиенты могут сохранить неподтверждённые правки в своём IndexedDB, но это не делает их автоматически доверенной серверной резервной копией.

Стоимость снимка

В учебном примере на каждый update кодируется всё известное состояние. Это понятно для маленького плана встречи, но стоимость растёт вместе с документом и частотой ввода. Синхронная запись также задерживает обработку других запросов. Для большого сервиса можно хранить журнал updates, выполнять уплотнение и делать снимки реже, сохраняя чёткую границу подтверждения долговременной записи.

При таком переходе потребуется учесть повторную доставку, порядок фиксации, очистку старого журнала и восстановление после частичного уплотнения. Просто удалить старые updates после создания файла недостаточно: нужно доказать, что новый снимок содержит всё подтверждённое состояние. Это уже задача хранилища, тогда как Yjs предоставляет удобные двоичные представления для реализации выбранной схемы.

Сравните самостоятельно три статуса: локальная правка в textarea, применение сервером в память и получение клиентом ack после записи. Между ними существуют реальные промежутки и варианты отказа. Понимание этих границ — результат урока. Далее серверный протокол начнёт различать читателя и редактора, чтобы наличие общей сохранённой копии не означало доступность изменения каждому вошедшему участнику.

Для передачи проекта запишите отдельно путь серверного файла и предел однопроцессности. Эти сведения помогут сопровождающему не включить storage в статический download и не направить два экземпляра приложения к одному временно заменяемому файлу. Наличие atomic rename не отменяет таких организационных условий. Именно условия использования делают границы подтверждения понятными.

Первоначальный шаблон также сохраняется до обслуживания комнаты, даже если первым вошёл только читатель. Иначе следующий запуск мог бы создать новые операции исходного текста и объединить их с прежним браузерным кешем. Ошибка записи ревизии переводит сервер в тот же отказ, что ошибка текущего состояния: неподтверждённую память нельзя продолжать выдавать за согласованную дисковую копию.

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

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