Допустим, у нас есть серверная часть, которая может хранить некоторые сущности.
И у него есть API для создания, чтения, изменения и удаления этих объектов, сокращенно CRUD. Но API находится на сервере, а пользователь ушёл куда-то глубоко и половина запросов истекает по таймауту.
Не хотелось бы показывать бесконечный прелоадер и вообще блокировать действия пользователя.
Оффлайн сначала предполагает загрузку приложения из кеша, так может быть данные можно взять оттуда? Все данные предлагается хранить в IndexedDB (предположим, что их не очень много) и по возможности синхронизировать с сервером.
Возникает несколько проблем:
- Если Id сущности генерируется на сервере, в базе данных, то как мы можем жить без Id, пока сервер недоступен?
- Как при синхронизации с сервером отличить сущности, созданные на клиенте, от удаленных на сервере другим пользователем?
- Как разрешать конфликты?
Идентификация
Нам нужен идентификатор, поэтому создадим его сами.Для этого подойдет GUID или `+new Date()`, но с некоторыми оговорками.
Только когда придет ответ от сервера с реальным Id, нужно его везде заменить.
Если на эту вновь созданную сущность уже ссылаются другие, то эти ссылки также необходимо исправить.
Синхронизация
Давайте не будем изобретать велосипед, а посмотрим на репликацию базы данных.На это можно смотреть бесконечно, как на огонь, но вкратце один из вариантов выглядит так: помимо сохранения сущности в IndexedDB, мы напишем лог изменений: [time, 'update', Id=3, Имя='Иван'], [время , 'создать', Имя='Иван', Фамилия='Петров'], [время, 'удалить', Id=3].
При получении свежих данных с сервера мы можем сравнить их с существующими данными и посчитать аналогичный лог изменений на сервере.
После этого слейте логи и отправьте необходимые изменения на сервер, если интернет еще не пропал, и только после этого запишите изменения в IndexedDB. И не забудьте обновить свой идентификатор.
Конфликты
Конфликт – это не спор двух пользователей, чья точка зрения верна, и поочередно исправляющих один и тот же пост до посинения.А вот ситуация, когда пользователи довольны и каждый видит свою версию — это конфликт, а конкретно несогласованность.
Непрерывной согласованности в веб-приложениях добиться несложно — при каждом изменении блокируйте всех клиентов, затронутых этим изменением, до тех пор, пока все они не подтвердят получение.
Это никому не нравится, поэтому приходится идти на компромисс: ладно, пусть иногда пользователи видят разное, но если все зависнут и перестанут вносить изменения, то через некоторое время у всех будет одно и то же.
Для этой цели был придуман термин «возможная согласованность».
Оказалось, что этого можно добиться незаметно для пользователя, но не так-то просто.
Может быть использован Операционные преобразования (ОТ) или Бесконфликтные реплицируемые типы данных (CRDT), но для них вам придется довольно радикально изменить формат обмена данными с сервером.
Если такой возможности нет, то можно на коленках сделать минимальный CRDT: добавить в сущность поле UpdatedAt и записать в него время последнего изменения.
Это не избавит от всех конфликтов, но на порядок уменьшит их количество.
Итак, при объединении двух журналов мы группируем их по идентификатору сущности и далее работаем с каждой группой отдельно.
Если в одном из логов есть операция удаления, то оставляем только его.
У пользователя, который удалил запись, вероятно, были веские причины для этого, и он не хотел бы, чтобы запись внезапно возобновилась.
Никто не любит зомби, кроме зомби.
Если в одном из логов есть операция создания сущности, то другой лог должен быть пустым, потому что Id уникальный, ага.
С изменениями немного сложнее — нужно посмотреть время последнего изменения сущности в каждом из журналов.
Сравнивать.
И выберите журнал, в котором изменение пришло позже.
Победа последней записи.
Давайте проверим окончательную согласованность: если все пользователи перестанут вносить изменения и подключатся к Интернету, у всех будет последняя версия сущностей.
Большой.
function mergeLogs(left, right){ const ids = new Set([ .
left.map(x => x.id), .
right.map(x => x.id) ]); return [.
ids].
map(id => mergeIdLogs( left.filter(x => x.id == id), right.filter(x => x.id ==id) )).
reduce((a,b) => ({ left: [.
a.left, .
b.left], right: [.
a.right, .
b.right] }), {left: [], right: []}); } function mergeIdLogs(left,right){ const isWin = log => log.some(x => ['create','delete'].
includes(x.type)); const getMaxUpdate = log => Math.max(.
log.map(x => +x.updatedAt)); if (isWin(left)) return {left: [], right: left}; if (isWin(right)) return {left: right, right: []}; if (getMaxUpdate(left) > getMaxUpdate(right)) return {left: [], right: left}; else return {left: right, right: []}; }
Эпилог
Никакой реализации не будет, потому что в каждом конкретном случае дьявол кроется в деталях, и по большому счету реализовывать здесь нечего — генерация идентификатора и запись в indexedDB. Конечно, лучше будет CRDT или OT, но если нужно сделать быстро, а в бэкенд не пускают, то подойдет и этот. Теги: #разработка сайтов #кэширование #crud #indexeddb #offline-first-
Восстановите Реестр Windows Простым Способом
19 Oct, 24 -
Электронная Книга Будущего
19 Oct, 24 -
Бенк В2410
19 Oct, 24