histo-revisions

peer-to-peer synchronizable database written in javascript

#histo-revisions

A database that tracks changing revisions of data.

Each revision is accessible through a unique ref which is based on a cryptographic hash of the data's content and history. A ref is similar to a git commit hash.

Each written data object is combined with a link to its ancestors - the combination is called a revision. A revision can be compared to a git commit object.

Revisions look like this:

{
  ancestors: ['someref'],
  data: 'some data'
}

The cryptographic hash of the revision is its ref.

##Documentation

###require('histo-revisions') -> revisions

###revisions.createDB(opts) -> db opts is expected to be:

{
  name: 'your-db-name', // will generate uuid if omitted 
  revisionStore: revStore,
  branchStore: branchStore
}

With revStore being an object with a content-addressable store interface:

  • put(data, cb): should write some data and responds with a unique identifier of the data in the callback
  • get(identifier, cb): should respond with the data in the callback
  • del(identifier, cb): should delete the data for the specified identifier

branchStore is expected to be a simple key-value store interface:

  • put(key, value, cb)
  • get(key, cb)
  • del(key, cb)

###db.put(data, [ancestorRefs], cb) Writes some data to the db. If `ancestorRefs is not specified the current head is used as the ancestor. On success a ref to the written data is passed to the callback.

###db.get([ref], cb) Reads the data for a given ref. If ref is not specified the current head is used.

###db.head(cb) -> ref The ref of the current head of the store is passed to the callback. The head changes every time new data is written or the head was explicitly set through setHead.

###db.setHead(ref, [previousHead], cb) Explicitly sets the head to a known ref. You can pass in the previous head if you want to use optimistic locking. If the head has been updated in the meantime, the function will callback with an error.

###db.remoteHead(remoteName, cb) Responds with the head ref of a remote database that is known to db.

###db.setRemoteHead(remoteName, ref, cb) Updates the remote head of a remote database. This function is usually not called directly but by a synchronizer.

###db.refDifference(fromRef, toRef, cb) Responds with the list of refs that is required to get from ref1 to ref2.

The difference is computed using the graph-difference module.

###db.commonAncestor(ref1, ref2, cb) Responds with the common ancestor ref of two refs.

The common ancestor is computed using the ancestor module.

###db.ancestors(ref, cb) Responds with the ancestors of ref.

###db.createStream(refs) -> stream Returns a simple-stream source for reading the revisions for a given list of refs.

###db.writeStream(stream, cb) Writes the stream source of revisions to the database.

###revisions.createSynchronizer(sourceDB, targetDB) -> synchronizer sourceDB is expected to be an object with the subset of db functions and properties:

  • name
  • head(cb)
  • refDifference(fromRef, toRef, cb)
  • createStream(refs) -> stream

targetDB requires the following set of functions:

  • head(cb)
  • remoteHead(remoteName, cb)
  • writeStream(stream, cb)
  • setRemoteHead(remoteName, ref, cb)

###synchronizer.run(cb) On success the synchronizer will have written all new revisions to targetDB which is determined by the ref difference from sourceDB to targetDB.

###Merging synchronized data Synchronizing two databases does not update the target databases' head. It will only write all revisions to the target. To merge the data you need to read the remote head of the source database, get the corresponding data, merge the data and write a new revision:

function readRemoteRev(dbremoteNamecb) {
  db.remoteHead(remoteName, function(errremoteHead) {
    db.get(remoteHead, function(errremoteData) {
      cb(null, {head: remoteHead, data: remoteData});
    });
  });
}
 
function readLocalRev(dbcb) {
  db.head(function(errlocalHead) {
    db.get(localHead, function(localData) {
      cb(null, {head: localHead, data: remoteData});
    });
  });
}
 
readRemoteRev(targetDB, 'your-remote-name', function(errremoteRev) {
  readLocalRev(targetDB, function(errlocalRev) {
    var mergedData = yourMergeFunction(remoteRev.data, localRev.data);
    var ancestors = [remoteRev.head, localRev.head];
    targetDB.put(mergedData, ancestors, function(errmergedRef) {
      targetDB.setHead(mergedRef, localRev.head, function(err) {
        // on err repeat merging with new local head 
      });
    });
  });
});

##Todo

  • list of remotes
  • garbage collect history

##Contributors This project was created by Mirko Kiefer (@mirkokiefer).