UNPKG

kademlia-dht

Version:

Network-agnostic Kademlia Distributed Hash Table

158 lines (144 loc) 5.3 kB
'use strict'; var Bucket = require('./bucket.js'); var Id = require('./id.js'); var Contact = require('./contact.js'); var LookupList = require('./lookup-list.js'); // Associate Kademlia IDs with their endpoints. `localId` must be the ID of the // local node, more close contacts are effectively stored than far contacts. // var RoutingTable = function (localId, bucketSize) { if (!(localId instanceof Id)) throw new Error('id must be a valid identifier'); this._bucketSize = bucketSize; this._root = new Bucket(bucketSize); Object.defineProperty(this, 'id', {value: localId}); }; // Force a callback to be async. // function makeAsync(cb) { return function () { var args = arguments; process.nextTick(function () { cb.apply(null, args); }); }; } // Split a bucket, creating a new node, and insert the new contact. `this` is // assumed to be the RoutingTable. The `opt.bucket` is split left/right // depending on the `opt.nth` bit of the contact IDs. // RoutingTable.prototype._splitAndStore = function (contact, opt) { var node = {left: new Bucket(this._bucketSize), right: new Bucket(this._bucketSize)}; opt.bucket.split(opt.nth, node.left, node.right); if (opt.parent === null) this._root = node; else if (opt.parent.left === opt.bucket) opt.parent.left = node; else opt.parent.right = node; var bucket = opt.bit ? node.right : node.left; return bucket.store(contact); }; // Store (or update) a contact. Return `null` if everything went well (it does // not always mean the contact was added, though). In case a bucket is full and // not allowed to split, return the oldest contact of the bucket; the new // contact is then not added. It's up to the caller to check the validity of // the returned contact, remove it if it's invalid, and retry to add the new // contact. // RoutingTable.prototype.store = function (contact) { if (!(contact instanceof Contact)) throw new Error('invalid contact'); if (contact.id.equal(this.id)) return null; var res = this._findBucket(contact); if (res.bucket.store(contact)) return null; // FIXME: add the special mode splitting buckets even when we're not close. // The whitepaper is not very clear about it. if (!res.allowSplit || res.nth + 1 === Id.BIT_SIZE) { return res.bucket.oldest; } this._splitAndStore(contact, res); return null; }; // Store several contacts. // RoutingTable.prototype.storeSome = function (contacts) { for (var i = 0; i < contacts.length; ++i) { this.store(contacts[i]); } }; // Remove the `contact`, generally because it had been detected as invalid or // offline. // RoutingTable.prototype.remove = function (contact) { if (!(contact instanceof Contact)) throw new Error('invalid contact'); var res = this._findBucket(contact); res.bucket.remove(contact); }; // Find the bucket closest to the specified ID. Return an object containing // `{parent, bucket, allowSplit, nth, bit}` // RoutingTable.prototype._findBucket = function (contact) { var parent = null; var node = this._root; var allowSplit = true; for (var i = 0; i < Id.BIT_SIZE; ++i) { var bit = contact.id.at(i); allowSplit &= bit === this.id.at(i); if (node instanceof Bucket) return {parent: parent, bucket: node, allowSplit: allowSplit, nth: i, bit: bit}; parent = node; node = bit ? node.right : node.left; } }; RoutingTable.prototype._find = function (id, rank, node, count, list) { if (node instanceof Bucket) { list.insertMany(node.obtain()); return; } var self = this; function findIn(main, other) { self._find(id, rank + 1, main, count, list); if (list.length < count) self._find(id, rank + 1, other, count, list); } if (id.at(rank)) { findIn(node.right, node.left); } else { findIn(node.left, node.right); } }; // Get the `count` known contacts closest from `id`. // Ideally, it returns all contacts of the bucket closest to id. It completes // with neighbour bucket contacts if RoutingTable.BUCKET_SIZE is not attained. // RoutingTable.prototype.find = function (id, count) { if (!(id instanceof Id)) throw new Error('invalid id'); if (typeof count === 'undefined') count = this._bucketSize; var list = new LookupList(id, count); this._find(id, 0, this._root, count, list); return list.getContacts(); }; function nodeToString(node, prefix, indent) { var res = ''; if (node instanceof Bucket) { res += new Array(indent).join(' ') + node.toString(indent + 4) + '\n'; } else { res += new Array(indent).join(' ') + '+ ' + prefix + '0:\n'; res += nodeToString(node.left, prefix + '0', indent + 4); res += new Array(indent).join(' ') + '+ ' + prefix + '1:\n'; res += nodeToString(node.right, prefix + '1', indent + 4); } return res; } // Get a string representation. // RoutingTable.prototype.toString = function (indent) { if (typeof indent === 'undefined') indent = 0; return nodeToString(this._root, '', indent); }; module.exports = RoutingTable;