├── .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 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](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 | --------------------------------------------------------------------------------