@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,161 @@
1
+ /* eslint-disable max-depth */
2
+ import { DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK } from '../constants.js'
3
+ import { BlockPresenceType, type BitswapMessage, WantType } from '../pb/message.js'
4
+ import { cidToPrefix } from '../utils/cid-prefix.js'
5
+ import type { Network } from '../network.js'
6
+ import type { PeerId } from '@libp2p/interface'
7
+ import type { Blockstore } from 'interface-blockstore'
8
+ import type { AbortOptions } from 'it-length-prefixed-stream'
9
+ import type { CID } from 'multiformats/cid'
10
+
11
+ export interface LedgerComponents {
12
+ peerId: PeerId
13
+ blockstore: Blockstore
14
+ network: Network
15
+ }
16
+
17
+ export interface LedgerInit {
18
+ maxSizeReplaceHasWithBlock?: number
19
+ }
20
+
21
+ export interface PeerWantListEntry {
22
+ /**
23
+ * The CID the peer has requested
24
+ */
25
+ cid: CID
26
+
27
+ /**
28
+ * The priority with which the remote should return the block
29
+ */
30
+ priority: number
31
+
32
+ /**
33
+ * If we want the block or if we want the remote to tell us if they have the
34
+ * block - note if the block is small they'll send it to us anyway.
35
+ */
36
+ wantType: WantType
37
+
38
+ /**
39
+ * Whether the remote should tell us if they have the block or not
40
+ */
41
+ sendDontHave: boolean
42
+
43
+ /**
44
+ * If we don't have the block and we've told them we don't have the block
45
+ */
46
+ sentDontHave?: boolean
47
+ }
48
+
49
+ export class Ledger {
50
+ public peerId: PeerId
51
+ private readonly blockstore: Blockstore
52
+ private readonly network: Network
53
+ public wants: Map<string, PeerWantListEntry>
54
+ public exchangeCount: number
55
+ public bytesSent: number
56
+ public bytesReceived: number
57
+ public lastExchange?: number
58
+ private readonly maxSizeReplaceHasWithBlock: number
59
+
60
+ constructor (components: LedgerComponents, init: LedgerInit) {
61
+ this.peerId = components.peerId
62
+ this.blockstore = components.blockstore
63
+ this.network = components.network
64
+ this.wants = new Map()
65
+
66
+ this.exchangeCount = 0
67
+ this.bytesSent = 0
68
+ this.bytesReceived = 0
69
+ this.maxSizeReplaceHasWithBlock = init.maxSizeReplaceHasWithBlock ?? DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK
70
+ }
71
+
72
+ sentBytes (n: number): void {
73
+ this.exchangeCount++
74
+ this.lastExchange = (new Date()).getTime()
75
+ this.bytesSent += n
76
+ }
77
+
78
+ receivedBytes (n: number): void {
79
+ this.exchangeCount++
80
+ this.lastExchange = (new Date()).getTime()
81
+ this.bytesReceived += n
82
+ }
83
+
84
+ debtRatio (): number {
85
+ return (this.bytesSent / (this.bytesReceived + 1)) // +1 is to prevent division by zero
86
+ }
87
+
88
+ public async sendBlocksToPeer (options?: AbortOptions): Promise<void> {
89
+ const message: Pick<BitswapMessage, 'blockPresences' | 'blocks'> = {
90
+ blockPresences: [],
91
+ blocks: []
92
+ }
93
+ const sentBlocks = new Set<string>()
94
+
95
+ for (const [key, entry] of this.wants.entries()) {
96
+ const has = await this.blockstore.has(entry.cid, options)
97
+
98
+ if (!has) {
99
+ // we don't have the requested block and the remote is not interested
100
+ // in us telling them that
101
+ if (!entry.sendDontHave) {
102
+ continue
103
+ }
104
+
105
+ // we have already told them we don't have the block
106
+ if (entry.sentDontHave === true) {
107
+ continue
108
+ }
109
+
110
+ entry.sentDontHave = true
111
+ message.blockPresences.push({
112
+ cid: entry.cid.bytes,
113
+ type: BlockPresenceType.DontHaveBlock
114
+ })
115
+
116
+ continue
117
+ }
118
+
119
+ const block = await this.blockstore.get(entry.cid, options)
120
+
121
+ // do they want the block or just us to tell them we have the block
122
+ if (entry.wantType === WantType.WantHave) {
123
+ if (block.byteLength < this.maxSizeReplaceHasWithBlock) {
124
+ // if the block is small we just send it to them
125
+ sentBlocks.add(key)
126
+ message.blocks.push({
127
+ data: block,
128
+ prefix: cidToPrefix(entry.cid)
129
+ })
130
+ } else {
131
+ // otherwise tell them we have the block
132
+ message.blockPresences.push({
133
+ cid: entry.cid.bytes,
134
+ type: BlockPresenceType.HaveBlock
135
+ })
136
+ }
137
+ } else {
138
+ // they want the block, send it to them
139
+ sentBlocks.add(key)
140
+ message.blocks.push({
141
+ data: block,
142
+ prefix: cidToPrefix(entry.cid)
143
+ })
144
+ }
145
+ }
146
+
147
+ // only send the message if we actually have something to send
148
+ if (message.blocks.length > 0 || message.blockPresences.length > 0) {
149
+ await this.network.sendMessage(this.peerId, message, options)
150
+
151
+ // update accounting
152
+ this.sentBytes(message.blocks.reduce((acc, curr) => acc + curr.data.byteLength, 0))
153
+
154
+ // remove sent blocks from local copy of their want list - they can still
155
+ // re-request if required
156
+ for (const key of sentBlocks) {
157
+ this.wants.delete(key)
158
+ }
159
+ }
160
+ }
161
+ }
package/src/session.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { CodeError } from '@libp2p/interface'
2
+ import { PeerSet } from '@libp2p/peer-collections'
3
+ import { PeerQueue } from '@libp2p/utils/peer-queue'
4
+ import map from 'it-map'
5
+ import merge from 'it-merge'
6
+ import pDefer, { type DeferredPromise } from 'p-defer'
7
+ import type { BitswapWantProgressEvents, BitswapSession as BitswapSessionInterface } from './index.js'
8
+ import type { Network } from './network.js'
9
+ import type { WantList } from './want-list.js'
10
+ import type { ComponentLogger, Logger, PeerId } from '@libp2p/interface'
11
+ import type { AbortOptions } from 'interface-store'
12
+ import type { CID } from 'multiformats/cid'
13
+ import type { ProgressOptions } from 'progress-events'
14
+
15
+ export interface BitswapSessionComponents {
16
+ network: Network
17
+ wantList: WantList
18
+ logger: ComponentLogger
19
+ }
20
+
21
+ export interface BitswapSessionInit extends AbortOptions {
22
+ root: CID
23
+ queryConcurrency: number
24
+ minProviders: number
25
+ maxProviders: number
26
+ connectedPeers: PeerId[]
27
+ }
28
+
29
+ class BitswapSession implements BitswapSessionInterface {
30
+ public readonly root: CID
31
+ public readonly peers: PeerSet
32
+ private readonly log: Logger
33
+ private readonly wantList: WantList
34
+ private readonly network: Network
35
+ private readonly queue: PeerQueue
36
+ private readonly maxProviders: number
37
+
38
+ constructor (components: BitswapSessionComponents, init: BitswapSessionInit) {
39
+ this.peers = new PeerSet()
40
+ this.root = init.root
41
+ this.maxProviders = init.maxProviders
42
+ this.log = components.logger.forComponent(`helia:bitswap:session:${init.root}`)
43
+ this.wantList = components.wantList
44
+ this.network = components.network
45
+
46
+ this.queue = new PeerQueue({
47
+ concurrency: init.queryConcurrency
48
+ })
49
+ this.queue.addEventListener('error', (evt) => {
50
+ this.log.error('error querying peer for %c', this.root, evt.detail)
51
+ })
52
+ }
53
+
54
+ async want (cid: CID, options: AbortOptions & ProgressOptions<BitswapWantProgressEvents> = {}): Promise<Uint8Array> {
55
+ if (this.peers.size === 0) {
56
+ throw new CodeError('Bitswap session had no peers', 'ERR_NO_SESSION_PEERS')
57
+ }
58
+
59
+ this.log('sending WANT-BLOCK for %c to', cid, this.peers)
60
+
61
+ const result = await Promise.any(
62
+ [...this.peers].map(async peerId => {
63
+ return this.wantList.wantBlock(cid, {
64
+ peerId,
65
+ ...options
66
+ })
67
+ })
68
+ )
69
+
70
+ this.log('received block for %c from %p', cid, result.sender)
71
+
72
+ // TODO findNewProviders when promise.any throws aggregate error and signal
73
+ // is not aborted
74
+
75
+ return result.block
76
+ }
77
+
78
+ async findNewProviders (cid: CID, count: number, options: AbortOptions = {}): Promise<void> {
79
+ const deferred: DeferredPromise<void> = pDefer()
80
+ let found = 0
81
+
82
+ this.log('find %d-%d new provider(s) for %c', count, this.maxProviders, cid)
83
+
84
+ const source = merge(
85
+ [...this.wantList.peers.keys()],
86
+ map(this.network.findProviders(cid, options), prov => prov.id)
87
+ )
88
+
89
+ void Promise.resolve()
90
+ .then(async () => {
91
+ for await (const peerId of source) {
92
+ // eslint-disable-next-line no-loop-func
93
+ await this.queue.add(async () => {
94
+ try {
95
+ this.log('asking potential session peer %p if they have %c', peerId, cid)
96
+ const result = await this.wantList.wantPresence(cid, {
97
+ peerId,
98
+ ...options
99
+ })
100
+
101
+ if (!result.has) {
102
+ this.log('potential session peer %p did not have %c', peerId, cid)
103
+ return
104
+ }
105
+
106
+ this.log('potential session peer %p had %c', peerId, cid)
107
+ found++
108
+
109
+ // add to list
110
+ this.peers.add(peerId)
111
+
112
+ if (found === count) {
113
+ this.log('found %d session peers', found)
114
+
115
+ deferred.resolve()
116
+ }
117
+
118
+ if (found === this.maxProviders) {
119
+ this.log('found max provider session peers', found)
120
+
121
+ this.queue.clear()
122
+ }
123
+ } catch (err: any) {
124
+ this.log.error('error querying potential session peer %p for %c', peerId, cid, err.errors ?? err)
125
+ }
126
+ }, {
127
+ peerId
128
+ })
129
+ }
130
+
131
+ this.log('found %d session peers total', found)
132
+
133
+ if (count > 0) {
134
+ deferred.reject(new CodeError(`Found ${found} of ${count} providers`, 'ERR_NO_PROVIDERS_FOUND'))
135
+ }
136
+ })
137
+
138
+ return deferred.promise
139
+ }
140
+ }
141
+
142
+ export async function createBitswapSession (components: BitswapSessionComponents, init: BitswapSessionInit): Promise<BitswapSessionInterface> {
143
+ const session = new BitswapSession(components, init)
144
+
145
+ await session.findNewProviders(init.root, init.minProviders, {
146
+ signal: init.signal
147
+ })
148
+
149
+ return session
150
+ }
package/src/stats.ts ADDED
@@ -0,0 +1,67 @@
1
+ import type { MetricGroup, Metrics, PeerId } from '@libp2p/interface'
2
+
3
+ export interface StatsComponents {
4
+ metrics?: Metrics
5
+ }
6
+
7
+ export class Stats {
8
+ private readonly blocksReceived?: MetricGroup
9
+ private readonly duplicateBlocksReceived?: MetricGroup
10
+ private readonly dataReceived?: MetricGroup
11
+ private readonly duplicateDataReceived?: MetricGroup
12
+
13
+ constructor (components: StatsComponents) {
14
+ this.blocksReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_received_blocks')
15
+ this.duplicateBlocksReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_duplicate_received_blocks')
16
+ this.dataReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_data_received_bytes')
17
+ this.duplicateDataReceived = components.metrics?.registerMetricGroup('ipfs_bitswap_duplicate_data_received_bytes')
18
+ }
19
+
20
+ updateBlocksReceived (count: number = 1, peerId?: PeerId): void {
21
+ const stats: Record<string, number | unknown> = {
22
+ global: count
23
+ }
24
+
25
+ if (peerId != null) {
26
+ stats[peerId.toString()] = count
27
+ }
28
+
29
+ this.blocksReceived?.increment(stats)
30
+ }
31
+
32
+ updateDuplicateBlocksReceived (count: number = 1, peerId?: PeerId): void {
33
+ const stats: Record<string, number | unknown> = {
34
+ global: count
35
+ }
36
+
37
+ if (peerId != null) {
38
+ stats[peerId.toString()] = count
39
+ }
40
+
41
+ this.duplicateBlocksReceived?.increment(stats)
42
+ }
43
+
44
+ updateDataReceived (bytes: number, peerId?: PeerId): void {
45
+ const stats: Record<string, number> = {
46
+ global: bytes
47
+ }
48
+
49
+ if (peerId != null) {
50
+ stats[peerId.toString()] = bytes
51
+ }
52
+
53
+ this.dataReceived?.increment(stats)
54
+ }
55
+
56
+ updateDuplicateDataReceived (bytes: number, peerId?: PeerId): void {
57
+ const stats: Record<string, number> = {
58
+ global: bytes
59
+ }
60
+
61
+ if (peerId != null) {
62
+ stats[peerId.toString()] = bytes
63
+ }
64
+
65
+ this.duplicateDataReceived?.increment(stats)
66
+ }
67
+ }
@@ -0,0 +1,8 @@
1
+ import ve from './varint-encoder.js'
2
+ import type { CID } from 'multiformats/cid'
3
+
4
+ export function cidToPrefix (cid: CID): Uint8Array {
5
+ return ve([
6
+ cid.version, cid.code, cid.multihash.code, cid.multihash.digest.byteLength
7
+ ])
8
+ }
@@ -0,0 +1,19 @@
1
+ import { decode, encodingLength } from 'uint8-varint'
2
+
3
+ function varintDecoder (buf: Uint8Array): number[] {
4
+ if (!(buf instanceof Uint8Array)) {
5
+ throw new Error('arg needs to be a Uint8Array')
6
+ }
7
+
8
+ const result: number[] = []
9
+
10
+ while (buf.length > 0) {
11
+ const num = decode(buf)
12
+ result.push(num)
13
+ buf = buf.slice(encodingLength(num))
14
+ }
15
+
16
+ return result
17
+ }
18
+
19
+ export default varintDecoder
@@ -0,0 +1,18 @@
1
+ import { encode, encodingLength } from 'uint8-varint'
2
+
3
+ function varintEncoder (buf: number[]): Uint8Array {
4
+ let out = new Uint8Array(buf.reduce((acc, curr) => {
5
+ return acc + encodingLength(curr)
6
+ }, 0))
7
+ let offset = 0
8
+
9
+ for (const num of buf) {
10
+ out = encode(num, out, offset)
11
+
12
+ offset += encodingLength(num)
13
+ }
14
+
15
+ return out
16
+ }
17
+
18
+ export default varintEncoder