Caching CRUD in IndexedDB

Let's say we have a backend that can store some kind of entities. And it has an api for creating, reading, modifying and deleting these entities, abbreviated as CRUD. But the api is on the server, and the user got somewhere deep and half of the requests fall on timeout. I would not like to show an endless preloader and generally block user actions. Offline first assumes loading the application from the cache, so maybe the data should be taken from there?





It is suggested to store all data in IndexedDB (let's say that there are not very many of them), and, if possible, synchronize with the server. Several problems arise:





  1. If the Id of the entity is generated on the server, in the database, then how to live without the Id while the server is unavailable?





  2. When synchronizing with the server, how to distinguish entities created on the client from those deleted on the server by another user?





  3. How to resolve conflicts?





Identification

The identifier is needed, so we will create it ourselves. A GUID or `+ new Date ()` is fine for this, with some caveats. Only when a response comes from the server with the real Id, you need to replace it everywhere. If this newly created entity is already referenced by others, then these links also need to be corrected.





Synchronization

We will not reinvent the wheel, let's look at database replication. You can look at it endlessly, like a fire, but in short, one of the options looks like this: in addition to saving the entity in IndexedDB, we will write a log of changes: [time, 'update', Id = 3, Name = 'Ivan'], [time , 'create', Name = 'Ivan', Surname = 'Petrov'], [time, 'delete', Id = 3] ...





, . , , IndexedDB. Id.





- , , . , - , . - , , . , : , , , . Eventual Consistency.





, , . Operational Transformations (OT) Conflict-free Replicated Data Types (CRDT) . , CRDT : UpdatedAt . , .





, Id . , . , , . . , , Id , . - . . , . Last write win. Eventual Consistency: , . .





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: []};
}
      
      



There will be no implementation, because in each specific case there is a devil in the details, and there is, by and large, nothing to implement here - the generation of an identifier and writing to indexedDB.





Of course, CRDT or OT will be better, but if you need to do it quickly, but they are not allowed on the backend, then this work will do.








All Articles