Almacenamiento en caché de CRUD en IndexedDB

Digamos que tenemos un backend que puede almacenar algún tipo de entidades. Y tiene una api para crear, leer, modificar y borrar estas entidades, abreviada como CRUD. Pero la API está en el servidor y el usuario llegó a algún lugar profundo y la mitad de las solicitudes caen en el tiempo de espera. No me gustaría mostrar un precargador interminable y, en general, bloquear las acciones del usuario. Offline primero asume que se carga la aplicación desde la caché, por lo que tal vez los datos deberían tomarse de allí.





Se sugiere almacenar todos los datos en IndexedDB (digamos que no hay muchos) y, si es posible, sincronizar con el servidor. Surgen varios problemas:





  1. Si la identificación de la entidad se genera en el servidor, en la base de datos, ¿cómo vivir sin la identificación mientras el servidor no está disponible?





  2. Al sincronizar con el servidor, ¿cómo distinguir las entidades creadas en el cliente de las eliminadas en el servidor por otro usuario?





  3. ¿Cómo resolver conflictos?





Identificación

El identificador es necesario, así que lo crearemos nosotros mismos. Un GUID o `+ new Date ()` está bien para esto, con algunas advertencias. Solo cuando una respuesta proviene del servidor con la identificación real, debe reemplazarla en todas partes. Si esta entidad recién creada ya es referenciada por otros, entonces estos enlaces también deben corregirse.





Sincronización

No reinventaremos la rueda, veamos la replicación de la base de datos. Puedes mirarlo interminablemente, como un fuego, pero en resumen, una de las opciones se ve así: además de guardar la entidad en IndexedDB, escribiremos un registro de cambios: [time, 'update', Id = 3 , Nombre = 'Iván'], [hora, 'crear', Nombre = 'Iván', Apellido = 'Petrov'], [hora, 'eliminar', 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: []};
}
      
      



No habrá implementación, porque en cada caso específico hay un diablo en los detalles y, en general, no hay nada que implementar aquí: la generación de un identificador y la escritura en indexedDB.





Por supuesto, CRDT u OT serán mejores, pero si necesita hacerlo rápidamente, pero no están permitidos en el backend, entonces este trabajo funcionará.








All Articles