UNPKG

kademlia-table

Version:

XOR distance based routing table used for P2P networks, based on the Kademlia DHT paper

183 lines (179 loc) 5.97 kB
'use strict'; var uint8arrayTools = require('uint8array-tools'); // src/getBitwiseDistance.ts var getBitwiseDistance = (a, b, byteLength = Math.min(a.byteLength, b.byteLength)) => { for (let i = 0; i < byteLength; i++) { const ai = a[i]; const bi = b[i]; if (ai !== bi) return 8 * byteLength - (i * 8 + Math.clz32(ai ^ bi) - 24); } return 0; }; var KademliaTable = class { constructor(id, options) { this.id = id; this.bucketSize = options.bucketSize || 20; this.buckets = new Array(id.length * 8 + 1).fill(void 0).map(() => ({ items: [], replacementItems: [] })); this.errorLimit = options.errorLimit || 5; this.getId = options.getId; } get length() { return this.buckets.reduce((sum, bucket) => sum + bucket.items.length, 0); } *[Symbol.iterator]() { for (const bucket of this.buckets) for (const item of bucket.items) yield item.node; } add(node, d = this.getDistance(this.getId(node))) { const bucket = this.getBucket(d); if (!bucket || bucket.items.find((item) => uint8arrayTools.compare(this.getId(item.node), this.getId(node)) === 0) || bucket.replacementItems.find((item) => uint8arrayTools.compare(this.getId(item.node), this.getId(node)) === 0)) return false; if (bucket.items.length >= this.bucketSize) { if (bucket.replacementItems.length >= this.bucketSize) return false; bucket.replacementItems.push({ node, errorCount: 0 }); return false; } bucket.items.push({ node, errorCount: 0 }); return true; } clear() { for (let i = 0; i < this.buckets.length; i++) this.buckets[i] = { items: [], replacementItems: [] }; } *iterateClosestToId(id) { return this.iterateNodes(id); } listClosestToId(id, limit = this.buckets.length * this.bucketSize) { return this.listNodes(id, void 0, limit); } get(id, d = this.getDistance(id)) { return this.getBucket(d)?.items.find((item) => uint8arrayTools.compare(this.getId(item.node), id) === 0)?.node; } getBucket(d) { return this.buckets.at(d); } getDistance(id) { return getBitwiseDistance(this.id, id); } *iterateNodes(id, d = this.getDistance(id)) { const bucketLimit = Math.max(d, this.buckets.length - 1 - d); let depth = 0; let offset = (depth % 2 === 0 ? 1 : -1) * Math.ceil(depth / 2); while (Math.abs(offset) <= bucketLimit) { const i = d + offset; if (0 <= i && i < this.buckets.length) { const bucket = this.getBucket(i); if (bucket) for (const item of bucket.items) yield item.node; } depth++; offset = (depth % 2 === 0 ? 1 : -1) * Math.ceil(depth / 2); } } listNodes(id, d = this.getDistance(id), limit = this.buckets.length * this.bucketSize) { if (!limit) return []; const nodes = []; for (const node of this.iterateNodes(id, d)) { nodes.push(node); if (nodes.length >= limit) return nodes; } return nodes; } has(id, d = this.getDistance(id)) { return !!this.getBucket(d)?.items.find((item) => uint8arrayTools.compare(this.getId(item.node), id) === 0); } markError(id, d = this.getDistance(id)) { const bucket = this.getBucket(d); if (!bucket) return false; const index = bucket.items.findIndex((item) => uint8arrayTools.compare(this.getId(item.node), id) === 0); if (index === -1) return false; const items = bucket.items.splice(index, 1); bucket.items.push( ...items.map((item) => ({ ...item, errorCount: item.errorCount + 1 })) ); for (const item of items) { if (item.errorCount + 1 < this.errorLimit) continue; this.remove(this.getId(item.node)); } return true; } markSuccess(id, d = this.getDistance(id)) { const bucket = this.getBucket(d); if (!bucket) return false; const index = bucket.items.findIndex((item) => uint8arrayTools.compare(this.getId(item.node), id) === 0); if (index === -1) return false; const items = bucket.items.splice(index, 1); bucket.items.push( ...items.map((item) => ({ ...item, errorCount: 0 })) ); return true; } update(node, d = this.getDistance(this.getId(node))) { const bucket = this.getBucket(d); if (!bucket) return false; const index = bucket.items.findIndex((item2) => uint8arrayTools.compare(this.getId(item2.node), this.getId(node)) === 0); if (index === -1) { const replacementIndex = bucket.replacementItems.findIndex((item2) => uint8arrayTools.compare(this.getId(item2.node), this.getId(node)) === 0); if (replacementIndex === -1) return false; const replacementItem = bucket.replacementItems[index]; bucket.replacementItems[index] = { node, errorCount: replacementItem.errorCount }; return false; } const item = bucket.items[index]; bucket.items[index] = { node, errorCount: item.errorCount }; return true; } remove(id, d = this.getDistance(id), force) { const bucket = this.getBucket(d); if (!bucket) return false; const index = bucket.items.findIndex((item) => uint8arrayTools.compare(this.getId(item.node), id) === 0); if (index === -1) { const replacementIndex = bucket.replacementItems.findIndex((item) => uint8arrayTools.compare(this.getId(item.node), id) === 0); if (replacementIndex === -1) return false; bucket.replacementItems.splice(replacementIndex, 1); return false; } if (bucket.replacementItems.length || force) { bucket.items.splice(index, 1); const replacementItem = bucket.replacementItems.shift(); if (replacementItem) bucket.items.unshift(replacementItem); } return true; } }; exports.KademliaTable = KademliaTable; exports.getBitwiseDistance = getBitwiseDistance;