@helia/bitswap 0.0.0 → 1.0.0

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.
package/src/want-list.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { AbortError } from '@libp2p/interface'
2
- import { trackedPeerMap, PeerSet } from '@libp2p/peer-collections'
1
+ import { TypedEventEmitter, setMaxListeners } from '@libp2p/interface'
2
+ import { trackedPeerMap } from '@libp2p/peer-collections'
3
3
  import { trackedMap } from '@libp2p/utils/tracked-map'
4
4
  import all from 'it-all'
5
5
  import filter from 'it-filter'
@@ -8,14 +8,16 @@ import { pipe } from 'it-pipe'
8
8
  import { CID } from 'multiformats/cid'
9
9
  import { sha256 } from 'multiformats/hashes/sha2'
10
10
  import pDefer from 'p-defer'
11
+ import { raceEvent } from 'race-event'
12
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
11
13
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
12
14
  import { DEFAULT_MESSAGE_SEND_DELAY } from './constants.js'
13
15
  import { BlockPresenceType, WantType } from './pb/message.js'
14
16
  import vd from './utils/varint-decoder.js'
15
- import type { MultihashHasherLoader } from './index.js'
17
+ import type { BitswapNotifyProgressEvents, MultihashHasherLoader } from './index.js'
16
18
  import type { BitswapNetworkWantProgressEvents, Network } from './network.js'
17
19
  import type { BitswapMessage } from './pb/message.js'
18
- import type { ComponentLogger, Metrics, PeerId, Startable, AbortOptions } from '@libp2p/interface'
20
+ import type { ComponentLogger, PeerId, Startable, AbortOptions, Libp2p, TypedEventTarget } from '@libp2p/interface'
19
21
  import type { Logger } from '@libp2p/logger'
20
22
  import type { PeerMap } from '@libp2p/peer-collections'
21
23
  import type { DeferredPromise } from 'p-defer'
@@ -24,7 +26,7 @@ import type { ProgressOptions } from 'progress-events'
24
26
  export interface WantListComponents {
25
27
  network: Network
26
28
  logger: ComponentLogger
27
- metrics?: Metrics
29
+ libp2p: Libp2p
28
30
  }
29
31
 
30
32
  export interface WantListInit {
@@ -58,29 +60,9 @@ export interface WantListEntry {
58
60
  * Whether the remote should tell us if they have the block or not
59
61
  */
60
62
  sendDontHave: boolean
61
-
62
- /**
63
- * If this set has members, the want will only be sent to these peers
64
- */
65
- session: PeerSet
66
-
67
- /**
68
- * Promises returned from `.wantBlock` for this block
69
- */
70
- blockWantListeners: Array<DeferredPromise<WantBlockResult>>
71
-
72
- /**
73
- * Promises returned from `.wantPresence` for this block
74
- */
75
- blockPresenceListeners: Array<DeferredPromise<WantPresenceResult>>
76
63
  }
77
64
 
78
65
  export interface WantOptions extends AbortOptions, ProgressOptions<BitswapNetworkWantProgressEvents> {
79
- /**
80
- * If set, this WantList entry will only be sent to this peer
81
- */
82
- peerId?: PeerId
83
-
84
66
  /**
85
67
  * Allow prioritising blocks
86
68
  */
@@ -108,7 +90,12 @@ export interface WantHaveResult {
108
90
 
109
91
  export type WantPresenceResult = WantDontHaveResult | WantHaveResult
110
92
 
111
- export class WantList implements Startable {
93
+ export interface WantListEvents {
94
+ block: CustomEvent<WantBlockResult>
95
+ presence: CustomEvent<WantPresenceResult>
96
+ }
97
+
98
+ export class WantList extends TypedEventEmitter<WantListEvents> implements Startable, TypedEventTarget<WantListEvents> {
112
99
  /**
113
100
  * Tracks what CIDs we've previously sent to which peers
114
101
  */
@@ -119,15 +106,19 @@ export class WantList implements Startable {
119
106
  private readonly sendMessagesDelay: number
120
107
  private sendMessagesTimeout?: ReturnType<typeof setTimeout>
121
108
  private readonly hashLoader?: MultihashHasherLoader
109
+ private sendingMessages?: DeferredPromise<void>
122
110
 
123
111
  constructor (components: WantListComponents, init: WantListInit = {}) {
112
+ super()
113
+
114
+ setMaxListeners(Infinity, this)
124
115
  this.peers = trackedPeerMap({
125
- name: 'ipfs_bitswap_peers',
126
- metrics: components.metrics
116
+ name: 'helia_bitswap_peers',
117
+ metrics: components.libp2p.metrics
127
118
  })
128
119
  this.wants = trackedMap({
129
- name: 'ipfs_bitswap_wantlist',
130
- metrics: components.metrics
120
+ name: 'helia_bitswap_wantlist',
121
+ metrics: components.libp2p.metrics
131
122
  })
132
123
  this.network = components.network
133
124
  this.sendMessagesDelay = init.sendMessagesDelay ?? DEFAULT_MESSAGE_SEND_DELAY
@@ -160,17 +151,10 @@ export class WantList implements Startable {
160
151
  if (entry == null) {
161
152
  entry = {
162
153
  cid,
163
- session: new PeerSet(),
164
154
  priority: options.priority ?? 1,
165
155
  wantType: options.wantType ?? WantType.WantBlock,
166
156
  cancel: false,
167
- sendDontHave: true,
168
- blockWantListeners: [],
169
- blockPresenceListeners: []
170
- }
171
-
172
- if (options.peerId != null) {
173
- entry.session.add(options.peerId)
157
+ sendDontHave: true
174
158
  }
175
159
 
176
160
  this.wants.set(cidStr, entry)
@@ -182,46 +166,41 @@ export class WantList implements Startable {
182
166
  entry.wantType = WantType.WantBlock
183
167
  }
184
168
 
185
- // if this want was part of a session..
186
- if (entry.session.size > 0) {
187
- // if the new want is also part of a session, expand the want session to
188
- // include both sets of peers
189
- if (options.peerId != null) {
190
- entry.session.add(options.peerId)
191
- }
192
-
193
- // if the new want is not part of a session, make this want a non-session
194
- // want - nb. this will cause this WantList entry to be sent to every peer
195
- // instead of just the ones in the session
196
- if (options.peerId == null) {
197
- entry.session.clear()
198
- }
199
- }
200
-
201
- // add a promise that will be resolved or rejected when the response arrives
202
- let deferred: DeferredPromise<WantBlockResult | WantPresenceResult>
203
-
204
- if (options.wantType === WantType.WantBlock) {
205
- const p = deferred = pDefer<WantBlockResult>()
169
+ // broadcast changes
170
+ await this.sendMessagesDebounced()
206
171
 
207
- entry.blockWantListeners.push(p)
208
- } else {
209
- const p = deferred = pDefer<WantPresenceResult>()
172
+ try {
173
+ if (options.wantType === WantType.WantBlock) {
174
+ const event = await raceEvent<CustomEvent<WantBlockResult>>(this, 'block', options?.signal, {
175
+ filter: (event) => {
176
+ return uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
177
+ },
178
+ errorMessage: 'Want was aborted'
179
+ })
210
180
 
211
- entry.blockPresenceListeners.push(p)
212
- }
181
+ return event.detail
182
+ }
213
183
 
214
- // reject the promise if the want is rejected
215
- const abortListener = (): void => {
216
- this.log('want for %c was aborted, cancelling want', cid)
184
+ const event = await raceEvent<CustomEvent<WantPresenceResult>>(this, 'presence', options?.signal, {
185
+ filter: (event) => {
186
+ return uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
187
+ },
188
+ errorMessage: 'Want was aborted'
189
+ })
217
190
 
218
- if (entry != null) {
191
+ return event.detail
192
+ } finally {
193
+ if (options.signal?.aborted === true) {
194
+ this.log('want for %c was aborted, cancelling want', cid)
219
195
  entry.cancel = true
196
+ // broadcast changes
197
+ await this.sendMessagesDebounced()
220
198
  }
221
-
222
- deferred.reject(new AbortError('Want was aborted'))
223
199
  }
224
- options.signal?.addEventListener('abort', abortListener)
200
+ }
201
+
202
+ private async sendMessagesDebounced (): Promise<void> {
203
+ await this.sendingMessages?.promise
225
204
 
226
205
  // broadcast changes
227
206
  clearTimeout(this.sendMessagesTimeout)
@@ -231,77 +210,65 @@ export class WantList implements Startable {
231
210
  this.log('error sending messages to peers', err)
232
211
  })
233
212
  }, this.sendMessagesDelay)
234
-
235
- try {
236
- return await deferred.promise
237
- } finally {
238
- // remove listener
239
- options.signal?.removeEventListener('abort', abortListener)
240
- // remove deferred promise
241
- if (options.wantType === WantType.WantBlock) {
242
- entry.blockWantListeners = entry.blockWantListeners.filter(recipient => recipient !== deferred)
243
- } else {
244
- entry.blockPresenceListeners = entry.blockPresenceListeners.filter(recipient => recipient !== deferred)
245
- }
246
- }
247
213
  }
248
214
 
249
215
  private async sendMessages (): Promise<void> {
250
- for (const [peerId, sentWants] of this.peers) {
251
- const sent = new Set<string>()
252
- const message: Partial<BitswapMessage> = {
253
- wantlist: {
254
- full: false,
255
- entries: pipe(
256
- this.wants.entries(),
257
- (source) => filter(source, ([key, entry]) => {
258
- // skip session-only wants
259
- if (entry.session.size > 0 && !entry.session.has(peerId)) {
260
- return false
261
- }
262
-
263
- const sentPreviously = sentWants.has(key)
264
-
265
- // don't cancel if we've not sent it to them before
266
- if (entry.cancel) {
267
- return sentPreviously
268
- }
269
-
270
- // only send if we've not sent it to them before
271
- return !sentPreviously
272
- }),
273
- (source) => map(source, ([key, entry]) => {
274
- sent.add(key)
275
-
276
- return {
277
- cid: entry.cid.bytes,
278
- priority: entry.priority,
279
- wantType: entry.wantType,
280
- cancel: entry.cancel,
281
- sendDontHave: entry.sendDontHave
282
- }
283
- }),
284
- (source) => all(source)
285
- )
216
+ this.sendingMessages = pDefer()
217
+
218
+ await Promise.all(
219
+ [...this.peers.entries()].map(async ([peerId, sentWants]) => {
220
+ const sent = new Set<string>()
221
+ const message: Partial<BitswapMessage> = {
222
+ wantlist: {
223
+ full: false,
224
+ entries: pipe(
225
+ this.wants.entries(),
226
+ (source) => filter(source, ([key, entry]) => {
227
+ const sentPreviously = sentWants.has(key)
228
+
229
+ // don't cancel if we've not sent it to them before
230
+ if (entry.cancel) {
231
+ return sentPreviously
232
+ }
233
+
234
+ // only send if we've not sent it to them before
235
+ return !sentPreviously
236
+ }),
237
+ (source) => map(source, ([key, entry]) => {
238
+ sent.add(key)
239
+
240
+ return {
241
+ cid: entry.cid.bytes,
242
+ priority: entry.priority,
243
+ wantType: entry.wantType,
244
+ cancel: entry.cancel,
245
+ sendDontHave: entry.sendDontHave
246
+ }
247
+ }),
248
+ (source) => all(source)
249
+ )
250
+ }
286
251
  }
287
- }
288
252
 
289
- if (message.wantlist?.entries.length === 0) {
290
- return
291
- }
253
+ if (message.wantlist?.entries.length === 0) {
254
+ return
255
+ }
292
256
 
293
- // add message to send queue
294
- try {
295
- await this.network.sendMessage(peerId, message)
257
+ // add message to send queue
258
+ try {
259
+ await this.network.sendMessage(peerId, message)
296
260
 
297
- // update list of messages sent to remote
298
- for (const key of sent) {
299
- sentWants.add(key)
261
+ // update list of messages sent to remote
262
+ for (const key of sent) {
263
+ sentWants.add(key)
264
+ }
265
+ } catch (err: any) {
266
+ this.log.error('error sending full wantlist to new peer', err)
300
267
  }
301
- } catch (err: any) {
302
- this.log.error('error sending full wantlist to new peer', err)
303
- }
304
- }
268
+ })
269
+ ).catch(err => {
270
+ this.log.error('error sending messages', err)
271
+ })
305
272
 
306
273
  // queued all message sends, remove cancelled wants from wantlist and sent
307
274
  // wants
@@ -314,6 +281,8 @@ export class WantList implements Startable {
314
281
  }
315
282
  }
316
283
  }
284
+
285
+ this.sendingMessages.resolve()
317
286
  }
318
287
 
319
288
  has (cid: CID): boolean {
@@ -324,38 +293,28 @@ export class WantList implements Startable {
324
293
  /**
325
294
  * Add a CID to the wantlist
326
295
  */
327
- async wantPresence (cid: CID, options: WantOptions = {}): Promise<WantPresenceResult> {
328
- if (options.peerId != null && this.peers.get(options.peerId) == null) {
329
- const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
330
-
331
- try {
332
- // if we don't have them as a peer, add them
333
- this.peers.set(options.peerId, new Set([cidStr]))
334
-
335
- // sending WantHave directly to peer
336
- await this.network.sendMessage(options.peerId, {
337
- wantlist: {
338
- full: false,
339
- entries: [{
340
- cid: cid.bytes,
341
- sendDontHave: true,
342
- wantType: WantType.WantHave,
343
- priority: 1
344
- }]
345
- }
346
- })
347
- } catch (err) {
348
- // sending failed, remove them as a peer
349
- this.peers.delete(options.peerId)
350
-
351
- throw err
296
+ async wantSessionPresence (cid: CID, peerId: PeerId, options: WantOptions = {}): Promise<WantPresenceResult> {
297
+ // sending WantHave directly to peer
298
+ await this.network.sendMessage(peerId, {
299
+ wantlist: {
300
+ full: false,
301
+ entries: [{
302
+ cid: cid.bytes,
303
+ sendDontHave: true,
304
+ wantType: WantType.WantHave,
305
+ priority: 1
306
+ }]
352
307
  }
353
- }
308
+ })
354
309
 
355
- return this.addEntry(cid, {
356
- ...options,
357
- wantType: WantType.WantHave
310
+ // wait for peer response
311
+ const event = await raceEvent<CustomEvent<WantHaveResult | WantDontHaveResult>>(this, 'presence', options.signal, {
312
+ filter: (event) => {
313
+ return peerId.equals(event.detail.sender) && uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
314
+ }
358
315
  })
316
+
317
+ return event.detail
359
318
  }
360
319
 
361
320
  /**
@@ -368,15 +327,56 @@ export class WantList implements Startable {
368
327
  })
369
328
  }
370
329
 
330
+ /**
331
+ * Add a CID to the wantlist
332
+ */
333
+ async wantSessionBlock (cid: CID, peerId: PeerId, options: WantOptions = {}): Promise<WantPresenceResult> {
334
+ // sending WantBlockResult directly to peer
335
+ await this.network.sendMessage(peerId, {
336
+ wantlist: {
337
+ full: false,
338
+ entries: [{
339
+ cid: cid.bytes,
340
+ sendDontHave: true,
341
+ wantType: WantType.WantBlock,
342
+ priority: 1
343
+ }]
344
+ }
345
+ })
346
+
347
+ // wait for peer response
348
+ const event = await raceEvent<CustomEvent<WantPresenceResult>>(this, 'presence', options.signal, {
349
+ filter: (event) => {
350
+ return peerId.equals(event.detail.sender) && uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
351
+ }
352
+ })
353
+
354
+ return event.detail
355
+ }
356
+
357
+ /**
358
+ * Invoked when a block has been received from an external source
359
+ */
360
+ async receivedBlock (cid: CID, options: ProgressOptions<BitswapNotifyProgressEvents> & AbortOptions): Promise<void> {
361
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
362
+
363
+ const entry = this.wants.get(cidStr)
364
+
365
+ if (entry == null) {
366
+ return
367
+ }
368
+
369
+ entry.cancel = true
370
+
371
+ await this.sendMessagesDebounced()
372
+ }
373
+
371
374
  /**
372
375
  * Invoked when a message is received from a bitswap peer
373
376
  */
374
377
  private async receiveMessage (sender: PeerId, message: BitswapMessage): Promise<void> {
375
378
  this.log('received message from %p', sender)
376
-
377
- // blocks received
378
- const blockResults: WantBlockResult[] = []
379
- const presenceResults: WantPresenceResult[] = []
379
+ let blocksCancelled = false
380
380
 
381
381
  // process blocks
382
382
  for (const block of message.blocks) {
@@ -384,7 +384,6 @@ export class WantList implements Startable {
384
384
  continue
385
385
  }
386
386
 
387
- this.log('received block')
388
387
  const values = vd(block.prefix)
389
388
  const cidVersion = values[0]
390
389
  const multicodec = values[1]
@@ -403,66 +402,55 @@ export class WantList implements Startable {
403
402
 
404
403
  this.log('received block from %p for %c', sender, cid)
405
404
 
406
- blockResults.push({
407
- sender,
408
- cid,
409
- block: block.data
410
- })
411
-
412
- presenceResults.push({
413
- sender,
414
- cid,
415
- has: true
405
+ this.safeDispatchEvent<WantBlockResult>('block', {
406
+ detail: {
407
+ sender,
408
+ cid,
409
+ block: block.data
410
+ }
416
411
  })
417
- }
418
-
419
- // process block presences
420
- for (const { cid: cidBytes, type } of message.blockPresences) {
421
- const cid = CID.decode(cidBytes)
422
-
423
- this.log('received %s from %p for %c', type, sender, cid)
424
412
 
425
- presenceResults.push({
426
- sender,
427
- cid,
428
- has: type === BlockPresenceType.HaveBlock
413
+ this.safeDispatchEvent<WantHaveResult | WantDontHaveResult>('presence', {
414
+ detail: {
415
+ sender,
416
+ cid,
417
+ has: true,
418
+ block: block.data
419
+ }
429
420
  })
430
- }
431
421
 
432
- for (const result of blockResults) {
433
- const cidStr = uint8ArrayToString(result.cid.multihash.bytes, 'base64')
422
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
434
423
  const entry = this.wants.get(cidStr)
435
424
 
436
425
  if (entry == null) {
437
426
  return
438
427
  }
439
428
 
440
- const recipients = entry.blockWantListeners
441
- entry.blockWantListeners = []
442
- recipients.forEach((p) => {
443
- p.resolve(result)
444
- })
445
-
446
429
  // since we received the block, flip the cancel flag to send cancels to
447
430
  // any peers on the next message sending iteration, this will remove it
448
431
  // from the internal want list
449
432
  entry.cancel = true
433
+ blocksCancelled = true
450
434
  }
451
435
 
452
- for (const result of presenceResults) {
453
- const cidStr = uint8ArrayToString(result.cid.multihash.bytes, 'base64')
454
- const entry = this.wants.get(cidStr)
436
+ // process block presences
437
+ for (const { cid: cidBytes, type } of message.blockPresences) {
438
+ const cid = CID.decode(cidBytes)
455
439
 
456
- if (entry == null) {
457
- return
458
- }
440
+ this.log('received %s from %p for %c', type, sender, cid)
459
441
 
460
- const recipients = entry.blockPresenceListeners
461
- entry.blockPresenceListeners = []
462
- recipients.forEach((p) => {
463
- p.resolve(result)
442
+ this.safeDispatchEvent<WantHaveResult | WantDontHaveResult>('presence', {
443
+ detail: {
444
+ sender,
445
+ cid,
446
+ has: type === BlockPresenceType.HaveBlock
447
+ }
464
448
  })
465
449
  }
450
+
451
+ if (blocksCancelled) {
452
+ await this.sendMessagesDebounced()
453
+ }
466
454
  }
467
455
 
468
456
  /**
@@ -477,7 +465,6 @@ export class WantList implements Startable {
477
465
  full: true,
478
466
  entries: pipe(
479
467
  this.wants.entries(),
480
- (source) => filter(source, ([key, entry]) => !entry.cancel && (entry.session.size > 0 && !entry.session.has(peerId))),
481
468
  (source) => filter(source, ([key, entry]) => !entry.cancel),
482
469
  (source) => map(source, ([key, entry]) => {
483
470
  sentWants.add(key)
@@ -525,5 +512,6 @@ export class WantList implements Startable {
525
512
 
526
513
  stop (): void {
527
514
  this.peers.clear()
515
+ clearTimeout(this.sendMessagesTimeout)
528
516
  }
529
517
  }