├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 'Dominic Tarr' 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # monotonic-map 2 | 3 | A simple replication protocol for _maps_ to monotonic values. 4 | 5 | A _Map_ is also known as a key:value store, for example, javascript `{}` objects. 6 | _Monotonic_ means that the values only go in one direction. I.e. a familiar example is counting. Another example is a Set, if items are never removed. 7 | 8 | ## overview 9 | 10 | This is a very simple replication protocol which was created 11 | as an abstraction of the vector clock exchange in 12 | [scuttlebutt](https://github.com/dominictarr/scuttlebutt) 13 | as intended for use in [scuttlebot](https://github.com/ssbc/scuttlebot) as part of 14 | [epidemic-broadcast-trees](https://github.com/dominictarr/epidemic-broadcast-trees) 15 | 16 | Instead of a _clock_ we send a _map_, where we consider 17 | the values to be always increasing (defined by user provided function) 18 | 19 | This produces a replication protocol where essentially, one peer 20 | sends the values they have. A peer doesn't need to send a value 21 | if they know the peer already has it. But this also means that 22 | a peer can send the same update back (confirming it), to let the sender know they 23 | received it. (sending this information is implicit in [epidemic-broadcast-trees](https://github.com/dominictarr/epidemic-broadcast-trees), anyway) 24 | 25 | If replication is more common than updates, then confirming 26 | the update will save the remote sending it again on the next connection. 27 | But if it's more likely that the value is updated again anyway, 28 | then it would be more efficient to just let it update. 29 | 30 | ## api 31 | 32 | ``` js 33 | var MonotonicMap = require('monotonic-map') 34 | ``` 35 | 36 | ### `mm = MonotonicMap(compare(a,b)=>-1|0|1)` 37 | 38 | define a `MonotonicMap` around a `compare` function. 39 | `compare` is used to check that the new value for a key is 40 | greater than the current value. see _mm.set_. 41 | 42 | ### mm.get(key) 43 | 44 | returns the current value, if defined. 45 | 46 | ### mm.set(key, value) 47 | 48 | if `compare(value, _value = mm.get(key)) > 0` then `value` 49 | becomes the new value for this key. if `compare` returns 0 50 | then the value is not changed. If it returns < 0, an error is thrown. 51 | 52 | ### mm.send(peer_id) => map 53 | 54 | get a `map` to send to a remote `peer_id`. 55 | `map` will be a serializable representation (.i.e. plain js object) of the state of the current map. 56 | 57 | ### mm.receive (peer_id, map) 58 | 59 | receive `map` from `peer_id`. If any value in the map are greater 60 | than the current values for the local key, then they will be 61 | updated. 62 | 63 | ## License 64 | 65 | MIT 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | function copy (obj) { 3 | var o = {} 4 | for(var k in obj) o[k] = obj[k] 5 | return o 6 | } 7 | 8 | module.exports = function (compare) { 9 | 10 | var store = {} 11 | var peers = {} 12 | 13 | return { 14 | set: function (key, value) { 15 | if(store[key] == null) 16 | return store[key] = value 17 | var c = compare(value, store[key]) 18 | if(c > 0) 19 | store[key] = value 20 | else if (c < 0) 21 | throw new Error('tried to set old value') 22 | }, 23 | get: function (key) { 24 | return store[key] 25 | }, 26 | send: function (peer) { 27 | if(!peers[peer]) { 28 | console.log('COPY TO', peer, store) 29 | return copy(store) 30 | } 31 | else { 32 | var send = {} 33 | for(var k in store) { 34 | var value = store[k], _value = peers[peer][k] 35 | if(_value == null || compare(value, _value) > 0) 36 | send[k] = value 37 | } 38 | return send 39 | } 40 | }, 41 | receive: function (map, peer, response) { 42 | peers[peer] = peers[peer] || {} 43 | var changes = {} 44 | for(var k in map) { 45 | if(store[k] == null || compare(map[k], store[k]) > 0) 46 | changes[k] = store[k] = map[k] 47 | peers[peer][k] = map[k] 48 | } 49 | if(response) 50 | //check if we have something missing from peer. 51 | for(var k in store) { 52 | if(peers[peer][k] == null || compare(store[k], peers[peer][k])) 53 | changes[k] = store[k] 54 | } 55 | return changes 56 | }, 57 | store: store, 58 | peers: peers 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monotonic-map", 3 | "description": "", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/dominictarr/monotonic-map", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/dominictarr/monotonic-map.git" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "tape": "^4.6.3" 13 | }, 14 | "scripts": { 15 | "test": "set -e; for t in test/*.js; do node $t; done" 16 | }, 17 | "author": "'Dominic Tarr' (http://dominictarr.com)", 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | 3 | var MonotonicMap = require('../') 4 | 5 | function compare (a, b) { 6 | return a > b ? 1 : a < b ? -1 : 0 7 | } 8 | 9 | function Replicate1(t, alice, bob) { 10 | return function (ab, ba, _a, _b) { 11 | var _ab = alice.send('bob') 12 | t.deepEqual(_ab, ab, 'alice sent to bob') 13 | 14 | var _ba = bob.send('alice') 15 | t.deepEqual(_ba, ba, 'bob sent to alice') 16 | 17 | t.deepEqual(alice.receive(_ba, 'bob'), _a, 'alice updated from bob') 18 | t.deepEqual(bob.receive(_ab, 'alice'), _b, 'bob updated from alice') 19 | 20 | t.deepEqual(alice.receive(_b, 'bob'), {}) 21 | t.deepEqual(bob.receive(_a, 'alice'), {}) 22 | } 23 | } 24 | 25 | function isEmpty(obj) { 26 | for(var k in obj) 27 | return false 28 | return true 29 | } 30 | 31 | function Replicate2(t, alice, bob) { 32 | return function (ab, ba, _a, _b) { 33 | var args = [].slice.call(arguments) 34 | var peers = [alice, bob], names = ['alice', 'bob'] 35 | var data = alice.send('bob'), i = 1 36 | t.deepEqual(data, args.shift()) 37 | while(!isEmpty(data)) { 38 | data = peers[i%2].receive(data, names[(i+1)%2], true) 39 | t.deepEqual(data, args.shift(), 'send:'+i) 40 | i ++ 41 | } 42 | t.equal(args.length, 0) 43 | } 44 | } 45 | 46 | 47 | tape('simple', function (t) { 48 | var alice = MonotonicMap(compare) 49 | var bob = MonotonicMap(compare) 50 | var replicate = Replicate1(t, alice, bob) 51 | 52 | alice.set('a', 1) 53 | alice.set('b', 2) 54 | alice.set('a', 2) //update. 55 | t.throws(function () { 56 | alice.set('a', 0) 57 | }) 58 | 59 | // t.deepEqual(alice.send('bob'), {a: 2, b: 2}, 'alice send to bob?') 60 | 61 | console.log('Alice', alice) 62 | replicate( 63 | {a: 2, b: 2}, //a->b 64 | {}, //b->a 65 | {}, //received by alice 66 | {a: 2, b: 2} //received by bob 67 | ) 68 | 69 | bob.set('b', 3) //update just one item relative to alice. 70 | 71 | // t.deepEqual(bob.send('alice'), {b: 3}) 72 | // 73 | // _data = alice.send('bob') 74 | // t.deepEqual(alice.receive(bob.send('alice'), 'bob'), {b: 3}) 75 | // t.deepEqual(bob.receive(_data, 'alice'), {}) 76 | 77 | replicate 78 | ( 79 | {}, 80 | {b: 3}, 81 | {b: 3}, 82 | {} 83 | ) 84 | 85 | bob.set('c', 7) 86 | 87 | t.deepEqual(bob.send('alice'), {c: 7}) 88 | //bob doesn't actually know for sure that alice has b:3 yet, 89 | //so it's still in the send set. 90 | _data = alice.send('bob') 91 | t.deepEqual(alice.receive(bob.send('alice'), 'bob'), {c:7}) 92 | t.deepEqual(bob.receive(_data, 'alice'), {}) 93 | 94 | 95 | console.log("Alice", alice) 96 | console.log('Bob', bob) 97 | 98 | replicate 99 | ( 100 | {}, 101 | {c: 7}, 102 | {}, 103 | {} 104 | ) 105 | 106 | t.end() 107 | }) 108 | 109 | tape('reply', function (t) { 110 | var alice = MonotonicMap(compare) 111 | var bob = MonotonicMap(compare) 112 | var replicate = Replicate2(t, alice, bob) 113 | 114 | alice.set('a', 1) 115 | 116 | replicate({a:1}, {a:1}, {}) 117 | 118 | alice.set('c', 1) 119 | 120 | replicate({c:1}, {c:1}, {}) 121 | 122 | alice.set('a', 2) 123 | bob.set('b', 1) 124 | 125 | replicate({a:2}, {b:1, a: 2}, {b:1}, {}) 126 | 127 | t.end() 128 | }) 129 | 130 | 131 | 132 | 133 | 134 | --------------------------------------------------------------------------------