nponiros/sync_server

View on GitHub
lib/sync/apply_client_changes.js

Summary

Maintainability
B
6 hrs
Test Coverage
'use strict';

/*
 * License:
 * The contents of this file were copied from
 * https://github.com/dfahlander/Dexie.js/blob/master/samples/remote-sync/websocket/WebSocketSyncServer.js
 * and are under the Apache 2 License.
 *
 * The code was modified to improve readability and to make it work with an asynchronous database.
 */
const reduceChanges = require('./reduce_changes');
const resolveConflicts = require('./resolve_conflicts');
const applyModifications = require('./apply_modifications');
const { CREATE, UPDATE, DELETE } = require('./types');

// Current revision will be incremented for each change
function applyClientChanges(db, baseRevision, currentRevision, clientChanges, clientIdentity) {
  function handleCreate(change) {
    return db.addData(change.table, change.key, change.obj)
      .then(() => {
        return db.addChangesData({
          rev: ++currentRevision.rev,
          source: clientIdentity,
          type: CREATE,
          table: change.table,
          key: change.key,
          obj: change.obj,
        });
      });
  }

  function handleUpdate(change) {
    return db.getData(change.table, change.key)
      .then((data) => {
        if (data) {
          applyModifications(data, change.mods);
          return db.updateData(change.table, change.key, data)
            .then(() => {
              return db.addChangesData({
                rev: ++currentRevision.rev,
                source: clientIdentity,
                type: UPDATE,
                table: change.table,
                key: change.key,
                mods: change.mods,
              });
            });
        }
        return Promise.resolve();
      });
  }

  function handleDelete(change) {
    return db.getData(change.table, change.key)
      .then((data) => {
        if (data) {
          return db.removeData(change.table, change.key)
            .then(() => {
              return db.addChangesData({
                rev: ++currentRevision.rev,
                source: clientIdentity,
                type: DELETE,
                table: change.table,
                key: change.key,
              });
            });
        }
        return Promise.resolve();
      });
  }

  const actions = {
    [CREATE]: handleCreate,
    [UPDATE]: handleUpdate,
    [DELETE]: handleDelete,
  };

  // ----------------------------------------------
  // HERE COMES THE QUITE IMPORTANT SYNC ALGORITHM!
  //
  // 1. Reduce all server changes (not client changes) that have occurred after given
  //    baseRevision (our changes) to a set (key/value object where key is the combination of table/primaryKey)
  // 2. Check all client changes against reduced server
  //    changes to detect conflict. Resolve conflicts:
  //      If server created an object with same key as client creates, updates or deletes: Always discard client change.
  //      If server deleted an object with same key as client creates, updates or deletes: Always discard client change.
  //      If server updated an object with same key as client updates:
  //        Apply all properties the client updates unless they conflict with server updates
  //      If server updated an object with same key as client creates:
  // Apply the client create but apply the server update on top
  //      If server updated an object with same key as client deletes: Let client win. Deletes always wins over Updates.
  //
  // 3. After resolving conflicts, apply client changes into server database.
  // 4. Send an ack to the client that we have persisted its changes
  // ----------------------------------------------

  return db.getChangesData(baseRevision)
    .then((serverChanges) => reduceChanges(serverChanges))
    .then((reducedServerChangeSet) => resolveConflicts(clientChanges, reducedServerChangeSet))
    .then((resolved) => {
      const promises = resolved
        .map((change) => actions[change.type](change));
      return Promise.all(promises);
    });
}

module.exports = applyClientChanges;