├── .gitignore ├── index.js ├── package.json ├── readme.md └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Params are containers, 2 | // which have a clock key { clock: {} } 3 | 4 | // increments the counter for nodeId 5 | exports.increment = function(o, nodeId) { 6 | if(o.clock) { 7 | o.clock[nodeId] = (typeof o.clock[nodeId] == 'undefined' ? 1 : o.clock[nodeId] + 1); 8 | } else { 9 | o[nodeId] = (typeof o[nodeId] == 'undefined' ? 1 : o[nodeId] + 1); 10 | } 11 | return o; 12 | }; 13 | 14 | function allKeys(a, b){ 15 | var last = null; 16 | return Object.keys(a) 17 | .concat(Object.keys(b)) 18 | .sort() 19 | .filter(function(item) { 20 | // to make a set of sorted keys unique, just check that consecutive keys are different 21 | var isDuplicate = (item == last); 22 | last = item; 23 | return !isDuplicate; 24 | }); 25 | } 26 | 27 | // like a regular sort function, returns: 28 | // if a < b: -1 29 | // if a == b: 0 30 | // if a > b: 1 31 | // E.g. if used to sort an array of keys, will order them in ascending order (1, 2, 3 ..) 32 | exports.ascSort = exports.compare = function(a, b) { 33 | var isGreater = false, 34 | isLess = false; 35 | 36 | // allow this function to be called with objects that contain clocks, or the clocks themselves 37 | if(a.clock) a = a.clock; 38 | if(b.clock) b = b.clock; 39 | 40 | allKeys(a, b).forEach(function(key) { 41 | var diff = (a[key] || 0) - (b[key] || 0); 42 | if(diff > 0) isGreater = true; 43 | if(diff < 0) isLess = true; 44 | }); 45 | 46 | if(isGreater && isLess) return 0; 47 | if(isLess) return -1; 48 | if(isGreater) return 1; 49 | return 0; // neither is set, so equal 50 | }; 51 | 52 | // sort in descending order (N, ... 3, 2, 1) 53 | exports.descSort = function(a, b) { 54 | return 0 - exports.ascSort(a, b); 55 | }; 56 | 57 | // equal, or not less and not greater than 58 | exports.isConcurrent = function(a, b) { 59 | return !!(exports.compare(a, b) == 0); 60 | }; 61 | 62 | // identical 63 | exports.isIdentical = function(a, b) { 64 | // allow this function to be called with objects that contain clocks, or the clocks themselves 65 | if(a.clock) a = a.clock; 66 | if(b.clock) b = b.clock; 67 | 68 | return allKeys(a, b).every(function(key) { 69 | if(typeof a[key] == 'undefined' || typeof b[key] == 'undefined') return false; 70 | var diff = a[key] - b[key]; 71 | if(diff != 0) return false; 72 | return true; 73 | }); 74 | }; 75 | 76 | 77 | // given two vector clocks, returns a new vector clock with all values greater than 78 | // those of the merged clocks 79 | exports.merge = function(a, b) { 80 | var last = null, result = {}, wantContainer = false; 81 | // allow this function to be called with objects that contain clocks, or the clocks themselves 82 | if(a.clock && b.clock) wantContainer = true; 83 | if(a.clock) a = a.clock; 84 | if(b.clock) b = b.clock; 85 | 86 | allKeys(a, b).forEach(function(key) { 87 | result[key] = Math.max(a[key] || 0, b[key] || 0); 88 | }); 89 | if(wantContainer) { 90 | return { clock: result }; 91 | } 92 | return result; 93 | }; 94 | 95 | exports.GT = 1; 96 | exports.LT = -1; 97 | exports.CONCURRENT = 0; 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vectorclock", 3 | "version": "0.0.0", 4 | "description": "A simple implementation of vector clocks in Javascript.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "./node_modules/.bin/mocha --ui exports --reporter spec --slow 2000ms --bail test/test.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mixu/vectorclock.git" 15 | }, 16 | "keywords": [ 17 | "vector", 18 | "clock", 19 | "vector", 20 | "logical", 21 | "clock", 22 | "time", 23 | "version", 24 | "versioning", 25 | "conflict", 26 | "compare", 27 | "distributed" 28 | ], 29 | "author": "Mikito Takada ", 30 | "license": "BSD", 31 | "devDependencies": { 32 | "mocha": "~1.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # vectorclock 2 | 3 | A simple implementation of vector clocks in Javascript. 4 | 5 | ## API 6 | 7 | [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 } }`. 8 | 9 | Recommended reading: 10 | 11 | - 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. 12 | - 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 13 | - 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)". 14 | 15 | ## API 16 | 17 | - `increment(clock, nodeId)`: increment a vector clock at "nodeId" 18 | - `merge(a, b)`: given two vector clocks, returns a new vector clock with all values greater than those of the merged clocks 19 | - `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) 20 | - `descSort(a, b)`: sorts in descending order (N, ... 3, 2, 1) 21 | - `isConcurrent(a, b)`: if A and B are equal, or if they occurred concurrently. 22 | - `isIdentical(a, b)`: if every value in both vector clocks is equal. 23 | 24 | ## Implementing read repair using vector clocks 25 | 26 | Here is one way to implement read repair by detecting which clocks are concurrent, and if necessary, returning multiple values: 27 | 28 | var responses = [ { clock: ... }, { clock: ... }]; 29 | // sort the responses by the vector clocks 30 | responses.sort(VClock.descSort); 31 | // then compare them to the topmost 32 | // (in sequential order, the greatest) item 33 | var repaired = [ responses.shift() ]; 34 | responses.forEach(function(item, index) { 35 | // if they are concurrent with that item, then there is a conflict 36 | // that we cannot resolve, so we need to return the item. 37 | if(VClock.isConcurrent(item, repaired[0]) && 38 | !VClock.isIdentical(item, repaired[0])) { 39 | repaired.push(item); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | vclock = require('../index.js'); 3 | 4 | exports['given two clocks'] = { 5 | 6 | beforeEach: function() { 7 | this.a = { clock: {}}; 8 | this.b = { clock: {}}; 9 | }, 10 | 11 | 'at the same node': { 12 | 'an empty vector clock should be identical to another empty vector clock': function() { 13 | assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT); 14 | assert.equal( vclock.compare(this.b, this.a), vclock.CONCURRENT); 15 | assert.equal( vclock.isIdentical(this.a, this.b), true); 16 | }, 17 | 18 | 'a clock incremented once should be greater than 0': function(){ 19 | vclock.increment(this.a, 'node-1'); 20 | assert.equal( vclock.compare(this.a, this.b), vclock.GT); 21 | assert.equal( vclock.compare(this.b, this.a), vclock.LT); 22 | assert.ok( !vclock.isIdentical(this.a, this.b)); 23 | }, 24 | 25 | 'a clock incremented twice should be greater than 1': function() { 26 | vclock.increment(this.a, 'node-1'); 27 | vclock.increment(this.a, 'node-1'); 28 | vclock.increment(this.b, 'node-1'); 29 | assert.equal( vclock.compare(this.a, this.b), vclock.GT); 30 | assert.equal( vclock.compare(this.b, this.a), vclock.LT); 31 | assert.ok( !vclock.isIdentical(this.a, this.b)); 32 | }, 33 | 34 | 'two clocks with the same history should be equal and concurrent': function() { 35 | vclock.increment(this.a, 'node-1'); 36 | vclock.increment(this.b, 'node-1'); 37 | assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT); 38 | assert.equal( vclock.compare(this.b, this.a), vclock.CONCURRENT); 39 | assert.ok( vclock.isIdentical(this.a, this.b)); 40 | } 41 | }, 42 | 43 | 'at different nodes': { 44 | 45 | beforeEach: function() { 46 | vclock.increment(this.a, 'node-1'); 47 | vclock.increment(this.b, 'node-1'); 48 | vclock.increment(this.a, 'node-1'); 49 | vclock.increment(this.b, 'node-2'); 50 | }, 51 | 52 | 'clocks incremented at different nodes should be concurrent but not equal': function() { 53 | assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT); 54 | assert.equal( vclock.compare(this.b, this.a), vclock.CONCURRENT); 55 | assert.ok( !vclock.isIdentical(this.a, this.b)); 56 | vclock.increment(this.a, 'node-1'); 57 | assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT); 58 | assert.ok( !vclock.isIdentical(this.a, this.b)); 59 | vclock.increment(this.b, 'node-2'); 60 | vclock.increment(this.b, 'node-2'); 61 | vclock.increment(this.b, 'node-2'); 62 | assert.equal( vclock.compare(this.a, this.b), vclock.CONCURRENT); 63 | assert.ok( !vclock.isIdentical(this.a, this.b)); 64 | }, 65 | 66 | 'a merged clock should be greater than either of the clocks': function() { 67 | var newClock = vclock.merge(this.a, this.b); 68 | assert.equal( vclock.compare(newClock, this.b), vclock.GT); 69 | assert.equal( vclock.compare(newClock, this.a), vclock.GT); 70 | 71 | } 72 | 73 | } 74 | 75 | }; 76 | 77 | // if this module is the script being run, then run the tests: 78 | if (module == require.main) { 79 | var mocha = require('child_process').spawn('mocha', [ '--colors', '--ui', 'exports', '--reporter', 'spec', __filename ]); 80 | mocha.stdout.pipe(process.stdout); 81 | mocha.stderr.pipe(process.stderr); 82 | } 83 | --------------------------------------------------------------------------------