@helia/bitswap 0.0.0-329652a

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 (70) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +64 -0
  3. package/dist/index.min.js +3 -0
  4. package/dist/src/bitswap.d.ts +50 -0
  5. package/dist/src/bitswap.d.ts.map +1 -0
  6. package/dist/src/bitswap.js +120 -0
  7. package/dist/src/bitswap.js.map +1 -0
  8. package/dist/src/constants.d.ts +12 -0
  9. package/dist/src/constants.d.ts.map +1 -0
  10. package/dist/src/constants.js +12 -0
  11. package/dist/src/constants.js.map +1 -0
  12. package/dist/src/index.d.ts +178 -0
  13. package/dist/src/index.d.ts.map +1 -0
  14. package/dist/src/index.js +12 -0
  15. package/dist/src/index.js.map +1 -0
  16. package/dist/src/network.d.ts +84 -0
  17. package/dist/src/network.d.ts.map +1 -0
  18. package/dist/src/network.js +370 -0
  19. package/dist/src/network.js.map +1 -0
  20. package/dist/src/pb/message.d.ts +67 -0
  21. package/dist/src/pb/message.d.ts.map +1 -0
  22. package/dist/src/pb/message.js +359 -0
  23. package/dist/src/pb/message.js.map +1 -0
  24. package/dist/src/peer-want-lists/index.d.ts +44 -0
  25. package/dist/src/peer-want-lists/index.d.ts.map +1 -0
  26. package/dist/src/peer-want-lists/index.js +116 -0
  27. package/dist/src/peer-want-lists/index.js.map +1 -0
  28. package/dist/src/peer-want-lists/ledger.d.ts +54 -0
  29. package/dist/src/peer-want-lists/ledger.d.ts.map +1 -0
  30. package/dist/src/peer-want-lists/ledger.js +104 -0
  31. package/dist/src/peer-want-lists/ledger.js.map +1 -0
  32. package/dist/src/session.d.ts +20 -0
  33. package/dist/src/session.d.ts.map +1 -0
  34. package/dist/src/session.js +100 -0
  35. package/dist/src/session.js.map +1 -0
  36. package/dist/src/stats.d.ts +16 -0
  37. package/dist/src/stats.d.ts.map +1 -0
  38. package/dist/src/stats.js +49 -0
  39. package/dist/src/stats.js.map +1 -0
  40. package/dist/src/utils/cid-prefix.d.ts +3 -0
  41. package/dist/src/utils/cid-prefix.d.ts.map +1 -0
  42. package/dist/src/utils/cid-prefix.js +7 -0
  43. package/dist/src/utils/cid-prefix.js.map +1 -0
  44. package/dist/src/utils/varint-decoder.d.ts +3 -0
  45. package/dist/src/utils/varint-decoder.d.ts.map +1 -0
  46. package/dist/src/utils/varint-decoder.js +15 -0
  47. package/dist/src/utils/varint-decoder.js.map +1 -0
  48. package/dist/src/utils/varint-encoder.d.ts +3 -0
  49. package/dist/src/utils/varint-encoder.d.ts.map +1 -0
  50. package/dist/src/utils/varint-encoder.js +14 -0
  51. package/dist/src/utils/varint-encoder.js.map +1 -0
  52. package/dist/src/want-list.d.ts +120 -0
  53. package/dist/src/want-list.d.ts.map +1 -0
  54. package/dist/src/want-list.js +361 -0
  55. package/dist/src/want-list.js.map +1 -0
  56. package/package.json +200 -0
  57. package/src/bitswap.ts +152 -0
  58. package/src/constants.ts +11 -0
  59. package/src/index.ts +215 -0
  60. package/src/network.ts +506 -0
  61. package/src/pb/message.proto +42 -0
  62. package/src/pb/message.ts +450 -0
  63. package/src/peer-want-lists/index.ts +165 -0
  64. package/src/peer-want-lists/ledger.ts +161 -0
  65. package/src/session.ts +150 -0
  66. package/src/stats.ts +67 -0
  67. package/src/utils/cid-prefix.ts +8 -0
  68. package/src/utils/varint-decoder.ts +19 -0
  69. package/src/utils/varint-encoder.ts +18 -0
  70. package/src/want-list.ts +529 -0
@@ -0,0 +1,529 @@
1
+ import { AbortError } from '@libp2p/interface'
2
+ import { trackedPeerMap, PeerSet } from '@libp2p/peer-collections'
3
+ import { trackedMap } from '@libp2p/utils/tracked-map'
4
+ import all from 'it-all'
5
+ import filter from 'it-filter'
6
+ import map from 'it-map'
7
+ import { pipe } from 'it-pipe'
8
+ import { CID } from 'multiformats/cid'
9
+ import { sha256 } from 'multiformats/hashes/sha2'
10
+ import pDefer from 'p-defer'
11
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
12
+ import { DEFAULT_MESSAGE_SEND_DELAY } from './constants.js'
13
+ import { BlockPresenceType, WantType } from './pb/message.js'
14
+ import vd from './utils/varint-decoder.js'
15
+ import type { MultihashHasherLoader } from './index.js'
16
+ import type { BitswapNetworkWantProgressEvents, Network } from './network.js'
17
+ import type { BitswapMessage } from './pb/message.js'
18
+ import type { ComponentLogger, Metrics, PeerId, Startable, AbortOptions } from '@libp2p/interface'
19
+ import type { Logger } from '@libp2p/logger'
20
+ import type { PeerMap } from '@libp2p/peer-collections'
21
+ import type { DeferredPromise } from 'p-defer'
22
+ import type { ProgressOptions } from 'progress-events'
23
+
24
+ export interface WantListComponents {
25
+ network: Network
26
+ logger: ComponentLogger
27
+ metrics?: Metrics
28
+ }
29
+
30
+ export interface WantListInit {
31
+ sendMessagesDelay?: number
32
+ hashLoader?: MultihashHasherLoader
33
+ }
34
+
35
+ export interface WantListEntry {
36
+ /**
37
+ * The CID we send to the remote
38
+ */
39
+ cid: CID
40
+
41
+ /**
42
+ * The priority with which the remote should return the block
43
+ */
44
+ priority: number
45
+
46
+ /**
47
+ * If we want the block or if we want the remote to tell us if they have the
48
+ * block - note if the block is small they'll send it to us anyway.
49
+ */
50
+ wantType: WantType
51
+
52
+ /**
53
+ * Whether we are cancelling the block want or not
54
+ */
55
+ cancel: boolean
56
+
57
+ /**
58
+ * Whether the remote should tell us if they have the block or not
59
+ */
60
+ 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
+ }
77
+
78
+ 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
+ /**
85
+ * Allow prioritising blocks
86
+ */
87
+ priority?: number
88
+ }
89
+
90
+ export interface WantBlockResult {
91
+ sender: PeerId
92
+ cid: CID
93
+ block: Uint8Array
94
+ }
95
+
96
+ export interface WantDontHaveResult {
97
+ sender: PeerId
98
+ cid: CID
99
+ has: false
100
+ }
101
+
102
+ export interface WantHaveResult {
103
+ sender: PeerId
104
+ cid: CID
105
+ has: true
106
+ block?: Uint8Array
107
+ }
108
+
109
+ export type WantPresenceResult = WantDontHaveResult | WantHaveResult
110
+
111
+ export class WantList implements Startable {
112
+ /**
113
+ * Tracks what CIDs we've previously sent to which peers
114
+ */
115
+ public readonly peers: PeerMap<Set<string>>
116
+ public readonly wants: Map<string, WantListEntry>
117
+ private readonly network: Network
118
+ private readonly log: Logger
119
+ private readonly sendMessagesDelay: number
120
+ private sendMessagesTimeout?: ReturnType<typeof setTimeout>
121
+ private readonly hashLoader?: MultihashHasherLoader
122
+
123
+ constructor (components: WantListComponents, init: WantListInit = {}) {
124
+ this.peers = trackedPeerMap({
125
+ name: 'ipfs_bitswap_peers',
126
+ metrics: components.metrics
127
+ })
128
+ this.wants = trackedMap({
129
+ name: 'ipfs_bitswap_wantlist',
130
+ metrics: components.metrics
131
+ })
132
+ this.network = components.network
133
+ this.sendMessagesDelay = init.sendMessagesDelay ?? DEFAULT_MESSAGE_SEND_DELAY
134
+ this.log = components.logger.forComponent('helia:bitswap:wantlist')
135
+ this.hashLoader = init.hashLoader
136
+
137
+ this.network.addEventListener('bitswap:message', (evt) => {
138
+ this.receiveMessage(evt.detail.peer, evt.detail.message)
139
+ .catch(err => {
140
+ this.log.error('error receiving bitswap message from %p', evt.detail.peer, err)
141
+ })
142
+ })
143
+ this.network.addEventListener('peer:connected', evt => {
144
+ this.peerConnected(evt.detail)
145
+ .catch(err => {
146
+ this.log.error('error processing newly connected bitswap peer %p', evt.detail, err)
147
+ })
148
+ })
149
+ this.network.addEventListener('peer:disconnected', evt => {
150
+ this.peerDisconnected(evt.detail)
151
+ })
152
+ }
153
+
154
+ private async addEntry (cid: CID, options: WantOptions & { wantType: WantType.WantBlock }): Promise<WantBlockResult>
155
+ private async addEntry (cid: CID, options: WantOptions & { wantType: WantType.WantHave }): Promise<WantPresenceResult>
156
+ private async addEntry (cid: CID, options: WantOptions & { wantType: WantType }): Promise<WantBlockResult | WantPresenceResult> {
157
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
158
+ let entry = this.wants.get(cidStr)
159
+
160
+ if (entry == null) {
161
+ entry = {
162
+ cid,
163
+ session: new PeerSet(),
164
+ priority: options.priority ?? 1,
165
+ wantType: options.wantType ?? WantType.WantBlock,
166
+ cancel: false,
167
+ sendDontHave: true,
168
+ blockWantListeners: [],
169
+ blockPresenceListeners: []
170
+ }
171
+
172
+ if (options.peerId != null) {
173
+ entry.session.add(options.peerId)
174
+ }
175
+
176
+ this.wants.set(cidStr, entry)
177
+ }
178
+
179
+ // upgrade want-have to want-block if the new want is a WantBlock but the
180
+ // previous want was a WantHave
181
+ if (entry.wantType === WantType.WantHave && options.wantType === WantType.WantBlock) {
182
+ entry.wantType = WantType.WantBlock
183
+ }
184
+
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>()
206
+
207
+ entry.blockWantListeners.push(p)
208
+ } else {
209
+ const p = deferred = pDefer<WantPresenceResult>()
210
+
211
+ entry.blockPresenceListeners.push(p)
212
+ }
213
+
214
+ // reject the promise if the want is rejected
215
+ const abortListener = (): void => {
216
+ this.log('want for %c was aborted, cancelling want', cid)
217
+
218
+ if (entry != null) {
219
+ entry.cancel = true
220
+ }
221
+
222
+ deferred.reject(new AbortError('Want was aborted'))
223
+ }
224
+ options.signal?.addEventListener('abort', abortListener)
225
+
226
+ // broadcast changes
227
+ clearTimeout(this.sendMessagesTimeout)
228
+ this.sendMessagesTimeout = setTimeout(() => {
229
+ void this.sendMessages()
230
+ .catch(err => {
231
+ this.log('error sending messages to peers', err)
232
+ })
233
+ }, 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
+ }
248
+
249
+ 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
+ )
286
+ }
287
+ }
288
+
289
+ if (message.wantlist?.entries.length === 0) {
290
+ return
291
+ }
292
+
293
+ // add message to send queue
294
+ try {
295
+ await this.network.sendMessage(peerId, message)
296
+
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)
303
+ }
304
+ }
305
+
306
+ // queued all message sends, remove cancelled wants from wantlist and sent
307
+ // wants
308
+ for (const [key, entry] of this.wants) {
309
+ if (entry.cancel) {
310
+ this.wants.delete(key)
311
+
312
+ for (const sentWants of this.peers.values()) {
313
+ sentWants.delete(key)
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ has (cid: CID): boolean {
320
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
321
+ return this.wants.has(cidStr)
322
+ }
323
+
324
+ /**
325
+ * Add a CID to the wantlist
326
+ */
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
352
+ }
353
+ }
354
+
355
+ return this.addEntry(cid, {
356
+ ...options,
357
+ wantType: WantType.WantHave
358
+ })
359
+ }
360
+
361
+ /**
362
+ * Add a CID to the wantlist
363
+ */
364
+ async wantBlock (cid: CID, options: WantOptions = {}): Promise<WantBlockResult> {
365
+ return this.addEntry(cid, {
366
+ ...options,
367
+ wantType: WantType.WantBlock
368
+ })
369
+ }
370
+
371
+ /**
372
+ * Invoked when a message is received from a bitswap peer
373
+ */
374
+ private async receiveMessage (sender: PeerId, message: BitswapMessage): Promise<void> {
375
+ this.log('received message from %p', sender)
376
+
377
+ // blocks received
378
+ const blockResults: WantBlockResult[] = []
379
+ const presenceResults: WantPresenceResult[] = []
380
+
381
+ // process blocks
382
+ for (const block of message.blocks) {
383
+ if (block.prefix == null || block.data == null) {
384
+ continue
385
+ }
386
+
387
+ this.log('received block')
388
+ const values = vd(block.prefix)
389
+ const cidVersion = values[0]
390
+ const multicodec = values[1]
391
+ const hashAlg = values[2]
392
+ // const hashLen = values[3] // We haven't need to use this so far
393
+
394
+ const hasher = hashAlg === sha256.code ? sha256 : await this.hashLoader?.getHasher(hashAlg)
395
+
396
+ if (hasher == null) {
397
+ this.log.error('unknown hash algorithm', hashAlg)
398
+ continue
399
+ }
400
+
401
+ const hash = await hasher.digest(block.data)
402
+ const cid = CID.create(cidVersion === 0 ? 0 : 1, multicodec, hash)
403
+
404
+ this.log('received block from %p for %c', sender, cid)
405
+
406
+ blockResults.push({
407
+ sender,
408
+ cid,
409
+ block: block.data
410
+ })
411
+
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
429
+ })
430
+ }
431
+
432
+ for (const result of blockResults) {
433
+ const cidStr = uint8ArrayToString(result.cid.multihash.bytes, 'base64')
434
+ const entry = this.wants.get(cidStr)
435
+
436
+ if (entry == null) {
437
+ return
438
+ }
439
+
440
+ const recipients = entry.blockWantListeners
441
+ entry.blockWantListeners = []
442
+ recipients.forEach((p) => {
443
+ p.resolve(result)
444
+ })
445
+
446
+ // since we received the block, flip the cancel flag to send cancels to
447
+ // any peers on the next message sending iteration, this will remove it
448
+ // from the internal want list
449
+ entry.cancel = true
450
+ }
451
+
452
+ for (const result of presenceResults) {
453
+ const cidStr = uint8ArrayToString(result.cid.multihash.bytes, 'base64')
454
+ const entry = this.wants.get(cidStr)
455
+
456
+ if (entry == null) {
457
+ return
458
+ }
459
+
460
+ const recipients = entry.blockPresenceListeners
461
+ entry.blockPresenceListeners = []
462
+ recipients.forEach((p) => {
463
+ p.resolve(result)
464
+ })
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Invoked when the network topology notices a new peer that supports Bitswap
470
+ */
471
+ async peerConnected (peerId: PeerId): Promise<void> {
472
+ const sentWants = new Set<string>()
473
+
474
+ // new peer, give them the full wantlist
475
+ const message: Partial<BitswapMessage> = {
476
+ wantlist: {
477
+ full: true,
478
+ entries: pipe(
479
+ this.wants.entries(),
480
+ (source) => filter(source, ([key, entry]) => !entry.cancel && (entry.session.size > 0 && !entry.session.has(peerId))),
481
+ (source) => filter(source, ([key, entry]) => !entry.cancel),
482
+ (source) => map(source, ([key, entry]) => {
483
+ sentWants.add(key)
484
+
485
+ return {
486
+ cid: entry.cid.bytes,
487
+ priority: 1,
488
+ wantType: WantType.WantBlock,
489
+ cancel: false,
490
+ sendDontHave: false
491
+ }
492
+ }),
493
+ (source) => all(source)
494
+ )
495
+ }
496
+ }
497
+
498
+ // only send the wantlist if we have something to send
499
+ if (message.wantlist?.entries.length === 0) {
500
+ this.peers.set(peerId, sentWants)
501
+
502
+ return
503
+ }
504
+
505
+ try {
506
+ await this.network.sendMessage(peerId, message)
507
+
508
+ this.peers.set(peerId, sentWants)
509
+ } catch (err) {
510
+ this.log.error('error sending full wantlist to new peer %p', peerId, err)
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Invoked when the network topology notices peer that supports Bitswap has
516
+ * disconnected
517
+ */
518
+ peerDisconnected (peerId: PeerId): void {
519
+ this.peers.delete(peerId)
520
+ }
521
+
522
+ start (): void {
523
+
524
+ }
525
+
526
+ stop (): void {
527
+ this.peers.clear()
528
+ }
529
+ }