UNPKG

@yipsec/kdns

Version:
188 lines (157 loc) 5.59 kB
/** * @module kdns/router */ 'use strict'; const { EventEmitter } = require('node:events'); const { Bucket } = require('./bucket'); const keys = require('./keys'); const constants = require('./constants'); class Router extends Map { /** * Contact is inserted into the routing table * @event module:kdns/router~Router#contact_added * @param {string} fingerprint - Node ID of the inserted contact */ /** * Contact is evicted from the routing table * @event module:kdns/router~Router#contact_deleted * @param {string} fingerprint - Node ID of the evicted contact */ /** * Kademlia routing table consisting of {@link module:kdns/constants~B} total * {@link module:kdns/bucket~Bucket}s - each holding up to {@link module:kdns/constants~K} * total {@link module:kdns/contacts~Contact}s. * @constructor * @extends Map * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map * @param {buffer} identity - Reference point for calculating distances */ constructor(identity) { super(); /** * Exposes events arising from handling protocol messages. Some of these * events may require handling if they pass a {@link Protocol~HandlerResponse} * @property {buffer} identity - Reference key for distance calculation */ this.identity = identity || keys.getRandomKeyBuffer(); /** * Exposes events arising from handling protocol messages. Some of these * events may require handling if they pass a {@link Protocol~HandlerResponse} * @property {EventEmitter} events - Router events interface */ this.events = new EventEmitter(); for (let b = 0; b < constants.B; b++) { this.set(b, new Bucket()); } } /** * The total contacts in the routing table * @property {number} size */ get size() { let contacts = 0; this.forEach((bucket) => contacts += bucket.length); return contacts; } /** * The total buckets in the routing table * @property {number} length */ get length() { let buckets = 0; this.forEach(() => buckets++); return buckets; } /** * Returns the bucket index of the given node id * @param {string|buffer} nodeId - Node identity to get index for * @returns {number} */ indexOf(nodeId) { return keys.getBucketIndex(this.identity, nodeId); } /** * Returns the contact object associated with the given node id * @param {string|buffer} nodeId - Node identity of the contact * @returns {module:kdns/contacts~Contact} */ getContactByNodeId(nodeId) { nodeId = nodeId.toString('hex'); return this.get(this.indexOf(nodeId)).get(nodeId); } /** * Removes the contact from the routing table given a node id * @param {string|buffer} nodeId - Node identity to remove * @fires module:kdns/router~Router#contact_deleted * @return {undefined} */ removeContactByNodeId(nodeId) { nodeId = nodeId.toString('hex'); this.events.emit('contact_deleted', nodeId); return this.get(this.indexOf(nodeId)).delete(nodeId); } /** * Adds the contact to the routing table in the proper bucket position, * returning the [bucketIndex, bucket, contactIndex, contact]; if the * returned contactIndex is -1, it indicates the bucket is full and the * contact was not added; kademlia implementations should PING the contact * at bucket.head to determine if it should be dropped before calling this * method again. * @fires module:kdns/router~Router#contact_added * @param {string|buffer} nodeId - Node identity to add * @param {module:kdns/contacts~Contact} contact - Contact information for peer * @returns {array} */ addContactByNodeId(nodeId, contact) { nodeId = nodeId.toString('hex'); const bucketIndex = this.indexOf(nodeId); const bucket = this.get(bucketIndex); const contactIndex = bucket.set(nodeId, contact); this.events.emit('contact_added', nodeId); return [bucketIndex, bucket, contactIndex, contact]; } /** * Returns the [index, bucket] of the occupied bucket with the lowest index * @returns {module:kdns/bucket~Bucket} */ getClosestBucket() { for (let [index, bucket] of this) { if (index < constants.B - 1 && bucket.length === 0) { continue; } return [index, bucket]; } } /** * Returns a array of N contacts closest to the supplied key * @param {string|buffer} key - Key to get buckets for * @param {number} [n=20] - Number of results to return * @param {boolean} [exclusive=false] - Exclude exact matches * @returns {Map} */ getClosestContactsToKey(key, n = constants.K, exclusive = false) { const bucketIndex = this.indexOf(key); const contactResults = new Map(); function _addNearestFromBucket(bucket) { let entries = [...bucket.getClosestToKey(key, n, exclusive).entries()]; entries.splice(0, n - contactResults.size) .forEach(([id, contact]) => { /* istanbul ignore else */ if (contactResults.size < n) { contactResults.set(id, contact); } }); } let ascIndex = bucketIndex; let descIndex = bucketIndex; _addNearestFromBucket(this.get(bucketIndex)); while (contactResults.size < n && descIndex >= 0) { _addNearestFromBucket(this.get(descIndex--)); } while (contactResults.size < n && ascIndex < constants.B) { _addNearestFromBucket(this.get(ascIndex++)); } return contactResults; } } module.exports.Router = Router;