@libp2p/kad-dht 0.28.6

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 (166) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +105 -0
  3. package/dist/src/constants.d.ts +20 -0
  4. package/dist/src/constants.d.ts.map +1 -0
  5. package/dist/src/constants.js +34 -0
  6. package/dist/src/constants.js.map +1 -0
  7. package/dist/src/content-fetching/index.d.ts +55 -0
  8. package/dist/src/content-fetching/index.d.ts.map +1 -0
  9. package/dist/src/content-fetching/index.js +190 -0
  10. package/dist/src/content-fetching/index.js.map +1 -0
  11. package/dist/src/content-routing/index.d.ts +42 -0
  12. package/dist/src/content-routing/index.d.ts.map +1 -0
  13. package/dist/src/content-routing/index.js +129 -0
  14. package/dist/src/content-routing/index.js.map +1 -0
  15. package/dist/src/dual-kad-dht.d.ts +65 -0
  16. package/dist/src/dual-kad-dht.d.ts.map +1 -0
  17. package/dist/src/dual-kad-dht.js +191 -0
  18. package/dist/src/dual-kad-dht.js.map +1 -0
  19. package/dist/src/index.d.ts +4 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +15 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/kad-dht.d.ts +131 -0
  24. package/dist/src/kad-dht.d.ts.map +1 -0
  25. package/dist/src/kad-dht.js +268 -0
  26. package/dist/src/kad-dht.js.map +1 -0
  27. package/dist/src/message/dht.d.ts +297 -0
  28. package/dist/src/message/dht.js +921 -0
  29. package/dist/src/message/index.d.ts +32 -0
  30. package/dist/src/message/index.d.ts.map +1 -0
  31. package/dist/src/message/index.js +81 -0
  32. package/dist/src/message/index.js.map +1 -0
  33. package/dist/src/network.d.ts +60 -0
  34. package/dist/src/network.d.ts.map +1 -0
  35. package/dist/src/network.js +124 -0
  36. package/dist/src/network.js.map +1 -0
  37. package/dist/src/peer-list/index.d.ts +29 -0
  38. package/dist/src/peer-list/index.d.ts.map +1 -0
  39. package/dist/src/peer-list/index.js +44 -0
  40. package/dist/src/peer-list/index.js.map +1 -0
  41. package/dist/src/peer-list/peer-distance-list.d.ts +34 -0
  42. package/dist/src/peer-list/peer-distance-list.d.ts.map +1 -0
  43. package/dist/src/peer-list/peer-distance-list.js +64 -0
  44. package/dist/src/peer-list/peer-distance-list.js.map +1 -0
  45. package/dist/src/peer-routing/index.d.ts +71 -0
  46. package/dist/src/peer-routing/index.d.ts.map +1 -0
  47. package/dist/src/peer-routing/index.js +256 -0
  48. package/dist/src/peer-routing/index.js.map +1 -0
  49. package/dist/src/providers.d.ts +64 -0
  50. package/dist/src/providers.d.ts.map +1 -0
  51. package/dist/src/providers.js +208 -0
  52. package/dist/src/providers.js.map +1 -0
  53. package/dist/src/query/events.d.ts +46 -0
  54. package/dist/src/query/events.d.ts.map +1 -0
  55. package/dist/src/query/events.js +73 -0
  56. package/dist/src/query/events.js.map +1 -0
  57. package/dist/src/query/manager.d.ts +40 -0
  58. package/dist/src/query/manager.d.ts.map +1 -0
  59. package/dist/src/query/manager.js +140 -0
  60. package/dist/src/query/manager.js.map +1 -0
  61. package/dist/src/query/query-path.d.ts +58 -0
  62. package/dist/src/query/query-path.d.ts.map +1 -0
  63. package/dist/src/query/query-path.js +171 -0
  64. package/dist/src/query/query-path.js.map +1 -0
  65. package/dist/src/query/types.d.ts +16 -0
  66. package/dist/src/query/types.d.ts.map +1 -0
  67. package/dist/src/query/types.js +2 -0
  68. package/dist/src/query/types.js.map +1 -0
  69. package/dist/src/query-self.d.ts +31 -0
  70. package/dist/src/query-self.d.ts.map +1 -0
  71. package/dist/src/query-self.js +73 -0
  72. package/dist/src/query-self.js.map +1 -0
  73. package/dist/src/routing-table/generated-prefix-list-browser.d.ts +3 -0
  74. package/dist/src/routing-table/generated-prefix-list-browser.d.ts.map +1 -0
  75. package/dist/src/routing-table/generated-prefix-list-browser.js +1027 -0
  76. package/dist/src/routing-table/generated-prefix-list-browser.js.map +1 -0
  77. package/dist/src/routing-table/generated-prefix-list.d.ts +3 -0
  78. package/dist/src/routing-table/generated-prefix-list.d.ts.map +1 -0
  79. package/dist/src/routing-table/generated-prefix-list.js +4099 -0
  80. package/dist/src/routing-table/generated-prefix-list.js.map +1 -0
  81. package/dist/src/routing-table/index.d.ts +91 -0
  82. package/dist/src/routing-table/index.d.ts.map +1 -0
  83. package/dist/src/routing-table/index.js +183 -0
  84. package/dist/src/routing-table/index.js.map +1 -0
  85. package/dist/src/routing-table/refresh.d.ts +50 -0
  86. package/dist/src/routing-table/refresh.d.ts.map +1 -0
  87. package/dist/src/routing-table/refresh.js +204 -0
  88. package/dist/src/routing-table/refresh.js.map +1 -0
  89. package/dist/src/routing-table/types.d.ts +24 -0
  90. package/dist/src/routing-table/types.d.ts.map +1 -0
  91. package/dist/src/rpc/handlers/add-provider.d.ts +13 -0
  92. package/dist/src/rpc/handlers/add-provider.d.ts.map +1 -0
  93. package/dist/src/rpc/handlers/add-provider.js +42 -0
  94. package/dist/src/rpc/handlers/add-provider.js.map +1 -0
  95. package/dist/src/rpc/handlers/find-node.d.ts +18 -0
  96. package/dist/src/rpc/handlers/find-node.d.ts.map +1 -0
  97. package/dist/src/rpc/handlers/find-node.js +32 -0
  98. package/dist/src/rpc/handlers/find-node.js.map +1 -0
  99. package/dist/src/rpc/handlers/get-providers.d.ts +24 -0
  100. package/dist/src/rpc/handlers/get-providers.d.ts.map +1 -0
  101. package/dist/src/rpc/handlers/get-providers.js +60 -0
  102. package/dist/src/rpc/handlers/get-providers.js.map +1 -0
  103. package/dist/src/rpc/handlers/get-value.d.ts +27 -0
  104. package/dist/src/rpc/handlers/get-value.d.ts.map +1 -0
  105. package/dist/src/rpc/handlers/get-value.js +94 -0
  106. package/dist/src/rpc/handlers/get-value.js.map +1 -0
  107. package/dist/src/rpc/handlers/index.d.ts +13 -0
  108. package/dist/src/rpc/handlers/index.d.ts.map +1 -0
  109. package/dist/src/rpc/handlers/ping.d.ts +7 -0
  110. package/dist/src/rpc/handlers/ping.d.ts.map +1 -0
  111. package/dist/src/rpc/handlers/ping.js +9 -0
  112. package/dist/src/rpc/handlers/ping.js.map +1 -0
  113. package/dist/src/rpc/handlers/put-value.d.ts +18 -0
  114. package/dist/src/rpc/handlers/put-value.d.ts.map +1 -0
  115. package/dist/src/rpc/handlers/put-value.js +35 -0
  116. package/dist/src/rpc/handlers/put-value.js.map +1 -0
  117. package/dist/src/rpc/index.d.ts +38 -0
  118. package/dist/src/rpc/index.d.ts.map +1 -0
  119. package/dist/src/rpc/index.js +75 -0
  120. package/dist/src/rpc/index.js.map +1 -0
  121. package/dist/src/rpc/types.d.ts +6 -0
  122. package/dist/src/rpc/types.d.ts.map +1 -0
  123. package/dist/src/topology-listener.d.ts +33 -0
  124. package/dist/src/topology-listener.d.ts.map +1 -0
  125. package/dist/src/topology-listener.js +50 -0
  126. package/dist/src/topology-listener.js.map +1 -0
  127. package/dist/src/types.d.ts +143 -0
  128. package/dist/src/types.d.ts.map +1 -0
  129. package/dist/src/utils.d.ts +33 -0
  130. package/dist/src/utils.d.ts.map +1 -0
  131. package/dist/src/utils.js +89 -0
  132. package/dist/src/utils.js.map +1 -0
  133. package/package.json +200 -0
  134. package/src/constants.ts +50 -0
  135. package/src/content-fetching/index.ts +276 -0
  136. package/src/content-routing/index.ts +202 -0
  137. package/src/dual-kad-dht.ts +257 -0
  138. package/src/index.ts +21 -0
  139. package/src/kad-dht.ts +396 -0
  140. package/src/message/dht.d.ts +297 -0
  141. package/src/message/dht.js +921 -0
  142. package/src/message/dht.proto +75 -0
  143. package/src/message/index.ts +111 -0
  144. package/src/network.ts +185 -0
  145. package/src/peer-list/index.ts +54 -0
  146. package/src/peer-list/peer-distance-list.ts +93 -0
  147. package/src/peer-routing/index.ts +332 -0
  148. package/src/providers.ts +278 -0
  149. package/src/query/events.ts +126 -0
  150. package/src/query/manager.ts +188 -0
  151. package/src/query/query-path.ts +263 -0
  152. package/src/query/types.ts +22 -0
  153. package/src/query-self.ts +106 -0
  154. package/src/routing-table/generated-prefix-list-browser.ts +1026 -0
  155. package/src/routing-table/generated-prefix-list.ts +4098 -0
  156. package/src/routing-table/index.ts +265 -0
  157. package/src/routing-table/refresh.ts +263 -0
  158. package/src/rpc/handlers/add-provider.ts +63 -0
  159. package/src/rpc/handlers/find-node.ts +57 -0
  160. package/src/rpc/handlers/get-providers.ts +95 -0
  161. package/src/rpc/handlers/get-value.ts +130 -0
  162. package/src/rpc/handlers/ping.ts +13 -0
  163. package/src/rpc/handlers/put-value.ts +58 -0
  164. package/src/rpc/index.ts +118 -0
  165. package/src/topology-listener.ts +78 -0
  166. package/src/utils.ts +108 -0
@@ -0,0 +1,332 @@
1
+ import errcode from 'err-code'
2
+ import { verifyRecord } from '@libp2p/record/validators'
3
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
+ import { Message, MESSAGE_TYPE } from '../message/index.js'
5
+ import * as utils from '../utils.js'
6
+ import {
7
+ queryErrorEvent,
8
+ finalPeerEvent,
9
+ valueEvent
10
+ } from '../query/events.js'
11
+ import { PeerDistanceList } from '../peer-list/peer-distance-list.js'
12
+ import { Libp2pRecord } from '@libp2p/record'
13
+ import { base58btc } from 'multiformats/bases/base58'
14
+ import { logger } from '@libp2p/logger'
15
+ import { keys } from '@libp2p/crypto'
16
+ import { peerIdFromKeys } from '@libp2p/peer-id'
17
+ import type { DHTRecord, QueryOptions, Validators } from '@libp2p/interfaces/dht'
18
+ import type { RoutingTable } from '../routing-table/index.js'
19
+ import type { PeerStore } from '@libp2p/interfaces/peer-store'
20
+ import type { QueryManager } from '../query/manager.js'
21
+ import type { Network } from '../network.js'
22
+ import type { Logger } from '@libp2p/logger'
23
+ import type { AbortOptions } from '@libp2p/interfaces'
24
+ import type { QueryFunc } from '../query/types.js'
25
+ import type { PeerData } from '@libp2p/interfaces/peer-data'
26
+ import type { PeerId } from '@libp2p/interfaces/peer-id'
27
+
28
+ export interface PeerRoutingOptions {
29
+ peerId: PeerId
30
+ routingTable: RoutingTable
31
+ peerStore: PeerStore
32
+ network: Network
33
+ validators: Validators
34
+ queryManager: QueryManager
35
+ lan: boolean
36
+ }
37
+
38
+ export class PeerRouting {
39
+ private readonly log: Logger
40
+ private readonly peerId: PeerId
41
+ private readonly routingTable: RoutingTable
42
+ private readonly peerStore: PeerStore
43
+ private readonly network: Network
44
+ private readonly validators: Validators
45
+ private readonly queryManager: QueryManager
46
+
47
+ constructor (options: PeerRoutingOptions) {
48
+ const { peerId, routingTable, peerStore, network, validators, queryManager, lan } = options
49
+
50
+ this.peerId = peerId
51
+ this.routingTable = routingTable
52
+ this.peerStore = peerStore
53
+ this.network = network
54
+ this.validators = validators
55
+ this.queryManager = queryManager
56
+ this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:peer-routing`)
57
+ }
58
+
59
+ /**
60
+ * Look if we are connected to a peer with the given id.
61
+ * Returns its id and addresses, if found, otherwise `undefined`.
62
+ */
63
+ async findPeerLocal (peer: PeerId) {
64
+ let peerData
65
+ const p = await this.routingTable.find(peer)
66
+
67
+ if (p != null) {
68
+ this.log('findPeerLocal found %p in routing table', peer)
69
+
70
+ try {
71
+ peerData = await this.peerStore.get(p)
72
+ } catch (err: any) {
73
+ if (err.code !== 'ERR_NOT_FOUND') {
74
+ throw err
75
+ }
76
+ }
77
+ }
78
+
79
+ if (peerData == null) {
80
+ try {
81
+ peerData = await this.peerStore.get(peer)
82
+ } catch (err: any) {
83
+ if (err.code !== 'ERR_NOT_FOUND') {
84
+ throw err
85
+ }
86
+ }
87
+ }
88
+
89
+ if (peerData != null) {
90
+ this.log('findPeerLocal found %p in peer store', peer)
91
+
92
+ return {
93
+ id: peerData.id,
94
+ multiaddrs: peerData.addresses.map((address) => address.multiaddr),
95
+ protocols: []
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get a value via rpc call for the given parameters
102
+ */
103
+ async * _getValueSingle (peer: PeerId, key: Uint8Array, options: AbortOptions = {}) { // eslint-disable-line require-await
104
+ const msg = new Message(MESSAGE_TYPE.GET_VALUE, key, 0)
105
+ yield * this.network.sendRequest(peer, msg, options)
106
+ }
107
+
108
+ /**
109
+ * Get the public key directly from a node
110
+ */
111
+ async * getPublicKeyFromNode (peer: PeerId, options: AbortOptions = {}) {
112
+ const pkKey = utils.keyForPublicKey(peer)
113
+
114
+ for await (const event of this._getValueSingle(peer, pkKey, options)) {
115
+ yield event
116
+
117
+ if (event.name === 'PEER_RESPONSE' && event.record != null) {
118
+ const recPeer = await peerIdFromKeys(keys.marshalPublicKey({ bytes: event.record.value }))
119
+
120
+ // compare hashes of the pub key
121
+ if (!recPeer.equals(peer)) {
122
+ throw errcode(new Error('public key does not match id'), 'ERR_PUBLIC_KEY_DOES_NOT_MATCH_ID')
123
+ }
124
+
125
+ if (recPeer.publicKey == null) {
126
+ throw errcode(new Error('public key missing'), 'ERR_PUBLIC_KEY_MISSING')
127
+ }
128
+
129
+ yield valueEvent({ from: peer, value: recPeer.publicKey })
130
+ }
131
+ }
132
+
133
+ throw errcode(new Error(`Node not responding with its public key: ${peer.toString(base58btc)}`), 'ERR_INVALID_RECORD')
134
+ }
135
+
136
+ /**
137
+ * Search for a peer with the given ID
138
+ */
139
+ async * findPeer (id: PeerId, options: QueryOptions = {}) {
140
+ this.log('findPeer %p', id)
141
+
142
+ // Try to find locally
143
+ const pi = await this.findPeerLocal(id)
144
+
145
+ // already got it
146
+ if (pi != null) {
147
+ this.log('found local')
148
+ yield finalPeerEvent({
149
+ from: this.peerId,
150
+ peer: pi
151
+ })
152
+ return
153
+ }
154
+
155
+ const key = await utils.convertPeerId(id)
156
+ const peers = this.routingTable.closestPeers(key)
157
+
158
+ // sanity check
159
+ const match = peers.find((p) => p.equals(id))
160
+
161
+ if (match != null) {
162
+ try {
163
+ const peer = await this.peerStore.get(id)
164
+
165
+ this.log('found in peerStore')
166
+ yield finalPeerEvent({
167
+ from: this.peerId,
168
+ peer: {
169
+ id: peer.id,
170
+ multiaddrs: peer.addresses.map((address) => address.multiaddr),
171
+ protocols: []
172
+ }
173
+ })
174
+
175
+ return
176
+ } catch (err: any) {
177
+ if (err.code !== 'ERR_NOT_FOUND') {
178
+ throw err
179
+ }
180
+ }
181
+ }
182
+
183
+ const self = this // eslint-disable-line @typescript-eslint/no-this-alias
184
+
185
+ const findPeerQuery: QueryFunc = async function * ({ peer, signal }) {
186
+ const request = new Message(MESSAGE_TYPE.FIND_NODE, id.toBytes(), 0)
187
+
188
+ for await (const event of self.network.sendRequest(peer, request, { signal })) {
189
+ yield event
190
+
191
+ if (event.name === 'PEER_RESPONSE') {
192
+ const match = event.closer.find((p) => p.id.equals(id))
193
+
194
+ // found the peer
195
+ if (match != null) {
196
+ yield finalPeerEvent({ from: event.from, peer: match })
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ let foundPeer = false
203
+
204
+ for await (const event of this.queryManager.run(id.toBytes(), peers, findPeerQuery, options)) {
205
+ if (event.name === 'FINAL_PEER') {
206
+ foundPeer = true
207
+ }
208
+
209
+ yield event
210
+ }
211
+
212
+ if (!foundPeer) {
213
+ yield queryErrorEvent({ from: this.peerId, error: errcode(new Error('Not found'), 'ERR_NOT_FOUND') })
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Kademlia 'node lookup' operation on a key, which could be a the
219
+ * bytes from a multihash or a peer ID
220
+ */
221
+ async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}) {
222
+ this.log('getClosestPeers to %b', key)
223
+ const id = await utils.convertBuffer(key)
224
+ const tablePeers = this.routingTable.closestPeers(id)
225
+ const self = this // eslint-disable-line @typescript-eslint/no-this-alias
226
+
227
+ const peers = new PeerDistanceList(id, this.routingTable.kBucketSize)
228
+ await Promise.all(tablePeers.map(async peer => await peers.add(peer)))
229
+
230
+ const getCloserPeersQuery: QueryFunc = async function * ({ peer, signal }) {
231
+ self.log('closerPeersSingle %s from %p', uint8ArrayToString(key, 'base32'), peer)
232
+ const request = new Message(MESSAGE_TYPE.FIND_NODE, key, 0)
233
+
234
+ yield * self.network.sendRequest(peer, request, { signal })
235
+ }
236
+
237
+ for await (const event of this.queryManager.run(key, tablePeers, getCloserPeersQuery, options)) {
238
+ yield event
239
+
240
+ if (event.name === 'PEER_RESPONSE') {
241
+ await Promise.all(event.closer.map(async peerData => await peers.add(peerData.id)))
242
+ }
243
+ }
244
+
245
+ this.log('found %d peers close to %b', peers.length, key)
246
+
247
+ for (const peer of peers.peers) {
248
+ yield finalPeerEvent({
249
+ from: this.peerId,
250
+ peer: {
251
+ id: peer,
252
+ multiaddrs: (await (this.peerStore.addressBook.get(peer)) ?? []).map(addr => addr.multiaddr),
253
+ protocols: []
254
+ }
255
+ })
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Query a particular peer for the value for the given key.
261
+ * It will either return the value or a list of closer peers.
262
+ *
263
+ * Note: The peerStore is updated with new addresses found for the given peer.
264
+ */
265
+ async * getValueOrPeers (peer: PeerId, key: Uint8Array, options: AbortOptions = {}) {
266
+ for await (const event of this._getValueSingle(peer, key, options)) {
267
+ if (event.name === 'PEER_RESPONSE') {
268
+ if (event.record != null) {
269
+ // We have a record
270
+ try {
271
+ await this._verifyRecordOnline(event.record)
272
+ } catch (err: any) {
273
+ const errMsg = 'invalid record received, discarded'
274
+ this.log(errMsg)
275
+
276
+ yield queryErrorEvent({ from: event.from, error: errcode(new Error(errMsg), 'ERR_INVALID_RECORD') })
277
+ continue
278
+ }
279
+ }
280
+ }
281
+
282
+ yield event
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Verify a record, fetching missing public keys from the network.
288
+ * Throws an error if the record is invalid.
289
+ */
290
+ async _verifyRecordOnline (record: DHTRecord) {
291
+ await verifyRecord(this.validators, new Libp2pRecord(record.key, record.value, record.timeReceived))
292
+ }
293
+
294
+ /**
295
+ * Get the nearest peers to the given query, but if closer
296
+ * than self
297
+ */
298
+ async getCloserPeersOffline (key: Uint8Array, closerThan: PeerId) {
299
+ const id = await utils.convertBuffer(key)
300
+ const ids = this.routingTable.closestPeers(id)
301
+ const output: PeerData[] = []
302
+
303
+ for (const peerId of ids) {
304
+ if (peerId.equals(closerThan)) {
305
+ continue
306
+ }
307
+
308
+ try {
309
+ const addresses = await this.peerStore.addressBook.get(peerId)
310
+ const protocols = await this.peerStore.protoBook.get(peerId)
311
+
312
+ output.push({
313
+ id: peerId,
314
+ multiaddrs: addresses.map((address) => address.multiaddr),
315
+ protocols
316
+ })
317
+ } catch (err: any) {
318
+ if (err.code !== 'ERR_NOT_FOUND') {
319
+ throw err
320
+ }
321
+ }
322
+ }
323
+
324
+ if (output.length > 0) {
325
+ this.log('getCloserPeersOffline found %d peer(s) closer to %b than %p', output.length, key, closerThan)
326
+ } else {
327
+ this.log('getCloserPeersOffline could not find peer closer to %b than %p', key, closerThan)
328
+ }
329
+
330
+ return output
331
+ }
332
+ }
@@ -0,0 +1,278 @@
1
+ import cache from 'hashlru'
2
+ import varint from 'varint'
3
+ import { Key } from 'interface-datastore/key'
4
+ import Queue from 'p-queue'
5
+ import {
6
+ PROVIDERS_CLEANUP_INTERVAL,
7
+ PROVIDERS_VALIDITY,
8
+ PROVIDERS_LRU_CACHE_SIZE,
9
+ PROVIDER_KEY_PREFIX
10
+ } from './constants.js'
11
+ import { logger } from '@libp2p/logger'
12
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
13
+ import { peerIdFromString } from '@libp2p/peer-id'
14
+ import type { Datastore } from 'interface-datastore'
15
+ import type { Startable } from '@libp2p/interfaces'
16
+ import type { CID } from 'multiformats'
17
+ import type { PeerId } from '@libp2p/interfaces/peer-id'
18
+
19
+ const log = logger('libp2p:kad-dht:providers')
20
+
21
+ export interface ProvidersOptions {
22
+ datastore: Datastore
23
+ cacheSize?: number
24
+ /**
25
+ * How often invalid records are cleaned. (in seconds)
26
+ */
27
+ cleanupInterval?: number
28
+ /**
29
+ * How long is a provider valid for. (in seconds)
30
+ */
31
+ provideValidity?: number
32
+ }
33
+
34
+ /**
35
+ * This class manages known providers.
36
+ * A provider is a peer that we know to have the content for a given CID.
37
+ *
38
+ * Every `cleanupInterval` providers are checked if they
39
+ * are still valid, i.e. younger than the `provideValidity`.
40
+ * If they are not, they are deleted.
41
+ *
42
+ * To ensure the list survives restarts of the daemon,
43
+ * providers are stored in the datastore, but to ensure
44
+ * access is fast there is an LRU cache in front of that.
45
+ */
46
+ export class Providers implements Startable {
47
+ private readonly datastore: Datastore
48
+ private readonly cache: ReturnType<typeof cache>
49
+ private readonly cleanupInterval: number
50
+ private readonly provideValidity: number
51
+ private readonly syncQueue: Queue
52
+ private started: boolean
53
+ private cleaner?: NodeJS.Timer
54
+
55
+ constructor (options: ProvidersOptions) {
56
+ const { datastore, cacheSize, cleanupInterval, provideValidity } = options
57
+
58
+ this.datastore = datastore
59
+ this.cleanupInterval = cleanupInterval ?? PROVIDERS_CLEANUP_INTERVAL
60
+ this.provideValidity = provideValidity ?? PROVIDERS_VALIDITY
61
+ this.cache = cache(cacheSize ?? PROVIDERS_LRU_CACHE_SIZE)
62
+ this.syncQueue = new Queue({ concurrency: 1 })
63
+ this.started = false
64
+ }
65
+
66
+ isStarted () {
67
+ return this.started
68
+ }
69
+
70
+ /**
71
+ * Start the provider cleanup service
72
+ */
73
+ async start () {
74
+ if (this.started) {
75
+ return
76
+ }
77
+
78
+ this.started = true
79
+
80
+ this.cleaner = setInterval(
81
+ () => {
82
+ this._cleanup().catch(err => {
83
+ log.error(err)
84
+ })
85
+ },
86
+ this.cleanupInterval
87
+ )
88
+ }
89
+
90
+ /**
91
+ * Release any resources.
92
+ */
93
+ async stop () {
94
+ this.started = false
95
+
96
+ if (this.cleaner != null) {
97
+ clearInterval(this.cleaner)
98
+ this.cleaner = undefined
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check all providers if they are still valid, and if not delete them
104
+ */
105
+ async _cleanup () {
106
+ return await this.syncQueue.add(async () => {
107
+ const start = Date.now()
108
+
109
+ let count = 0
110
+ let deleteCount = 0
111
+ const deleted = new Map<string, Set<string>>()
112
+ const batch = this.datastore.batch()
113
+
114
+ // Get all provider entries from the datastore
115
+ const query = this.datastore.query({ prefix: PROVIDER_KEY_PREFIX })
116
+
117
+ for await (const entry of query) {
118
+ try {
119
+ // Add a delete to the batch for each expired entry
120
+ const { cid, peerId } = parseProviderKey(entry.key)
121
+ const time = readTime(entry.value).getTime()
122
+ const now = Date.now()
123
+ const delta = now - time
124
+ const expired = delta > this.provideValidity
125
+
126
+ log('comparing: %d - %d = %d > %d %s', now, time, delta, this.provideValidity, expired ? '(expired)' : '')
127
+
128
+ if (expired) {
129
+ deleteCount++
130
+ batch.delete(entry.key)
131
+ const peers = deleted.get(cid) ?? new Set<string>()
132
+ peers.add(peerId)
133
+ deleted.set(cid, peers)
134
+ }
135
+ count++
136
+ } catch (err: any) {
137
+ log.error(err.message)
138
+ }
139
+ }
140
+
141
+ // Commit the deletes to the datastore
142
+ if (deleted.size > 0) {
143
+ log('deleting %d / %d entries', deleteCount, count)
144
+ await batch.commit()
145
+ } else {
146
+ log('nothing to delete')
147
+ }
148
+
149
+ // Clear expired entries from the cache
150
+ for (const [cid, peers] of deleted) {
151
+ const key = makeProviderKey(cid)
152
+ const provs = this.cache.get(key)
153
+
154
+ if (provs != null) {
155
+ for (const peerId of peers) {
156
+ provs.delete(peerId)
157
+ }
158
+
159
+ if (provs.size === 0) {
160
+ this.cache.remove(key)
161
+ } else {
162
+ this.cache.set(key, provs)
163
+ }
164
+ }
165
+ }
166
+
167
+ log('Cleanup successful (%dms)', Date.now() - start)
168
+ })
169
+ }
170
+
171
+ /**
172
+ * Get the currently known provider peer ids for a given CID
173
+ */
174
+ async _getProvidersMap (cid: CID) {
175
+ const cacheKey = makeProviderKey(cid)
176
+ let provs: Map<string, Date> = this.cache.get(cacheKey)
177
+
178
+ if (provs == null) {
179
+ provs = await loadProviders(this.datastore, cid)
180
+ this.cache.set(cacheKey, provs)
181
+ }
182
+
183
+ return provs
184
+ }
185
+
186
+ /**
187
+ * Add a new provider for the given CID
188
+ */
189
+ async addProvider (cid: CID, provider: PeerId) {
190
+ return await this.syncQueue.add(async () => {
191
+ log('%p provides %s', provider, cid)
192
+ const provs = await this._getProvidersMap(cid)
193
+
194
+ log('loaded %s provs', provs.size)
195
+ const now = new Date()
196
+ provs.set(provider.toString(), now)
197
+
198
+ const dsKey = makeProviderKey(cid)
199
+ this.cache.set(dsKey, provs)
200
+
201
+ await writeProviderEntry(this.datastore, cid, provider, now)
202
+ })
203
+ }
204
+
205
+ /**
206
+ * Get a list of providers for the given CID
207
+ */
208
+ async getProviders (cid: CID): Promise<PeerId[]> {
209
+ return await this.syncQueue.add(async () => {
210
+ log('get providers for %s', cid)
211
+ const provs = await this._getProvidersMap(cid)
212
+
213
+ return [...provs.keys()].map(peerIdStr => {
214
+ return peerIdFromString(peerIdStr)
215
+ })
216
+ })
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Encode the given key its matching datastore key
222
+ */
223
+ function makeProviderKey (cid: CID | string) {
224
+ const cidStr = typeof cid === 'string' ? cid : uint8ArrayToString(cid.multihash.bytes, 'base32')
225
+
226
+ return `${PROVIDER_KEY_PREFIX}/${cidStr}`
227
+ }
228
+
229
+ /**
230
+ * Write a provider into the given store
231
+ */
232
+ async function writeProviderEntry (store: Datastore, cid: CID, peer: PeerId, time: Date) { // eslint-disable-line require-await
233
+ const dsKey = [
234
+ makeProviderKey(cid),
235
+ '/',
236
+ peer.toString()
237
+ ].join('')
238
+
239
+ const key = new Key(dsKey)
240
+ const buffer = Uint8Array.from(varint.encode(time.getTime()))
241
+
242
+ return await store.put(key, buffer)
243
+ }
244
+
245
+ /**
246
+ * Parse the CID and provider peer id from the key
247
+ */
248
+ function parseProviderKey (key: Key) {
249
+ const parts = key.toString().split('/')
250
+
251
+ if (parts.length !== 5) {
252
+ throw new Error(`incorrectly formatted provider entry key in datastore: ${key.toString()}`)
253
+ }
254
+
255
+ return {
256
+ cid: parts[3],
257
+ peerId: parts[4]
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Load providers for the given CID from the store
263
+ */
264
+ async function loadProviders (store: Datastore, cid: CID) {
265
+ const providers = new Map<string, Date>()
266
+ const query = store.query({ prefix: makeProviderKey(cid) })
267
+
268
+ for await (const entry of query) {
269
+ const { peerId } = parseProviderKey(entry.key)
270
+ providers.set(peerId, readTime(entry.value))
271
+ }
272
+
273
+ return providers
274
+ }
275
+
276
+ function readTime (buf: Uint8Array) {
277
+ return new Date(varint.decode(buf))
278
+ }