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
JavaScript
'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;