helia 6.1.4 → 7.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.
Files changed (92) hide show
  1. package/dist/index.min.js +65 -55
  2. package/dist/index.min.js.map +4 -4
  3. package/dist/src/block-storage.d.ts +62 -0
  4. package/dist/src/block-storage.d.ts.map +1 -0
  5. package/dist/src/block-storage.js +159 -0
  6. package/dist/src/block-storage.js.map +1 -0
  7. package/dist/src/datastore-version.d.ts +3 -0
  8. package/dist/src/datastore-version.d.ts.map +1 -0
  9. package/dist/src/datastore-version.js +20 -0
  10. package/dist/src/datastore-version.js.map +1 -0
  11. package/dist/src/get-codec.d.ts +4 -0
  12. package/dist/src/get-codec.d.ts.map +1 -0
  13. package/dist/src/get-codec.js +31 -0
  14. package/dist/src/get-codec.js.map +1 -0
  15. package/dist/src/get-crypto.d.ts +4 -0
  16. package/dist/src/get-crypto.d.ts.map +1 -0
  17. package/dist/src/get-crypto.js +35 -0
  18. package/dist/src/get-crypto.js.map +1 -0
  19. package/dist/src/get-hasher.d.ts +4 -0
  20. package/dist/src/get-hasher.d.ts.map +1 -0
  21. package/dist/src/get-hasher.js +31 -0
  22. package/dist/src/get-hasher.js.map +1 -0
  23. package/dist/src/helia.d.ts +165 -0
  24. package/dist/src/helia.d.ts.map +1 -0
  25. package/dist/src/helia.js +165 -0
  26. package/dist/src/helia.js.map +1 -0
  27. package/dist/src/index.d.ts +30 -19
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +78 -12
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/networked-storage.d.ts +28 -0
  32. package/dist/src/networked-storage.d.ts.map +1 -0
  33. package/dist/src/networked-storage.js +52 -0
  34. package/dist/src/networked-storage.js.map +1 -0
  35. package/dist/src/pins.d.ts +21 -0
  36. package/dist/src/pins.d.ts.map +1 -0
  37. package/dist/src/pins.js +140 -0
  38. package/dist/src/pins.js.map +1 -0
  39. package/dist/src/routing.d.ts +51 -0
  40. package/dist/src/routing.d.ts.map +1 -0
  41. package/dist/src/routing.js +311 -0
  42. package/dist/src/routing.js.map +1 -0
  43. package/dist/src/session-storage.d.ts +47 -0
  44. package/dist/src/session-storage.d.ts.map +1 -0
  45. package/dist/src/session-storage.js +148 -0
  46. package/dist/src/session-storage.js.map +1 -0
  47. package/dist/src/storage.d.ts +55 -0
  48. package/dist/src/storage.d.ts.map +1 -0
  49. package/dist/src/storage.js +225 -0
  50. package/dist/src/storage.js.map +1 -0
  51. package/dist/src/version.d.ts +1 -1
  52. package/dist/src/version.js +1 -1
  53. package/dist/typedoc-urls.json +3 -6
  54. package/package.json +38 -41
  55. package/src/block-storage.ts +194 -0
  56. package/src/datastore-version.ts +25 -0
  57. package/src/get-codec.ts +39 -0
  58. package/src/get-crypto.ts +44 -0
  59. package/src/get-hasher.ts +39 -0
  60. package/src/helia.ts +364 -0
  61. package/src/index.ts +58 -30
  62. package/src/networked-storage.ts +74 -0
  63. package/src/pins.ts +208 -0
  64. package/src/routing.ts +389 -0
  65. package/src/session-storage.ts +174 -0
  66. package/src/storage.ts +294 -0
  67. package/src/version.ts +1 -1
  68. package/dist/src/utils/bootstrappers.d.ts +0 -4
  69. package/dist/src/utils/bootstrappers.d.ts.map +0 -1
  70. package/dist/src/utils/bootstrappers.js +0 -13
  71. package/dist/src/utils/bootstrappers.js.map +0 -1
  72. package/dist/src/utils/helia-defaults.d.ts +0 -48
  73. package/dist/src/utils/helia-defaults.d.ts.map +0 -1
  74. package/dist/src/utils/helia-defaults.js +0 -85
  75. package/dist/src/utils/helia-defaults.js.map +0 -1
  76. package/dist/src/utils/libp2p-defaults.browser.d.ts +0 -19
  77. package/dist/src/utils/libp2p-defaults.browser.d.ts.map +0 -1
  78. package/dist/src/utils/libp2p-defaults.browser.js +0 -73
  79. package/dist/src/utils/libp2p-defaults.browser.js.map +0 -1
  80. package/dist/src/utils/libp2p-defaults.d.ts +0 -49
  81. package/dist/src/utils/libp2p-defaults.d.ts.map +0 -1
  82. package/dist/src/utils/libp2p-defaults.js +0 -113
  83. package/dist/src/utils/libp2p-defaults.js.map +0 -1
  84. package/dist/src/utils/libp2p.d.ts +0 -19
  85. package/dist/src/utils/libp2p.d.ts.map +0 -1
  86. package/dist/src/utils/libp2p.js +0 -19
  87. package/dist/src/utils/libp2p.js.map +0 -1
  88. package/src/utils/bootstrappers.ts +0 -12
  89. package/src/utils/helia-defaults.ts +0 -92
  90. package/src/utils/libp2p-defaults.browser.ts +0 -92
  91. package/src/utils/libp2p-defaults.ts +0 -137
  92. package/src/utils/libp2p.ts +0 -42
package/src/pins.ts ADDED
@@ -0,0 +1,208 @@
1
+ import { AlreadyPinnedError, depthFirstWalker } from '@helia/utils'
2
+ import { InvalidParametersError } from '@libp2p/interface'
3
+ import * as cborg from 'cborg'
4
+ import { Key } from 'interface-datastore'
5
+ import { base36 } from 'multiformats/bases/base36'
6
+ import { CID } from 'multiformats/cid'
7
+ import { CustomProgressEvent } from 'progress-events'
8
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
9
+ import type { CodecLoader } from '@helia/interface'
10
+ import type { AddOptions, IsPinnedOptions, LsOptions, Pin, Pins, RmOptions } from '@helia/interface'
11
+ import type { AbortOptions } from '@libp2p/interface'
12
+ import type { Blockstore } from 'interface-blockstore'
13
+ import type { Datastore } from 'interface-datastore'
14
+ import type { Version } from 'multiformats/cid'
15
+
16
+ interface DatastorePin {
17
+ /**
18
+ * 0 for a direct pin or an arbitrary (+ve, whole) number or Infinity
19
+ */
20
+ depth: number
21
+
22
+ /**
23
+ * User-specific metadata for the pin
24
+ */
25
+ metadata: Record<string, string | number | boolean>
26
+ }
27
+
28
+ interface DatastorePinnedBlock {
29
+ pinCount: number
30
+ pinnedBy: Uint8Array[]
31
+ }
32
+
33
+ /**
34
+ * Callback for updating a {@link DatastorePinnedBlock}'s properties when
35
+ * calling `#updatePinnedBlock`
36
+ *
37
+ * The callback should return `false` to prevent any pinning modifications to
38
+ * the block, and true in all other cases.
39
+ */
40
+ interface WithPinnedBlockCallback {
41
+ (pinnedBlock: DatastorePinnedBlock): boolean
42
+ }
43
+
44
+ const DATASTORE_PIN_PREFIX = '/pin/'
45
+ const DATASTORE_BLOCK_PREFIX = '/pinned-block/'
46
+ const DATASTORE_ENCODING = base36
47
+
48
+ function toDSKey (cid: CID): Key {
49
+ if (cid.version === 0) {
50
+ cid = cid.toV1()
51
+ }
52
+
53
+ return new Key(`${DATASTORE_PIN_PREFIX}${cid.toString(DATASTORE_ENCODING)}`)
54
+ }
55
+
56
+ export class PinsImpl implements Pins {
57
+ private readonly datastore: Datastore
58
+ private readonly blockstore: Blockstore
59
+ private readonly getCodec: CodecLoader
60
+
61
+ constructor (datastore: Datastore, blockstore: Blockstore, getCodec: CodecLoader) {
62
+ this.datastore = datastore
63
+ this.blockstore = blockstore
64
+ this.getCodec = getCodec
65
+ }
66
+
67
+ async * add (cid: CID<unknown, number, number, Version>, options: AddOptions = {}): AsyncGenerator<CID, void, undefined> {
68
+ const pinKey = toDSKey(cid)
69
+
70
+ if (await this.datastore.has(pinKey)) {
71
+ throw new AlreadyPinnedError('Already pinned')
72
+ }
73
+
74
+ const depth = Math.round(options.depth ?? Infinity)
75
+
76
+ if (depth < 0) {
77
+ throw new InvalidParametersError('Depth must be greater than or equal to 0')
78
+ }
79
+
80
+ const walker = (options.walker ?? depthFirstWalker())({
81
+ blockstore: this.blockstore,
82
+ getCodec: this.getCodec
83
+ })
84
+
85
+ for await (const node of walker.walk(cid, { ...options, depth })) {
86
+ const childCid = node.block.cid
87
+ await this.#updatePinnedBlock(childCid, (pinnedBlock: DatastorePinnedBlock) => {
88
+ // do not update pinned block if this block is already pinned by this CID
89
+ if (pinnedBlock.pinnedBy.find(c => uint8ArrayEquals(c, cid.bytes)) != null) {
90
+ return false
91
+ }
92
+
93
+ pinnedBlock.pinCount++
94
+ pinnedBlock.pinnedBy.push(cid.bytes)
95
+ return true
96
+ }, options)
97
+
98
+ yield childCid
99
+ }
100
+
101
+ const pin: DatastorePin = {
102
+ depth,
103
+ metadata: options.metadata ?? {}
104
+ }
105
+
106
+ await this.datastore.put(pinKey, cborg.encode(pin), options)
107
+ }
108
+
109
+ /**
110
+ * Update the pin count for the CID
111
+ */
112
+ async #updatePinnedBlock (cid: CID, withPinnedBlock: WithPinnedBlockCallback, options: AddOptions): Promise<void> {
113
+ const blockKey = new Key(`${DATASTORE_BLOCK_PREFIX}${DATASTORE_ENCODING.encode(cid.multihash.bytes)}`)
114
+
115
+ let pinnedBlock: DatastorePinnedBlock = {
116
+ pinCount: 0,
117
+ pinnedBy: []
118
+ }
119
+
120
+ try {
121
+ pinnedBlock = cborg.decode(await this.datastore.get(blockKey, options))
122
+ } catch (err: any) {
123
+ if (err.name !== 'NotFoundError') {
124
+ throw err
125
+ }
126
+ }
127
+
128
+ const shouldContinue = withPinnedBlock(pinnedBlock)
129
+
130
+ if (!shouldContinue) {
131
+ return
132
+ }
133
+
134
+ if (pinnedBlock.pinCount === 0) {
135
+ if (await this.datastore.has(blockKey)) {
136
+ await this.datastore.delete(blockKey)
137
+ return
138
+ }
139
+ }
140
+
141
+ await this.datastore.put(blockKey, cborg.encode(pinnedBlock), options)
142
+ options.onProgress?.(new CustomProgressEvent<CID>('helia:pin:add', cid))
143
+ }
144
+
145
+ async * rm (cid: CID<unknown, number, number, Version>, options: RmOptions = {}): AsyncGenerator<CID, void, undefined> {
146
+ const pinKey = toDSKey(cid)
147
+ const buf = await this.datastore.get(pinKey, options)
148
+ const pin = cborg.decode(buf)
149
+
150
+ await this.datastore.delete(pinKey, options)
151
+
152
+ const walker = (options.walker ?? depthFirstWalker())({
153
+ blockstore: this.blockstore,
154
+ getCodec: this.getCodec
155
+ })
156
+
157
+ for await (const node of walker.walk(cid, { ...options, depth: pin.depth })) {
158
+ const childCid = node.block.cid
159
+ await this.#updatePinnedBlock(childCid, (pinnedBlock): boolean => {
160
+ pinnedBlock.pinCount--
161
+ pinnedBlock.pinnedBy = pinnedBlock.pinnedBy.filter(c => uint8ArrayEquals(c, cid.bytes))
162
+ return true
163
+ }, {
164
+ ...options,
165
+ depth: pin.depth
166
+ })
167
+
168
+ yield childCid
169
+ }
170
+ }
171
+
172
+ async * ls (options: LsOptions = {}): AsyncGenerator<Pin, void, undefined> {
173
+ for await (const { key, value } of this.datastore.query({
174
+ prefix: DATASTORE_PIN_PREFIX + (options.cid != null ? `${options.cid.toString(base36)}` : '')
175
+ }, options)) {
176
+ const cid = CID.parse(key.toString().substring(5), base36)
177
+ const pin = cborg.decode(value)
178
+
179
+ yield {
180
+ cid,
181
+ ...pin
182
+ }
183
+ }
184
+ }
185
+
186
+ async isPinned (cid: CID, options: IsPinnedOptions = {}): Promise<boolean> {
187
+ const blockKey = new Key(`${DATASTORE_BLOCK_PREFIX}${DATASTORE_ENCODING.encode(cid.multihash.bytes)}`)
188
+
189
+ return this.datastore.has(blockKey, options)
190
+ }
191
+
192
+ async get (cid: CID, options?: AbortOptions): Promise<Pin> {
193
+ const pinKey = toDSKey(cid)
194
+ const buf = await this.datastore.get(pinKey, options)
195
+
196
+ return cborg.decode(buf)
197
+ }
198
+
199
+ async setMetadata (cid: CID, metadata: Record<string, string | number | boolean> | undefined, options?: AbortOptions): Promise<void> {
200
+ const pinKey = toDSKey(cid)
201
+ const buf = await this.datastore.get(pinKey, options)
202
+ const pin: DatastorePin = cborg.decode(buf)
203
+
204
+ pin.metadata = metadata ?? {}
205
+
206
+ await this.datastore.put(pinKey, cborg.encode(pin), options)
207
+ }
208
+ }
package/src/routing.ts ADDED
@@ -0,0 +1,389 @@
1
+ import { NoRoutersAvailableError } from '@helia/interface'
2
+ import { GetFailedError } from '@helia/utils'
3
+ import { NotFoundError, start, stop } from '@libp2p/interface'
4
+ import { Queue } from '@libp2p/utils'
5
+ import merge from 'it-merge'
6
+ import { CustomProgressEvent } from 'progress-events'
7
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
8
+ import type { Routing as RoutingInterface, Provider, RoutingOptions, RoutingFindProvidersProgressEvents, RoutingProvideProgressEvents, RoutingPutProgressEvents, RoutingGetProgressEvents, RoutingFindPeerProgressEvents, RoutingGetClosestPeersProgressEvents, RoutingCancelReprovideProgressEvents, Router, Peer } from '@helia/interface'
9
+ import type { ComponentLogger, Logger, Metrics, Startable } from '@libp2p/interface'
10
+ import type { AbortOptions } from 'abort-error'
11
+ import type { CID } from 'multiformats/cid'
12
+
13
+ const DEFAULT_PROVIDER_LOOKUP_CONCURRENCY = 5
14
+
15
+ export interface RoutingInit {
16
+ routers: Router[]
17
+ providerLookupConcurrency?: number
18
+ }
19
+
20
+ export interface RoutingComponents {
21
+ logger: ComponentLogger
22
+ metrics?: Metrics
23
+ }
24
+
25
+ interface PeerQueueOptions extends AbortOptions {
26
+ peer: CID
27
+ }
28
+
29
+ export class Routing implements RoutingInterface, Startable {
30
+ public name: string
31
+
32
+ private readonly log: Logger
33
+ private readonly routers: Router[]
34
+ private readonly providerLookupConcurrency: number
35
+
36
+ constructor (components: RoutingComponents, init: RoutingInit) {
37
+ this.name = 'helia'
38
+ this.log = components.logger.forComponent('helia:routing')
39
+ this.routers = init.routers ?? []
40
+ this.providerLookupConcurrency = init.providerLookupConcurrency ?? DEFAULT_PROVIDER_LOOKUP_CONCURRENCY
41
+
42
+ this.findProviders = components.metrics?.traceFunction('helia.routing.findProviders', this.findProviders.bind(this), {
43
+ optionsIndex: 1
44
+ }) ?? this.findProviders
45
+ this.provide = components.metrics?.traceFunction('helia.routing.provide', this.provide.bind(this), {
46
+ optionsIndex: 1
47
+ }) ?? this.provide
48
+ this.cancelReprovide = components.metrics?.traceFunction('helia.routing.cancelReprovide', this.cancelReprovide.bind(this), {
49
+ optionsIndex: 1
50
+ }) ?? this.cancelReprovide
51
+ this.put = components.metrics?.traceFunction('helia.routing.put', this.put.bind(this), {
52
+ optionsIndex: 2
53
+ }) ?? this.put
54
+ this.get = components.metrics?.traceFunction('helia.routing.get', this.get.bind(this), {
55
+ optionsIndex: 1
56
+ }) ?? this.get
57
+ this.findPeer = components.metrics?.traceFunction('helia.routing.findPeer', this.findPeer.bind(this), {
58
+ optionsIndex: 1
59
+ }) ?? this.findPeer
60
+ this.getClosestPeers = components.metrics?.traceFunction('helia.routing.getClosestPeers', this.getClosestPeers.bind(this), {
61
+ optionsIndex: 1
62
+ }) ?? this.getClosestPeers
63
+ }
64
+
65
+ async start (): Promise<void> {
66
+ await start(...this.routers)
67
+ }
68
+
69
+ async stop (): Promise<void> {
70
+ await stop(...this.routers)
71
+ }
72
+
73
+ hasRouter (name: string): boolean {
74
+ return this.routers.findIndex(r => r.name === name) !== -1
75
+ }
76
+
77
+ addRouter (router: Router): void {
78
+ this.routers.push(router)
79
+ }
80
+
81
+ /**
82
+ * Iterates over all content routers in parallel to find providers of the
83
+ * given key
84
+ */
85
+ async * findProviders (key: CID, options: RoutingOptions<RoutingFindProvidersProgressEvents> = {}): AsyncIterable<Provider> {
86
+ if (this.routers.length === 0) {
87
+ throw new NoRoutersAvailableError('No content routers available')
88
+ }
89
+
90
+ // provider multiaddrs are only cached for a limited time, so they can come
91
+ // back as an empty array - when this happens we have to do a FIND_PEER
92
+ // query to get updated addresses, but we shouldn't block on this so use a
93
+ // separate bounded queue to perform this lookup
94
+ const queue = new Queue<Provider | null, PeerQueueOptions>({
95
+ concurrency: this.providerLookupConcurrency
96
+ })
97
+
98
+ let foundProviders = 0
99
+ const errors: Error[] = []
100
+ const self = this
101
+ let routersFinished = 0
102
+
103
+ this.log('findProviders for %c start using routers %s', key, this.routers.map(r => r.toString()).join(', '))
104
+
105
+ const routers = supports(this.routers, 'findProviders')
106
+ .map(async function * (router) {
107
+ let foundProviders = 0
108
+
109
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:find-providers:start', {
110
+ routing: router.name,
111
+ cid: key
112
+ }))
113
+
114
+ try {
115
+ for await (const prov of router.findProviders(key, options)) {
116
+ foundProviders++
117
+
118
+ // @ts-expect-error router.name is a string, needs to be specific
119
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:find-providers:provider', {
120
+ routing: router.name,
121
+ cid: key,
122
+ provider: prov
123
+ }))
124
+
125
+ yield prov
126
+ }
127
+ } catch (err: any) {
128
+ errors.push(err)
129
+ } finally {
130
+ self.log('router %s found %d providers for %c', router, foundProviders, key)
131
+
132
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:find-providers:end', {
133
+ routing: router.name,
134
+ cid: key,
135
+ found: foundProviders
136
+ }))
137
+
138
+ routersFinished++
139
+
140
+ // if all routers have finished and there are no jobs to find updated
141
+ // peer multiaddres running or queued, cause the generator to exit
142
+ if (routersFinished === routers.length && queue.size === 0) {
143
+ queue.emitIdle()
144
+ }
145
+ }
146
+ })
147
+
148
+ for await (const peer of merge(
149
+ queue.toGenerator(),
150
+ ...routers)
151
+ ) {
152
+ // the peer was yielded by a content router without multiaddrs and we
153
+ // failed to load them
154
+ if (peer == null) {
155
+ continue
156
+ }
157
+
158
+ // have to refresh peer info for this peer to get updated multiaddrs
159
+ if (peer.multiaddrs.length === 0) {
160
+ // already looking this peer up
161
+ if (queue.queue.find(job => job.options.peer.equals(peer.id)) != null) {
162
+ continue
163
+ }
164
+
165
+ queue.add(async () => {
166
+ try {
167
+ const provider = await this.findPeer(peer.id, options)
168
+
169
+ if (provider.multiaddrs.length === 0) {
170
+ return null
171
+ }
172
+
173
+ return {
174
+ ...provider,
175
+ protocols: peer.protocols,
176
+ routing: peer.routing
177
+ }
178
+ } catch (err) {
179
+ this.log.error('could not load multiaddrs for peer %p - %e', peer.id, err)
180
+ return null
181
+ }
182
+ }, {
183
+ peer: peer.id,
184
+ signal: options.signal
185
+ })
186
+ .catch(err => {
187
+ this.log.error('could not load multiaddrs for peer %p - %e', peer.id, err)
188
+ })
189
+
190
+ continue
191
+ }
192
+
193
+ foundProviders++
194
+ yield peer
195
+ }
196
+
197
+ this.log('findProviders finished, found %d providers for %c', foundProviders, key)
198
+ }
199
+
200
+ /**
201
+ * Iterates over all content routers in parallel to notify it is
202
+ * a provider of the given key
203
+ */
204
+ async provide (key: CID, options: RoutingOptions<RoutingProvideProgressEvents> = {}): Promise<void> {
205
+ if (this.routers.length === 0) {
206
+ throw new NoRoutersAvailableError('No content routers available')
207
+ }
208
+
209
+ await Promise.all(
210
+ supports(this.routers, 'provide')
211
+ .map(async (router) => {
212
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:provide:start', {
213
+ routing: router.name,
214
+ cid: key
215
+ }))
216
+
217
+ await router.provide(key, options)
218
+
219
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:provide:end', {
220
+ routing: router.name,
221
+ cid: key
222
+ }))
223
+ })
224
+ )
225
+ }
226
+
227
+ async cancelReprovide (key: CID, options: RoutingOptions<RoutingCancelReprovideProgressEvents> = {}): Promise<void> {
228
+ await Promise.all(
229
+ supports(this.routers, 'cancelReprovide')
230
+ .map(async (router) => {
231
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:cancel-reprovide:start', {
232
+ routing: router.name,
233
+ cid: key
234
+ }))
235
+
236
+ await router.cancelReprovide(key, options)
237
+
238
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:cancel-reprovide:end', {
239
+ routing: router.name,
240
+ cid: key
241
+ }))
242
+ })
243
+ )
244
+ }
245
+
246
+ /**
247
+ * Store the given key/value pair in the available content routings
248
+ */
249
+ async put (key: Uint8Array, value: Uint8Array, options?: RoutingOptions<RoutingPutProgressEvents>): Promise<void> {
250
+ await Promise.all(
251
+ supports(this.routers, 'put')
252
+ .map(async (router) => {
253
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:put:start', {
254
+ routing: router.name,
255
+ key,
256
+ value
257
+ }))
258
+
259
+ await router.put(key, value, options)
260
+
261
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:put:end', {
262
+ routing: router.name,
263
+ key,
264
+ value
265
+ }))
266
+ })
267
+ )
268
+ }
269
+
270
+ /**
271
+ * Get the value to the given key. The first value offered by any configured
272
+ * router will be returned.
273
+ */
274
+ async get (key: Uint8Array, options?: RoutingOptions<RoutingGetProgressEvents>): Promise<Uint8Array> {
275
+ const errors: Error[] = []
276
+ let result: Uint8Array | undefined
277
+
278
+ try {
279
+ result = await Promise.any(
280
+ supports(this.routers, 'get')
281
+ .map(async (router) => {
282
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:get:start', {
283
+ routing: router.name,
284
+ key
285
+ }))
286
+
287
+ try {
288
+ return await router.get(key, options)
289
+ } catch (err: any) {
290
+ this.log('router %s failed with %e', router, err)
291
+ errors.push(err)
292
+ } finally {
293
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:get:end', {
294
+ routing: router.name,
295
+ key
296
+ }))
297
+ }
298
+ })
299
+ )
300
+ } catch {
301
+ // ignore AggregateError as we will throw a better-named one
302
+ }
303
+
304
+ if (result == null) {
305
+ throw new GetFailedError(errors, `Failed to get value key ${uint8ArrayToString(key, 'base58btc')}`)
306
+ }
307
+
308
+ return result
309
+ }
310
+
311
+ /**
312
+ * Iterates over all peer routers in parallel to find the given peer
313
+ */
314
+ async findPeer (id: CID, options?: RoutingOptions<RoutingFindPeerProgressEvents>): Promise<Peer> {
315
+ if (this.routers.length === 0) {
316
+ throw new NoRoutersAvailableError('No peer routers available')
317
+ }
318
+
319
+ const self = this
320
+ const source = merge(
321
+ ...supports(this.routers, 'findPeer')
322
+ .map(router => (async function * () {
323
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:find-peer:start', {
324
+ routing: router.name,
325
+ peerId: id
326
+ }))
327
+
328
+ try {
329
+ yield await router.findPeer(id, options)
330
+ } catch (err) {
331
+ self.log.error(err)
332
+ } finally {
333
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:find-peer:end', {
334
+ routing: router.name,
335
+ peerId: id
336
+ }))
337
+ }
338
+ })())
339
+ )
340
+
341
+ for await (const peer of source) {
342
+ if (peer == null) {
343
+ continue
344
+ }
345
+
346
+ return peer
347
+ }
348
+
349
+ throw new NotFoundError('Could not find peer in routing')
350
+ }
351
+
352
+ /**
353
+ * Attempt to find the closest peers on the network to the given key
354
+ */
355
+ async * getClosestPeers (key: Uint8Array, options: RoutingOptions<RoutingGetClosestPeersProgressEvents> = {}): AsyncIterable<Peer> {
356
+ if (this.routers.length === 0) {
357
+ throw new NoRoutersAvailableError('No peer routers available')
358
+ }
359
+
360
+ for await (const peer of merge(
361
+ ...supports(this.routers, 'getClosestPeers')
362
+ .map(async function * (router) {
363
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:get-closest-peers:start', {
364
+ routing: router.name,
365
+ key
366
+ }))
367
+
368
+ try {
369
+ yield * router.getClosestPeers(key, options)
370
+ } finally {
371
+ options?.onProgress?.(new CustomProgressEvent('helia:routing:get-closest-peers:end', {
372
+ routing: router.name,
373
+ key
374
+ }))
375
+ }
376
+ })
377
+ )) {
378
+ if (peer == null) {
379
+ continue
380
+ }
381
+
382
+ yield peer
383
+ }
384
+ }
385
+ }
386
+
387
+ function supports <Operation extends keyof Routing> (routers: any[], key: Operation): Array<Pick<Routing, Operation | 'name'>> {
388
+ return routers.filter(router => router[key] != null)
389
+ }