helia 0.0.0-031ca73

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/src/helia.ts ADDED
@@ -0,0 +1,117 @@
1
+ import type { GCOptions, Helia } from '@helia/interface'
2
+ import type { Libp2p } from '@libp2p/interface-libp2p'
3
+ import type { Datastore } from 'interface-datastore'
4
+ import { identity } from 'multiformats/hashes/identity'
5
+ import { sha256, sha512 } from 'multiformats/hashes/sha2'
6
+ import type { MultihashHasher } from 'multiformats/hashes/interface'
7
+ import type { HeliaInit } from '.'
8
+ import { Bitswap, createBitswap } from 'ipfs-bitswap'
9
+ import { BlockStorage } from './storage.js'
10
+ import type { Pins } from '@helia/interface/pins'
11
+ import { PinsImpl } from './pins.js'
12
+ import { assertDatastoreVersionIsCurrent } from './utils/datastore-version.js'
13
+ import drain from 'it-drain'
14
+ import { CustomProgressEvent } from 'progress-events'
15
+ import { MemoryDatastore } from 'datastore-core'
16
+ import { MemoryBlockstore } from 'blockstore-core'
17
+
18
+ export class HeliaImpl implements Helia {
19
+ public libp2p: Libp2p
20
+ public blockstore: BlockStorage
21
+ public datastore: Datastore
22
+ public pins: Pins
23
+
24
+ #bitswap?: Bitswap
25
+
26
+ constructor (init: HeliaInit) {
27
+ const hashers: MultihashHasher[] = [
28
+ sha256,
29
+ sha512,
30
+ identity,
31
+ ...(init.hashers ?? [])
32
+ ]
33
+
34
+ const datastore = init.datastore ?? new MemoryDatastore()
35
+ const blockstore = init.blockstore ?? new MemoryBlockstore()
36
+
37
+ // @ts-expect-error incomplete libp2p implementation
38
+ const libp2p = init.libp2p ?? new Proxy<Libp2p>({}, {
39
+ get (_, prop) {
40
+ const noop = (): void => {}
41
+ const noops = ['start', 'stop']
42
+
43
+ if (noops.includes(prop.toString())) {
44
+ return noop
45
+ }
46
+
47
+ if (prop === 'isProxy') {
48
+ return true
49
+ }
50
+
51
+ throw new Error('Please configure Helia with a libp2p instance')
52
+ },
53
+ set () {
54
+ throw new Error('Please configure Helia with a libp2p instance')
55
+ }
56
+ })
57
+
58
+ this.pins = new PinsImpl(datastore, blockstore, init.dagWalkers ?? [])
59
+
60
+ if (init.libp2p != null) {
61
+ this.#bitswap = createBitswap(libp2p, blockstore, {
62
+ hashLoader: {
63
+ getHasher: async (codecOrName: string | number) => {
64
+ const hasher = hashers.find(hasher => {
65
+ return hasher.code === codecOrName || hasher.name === codecOrName
66
+ })
67
+
68
+ if (hasher != null) {
69
+ return await Promise.resolve(hasher)
70
+ }
71
+
72
+ throw new Error(`Could not load hasher for code/name "${codecOrName}"`)
73
+ }
74
+ }
75
+ })
76
+ }
77
+
78
+ this.libp2p = libp2p
79
+ this.blockstore = new BlockStorage(blockstore, this.pins, this.#bitswap)
80
+ this.datastore = datastore
81
+ }
82
+
83
+ async start (): Promise<void> {
84
+ await assertDatastoreVersionIsCurrent(this.datastore)
85
+
86
+ this.#bitswap?.start()
87
+ await this.libp2p.start()
88
+ }
89
+
90
+ async stop (): Promise<void> {
91
+ this.#bitswap?.stop()
92
+ await this.libp2p.stop()
93
+ }
94
+
95
+ async gc (options: GCOptions = {}): Promise<void> {
96
+ const releaseLock = await this.blockstore.lock.writeLock()
97
+
98
+ try {
99
+ const helia = this
100
+ const blockstore = this.blockstore.unwrap()
101
+
102
+ await drain(blockstore.deleteMany((async function * () {
103
+ for await (const cid of blockstore.queryKeys({})) {
104
+ if (await helia.pins.isPinned(cid, options)) {
105
+ continue
106
+ }
107
+
108
+ yield cid
109
+
110
+ options.onProgress?.(new CustomProgressEvent('helia:gc:deleted', cid))
111
+ }
112
+ }())))
113
+ } finally {
114
+ releaseLock()
115
+ }
116
+ }
117
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * Create a Helia node.
5
+ *
6
+ * @example
7
+ *
8
+ * ```typescript
9
+ * import { createLibp2p } from 'libp2p'
10
+ * import { MemoryDatastore } from 'datastore-core'
11
+ * import { MemoryBlockstore } from 'blockstore-core'
12
+ * import { createHelia } from 'helia'
13
+ * import { unixfs } from '@helia/unixfs'
14
+ * import { CID } from 'multiformats/cid'
15
+ *
16
+ * const node = await createHelia({
17
+ * blockstore: new MemoryBlockstore(),
18
+ * datastore: new MemoryDatastore(),
19
+ * libp2p: await createLibp2p({
20
+ * //... libp2p options
21
+ * })
22
+ * })
23
+ * const fs = unixfs(node)
24
+ * fs.cat(CID.parse('bafyFoo'))
25
+ * ```
26
+ */
27
+
28
+ import type { Helia } from '@helia/interface'
29
+ import type { Libp2p } from '@libp2p/interface-libp2p'
30
+ import type { Blockstore } from 'interface-blockstore'
31
+ import type { Datastore } from 'interface-datastore'
32
+ import type { CID } from 'multiformats/cid'
33
+ import type { MultihashHasher } from 'multiformats/hashes/interface'
34
+ import { HeliaImpl } from './helia.js'
35
+
36
+ /**
37
+ * DAGWalkers take a block and yield CIDs encoded in that block
38
+ */
39
+ export interface DAGWalker {
40
+ codec: number
41
+ walk: (block: Uint8Array) => AsyncGenerator<CID, void, undefined>
42
+ }
43
+
44
+ /**
45
+ * Options used to create a Helia node.
46
+ */
47
+ export interface HeliaInit {
48
+ /**
49
+ * A libp2p node is required to perform network operations
50
+ */
51
+ libp2p?: Libp2p
52
+
53
+ /**
54
+ * The blockstore is where blocks are stored
55
+ */
56
+ blockstore?: Blockstore
57
+
58
+ /**
59
+ * The datastore is where data is stored
60
+ */
61
+ datastore?: Datastore
62
+
63
+ /**
64
+ * By default sha256, sha512 and identity hashes are supported for
65
+ * bitswap operations. To bitswap blocks with CIDs using other hashes
66
+ * pass appropriate MultihashHashers here.
67
+ */
68
+ hashers?: MultihashHasher[]
69
+
70
+ /**
71
+ * In order to pin CIDs that correspond to a DAG, it's necessary to know
72
+ * how to traverse that DAG. DAGWalkers take a block and yield any CIDs
73
+ * encoded within that block.
74
+ */
75
+ dagWalkers?: DAGWalker[]
76
+
77
+ /**
78
+ * Pass `false` to not start the helia node
79
+ */
80
+ start?: boolean
81
+ }
82
+
83
+ /**
84
+ * Create and return a Helia node
85
+ */
86
+ export async function createHelia (init: HeliaInit = {}): Promise<Helia> {
87
+ const helia = new HeliaImpl(init)
88
+
89
+ if (init.start !== false) {
90
+ await helia.start()
91
+ }
92
+
93
+ return helia
94
+ }
package/src/pins.ts ADDED
@@ -0,0 +1,238 @@
1
+ import type { AddOptions, IsPinnedOptions, LsOptions, Pin, Pins, RmOptions } from '@helia/interface/pins'
2
+ import { Datastore, Key } from 'interface-datastore'
3
+ import { CID, Version } from 'multiformats/cid'
4
+ import * as cborg from 'cborg'
5
+ import { base36 } from 'multiformats/bases/base36'
6
+ import type { Blockstore } from 'interface-blockstore'
7
+ import PQueue from 'p-queue'
8
+ import type { AbortOptions } from '@libp2p/interfaces'
9
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
10
+ import defer from 'p-defer'
11
+ import type { DAGWalker } from './index.js'
12
+ import { cborWalker, dagPbWalker, jsonWalker, rawWalker } from './utils/dag-walkers.js'
13
+
14
+ const DEFAULT_DAG_WALKERS = [
15
+ rawWalker,
16
+ dagPbWalker,
17
+ cborWalker,
18
+ jsonWalker
19
+ ]
20
+
21
+ interface DatastorePin {
22
+ /**
23
+ * 0 for a direct pin or an arbitrary (+ve, whole) number or Infinity
24
+ */
25
+ depth: number
26
+
27
+ /**
28
+ * User-specific metadata for the pin
29
+ */
30
+ metadata: Record<string, string | number | boolean>
31
+ }
32
+
33
+ interface DatastorePinnedBlock {
34
+ pinCount: number
35
+ pinnedBy: Uint8Array[]
36
+ }
37
+
38
+ const DATASTORE_PIN_PREFIX = '/pin/'
39
+ const DATASTORE_BLOCK_PREFIX = '/pinned-block/'
40
+ const DATASTORE_ENCODING = base36
41
+ // const DAG_WALK_MAX_QUEUE_LENGTH = 10
42
+ const DAG_WALK_QUEUE_CONCURRENCY = 1
43
+
44
+ interface WalkDagOptions extends AbortOptions {
45
+ depth: number
46
+ }
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 dagWalkers: Record<number, DAGWalker>
60
+
61
+ constructor (datastore: Datastore, blockstore: Blockstore, dagWalkers: DAGWalker[]) {
62
+ this.datastore = datastore
63
+ this.blockstore = blockstore
64
+ this.dagWalkers = {}
65
+
66
+ ;[...DEFAULT_DAG_WALKERS, ...dagWalkers].forEach(dagWalker => {
67
+ this.dagWalkers[dagWalker.codec] = dagWalker
68
+ })
69
+ }
70
+
71
+ async add (cid: CID<unknown, number, number, Version>, options: AddOptions = {}): Promise<Pin> {
72
+ const pinKey = toDSKey(cid)
73
+
74
+ if (await this.datastore.has(pinKey)) {
75
+ throw new Error('Already pinned')
76
+ }
77
+
78
+ const depth = Math.round(options.depth ?? Infinity)
79
+
80
+ if (depth < 0) {
81
+ throw new Error('Depth must be greater than or equal to 0')
82
+ }
83
+
84
+ // use a queue to walk the DAG instead of recursion so we can traverse very large DAGs
85
+ const queue = new PQueue({
86
+ concurrency: DAG_WALK_QUEUE_CONCURRENCY
87
+ })
88
+ void queue.add(async () => {
89
+ await this.#walkDag(cid, queue, (pinnedBlock) => {
90
+ // do not update pinned block if this block is already pinned by this CID
91
+ if (pinnedBlock.pinnedBy.find(c => uint8ArrayEquals(c, cid.bytes)) != null) {
92
+ return
93
+ }
94
+
95
+ pinnedBlock.pinCount++
96
+ pinnedBlock.pinnedBy.push(cid.bytes)
97
+ }, {
98
+ ...options,
99
+ depth
100
+ })
101
+ })
102
+
103
+ // if a job in the queue errors, throw that error
104
+ const deferred = defer()
105
+
106
+ queue.on('error', (err) => {
107
+ queue.clear()
108
+ deferred.reject(err)
109
+ })
110
+
111
+ // wait for the queue to complete or error
112
+ await Promise.race([
113
+ queue.onIdle(),
114
+ deferred.promise
115
+ ])
116
+
117
+ const pin: DatastorePin = {
118
+ depth,
119
+ metadata: options.metadata ?? {}
120
+ }
121
+
122
+ await this.datastore.put(pinKey, cborg.encode(pin), options)
123
+
124
+ return {
125
+ cid,
126
+ ...pin
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Walk the DAG behind the passed CID, ensure all blocks are present in the blockstore
132
+ * and update the pin count for them
133
+ */
134
+ async #walkDag (cid: CID, queue: PQueue, withPinnedBlock: (pinnedBlock: DatastorePinnedBlock) => void, options: WalkDagOptions): Promise<void> {
135
+ if (options.depth === -1) {
136
+ return
137
+ }
138
+
139
+ const dagWalker = this.dagWalkers[cid.code]
140
+
141
+ if (dagWalker == null) {
142
+ throw new Error(`No dag walker found for cid codec ${cid.code}`)
143
+ }
144
+
145
+ const block = await this.blockstore.get(cid)
146
+
147
+ await this.#updatePinnedBlock(cid, withPinnedBlock, options)
148
+
149
+ // walk dag, ensure all blocks are present
150
+ for await (const cid of dagWalker.walk(block)) {
151
+ void queue.add(async () => {
152
+ await this.#walkDag(cid, queue, withPinnedBlock, {
153
+ ...options,
154
+ depth: options.depth - 1
155
+ })
156
+ })
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Update the pin count for the CID
162
+ */
163
+ async #updatePinnedBlock (cid: CID, withPinnedBlock: (pinnedBlock: DatastorePinnedBlock) => void, options: AbortOptions): Promise<void> {
164
+ const blockKey = new Key(`${DATASTORE_BLOCK_PREFIX}${DATASTORE_ENCODING.encode(cid.multihash.bytes)}`)
165
+
166
+ let pinnedBlock: DatastorePinnedBlock = {
167
+ pinCount: 0,
168
+ pinnedBy: []
169
+ }
170
+
171
+ try {
172
+ pinnedBlock = cborg.decode(await this.datastore.get(blockKey, options))
173
+ } catch (err: any) {
174
+ if (err.code !== 'ERR_NOT_FOUND') {
175
+ throw err
176
+ }
177
+ }
178
+
179
+ withPinnedBlock(pinnedBlock)
180
+
181
+ if (pinnedBlock.pinCount === 0) {
182
+ if (await this.datastore.has(blockKey)) {
183
+ await this.datastore.delete(blockKey)
184
+ return
185
+ }
186
+ }
187
+
188
+ await this.datastore.put(blockKey, cborg.encode(pinnedBlock), options)
189
+ }
190
+
191
+ async rm (cid: CID<unknown, number, number, Version>, options: RmOptions = {}): Promise<Pin> {
192
+ const pinKey = toDSKey(cid)
193
+ const buf = await this.datastore.get(pinKey, options)
194
+ const pin = cborg.decode(buf)
195
+
196
+ await this.datastore.delete(pinKey, options)
197
+
198
+ // use a queue to walk the DAG instead of recursion so we can traverse very large DAGs
199
+ const queue = new PQueue({
200
+ concurrency: DAG_WALK_QUEUE_CONCURRENCY
201
+ })
202
+ void queue.add(async () => {
203
+ await this.#walkDag(cid, queue, (pinnedBlock) => {
204
+ pinnedBlock.pinCount--
205
+ pinnedBlock.pinnedBy = pinnedBlock.pinnedBy.filter(c => uint8ArrayEquals(c, cid.bytes))
206
+ }, {
207
+ ...options,
208
+ depth: pin.depth
209
+ })
210
+ })
211
+ await queue.onIdle()
212
+
213
+ return {
214
+ cid,
215
+ ...pin
216
+ }
217
+ }
218
+
219
+ async * ls (options: LsOptions = {}): AsyncGenerator<Pin, void, undefined> {
220
+ for await (const { key, value } of this.datastore.query({
221
+ prefix: DATASTORE_PIN_PREFIX + (options.cid != null ? `${options.cid.toString(base36)}` : '')
222
+ }, options)) {
223
+ const cid = CID.parse(key.toString().substring(5), base36)
224
+ const pin = cborg.decode(value)
225
+
226
+ yield {
227
+ cid,
228
+ ...pin
229
+ }
230
+ }
231
+ }
232
+
233
+ async isPinned (cid: CID, options: IsPinnedOptions = {}): Promise<boolean> {
234
+ const blockKey = new Key(`${DATASTORE_BLOCK_PREFIX}${DATASTORE_ENCODING.encode(cid.multihash.bytes)}`)
235
+
236
+ return await this.datastore.has(blockKey, options)
237
+ }
238
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,214 @@
1
+ import { BaseBlockstore } from 'blockstore-core'
2
+ import merge from 'it-merge'
3
+ import { pushable } from 'it-pushable'
4
+ import filter from 'it-filter'
5
+ import type { Blockstore, KeyQuery, Query } from 'interface-blockstore'
6
+ import type { Bitswap } from 'ipfs-bitswap'
7
+ import type { CID } from 'multiformats/cid'
8
+ import type { AbortOptions } from '@libp2p/interfaces'
9
+ import type { AwaitIterable } from 'interface-store'
10
+ import type { Mortice } from 'mortice'
11
+ import createMortice from 'mortice'
12
+ import type { Pins } from '@helia/interface/pins'
13
+
14
+ export interface BlockStorageOptions extends AbortOptions {
15
+ progress?: (evt: Event) => void
16
+ }
17
+
18
+ /**
19
+ * BlockStorage is a hybrid blockstore that puts/gets blocks from a configured
20
+ * blockstore (that may be on disk, s3, or something else). If the blocks are
21
+ * not present Bitswap will be used to fetch them from network peers.
22
+ */
23
+ export class BlockStorage extends BaseBlockstore implements Blockstore {
24
+ public lock: Mortice
25
+ private readonly child: Blockstore
26
+ private readonly bitswap?: Bitswap
27
+ private readonly pins: Pins
28
+
29
+ /**
30
+ * Create a new BlockStorage
31
+ */
32
+ constructor (blockstore: Blockstore, pins: Pins, bitswap?: Bitswap) {
33
+ super()
34
+
35
+ this.child = blockstore
36
+ this.bitswap = bitswap
37
+ this.pins = pins
38
+ this.lock = createMortice()
39
+ }
40
+
41
+ async open (): Promise<void> {
42
+ await this.child.open()
43
+ }
44
+
45
+ async close (): Promise<void> {
46
+ await this.child.close()
47
+ }
48
+
49
+ unwrap (): Blockstore {
50
+ return this.child
51
+ }
52
+
53
+ /**
54
+ * Put a block to the underlying datastore
55
+ */
56
+ async put (cid: CID, block: Uint8Array, options: AbortOptions = {}): Promise<void> {
57
+ const releaseLock = await this.lock.readLock()
58
+
59
+ try {
60
+ if (this.bitswap?.isStarted() === true) {
61
+ await this.bitswap.put(cid, block, options)
62
+ } else {
63
+ await this.child.put(cid, block, options)
64
+ }
65
+ } finally {
66
+ releaseLock()
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Put a multiple blocks to the underlying datastore
72
+ */
73
+ async * putMany (blocks: AwaitIterable<{ key: CID, value: Uint8Array }>, options: AbortOptions = {}): AsyncGenerator<{ key: CID, value: Uint8Array }, void, undefined> {
74
+ const releaseLock = await this.lock.readLock()
75
+
76
+ try {
77
+ const missingBlocks = filter(blocks, async ({ key }) => {
78
+ return !(await this.child.has(key))
79
+ })
80
+
81
+ const store = this.bitswap?.isStarted() === true ? this.bitswap : this.child
82
+
83
+ yield * store.putMany(missingBlocks, options)
84
+ } finally {
85
+ releaseLock()
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Get a block by cid
91
+ */
92
+ async get (cid: CID, options: BlockStorageOptions = {}): Promise<Uint8Array> {
93
+ const releaseLock = await this.lock.readLock()
94
+
95
+ try {
96
+ if (!(await this.has(cid)) && this.bitswap?.isStarted() === true) {
97
+ return await this.bitswap?.get(cid, options)
98
+ } else {
99
+ return await this.child.get(cid, options)
100
+ }
101
+ } finally {
102
+ releaseLock()
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Get multiple blocks back from an array of cids
108
+ */
109
+ async * getMany (cids: AwaitIterable<CID>, options: BlockStorageOptions = {}): AsyncGenerator<Uint8Array, void, undefined> {
110
+ const releaseLock = await this.lock.readLock()
111
+
112
+ try {
113
+ const getFromBitswap = pushable<CID>({ objectMode: true })
114
+ const getFromChild = pushable<CID>({ objectMode: true })
115
+
116
+ void Promise.resolve().then(async () => {
117
+ for await (const cid of cids) {
118
+ if (!(await this.has(cid)) && this.bitswap?.isStarted() === true) {
119
+ getFromBitswap.push(cid)
120
+ } else {
121
+ getFromChild.push(cid)
122
+ }
123
+ }
124
+
125
+ getFromBitswap.end()
126
+ getFromChild.end()
127
+ }).catch(err => {
128
+ getFromBitswap.throw(err)
129
+ })
130
+
131
+ const streams = [
132
+ this.child.getMany(getFromChild, options)
133
+ ]
134
+
135
+ if (this.bitswap?.isStarted() === true) {
136
+ streams.push(this.bitswap.getMany(getFromBitswap, options))
137
+ }
138
+
139
+ yield * merge(...streams)
140
+ } finally {
141
+ releaseLock()
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Delete a block from the blockstore
147
+ */
148
+ async delete (cid: CID, options: AbortOptions = {}): Promise<void> {
149
+ const releaseLock = await this.lock.writeLock()
150
+
151
+ try {
152
+ if (await this.pins.isPinned(cid)) {
153
+ throw new Error('CID was pinned')
154
+ }
155
+
156
+ await this.child.delete(cid, options)
157
+ } finally {
158
+ releaseLock()
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Delete multiple blocks from the blockstore
164
+ */
165
+ async * deleteMany (cids: AwaitIterable<CID>, options: AbortOptions = {}): AsyncGenerator<CID, void, undefined> {
166
+ const releaseLock = await this.lock.writeLock()
167
+
168
+ try {
169
+ const storage = this
170
+
171
+ yield * this.child.deleteMany((async function * () {
172
+ for await (const cid of cids) {
173
+ if (await storage.pins.isPinned(cid)) {
174
+ throw new Error('CID was pinned')
175
+ }
176
+
177
+ yield cid
178
+ }
179
+ }()), options)
180
+ } finally {
181
+ releaseLock()
182
+ }
183
+ }
184
+
185
+ async has (cid: CID, options: AbortOptions = {}): Promise<boolean> {
186
+ const releaseLock = await this.lock.readLock()
187
+
188
+ try {
189
+ return await this.child.has(cid, options)
190
+ } finally {
191
+ releaseLock()
192
+ }
193
+ }
194
+
195
+ async * query (q: Query, options: AbortOptions = {}): AsyncGenerator<{ key: CID, value: Uint8Array }, void, undefined> {
196
+ const releaseLock = await this.lock.readLock()
197
+
198
+ try {
199
+ yield * this.child.query(q, options)
200
+ } finally {
201
+ releaseLock()
202
+ }
203
+ }
204
+
205
+ async * queryKeys (q: KeyQuery, options: AbortOptions = {}): AsyncGenerator<CID, void, undefined> {
206
+ const releaseLock = await this.lock.readLock()
207
+
208
+ try {
209
+ yield * this.child.queryKeys(q, options)
210
+ } finally {
211
+ releaseLock()
212
+ }
213
+ }
214
+ }