Локальная запись и границы синхронизации
В «Блокноте» пользователь пишет план урока, закрывает ноутбук и продолжает работу без доступного сервера. После восстановления соединения заметки должны появиться в другой копии приложения. Главная задача первой главы — определить, что именно означает сохранение и как объяснить человеку ещё не отправленный текст. Рассмотрим изменяемые прикладные записи, а не кеш готовых страниц.
Предполагаются основы JavaScript, HTML и HTTP. Полный конечный пример находится в архиве курса; автор подготовил файлы, но не запускал сервер, браузерные сценарии или тесты. Все описанные изменения интерфейса ожидаемые. Курс об офлайн-чтении уже рассматривает оболочку PWA и кеширование; здесь оболочка открывается с локального сервера, а самостоятельным предметом становится согласование пользовательских данных.
Два места хранения одной заметки
Начнём с заметки «План урока» и текста «Разобрать HTTP». В обычной серверной форме браузер отправляет запрос и только после ответа сообщает об успехе. При недоступной сети такая форма не сохраняет новую работу. В нашем приложении сначала завершается запись в IndexedDB, затем интерфейс показывает «Сохранено локально». Серверный запрос выполняется отдельно и не определяет возможность продолжать редактирование.
Локальное сохранение и получение серверного подтверждения дают разные гарантии. Первый результат означает, что следующая загрузка того же origin может прочитать запись из браузерной базы. Второй означает, что сервер принял определённое изменение и присвоил ему ревизию. Ни один результат сам по себе не доказывает, что другое устройство уже получило текст. Оно должно выполнить собственное получение изменений.
Обратите внимание на origin: схема, имя хоста и порт определяют границу браузерного хранилища. http://localhost:4174 и другой порт не разделяют одну IndexedDB автоматически. Поэтому адрес входит в условия лаборатории. Две вкладки одного профиля используют общую базу, а два независимых браузерных профиля получают отдельные локальные базы и могут изображать два устройства одного аккаунта.
Мы не называем IndexedDB встроенной службой синхронизации. Она предоставляет транзакции и сохранение объектов, но не выбирает сервер, не определяет владельца и не решает конфликт двух правок. Эти правила предстоит написать в приложении. Такая граница помогает понять ошибку: если заметка осталась только в одном браузере, причиной может быть очередь, сеть, авторизация или серверный контракт, а не отсутствие особой настройки IndexedDB.
Идентичность, версия и намерение
У каждой записи появится id, который остаётся постоянным после переименования. Заголовок не подходит на роль ключа: два урока могут иметь одинаковое название, а исправление опечатки не должно создавать другую заметку. Для новой записи клиент генерирует UUID. Отдельный opId позже обозначит конкретное намерение изменить эту запись; совпадение этих двух идентификаторов не требуется.
Серверная revision сообщает, от какого подтверждённого состояния началась правка. Новая заметка использует ноль, первый принятый вариант получает единицу. Локальная localVersion отсчитывает изменения в браузерной базе и помогает распознать редактирование во время запроса. Значения могут отличаться: три локальных сохранения ещё не означают три принятые сервером ревизии.
Ниже показана форма объекта после первого локального сохранения. Это схема данных, а не команда ручного заполнения всей базы.
const note = {
id: crypto.randomUUID(),
title: 'План урока', body: 'Разобрать HTTP',
deleted: false, revision: 0, localVersion: 1,
conflict: null
};
Если поле revision остаётся нулём, запись всё равно может быть полностью пригодна для локального чтения и редактирования. Однако подпись «согласована» была бы преждевременной. В конечном интерфейсе наличие операции в outbox даёт состояние «ожидает отправки». Так человек видит разницу между сохранённым текстом и доставкой, не разбираясь в числах внутреннего протокола.
Очередь хранит обязательство перед сервером
Outbox — локальное хранилище не подтверждённых операций. Когда пользователь сохраняет заметку, нужно одновременно записать и её текст, и обязательство отправки. Если сначала сохранить заметку, а очередь изменить второй независимой транзакцией, между действиями может закрыться вкладка. Интерфейс покажет текст, но механизм отправки больше не узнает, что его следует передать.
Обратный порядок также неудобен: очередь может содержать изменение, которое локальный список ещё не показывает. Поэтому два object store участвуют в одной readwrite транзакции. При успешном завершении существуют обе части; при отмене не считается успешной ни одна. Ожидаемый результат после перезагрузки — заметка видна, а её операция остаётся в очереди, если сервер ещё не ответил.
Очередь не равна журналу каждой клавиши. В выбранной модели хранится один неизменяемый снимок на заметку. Последующие локальные правки остаются в notes, а после подтверждения первого снимка создаётся следующая операция, если текст успел измениться. Это ограничивает очередь, сохраняя важное условие: уже отправленному идентификатору нельзя тихо назначить другое содержимое.
Проект не обещает мгновенной работы нескольких авторов в одном документе. Если два устройства изменили одну серверную ревизию, второе получит конфликт и покажет обе версии. Пользователь выберет серверный вариант или подготовит ручное объединение. Для совместного редактирования по символам потребовалась бы другая модель, поэтому алгоритмы CRDT в этот контракт не добавляются.
Наблюдаемая последовательность действий
Представьте будущий самостоятельный проход. Откройте Alice, создайте заметку и включите «Учебное отключение запросов». После сохранения текст остаётся в списке с подписью ожидания. Измените его на «Разобрать HTTP и кеширование». Отсутствие серверного ответа не блокирует форму: второе локальное сохранение меняет localVersion и сохраняет последнюю редакцию.
После восстановления запросов сервер может сначала принять прежний снимок. Это допустимый промежуточный результат, поскольку программа ещё хранит новую локальную редакцию. Подтверждение обновит известную revision и подготовит её отправку. Когда очередь опустеет и страница изменений будет получена, можно говорить о согласованном состоянии этой копии. Число секунд до такого результата в материале не измерялось.
Переключатель учебного отключения действует на новые обращения; запрос, который уже отправлен, может завершиться. Это полезное ограничение: настоящий обрыв сети также не позволяет клиенту надёжно определить, применил ли сервер последнее сообщение. Поэтому в будущих главах понадобятся стабильный opId и повтор того же запроса. Удалять очередь по одному факту отправки нельзя.
Сохранение в браузере также имеет пределы. Пользователь может очистить данные сайта, браузер может ограничить доступное место, а потеря компьютера уничтожит единственную локальную копию. Позднее добавим экспорт и объясним устойчивое хранение. Сейчас результатом является точный контракт: текст сохраняется локально, намерение доставки переживает закрытие вкладки, а подтверждённое серверное состояние обозначается отдельно.