@helia/utils 0.0.0-031519c

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 (44) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +66 -0
  3. package/dist/index.min.js +3 -0
  4. package/dist/src/index.d.ts +100 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/src/index.js +115 -0
  7. package/dist/src/index.js.map +1 -0
  8. package/dist/src/pins.d.ts +17 -0
  9. package/dist/src/pins.d.ts.map +1 -0
  10. package/dist/src/pins.js +155 -0
  11. package/dist/src/pins.js.map +1 -0
  12. package/dist/src/routing.d.ts +43 -0
  13. package/dist/src/routing.d.ts.map +1 -0
  14. package/dist/src/routing.js +122 -0
  15. package/dist/src/routing.js.map +1 -0
  16. package/dist/src/storage.d.ts +63 -0
  17. package/dist/src/storage.d.ts.map +1 -0
  18. package/dist/src/storage.js +140 -0
  19. package/dist/src/storage.js.map +1 -0
  20. package/dist/src/utils/dag-walkers.d.ts +28 -0
  21. package/dist/src/utils/dag-walkers.d.ts.map +1 -0
  22. package/dist/src/utils/dag-walkers.js +171 -0
  23. package/dist/src/utils/dag-walkers.js.map +1 -0
  24. package/dist/src/utils/datastore-version.d.ts +3 -0
  25. package/dist/src/utils/datastore-version.d.ts.map +1 -0
  26. package/dist/src/utils/datastore-version.js +19 -0
  27. package/dist/src/utils/datastore-version.js.map +1 -0
  28. package/dist/src/utils/default-hashers.d.ts +3 -0
  29. package/dist/src/utils/default-hashers.d.ts.map +1 -0
  30. package/dist/src/utils/default-hashers.js +15 -0
  31. package/dist/src/utils/default-hashers.js.map +1 -0
  32. package/dist/src/utils/networked-storage.d.ts +67 -0
  33. package/dist/src/utils/networked-storage.d.ts.map +1 -0
  34. package/dist/src/utils/networked-storage.js +206 -0
  35. package/dist/src/utils/networked-storage.js.map +1 -0
  36. package/package.json +91 -0
  37. package/src/index.ts +225 -0
  38. package/src/pins.ts +227 -0
  39. package/src/routing.ts +169 -0
  40. package/src/storage.ts +172 -0
  41. package/src/utils/dag-walkers.ts +198 -0
  42. package/src/utils/datastore-version.ts +23 -0
  43. package/src/utils/default-hashers.ts +18 -0
  44. package/src/utils/networked-storage.ts +261 -0
package/src/index.ts ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * Exports a `Helia` class that implements the {@link HeliaInterface} API.
5
+ *
6
+ * In general you should use the `helia` or `@helia/http` modules instead which
7
+ * pre-configure Helia for certain use-cases (p2p or pure-HTTP).
8
+ *
9
+ * @example
10
+ *
11
+ * ```typescript
12
+ * import { Helia } from '@helia/utils'
13
+ *
14
+ * const node = new Helia({
15
+ * // ...options
16
+ * })
17
+ * ```
18
+ */
19
+
20
+ import { contentRoutingSymbol, peerRoutingSymbol, start, stop } from '@libp2p/interface'
21
+ import { defaultLogger } from '@libp2p/logger'
22
+ import drain from 'it-drain'
23
+ import { CustomProgressEvent } from 'progress-events'
24
+ import { PinsImpl } from './pins.js'
25
+ import { Routing as RoutingClass } from './routing.js'
26
+ import { BlockStorage } from './storage.js'
27
+ import { defaultDagWalkers } from './utils/dag-walkers.js'
28
+ import { assertDatastoreVersionIsCurrent } from './utils/datastore-version.js'
29
+ import { defaultHashers } from './utils/default-hashers.js'
30
+ import { NetworkedStorage } from './utils/networked-storage.js'
31
+ import type { DAGWalker, GCOptions, Helia as HeliaInterface, Routing } from '@helia/interface'
32
+ import type { BlockBroker } from '@helia/interface/blocks'
33
+ import type { Pins } from '@helia/interface/pins'
34
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
35
+ import type { Blockstore } from 'interface-blockstore'
36
+ import type { Datastore } from 'interface-datastore'
37
+ import type { CID } from 'multiformats/cid'
38
+ import type { MultihashHasher } from 'multiformats/hashes/interface'
39
+
40
+ /**
41
+ * Options used to create a Helia node.
42
+ */
43
+ export interface HeliaInit {
44
+ /**
45
+ * The blockstore is where blocks are stored
46
+ */
47
+ blockstore: Blockstore
48
+
49
+ /**
50
+ * The datastore is where data is stored
51
+ */
52
+ datastore: Datastore
53
+
54
+ /**
55
+ * By default sha256, sha512 and identity hashes are supported for
56
+ * bitswap operations. To bitswap blocks with CIDs using other hashes
57
+ * pass appropriate MultihashHashers here.
58
+ */
59
+ hashers?: MultihashHasher[]
60
+
61
+ /**
62
+ * In order to pin CIDs that correspond to a DAG, it's necessary to know
63
+ * how to traverse that DAG. DAGWalkers take a block and yield any CIDs
64
+ * encoded within that block.
65
+ */
66
+ dagWalkers?: DAGWalker[]
67
+
68
+ /**
69
+ * A list of strategies used to fetch blocks when they are not present in
70
+ * the local blockstore
71
+ */
72
+ blockBrokers: Array<(components: any) => BlockBroker>
73
+
74
+ /**
75
+ * Garbage collection requires preventing blockstore writes during searches
76
+ * for unpinned blocks as DAGs are typically pinned after they've been
77
+ * imported - without locking this could lead to the deletion of blocks while
78
+ * they are being added to the blockstore.
79
+ *
80
+ * By default this lock is held on the current process and other processes
81
+ * will contact this process for access.
82
+ *
83
+ * If Helia is being run in multiple processes, one process must hold the GC
84
+ * lock so use this option to control which process that is.
85
+ *
86
+ * @default true
87
+ */
88
+ holdGcLock?: boolean
89
+
90
+ /**
91
+ * An optional logging component to pass to libp2p. If not specified the
92
+ * default implementation from libp2p will be used.
93
+ */
94
+ logger?: ComponentLogger
95
+
96
+ /**
97
+ * Routers perform operations such as looking up content providers,
98
+ * information about network peers or getting/putting records.
99
+ */
100
+ routers?: Array<Partial<Routing>>
101
+
102
+ /**
103
+ * Components used by subclasses
104
+ */
105
+ components?: Record<string, any>
106
+ }
107
+
108
+ interface Components {
109
+ blockstore: Blockstore
110
+ datastore: Datastore
111
+ hashers: Record<number, MultihashHasher>
112
+ dagWalkers: Record<number, DAGWalker>
113
+ logger: ComponentLogger
114
+ blockBrokers: BlockBroker[]
115
+ }
116
+
117
+ export class Helia implements HeliaInterface {
118
+ public blockstore: BlockStorage
119
+ public datastore: Datastore
120
+ public pins: Pins
121
+ public logger: ComponentLogger
122
+ public routing: Routing
123
+ public dagWalkers: Record<number, DAGWalker>
124
+ public hashers: Record<number, MultihashHasher>
125
+ private readonly log: Logger
126
+
127
+ constructor (init: HeliaInit) {
128
+ this.logger = init.logger ?? defaultLogger()
129
+ this.log = this.logger.forComponent('helia')
130
+ this.hashers = defaultHashers(init.hashers)
131
+ this.dagWalkers = defaultDagWalkers(init.dagWalkers)
132
+
133
+ const components: Components = {
134
+ blockstore: init.blockstore,
135
+ datastore: init.datastore,
136
+ hashers: this.hashers,
137
+ dagWalkers: this.dagWalkers,
138
+ logger: this.logger,
139
+ blockBrokers: [],
140
+ ...(init.components ?? {})
141
+ }
142
+
143
+ components.blockBrokers = init.blockBrokers.map((fn) => {
144
+ return fn(components)
145
+ })
146
+
147
+ const networkedStorage = new NetworkedStorage(components)
148
+
149
+ this.pins = new PinsImpl(init.datastore, networkedStorage, this.dagWalkers)
150
+
151
+ this.blockstore = new BlockStorage(networkedStorage, this.pins, {
152
+ holdGcLock: init.holdGcLock ?? true
153
+ })
154
+ this.datastore = init.datastore
155
+ this.routing = new RoutingClass(components, {
156
+ routers: (init.routers ?? []).flatMap((router: any) => {
157
+ // if the router itself is a router
158
+ const routers = [
159
+ router
160
+ ]
161
+
162
+ // if the router provides a libp2p-style ContentRouter
163
+ if (router[contentRoutingSymbol] != null) {
164
+ routers.push(router[contentRoutingSymbol])
165
+ }
166
+
167
+ // if the router provides a libp2p-style PeerRouter
168
+ if (router[peerRoutingSymbol] != null) {
169
+ routers.push(router[peerRoutingSymbol])
170
+ }
171
+
172
+ return routers
173
+ })
174
+ })
175
+ }
176
+
177
+ async start (): Promise<void> {
178
+ await assertDatastoreVersionIsCurrent(this.datastore)
179
+ await start(
180
+ this.blockstore,
181
+ this.datastore,
182
+ this.routing
183
+ )
184
+ }
185
+
186
+ async stop (): Promise<void> {
187
+ await stop(
188
+ this.blockstore,
189
+ this.datastore,
190
+ this.routing
191
+ )
192
+ }
193
+
194
+ async gc (options: GCOptions = {}): Promise<void> {
195
+ const releaseLock = await this.blockstore.lock.writeLock()
196
+
197
+ try {
198
+ const helia = this
199
+ const blockstore = this.blockstore.unwrap()
200
+
201
+ this.log('gc start')
202
+
203
+ await drain(blockstore.deleteMany((async function * (): AsyncGenerator<CID> {
204
+ for await (const { cid } of blockstore.getAll()) {
205
+ try {
206
+ if (await helia.pins.isPinned(cid, options)) {
207
+ continue
208
+ }
209
+
210
+ yield cid
211
+
212
+ options.onProgress?.(new CustomProgressEvent<CID>('helia:gc:deleted', cid))
213
+ } catch (err) {
214
+ helia.log.error('Error during gc', err)
215
+ options.onProgress?.(new CustomProgressEvent<Error>('helia:gc:error', err))
216
+ }
217
+ }
218
+ }())))
219
+ } finally {
220
+ releaseLock()
221
+ }
222
+
223
+ this.log('gc finished')
224
+ }
225
+ }
package/src/pins.ts ADDED
@@ -0,0 +1,227 @@
1
+ import { Queue } from '@libp2p/utils/queue'
2
+ import * as cborg from 'cborg'
3
+ import { type Datastore, Key } from 'interface-datastore'
4
+ import { base36 } from 'multiformats/bases/base36'
5
+ import { CID, type Version } from 'multiformats/cid'
6
+ import { CustomProgressEvent, type ProgressOptions } from 'progress-events'
7
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
8
+ import type { DAGWalker } from '@helia/interface'
9
+ import type { GetBlockProgressEvents } from '@helia/interface/blocks'
10
+ import type { AddOptions, AddPinEvents, IsPinnedOptions, LsOptions, Pin, Pins, RmOptions } from '@helia/interface/pins'
11
+ import type { AbortOptions } from '@libp2p/interface'
12
+ import type { Blockstore } from 'interface-blockstore'
13
+
14
+ interface DatastorePin {
15
+ /**
16
+ * 0 for a direct pin or an arbitrary (+ve, whole) number or Infinity
17
+ */
18
+ depth: number
19
+
20
+ /**
21
+ * User-specific metadata for the pin
22
+ */
23
+ metadata: Record<string, string | number | boolean>
24
+ }
25
+
26
+ interface DatastorePinnedBlock {
27
+ pinCount: number
28
+ pinnedBy: Uint8Array[]
29
+ }
30
+
31
+ /**
32
+ * Callback for updating a {@link DatastorePinnedBlock}'s properties when
33
+ * calling `#updatePinnedBlock`
34
+ *
35
+ * The callback should return `false` to prevent any pinning modifications to
36
+ * the block, and true in all other cases.
37
+ */
38
+ interface WithPinnedBlockCallback {
39
+ (pinnedBlock: DatastorePinnedBlock): boolean
40
+ }
41
+
42
+ const DATASTORE_PIN_PREFIX = '/pin/'
43
+ const DATASTORE_BLOCK_PREFIX = '/pinned-block/'
44
+ const DATASTORE_ENCODING = base36
45
+ const DAG_WALK_QUEUE_CONCURRENCY = 1
46
+
47
+ interface WalkDagOptions extends AbortOptions, ProgressOptions<GetBlockProgressEvents | AddPinEvents> {
48
+ depth: number
49
+ }
50
+
51
+ function toDSKey (cid: CID): Key {
52
+ if (cid.version === 0) {
53
+ cid = cid.toV1()
54
+ }
55
+
56
+ return new Key(`${DATASTORE_PIN_PREFIX}${cid.toString(DATASTORE_ENCODING)}`)
57
+ }
58
+
59
+ export class PinsImpl implements Pins {
60
+ private readonly datastore: Datastore
61
+ private readonly blockstore: Blockstore
62
+ private readonly dagWalkers: Record<number, DAGWalker>
63
+
64
+ constructor (datastore: Datastore, blockstore: Blockstore, dagWalkers: Record<number, DAGWalker>) {
65
+ this.datastore = datastore
66
+ this.blockstore = blockstore
67
+ this.dagWalkers = dagWalkers
68
+ }
69
+
70
+ async * add (cid: CID<unknown, number, number, Version>, options: AddOptions = {}): AsyncGenerator<CID, void, undefined> {
71
+ const pinKey = toDSKey(cid)
72
+
73
+ if (await this.datastore.has(pinKey)) {
74
+ throw new Error('Already pinned')
75
+ }
76
+
77
+ const depth = Math.round(options.depth ?? Infinity)
78
+
79
+ if (depth < 0) {
80
+ throw new Error('Depth must be greater than or equal to 0')
81
+ }
82
+
83
+ // use a queue to walk the DAG instead of recursion so we can traverse very large DAGs
84
+ const queue = new Queue<AsyncGenerator<CID>>({
85
+ concurrency: DAG_WALK_QUEUE_CONCURRENCY
86
+ })
87
+
88
+ for await (const childCid of this.#walkDag(cid, queue, {
89
+ ...options,
90
+ depth
91
+ })) {
92
+ await this.#updatePinnedBlock(childCid, (pinnedBlock: DatastorePinnedBlock) => {
93
+ // do not update pinned block if this block is already pinned by this CID
94
+ if (pinnedBlock.pinnedBy.find(c => uint8ArrayEquals(c, cid.bytes)) != null) {
95
+ return false
96
+ }
97
+
98
+ pinnedBlock.pinCount++
99
+ pinnedBlock.pinnedBy.push(cid.bytes)
100
+ return true
101
+ }, options)
102
+
103
+ yield childCid
104
+ }
105
+
106
+ const pin: DatastorePin = {
107
+ depth,
108
+ metadata: options.metadata ?? {}
109
+ }
110
+
111
+ await this.datastore.put(pinKey, cborg.encode(pin), options)
112
+ }
113
+
114
+ /**
115
+ * Walk a DAG in an iterable fashion
116
+ */
117
+ async * #walkDag (cid: CID, queue: Queue<AsyncGenerator<CID>>, options: WalkDagOptions): AsyncGenerator<CID> {
118
+ if (options.depth === -1) {
119
+ return
120
+ }
121
+
122
+ const dagWalker = this.dagWalkers[cid.code]
123
+
124
+ if (dagWalker == null) {
125
+ throw new Error(`No dag walker found for cid codec ${cid.code}`)
126
+ }
127
+
128
+ const block = await this.blockstore.get(cid, options)
129
+
130
+ yield cid
131
+
132
+ // walk dag, ensure all blocks are present
133
+ for await (const cid of dagWalker.walk(block)) {
134
+ yield * await queue.add(async () => {
135
+ return this.#walkDag(cid, queue, {
136
+ ...options,
137
+ depth: options.depth - 1
138
+ })
139
+ })
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Update the pin count for the CID
145
+ */
146
+ async #updatePinnedBlock (cid: CID, withPinnedBlock: WithPinnedBlockCallback, options: AddOptions): Promise<void> {
147
+ const blockKey = new Key(`${DATASTORE_BLOCK_PREFIX}${DATASTORE_ENCODING.encode(cid.multihash.bytes)}`)
148
+
149
+ let pinnedBlock: DatastorePinnedBlock = {
150
+ pinCount: 0,
151
+ pinnedBy: []
152
+ }
153
+
154
+ try {
155
+ pinnedBlock = cborg.decode(await this.datastore.get(blockKey, options))
156
+ } catch (err: any) {
157
+ if (err.code !== 'ERR_NOT_FOUND') {
158
+ throw err
159
+ }
160
+ }
161
+
162
+ const shouldContinue = withPinnedBlock(pinnedBlock)
163
+
164
+ if (!shouldContinue) {
165
+ return
166
+ }
167
+
168
+ if (pinnedBlock.pinCount === 0) {
169
+ if (await this.datastore.has(blockKey)) {
170
+ await this.datastore.delete(blockKey)
171
+ return
172
+ }
173
+ }
174
+
175
+ await this.datastore.put(blockKey, cborg.encode(pinnedBlock), options)
176
+ options.onProgress?.(new CustomProgressEvent<CID>('helia:pin:add', cid))
177
+ }
178
+
179
+ async * rm (cid: CID<unknown, number, number, Version>, options: RmOptions = {}): AsyncGenerator<CID, void, undefined> {
180
+ const pinKey = toDSKey(cid)
181
+ const buf = await this.datastore.get(pinKey, options)
182
+ const pin = cborg.decode(buf)
183
+
184
+ await this.datastore.delete(pinKey, options)
185
+
186
+ // use a queue to walk the DAG instead of recursion so we can traverse very large DAGs
187
+ const queue = new Queue<AsyncGenerator<CID>>({
188
+ concurrency: DAG_WALK_QUEUE_CONCURRENCY
189
+ })
190
+
191
+ for await (const childCid of this.#walkDag(cid, queue, {
192
+ ...options,
193
+ depth: pin.depth
194
+ })) {
195
+ await this.#updatePinnedBlock(childCid, (pinnedBlock): boolean => {
196
+ pinnedBlock.pinCount--
197
+ pinnedBlock.pinnedBy = pinnedBlock.pinnedBy.filter(c => uint8ArrayEquals(c, cid.bytes))
198
+ return true
199
+ }, {
200
+ ...options,
201
+ depth: pin.depth
202
+ })
203
+
204
+ yield childCid
205
+ }
206
+ }
207
+
208
+ async * ls (options: LsOptions = {}): AsyncGenerator<Pin, void, undefined> {
209
+ for await (const { key, value } of this.datastore.query({
210
+ prefix: DATASTORE_PIN_PREFIX + (options.cid != null ? `${options.cid.toString(base36)}` : '')
211
+ }, options)) {
212
+ const cid = CID.parse(key.toString().substring(5), base36)
213
+ const pin = cborg.decode(value)
214
+
215
+ yield {
216
+ cid,
217
+ ...pin
218
+ }
219
+ }
220
+ }
221
+
222
+ async isPinned (cid: CID, options: IsPinnedOptions = {}): Promise<boolean> {
223
+ const blockKey = new Key(`${DATASTORE_BLOCK_PREFIX}${DATASTORE_ENCODING.encode(cid.multihash.bytes)}`)
224
+
225
+ return this.datastore.has(blockKey, options)
226
+ }
227
+ }
package/src/routing.ts ADDED
@@ -0,0 +1,169 @@
1
+ import { CodeError, start, stop } from '@libp2p/interface'
2
+ import { PeerSet } from '@libp2p/peer-collections'
3
+ import merge from 'it-merge'
4
+ import type { Routing as RoutingInterface, Provider, RoutingOptions } from '@helia/interface'
5
+ import type { AbortOptions, ComponentLogger, Logger, PeerId, PeerInfo, Startable } from '@libp2p/interface'
6
+ import type { CID } from 'multiformats/cid'
7
+
8
+ export interface RoutingInit {
9
+ routers: Array<Partial<RoutingInterface>>
10
+ }
11
+
12
+ export interface RoutingComponents {
13
+ logger: ComponentLogger
14
+ }
15
+
16
+ export class Routing implements RoutingInterface, Startable {
17
+ private readonly log: Logger
18
+ private readonly routers: Array<Partial<RoutingInterface>>
19
+
20
+ constructor (components: RoutingComponents, init: RoutingInit) {
21
+ this.log = components.logger.forComponent('helia:routing')
22
+ this.routers = init.routers ?? []
23
+ }
24
+
25
+ async start (): Promise<void> {
26
+ await start(...this.routers)
27
+ }
28
+
29
+ async stop (): Promise<void> {
30
+ await stop(...this.routers)
31
+ }
32
+
33
+ /**
34
+ * Iterates over all content routers in parallel to find providers of the given key
35
+ */
36
+ async * findProviders (key: CID, options: RoutingOptions = {}): AsyncIterable<Provider> {
37
+ if (this.routers.length === 0) {
38
+ throw new CodeError('No content routers available', 'ERR_NO_ROUTERS_AVAILABLE')
39
+ }
40
+
41
+ const seen = new PeerSet()
42
+
43
+ for await (const peer of merge(
44
+ ...supports(this.routers, 'findProviders')
45
+ .map(router => router.findProviders(key, options))
46
+ )) {
47
+ // the peer was yielded by a content router without multiaddrs and we
48
+ // failed to load them
49
+ if (peer == null) {
50
+ continue
51
+ }
52
+
53
+ // deduplicate peers
54
+ if (seen.has(peer.id)) {
55
+ continue
56
+ }
57
+
58
+ seen.add(peer.id)
59
+
60
+ yield peer
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Iterates over all content routers in parallel to notify it is
66
+ * a provider of the given key
67
+ */
68
+ async provide (key: CID, options: AbortOptions = {}): Promise<void> {
69
+ if (this.routers.length === 0) {
70
+ throw new CodeError('No content routers available', 'ERR_NO_ROUTERS_AVAILABLE')
71
+ }
72
+
73
+ await Promise.all(
74
+ supports(this.routers, 'provide')
75
+ .map(async (router) => {
76
+ await router.provide(key, options)
77
+ })
78
+ )
79
+ }
80
+
81
+ /**
82
+ * Store the given key/value pair in the available content routings
83
+ */
84
+ async put (key: Uint8Array, value: Uint8Array, options?: AbortOptions): Promise<void> {
85
+ await Promise.all(
86
+ supports(this.routers, 'put')
87
+ .map(async (router) => {
88
+ await router.put(key, value, options)
89
+ })
90
+ )
91
+ }
92
+
93
+ /**
94
+ * Get the value to the given key.
95
+ * Times out after 1 minute by default.
96
+ */
97
+ async get (key: Uint8Array, options?: AbortOptions): Promise<Uint8Array> {
98
+ return Promise.any(
99
+ supports(this.routers, 'get')
100
+ .map(async (router) => {
101
+ return router.get(key, options)
102
+ })
103
+ )
104
+ }
105
+
106
+ /**
107
+ * Iterates over all peer routers in parallel to find the given peer
108
+ */
109
+ async findPeer (id: PeerId, options?: RoutingOptions): Promise<PeerInfo> {
110
+ if (this.routers.length === 0) {
111
+ throw new CodeError('No peer routers available', 'ERR_NO_ROUTERS_AVAILABLE')
112
+ }
113
+
114
+ const self = this
115
+ const source = merge(
116
+ ...supports(this.routers, 'findPeer')
117
+ .map(router => (async function * () {
118
+ try {
119
+ yield await router.findPeer(id, options)
120
+ } catch (err) {
121
+ self.log.error(err)
122
+ }
123
+ })())
124
+ )
125
+
126
+ for await (const peer of source) {
127
+ if (peer == null) {
128
+ continue
129
+ }
130
+
131
+ return peer
132
+ }
133
+
134
+ throw new CodeError('Could not find peer in routing', 'ERR_NOT_FOUND')
135
+ }
136
+
137
+ /**
138
+ * Attempt to find the closest peers on the network to the given key
139
+ */
140
+ async * getClosestPeers (key: Uint8Array, options: RoutingOptions = {}): AsyncIterable<PeerInfo> {
141
+ if (this.routers.length === 0) {
142
+ throw new CodeError('No peer routers available', 'ERR_NO_ROUTERS_AVAILABLE')
143
+ }
144
+
145
+ const seen = new PeerSet()
146
+
147
+ for await (const peer of merge(
148
+ ...supports(this.routers, 'getClosestPeers')
149
+ .map(router => router.getClosestPeers(key, options))
150
+ )) {
151
+ if (peer == null) {
152
+ continue
153
+ }
154
+
155
+ // deduplicate peers
156
+ if (seen.has(peer.id)) {
157
+ continue
158
+ }
159
+
160
+ seen.add(peer.id)
161
+
162
+ yield peer
163
+ }
164
+ }
165
+ }
166
+
167
+ function supports <Operation extends keyof Routing> (routers: any[], key: Operation): Array<Pick<Routing, Operation>> {
168
+ return routers.filter(router => router[key] != null)
169
+ }