├── LICENSE-APACHE ├── LICENSE-PARITY ├── bin └── cmd.js ├── config.js ├── contributing.md ├── example └── kv.js ├── index.js ├── lib ├── hash-table.js ├── hash.js ├── path.js └── rslice.js ├── package.json ├── readme.md └── test └── kv.js /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-PARITY: -------------------------------------------------------------------------------- 1 | The Parity Public License 6.0.0 2 | 3 | Contributor: James Halliday 4 | 5 | Source Code: https://github.com/peermaps/bitfield-db 6 | 7 | This license lets you use and share this software for free, as 8 | long as you contribute software you make with it. Specifically: 9 | 10 | If you follow the rules below, you may do everything with this 11 | software that would otherwise infringe either the contributor's 12 | copyright in it, any patent claim the contributor can license, 13 | or both. 14 | 15 | 1. Contribute changes and additions you make to this software. 16 | 17 | 2. If you combine this software with other software, contribute 18 | that other software. 19 | 20 | 3. Contribute software you develop, deploy, monitor, or run with 21 | this software. 22 | 23 | 4. Ensure everyone who gets a copy of this software from you, in 24 | source code or any other form, gets the text of this license 25 | and the contributor and source code lines above. 26 | 27 | 5. Do not make any legal claim against anyone accusing this 28 | software, with or without changes, alone or with other 29 | software, of infringing any patent claim. 30 | 31 | To contribute software, publish all its source code, in the 32 | preferred form for making changes, through a freely accessible 33 | distribution system widely used for similar source code, and 34 | license contributions not already licensed to the public on terms 35 | as permissive as this license accordingly. 36 | 37 | You are excused for unknowingly breaking 1, 2, or 3 if you 38 | contribute as required, or stop doing anything requiring this 39 | license, within 30 days of learning you broke the rule. 40 | 41 | **As far as the law allows, this software comes as is, without 42 | any warranty, and the contributor will not be liable to anyone 43 | for any damages related to this software or this license, for any 44 | kind of legal claim.** 45 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var argv = require('minimist')(process.argv.slice(2), { 3 | alias: { d: 'datadir', c: 'config' } 4 | }) 5 | if (argv._[0] === 'id') { 6 | var KV = require('../') 7 | var kv = new KV({ 8 | network: require('peermq/network'), 9 | storage: argv.datadir 10 | }) 11 | kv.getId(function (err, id) { 12 | if (err) console.error(err) 13 | else console.log(id.toString('hex')) 14 | }) 15 | } else if (argv._[0] === 'init') { 16 | var Config = require('../config') 17 | console.log(new Config({ 18 | capacities: argv.capacity, 19 | writers: [].concat(argv.writer) 20 | }).serialize()) 21 | } else if (argv._[0] === 'get') { 22 | var kv = getKV() 23 | kv.connect() 24 | kv.get(argv._[1], function (err, value) { 25 | if (err) console.error(err) 26 | else console.log(value) 27 | }) 28 | } else if (argv._[0] === 'write') { 29 | var kv = getKV() 30 | Object.entries(argv.put || {}).forEach(([key,value]) => { 31 | kv.put(key, value) 32 | }) 33 | Object.keys(argv.del || {}).forEach(key => { 34 | kv.del(key) 35 | }) 36 | kv.flush(function (err) { 37 | if (err) console.error(err) 38 | }) 39 | } else if (argv._[0] === 'listen') { 40 | var kv = getKV() 41 | kv.listen(function (err, pubKey, server) { 42 | if (err) console.error(err) 43 | else console.log(pubKey.toString('hex')) 44 | }) 45 | } else if (argv._[0] === 'connect') { 46 | getKV().connect() 47 | } 48 | 49 | function getKV () { 50 | var KV = require('../') 51 | var Config = require('../config') 52 | var fs = require('fs') 53 | return new KV({ 54 | network: require('peermq/network'), 55 | storage: argv.datadir, 56 | config: Config.parse(fs.readFileSync(argv.config, 'utf8')) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var RS = require('random-slicing') 2 | 3 | module.exports = Config 4 | 5 | function Config (opts) { 6 | if (!(this instanceof Config)) return new Config(opts) 7 | if (opts.rs) { 8 | this._rs = opts.rs 9 | } else if (opts.bins) { 10 | this._rs = new RS(opts.bins) 11 | } else if (opts.capacities) { 12 | this._rs = new RS 13 | this._rs.set(opts.capacities) 14 | } 15 | this._writers = opts.writers 16 | } 17 | 18 | Config.parse = function (str) { 19 | return new Config(JSON.parse(str)) 20 | } 21 | 22 | Config.prototype.update = function (opts) { 23 | if (opts.capacities) this._rs.update(capacities) 24 | if (opts.writers) this._writers = opts.writers 25 | } 26 | 27 | Config.prototype.addWriter = function (key) { 28 | this._writers.push(key) 29 | } 30 | 31 | Config.prototype.removeWriter = function (key) { 32 | var ix = this._writers.indexOf(key) 33 | if (ix >= 0) this._writers.splice(ix,1) 34 | } 35 | 36 | Config.prototype.serialize = function () { 37 | return '{"writers":' + JSON.stringify(this._writers) 38 | + ',"bins":' + this._rs.serialize() + '}' 39 | } 40 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | By contributing, you agree to release your modifications under the Apache 2.0 2 | license (see the file LICENSE-APACHE). 3 | -------------------------------------------------------------------------------- /example/kv.js: -------------------------------------------------------------------------------- 1 | var argv = require('minimist')(process.argv.slice(2), { 2 | alias: { d: 'datadir' } 3 | }) 4 | 5 | var kv = require('../')({ 6 | network: require('peermq/network'), 7 | bins: argv.bins, 8 | storage: argv.datadir 9 | }) 10 | 11 | if (argv._[0] === 'id') { 12 | kv.getId(function (err, id) { 13 | if (err) console.error(err) 14 | else console.log(id.toString('hex')) 15 | }) 16 | } else if (argv._[0] === 'add-peer') { 17 | argv._.slice(1).forEach(function (peer) { 18 | kv.addPeer(peer) 19 | }) 20 | } else if (argv._[0] === 'listen') { 21 | kv.listen(function (err, pubKey) { 22 | if (err) console.error(err) 23 | else console.log(pubKey.toString('hex')) 24 | }) 25 | } else if (argv._[0] === 'connect') { 26 | kv.connect() 27 | } else { 28 | ;[].concat(argv.put).forEach(s => { 29 | var [key,value] = s.split('=') 30 | kv.put(key, value) 31 | }) 32 | ;[].concat(argv.del).forEach(key => { 33 | kv.del(key) 34 | }) 35 | kv.flush(function (err) { 36 | if (err) console.error(err) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var peermq = require('peermq') 2 | var hypercore = require('hypercore') 3 | var hypertrie = require('hypertrie') 4 | var { EventEmitter } = require('events') 5 | var { Transform } = require('readable-stream') 6 | var sodium = require('sodium-universal') 7 | var RS = require('random-slicing') 8 | var hash = require('./lib/hash.js') 9 | var HashTable = require('./lib/hash-table.js') 10 | var varint = require('varint') 11 | var pump = require('pump') 12 | var { createHash } = require('crypto') 13 | var path = require('path') 14 | 15 | var PUT = 1, DEL = 2 16 | var TOPIC_PREFIX = Buffer.from('peermq!') 17 | var RECEIPT_SET_FROM = 1, RECEIPT_REQ = 2, 18 | RECEIPT_RES_OK = 3, RECEIPT_RES_FAIL = 4 19 | 20 | module.exports = KV 21 | 22 | function KV (opts) { 23 | var self = this 24 | if (!(self instanceof KV)) return new KV(opts) 25 | self._storage = opts.storage 26 | if (typeof self._storage !== 'string' 27 | && typeof self._storage !== 'function') { 28 | throw new Error('storage must be a string path or function.' 29 | + ' received: ' + typeof self._storage) 30 | } 31 | self._network = opts.network 32 | self._mq = peermq({ 33 | topic: function (buf) { 34 | var h = createHash('sha256') 35 | h.update(TOPIC_PREFIX) 36 | h.update(buf) 37 | return h.digest() 38 | }, 39 | network: self._network, 40 | storage: function (name) { 41 | return self._getStorage('mq',name) 42 | } 43 | }) 44 | if (opts.config) { 45 | self.setWriters(opts.config._writers, function (err) { 46 | if (err) self.emit('error', err) 47 | }) 48 | self._rs = opts.config._rs 49 | } else if (typeof opts.bins === 'string') { 50 | self._rs = RS.parse(opts.bins) 51 | } else if (opts.bins && typeof opts.bins.getBins === 'function') { 52 | self._rs = opts.bins 53 | } else { 54 | self._rs = new RS(opts.bins) 55 | } 56 | self._table = new HashTable(self._rs.getBins()) 57 | self._blocks = {} 58 | self._connections = { trie: {}, mq: {} } 59 | self._core = null 60 | self._trieCores = {} 61 | self._tries = {} 62 | self._connectReceipts = {} 63 | } 64 | KV.prototype = Object.create(EventEmitter.prototype) 65 | 66 | KV.prototype._getStorage = function (prefix, name) { 67 | if (typeof this._storage === 'string') { 68 | return path.join(this._storage, prefix, name) 69 | } else if (typeof this._storage === 'function') { 70 | return this._storage(path.join(prefix, name)) 71 | } else { 72 | throw new Error('unsupported storage type ' + typeof this._storage) 73 | } 74 | } 75 | 76 | KV.prototype._getStorageFn = function (prefix, name) { 77 | var self = this 78 | return function (x) { 79 | return self._getStorage(prefix, path.join(name, x)) 80 | } 81 | } 82 | 83 | KV.prototype.setWriters = function (writers, cb) { 84 | var self = this 85 | if (!cb) cb = noop 86 | self._mq.getPeers(function (err, peers) { 87 | if (err) return cb(err) 88 | var add = [], remove = [] 89 | for (var i = 0; i < writers.length; i++) { 90 | var w = writers[i] 91 | if (peers.indexOf(w) < 0) { 92 | add.push(w) 93 | } 94 | } 95 | for (var i = 0; i < peers.length; i++) { 96 | var p = peers[i] 97 | if (writers.indexOf(p) < 0) { 98 | remove.push(p) 99 | } 100 | } 101 | self._mq.addPeers(add, function (err) { 102 | if (err) return cb(err) 103 | self._mq.removePeers(remove, function (err) { 104 | if (err) cb(err) 105 | else cb() 106 | }) 107 | }) 108 | }) 109 | } 110 | 111 | KV.prototype.getWriters = function (cb) { 112 | this._mq.getPeers(cb) 113 | } 114 | 115 | KV.prototype.addWriter = function (peer, cb) { 116 | this._mq.addPeer(peer, cb) 117 | } 118 | 119 | KV.prototype.removeWriter = function (peer, cb) { 120 | this._mq.removePeer(peer, cb) 121 | } 122 | 123 | KV.prototype.setCapacities = function (update) { 124 | this._rs.set(update) 125 | this._table.update(this._rs.getBins()) 126 | } 127 | 128 | KV.prototype.setConfig = function (config, cb) { 129 | this._rs = config._rs 130 | this._table.update(this._rs.getBins()) 131 | this.setWriters(config._writers, cb) 132 | } 133 | 134 | KV.prototype.get = function (key, opts, cb) { 135 | var self = this 136 | if (typeof opts === 'function') { 137 | cb = opts 138 | opts = {} 139 | } 140 | if (!opts) opts = {} 141 | if (!cb) cb = noop 142 | var hkey = hash(sodium, [key]) 143 | var nodeKey = this._table.lookup(hkey) 144 | var k = nodeKey.toString('hex') 145 | var trie = this._tries[k] 146 | if (trie) get(trie) 147 | else this.once('_trie!'+k, get) 148 | 149 | function get (trie) { 150 | trie.get(key, cb) 151 | } 152 | } 153 | 154 | KV.prototype.put = function (key, value) { 155 | if (typeof key === 'string') key = Buffer.from(key) 156 | if (typeof value === 'string') value = Buffer.from(value) 157 | var hkey = hash(sodium, [key]) 158 | var nodeKey = this._table.lookup(hkey) 159 | if (!this._blocks[nodeKey]) this._blocks[nodeKey] = [] 160 | this._blocks[nodeKey].push({ type: 'put', key, value }) 161 | } 162 | 163 | KV.prototype.del = function (key) { 164 | var hkey = hash(sodium, [key]) 165 | var nodeKey = this._table.lookup(hkey) 166 | if (!this._blocks[nodeKey]) this._blocks[nodeKey] = [] 167 | this._blocks[nodeKey].push({ type: 'del', key, value }) 168 | } 169 | 170 | KV.prototype.flush = function (opts, cb) { 171 | if (typeof opts === 'function') { 172 | cb = opts 173 | opts = {} 174 | } 175 | if (!opts) opts = {} 176 | if (!cb) cb = noop 177 | var self = this 178 | var finished = false 179 | var pending = 1 180 | var seqs = {} 181 | Object.keys(self._blocks).forEach(function (key) { 182 | pending++ 183 | var len = 0 184 | for (var i = 0; i < self._blocks[key].length; i++) { 185 | var b = self._blocks[key][i] 186 | len += 1 187 | len += varint.encodingLength(b.key.length) 188 | len += b.key.length 189 | len += varint.encodingLength(b.value.length) 190 | len += b.value.length 191 | } 192 | var message = Buffer.alloc(len) 193 | var offset = 0 194 | for (var i = 0; i < self._blocks[key].length; i++) { 195 | var b = self._blocks[key][i] 196 | if (b.type === 'put') { 197 | message[offset++] = PUT 198 | } else if (b.type === 'del') { 199 | message[offset++] = DEL 200 | } 201 | var nkey = varint.encode(b.key.length) 202 | for (var j = 0; j < nkey.length; j++) { 203 | message[offset++] = nkey[j] 204 | } 205 | for (var j = 0; j < b.key.length; j++) { 206 | message[offset++] = b.key[j] 207 | } 208 | var vkey = varint.encode(b.value.length) 209 | for (var j = 0; j < vkey.length; j++) { 210 | message[offset++] = vkey[j] 211 | } 212 | for (var j = 0; j < b.value.length; j++) { 213 | message[offset++] = b.value[j] 214 | } 215 | } 216 | self._mq.send({ to: key, message }, function (err, seq) { 217 | if (!err) seqs[key] = seq 218 | check(err) 219 | }) 220 | }) 221 | check() 222 | function check (err) { 223 | if (finished) return 224 | if (err) { 225 | finished = true 226 | return cb(err) 227 | } 228 | if (--pending === 0) return wait() 229 | } 230 | function wait () { 231 | pending = 1 232 | Object.keys(seqs).forEach(function (key) { 233 | var seq = seqs[key] 234 | if (!self._connectReceipts[key]) self._connectReceipts[key] = {} 235 | if (!self._connectReceipts[key][seq]) self._connectReceipts[key][seq] = [] 236 | pending++ 237 | var core = self._trieCores[key] 238 | self._connectReceipts[key][seq].push(f) 239 | function f (err, coreLen) { 240 | if (finished) return 241 | if (err) { 242 | finished = true 243 | return cb(err) 244 | } 245 | core.update(coreLen, function (err) { 246 | if (finished) return 247 | if (err) { 248 | finished = true 249 | return cb(err) 250 | } 251 | if (--pending === 0) return cb() 252 | }) 253 | } 254 | }) 255 | if (--pending === 0) return cb() 256 | } 257 | } 258 | 259 | KV.prototype.connect = function () { 260 | var self = this 261 | self._mq.getKeyPairs(function (err, kp) { 262 | if (err) return self.emit('error', err) 263 | var pubKey = kp.hypercore.publicKey 264 | var bins = self._rs.getBins() 265 | Object.keys(bins).forEach(function (key) { 266 | var m = self._connections.mq[key] = self._mq.connect(key) 267 | var n = 0 268 | m.on('ack', function (ack) { 269 | ext.send(receiptReq(n++, ack)) 270 | }) 271 | var bkey = Buffer.from(key, 'hex') 272 | var c = self._connections.trie[key] = self._network.connect(bkey) 273 | self._trieCores[key] = hypercore(self._getStorageFn('kv',key), bkey) 274 | var trie = hypertrie(null, { feed: self._trieCores[key] }) 275 | self._tries[key] = hypertrie(null, { feed: self._trieCores[key] }) 276 | 277 | var r = self._trieCores[key].replicate(true, { 278 | sparse: true, 279 | live: true 280 | }) 281 | var ext = r.registerExtension('kvswarm', { 282 | encoding: 'binary', 283 | onmessage: function (msg, peer) { 284 | if (msg[0] === RECEIPT_RES_FAIL || msg[0] === RECEIPT_RES_OK) { 285 | var seq = varint.decode(msg,1) 286 | if (!self._connectReceipts[key]) return 287 | if (!self._connectReceipts[key][seq]) return 288 | var rs = self._connectReceipts[key][seq] 289 | if (msg[0] === RECEIPT_RES_FAIL) { 290 | var err = new Error('receipt failed') 291 | for (var i = 0; i < rs.length; i++) { 292 | rs[i](err) 293 | } 294 | } else if (msg[0] === RECEIPT_RES_OK) { 295 | var coreLen = varint.decode(msg,1+varint.encodingLength(seq)) 296 | for (var i = 0; i < rs.length; i++) { 297 | rs[i](null, coreLen) 298 | } 299 | } 300 | delete self._connectReceipts[key][seq] 301 | if (Object.keys(self._connectReceipts[key]).length === 0) { 302 | delete self._connectReceipts[key] 303 | } 304 | } 305 | } 306 | }) 307 | ext.send(receiptFrom(pubKey)) 308 | r.on('error', function (err) { 309 | console.log('error=',err) 310 | }) 311 | pump(c, r, c, function (err) { 312 | // todo: reconnect 313 | }) 314 | trie.ready(function () { 315 | self._tries[key] = trie 316 | self.emit('_trie!'+key, trie) 317 | }) 318 | }) 319 | }) 320 | } 321 | 322 | KV.prototype.disconnect = function () { 323 | Object.keys(self._connections.mq).forEach(function (key) { 324 | self._connections.mq[key].close() 325 | }) 326 | Object.keys(self._connections.trie).forEach(function (key) { 327 | self._connections.trie[key].close() 328 | }) 329 | } 330 | 331 | KV.prototype.getId = function (cb) { 332 | this._mq.getId(cb) 333 | } 334 | 335 | KV.prototype.listen = function (cb) { 336 | if (!cb) cb = noop 337 | var self = this 338 | if (self._unread) throw new Error('already listening') 339 | self._unread = self._mq.createReadStream('unread', { live: true }) 340 | var receipts = [] 341 | self._mq.getKeyPairs(function (err, kp) { 342 | if (err) return cb(err) 343 | self._core = hypercore( 344 | self._getStorageFn('kv','core'), 345 | kp.hypercore.publicKey, 346 | { 347 | storeSecretKey: false, 348 | secretKey: kp.hypercore.secretKey 349 | } 350 | ) 351 | self._trie = hypertrie(null, { feed: self._core }) 352 | self._unread.pipe(new Transform({ 353 | objectMode: true, 354 | transform: function ({ from, seq, data }, enc, next) { 355 | //console.log(`RECEIVED ${from}@${seq}: ${data}`) 356 | self._handleData(data, function (err) { 357 | if (err) { 358 | console.log(err + '\n') 359 | } 360 | self._mq.clear({ from, seq }, check) 361 | function check (err) { 362 | next(err) 363 | for (var i = 0; i < receipts.length; i++) { 364 | var r = receipts[i] 365 | if (seq >= r[0]) { 366 | receipts.splice(i--,1) 367 | r[2](err) 368 | } 369 | } 370 | } 371 | }) 372 | } 373 | })) 374 | // todo: these 2 protocol swarms should be overlayed with an extension 375 | self._mq.listen(function (err, server) { 376 | if (err) return cb(err) 377 | self._mqServer = server 378 | cb(null, kp.hypercore.publicKey, server) 379 | }) 380 | self._server = self._network.createServer(function (stream) { 381 | var r = self._core.replicate(false, { download: false, live: true }) 382 | var from = null 383 | var ext = r.registerExtension('kvswarm', { 384 | encoding: 'binary', 385 | onmessage: function (msg, peer) { 386 | if (msg[0] === RECEIPT_SET_FROM) { 387 | from = msg.slice(1,33).toString('hex') 388 | } else if (msg[0] === RECEIPT_REQ) { 389 | if (!from) return ext.send(receiptFail(n, 'recipient not set')) 390 | var offset = 1 391 | var n = varint.decode(msg,offset) 392 | offset += varint.encodingLength(n) 393 | var start = varint.decode(msg,offset) 394 | offset += varint.encodingLength(start) 395 | var len = varint.decode(msg,offset) 396 | var bf = self._mq._bitfield.read[from] 397 | if (!bf) return ext.send(receiptFail(n, 'bitfield not found')) 398 | hasAll(bf, start, len, function (err, all) { 399 | if (err) return ext.send(receiptFail(n, err.message)) 400 | if (all) return ext.send(receiptOk(n, self._trie.feed.length)) 401 | // otherwise wait until the data has been completely processed 402 | receipts.push([ 403 | start, len, function (err) { 404 | return ext.send(err 405 | ? receiptFail(n, err) 406 | : receiptOk(n, self._trie.feed.length)) 407 | } 408 | ]) 409 | }) 410 | } 411 | } 412 | }) 413 | pump(stream, r, stream, function (err) { 414 | console.log('error=',err) 415 | }) 416 | }) 417 | self._server.listen(kp.hypercore.publicKey) 418 | }) 419 | } 420 | 421 | KV.prototype._handleData = function (data, cb) { 422 | var offset = 0, pending = 1, finished = false 423 | try { 424 | while (offset < data.length) { 425 | if (data[0] === PUT) { 426 | offset += 1 427 | var klen = varint.decode(data, offset) 428 | offset += varint.encodingLength(klen) 429 | var key = data.slice(offset,offset+klen) 430 | offset += klen 431 | var vlen = varint.decode(data, offset) 432 | offset += varint.encodingLength(vlen) 433 | var value = data.slice(offset,offset+vlen) 434 | offset += vlen 435 | pending++ 436 | this._trie.put(key.toString(), value, done) 437 | } else if (data[0] === DEL) { 438 | offset += 1 439 | var klen = varint.decode(data, offset) 440 | offset += varint.encodingLength(klen) 441 | var key = data.slice(offset,offset+klen) 442 | offset += klen 443 | pending++ 444 | this._trie.del(key.toString(), done) 445 | } else { 446 | break 447 | } 448 | } 449 | } catch (err) { 450 | process.nextTick(done, err) 451 | } 452 | done() 453 | function done (err) { 454 | if (finished) {} 455 | else if (err) { 456 | finished = true 457 | cb(err) 458 | } else if (--pending === 0) { 459 | cb() 460 | } 461 | } 462 | } 463 | 464 | function noop () {} 465 | 466 | function writeBuf (out, offset, src) { 467 | for (var i = 0; i < src.length; i++) { 468 | out[offset+i] = src[i] 469 | } 470 | } 471 | 472 | function receiptReq (n, ack) { 473 | var nLen = varint.encodingLength(n) 474 | var startLen = varint.encodingLength(ack.start) 475 | var lenLen = varint.encodingLength(ack.length) 476 | var buf = Buffer.alloc(1 + nLen + startLen + lenLen) 477 | var offset = 0 478 | buf[offset] = RECEIPT_REQ 479 | offset += 1 480 | writeBuf(buf, offset, varint.encode(n)) 481 | offset += nLen 482 | writeBuf(buf, offset, varint.encode(ack.start)) 483 | offset += startLen 484 | writeBuf(buf, offset, varint.encode(ack.length)) 485 | offset += lenLen 486 | return buf 487 | } 488 | 489 | function receiptFrom (key) { 490 | var buf = Buffer.alloc(1 + key.length) 491 | buf[0] = RECEIPT_SET_FROM 492 | writeBuf(buf, 1, key) 493 | return buf 494 | } 495 | 496 | function receiptOk (n, coreLen) { 497 | var nLen = varint.encodingLength(n) 498 | var csLen = varint.encodingLength(coreLen) 499 | var buf = Buffer.alloc(1 + nLen + csLen) 500 | var offset = 0 501 | buf[offset] = RECEIPT_RES_OK 502 | offset += 1 503 | writeBuf(buf, offset, varint.encode(n)) 504 | offset += nLen 505 | writeBuf(buf, offset, varint.encode(coreLen)) 506 | offset += csLen 507 | return buf 508 | } 509 | 510 | function receiptFail (n, msg) { 511 | var nLen = varint.encodingLength(n) 512 | var buf = Buffer.alloc(1 + nLen + msg.length) 513 | var offset = 0 514 | buf[offset] = RECEIPT_RES_FAIL 515 | offset += 1 516 | writeBuf(buf, offset, varint.encode(n)) 517 | offset += nLen 518 | writeBuf(buf, offset, msg) 519 | return buf 520 | } 521 | 522 | function hasAll (bf, start, len, cb) { 523 | var pending = 1, finished = false 524 | for (var i = 0; i < len; i++) { 525 | bf.has(start+i, function (err, ex) { 526 | if (finished) return 527 | if (err) { 528 | finished = true 529 | return cb(err) 530 | } 531 | if (!ex) { 532 | finished = true 533 | return cb(null, false) 534 | } 535 | if (--pending === 0) cb(null, true) 536 | }) 537 | } 538 | if (--pending === 0) cb(null, true) 539 | } 540 | -------------------------------------------------------------------------------- /lib/hash-table.js: -------------------------------------------------------------------------------- 1 | module.exports = HashTable 2 | 3 | var HBYTES = 16 // hypertrie hashes are 16 bytes 4 | var MAX = 256n**BigInt(HBYTES)-1n 5 | 6 | function HashTable (bins) { 7 | if (!(this instanceof HashTable)) return new HashTable(bins) 8 | this._table = [] 9 | this.update(bins) 10 | } 11 | 12 | HashTable.prototype.update = function (bins) { 13 | var self = this 14 | Object.keys(bins).forEach(function (key) { 15 | bins[key].slices.forEach(function (slice) { 16 | var start = slice[0], end = slice[1] 17 | var hstart = (start[0]*MAX/start[1]).toString(16).padStart(HBYTES*2,'0') 18 | var hend = (end[0]*MAX/end[1]).toString(16).padStart(HBYTES*2,'0') 19 | self._table.push([ 20 | Buffer.from(hstart,'hex'), 21 | Buffer.from(hend,'hex'), 22 | key 23 | ]) 24 | }) 25 | }) 26 | self._table.sort(cmpIv) 27 | } 28 | function cmpIv (a, b) { return a[0] < b[0] ? -1 : +1 } 29 | 30 | HashTable.prototype.lookup = function (hash) { 31 | var len = this._table.length 32 | var start = 0, end = len 33 | while (true) { 34 | var n = Math.floor((end + start) / 2) 35 | var t = this._table[n] 36 | var c0 = Buffer.compare(hash, t[0]) 37 | var c1 = Buffer.compare(hash, t[1]) 38 | if (c0 >= 0 && (c1 < 0 || (c1 === 0 && n === len-1))) { 39 | return t[2] 40 | } else if (start === n || end === n) { 41 | break 42 | } else if (c0 < 0) { 43 | end = n 44 | } else { 45 | start = n 46 | } 47 | } 48 | return null 49 | } 50 | -------------------------------------------------------------------------------- /lib/hash.js: -------------------------------------------------------------------------------- 1 | // copied from hypertrie/lib/node.js 2 | var KEY = Buffer.alloc(16) 3 | 4 | module.exports = function hash (sodium, keys) { 5 | const buf = Buffer.allocUnsafe(8 * keys.length) 6 | for (var i = 0; i < keys.length; i++) { 7 | const key = Buffer.from(keys[i]) 8 | sodium.crypto_shorthash(i ? buf.slice(i * 8) : buf, key, KEY) 9 | } 10 | return buf 11 | } 12 | -------------------------------------------------------------------------------- /lib/path.js: -------------------------------------------------------------------------------- 1 | // copied from hypertrie/lib/node.js 2 | module.exports = function path (i, hash) { 3 | i-- 4 | const j = i >> 2 5 | if (j >= hash.length) return 4 6 | return (hash[j] >> (2 * (i & 3))) & 3 7 | } 8 | -------------------------------------------------------------------------------- /lib/rslice.js: -------------------------------------------------------------------------------- 1 | var almostEqual = require('almost-equal') 2 | 3 | module.exports = RSlice 4 | 5 | function RSlice (bins) { 6 | if (!(this instanceof RSlice)) return new RSlice(bins) 7 | this.bins = bins || {} 8 | this._binKeys = Object.keys(this.bins) 9 | this._totalSize = 0 10 | for (var i = 0; i < this._binKeys.length; i++) { 11 | var b = this.bins[this._binKeys[i]] 12 | this._totalSize += b.size 13 | } 14 | } 15 | 16 | RSlice.prototype.set = function (key, size) { 17 | var slices = [] 18 | var gaps = [] 19 | var newSize = this._totalSize + size 20 | var n = this._binKeys.length 21 | for (var i = 0; i < n; i++) { 22 | var bin = this.bins[this._binKeys[i]] 23 | var take = bin.size / n 24 | var takeFloat = take / newSize 25 | for (var j = 0; j < bin.slices.length; j++) { 26 | var prevFloatSize = length(bin.slices[j]) 27 | var newFloatSize = prevFloatSize*this._totalSize/newSize 28 | var remFloatSize = prevFloatSize - newFloatSize 29 | var matched = false 30 | // first search for an exact size segment to swap out 31 | for (var k = 0; k < bin.slices.length; k++) { 32 | if (almostEqual(length(bin.slices[k]), remFloatSize)) { 33 | matched = true 34 | slices.push(bin.slices[k]) 35 | bin.slices.splice(k,1) 36 | break 37 | } 38 | } 39 | if (matched) continue 40 | // otherwise keep assigning smaller intervals 41 | for (var k = 0; k < bin.slices.length; k++) { 42 | var len = length(bin.slices[k]) 43 | if (almostEqual(len, remFloatSize)) { 44 | matched = true 45 | slices.push(bin.slices[k]) 46 | bin.slices.splice(k,1) 47 | break 48 | } else if (len < remFloatSize) { 49 | remFloatSize -= len 50 | slices.push(bin.slices[k]) 51 | bin.slices.splice(k,1) 52 | } 53 | } 54 | if (matched) continue 55 | // find a suitable node to split 56 | for (var k = 0; k < bin.slices.length; k++) { 57 | // (should always be the first one) 58 | var iv = bin.slices[0] 59 | var len = length(iv) 60 | if (len < remFloatSize) continue 61 | slices.push([iv[1]-remFloatSize,iv[1]]) 62 | iv[1] -= remFloatSize 63 | break 64 | } 65 | } 66 | } 67 | this.bins[key] = { size, slices } 68 | this._binKeys.push(key) 69 | } 70 | 71 | function adjacent (g, iv) { 72 | if (!g) return false 73 | return almostEqual(g[1],iv[0]) || almostEqual(g[0],iv[1]) 74 | } 75 | 76 | function length (iv) { return iv[1]-iv[0] } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kvswarm", 3 | "version": "1.0.1", 4 | "description": "key/value store with horizontal partitioning and p2p content distribution", 5 | "license": "Parity-6.0.0 AND Apache-2.0", 6 | "bin": { 7 | "kvswarm": "bin/cmd.js" 8 | }, 9 | "dependencies": { 10 | "hypercore": "^8.2.5", 11 | "hypertrie": "^4.0.0", 12 | "minimist": "^1.2.0", 13 | "peermq": "^4.0.1", 14 | "pump": "^3.0.0", 15 | "random-slicing": "^2.0.2", 16 | "varint": "^5.0.0" 17 | }, 18 | "devDependencies": { 19 | "random-access-memory": "^3.1.1", 20 | "tape": "^4.11.0" 21 | }, 22 | "scripts": { 23 | "test": "tape test/*.js", 24 | "real-network-test": "PEERMQ_REAL_NETWORK=true tape test/*.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # kvswarm 2 | 3 | key/value store split across multiple writer nodes (horizontal partitioning) 4 | with p2p content distribution and networking 5 | 6 | uses [random slicing][] to provide an address space that minimizes moves during 7 | resizes and [peermq][] to provide robust delivery of messages with an offline 8 | write log 9 | 10 | [random slicing]: https://github.com/peermaps/random-slicing 11 | 12 | # status 13 | 14 | * [x] get/put across multiple nodes 15 | * [x] hot-swap configuration 16 | * [ ] move data after capacity adjustment 17 | * [ ] mirror nodes 18 | * [ ] redundancy balancing 19 | 20 | # example 21 | 22 | ## command-line example 23 | 24 | In this example, we initialize a swarm with 3 storage nodes (A, B, and C) 25 | and authorize nodes X and Y for writing to the swarm. 26 | 27 | ``` 28 | $ A=$(kvswarm id -d /tmp/a) 29 | $ B=$(kvswarm id -d /tmp/b) 30 | $ C=$(kvswarm id -d /tmp/c) 31 | $ X=$(kvswarm id -d /tmp/x) 32 | $ Y=$(kvswarm id -d /tmp/y) 33 | $ kvswarm init --capacity.$A=5 --capacity.$B=10 --capacity.$C=7 --writer=$X --writer=$Y > config.json 34 | $ kvswarm listen -d /tmp/a -c config.json & 35 | $ kvswarm listen -d /tmp/b -c config.json & 36 | $ kvswarm listen -d /tmp/c -c config.json & 37 | ``` 38 | 39 | Once all the nodes are setup, you can write documents from nodes X and Y: 40 | 41 | ``` 42 | $ kvswarm -c config.json -d /tmp/x write --put.greeting=hi 43 | $ kvswarm -c config.json -d /tmp/y write --put.cool=beans 44 | $ kvswarm -c config.json -d /tmp/x connect & 45 | $ kvswarm -c config.json -d /tmp/y connect & 46 | ``` 47 | 48 | and any node can read from the database: 49 | 50 | ``` 51 | $ kvswarm -c config.json -d /tmp/x get cool 52 | beans 53 | $ kvswarm -c config.json -d /tmp/y get greeting 54 | hi 55 | ``` 56 | 57 | # api 58 | 59 | ``` 60 | var kvswarm = require('kvswarm') 61 | var Config = require('kvswarm/config') 62 | ``` 63 | 64 | ## var kv = kvswarm(opts) 65 | 66 | Create a new kvswarm instance from: 67 | 68 | * `opts.network` - network interface (use `require('peermq/network')`) 69 | * `opts.storage` - string that represents a base path OR a function that 70 | receives a string name argument and returns a random-access adaptor or a 71 | string path 72 | * `opts.config` - `Config` instance to set writers and bin slicings 73 | * `opts.bins` - object mapping node keys to slicing arrays 74 | (see the [random-slicing][] module for how these arrays are formatted) 75 | 76 | ## kv.get(key, opts, cb) 77 | 78 | Get the value of a string `key` as `cb(err, value)`. 79 | 80 | kvswarm is in sparse mode by default, so a `get()` will trigger a download for 81 | the relevant key. 82 | 83 | `opts` are passed through to hypercore's `get()` method. 84 | 85 | ## kv.put(key, value) 86 | 87 | Append a PUT message to the write cache for `key` and `value`. 88 | 89 | The write cache is ephemeral. 90 | 91 | ## kv.del(key) 92 | 93 | Append a DEL message to the write cache for `key`. 94 | 95 | The write cache is ephemeral. 96 | 97 | ## kv.flush(opts, cb) 98 | 99 | Flush the write cache to the outgoing write logs that correspond to the address 100 | space for each key. Less frequent, larger flushes will be faster to process, but the 101 | data won't be saved to durable storage until it's written into a write block 102 | with `flush()`. 103 | 104 | * `opts.ack` - when true, wait for an acknowledgement that the write node 105 | received this 106 | * `opts.receipt` - when true, wait until the write block associated with this flush was 107 | written, processed, and the hypercore update has propagated back so you can 108 | `get()` (implies `opts.ack`) 109 | 110 | ## kv.connect() 111 | 112 | Connect to the network swarm. 113 | 114 | ## kv.disconnect() 115 | 116 | Disconnect from the network swarm. 117 | 118 | ## kv.listen(cb) 119 | 120 | Listen for incoming connections. Write and mirror nodes should call this 121 | method. 122 | 123 | ## kv.setWriters(writers, cb) 124 | 125 | Set the list of authorized writers as an array of hex string keys. 126 | 127 | ## kv.getWriters(cb) 128 | 129 | Get an array of writer nodes by their hex string key as `cb(err, writers)`. 130 | 131 | ## kv.addWriter(key, cb) 132 | 133 | Add a writer by its node key as a hex string. 134 | 135 | ## kv.removeWriter(key, cb) 136 | 137 | Remove a writer by its node key as a hex string. 138 | 139 | ## kv.setCapacities(capacities) 140 | 141 | Set the capacities of write nodes with an object mapping hex string ids to 142 | unitless numeric capacities. 143 | 144 | ## kv.setConfig(config, cb) 145 | 146 | Set the configuration for the kv to use at runtime. 147 | 148 | ## var config = new Config(opts) 149 | 150 | Create a new configuration from: 151 | 152 | * `opts.writers` - array of node keys authorized to write new messages 153 | * `opts.capacities` - object mapping node keys to capacity values 154 | (unitless numeric values) 155 | 156 | ### var config = Config.parse(str) 157 | 158 | Create a new configuration object from a string `str`. 159 | 160 | ## var str = config.serialize() 161 | 162 | Create a string that can be stored on disk and fed to `Config.parse(str)` later. 163 | 164 | ### config.update(opts) 165 | 166 | Update any of: 167 | 168 | * `opts.capacities` - new capacities to use. uses existing slices to adjust. 169 | * `opts.writers` - new set of writers as an array of hex string keys 170 | 171 | ## config.addWriter(key) 172 | 173 | Add a writer by its hex string `key` 174 | 175 | ### config.removeWriter(key) 176 | 177 | Remove a writer by its hex string `key` 178 | 179 | # license 180 | 181 | [license zero parity](https://licensezero.com/licenses/parity) 182 | and [apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) 183 | (contributions) 184 | -------------------------------------------------------------------------------- /test/kv.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var KV = require('../') 3 | var Config = require('../config') 4 | var ram = require('random-access-memory') 5 | var network = require('peermq/test/lib/network.js')() 6 | 7 | test('kv', function (t) { 8 | t.plan(16) 9 | var nodes = { 10 | A: new KV({ network, storage: storage('A') }), 11 | B: new KV({ network, storage: storage('B') }), 12 | C: new KV({ network, storage: storage('C') }), 13 | X: new KV({ network, storage: storage('X') }), 14 | Y: new KV({ network, storage: storage('Y') }) 15 | } 16 | var ids = {} 17 | var pending = 1 18 | Object.keys(nodes).forEach(function (key) { 19 | pending++ 20 | nodes[key].getId(function (err, id) { 21 | t.ifError(err) 22 | ids[key] = id.toString('hex') 23 | if (--pending === 0) setConfig() 24 | }) 25 | }) 26 | if (--pending === 0) setConfig() 27 | 28 | function setConfig () { 29 | var capacities = {} 30 | capacities[ids.A] = 5 31 | capacities[ids.B] = 12 32 | capacities[ids.C] = 8 33 | var config = new Config({ 34 | capacities, 35 | writers: [ ids.X, ids.Y ] 36 | }) 37 | var pending = 6 38 | nodes.A.setConfig(config, done) 39 | nodes.B.setConfig(config, done) 40 | nodes.C.setConfig(config, done) 41 | nodes.X.setConfig(config, done) 42 | nodes.Y.setConfig(config, done) 43 | done() 44 | function done () { if (--pending === 0) connect() } 45 | } 46 | function connect () { 47 | nodes.A.listen() 48 | nodes.B.listen() 49 | nodes.C.listen() 50 | nodes.X.connect() 51 | nodes.Y.connect() 52 | write() 53 | } 54 | function write () { 55 | nodes.X.put('greeting', 'hi') 56 | nodes.Y.put('cool', 'beans') 57 | var pending = 3 58 | nodes.X.flush({ receipt: true }, done) 59 | nodes.Y.flush({ receipt: true }, done) 60 | done() 61 | function done (err) { 62 | t.ifError(err) 63 | if (--pending === 0) sync() 64 | } 65 | } 66 | function sync () { 67 | nodes.X.get('cool', function (err, res) { 68 | t.ifError(err) 69 | t.deepEqual(res.value, Buffer.from('beans')) 70 | }) 71 | nodes.X.get('greeting', function (err, res) { 72 | t.ifError(err) 73 | t.deepEqual(res.value, Buffer.from('hi')) 74 | }) 75 | nodes.Y.get('cool', function (err, res) { 76 | t.ifError(err) 77 | t.deepEqual(res.value, Buffer.from('beans')) 78 | }) 79 | nodes.Y.get('greeting', function (err, res) { 80 | t.ifError(err) 81 | t.deepEqual(res.value, Buffer.from('hi')) 82 | }) 83 | } 84 | }) 85 | 86 | function storage (id) { return ram } 87 | --------------------------------------------------------------------------------