@helia/bitswap 0.0.0-7cd012a → 0.0.0-9c8a2c0

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,4 +1,4 @@
1
- import { AbortError } from '@libp2p/interface'
1
+ import { TypedEventEmitter, setMaxListeners } from '@libp2p/interface'
2
2
  import { trackedPeerMap, PeerSet } from '@libp2p/peer-collections'
3
3
  import { trackedMap } from '@libp2p/utils/tracked-map'
4
4
  import all from 'it-all'
@@ -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 {
@@ -63,16 +65,6 @@ export interface WantListEntry {
63
65
  * If this set has members, the want will only be sent to these peers
64
66
  */
65
67
  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
68
  }
77
69
 
78
70
  export interface WantOptions extends AbortOptions, ProgressOptions<BitswapNetworkWantProgressEvents> {
@@ -108,7 +100,12 @@ export interface WantHaveResult {
108
100
 
109
101
  export type WantPresenceResult = WantDontHaveResult | WantHaveResult
110
102
 
111
- export class WantList implements Startable {
103
+ export interface WantListEvents {
104
+ block: CustomEvent<WantBlockResult>
105
+ presence: CustomEvent<WantPresenceResult>
106
+ }
107
+
108
+ export class WantList extends TypedEventEmitter<WantListEvents> implements Startable, TypedEventTarget<WantListEvents> {
112
109
  /**
113
110
  * Tracks what CIDs we've previously sent to which peers
114
111
  */
@@ -119,15 +116,19 @@ export class WantList implements Startable {
119
116
  private readonly sendMessagesDelay: number
120
117
  private sendMessagesTimeout?: ReturnType<typeof setTimeout>
121
118
  private readonly hashLoader?: MultihashHasherLoader
119
+ private sendingMessages?: DeferredPromise<void>
122
120
 
123
121
  constructor (components: WantListComponents, init: WantListInit = {}) {
122
+ super()
123
+
124
+ setMaxListeners(Infinity, this)
124
125
  this.peers = trackedPeerMap({
125
126
  name: 'ipfs_bitswap_peers',
126
- metrics: components.metrics
127
+ metrics: components.libp2p.metrics
127
128
  })
128
129
  this.wants = trackedMap({
129
130
  name: 'ipfs_bitswap_wantlist',
130
- metrics: components.metrics
131
+ metrics: components.libp2p.metrics
131
132
  })
132
133
  this.network = components.network
133
134
  this.sendMessagesDelay = init.sendMessagesDelay ?? DEFAULT_MESSAGE_SEND_DELAY
@@ -164,9 +165,7 @@ export class WantList implements Startable {
164
165
  priority: options.priority ?? 1,
165
166
  wantType: options.wantType ?? WantType.WantBlock,
166
167
  cancel: false,
167
- sendDontHave: true,
168
- blockWantListeners: [],
169
- blockPresenceListeners: []
168
+ sendDontHave: true
170
169
  }
171
170
 
172
171
  if (options.peerId != null) {
@@ -198,30 +197,41 @@ export class WantList implements Startable {
198
197
  }
199
198
  }
200
199
 
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>()
200
+ // broadcast changes
201
+ await this.sendMessagesDebounced()
206
202
 
207
- entry.blockWantListeners.push(p)
208
- } else {
209
- const p = deferred = pDefer<WantPresenceResult>()
203
+ try {
204
+ if (options.wantType === WantType.WantBlock) {
205
+ const event = await raceEvent<CustomEvent<WantBlockResult>>(this, 'block', options?.signal, {
206
+ filter: (event) => {
207
+ return uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
208
+ },
209
+ errorMessage: 'Want was aborted'
210
+ })
210
211
 
211
- entry.blockPresenceListeners.push(p)
212
- }
212
+ return event.detail
213
+ }
213
214
 
214
- // reject the promise if the want is rejected
215
- const abortListener = (): void => {
216
- this.log('want for %c was aborted, cancelling want', cid)
215
+ const event = await raceEvent<CustomEvent<WantPresenceResult>>(this, 'presence', options?.signal, {
216
+ filter: (event) => {
217
+ return uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
218
+ },
219
+ errorMessage: 'Want was aborted'
220
+ })
217
221
 
218
- if (entry != null) {
222
+ return event.detail
223
+ } finally {
224
+ if (options.signal?.aborted === true) {
225
+ this.log('want for %c was aborted, cancelling want', cid)
219
226
  entry.cancel = true
227
+ // broadcast changes
228
+ await this.sendMessagesDebounced()
220
229
  }
221
-
222
- deferred.reject(new AbortError('Want was aborted'))
223
230
  }
224
- options.signal?.addEventListener('abort', abortListener)
231
+ }
232
+
233
+ private async sendMessagesDebounced (): Promise<void> {
234
+ await this.sendingMessages?.promise
225
235
 
226
236
  // broadcast changes
227
237
  clearTimeout(this.sendMessagesTimeout)
@@ -231,77 +241,70 @@ export class WantList implements Startable {
231
241
  this.log('error sending messages to peers', err)
232
242
  })
233
243
  }, 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
244
  }
248
245
 
249
246
  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
- )
247
+ this.sendingMessages = pDefer()
248
+
249
+ await Promise.all(
250
+ [...this.peers.entries()].map(async ([peerId, sentWants]) => {
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
+ )
286
+ }
286
287
  }
287
- }
288
288
 
289
- if (message.wantlist?.entries.length === 0) {
290
- return
291
- }
289
+ if (message.wantlist?.entries.length === 0) {
290
+ return
291
+ }
292
292
 
293
- // add message to send queue
294
- try {
295
- await this.network.sendMessage(peerId, message)
293
+ // add message to send queue
294
+ try {
295
+ await this.network.sendMessage(peerId, message)
296
296
 
297
- // update list of messages sent to remote
298
- for (const key of sent) {
299
- sentWants.add(key)
297
+ // update list of messages sent to remote
298
+ for (const key of sent) {
299
+ sentWants.add(key)
300
+ }
301
+ } catch (err: any) {
302
+ this.log.error('error sending full wantlist to new peer', err)
300
303
  }
301
- } catch (err: any) {
302
- this.log.error('error sending full wantlist to new peer', err)
303
- }
304
- }
304
+ })
305
+ ).catch(err => {
306
+ this.log.error('error sending messages', err)
307
+ })
305
308
 
306
309
  // queued all message sends, remove cancelled wants from wantlist and sent
307
310
  // wants
@@ -314,6 +317,8 @@ export class WantList implements Startable {
314
317
  }
315
318
  }
316
319
  }
320
+
321
+ this.sendingMessages.resolve()
317
322
  }
318
323
 
319
324
  has (cid: CID): boolean {
@@ -325,31 +330,30 @@ export class WantList implements Startable {
325
330
  * Add a CID to the wantlist
326
331
  */
327
332
  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')
333
+ if (options.peerId != null) {
334
+ const peer = options.peerId
330
335
 
331
- try {
332
- // if we don't have them as a peer, add them
333
- this.peers.set(options.peerId, new Set([cidStr]))
336
+ // sending WantHave directly to peer
337
+ await this.network.sendMessage(options.peerId, {
338
+ wantlist: {
339
+ full: false,
340
+ entries: [{
341
+ cid: cid.bytes,
342
+ sendDontHave: true,
343
+ wantType: WantType.WantHave,
344
+ priority: 1
345
+ }]
346
+ }
347
+ })
334
348
 
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)
349
+ // wait for peer response
350
+ const event = await raceEvent<CustomEvent<WantHaveResult | WantDontHaveResult>>(this, 'presence', options.signal, {
351
+ filter: (event) => {
352
+ return peer.equals(event.detail.sender) && uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
353
+ }
354
+ })
350
355
 
351
- throw err
352
- }
356
+ return event.detail
353
357
  }
354
358
 
355
359
  return this.addEntry(cid, {
@@ -368,15 +372,29 @@ export class WantList implements Startable {
368
372
  })
369
373
  }
370
374
 
375
+ /**
376
+ * Invoked when a block has been received from an external source
377
+ */
378
+ async receivedBlock (cid: CID, options: ProgressOptions<BitswapNotifyProgressEvents> & AbortOptions): Promise<void> {
379
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
380
+
381
+ const entry = this.wants.get(cidStr)
382
+
383
+ if (entry == null) {
384
+ return
385
+ }
386
+
387
+ entry.cancel = true
388
+
389
+ await this.sendMessagesDebounced()
390
+ }
391
+
371
392
  /**
372
393
  * Invoked when a message is received from a bitswap peer
373
394
  */
374
395
  private async receiveMessage (sender: PeerId, message: BitswapMessage): Promise<void> {
375
396
  this.log('received message from %p', sender)
376
-
377
- // blocks received
378
- const blockResults: WantBlockResult[] = []
379
- const presenceResults: WantPresenceResult[] = []
397
+ let blocksCancelled = false
380
398
 
381
399
  // process blocks
382
400
  for (const block of message.blocks) {
@@ -384,7 +402,6 @@ export class WantList implements Startable {
384
402
  continue
385
403
  }
386
404
 
387
- this.log('received block')
388
405
  const values = vd(block.prefix)
389
406
  const cidVersion = values[0]
390
407
  const multicodec = values[1]
@@ -403,66 +420,55 @@ export class WantList implements Startable {
403
420
 
404
421
  this.log('received block from %p for %c', sender, cid)
405
422
 
406
- blockResults.push({
407
- sender,
408
- cid,
409
- block: block.data
423
+ this.safeDispatchEvent<WantBlockResult>('block', {
424
+ detail: {
425
+ sender,
426
+ cid,
427
+ block: block.data
428
+ }
410
429
  })
411
430
 
412
- presenceResults.push({
413
- sender,
414
- cid,
415
- has: true
416
- })
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
-
425
- presenceResults.push({
426
- sender,
427
- cid,
428
- has: type === BlockPresenceType.HaveBlock
431
+ this.safeDispatchEvent<WantHaveResult | WantDontHaveResult>('presence', {
432
+ detail: {
433
+ sender,
434
+ cid,
435
+ has: true,
436
+ block: block.data
437
+ }
429
438
  })
430
- }
431
439
 
432
- for (const result of blockResults) {
433
- const cidStr = uint8ArrayToString(result.cid.multihash.bytes, 'base64')
440
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
434
441
  const entry = this.wants.get(cidStr)
435
442
 
436
443
  if (entry == null) {
437
444
  return
438
445
  }
439
446
 
440
- const recipients = entry.blockWantListeners
441
- entry.blockWantListeners = []
442
- recipients.forEach((p) => {
443
- p.resolve(result)
444
- })
445
-
446
447
  // since we received the block, flip the cancel flag to send cancels to
447
448
  // any peers on the next message sending iteration, this will remove it
448
449
  // from the internal want list
449
450
  entry.cancel = true
451
+ blocksCancelled = true
450
452
  }
451
453
 
452
- for (const result of presenceResults) {
453
- const cidStr = uint8ArrayToString(result.cid.multihash.bytes, 'base64')
454
- const entry = this.wants.get(cidStr)
454
+ // process block presences
455
+ for (const { cid: cidBytes, type } of message.blockPresences) {
456
+ const cid = CID.decode(cidBytes)
455
457
 
456
- if (entry == null) {
457
- return
458
- }
458
+ this.log('received %s from %p for %c', type, sender, cid)
459
459
 
460
- const recipients = entry.blockPresenceListeners
461
- entry.blockPresenceListeners = []
462
- recipients.forEach((p) => {
463
- p.resolve(result)
460
+ this.safeDispatchEvent<WantHaveResult | WantDontHaveResult>('presence', {
461
+ detail: {
462
+ sender,
463
+ cid,
464
+ has: type === BlockPresenceType.HaveBlock
465
+ }
464
466
  })
465
467
  }
468
+
469
+ if (blocksCancelled) {
470
+ await this.sendMessagesDebounced()
471
+ }
466
472
  }
467
473
 
468
474
  /**