{"id":13482387,"url":"https://github.com/tristanls/k-bucket","last_synced_at":"2025-04-12T21:26:59.766Z","repository":{"id":9343017,"uuid":"11192196","full_name":"tristanls/k-bucket","owner":"tristanls","description":"Kademlia DHT K-bucket implementation as a binary tree","archived":false,"fork":false,"pushed_at":"2023-04-28T06:46:56.000Z","size":192,"stargazers_count":155,"open_issues_count":2,"forks_count":33,"subscribers_count":11,"default_branch":"master","last_synced_at":"2024-05-15T21:32:02.907Z","etag":null,"topics":["dht"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tristanls.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["tristanls"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2013-07-05T05:18:37.000Z","updated_at":"2024-06-18T12:39:12.501Z","dependencies_parsed_at":"2024-06-18T12:39:09.164Z","dependency_job_id":"ef43fbbf-e92f-4aba-855e-b0fd34527882","html_url":"https://github.com/tristanls/k-bucket","commit_stats":{"total_commits":155,"total_committers":10,"mean_commits":15.5,"dds":0.2129032258064516,"last_synced_commit":"3aa5b4f1dacb835752995a25409ab319d2070b9e"},"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tristanls%2Fk-bucket","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tristanls%2Fk-bucket/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tristanls%2Fk-bucket/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tristanls%2Fk-bucket/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tristanls","download_url":"https://codeload.github.com/tristanls/k-bucket/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248633584,"owners_count":21136893,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dht"],"created_at":"2024-07-31T17:01:01.518Z","updated_at":"2025-04-12T21:26:59.735Z","avatar_url":"https://github.com/tristanls.png","language":"JavaScript","funding_links":["https://github.com/sponsors/tristanls"],"categories":["Modules","Protocols"],"sub_categories":[],"readme":"# k-bucket\n\n_Stability: 2 - [Stable](https://github.com/tristanls/stability-index#stability-2---stable)_\n\n[![NPM version](https://badge.fury.io/js/k-bucket.png)](http://npmjs.org/package/k-bucket)\n\nKademlia DHT K-bucket implementation as a binary tree.\n\n## Contributors\n\n[@tristanls](https://github.com/tristanls), [@mikedeboer](https://github.com/mikedeboer), [@deoxxa](https://github.com/deoxxa), [@feross](https://github.com/feross), [@nathanph](https://github.com/nathanph), [@allouis](https://github.com/allouis), [@fanatid](https://github.com/fanatid), [@robertkowalski](https://github.com/robertkowalski), [@nazar-pc](https://github.com/nazar-pc), [@jimmywarting](https://github.com/jimmywarting), [@achingbrain](https://github.com/achingbrain)\n\n## Installation\n\n    npm install k-bucket\n\n## Tests\n\n    npm test\n\n## Usage\n\n```javascript\nconst KBucket = require('k-bucket')\n\nconst kBucket1 = new KBucket({\n    localNodeId: Buffer.from('my node id') // default: random data\n})\n// or without using Buffer (for example, in the browser)\nconst id = 'my node id'\nconst nodeId = new Uint8Array(id.length)\nfor (let i = 0, len = nodeId.length; i \u003c len; ++i) {\n  nodeId[i] = id.charCodeAt(i)\n}\nconst kBucket2 = new KBucket({\n  localNodeId: nodeId // default: random data\n})\n```\n\n## Overview\n\nA [*Distributed Hash Table (DHT)*](http://en.wikipedia.org/wiki/Distributed_hash_table) is a decentralized distributed system that provides a lookup table similar to a hash table.\n\n*k-bucket* is an implementation of a storage mechanism for keys within a DHT. It stores `contact` objects which represent locations and addresses of nodes in the decentralized distributed system. `contact` objects are typically identified by a SHA-1 hash, however this restriction is lifted in this implementation. Additionally, node ids of different lengths can be compared.\n\nThis Kademlia DHT k-bucket implementation is meant to be as minimal as possible. It assumes that `contact` objects consist only of `id`. It is useful, and necessary, to attach other properties to a `contact`. For example, one may want to attach `ip` and `port` properties, which allow an application to send IP traffic to the `contact`. However, this information is extraneous and irrelevant to the operation of a k-bucket.\n\n### arbiter function\n\nThis *k-bucket* implementation implements a conflict resolution mechanism using an `arbiter` function. The purpose of the `arbiter` is to choose between two `contact` objects with the same `id` but perhaps different properties and determine which one should be stored.  As the `arbiter` function returns the actual object to be stored, it does not need to make an either/or choice, but instead could perform some sort of operation and return the result as a new object that would then be stored. See [kBucket._update(node, index, contact)](#kbucket_updatenode-index-contact) for detailed semantics of which `contact` (`incumbent` or `candidate`) is selected.\n\nFor example, an `arbiter` function implementing a `vectorClock` mechanism would look something like:\n\n```javascript\n// contact example\nvar contact = {\n    id: Buffer.from('contactId'),\n    vectorClock: 0\n};\n\nfunction arbiter(incumbent, candidate) {\n    if (incumbent.vectorClock \u003e candidate.vectorClock) {\n        return incumbent;\n    }\n    return candidate;\n};\n```\n\nAlternatively, consider an arbiter that implements a Grow-Only-Set CRDT mechanism:\n\n```javascript\n// contact example\nconst contact = {\n    id: Buffer.from('workerService'),\n    workerNodes: {\n        '17asdaf7effa2': { host: '127.0.0.1', port: 1337 },\n        '17djsyqeryasu': { host: '127.0.0.1', port: 1338 }\n    }\n};\n\nfunction arbiter(incumbent, candidate) {\n    // we create a new object so that our selection is guaranteed to replace\n    // the incumbent\n    const merged = {\n        id: incumbent.id, // incumbent.id === candidate.id within an arbiter\n        workerNodes: incumbent.workerNodes\n    }\n\n    Object.keys(candidate.workerNodes).forEach(workerNodeId =\u003e {\n        merged.workerNodes[workerNodeId] = candidate.workerNodes[workerNodeId];\n    })\n\n    return merged\n}\n```\n\nNotice that in the above case, the Grow-Only-Set assumes that each worker node has a globally unique id.\n\n## Documentation\n\n### KBucket\n\nImplementation of a Kademlia DHT k-bucket used for storing contact (peer node) information.\n\nFor a step by step example of k-bucket operation you may find the following slideshow useful: [Distribute All The Things](https://docs.google.com/presentation/d/11qGZlPWu6vEAhA7p3qsQaQtWH7KofEC9dMeBFZ1gYeA/edit#slide=id.g1718cc2bc_0661).\n\nKBucket starts off as a single k-bucket with capacity of _k_. As contacts are added, once the _k+1_ contact is added, the k-bucket is split into two k-buckets. The split happens according to the first bit of the contact node id. The k-bucket that would contain the local node id is the \"near\" k-bucket, and the other one is the \"far\" k-bucket. The \"far\" k-bucket is marked as _don't split_ in order to prevent further splitting. The contact nodes that existed are then redistributed along the two new k-buckets and the old k-bucket becomes an inner node within a tree data structure.\n\nAs even more contacts are added to the \"near\" k-bucket, the \"near\" k-bucket will split again as it becomes full. However, this time it is split along the second bit of the contact node id. Again, the two newly created k-buckets are marked \"near\" and \"far\" and the \"far\" k-bucket is marked as _don't split_. Again, the contact nodes that existed in the old bucket are redistributed. This continues as long as nodes are being added to the \"near\" k-bucket, until the number of splits reaches the length of the local node id.\n\nAs more contacts are added to the \"far\" k-bucket and it reaches its capacity, it does not split. Instead, the k-bucket emits a \"ping\" event (register a listener: `kBucket.on('ping', function (oldContacts, newContact) {...});` and includes an array of old contact nodes that it hasn't heard from in a while and requires you to confirm that those contact nodes still respond (literally respond to a PING RPC). If an old contact node still responds, it should be re-added (`kBucket.add(oldContact)`) back to the k-bucket. This puts the old contact on the \"recently heard from\" end of the list of nodes in the k-bucket. If the old contact does not respond, it should be removed (`kBucket.remove(oldContact.id)`) and the new contact being added now has room to be stored (`kBucket.add(newContact)`).\n\n**Public API**\n  * [KBucket.arbiter(incumbent, candidate)](#kbucketarbiterincumbent-candidate)\n  * [KBucket.distance(firstId, secondId)](#kbucketdistancefirstid-secondid)\n  * [new KBucket(options)](#new-kbucketoptions)\n  * [kBucket.add(contact)](#kbucketaddcontact)\n  * [kBucket.closest(id [, n = Infinity])](#kbucketclosestid--n--infinity)\n  * [kBucket.count()](#kbucketcount)\n  * [kBucket.get(id)](#kbucketgetid)\n  * [kBucket.metadata](#kbucketmetadata)\n  * [kBucket.remove(id)](#kbucketremoveid)\n  * [kBucket.toArray()](#kbuckettoarray)\n  * [kBucket.toIterable()](#kbuckettoiterable)\n  * [Event 'added'](#event-added)\n  * [Event 'ping'](#event-ping)\n  * [Event 'removed'](#event-removed)\n  * [Event 'updated'](#event-updated)\n\n#### KBucket.arbiter(incumbent, candidate)\n\n  * `incumbent`: _Object_ Contact currently stored in the k-bucket.\n  * `candidate`: _Object_ Contact being added to the k-bucket.\n  * Return: _Object_ Contact to updated the k-bucket with.\n\nDefault arbiter function for contacts with the same `id`. Uses `contact.vectorClock` to select which contact to update the k-bucket with. Contact with larger `vectorClock` field will be selected. If `vectorClock` is the same, `candidat` will be selected.\n\n#### KBucket.distance(firstId, secondId)\n\n  * `firstId`: _Uint8Array_ Uint8Array containing first id.\n  * `secondId`: _Uint8Array_ Uint8Array containing second id.\n  * Return: _Integer_ The XOR distance between `firstId` and `secondId`.\n\nDefault distance function. Finds the XOR distance between firstId and secondId.\n\n#### new KBucket(options)\n\n  * `options`:\n    * `arbiter`: _Function_ _(Default: vectorClock arbiter)_\n        `function (incumbent, candidate) { return contact; }` An optional `arbiter` function that given two `contact` objects with the same `id` returns the desired object to be used for updating the k-bucket. For more details, see [arbiter function](#arbiter-function).\n    * `distance`: _Function_\n        `function (firstId, secondId) { return distance }` An optional `distance` function that gets two `id` Uint8Arrays and return distance (as number) between them.\n    * `localNodeId`: _Uint8Array_ An optional Uint8Array representing the local node id. If not provided, a local node id will be created via `crypto.randomBytes(20)`.\n    * `metadata`: _Object_ _(Default: {})_ Optional satellite data to include with the k-bucket. `metadata` property is guaranteed not be altered, it is provided as an explicit container for users of k-bucket to store implementation-specific data.\n    * `numberOfNodesPerKBucket`: _Integer_ _(Default: 20)_ The number of nodes that a k-bucket can contain before being full or split.\n    * `numberOfNodesToPing`: _Integer_ _(Default: 3)_ The number of nodes to ping when a bucket that should not be split becomes full. KBucket will emit a `ping` event that contains `numberOfNodesToPing` nodes that have not been contacted the longest.\n\nCreates a new KBucket.\n\n#### kBucket.add(contact)\n\n  * `contact`: _Object_ The contact object to add.\n    * `id`: _Uint8Array_ Contact node id.\n    * Any satellite data that is part of the `contact` object will not be altered, only `id` is used.\n  * Return: _Object_ The k-bucket itself.\n\nAdds a `contact` to the k-bucket.\n\n#### kBucket.closest(id [, n = Infinity])\n\n  * `id`: _Uint8Array_ Contact node id.\n  * `n`: _Integer_ _(Default: Infinity)_ The maximum number of closest contacts to return.\n  * Return: _Array_ Maximum of `n` closest contacts to the node id.\n\nGet the `n` closest contacts to the provided node id. \"Closest\" here means: closest according to the XOR metric of the `contact` node id.\n\n#### kBucket.count()\n\n  * Return: _Number_ The number of contacts held in the tree\n\nCounts the total number of contacts in the tree.\n\n#### kBucket.get(id)\n\n  * `id`: _Uint8Array_ The ID of the `contact` to fetch.\n  * Return: _Object_ The `contact` if available, otherwise null\n\nRetrieves the `contact`.\n\n#### kBucket.metadata\n\n  * `metadata`: _Object_ _(Default: {})_\n\nThe `metadata` property serves as a container that can be used by implementations using k-bucket. One example is storing a timestamp to indicate the last time when a node in the bucket was responding to a ping.\n\n#### kBucket.remove(id)\n\n  * `id`: _Uint8Array_ The ID of the `contact` to remove.\n  * Return: _Object_ The k-bucket itself.\n\nRemoves `contact` with the provided `id`.\n\n#### kBucket.toArray()\n\n  * Return: _Array_ All of the contacts in the tree, as an array\n\nTraverses the tree, putting all the contacts into one array.\n\n#### kBucket.toIterable()\n\n  * Return: _Iterable_ All of the contacts in the tree, as an iterable\n\nTraverses the tree, yielding contacts as they are encountered.\n\n#### kBucket._determineNode(node, id [, bitIndex = 0])\n\n_**CAUTION: reserved for internal use**_\n\n  * `node`: internal object that has 2 leafs: left and right\n  * `id`: _Uint8Array_ Id to compare `localNodeId` with.\n  * `bitIndex`: _Integer_ _(Default: 0)_  The bit index to which bit to check in the `id` Uint8Array.\n  * Return: _Object_ left leaf if `id` at `bitIndex` is 0, right leaf otherwise.\n\n#### kBucket._indexOf(id)\n\n_**CAUTION: reserved for internal use**_\n\n  * `node`: internal object that has 2 leafs: left and right\n  * `id`: _Uint8Array_ Contact node id.\n  * Return: _Integer_ Index of `contact` with provided `id` if it exists, -1 otherwise.\n\nReturns the index of the `contact` with provided `id` if it exists, returns -1 otherwise.\n\n#### kBucket._split(node [, bitIndex])\n\n_**CAUTION: reserved for internal use**_\n\n  * `node`: _Object_ node for splitting\n  * `bitIndex`: _Integer_ _(Default: 0)_ The bit index to which bit to check in the `id` Uint8Array.\n\nSplits the node, redistributes contacts to the new nodes, and marks the node that was split as an inner node of the binary tree of nodes by setting `self.contacts = null`. Also, marks the \"far away\" node as `dontSplit`.\n\n#### kBucket._update(node, index, contact)\n\n_**CAUTION: reserved for internal use**_\n\n  * `node`: internal object that has 2 leafs: left and right\n  * `index`: _Integer_ The index in the bucket where contact exists (index has already been computed in previous calculation).\n  * `contact`: _Object_ The contact object to update.\n    * `id`: _Uint8Array_ Contact node id\n    * Any satellite data that is part of the `contact` object will not be altered, only `id` is used.\n\nUpdates the `contact` by using the `arbiter` function to compare the incumbent and the candidate. If `arbiter` function selects the old `contact` but the candidate is some new `contact`, then the new `contact` is abandoned. If `arbiter` function selects the old `contact` and the candidate is that same old `contact`, the `contact` is marked as most recently contacted (by being moved to the right/end of the bucket array). If `arbiter` function selects the new `contact`, the old `contact` is removed and the new `contact` is marked as most recently contacted.\n\n#### Event: 'added'\n\n  * `newContact`: _Object_ The new contact that was added.\n\nEmitted only when `newContact` was added to bucket and it was not stored in the bucket before.\n\n#### Event: 'ping'\n\n  * `oldContacts`: _Array_ The array of contacts to ping.\n  * `newContact`: _Object_ The new contact to be added if one of old contacts does not respond.\n\nEmitted every time a contact is added that would exceed the capacity of a _don't split_ k-bucket it belongs to.\n\n#### Event: 'removed'\n\n  * `contact`: _Object_ The contact that was removed.\n\nEmitted when `contact` was removed from the bucket.\n\n#### Event: 'updated'\n\n  * `oldContact`: _Object_ The contact that was stored prior to the update.\n  * `newContact`: _Object_ The new contact that is now stored after the update.\n\nEmitted when a previously existing (\"previously existing\" means `oldContact.id` equals `newContact.id`) contact was added to the bucket and it was replaced with `newContact`.\n\n## Releases\n\n[Current releases](https://github.com/tristanls/k-bucket/releases).\n\n### Policy\n\nWe follow the semantic versioning policy ([semver.org](http://semver.org/)) with a caveat:\n\n\u003e Given a version number MAJOR.MINOR.PATCH, increment the:\n\u003e\n\u003eMAJOR version when you make incompatible API changes,\u003cbr/\u003e\n\u003eMINOR version when you add functionality in a backwards-compatible manner, and\u003cbr/\u003e\n\u003ePATCH version when you make backwards-compatible bug fixes.\n\n**caveat**: Major version zero is a special case indicating development version that may make incompatible API changes without incrementing MAJOR version.\n\n## Sources\n\nThe implementation has been sourced from:\n\n  - [A formal specification of the Kademlia distributed hash table](http://maude.sip.ucm.es/kademlia/files/pita_kademlia.pdf)\n  - [Distributed Hash Tables (part 2)](https://web.archive.org/web/20140217064545/http://offthelip.org/?p=157)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftristanls%2Fk-bucket","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftristanls%2Fk-bucket","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftristanls%2Fk-bucket/lists"}