Commit 5972f7a224a6bfd3178683ba2f35c6109a72d504
initial
Dominic Tarr committed on 10/3/2016, 11:24:03 AMFiles changed
LICENSE | added |
README.md | added |
index.js | added |
package.json | added |
test/index.js | added |
LICENSE | |||
---|---|---|---|
@@ -1,0 +1,22 @@ | |||
1 … | +Copyright (c) 2016 '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. | ||
README.md | ||
---|---|---|
@@ -1,0 +1,81 @@ | ||
1 … | +# status-swarm | |
2 … | + | |
3 … | +A very simple (secure) replication protocol. | |
4 … | + | |
5 … | +`status-swarm` replicates a single value per node, | |
6 … | +where the node's public key is the id, and the value | |
7 … | +may be updated, but new values always win. | |
8 … | +`status-swarm` is intended for relatively long lived connections. | |
9 … | + | |
10 … | +When reconnecting to a node, overhead is small, | |
11 … | +you only send the last time stamp from that peer. | |
12 … | + | |
13 … | +However, you may receive the same message multiple times if connected to | |
14 … | +more than one peer, and upon connecting to a new peer for the first time, | |
15 … | +you may re-receive messages you have already seen. | |
16 … | + | |
17 … | +``` js | |
18 … | + | |
19 … | + | |
20 … | +``` | |
21 … | + | |
22 … | + | |
23 … | +## API | |
24 … | + | |
25 … | +### swarm = Swarm(keys) | |
26 … | + | |
27 … | +create a new instance with a given ssb-keys keypair. | |
28 … | + | |
29 … | +### swarm.update(value) | |
30 … | + | |
31 … | +update your status. the value will be wrapped and signed: | |
32 … | + | |
33 … | +``` js | |
34 … | +{ | |
35 … | + id: <your_id>, | |
36 … | + ts: <localtimestamp>, | |
37 … | + data: <value>, | |
38 … | + signature: <sig> | |
39 … | +} | |
40 … | +``` | |
41 … | + | |
42 … | +### swarm.get(id) | |
43 … | + | |
44 … | +return the current value for this `id` (or `undefined`) | |
45 … | + | |
46 … | +### swarm.last(id) | |
47 … | + | |
48 … | +get the timestamp that you last received something from a given node. | |
49 … | +This is _their_ local time, which may be skewed from yours, | |
50 … | +but you need to know their time, because you will use this to | |
51 … | +request they have received since this time. | |
52 … | + | |
53 … | +### swarm.send(since), swarm.recv(id) | |
54 … | + | |
55 … | +Set up replication between two peers. note that it's _not_ implemented as a duplex stream, | |
56 … | +but as a separate source and a sink. This method makes for a more symmetrical protocol. | |
57 … | +on connecting, each peer requests the remote's `swarm.send(ts||0)` over [rpc](https://github.com/ssbc/muxrpc) | |
58 … | + | |
59 … | +`send` can be exposed over muxrpc, but `recv` can be a private api. | |
60 … | +your code might look something like this: | |
61 … | + | |
62 … | +``` js | |
63 … | +//this code should exist on both ends. | |
64 … | +sbot.on('rpc:connect', function (rpc) { | |
65 … | + var id = rpc.id | |
66 … | + pull(rpc.send(swarm.last(id)), swarm.recv(id)) | |
67 … | +}) | |
68 … | + | |
69 … | +//advertise your network location, for example. | |
70 … | +rpc.update({host: HOST, port: PORT}) | |
71 … | +``` | |
72 … | + | |
73 … | +## TODO | |
74 … | + | |
75 … | +To stop the replication data growing too large, implement filtering (say, filter ids you don't care about) | |
76 … | +expire messages that are too old (say, to represent only peers that are online, say within the last few hours) | |
77 … | + | |
78 … | +## License | |
79 … | + | |
80 … | +MIT | |
81 … | + |
index.js | ||
---|---|---|
@@ -1,0 +1,84 @@ | ||
1 … | +var pull = require('pull-stream') | |
2 … | +var Notify = require('pull-notify') | |
3 … | +var ssbKeys = require('ssb-keys') | |
4 … | +var Cat = require('pull-cat') | |
5 … | +var timestamp = require('monotonic-timestamp') | |
6 … | + | |
7 … | +module.exports = function (keys) { | |
8 … | + | |
9 … | + var notify = Notify() | |
10 … | + var data = {} //key->value | |
11 … | + var local = {} //our local time we received a given message. | |
12 … | + var remote = {} //each remote's localtime | |
13 … | + var listeners = [] | |
14 … | + | |
15 … | + function broadcast(val, localtime) { | |
16 … | + notify(val) | |
17 … | + notify(localtime) | |
18 … | + listeners.forEach(function (l) { | |
19 … | + l(val, localtime) | |
20 … | + }) | |
21 … | + } | |
22 … | + | |
23 … | + function _update (value, id) { | |
24 … | + if('number' === typeof value) { | |
25 … | + remote[id] = Math.max(remote[id] || 0, value) | |
26 … | + return | |
27 … | + } | |
28 … | + | |
29 … | + if(data[value.id]) { | |
30 … | + if(data[value.id].ts >= value.ts) return 'seen'//no change | |
31 … | + } | |
32 … | + | |
33 … | + if(!ssbKeys.verifyObj({public: value.id}, value)) | |
34 … | + return 'invalid' //signature invalid | |
35 … | + | |
36 … | + data[value.id] = value | |
37 … | + var ts = local[value.id] = timestamp() | |
38 … | + | |
39 … | + broadcast(value, ts) | |
40 … | + } | |
41 … | + | |
42 … | + return { | |
43 … | + get: function (id) { | |
44 … | + return data[id] | |
45 … | + }, | |
46 … | + last: function (id) { | |
47 … | + return (id === keys.id ? local[id] : remote[id]) || 0 | |
48 … | + }, | |
49 … | + update: function (value) { | |
50 … | + var localtime = timestamp() | |
51 … | + var val = data[keys.id] = ssbKeys.signObj(keys, { | |
52 … | + id: keys.id, | |
53 … | + //statuses for same id with smaller timestamps are ignored. | |
54 … | + ts: localtime, | |
55 … | + data: value | |
56 … | + }) | |
57 … | + //special case, for us | |
58 … | + local[keys.id] = localtime | |
59 … | + broadcast(val, localtime) | |
60 … | + return val | |
61 … | + }, | |
62 … | + send: function (since) { | |
63 … | + var d = [] | |
64 … | + for(var k in data) if(local[k] > since) d.push(data[k]) | |
65 … | + if(d.length) | |
66 … | + d.push(timestamp()) //also send the localtime. | |
67 … | + return Cat([pull.values(d), notify.listen()]) | |
68 … | + }, | |
69 … | + recv: function (id) { | |
70 … | + return pull.drain(function (value) { | |
71 … | + _update(value, id) | |
72 … | + }) | |
73 … | + }, | |
74 … | + changes: function (onChange) { | |
75 … | + listeners.push(onChange) | |
76 … | + return function () { | |
77 … | + var i = listeners.indexOf(onChange) | |
78 … | + if(~i) listeners.splice(i, 1) | |
79 … | + } | |
80 … | + } | |
81 … | + } | |
82 … | +} | |
83 … | + | |
84 … | + |
package.json | ||
---|---|---|
@@ -1,0 +1,25 @@ | ||
1 … | +{ | |
2 … | + "name": "status-swarm", | |
3 … | + "description": "", | |
4 … | + "version": "0.0.0", | |
5 … | + "homepage": "https://github.com/dominictarr/status-swarm", | |
6 … | + "repository": { | |
7 … | + "type": "git", | |
8 … | + "url": "git://github.com/dominictarr/status-swarm.git" | |
9 … | + }, | |
10 … | + "dependencies": { | |
11 … | + "monotonic-timestamp": "0.0.9", | |
12 … | + "pull-cat": "^1.1.11", | |
13 … | + "pull-notify": "^0.1.1", | |
14 … | + "pull-stream": "^3.4.5", | |
15 … | + "ssb-keys": "^6.1.2" | |
16 … | + }, | |
17 … | + "devDependencies": { | |
18 … | + "tape": "^4.6.0" | |
19 … | + }, | |
20 … | + "scripts": { | |
21 … | + "test": "set -e; for t in test/*.js; do node $t; done" | |
22 … | + }, | |
23 … | + "author": "'Dominic Tarr' <dominic.tarr@gmail.com> (dominictarr.com)", | |
24 … | + "license": "MIT" | |
25 … | +} |
test/index.js | ||
---|---|---|
@@ -1,0 +1,67 @@ | ||
1 … | +var pull = require('pull-stream') | |
2 … | + | |
3 … | +var Swarm = require('../') | |
4 … | +var ssbKeys = require('ssb-keys') | |
5 … | + | |
6 … | +var tape = require('tape') | |
7 … | + | |
8 … | +var ak = ssbKeys.generate() | |
9 … | +var bk = ssbKeys.generate() | |
10 … | +var ck = ssbKeys.generate() | |
11 … | +var alice = Swarm(ak) | |
12 … | +var bob = Swarm(bk) | |
13 … | +var carol = Swarm(bk) | |
14 … | + | |
15 … | +//because everything in this module is sync, | |
16 … | +//testing is quite straightforward. | |
17 … | + | |
18 … | +tape('simple', function (t) { | |
19 … | + | |
20 … | + var start = Date.now() | |
21 … | + | |
22 … | + alice.update({foo: true, address: 'here'}) | |
23 … | + | |
24 … | + bob.update({bar: true, address: 'there'}) | |
25 … | + | |
26 … | + console.log(alice.get(ak.id)) | |
27 … | + | |
28 … | + var ary = [] | |
29 … | + pull(alice.send(0), pull.drain(function (data) { | |
30 … | + ary.push(data) | |
31 … | + })) | |
32 … | + | |
33 … | + var ts = ary.pop() | |
34 … | + t.deepEqual(ary, [alice.get(ak.id)]) | |
35 … | + t.ok(ts >= start) | |
36 … | + t.end() | |
37 … | + | |
38 … | +}) | |
39 … | + | |
40 … | +tape('replicate', function (t) { | |
41 … | + | |
42 … | + | |
43 … | + pull(bob.send(0), alice.recv(bk.id)) | |
44 … | + | |
45 … | + t.deepEqual(alice.get(bk.id), bob.get(bk.id)) | |
46 … | + | |
47 … | + bob.update({bar: false, address: 'yonder'}) | |
48 … | + | |
49 … | + t.deepEqual(alice.get(bk.id), bob.get(bk.id)) | |
50 … | + | |
51 … | + t.equal(alice.last(bk.id), bob.last(bk.id)) | |
52 … | + | |
53 … | + //connect bob -> alice -> carol | |
54 … | + pull(alice.send(Date.now()), carol.recv(bk.id)) | |
55 … | + | |
56 … | + t.deepEqual(carol.get(bk.id), undefined) | |
57 … | + | |
58 … | + bob.update({bar: 1, address: 'thar'}) | |
59 … | + | |
60 … | + t.deepEqual(carol.get(bk.id), bob.get(bk.id)) | |
61 … | + t.deepEqual(alice.get(bk.id), bob.get(bk.id)) | |
62 … | + | |
63 … | + | |
64 … | + t.end() | |
65 … | + | |
66 … | +}) | |
67 … | + |
Built with git-ssb-web