@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.
- package/LICENSE +4 -0
- package/README.md +66 -0
- package/dist/index.min.js +3 -0
- package/dist/src/index.d.ts +100 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +115 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/pins.d.ts +17 -0
- package/dist/src/pins.d.ts.map +1 -0
- package/dist/src/pins.js +155 -0
- package/dist/src/pins.js.map +1 -0
- package/dist/src/routing.d.ts +43 -0
- package/dist/src/routing.d.ts.map +1 -0
- package/dist/src/routing.js +122 -0
- package/dist/src/routing.js.map +1 -0
- package/dist/src/storage.d.ts +63 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +140 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/utils/dag-walkers.d.ts +28 -0
- package/dist/src/utils/dag-walkers.d.ts.map +1 -0
- package/dist/src/utils/dag-walkers.js +171 -0
- package/dist/src/utils/dag-walkers.js.map +1 -0
- package/dist/src/utils/datastore-version.d.ts +3 -0
- package/dist/src/utils/datastore-version.d.ts.map +1 -0
- package/dist/src/utils/datastore-version.js +19 -0
- package/dist/src/utils/datastore-version.js.map +1 -0
- package/dist/src/utils/default-hashers.d.ts +3 -0
- package/dist/src/utils/default-hashers.d.ts.map +1 -0
- package/dist/src/utils/default-hashers.js +15 -0
- package/dist/src/utils/default-hashers.js.map +1 -0
- package/dist/src/utils/networked-storage.d.ts +67 -0
- package/dist/src/utils/networked-storage.d.ts.map +1 -0
- package/dist/src/utils/networked-storage.js +206 -0
- package/dist/src/utils/networked-storage.js.map +1 -0
- package/package.json +91 -0
- package/src/index.ts +225 -0
- package/src/pins.ts +227 -0
- package/src/routing.ts +169 -0
- package/src/storage.ts +172 -0
- package/src/utils/dag-walkers.ts +198 -0
- package/src/utils/datastore-version.ts +23 -0
- package/src/utils/default-hashers.ts +18 -0
- 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
|
+
}
|