Кэширование Crud В Indexeddb

Допустим, у нас есть серверная часть, которая может хранить некоторые сущности.

И у него есть API для создания, чтения, изменения и удаления этих объектов, сокращенно CRUD. Но API находится на сервере, а пользователь ушёл куда-то глубоко и половина запросов истекает по таймауту.

Не хотелось бы показывать бесконечный прелоадер и вообще блокировать действия пользователя.

Оффлайн сначала предполагает загрузку приложения из кеша, так может быть данные можно взять оттуда? Все данные предлагается хранить в IndexedDB (предположим, что их не очень много) и по возможности синхронизировать с сервером.

Возникает несколько проблем:

  1. Если Id сущности генерируется на сервере, в базе данных, то как мы можем жить без Id, пока сервер недоступен?
  2. Как при синхронизации с сервером отличить сущности, созданные на клиенте, от удаленных на сервере другим пользователем?
  3. Как разрешать конфликты?


Идентификация

Нам нужен идентификатор, поэтому создадим его сами.

Для этого подойдет 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
Вместе с данным постом часто просматривают: