Permalink
Browse files

Initial import from NKO + add readme about API

  • Loading branch information...
0 parents commit 192a61d1cd8e05e5338363a35c38b6ea4b66f6ca @mixu committed Nov 16, 2012
Showing with 255 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +97 −0 index.js
  3. +34 −0 package.json
  4. +41 −0 readme.md
  5. +82 −0 test/test.js
@@ -0,0 +1 @@
+node_modules/
@@ -0,0 +1,97 @@
+// Params are containers,
+// which have a clock key { clock: {} }
+
+// increments the counter for nodeId
+exports.increment = function(o, nodeId) {
+ if(o.clock) {
+ o.clock[nodeId] = (typeof o.clock[nodeId] == 'undefined' ? 1 : o.clock[nodeId] + 1);
+ } else {
+ o[nodeId] = (typeof o[nodeId] == 'undefined' ? 1 : o[nodeId] + 1);
+ }
+ return o;
+};
+
+function allKeys(a, b){
+ var last = null;
+ return Object.keys(a)
+ .concat(Object.keys(b))
+ .sort()
+ .filter(function(item) {
+ // to make a set of sorted keys unique, just check that consecutive keys are different
+ var isDuplicate = (item == last);
+ last = item;
+ return !isDuplicate;
+ });
+}
+
+// like a regular sort function, returns:
+// if a < b: -1
+// if a == b: 0
+// if a > b: 1
+// E.g. if used to sort an array of keys, will order them in ascending order (1, 2, 3 ..)
+exports.ascSort = exports.compare = function(a, b) {
+ var isGreater = false,
+ isLess = false;
+
+ // allow this function to be called with objects that contain clocks, or the clocks themselves
+ if(a.clock) a = a.clock;
+ if(b.clock) b = b.clock;
+
+ allKeys(a, b).forEach(function(key) {
+ var diff = (a[key] || 0) - (b[key] || 0);
+ if(diff > 0) isGreater = true;
+ if(diff < 0) isLess = true;
+ });
+
+ if(isGreater && isLess) return 0;
+ if(isLess) return -1;
+ if(isGreater) return 1;
+ return 0; // neither is set, so equal
+};
+
+// sort in descending order (N, ... 3, 2, 1)
+exports.descSort = function(a, b) {
+ return 0 - exports.ascSort(a, b);
+};
+
+// equal, or not less and not greater than
+exports.isConcurrent = function(a, b) {
+ return !!(exports.compare(a, b) == 0);
+};
+
+// identical
+exports.isIdentical = function(a, b) {
+ // allow this function to be called with objects that contain clocks, or the clocks themselves
+ if(a.clock) a = a.clock;
+ if(b.clock) b = b.clock;
+
+ return allKeys(a, b).every(function(key) {
+ if(typeof a[key] == 'undefined' || typeof b[key] == 'undefined') return false;
+ var diff = a[key] - b[key];
+ if(diff != 0) return false;
+ return true;
+ });
+};
+
+
+// given two vector clocks, returns a new vector clock with all values greater than
+// those of the merged clocks
+exports.merge = function(a, b) {
+ var last = null, result = {}, wantContainer = false;
+ // allow this function to be called with objects that contain clocks, or the clocks themselves
+ if(a.clock && b.clock) wantContainer = true;
+ if(a.clock) a = a.clock;
+ if(b.clock) b = b.clock;
+
+ allKeys(a, b).forEach(function(key) {
+ result[key] = Math.max(a[key] || 0, b[key] || 0);
+ });
+ if(wantContainer) {
+ return { clock: result };
+ }
+ return result;
+};
+
+exports.GT = 1;
+exports.LT = -1;
+exports.CONCURRENT = 0;
@@ -0,0 +1,34 @@
+{
+ "name": "vectorclock",
+ "version": "0.0.0",
+ "description": "A simple implementation of vector clocks in Javascript.",
+ "main": "index.js",
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "test": "./node_modules/.bin/mocha --ui exports --reporter spec --slow 2000ms --bail test/test.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mixu/vectorclock.git"
+ },
+ "keywords": [
+ "vector",
+ "clock",
+ "vector",
+ "logical",
+ "clock",
+ "time",
+ "version",
+ "versioning",
+ "conflict",
+ "compare",
+ "distributed"
+ ],
+ "author": "Mikito Takada <mikito.takada@gmail.com>",
+ "license": "BSD",
+ "devDependencies": {
+ "mocha": "~1.7.0"
+ }
+}
@@ -0,0 +1,41 @@
+# vectorclock
+
+A simple implementation of vector clocks in Javascript.
+
+## API
+
+[Vector clocks](http://en.wikipedia.org/wiki/Vector_clock) are represented as plain old objects with a "clock" key (which is a hash). For example: `{ clock: { a: 1, b: 2 } }`.
+
+Recommended reading:
+
+- Leslie Lamport (1978). "[Time, clocks, and the ordering of events in a distributed system](https://www.google.com/search?q=Time%2C+clocks%2C+and+the+ordering+of+events+in+a+distributed+system)". Communications of the ACM 21 (7): 558-565.
+- Friedemann Mattern (1988). "[Virtual Time and Global States of Distributed Systems](https://www.google.com/search?q=Virtual%20Time%20and%20Global%20States%20of%20Distributed%20Systems)". Workshop on Parallel and Distributed Algorithms: pp. 215-226
+- Colin Fidge (1988), "[Timestamps in Message-Passing Systems That Preserve the Partial Ordering](https://www.google.com/search?q=Timestamps+in+Message-Passing+Systems+That+Preserve+the+Partial+Ordering)".
+
+## API
+
+- `increment(clock, nodeId)`: increment a vector clock at "nodeId"
+- `merge(a, b)`: given two vector clocks, returns a new vector clock with all values greater than those of the merged clocks
+- `compare(a, b)` / `ascSort(a, b)`: compare two vector clocks, returns -1 for a < b and 1 for a > b; 0 for concurrent and identical values. Can be used to sort an array of objects by their "clock" key via [].sort(VClock.ascSort)
+- `descSort(a, b)`: sorts in descending order (N, ... 3, 2, 1)
+- `isConcurrent(a, b)`: if A and B are equal, or if they occurred concurrently.
+- `isIdentical(a, b)`: if every value in both vector clocks is equal.
+
+## Implementing read repair using vector clocks
+
+Here is one way to implement read repair by detecting which clocks are concurrent, and if necessary, returning multiple values:
+
+ var responses = [ { clock: ... }, { clock: ... }];
+ // sort the responses by the vector clocks
+ responses.sort(VClock.descSort);
+ // then compare them to the topmost
+ // (in sequential order, the greatest) item
+ var repaired = [ responses.shift() ];
+ responses.forEach(function(item, index) {
+ // if they are concurrent with that item, then there is a conflict
+ // that we cannot resolve, so we need to return the item.
+ if(VClock.isConcurrent(item, repaired[0]) &&
+ !VClock.isIdentical(item, repaired[0])) {
+ repaired.push(item);
+ }
+ });
@@ -0,0 +1,82 @@
+var assert = require('assert'),
+ vclock = require('../index.js');
+
+exports['given two clocks'] = {
+
+ beforeEach: function() {
+ this.a = { clock: {}};
+ this.b = { clock: {}};
+ },
+
+ 'at the same node': {
+ 'an empty vector clock should be identical to another empty vector clock': function() {
+ assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT);
+ assert.equal( vclock.compare(this.b, this.a), vclock.CONCURRENT);
+ assert.equal( vclock.isIdentical(this.a, this.b), true);
+ },
+
+ 'a clock incremented once should be greater than 0': function(){
+ vclock.increment(this.a, 'node-1');
+ assert.equal( vclock.compare(this.a, this.b), vclock.GT);
+ assert.equal( vclock.compare(this.b, this.a), vclock.LT);
+ assert.ok( !vclock.isIdentical(this.a, this.b));
+ },
+
+ 'a clock incremented twice should be greater than 1': function() {
+ vclock.increment(this.a, 'node-1');
+ vclock.increment(this.a, 'node-1');
+ vclock.increment(this.b, 'node-1');
+ assert.equal( vclock.compare(this.a, this.b), vclock.GT);
+ assert.equal( vclock.compare(this.b, this.a), vclock.LT);
+ assert.ok( !vclock.isIdentical(this.a, this.b));
+ },
+
+ 'two clocks with the same history should be equal and concurrent': function() {
+ vclock.increment(this.a, 'node-1');
+ vclock.increment(this.b, 'node-1');
+ assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT);
+ assert.equal( vclock.compare(this.b, this.a), vclock.CONCURRENT);
+ assert.ok( vclock.isIdentical(this.a, this.b));
+ }
+ },
+
+ 'at different nodes': {
+
+ beforeEach: function() {
+ vclock.increment(this.a, 'node-1');
+ vclock.increment(this.b, 'node-1');
+ vclock.increment(this.a, 'node-1');
+ vclock.increment(this.b, 'node-2');
+ },
+
+ 'clocks incremented at different nodes should be concurrent but not equal': function() {
+ assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT);
+ assert.equal( vclock.compare(this.b, this.a), vclock.CONCURRENT);
+ assert.ok( !vclock.isIdentical(this.a, this.b));
+ vclock.increment(this.a, 'node-1');
+ assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT);
+ assert.ok( !vclock.isIdentical(this.a, this.b));
+ vclock.increment(this.b, 'node-2');
+ vclock.increment(this.b, 'node-2');
+ vclock.increment(this.b, 'node-2');
+ assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT);
+ assert.ok( !vclock.isIdentical(this.a, this.b));
+ },
+
+ 'a merged clock should be greater than either of the clocks': function() {
+ var newClock = vclock.merge(this.a, this.b);
+ assert.equal( vclock.compare(newClock, this.b), vclock.GT);
+ assert.equal( vclock.compare(newClock, this.a), vclock.GT);
+
+ }
+
+ }
+
+};
+
+// if this module is the script being run, then run the tests:
+if (module == require.main) {
+ var mocha = require('child_process').spawn('mocha', [ '--colors', '--ui', 'exports', '--reporter', 'spec', __filename ]);
+ mocha.stdout.pipe(process.stdout);
+ mocha.stderr.pipe(process.stderr);
+}

0 comments on commit 192a61d

Please sign in to comment.