├── .gitignore ├── Makefile ├── README.md ├── lib ├── consts.js ├── lineage.js ├── protocol.js └── types │ ├── clock.js │ ├── date.js │ ├── length_annealing_clock.js │ └── number.js ├── package.json └── test └── types ├── clock.js ├── date.js ├── length_annealing_clock.js └── number.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./node_modules/.bin/mocha $(TESTS) 3 | 4 | TESTS := $(shell find test/ -name '*.js') 5 | 6 | .PHONY: all test clean 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lineage 2 | ======= 3 | 4 | A data versioning library. 5 | 6 | ## Introduction 7 | 8 | Lineage is a JavaScript library for managing versions of data, especially 9 | helpful when reasoning about state in distributed systems. It provides several 10 | data types that can act as versions. Overall, lineage offers: 11 | 12 | - Defines a [protocol](https://github.com/codeparty/protocoljs) for 13 | incrementing, comparing, and merging versions. 14 | - Provides several data types that can act as versions. 15 | - Enables you to add your own data types that can act as versions. 16 | 17 | ## Installation 18 | 19 | ``` 20 | $ npm install lineage 21 | ``` 22 | 23 | ## Basics 24 | 25 | The most basic version data type you can use is a Number or Date. 26 | 27 | ## Numbers as versions 28 | 29 | ```javascript 30 | var lineage = require('lineage') 31 | , protocol = lineage.protocol; 32 | 33 | // Require this to extend Number with the version protocol provided by lineage. 34 | require('lineage/lib/types/number'); 35 | 36 | var oldVersion = 1; 37 | var newVersion = protocol.incr(oldVersion); // => 2 38 | 39 | var comparison = protocol.compare(oldVersion, newVersion); 40 | console.log(comparison === protocol.consts.LT); // true 41 | 42 | comparison = protocol.compare(oldVersion, oldVersion); 43 | console.log(comparison === protocol.consts.EQ); // true 44 | 45 | comparison = protocol.compare(newVersion, oldVersion); 46 | console.log(comparison === protocol.consts.GT); // true 47 | ``` 48 | 49 | ## Dates as versions 50 | 51 | Dates act similarly to numbers except version incrementing a Date just means 52 | returning the Date right now. 53 | 54 | ```javascript 55 | var lineage = require('lineage') 56 | , protocol = lineage.protocol; 57 | 58 | // Require this to extend Date with the version protocol provided by lineage 59 | require('lineage/lib/types/number'); 60 | 61 | var oldVersion = new Date(); 62 | 63 | setTimeout( function () { 64 | var newVersion = protocol.incr(oldVersion); // => 65 | var comparison = protocol.compare(oldVersion, newVersion); 66 | console.log(comparison ==== protocol.consts.LT); // true 67 | 68 | comparison = protocol.compare(oldVersion, oldVersion); 69 | console.log(comparison, protocol.consts.EQ); // true 70 | 71 | comparison = protocol.compare(newVersion, oldVersion); 72 | console.log(comparison === protocol.consts.GT); // true 73 | }, 1); 74 | ``` 75 | 76 | ## Vector Clocks 77 | 78 | For distributed systems, the ideal data structure to use is a vector clock, 79 | also known as a [Lamport Clock](http://en.wikipedia.org/wiki/Lamport_timestamps). 80 | 81 | Lineage provides vector clocks out of the box. 82 | 83 | We will get into why vector clocks are critical when you want to version data 84 | in a distributed system, but for a more complete treatment on the subject, 85 | please refer to [the canonical paper on Lamport 86 | clocks](http://research.microsoft.com/en-us/um/people/lamport/pubs/time-clocks.pdf). 87 | 88 | What a vector clock does behind the scenes is more straightforward. Vector 89 | clocks extend the concept of the Number as a counter that represents the 90 | version. Vector clocks are a set of counters, where each counter is associated 91 | with the actor/client/node that updated the vector clock. Every time a 92 | particular actor updates a vector clock, the counter it is associated with is 93 | incremented. 94 | 95 | ```JavaScript 96 | var lineage = require('lineage') 97 | , protocol = lineage.protocol 98 | , Clock = require('lineage/lib/types/clock'); 99 | 100 | var oldVersion = new Clock(); // => 101 | 102 | // Compared to Number as a version, Clocks as versions must associate every 103 | // update to a version with an actor id. Here that actor id is 'actor-A' 104 | var newVersion = protocol.incr(oldVersion, 'actor-A'); 105 | 106 | // Vectors updated by an actor compare to other vectors just how you would 107 | // expect them to. 108 | var comparison = protocol.compare(oldVersion, newVersion); 109 | console.log(comparison === protocol.consts.LT); // true 110 | 111 | comparison = protocol.compare(oldVersion, oldVersion); 112 | console.log(comparison === protocol.consts.EQ); // true 113 | 114 | comparison = protocol.compare(newVersion, oldVersion); 115 | console.log(comparison === protocol.consts.GT); // true 116 | ``` 117 | 118 | Up until this point, it would appear as though Clock versions compare to each 119 | other in the same way that Number versions compare to each other. So why even 120 | have a different data structure to use for versions? 121 | 122 | In a contrived real world scenario, consider a person in San Francisco who owns 123 | a calendar. If she adds an event to the calendar each day, then the calendar on 124 | any given day is a later version than the calendar the day before. In this 125 | scenario, using a Number as a counter version works perfectly well. 126 | 127 | Things become more interesting when we involve another actor updating the 128 | version -- in this case, we have a distributed system. 129 | 130 | Suppose the San Francisco citizen makes a copy of her calendar and sends it to 131 | a man in China. Now suppose that on the first day that they both have identical 132 | calendars, each of them decide to update their calendars. How would you compare 133 | the 2 calendars? They are older than their respective versions, but how do they 134 | compare to each other? They are not at equivalent versions, but one does not 135 | also have a greater or less version than the other. In this case, we say the 136 | two versions are concurrent. 137 | 138 | ```javascript 139 | var lineage = require('lineage') 140 | , protocol = lineage.protocol 141 | , Clock = require('lineage/lib/types/clock'); 142 | 143 | var sfVersion = new Clock(); // => 144 | var chineseVersion = new Clock(); // => 145 | 146 | protocol.incr(sfVersion, 'sf-person'); 147 | protocol.incr(chineseVersion, 'chinese-person'); 148 | 149 | var comparison = protocol.compare(sfVersion, chineseVersion); 150 | console.log(comparison === protocol.consts.CONCURRENT); // true 151 | ``` 152 | 153 | In distributed systems, we can reconcile data at concurrent versions by 154 | resolving the given data conflict inherent in the concurrent versions of data 155 | and then merge the two versions into a new version that is greater than the two 156 | versions. 157 | 158 | In the contrived real world example, this could occur if the Chinese man sends 159 | the San Francisco woman his calendar. She could then make a 3rd calendar that 160 | included both her and his events. This calendar would then be considered at a 161 | greater version than the two prior calendars because it reflects a calendar 162 | that takes into account the historic set of changes to both calendars. 163 | 164 | Continuing with our code from the last example: 165 | 166 | ```javascript 167 | var reconciledVersion = protocol.merge(sfVersion, chineseVersion); 168 | 169 | comparison = protocol.compare(reconciledVersion, sfVersion); 170 | console.log(comparison === protocol.consts.GT); // true 171 | 172 | comparison = protocol.compare(reconciledVersion, chineseVersion); 173 | console.log(comparison === protocol.consts.GT); // true 174 | ``` 175 | 176 | ## Annealing Vector Clocks 177 | 178 | Vector clocks are essential for handling data versioning in a distributed system. 179 | However, they can also grow in an unbounded way if the number of actors who can 180 | update the version grows in an unbounded way. If there are 100 actors that 181 | updated the clock, then the clock will consist of 100 counters behind the scenes. 182 | 183 | In practice, what is often used instead is a variation known as an annealed 184 | vector clock, that limits the number of actors to a maximum. If a vector clock 185 | has already been updated by a maximum number of actors, then a new actor who 186 | decides to update that version will effectively replace the counter that has 187 | not been updated the longest. 188 | 189 | ```javascript 190 | var lineage = require('lineage') 191 | , protocol = lineage.protocol 192 | , LengthAnnealedClock = require('lineage/lib/types/length_annealed_clock'); 193 | 194 | var versionA = new LengthAnnealedClock(2); // => 195 | var versionB = new LengthAnnealedClock(2); // => 196 | 197 | protocol.incr(versionA, 'actor-A'); 198 | protocol.incr(versionA, 'actor-B'); 199 | 200 | protocol.incr(versionB, 'actor-A'); 201 | protocol.incr(versionB, 'actor-B'); 202 | 203 | var comparison = protocol.compare(versionA, versionB); 204 | console.log(comparison === protocol.consts.EQ); 205 | 206 | // Here we update the version with a new actor, but the version clock is 207 | // already at its maximum number of counters. 208 | protocol.incr(versionB, 'actor-C'); 209 | 210 | comparison = protocol.compare(versionA, versionB); 211 | // Normally, in a non-annealed Clock, this comparison would place versionA as 212 | // LT (less than) versionB. 213 | // When we anneal a Clock, we can lose information of what other actors have 214 | // done to a version historically. Because of that, versionB actually counts as 215 | // being CONCURRENT to versionA. 216 | console.log(comparison === protocol.consts.CONCURRENT; 217 | ``` 218 | 219 | ### MIT License 220 | Copyright (c) 2012 by Brian Noguchi and Nate Smith 221 | 222 | Permission is hereby granted, free of charge, to any person obtaining a copy 223 | of this software and associated documentation files (the "Software"), to deal 224 | in the Software without restriction, including without limitation the rights 225 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 226 | copies of the Software, and to permit persons to whom the Software is 227 | furnished to do so, subject to the following conditions: 228 | 229 | The above copyright notice and this permission notice shall be included in 230 | all copies or substantial portions of the Software. 231 | 232 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 233 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 234 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 235 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 236 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 237 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 238 | THE SOFTWARE. 239 | -------------------------------------------------------------------------------- /lib/consts.js: -------------------------------------------------------------------------------- 1 | exports.LT = -1; 2 | exports.EQ = 0; 3 | exports.GT = 1; 4 | exports.CONCURRENT = null; 5 | -------------------------------------------------------------------------------- /lib/lineage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | protocol: require('./protocol') 3 | , consts: require('./consts') 4 | }; 5 | -------------------------------------------------------------------------------- /lib/protocol.js: -------------------------------------------------------------------------------- 1 | var protocol = require('protocoljs'); 2 | 3 | module.exports = protocol({ 4 | incr: ('Increments the version' 5 | , [protocol, ('agent', Object)]) 6 | , compare: ('Compares 2 versions of the same type' 7 | , [protocol, ('otherVersion', protocol)]) 8 | , merge: ('Merges 2 versions to create a version that is causally ahead of both input versions' 9 | , [('agent', Object), protocol, ('otherVersion', Object)]) 10 | }); 11 | -------------------------------------------------------------------------------- /lib/types/clock.js: -------------------------------------------------------------------------------- 1 | // Vector Clock (aka Logical Clock, or Lamport Clock) 2 | 3 | var VersionProtocol = require('../protocol') 4 | , consts = require('../consts') 5 | , LT = consts.LT 6 | , GT = consts.GT 7 | , EQ = consts.EQ 8 | , CONCURRENT = consts.CONCURRENT; 9 | 10 | module.exports = Clock; 11 | 12 | function Clock () { 13 | this.vector = {}; 14 | } 15 | 16 | Clock.prototype.protocolId = 'lineage:clock'; 17 | 18 | VersionProtocol(Clock, { 19 | incr: function (clock, agentId) { 20 | var vec = clock.vector; 21 | if (! (agentId in vec)) return vec[agentId] = 1; 22 | return ++vec[agentId]; 23 | } 24 | , compare: function (clock, otherClock) { 25 | var allKeys = {}, vec; 26 | for (var i = arguments.length; i--; ) { 27 | vec = arguments[i].vector; 28 | for (var agentId in vec) allKeys[agentId] = true; 29 | } 30 | 31 | var counter, otherCounter, mem; 32 | for (agentId in allKeys) { 33 | counter = clock.vector[agentId] || 0; 34 | otherCounter = otherClock.vector[agentId] || 0; 35 | 36 | if (counter < otherCounter) { 37 | if (mem == GT) return CONCURRENT; 38 | mem = LT; 39 | } else if (counter > otherCounter) { 40 | if (mem == LT) return CONCURRENT; 41 | mem = GT; 42 | } else if (counter == otherCounter) { 43 | if (mem != LT && mem != GT) mem = EQ; 44 | } 45 | } 46 | return mem; 47 | } 48 | , merge: function (agentId, clock, otherClock) { 49 | var newClock = new Clock(); 50 | 51 | var vec = newClock.vector = greedyZip(clock.vector, otherClock.vector, function (a, b) { 52 | return Math.max(a || 0, b || 0); 53 | }); 54 | if (vec[agentId]) { 55 | ++vec[agentId]; 56 | } else { 57 | vec[agentId] = 1; 58 | } 59 | return newClock; 60 | } 61 | }); 62 | 63 | // Given 2 Objects a and b, iterate through their keys. For each key, take the 64 | // corresponding value a[k] and the corresponding value b[k], and apply the 65 | // function fn -- i.e., fn(a[k], b[k]). Assign the result to the same key k on 66 | // a new Object we eventually return. 67 | function greedyZip (a, b, fn) { 68 | var out = {} 69 | , seen = {}; 70 | for (var k in a) { 71 | seen[k] = true; 72 | out[k] = fn(a[k], b[k]); 73 | } 74 | for (k in b) { 75 | if (k in seen) continue; 76 | out[k] = fn(a[k], b[k]); 77 | } 78 | return out; 79 | } 80 | -------------------------------------------------------------------------------- /lib/types/date.js: -------------------------------------------------------------------------------- 1 | var VersionProtocol = require('../protocol') 2 | , consts = require('../consts') 3 | , LT = consts.LT 4 | , GT = consts.GT 5 | , EQ = consts.EQ; 6 | 7 | module.exports = Date; 8 | 9 | VersionProtocol(Date, { 10 | incr: function (date, agent) { 11 | var newDate = new Date(); 12 | if (+newDate === +date) { 13 | return new Date(+date + 1); 14 | } 15 | return newDate; 16 | } 17 | , compare: function (dateA, dateB) { 18 | if (dateA < dateB) return LT; 19 | if (dateA > dateB) return GT; 20 | return EQ; 21 | } 22 | , merge: function (agent, dateA, dateB) { 23 | var now = new Date; 24 | if (now > dateA && now > dateB) return now; 25 | return new Date(Math.max(dateA, dateB)); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /lib/types/length_annealing_clock.js: -------------------------------------------------------------------------------- 1 | var VersionProtocol = require('../protocol') 2 | , consts = require('../consts') 3 | , LT = consts.LT 4 | , GT = consts.GT 5 | , EQ = consts.EQ 6 | , CONCURRENT = consts.CONCURRENT 7 | 8 | , Clock = require('./clock'); 9 | 10 | module.exports = LengthAnnealingClock; 11 | 12 | function LengthAnnealingClock (max) { 13 | this.vectorClock = new Clock(); 14 | 15 | this.max = max; 16 | 17 | // [agentId, timestamp] pairs sorted from earliest to most recent 18 | this.lruAgents = []; 19 | } 20 | 21 | function indexOf (array, fn) { 22 | for (var i = 0, l = array.length; i < l; i++) { 23 | if (fn(array[i])) return i; 24 | } 25 | return -1; 26 | }; 27 | 28 | LengthAnnealingClock.prototype.protocolId = 'lineage:length_annealing_clock'; 29 | 30 | VersionProtocol(LengthAnnealingClock, { 31 | incr: function (clock, agentId) { 32 | var max = clock.max 33 | , vec = clock.vectorClock.vector 34 | , lruAgents = clock.lruAgents 35 | , index = indexOf(lruAgents, function (pair) { 36 | return pair[0] === agentId; 37 | }) 38 | , counter; 39 | 40 | if (~index) { 41 | lruAgents.splice(index, 1); 42 | lruAgents.push([agentId, +new Date()]); 43 | } else { 44 | vec[agentId] = 0; 45 | } 46 | lruAgents.push([agentId, +new Date()]); 47 | if (lruAgents.length >= max) { 48 | var agentIdToRm = lruAgents.shift()[0]; 49 | delete vec[agentIdToRm]; 50 | } 51 | return ++vec[agentId]; 52 | } 53 | , compare: function (clock, otherClock) { 54 | return VersionProtocol.compare(clock.vectorClock, otherClock.vectorClock); 55 | } 56 | , merge: function (agentId, clock, otherClock) { 57 | var lruAgents = clock.lruAgents 58 | 59 | , otherLruAgents = otherClock.lruAgents 60 | 61 | , max = Math.max(clock.max, otherClock.max) 62 | 63 | , mergedClock = new LengthAnnealingClock(max) 64 | , mergedLruAgents = mergedClock.lruAgents 65 | ; 66 | 67 | mergedClock.vectorClock = VersionProtocol.merge(agentId, clock.vectorClock, otherClock.vectorClock); 68 | 69 | // Merge the lru agents 70 | var uniqueAgents = [[agentId, +new Date()]]; 71 | var agentLists = [lruAgents, otherLruAgents]; 72 | for (var k = agentLists.length; k--; ) { 73 | var agentList = agentLists[k] 74 | , pair, index, currAgentId, timestamp; 75 | for (var i = 0, l = agentList.length; i < l; i++) { 76 | pair = agentList[i]; 77 | currAgentId = pair[0]; 78 | if (currAgentId === agentId) { 79 | continue; 80 | } 81 | index = indexOf(uniqueAgents, function (pair) { 82 | return pair[0] === currAgentId; 83 | }); 84 | if (~index) { 85 | // Update the timestamp to the max timestamp per agentId 86 | timestamp = pair[1]; 87 | uniqueAgents[index][1] = Math.max(timestamp, uniqueAgents[index][1]); 88 | } else { 89 | uniqueAgents.push(pair); 90 | } 91 | } 92 | } 93 | 94 | // Sort the agents in chronological order and assign to the clock 95 | mergedClock.lruAgents = uniqueAgents.sort( function (pairA, pairB) { 96 | return pairA[1] - pairB[1]; 97 | }); 98 | return mergedClock; 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /lib/types/number.js: -------------------------------------------------------------------------------- 1 | var VersionProtocol = require('../protocol') 2 | , consts = require('../consts') 3 | , LT = consts.LT 4 | , GT = consts.GT 5 | , EQ = consts.EQ; 6 | 7 | module.exports = Number; 8 | 9 | VersionProtocol(Number, { 10 | incr: function (scalarClock, agent) { 11 | return scalarClock + 1; 12 | } 13 | , compare: function (scalarClockA, scalarClockB) { 14 | if (scalarClockA < scalarClockB) return LT; 15 | if (scalarClockA > scalarClockB) return GT; 16 | return EQ; 17 | } 18 | , merge: function (agent, scalarClockA, scalarClockB) { 19 | return Math.max(scalarClockA, scalarClockB) + 1; 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Brian Noguchi ", 3 | "name": "lineage", 4 | "description": "Data versioning done right", 5 | "version": "0.1.0", 6 | "homepage": "https://github.com/codeparty/lineage", 7 | "repository": { 8 | "url": "git://github.com/codeparty/lineage.git" 9 | }, 10 | "main": "lib/lineage.js", 11 | "scripts": { 12 | "test": "make test" 13 | }, 14 | "dependencies": { 15 | "protocoljs": "0.1.0" 16 | }, 17 | "devDependencies": { 18 | "expect.js": "*", 19 | "mocha": "*" 20 | }, 21 | "optionalDependencies": {}, 22 | "engines": { 23 | "node": "*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/types/clock.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js') 2 | , lineage = require('../../lib/lineage') 3 | , VersionProtocol = lineage.protocol 4 | , consts = lineage.consts 5 | , GT = consts.GT 6 | , LT = consts.LT 7 | , EQ = consts.EQ 8 | , CONCURRENT = consts.CONCURRENT 9 | , VectorClock = require('../../lib/types/clock'); 10 | 11 | describe('VectorClock as a version', shouldBehaveLikeAVectorClock(createClock)); 12 | 13 | exports.shouldBehaveLikeAVectorClock = shouldBehaveLikeAVectorClock; 14 | 15 | function createClock () { 16 | return new VectorClock(); 17 | } 18 | 19 | function shouldBehaveLikeAVectorClock (createClock) { 20 | return function () { 21 | it('a blank clock incremented once should be LT a clock incremented twice by the same agent', function () { 22 | var clockA = createClock() 23 | , clockB = createClock(); 24 | 25 | VersionProtocol.incr(clockA, 'agent-1'); 26 | VersionProtocol.incr(clockB, 'agent-1'); 27 | VersionProtocol.incr(clockB, 'agent-1'); 28 | expect(VersionProtocol.compare(clockA, clockB)).to.equal(LT); 29 | }); 30 | 31 | it('a blank clock incremented twice should be GT a clock incremented once by the same agent', function () { 32 | var clockA = createClock() 33 | , clockB = createClock(); 34 | 35 | VersionProtocol.incr(clockA, 'agent-1'); 36 | VersionProtocol.incr(clockA, 'agent-1'); 37 | VersionProtocol.incr(clockB, 'agent-1'); 38 | 39 | expect(VersionProtocol.compare(clockA, clockB)).to.equal(GT); 40 | }); 41 | 42 | it('a blank clock incremented the same number of times as another clock by the same agent should be EQ', function () { 43 | var clockA = createClock() 44 | , clockB = createClock(); 45 | 46 | VersionProtocol.incr(clockA, 'agent-1'); 47 | VersionProtocol.incr(clockB, 'agent-1'); 48 | 49 | expect(VersionProtocol.compare(clockA, clockB)).to.equal(EQ); 50 | }); 51 | 52 | it('clocks with the same causal past that are then incremented by different agents should be CONCURRENT', function () { 53 | var clockA = createClock() 54 | , clockB = createClock(); 55 | 56 | VersionProtocol.incr(clockA, 'agent-1'); 57 | VersionProtocol.incr(clockB, 'agent-2'); 58 | 59 | expect(VersionProtocol.compare(clockA, clockB)).to.equal(CONCURRENT); 60 | }); 61 | 62 | describe('a merged version', function () { 63 | it('should be greater than the versions that were merged', function () { 64 | var clockA = createClock() 65 | , clockB = createClock(); 66 | 67 | VersionProtocol.incr(clockA, 'agent-1'); 68 | VersionProtocol.incr(clockB, 'agent-2'); 69 | 70 | var mergedClock = VersionProtocol.merge('agent-1', clockA, clockB); 71 | 72 | expect(VersionProtocol.compare(mergedClock, clockA)).to.equal(GT); 73 | expect(VersionProtocol.compare(mergedClock, clockB)).to.equal(GT); 74 | }); 75 | }); 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /test/types/date.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js') 2 | , lineage = require('../../lib/lineage') 3 | , VersionProtocol = lineage.protocol 4 | , consts = lineage.consts 5 | , GT = consts.GT 6 | , LT = consts.LT 7 | , EQ = consts.EQ 8 | require('../../lib/types/date'); 9 | 10 | 11 | describe('Date as a version', function () { 12 | it('incr should update the time', function () { 13 | var date = new Date() 14 | setTimeout( function () { 15 | var newDate = VersionProtocol.incr(date); 16 | expect(+newDate).to.be.greaterThan(+date); 17 | }, 2); 18 | }); 19 | 20 | describe('compare(dateA, dateB)', function () { 21 | it('should be LT when dateA < dateB', function () { 22 | var date = new Date() 23 | , laterDate = new Date(+date + 6000); 24 | expect(VersionProtocol.compare(date, laterDate)).to.equal(LT); 25 | }); 26 | 27 | it('should be EQ when dateA === dateB', function () { 28 | var date = new Date() 29 | , equivDate = new Date(+date); 30 | expect(VersionProtocol.compare(date, equivDate)).to.equal(EQ); 31 | }); 32 | 33 | it('should be GT when dateA > dateB', function () { 34 | var date = new Date() 35 | , priorDate = new Date(+date - 60000); 36 | expect(VersionProtocol.compare(date, priorDate)).to.equal(GT); 37 | }); 38 | }); 39 | 40 | it('merge should return a version greater than both input versions', function () { 41 | var refDate = new Date() 42 | , dateA = new Date(+refDate - 60000) 43 | , dateB = new Date(+refDate - 30000); 44 | var dateM = VersionProtocol.merge('agent', dateA, dateB); 45 | expect(VersionProtocol.compare(dateM, dateA)).to.equal(GT); 46 | expect(VersionProtocol.compare(dateM, dateB)).to.equal(GT); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/types/length_annealing_clock.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js') 2 | , lineage = require('../../lib/lineage') 3 | , VersionProtocol = lineage.protocol 4 | , consts = lineage.consts 5 | , GT = consts.GT 6 | , LT = consts.LT 7 | , EQ = consts.EQ 8 | , CONCURRENT = consts.CONCURRENT 9 | , LengthAnnealingClock = require('../../lib/types/length_annealing_clock') 10 | 11 | , shouldBehaveLikeAVectorClock = require('./clock').shouldBehaveLikeAVectorClock; 12 | 13 | function createClock () { 14 | return new LengthAnnealingClock(2); 15 | } 16 | 17 | describe('LengthAnnealingClock as a version', function () { 18 | shouldBehaveLikeAVectorClock(createClock); 19 | 20 | describe('when the number of agents who updated the version exceed the limit', function () { 21 | describe('an incremented version', function () { 22 | it('should be CONCURRENT with the version before incrementing the version', function () { 23 | var clockA = createClock() 24 | , clockB = createClock(); 25 | VersionProtocol.incr(clockA, 'agent-1'); 26 | VersionProtocol.incr(clockA, 'agent-2'); 27 | 28 | VersionProtocol.incr(clockB, 'agent-1'); 29 | VersionProtocol.incr(clockB, 'agent-2'); 30 | 31 | expect(VersionProtocol.compare(clockA, clockB)).to.equal(EQ); 32 | 33 | VersionProtocol.incr(clockB, 'agent-3'); 34 | 35 | expect(VersionProtocol.compare(clockA, clockB)).to.equal(CONCURRENT); 36 | }); 37 | }); 38 | 39 | describe('a merged version', function () { 40 | it('should be GT the versions that were merged', function () { 41 | var clockA = createClock() 42 | , clockB = createClock(); 43 | VersionProtocol.incr(clockA, 'agent-1'); 44 | VersionProtocol.incr(clockA, 'agent-2'); 45 | 46 | VersionProtocol.incr(clockB, 'agent-1'); 47 | VersionProtocol.incr(clockB, 'agent-2'); 48 | VersionProtocol.incr(clockB, 'agent-3'); 49 | 50 | var mergedClock = VersionProtocol.merge('agent-3', clockA, clockB); 51 | expect(VersionProtocol.compare(mergedClock, clockA)).to.equal(GT); 52 | expect(VersionProtocol.compare(mergedClock, clockB)).to.equal(GT); 53 | }); 54 | 55 | it('should be CONCURRENT with a version that results from incrementing it', function () { 56 | var clockA = createClock() 57 | , clockB = createClock(); 58 | VersionProtocol.incr(clockA, 'agent-1'); 59 | VersionProtocol.incr(clockA, 'agent-2'); 60 | 61 | VersionProtocol.incr(clockB, 'agent-1'); 62 | VersionProtocol.incr(clockB, 'agent-2'); 63 | VersionProtocol.incr(clockB, 'agent-3'); 64 | 65 | var mergedClockX = VersionProtocol.merge('agent-3', clockA, clockB); 66 | var mergedClockY = VersionProtocol.merge('agent-3', clockA, clockB); 67 | expect(VersionProtocol.compare(mergedClockX, mergedClockY)).to.equal(EQ); 68 | VersionProtocol.incr(mergedClockY, 'agent-3'); 69 | expect(VersionProtocol.compare(mergedClockX, mergedClockY)).to.equal(CONCURRENT); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/types/number.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js') 2 | , lineage = require('../../lib/lineage') 3 | , VersionProtocol = lineage.protocol 4 | , consts = lineage.consts 5 | , GT = consts.GT 6 | , LT = consts.LT 7 | , EQ = consts.EQ 8 | require('../../lib/types/number'); 9 | 10 | 11 | describe('Number as a version', function () { 12 | it('incr should increment the number', function () { 13 | expect(VersionProtocol.incr(1)).to.equal(2); 14 | }); 15 | 16 | describe('compare(numA, numB)', function () { 17 | it('should be LT when numA < numB', function () { 18 | expect(VersionProtocol.compare(1, 2)).to.equal(LT); 19 | }); 20 | 21 | it('should be EQ when numA === numB', function () { 22 | expect(VersionProtocol.compare(1, 1)).to.equal(EQ); 23 | }); 24 | 25 | it('should be GT when numA > numB', function () { 26 | expect(VersionProtocol.compare(2, 1)).to.equal(GT); 27 | }); 28 | }); 29 | 30 | it('merge should return a version greater than both input versions', function () { 31 | var ver = VersionProtocol.merge('agent', 2, 6); 32 | expect(VersionProtocol.compare(ver, 2)).to.equal(GT); 33 | expect(VersionProtocol.compare(ver, 6)).to.equal(GT); 34 | }); 35 | }); 36 | --------------------------------------------------------------------------------