@libp2p/kad-dht 12.0.15 → 12.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/index.min.js +4 -4
  2. package/dist/src/constants.d.ts.map +1 -1
  3. package/dist/src/constants.js +1 -1
  4. package/dist/src/constants.js.map +1 -1
  5. package/dist/src/content-routing/index.d.ts.map +1 -1
  6. package/dist/src/content-routing/index.js +3 -2
  7. package/dist/src/content-routing/index.js.map +1 -1
  8. package/dist/src/index.d.ts +34 -1
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js.map +1 -1
  11. package/dist/src/network.d.ts +3 -0
  12. package/dist/src/network.d.ts.map +1 -1
  13. package/dist/src/network.js +33 -8
  14. package/dist/src/network.js.map +1 -1
  15. package/dist/src/peer-list/peer-distance-list.d.ts +13 -4
  16. package/dist/src/peer-list/peer-distance-list.d.ts.map +1 -1
  17. package/dist/src/peer-list/peer-distance-list.js +29 -21
  18. package/dist/src/peer-list/peer-distance-list.js.map +1 -1
  19. package/dist/src/peer-routing/index.d.ts +5 -5
  20. package/dist/src/peer-routing/index.d.ts.map +1 -1
  21. package/dist/src/peer-routing/index.js +15 -24
  22. package/dist/src/peer-routing/index.js.map +1 -1
  23. package/dist/src/query/manager.d.ts +3 -0
  24. package/dist/src/query/manager.d.ts.map +1 -1
  25. package/dist/src/query/manager.js +14 -5
  26. package/dist/src/query/manager.js.map +1 -1
  27. package/dist/src/query/query-path.d.ts +6 -6
  28. package/dist/src/query/query-path.d.ts.map +1 -1
  29. package/dist/src/query/query-path.js +32 -20
  30. package/dist/src/query/query-path.js.map +1 -1
  31. package/dist/src/routing-table/index.d.ts +11 -5
  32. package/dist/src/routing-table/index.d.ts.map +1 -1
  33. package/dist/src/routing-table/index.js +84 -42
  34. package/dist/src/routing-table/index.js.map +1 -1
  35. package/dist/src/routing-table/k-bucket.d.ts +80 -115
  36. package/dist/src/routing-table/k-bucket.d.ts.map +1 -1
  37. package/dist/src/routing-table/k-bucket.js +165 -311
  38. package/dist/src/routing-table/k-bucket.js.map +1 -1
  39. package/dist/src/routing-table/refresh.d.ts.map +1 -1
  40. package/dist/src/routing-table/refresh.js +9 -4
  41. package/dist/src/routing-table/refresh.js.map +1 -1
  42. package/package.json +8 -10
  43. package/src/constants.ts +1 -1
  44. package/src/content-routing/index.ts +3 -2
  45. package/src/index.ts +37 -1
  46. package/src/network.ts +38 -9
  47. package/src/peer-list/peer-distance-list.ts +36 -25
  48. package/src/peer-routing/index.ts +19 -28
  49. package/src/query/manager.ts +18 -5
  50. package/src/query/query-path.ts +46 -30
  51. package/src/routing-table/index.ts +100 -46
  52. package/src/routing-table/k-bucket.ts +214 -359
  53. package/src/routing-table/refresh.ts +10 -4
  54. package/dist/src/query/utils.d.ts +0 -6
  55. package/dist/src/query/utils.d.ts.map +0 -1
  56. package/dist/src/query/utils.js +0 -53
  57. package/dist/src/query/utils.js.map +0 -1
  58. package/src/query/utils.ts +0 -64
@@ -1,33 +1,9 @@
1
- /*
2
- index.js - Kademlia DHT K-bucket implementation as a binary tree.
3
-
4
- The MIT License (MIT)
5
-
6
- Copyright (c) 2013-2021 Tristan Slominski
7
-
8
- Permission is hereby granted, free of charge, to any person
9
- obtaining a copy of this software and associated documentation
10
- files (the "Software"), to deal in the Software without
11
- restriction, including without limitation the rights to use,
12
- copy, modify, merge, publish, distribute, sublicense, and/or sell
13
- copies of the Software, and to permit persons to whom the
14
- Software is furnished to do so, subject to the following
15
- conditions:
16
-
17
- The above copyright notice and this permission notice shall be
18
- included in all copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24
- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
- OTHER DEALINGS IN THE SOFTWARE.
28
- */
29
-
30
1
  import { TypedEventEmitter } from '@libp2p/interface'
2
+ import map from 'it-map'
3
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
+ import { xor as uint8ArrayXor } from 'uint8arrays/xor'
5
+ import { PeerDistanceList } from '../peer-list/peer-distance-list.js'
6
+ import { KBUCKET_SIZE } from './index.js'
31
7
  import type { PeerId } from '@libp2p/interface'
32
8
 
33
9
  function arrayEquals (array1: Uint8Array, array2: Uint8Array): boolean {
@@ -45,233 +21,184 @@ function arrayEquals (array1: Uint8Array, array2: Uint8Array): boolean {
45
21
  return true
46
22
  }
47
23
 
48
- function createNode (): Bucket {
49
- // @ts-expect-error loose types
50
- return { contacts: [], dontSplit: false, left: null, right: null }
51
- }
52
-
53
24
  function ensureInt8 (name: string, val?: Uint8Array): void {
54
25
  if (!(val instanceof Uint8Array)) {
55
26
  throw new TypeError(name + ' is not a Uint8Array')
56
27
  }
57
- }
58
28
 
59
- export interface PingEventDetails {
60
- oldContacts: Contact[]
61
- newContact: Contact
29
+ if (val.byteLength !== 32) {
30
+ throw new TypeError(name + ' had incorrect length')
31
+ }
62
32
  }
63
33
 
64
- export interface UpdatedEventDetails {
65
- incumbent: Contact
66
- selection: Contact
34
+ export interface PingEventDetails {
35
+ oldContacts: Peer[]
36
+ newContact: Peer
67
37
  }
68
38
 
69
39
  export interface KBucketEvents {
70
40
  'ping': CustomEvent<PingEventDetails>
71
- 'added': CustomEvent<Contact>
72
- 'removed': CustomEvent<Contact>
73
- 'updated': CustomEvent<UpdatedEventDetails>
41
+ 'added': CustomEvent<Peer>
42
+ 'removed': CustomEvent<Peer>
74
43
  }
75
44
 
76
45
  export interface KBucketOptions {
77
46
  /**
78
- * A Uint8Array representing the local node id
47
+ * The current peer. All subsequently added peers must have a KadID that is
48
+ * the same length as this peer.
79
49
  */
80
- localNodeId: Uint8Array
50
+ localPeer: Peer
81
51
 
82
52
  /**
83
- * The number of nodes that a k-bucket can contain before being full or split.
53
+ * How many bits of the key to use when forming the bucket trie. The larger
54
+ * this value, the deeper the tree will grow and the slower the lookups will
55
+ * be but the peers returned will be more specific to the key.
84
56
  */
85
- numberOfNodesPerKBucket?: number
57
+ prefixLength: number
86
58
 
87
59
  /**
88
- * The number of nodes to ping when a bucket that should not be split becomes
89
- * full. KBucket will emit a `ping` event that contains `numberOfNodesToPing`
90
- * nodes that have not been contacted the longest.
60
+ * The number of nodes that a max-depth k-bucket can contain before being
61
+ * full.
62
+ *
63
+ * @default 20
91
64
  */
92
- numberOfNodesToPing?: number
65
+ kBucketSize?: number
93
66
 
94
67
  /**
95
- * An optional `distance` function that gets two `id` Uint8Arrays and return
96
- * distance (as number) between them.
68
+ * The number of nodes that an intermediate k-bucket can contain before being
69
+ * split.
70
+ *
71
+ * @default kBucketSize
97
72
  */
98
- distance?(a: Uint8Array, b: Uint8Array): number
73
+ splitThreshold?: number
99
74
 
100
75
  /**
101
- * An optional `arbiter` function that given two `contact` objects with the
102
- * same `id` returns the desired object to be used for updating the k-bucket.
103
- * For more details, see [arbiter function](#arbiter-function).
76
+ * The number of nodes to ping when a bucket that should not be split becomes
77
+ * full. KBucket will emit a `ping` event that contains `numberOfNodesToPing`
78
+ * nodes that have not been contacted the longest.
104
79
  */
105
- arbiter?(incumbent: Contact, candidate: Contact): Contact
80
+ numberOfNodesToPing?: number
81
+ }
82
+
83
+ export interface Peer {
84
+ kadId: Uint8Array
85
+ peerId: PeerId
106
86
  }
107
87
 
108
- export interface Contact {
109
- id: Uint8Array
110
- peer: PeerId
111
- vectorClock?: number
88
+ export interface LeafBucket {
89
+ prefix: string
90
+ depth: number
91
+ peers: Peer[]
112
92
  }
113
93
 
114
- export interface Bucket {
115
- id: Uint8Array
116
- contacts: Contact[]
117
- dontSplit: boolean
94
+ export interface InternalBucket {
95
+ prefix: string
96
+ depth: number
118
97
  left: Bucket
119
98
  right: Bucket
120
99
  }
121
100
 
101
+ export type Bucket = LeafBucket | InternalBucket
102
+
103
+ export function isLeafBucket (obj: any): obj is LeafBucket {
104
+ return Array.isArray(obj?.peers)
105
+ }
106
+
122
107
  /**
123
- * Implementation of a Kademlia DHT k-bucket used for storing
124
- * contact (peer node) information.
108
+ * Implementation of a Kademlia DHT routing table as a prefix binary trie with
109
+ * configurable prefix length, bucket split threshold and size.
125
110
  */
126
111
  export class KBucket extends TypedEventEmitter<KBucketEvents> {
127
- public localNodeId: Uint8Array
128
112
  public root: Bucket
129
- private readonly numberOfNodesPerKBucket: number
113
+ public localPeer: Peer
114
+ private readonly prefixLength: number
115
+ private readonly splitThreshold: number
116
+ private readonly kBucketSize: number
130
117
  private readonly numberOfNodesToPing: number
131
- private readonly distance: (a: Uint8Array, b: Uint8Array) => number
132
- private readonly arbiter: (incumbent: Contact, candidate: Contact) => Contact
133
118
 
134
119
  constructor (options: KBucketOptions) {
135
120
  super()
136
121
 
137
- this.localNodeId = options.localNodeId
138
- this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket ?? 20
122
+ this.localPeer = options.localPeer
123
+ this.prefixLength = options.prefixLength
124
+ this.kBucketSize = options.kBucketSize ?? KBUCKET_SIZE
125
+ this.splitThreshold = options.splitThreshold ?? this.kBucketSize
139
126
  this.numberOfNodesToPing = options.numberOfNodesToPing ?? 3
140
- this.distance = options.distance ?? KBucket.distance
141
- // use an arbiter from options or vectorClock arbiter by default
142
- this.arbiter = options.arbiter ?? KBucket.arbiter
143
127
 
144
- ensureInt8('option.localNodeId as parameter 1', this.localNodeId)
145
-
146
- this.root = createNode()
147
- }
148
-
149
- /**
150
- * Default arbiter function for contacts with the same id. Uses
151
- * contact.vectorClock to select which contact to update the k-bucket with.
152
- * Contact with larger vectorClock field will be selected. If vectorClock is
153
- * the same, candidate will be selected.
154
- *
155
- * @param {object} incumbent - Contact currently stored in the k-bucket.
156
- * @param {object} candidate - Contact being added to the k-bucket.
157
- * @returns {object} Contact to updated the k-bucket with.
158
- */
159
- static arbiter (incumbent: Contact, candidate: Contact): Contact {
160
- return (incumbent.vectorClock ?? 0) > (candidate.vectorClock ?? 0) ? incumbent : candidate
161
- }
128
+ ensureInt8('options.localPeer.kadId', options.localPeer.kadId)
162
129
 
163
- /**
164
- * Default distance function. Finds the XOR
165
- * distance between firstId and secondId.
166
- *
167
- * @param {Uint8Array} firstId - Uint8Array containing first id.
168
- * @param {Uint8Array} secondId - Uint8Array containing second id.
169
- * @returns {number} Integer The XOR distance between firstId and secondId.
170
- */
171
- static distance (firstId: Uint8Array, secondId: Uint8Array): number {
172
- let distance = 0
173
- let i = 0
174
- const min = Math.min(firstId.length, secondId.length)
175
- const max = Math.max(firstId.length, secondId.length)
176
- for (; i < min; ++i) {
177
- distance = distance * 256 + (firstId[i] ^ secondId[i])
130
+ this.root = {
131
+ prefix: '',
132
+ depth: 0,
133
+ peers: []
178
134
  }
179
- for (; i < max; ++i) distance = distance * 256 + 255
180
- return distance
181
135
  }
182
136
 
183
137
  /**
184
138
  * Adds a contact to the k-bucket.
185
139
  *
186
- * @param {object} contact - the contact object to add
140
+ * @param {Peer} peer - the contact object to add
187
141
  */
188
- add (contact: Contact): KBucket {
189
- ensureInt8('contact.id', contact?.id)
142
+ add (peer: Peer): void {
143
+ ensureInt8('peer.kadId', peer?.kadId)
190
144
 
191
- let bitIndex = 0
192
- let node = this.root
193
-
194
- while (node.contacts === null) {
195
- // this is not a leaf node but an inner node with 'low' and 'high'
196
- // branches; we will check the appropriate bit of the identifier and
197
- // delegate to the appropriate node for further processing
198
- node = this._determineNode(node, contact.id, bitIndex++)
199
- }
145
+ const bucket = this._determineBucket(peer.kadId)
200
146
 
201
147
  // check if the contact already exists
202
- const index = this._indexOf(node, contact.id)
203
- if (index >= 0) {
204
- this._update(node, index, contact)
205
- return this
148
+ if (this._indexOf(bucket, peer.kadId) > -1) {
149
+ return
206
150
  }
207
151
 
208
- if (node.contacts.length < this.numberOfNodesPerKBucket) {
209
- node.contacts.push(contact)
210
- this.safeDispatchEvent('added', { detail: contact })
211
- return this
152
+ // are there too many peers in the bucket and can we make the trie deeper?
153
+ if (bucket.peers.length === this.splitThreshold && bucket.depth < this.prefixLength) {
154
+ // split the bucket
155
+ this._split(bucket)
156
+
157
+ // try again
158
+ this.add(peer)
159
+
160
+ return
212
161
  }
213
162
 
214
- // the bucket is full
215
- if (node.dontSplit) {
216
- // we are not allowed to split the bucket
217
- // we need to ping the first this.numberOfNodesToPing
218
- // in order to determine if they are alive
219
- // only if one of the pinged nodes does not respond, can the new contact
220
- // be added (this prevents DoS flodding with new invalid contacts)
221
- this.safeDispatchEvent('ping', {
222
- detail: {
223
- oldContacts: node.contacts.slice(0, this.numberOfNodesToPing),
224
- newContact: contact
225
- }
226
- })
227
- return this
163
+ // is there space in the bucket?
164
+ if (bucket.peers.length < this.kBucketSize) {
165
+ bucket.peers.push(peer)
166
+ this.safeDispatchEvent('added', { detail: peer })
167
+
168
+ return
228
169
  }
229
170
 
230
- this._split(node, bitIndex)
231
- return this.add(contact)
171
+ // we are at the bottom of the trie and the bucket is full so we can't add
172
+ // any more peers.
173
+ //
174
+ // instead ping the first this.numberOfNodesToPing in order to determine
175
+ // if they are still online.
176
+ //
177
+ // only add the new peer if one of the pinged nodes does not respond, this
178
+ // prevents DoS flooding with new invalid contacts.
179
+ this.safeDispatchEvent('ping', {
180
+ detail: {
181
+ oldContacts: bucket.peers.slice(0, this.numberOfNodesToPing),
182
+ newContact: peer
183
+ }
184
+ })
232
185
  }
233
186
 
234
187
  /**
235
- * Get the n closest contacts to the provided node id. "Closest" here means:
188
+ * Get 0-n closest contacts to the provided node id. "Closest" here means:
236
189
  * closest according to the XOR metric of the contact node id.
237
190
  *
238
191
  * @param {Uint8Array} id - Contact node id
239
- * @param {number} n - Integer (Default: Infinity) The maximum number of closest contacts to return
240
- * @returns {Array} Array Maximum of n closest contacts to the node id
192
+ * @returns {Generator<Peer, void, undefined>} Array Maximum of n closest contacts to the node id
241
193
  */
242
- closest (id: Uint8Array, n = Infinity): Contact[] {
243
- ensureInt8('id', id)
244
-
245
- if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) {
246
- throw new TypeError('n is not positive number')
247
- }
248
-
249
- let contacts: Contact[] = []
194
+ * closest (id: Uint8Array, n: number = this.kBucketSize): Generator<PeerId, void, undefined> {
195
+ const list = new PeerDistanceList(id, n)
250
196
 
251
- for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) {
252
- const node = nodes.pop()
253
-
254
- if (node == null) {
255
- continue
256
- }
257
-
258
- if (node.contacts === null) {
259
- const detNode = this._determineNode(node, id, bitIndex++)
260
- nodes.push(node.left === detNode ? node.right : node.left)
261
- nodes.push(detNode)
262
- } else {
263
- contacts = contacts.concat(node.contacts)
264
- }
197
+ for (const peer of this.toIterable()) {
198
+ list.addWitKadId({ id: peer.peerId, multiaddrs: [] }, peer.kadId)
265
199
  }
266
200
 
267
- return contacts
268
- .map(a => ({
269
- distance: this.distance(a.id, id),
270
- contact: a
271
- }))
272
- .sort((a, b) => a.distance - b.distance)
273
- .slice(0, n)
274
- .map(a => a.contact)
201
+ yield * map(list.peers, info => info.id)
275
202
  }
276
203
 
277
204
  /**
@@ -280,65 +207,25 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
280
207
  * @returns {number} The number of contacts held in the tree
281
208
  */
282
209
  count (): number {
283
- // return this.toArray().length
284
- let count = 0
285
- for (const nodes = [this.root]; nodes.length > 0;) {
286
- const node = nodes.pop()
287
-
288
- if (node == null) {
289
- continue
210
+ function countBucket (bucket: Bucket): number {
211
+ if (isLeafBucket(bucket)) {
212
+ return bucket.peers.length
290
213
  }
291
214
 
292
- if (node.contacts === null) {
293
- nodes.push(node.right, node.left)
294
- } else {
295
- count += node.contacts.length
296
- }
297
- }
215
+ let count = 0
298
216
 
299
- return count
300
- }
217
+ if (bucket.left != null) {
218
+ count += countBucket(bucket.left)
219
+ }
301
220
 
302
- /**
303
- * Determines whether the id at the bitIndex is 0 or 1.
304
- * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise
305
- *
306
- * @param {object} node - internal object that has 2 leafs: left and right
307
- * @param {Uint8Array} id - Id to compare localNodeId with.
308
- * @param {number} bitIndex - Integer (Default: 0) The bit index to which bit to check in the id Uint8Array.
309
- * @returns {object} left leaf if id at bitIndex is 0, right leaf otherwise.
310
- */
311
- _determineNode (node: any, id: Uint8Array, bitIndex: number): Bucket {
312
- // **NOTE** remember that id is a Uint8Array and has granularity of
313
- // bytes (8 bits), whereas the bitIndex is the _bit_ index (not byte)
314
-
315
- // id's that are too short are put in low bucket (1 byte = 8 bits)
316
- // (bitIndex >> 3) finds how many bytes the bitIndex describes
317
- // bitIndex % 8 checks if we have extra bits beyond byte multiples
318
- // if number of bytes is <= no. of bytes described by bitIndex and there
319
- // are extra bits to consider, this means id has less bits than what
320
- // bitIndex describes, id therefore is too short, and will be put in low
321
- // bucket
322
- const bytesDescribedByBitIndex = bitIndex >> 3
323
- const bitIndexWithinByte = bitIndex % 8
324
- if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) {
325
- return node.left
326
- }
221
+ if (bucket.right != null) {
222
+ count += countBucket(bucket.right)
223
+ }
327
224
 
328
- const byteUnderConsideration = id[bytesDescribedByBitIndex]
329
-
330
- // byteUnderConsideration is an integer from 0 to 255 represented by 8 bits
331
- // where 255 is 11111111 and 0 is 00000000
332
- // in order to find out whether the bit at bitIndexWithinByte is set
333
- // we construct (1 << (7 - bitIndexWithinByte)) which will consist
334
- // of all bits being 0, with only one bit set to 1
335
- // for example, if bitIndexWithinByte is 3, we will construct 00010000 by
336
- // (1 << (7 - 3)) -> (1 << 4) -> 16
337
- if ((byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) !== 0) {
338
- return node.right
225
+ return count
339
226
  }
340
227
 
341
- return node.left
228
+ return countBucket(this.root)
342
229
  }
343
230
 
344
231
  /**
@@ -347,175 +234,143 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
347
234
  * contact if we have it or null if not. If this is an inner node, determine
348
235
  * which branch of the tree to traverse and repeat.
349
236
  *
350
- * @param {Uint8Array} id - The ID of the contact to fetch.
351
- * @returns {object | null} The contact if available, otherwise null
352
- */
353
- get (id: Uint8Array): Contact | undefined {
354
- ensureInt8('id', id)
355
-
356
- let bitIndex = 0
357
-
358
- let node: Bucket = this.root
359
- while (node.contacts === null) {
360
- node = this._determineNode(node, id, bitIndex++)
361
- }
362
-
363
- // index of uses contact id for matching
364
- const index = this._indexOf(node, id)
365
- return index >= 0 ? node.contacts[index] : undefined
366
- }
367
-
368
- /**
369
- * Returns the index of the contact with provided
370
- * id if it exists, returns -1 otherwise.
371
- *
372
- * @param {object} node - internal object that has 2 leafs: left and right
373
- * @param {Uint8Array} id - Contact node id.
374
- * @returns {number} Integer Index of contact with provided id if it exists, -1 otherwise.
237
+ * @param {Uint8Array} kadId - The ID of the contact to fetch.
238
+ * @returns {object | undefined} The contact if available, otherwise null
375
239
  */
376
- _indexOf (node: Bucket, id: Uint8Array): number {
377
- for (let i = 0; i < node.contacts.length; ++i) {
378
- if (arrayEquals(node.contacts[i].id, id)) return i
379
- }
240
+ get (kadId: Uint8Array): Peer | undefined {
241
+ const bucket = this._determineBucket(kadId)
242
+ const index = this._indexOf(bucket, kadId)
380
243
 
381
- return -1
244
+ return bucket.peers[index]
382
245
  }
383
246
 
384
247
  /**
385
248
  * Removes contact with the provided id.
386
249
  *
387
- * @param {Uint8Array} id - The ID of the contact to remove
388
- * @returns {object} The k-bucket itself
250
+ * @param {Uint8Array} kadId - The ID of the contact to remove
389
251
  */
390
- remove (id: Uint8Array): KBucket {
391
- ensureInt8('the id as parameter 1', id)
392
-
393
- let bitIndex = 0
394
- let node = this.root
395
-
396
- while (node.contacts === null) {
397
- node = this._determineNode(node, id, bitIndex++)
398
- }
252
+ remove (kadId: Uint8Array): void {
253
+ const bucket = this._determineBucket(kadId)
254
+ const index = this._indexOf(bucket, kadId)
399
255
 
400
- const index = this._indexOf(node, id)
401
- if (index >= 0) {
402
- const contact = node.contacts.splice(index, 1)[0]
256
+ if (index > -1) {
257
+ const peer = bucket.peers.splice(index, 1)[0]
403
258
  this.safeDispatchEvent('removed', {
404
- detail: contact
259
+ detail: peer
405
260
  })
406
261
  }
407
-
408
- return this
409
262
  }
410
263
 
411
264
  /**
412
- * Splits the node, redistributes contacts to the new nodes, and marks the
413
- * node that was split as an inner node of the binary tree of nodes by
414
- * setting this.root.contacts = null
265
+ * Similar to `toArray()` but instead of buffering everything up into an
266
+ * array before returning it, yields contacts as they are encountered while
267
+ * walking the tree.
415
268
  *
416
- * @param {object} node - node for splitting
417
- * @param {number} bitIndex - the bitIndex to which byte to check in the Uint8Array for navigating the binary tree
269
+ * @returns {Iterable} All of the contacts in the tree, as an iterable
418
270
  */
419
- _split (node: Bucket, bitIndex: number): void {
420
- node.left = createNode()
421
- node.right = createNode()
271
+ * toIterable (): Generator<Peer, void, undefined> {
272
+ function * iterate (bucket: Bucket): Generator<Peer, void, undefined> {
273
+ if (isLeafBucket(bucket)) {
274
+ yield * bucket.peers
275
+ return
276
+ }
422
277
 
423
- // redistribute existing contacts amongst the two newly created nodes
424
- for (const contact of node.contacts) {
425
- this._determineNode(node, contact.id, bitIndex).contacts.push(contact)
278
+ yield * iterate(bucket.left)
279
+ yield * iterate(bucket.right)
426
280
  }
427
281
 
428
- // @ts-expect-error loose types
429
- node.contacts = null // mark as inner tree node
282
+ yield * iterate(this.root)
283
+ }
430
284
 
431
- // don't split the "far away" node
432
- // we check where the local node would end up and mark the other one as
433
- // "dontSplit" (i.e. "far away")
434
- const detNode = this._determineNode(node, this.localNodeId, bitIndex)
435
- const otherNode = node.left === detNode ? node.right : node.left
436
- otherNode.dontSplit = true
285
+ /**
286
+ * Default distance function. Finds the XOR distance between firstId and
287
+ * secondId.
288
+ *
289
+ * @param {Uint8Array} firstId - Uint8Array containing first id.
290
+ * @param {Uint8Array} secondId - Uint8Array containing second id.
291
+ * @returns {number} Integer The XOR distance between firstId and secondId.
292
+ */
293
+ distance (firstId: Uint8Array, secondId: Uint8Array): bigint {
294
+ return BigInt('0x' + uint8ArrayToString(uint8ArrayXor(firstId, secondId), 'base16'))
437
295
  }
438
296
 
439
297
  /**
440
- * Returns all the contacts contained in the tree as an array.
441
- * If this is a leaf, return a copy of the bucket. If this is not a leaf,
442
- * return the union of the low and high branches (themselves also as arrays).
298
+ * Determines whether the id at the bitIndex is 0 or 1
299
+ * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise
443
300
  *
444
- * @returns {Array} All of the contacts in the tree, as an array
301
+ * @param {Uint8Array} kadId - Id to compare localNodeId with
302
+ * @returns {LeafBucket} left leaf if id at bitIndex is 0, right leaf otherwise.
445
303
  */
446
- toArray (): Contact[] {
447
- let result: Contact[] = []
448
- for (const nodes = [this.root]; nodes.length > 0;) {
449
- const node = nodes.pop()
304
+ private _determineBucket (kadId: Uint8Array): LeafBucket {
305
+ const bitString = uint8ArrayToString(kadId, 'base2')
306
+ const prefix = bitString.substring(0, this.prefixLength)
450
307
 
451
- if (node == null) {
452
- continue
308
+ function findBucket (bucket: Bucket, bitIndex: number = 0): LeafBucket {
309
+ if (isLeafBucket(bucket)) {
310
+ return bucket
453
311
  }
454
312
 
455
- if (node.contacts === null) {
456
- nodes.push(node.right, node.left)
457
- } else {
458
- result = result.concat(node.contacts)
313
+ const bit = prefix[bitIndex]
314
+
315
+ if (bit === '0') {
316
+ return findBucket(bucket.left, bitIndex + 1)
459
317
  }
318
+
319
+ return findBucket(bucket.right, bitIndex + 1)
460
320
  }
461
- return result
321
+
322
+ return findBucket(this.root)
462
323
  }
463
324
 
464
325
  /**
465
- * Similar to `toArray()` but instead of buffering everything up into an
466
- * array before returning it, yields contacts as they are encountered while
467
- * walking the tree.
326
+ * Returns the index of the contact with provided
327
+ * id if it exists, returns -1 otherwise.
468
328
  *
469
- * @returns {Iterable} All of the contacts in the tree, as an iterable
329
+ * @param {object} bucket - internal object that has 2 leafs: left and right
330
+ * @param {Uint8Array} kadId - KadId of peer
331
+ * @returns {number} Integer Index of contact with provided id if it exists, -1 otherwise.
470
332
  */
471
- * toIterable (): Iterable<Contact> {
472
- for (const nodes = [this.root]; nodes.length > 0;) {
473
- const node = nodes.pop()
474
-
475
- if (node == null) {
476
- continue
477
- }
478
-
479
- if (node.contacts === null) {
480
- nodes.push(node.right, node.left)
481
- } else {
482
- yield * node.contacts
483
- }
484
- }
333
+ private _indexOf (bucket: LeafBucket, kadId: Uint8Array): number {
334
+ return bucket.peers.findIndex(peer => arrayEquals(peer.kadId, kadId))
485
335
  }
486
336
 
487
337
  /**
488
- * Updates the contact selected by the arbiter.
489
- * If the selection is our old contact and the candidate is some new contact
490
- * then the new contact is abandoned (not added).
491
- * If the selection is our old contact and the candidate is our old contact
492
- * then we are refreshing the contact and it is marked as most recently
493
- * contacted (by being moved to the right/end of the bucket array).
494
- * If the selection is our new contact, the old contact is removed and the new
495
- * contact is marked as most recently contacted.
338
+ * Modify the bucket, turn it from a leaf bucket to an internal bucket
496
339
  *
497
- * @param {object} node - internal object that has 2 leafs: left and right
498
- * @param {number} index - the index in the bucket where contact exists (index has already been computed in a previous calculation)
499
- * @param {object} contact - The contact object to update
340
+ * @param {any} bucket - bucket for splitting
500
341
  */
501
- _update (node: Bucket, index: number, contact: Contact): void {
502
- // sanity check
503
- if (!arrayEquals(node.contacts[index].id, contact.id)) {
504
- throw new Error('wrong index for _update')
342
+ private _split (bucket: LeafBucket): void {
343
+ const depth = bucket.depth + 1
344
+
345
+ // create child buckets
346
+ const left: LeafBucket = {
347
+ prefix: '0',
348
+ depth,
349
+ peers: []
350
+ }
351
+ const right: LeafBucket = {
352
+ prefix: '1',
353
+ depth,
354
+ peers: []
505
355
  }
506
356
 
507
- const incumbent = node.contacts[index]
508
- const selection = this.arbiter(incumbent, contact)
509
- // if the selection is our old contact and the candidate is some new
510
- // contact, then there is nothing to do
511
- if (selection === incumbent && incumbent !== contact) return
357
+ // redistribute peers
358
+ for (const peer of bucket.peers) {
359
+ const bitString = uint8ArrayToString(peer.kadId, 'base2')
512
360
 
513
- node.contacts.splice(index, 1) // remove old contact
514
- node.contacts.push(selection) // add more recent contact version
515
- this.safeDispatchEvent('updated', {
516
- detail: {
517
- incumbent, selection
361
+ if (bitString[depth] === '0') {
362
+ left.peers.push(peer)
363
+ } else {
364
+ right.peers.push(peer)
518
365
  }
519
- })
366
+ }
367
+
368
+ // convert leaf bucket to internal bucket
369
+ // @ts-expect-error peers is not a property of LeafBucket
370
+ delete bucket.peers
371
+ // @ts-expect-error left is not a property of LeafBucket
372
+ bucket.left = left
373
+ // @ts-expect-error right is not a property of LeafBucket
374
+ bucket.right = right
520
375
  }
521
376
  }