├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── example.js
├── kv.js
├── lib
└── commands.js
├── package.json
└── test
├── async-await.js
├── helper.js
├── multi-replica-die-twice.test.js
├── multi.test.js
└── one.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | # mac files
40 | .DS_Store
41 |
42 | # vim swap files
43 | *.swp
44 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "4"
5 | - "6"
6 | - "8"
7 | - "9"
8 |
9 | env:
10 | - CXX=g++-4.8
11 |
12 | addons:
13 | apt:
14 | sources:
15 | - ubuntu-toolchain-r-test
16 | packages:
17 | - g++-4.8
18 |
19 | after_script:
20 | - npm run coveralls
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Matteo Collina
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # upring-kv
2 |
3 | [![npm version][npm-badge]][npm-url]
4 | [![Build Status][travis-badge]][travis-url]
5 | [](http://standardjs.com/)
6 | [![Coverage Status][coveralls-badge]][coveralls-url]
7 |
8 | Key Value store plugin for UpRing.
9 |
10 | ## Install
11 |
12 | ```
13 | npm i upring-kv --save
14 | ```
15 |
16 | ## Usage
17 |
18 | This library exposes the standard `upring` plugin interface.
19 | Once you register it, it adds a `kv` name space with the API documented below.
20 | ```js
21 | const upring = require('upring')({
22 | logLevel: 'info',
23 | base: [],
24 | hashring: {
25 | joinTimeout: 200,
26 | replicaPoints: 10
27 | }
28 | })
29 |
30 | upring.use(require('upring-kv'))
31 |
32 | upring.on('up', onReady)
33 |
34 | function onReady () {
35 | upring.kv.put('hello', 'world', onPut)
36 | }
37 |
38 | function onPut (err) {
39 | if (err) {
40 | return upring.logger.error(err)
41 | }
42 | upring.kv.get('hello', onGet)
43 | }
44 |
45 | function onGet (err, value) {
46 | if (err) {
47 | return upring.logger.error(err)
48 | }
49 | console.log(value)
50 | upring.close()
51 | }
52 | ```
53 |
54 | ## API
55 |
56 | * kv#get()
57 | * kv#put()
58 | * kv#liveUpdates()
59 |
60 | -------------------------------------------------------
61 |
62 | ### kv.get(key, cb(err, value))
63 |
64 | Get a value from the hashring.
65 | *async-await* is supported as well:
66 | ```js
67 | await upring.kv.get('key')
68 | ```
69 |
70 | -------------------------------------------------------
71 |
72 | ### kv.put(key, value, cb(err))
73 |
74 | Put `value` in the hashring for the given key.
75 | *async-await* is supported as well:
76 | ```js
77 | await upring.kv.put('key', 'value')
78 | ```
79 |
80 | -------------------------------------------------------
81 |
82 | ### kv.liveUpdates(key)
83 |
84 | Returns a `Readable` stream in objectMode, which will include
85 | all updates of given `key`.
86 | It will emit the last value that was [`put`](#put), and it will re-emit
87 | it when reconnecting between multiple hosts.
88 |
89 |
90 | ## Acknowledgements
91 |
92 | This project is kindly sponsored by [nearForm](http://nearform.com).
93 |
94 | ## License
95 |
96 | MIT
97 |
98 | [coveralls-badge]: https://coveralls.io/repos/github/upringjs/upring-kv/badge.svg?branch=master
99 | [coveralls-url]: https://coveralls.io/github/upringjs/upring-kv?branch=master
100 | [npm-badge]: https://badge.fury.io/js/upring-kv.svg
101 | [npm-url]: https://badge.fury.io/js/upring-kv
102 | [travis-badge]: https://api.travis-ci.org/upringjs/upring-kv.svg
103 | [travis-url]: https://travis-ci.org/upringjs/upring-kv
104 |
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const upring = require('upring')({
4 | logLevel: 'info',
5 | base: [],
6 | hashring: {
7 | joinTimeout: 200,
8 | replicaPoints: 10
9 | }
10 | })
11 |
12 | upring.use(require('./kv'))
13 |
14 | upring.on('up', onReady)
15 |
16 | function onReady () {
17 | console.log('upring ready')
18 | upring.kv.put('hello', 'world', onPut)
19 | }
20 |
21 | function onPut (err) {
22 | if (err) {
23 | return upring.logger.error(err)
24 | }
25 | console.log('onPut')
26 | upring.kv.get('hello', onGet)
27 | }
28 |
29 | function onGet (err, value) {
30 | if (err) {
31 | return upring.logger.error(err)
32 | }
33 | console.log(value)
34 | upring.close()
35 | }
36 |
--------------------------------------------------------------------------------
/kv.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const clone = require('clone')
4 | const nes = require('never-ending-stream')
5 | const commands = require('./lib/commands')
6 |
7 | module.exports = function (upring, opts, next) {
8 | if (upring.kv) {
9 | return next(new Error('kv property already exist'))
10 | }
11 | upring.kv = new UpRingKV(upring, opts)
12 | next()
13 | }
14 |
15 | function UpRingKV (upring, opts) {
16 | opts = opts || {}
17 |
18 | this.upring = upring
19 |
20 | this.closed = false
21 | this.ns = opts.namespace || 'kv'
22 |
23 | commands(this)
24 |
25 | // expose the parent logger
26 | this.log = this.upring.log
27 | }
28 |
29 | UpRingKV.prototype.put = function (key, value, cb) {
30 | if (!this.upring.isReady) {
31 | this.upring.once('up', this.put.bind(this, key, value, cb))
32 | return
33 | }
34 |
35 | if (this.upring.allocatedToMe(key)) {
36 | value = clone(value)
37 | }
38 |
39 | if (typeof cb === 'function') {
40 | this.upring.request({ key, value, ns: this.ns, cmd: 'put' }, cb)
41 | } else {
42 | this.upring.requestp({ key, value, ns: this.ns, cmd: 'put' })
43 | }
44 | }
45 |
46 | UpRingKV.prototype.get = function (key, cb) {
47 | if (!this.upring.isReady) {
48 | this.upring.once('up', this.get.bind(this, key, cb))
49 | return
50 | }
51 |
52 | if (typeof cb === 'function') {
53 | this.upring.request({ key, ns: this.ns, cmd: 'get' }, function (err, result) {
54 | cb(err, result ? result.value : null)
55 | })
56 | } else {
57 | return new Promise((resolve, reject) => {
58 | this.upring.requestp({ key, ns: this.ns, cmd: 'get' })
59 | .then(result => resolve(result.value))
60 | .catch(err => reject(err))
61 | })
62 | }
63 | }
64 |
65 | UpRingKV.prototype.liveUpdates = function (key) {
66 | const result = nes.obj((done) => {
67 | this.upring.request({ key, ns: this.ns, cmd: 'liveUpdates' }, function (err, res) {
68 | if (err) {
69 | done(err)
70 | return
71 | }
72 |
73 | result.emit('newStream')
74 |
75 | done(null, res.streams.updates)
76 | })
77 | })
78 |
79 | return result
80 | }
81 |
--------------------------------------------------------------------------------
/lib/commands.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const eos = require('end-of-stream')
4 | const Readable = require('readable-stream').Readable
5 |
6 | function load (kv) {
7 | const upring = kv.upring
8 | const db = new Map()
9 | const streams = new Map()
10 | const ns = kv.ns
11 |
12 | upring.add(`ns:${ns},cmd:put`, put)
13 | upring.add(`ns:${ns},cmd:liveUpdates`, liveUpdates)
14 | upring.add(`ns:${ns},cmd:get`, get)
15 |
16 | function setupTracker (entry, reply, sendData) {
17 | if (entry.hasTracker) {
18 | reply()
19 | return
20 | }
21 |
22 | const key = entry.key
23 |
24 | entry.hasTracker = true
25 | entry.hasReplicator = false
26 |
27 | upring.log.debug({ key }, 'configuring tracker')
28 |
29 | const dest = upring._hashring.next(key)
30 | const tracker = upring.track(key, { replica: true })
31 |
32 | process.nextTick(function () {
33 | tracker.on('replica', sendData)
34 | })
35 | tracker.on('move', function (peer) {
36 | if (peer) {
37 | sendData(peer)
38 | }
39 | entry.hasTracker = false
40 | const rs = streams.get(key) || []
41 | rs.forEach((stream) => {
42 | stream.push(null)
43 | })
44 | streams.delete(key)
45 | setTimeout(function () {
46 | if (!entry.hasReplicator && !entry.hasTracker) {
47 | db.delete(key)
48 | }
49 | }, 30000).unref()
50 | })
51 |
52 | if (dest) {
53 | sendData(dest, reply)
54 | return false
55 | }
56 |
57 | return true
58 | }
59 |
60 | function setupReplicator (entry, sendData) {
61 | const key = entry.key
62 | entry.hasReplicator = true
63 | upring.log.debug({ key }, 'configuring replicator')
64 | upring.replica(key, function () {
65 | entry.hasReplicator = false
66 | setupTracker(entry, noop, sendData)
67 | })
68 | }
69 |
70 | function Entry (key) {
71 | this.key = key
72 | this.hasTracker = false
73 | this.hasReplicator = false
74 | this.value = null
75 | }
76 |
77 | function put (req, reply) {
78 | const key = req.key
79 | const sendData = genSendData(key)
80 | const entry = db.get(key) || new Entry(key)
81 | var needReply = true
82 |
83 | entry.value = req.value
84 | db.set(key, entry)
85 |
86 | if (upring.allocatedToMe(key)) {
87 | if (!entry.hasTracker) {
88 | needReply = setupTracker(entry, reply, sendData)
89 | }
90 | } else {
91 | if (!entry.hasReplicator) {
92 | setupReplicator(entry, sendData)
93 | }
94 | }
95 |
96 | upring.log.debug({ key, value: req.value }, 'setting data')
97 |
98 | if (needReply) {
99 | reply()
100 | }
101 |
102 | const array = streams.get(key)
103 |
104 | if (array) {
105 | array.forEach((stream) => stream.push(req.value))
106 | }
107 | }
108 |
109 | function genSendData (key) {
110 | return sendData
111 |
112 | function sendData (peer, cb) {
113 | if (typeof cb !== 'function') {
114 | cb = retry
115 | }
116 |
117 | const entry = db.get(key)
118 | if (!entry) {
119 | cb()
120 | return
121 | }
122 |
123 | const req = {
124 | ns: ns,
125 | cmd: 'put',
126 | key: entry.key,
127 | value: entry.value
128 | }
129 |
130 | upring.peerConn(peer).request(req, function (err) {
131 | if (err) {
132 | cb(err)
133 | return
134 | }
135 |
136 | upring.log.debug({ key, value: entry.value, to: peer }, 'replicated key')
137 |
138 | cb()
139 | })
140 | }
141 |
142 | function retry (err) {
143 | if (err) {
144 | upring.log.error(err)
145 | const dest = upring._hashring.next(key)
146 | if (!dest) {
147 | return upring.emit('error', err)
148 | }
149 |
150 | sendData(dest)
151 | }
152 | }
153 | }
154 |
155 | function get (req, reply) {
156 | const entry = db.get(req.key)
157 | const key = req.key
158 | req.skipList = req.skipList || []
159 | req.skipList.push(upring.whoami())
160 | const dest = upring._hashring.next(key, req.skipList)
161 |
162 | if ((entry && entry.value) || !dest) {
163 | reply(null, { key, value: entry ? entry.value : undefined })
164 | } else {
165 | upring.log.debug({ key }, 'data not found, checking if we are in the middle of a migration')
166 | upring.peerConn(dest)
167 | .request(req, function (err, res) {
168 | if (err) {
169 | reply(err)
170 | return
171 | }
172 |
173 | const entry = db.get(key)
174 |
175 | if (res && res.value && !entry && upring.allocatedToMe(key)) {
176 | upring.log.debug({ key }, 'set data because of migration')
177 | put({
178 | ns: ns,
179 | cmd: 'put',
180 | key,
181 | value: res.value
182 | }, function (err) {
183 | if (err) {
184 | reply(err)
185 | return
186 | }
187 |
188 | reply(null, res)
189 | })
190 |
191 | return
192 | }
193 |
194 | reply(null, res)
195 | })
196 | }
197 | }
198 |
199 | function liveUpdates (req, reply) {
200 | var array = streams.get(req.key)
201 |
202 | if (!array) {
203 | array = []
204 | streams.set(req.key, array)
205 | }
206 |
207 | const updates = new Readable({
208 | objectMode: true
209 | })
210 |
211 | updates._read = function () {}
212 |
213 | eos(updates, function () {
214 | array.splice(array.indexOf(updates), 1)
215 | })
216 |
217 | array.push(updates)
218 |
219 | const entry = db.get(req.key)
220 | if (entry && entry.hasTracker) {
221 | updates.push(entry.value)
222 | }
223 |
224 | reply(null, { streams: { updates } })
225 | }
226 | }
227 |
228 | function noop () {}
229 |
230 | module.exports = load
231 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "upring-kv",
3 | "version": "0.5.0",
4 | "description": "Key-Value store plugin for UpRing",
5 | "main": "kv.js",
6 | "scripts": {
7 | "test": "standard | snazzy && tap test/*test.js",
8 | "coverage": "tap --cov --coverage-report=html test/*.test.js",
9 | "coveralls": "tap test/*test.js --cov --coverage-report=text-lcov | coveralls"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/upringjs/upring-kv.git"
14 | },
15 | "keywords": [
16 | "upring",
17 | "consistent",
18 | "hashring",
19 | "key",
20 | "value"
21 | ],
22 | "author": "Matteo Collina ",
23 | "contributors": [
24 | {
25 | "name": "Tomas Della Vedova",
26 | "url": "http://delved.org"
27 | }
28 | ],
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/upringjs/upring-kv/issues"
32 | },
33 | "homepage": "https://github.com/upringjs/upring-kv#readme",
34 | "devDependencies": {
35 | "coveralls": "^3.0.0",
36 | "pre-commit": "^1.2.2",
37 | "snazzy": "^7.0.0",
38 | "standard": "^10.0.3",
39 | "tap": "^11.0.0",
40 | "upring": "^0.22.0"
41 | },
42 | "dependencies": {
43 | "clone": "^2.1.1",
44 | "end-of-stream": "^1.4.0",
45 | "never-ending-stream": "^2.0.0",
46 | "readable-stream": "^2.3.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/async-await.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function testAsyncAwait (instance, t) {
4 | t.test('asycn await', async t => {
5 | t.plan(1)
6 | try {
7 | await instance.kv.put('async', 'await')
8 | } catch (err) {
9 | t.fail(err)
10 | }
11 |
12 | try {
13 | const value = await instance.kv.get('async')
14 | t.strictEqual(value, 'await')
15 | } catch (err) {
16 | t.fail(err)
17 | }
18 | })
19 | }
20 |
21 | module.exports = testAsyncAwait
22 |
--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const upring = require('upring')
4 | const upringKV = require('..')
5 |
6 | function build (main) {
7 | const base = []
8 |
9 | if (main && main.whoami) {
10 | base.push(main.whoami())
11 | }
12 |
13 | const instance = upring({
14 | logLevel: 'fatal',
15 | base: base,
16 | hashring: {
17 | joinTimeout: 200,
18 | replicaPoints: 10
19 | }
20 | })
21 |
22 | instance.use(upringKV)
23 |
24 | return instance
25 | }
26 |
27 | module.exports.build = build
28 |
--------------------------------------------------------------------------------
/test/multi-replica-die-twice.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const build = require('./helper').build
4 | const t = require('tap')
5 | const maxInt = Math.pow(2, 32) - 1
6 |
7 | t.plan(16)
8 |
9 | var a = build()
10 | t.tearDown(a.close.bind(a))
11 |
12 | var c
13 | var b
14 | var key
15 |
16 | a.on('up', function () {
17 | t.pass('a up')
18 |
19 | join(a, function (instance) {
20 | t.pass('b up')
21 | b = instance
22 |
23 | key = 'hello'
24 |
25 | for (var i = 0; i < maxInt && !a.allocatedToMe(key); i += 1) {
26 | key = 'hello' + i
27 | }
28 | // key is now allocated to a
29 |
30 | a.kv.put(key, 'world', function (err) {
31 | t.error(err)
32 |
33 | b.kv.get(key, function (err, value) {
34 | t.error(err)
35 | t.equal(value, 'world')
36 |
37 | afterDown(a, b, function () {
38 | t.pass('a closed')
39 |
40 | join(b, function (instance) {
41 | t.pass('c joined')
42 | c = instance
43 |
44 | c.kv.get(key, function (err, value) {
45 | t.error(err)
46 | t.equal(value, 'world')
47 |
48 | closeBAndGet()
49 | })
50 | })
51 | })
52 | })
53 | })
54 | })
55 | })
56 |
57 | function afterDown (prev, next, cb) {
58 | var count = 0
59 | next.once('peerDown', function () {
60 | if (++count === 2) {
61 | cb()
62 | }
63 | })
64 | prev.close(function () {
65 | if (++count === 2) {
66 | cb()
67 | }
68 | })
69 | }
70 |
71 | function join (main, cb) {
72 | const instance = build(main)
73 |
74 | t.tearDown(instance.close.bind(instance))
75 |
76 | instance.on('up', function () {
77 | cb(instance)
78 | })
79 | }
80 |
81 | function closeBAndGet () {
82 | afterDown(b, c, function () {
83 | t.pass('b closed')
84 |
85 | c.kv.get(key, function (err, value) {
86 | t.error(err)
87 | t.equal(value, 'world')
88 |
89 | join(c, function (d) {
90 | t.pass('d up')
91 | setTimeout(function () {
92 | afterDown(c, d, function () {
93 | t.pass('c closed')
94 |
95 | d.kv.get(key, function (err, value) {
96 | t.error(err)
97 | t.equal(value, 'world')
98 | })
99 | })
100 | }, 1000)
101 | })
102 | })
103 | })
104 | }
105 |
--------------------------------------------------------------------------------
/test/multi.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const build = require('./helper').build
4 | const t = require('tap')
5 | const test = t.test
6 | const maxInt = Math.pow(2, 32) - 1
7 |
8 | test('get and put', function (t) {
9 | t.plan(8)
10 |
11 | const a = build()
12 | t.tearDown(a.close.bind(a))
13 |
14 | a.on('up', function () {
15 | t.pass('a up')
16 | const b = build(a)
17 |
18 | t.tearDown(b.close.bind(b))
19 |
20 | b.on('up', function () {
21 | t.pass('b up')
22 | var key = 'hello'
23 |
24 | // this is the instance upring, b
25 | for (var i = 0; i < maxInt && !this.allocatedToMe(key); i += 1) {
26 | key = 'hello' + i
27 | }
28 | // key is now allocated to b
29 |
30 | a.kv.put(key, 'world', function (err) {
31 | t.error(err)
32 |
33 | b.kv.get(key, function (err, value) {
34 | t.error(err)
35 | t.equal(value, 'world')
36 |
37 | b.close(function () {
38 | t.pass('closed')
39 |
40 | a.kv.get(key, function (err, value) {
41 | t.error(err)
42 | t.equal(value, 'world')
43 | })
44 | })
45 | })
46 | })
47 | })
48 | })
49 | })
50 |
51 | test('get empty', function (t) {
52 | t.plan(7)
53 |
54 | const a = build()
55 | t.tearDown(a.close.bind(a))
56 |
57 | a.on('up', function () {
58 | t.pass('a up')
59 | const b = build(a)
60 |
61 | t.tearDown(b.close.bind(b))
62 |
63 | b.on('up', function () {
64 | t.pass('b up')
65 |
66 | b.kv.get('hello', function (err, res) {
67 | t.error(err)
68 | t.equal(res, undefined)
69 |
70 | const c = build(a)
71 |
72 | t.tearDown(c.close.bind(c))
73 |
74 | c.on('up', function () {
75 | t.pass('c up')
76 |
77 | c.kv.get('hello', function (err, res) {
78 | t.error(err)
79 | t.equal(res, undefined)
80 | })
81 | })
82 | })
83 | })
84 | })
85 | })
86 |
87 | test('moving data', function (t) {
88 | t.plan(13)
89 |
90 | const a = build()
91 | t.tearDown(a.close.bind(a))
92 |
93 | a.on('up', function () {
94 | t.pass('a up')
95 | const b = build()
96 |
97 | t.tearDown(b.close.bind(b))
98 |
99 | b.on('up', function () {
100 | t.pass('b up')
101 |
102 | var key = 'hello'
103 |
104 | for (var i = 0; i < maxInt && !a.allocatedToMe(key); i += 1) {
105 | key = 'hello' + i
106 | }
107 | // key is now allocated to a
108 |
109 | a.kv.put(key, 'world', function (err) {
110 | t.error(err)
111 | b.join(a.whoami(), function () {
112 | t.pass('b joined')
113 |
114 | b.kv.get(key, function (err, value) {
115 | t.error(err)
116 | t.equal(value, 'world')
117 |
118 | var c
119 |
120 | b.once('peerDown', function (peer) {
121 | c = build(b)
122 |
123 | t.tearDown(c.close.bind(c))
124 |
125 | b.on('peerUp', function (peer) {
126 | c.ready(() => {
127 | c.kv.get(key, function (err, value) {
128 | t.error(err)
129 | t.equal(value, 'world')
130 |
131 | closeAndGet()
132 | })
133 | })
134 | })
135 |
136 | c.on('up', function () {
137 | t.pass('c joined')
138 | })
139 | })
140 |
141 | function closeAndGet () {
142 | b.close(function () {
143 | t.pass('b closed')
144 |
145 | c.kv.get(key, function (err, value) {
146 | t.error(err)
147 | t.equal(value, 'world')
148 | })
149 | })
150 | }
151 |
152 | a.close(function () {
153 | t.pass('a closed')
154 | })
155 | })
156 | })
157 | })
158 | })
159 | })
160 | })
161 |
162 | test('liveUpdates', function (t) {
163 | t.plan(9)
164 |
165 | const a = build()
166 | t.tearDown(a.close.bind(a))
167 |
168 | a.on('up', function () {
169 | t.pass('a up')
170 | const b = build(a)
171 |
172 | t.tearDown(b.close.bind(b))
173 |
174 | b.on('up', function () {
175 | t.pass('b up')
176 | var key = 'bbb'
177 |
178 | // this is the instance upring, b
179 | for (var i = 0; i < maxInt && !this.allocatedToMe(key); i += 1) {
180 | key = 'bbb' + i
181 | }
182 | // key is now allocated to b
183 |
184 | a.kv.put(key, 'world', function (err) {
185 | t.error(err)
186 |
187 | const stream = a.kv.liveUpdates(key)
188 | const expected = ['world', 'matteo', 'luca']
189 |
190 | stream.on('data', function (chunk) {
191 | t.deepEqual(chunk, expected.shift(), 'chunk matches')
192 | })
193 |
194 | stream.on('error', function () {
195 | t.fail('no error in stream')
196 | })
197 |
198 | stream.once('newStream', function () {
199 | b.kv.put(key, 'matteo', function (err) {
200 | t.error(err)
201 |
202 | stream.once('data', function () {
203 | b.close(function () {
204 | t.pass('closed')
205 |
206 | a.kv.put(key, 'luca', function (err) {
207 | t.error(err)
208 | })
209 | })
210 | })
211 | })
212 | })
213 | })
214 | })
215 | })
216 | })
217 |
--------------------------------------------------------------------------------
/test/one.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const build = require('./helper').build
4 | const t = require('tap')
5 | const test = t.test
6 |
7 | const instance = build()
8 |
9 | t.tearDown(instance.close.bind(instance))
10 |
11 | test('get empty', function (t) {
12 | t.plan(2)
13 |
14 | instance.ready(() => {
15 | instance.kv.get('hello', function (err, value) {
16 | t.error(err)
17 | t.equal(value, undefined)
18 | })
19 | })
20 | })
21 |
22 | test('get and put', function (t) {
23 | t.plan(3)
24 |
25 | instance.kv.put('hello', 'world', function (err) {
26 | t.error(err)
27 |
28 | instance.kv.get('hello', function (err, value) {
29 | t.error(err)
30 | t.equal(value, 'world')
31 | })
32 | })
33 | })
34 |
35 | test('get and JS objects', function (t) {
36 | t.plan(3)
37 |
38 | instance.kv.put('hello', { a: 42 }, function (err) {
39 | t.error(err)
40 |
41 | instance.kv.get('hello', function (err, value) {
42 | t.error(err)
43 | t.deepEqual(value, { a: 42 })
44 | })
45 | })
46 | })
47 |
48 | test('clones the object', function (t) {
49 | t.plan(3)
50 |
51 | const obj = { a: 42 }
52 | instance.kv.put('hello', obj, function (err) {
53 | t.error(err)
54 |
55 | instance.kv.get('hello', function (err, value) {
56 | t.error(err)
57 | t.notEqual(value, obj)
58 | })
59 | })
60 | })
61 |
62 | test('liveUpdates', function (t) {
63 | t.plan(5)
64 |
65 | const key = 'aaa'
66 | const expected = [
67 | 'world',
68 | 'matteo'
69 | ]
70 |
71 | const stream = instance.kv.liveUpdates(key)
72 | .on('data', function (data) {
73 | t.deepEqual(data, expected.shift())
74 | if (expected.length === 0) {
75 | stream.destroy()
76 | setImmediate(t.pass.bind(t), 'destroyed')
77 | }
78 | })
79 |
80 | t.tearDown(stream.destroy.bind(stream))
81 |
82 | instance.kv.put(key, 'world', function (err) {
83 | t.error(err)
84 | instance.kv.put(key, 'matteo', function (err) {
85 | t.error(err)
86 | })
87 | })
88 | })
89 |
90 | test('liveUpdates double', function (t) {
91 | t.plan(8)
92 |
93 | const key = 'hello2'
94 |
95 | const expected1 = [
96 | 'world',
97 | 'matteo'
98 | ]
99 |
100 | const expected2 = [
101 | 'world',
102 | 'matteo'
103 | ]
104 |
105 | const stream1 = instance.kv.liveUpdates(key)
106 | .on('data', function (data) {
107 | t.deepEqual(data, expected1.shift())
108 | if (expected1.length === 0) {
109 | stream1.destroy()
110 | setImmediate(t.pass.bind(t), 'destroyed')
111 | }
112 | })
113 |
114 | t.tearDown(stream1.destroy.bind(stream1))
115 |
116 | const stream2 = instance.kv.liveUpdates(key)
117 | .on('data', function (data) {
118 | t.deepEqual(data, expected2.shift())
119 | if (expected2.length === 0) {
120 | stream2.destroy()
121 | setImmediate(t.pass.bind(t), 'destroyed')
122 | }
123 | })
124 |
125 | t.tearDown(stream2.destroy.bind(stream2))
126 |
127 | instance.kv.put(key, 'world', function (err) {
128 | t.error(err)
129 | instance.kv.put(key, 'matteo', function (err) {
130 | t.error(err)
131 | })
132 | })
133 | })
134 |
135 | test('liveUpdates after', function (t) {
136 | t.plan(5)
137 |
138 | const expected = [
139 | 'world',
140 | 'matteo'
141 | ]
142 |
143 | instance.kv.put('hello', 'world', function (err) {
144 | t.error(err)
145 |
146 | const stream = instance.kv.liveUpdates('hello')
147 | .on('data', function (data) {
148 | t.deepEqual(data, expected.shift())
149 | if (expected.length === 0) {
150 | stream.destroy()
151 | setImmediate(t.pass.bind(t), 'destroyed')
152 | }
153 | })
154 |
155 | t.tearDown(stream.destroy.bind(stream))
156 |
157 | instance.kv.put('hello', 'matteo', function (err) {
158 | t.error(err)
159 | })
160 | })
161 | })
162 |
163 | if (Number(process.versions.node[0]) >= 8) {
164 | require('./async-await')(instance, t)
165 | } else {
166 | t.pass('Skip because Node version < 8')
167 | t.end()
168 | }
169 |
--------------------------------------------------------------------------------