Офлайн-черновик в IndexedDB
Reconnect из предыдущего урока сохраняет открытый документ при временной потере сети. Однако перезагрузка уничтожает JavaScript-объекты страницы. Чтобы вернуть локальный черновик после нового открытия, добавим IndexedDB-адаптер Yjs. Он хранит структуру изменений в браузере и восстанавливает её в новый Y.Doc. В этом уроке отделим готовность локального хранилища от готовности сервера и расположим загрузку в правильном порядке.
Состояние lesson-08 использует y-indexeddb 9.0.12 вместе с прежней веткой Yjs 13. Ключ кеша содержит комнату и введённого участника. Такое разделение уменьшает случайное смешивание черновиков Анны и Бориса в одном профиле, но само по себе не шифрует данные и не служит проверкой прав. Права по-прежнему определяет серверная сессия.
Храним структуру, а не только строку
Адаптер подписывается на изменения документа и сохраняет необходимую двоичную историю. После открытия он загружает известные updates в новую реплику. Это отличается от сохранения textarea.value в localStorage: восстановленная строка не содержит ту же идентичность операций. Для дальнейшего CRDT-слияния важно вернуть общую структуру, а не заново вставить визуально похожий текст.
Браузерная база принадлежит происхождению сайта и профилю браузера. Удаление данных сайта, приватный режим, квота или политика очистки способны сделать кеш недоступным. Поэтому наличие IndexedDB не означает гарантированного вечного хранения. Если пользователь завершает работу, приложение должно знать, какие правки уже подтверждены сервером, какие остаются локально и какой дополнительный экспорт доступен при необходимости.
Ниже показан точный фрагмент начала openEditor в src/app.js. Документ создан до адаптера. Ожидание whenSynced относится к загрузке имеющегося локального состояния, а последующее создание provider — к сетевому обмену. Полный файл в архиве включает прежнюю привязку textarea и функцию освобождения ресурсов.
const doc=new Y.Doc(), text=doc.getText('markdown');
const persistence=new IndexeddbPersistence(`draft-42:${user}`,doc);
await persistence.whenSynced;
Момент локальной готовности
Пока идёт начальная загрузка, поле оставлено запрещённым для ввода. Иначе пользователь мог бы начать редактировать пустую строку, после чего поздно пришедшая локальная история изменила бы видимый документ. В простой учебной привязке такой переход сложно объяснить пользователю. Мы сначала восстанавливаем известную реплику, затем разрешаем редактирование и подключаем транспорт.
Ожидание whenSynced не означает «всё синхронизировано с комнатой». Имя события относится к адаптеру IndexedDB. После него сервер ещё может иметь неизвестные операции. Поэтому строка интерфейса должна отдельно описывать локальную загрузку и сетевое согласование. Одно слово «Сохранено» слишком неопределённо: неясно, в браузере ли находится запись, на сервере ли она принята и переживёт ли перезапуск.
После самостоятельной проверки перезагрузка страницы в том же профиле должна восстановить известный локальный документ из IndexedDB, затем provider должен согласовать его с сервером. Такая последовательность следует из листинга, но при подготовке примеры не запускались. Поведение при отказе базы, очистке профиля и заполненной квоте остаётся задачей ручного наблюдения на конкретном браузере.
Имя базы и пользователь
Ключ draft-42:anna удобен для нашего ограниченного набора данных. Но браузерный код не должен считать его секретом. Человек с доступом к тому же профилю может исследовать данные сайта, а скрипт того же происхождения обладает доступом к хранилищу в рамках браузерной модели. При работе с чувствительными документами требуется отдельное решение о локальном кешировании, сроках очистки и допустимых устройствах.
Также важно определить поведение выхода из аккаунта. destroy адаптера снимает соединение и подписки, но не равнозначен удалению содержимого базы. Если продукт обещает «удалить локальную копию», нужна явная операция очистки с понятной областью действия и обработкой ошибок. Нельзя просто уничтожить объект и сообщить пользователю, что данные исчезли с диска.
В серии мы сохраняем кеш между открытиями, поскольку учим восстановлению черновика. Финальная поставка должна прямо описывать это поведение. Если на общем устройстве Борис выходит, а Анна входит, каждое имя базы получает свою копию. Но серверная проверка доступа всё равно нужна на каждом сетевом соединении: локальный namespace не может предоставить право читать новую комнату.
Восстановление и стартовый шаблон
Когда локальная база уже содержит данные, нельзя снова вставлять начальный план из браузера. Сервер остаётся назначенным источником первоначального содержимого. Клиент сначала восстанавливает известные операции, затем сообщает вектор серверу. Даже если видимый текст временно пуст, это не достаточное доказательство, что документ никогда не существовал: удалённый текст тоже может иметь внутреннюю историю.
Если понадобится миграция схемы, имя базы и версия данных требуют отдельного решения. Замена ключа создаст новую локальную копию, но не перенесёт автоматически старые правки. Сохранять обе базы навсегда тоже неудобно. Для реального продукта задайте поддерживаемые схемы, процедуру экспорта старого состояния и поведение клиента, который открыл документ после долгого перерыва.
Отказы и статус сохранения
Кеш помогает при автономной работе, но пользовательское действие может произойти непосредственно перед закрытием страницы. Асинхронное хранилище и жизненный цикл вкладки не дают оснований обещать, что любое последнее нажатие всегда завершило запись. Учебный пример не выводит покадровое подтверждение каждой операции IndexedDB. Такое обещание потребовало бы более точного контракта состояния записи и обработки отказов адаптера.
При ошибке загрузки не надо молча подменять документ новым пустым объектом. Это создаёт впечатление, что черновика не было. Лучше оставить ввод закрытым, показать причину и предложить понятное действие: повторить загрузку, экспортировать доступное состояние или очистить повреждённую копию после осознанного выбора. Код учебного состояния показывает успешную ветвь, а расширенный обработчик отказов остаётся требованием выпуска.
Для самостоятельного разбора сравните два сценария: сервер недоступен после уже успешного входа и весь профиль браузера очищен. В первом случае существующая вкладка может продолжать работу со своей репликой; во втором локальной истории больше нет. Кроме того, первоначальный вход в нашем примере требует локального сервера, поэтому приложение ещё не является полностью автономной оболочкой. Следующий урок добавит серверное сохранение и подтверждение записи, которое дополняет браузерный кеш.
Оглавление серии · Исходные состояния проекта.
API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.