helia 0.0.0-270bb98

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/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,209 @@
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, bitswap: Bitswap, pins: Pins) {
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.writeLock()
58
+
59
+ try {
60
+ if (this.bitswap.isStarted()) {
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.writeLock()
75
+
76
+ try {
77
+ const missingBlocks = filter(blocks, async ({ key }) => { return !(await this.child.has(key)) })
78
+
79
+ if (this.bitswap.isStarted()) {
80
+ yield * this.bitswap.putMany(missingBlocks, options)
81
+ } else {
82
+ yield * this.child.putMany(missingBlocks, options)
83
+ }
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()) {
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()) {
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
+ yield * merge(
132
+ this.bitswap.getMany(getFromBitswap, options),
133
+ this.child.getMany(getFromChild, options)
134
+ )
135
+ } finally {
136
+ releaseLock()
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Delete a block from the blockstore
142
+ */
143
+ async delete (cid: CID, options: AbortOptions = {}): Promise<void> {
144
+ const releaseLock = await this.lock.writeLock()
145
+
146
+ try {
147
+ if (await this.pins.isPinned(cid)) {
148
+ throw new Error('CID was pinned')
149
+ }
150
+
151
+ await this.child.delete(cid, options)
152
+ } finally {
153
+ releaseLock()
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Delete multiple blocks from the blockstore
159
+ */
160
+ async * deleteMany (cids: AwaitIterable<CID>, options: AbortOptions = {}): AsyncGenerator<CID, void, undefined> {
161
+ const releaseLock = await this.lock.writeLock()
162
+
163
+ try {
164
+ const storage = this
165
+
166
+ yield * this.child.deleteMany((async function * () {
167
+ for await (const cid of cids) {
168
+ if (await storage.pins.isPinned(cid)) {
169
+ throw new Error('CID was pinned')
170
+ }
171
+
172
+ yield cid
173
+ }
174
+ }()), options)
175
+ } finally {
176
+ releaseLock()
177
+ }
178
+ }
179
+
180
+ async has (cid: CID, options: AbortOptions = {}): Promise<boolean> {
181
+ const releaseLock = await this.lock.readLock()
182
+
183
+ try {
184
+ return await this.child.has(cid, options)
185
+ } finally {
186
+ releaseLock()
187
+ }
188
+ }
189
+
190
+ async * query (q: Query, options: AbortOptions = {}): AsyncGenerator<{ key: CID, value: Uint8Array }, void, undefined> {
191
+ const releaseLock = await this.lock.readLock()
192
+
193
+ try {
194
+ yield * this.child.query(q, options)
195
+ } finally {
196
+ releaseLock()
197
+ }
198
+ }
199
+
200
+ async * queryKeys (q: KeyQuery, options: AbortOptions = {}): AsyncGenerator<CID, void, undefined> {
201
+ const releaseLock = await this.lock.readLock()
202
+
203
+ try {
204
+ yield * this.child.queryKeys(q, options)
205
+ } finally {
206
+ releaseLock()
207
+ }
208
+ }
209
+ }
@@ -0,0 +1,169 @@
1
+ /* eslint max-depth: ["error", 7] */
2
+
3
+ import * as dagPb from '@ipld/dag-pb'
4
+ import * as cborg from 'cborg'
5
+ import { Type, Token } from 'cborg'
6
+ import * as cborgJson from 'cborg/json'
7
+ import type { DAGWalker } from '../index.js'
8
+ import * as raw from 'multiformats/codecs/raw'
9
+ import { CID } from 'multiformats'
10
+ import { base64 } from 'multiformats/bases/base64'
11
+
12
+ /**
13
+ * Dag walker for dag-pb CIDs
14
+ */
15
+ export const dagPbWalker: DAGWalker = {
16
+ codec: dagPb.code,
17
+ async * walk (block) {
18
+ const node = dagPb.decode(block)
19
+
20
+ yield * node.Links.map(l => l.Hash)
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Dag walker for raw CIDs
26
+ */
27
+ export const rawWalker: DAGWalker = {
28
+ codec: raw.code,
29
+ async * walk () {
30
+ // no embedded CIDs in a raw block
31
+ }
32
+ }
33
+
34
+ // https://github.com/ipfs/go-ipfs/issues/3570#issuecomment-273931692
35
+ const CID_TAG = 42
36
+
37
+ /**
38
+ * Dag walker for dag-cbor CIDs. Does not actually use dag-cbor since
39
+ * all we are interested in is extracting the the CIDs from the block
40
+ * so we can just use cborg for that.
41
+ */
42
+ export const cborWalker: DAGWalker = {
43
+ codec: 0x71,
44
+ async * walk (block) {
45
+ const cids: CID[] = []
46
+ const tags: cborg.TagDecoder[] = []
47
+ tags[CID_TAG] = (bytes) => {
48
+ if (bytes[0] !== 0) {
49
+ throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')
50
+ }
51
+
52
+ const cid = CID.decode(bytes.subarray(1)) // ignore leading 0x00
53
+
54
+ cids.push(cid)
55
+
56
+ return cid
57
+ }
58
+
59
+ cborg.decode(block, {
60
+ tags
61
+ })
62
+
63
+ yield * cids
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Borrowed from @ipld/dag-json
69
+ */
70
+ class DagJsonTokenizer extends cborgJson.Tokenizer {
71
+ private readonly tokenBuffer: cborg.Token[]
72
+
73
+ constructor (data: Uint8Array, options?: cborg.DecodeOptions) {
74
+ super(data, options)
75
+
76
+ this.tokenBuffer = []
77
+ }
78
+
79
+ done (): boolean {
80
+ return this.tokenBuffer.length === 0 && super.done()
81
+ }
82
+
83
+ _next (): cborg.Token {
84
+ if (this.tokenBuffer.length > 0) {
85
+ // @ts-expect-error https://github.com/Microsoft/TypeScript/issues/30406
86
+ return this.tokenBuffer.pop()
87
+ }
88
+ return super.next()
89
+ }
90
+
91
+ /**
92
+ * Implements rules outlined in https://github.com/ipld/specs/pull/356
93
+ */
94
+ next (): cborg.Token {
95
+ const token = this._next()
96
+
97
+ if (token.type === Type.map) {
98
+ const keyToken = this._next()
99
+ if (keyToken.type === Type.string && keyToken.value === '/') {
100
+ const valueToken = this._next()
101
+ if (valueToken.type === Type.string) { // *must* be a CID
102
+ const breakToken = this._next() // swallow the end-of-map token
103
+ if (breakToken.type !== Type.break) {
104
+ throw new Error('Invalid encoded CID form')
105
+ }
106
+ this.tokenBuffer.push(valueToken) // CID.parse will pick this up after our tag token
107
+ return new Token(Type.tag, 42, 0)
108
+ }
109
+ if (valueToken.type === Type.map) {
110
+ const innerKeyToken = this._next()
111
+ if (innerKeyToken.type === Type.string && innerKeyToken.value === 'bytes') {
112
+ const innerValueToken = this._next()
113
+ if (innerValueToken.type === Type.string) { // *must* be Bytes
114
+ for (let i = 0; i < 2; i++) {
115
+ const breakToken = this._next() // swallow two end-of-map tokens
116
+ if (breakToken.type !== Type.break) {
117
+ throw new Error('Invalid encoded Bytes form')
118
+ }
119
+ }
120
+ const bytes = base64.decode(`m${innerValueToken.value}`)
121
+ return new Token(Type.bytes, bytes, innerValueToken.value.length)
122
+ }
123
+ this.tokenBuffer.push(innerValueToken) // bail
124
+ }
125
+ this.tokenBuffer.push(innerKeyToken) // bail
126
+ }
127
+ this.tokenBuffer.push(valueToken) // bail
128
+ }
129
+ this.tokenBuffer.push(keyToken) // bail
130
+ }
131
+ return token
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Dag walker for dag-json CIDs. Does not actually use dag-json since
137
+ * all we are interested in is extracting the the CIDs from the block
138
+ * so we can just use cborg/json for that.
139
+ */
140
+ export const jsonWalker: DAGWalker = {
141
+ codec: 0x0129,
142
+ async * walk (block) {
143
+ const cids: CID[] = []
144
+ const tags: cborg.TagDecoder[] = []
145
+ tags[CID_TAG] = (string) => {
146
+ const cid = CID.parse(string)
147
+
148
+ cids.push(cid)
149
+
150
+ return cid
151
+ }
152
+
153
+ cborgJson.decode(block, {
154
+ tags,
155
+ tokenizer: new DagJsonTokenizer(block, {
156
+ tags,
157
+ allowIndefinite: true,
158
+ allowUndefined: true,
159
+ allowNaN: true,
160
+ allowInfinity: true,
161
+ allowBigInt: true,
162
+ strict: false,
163
+ rejectDuplicateMapKeys: false
164
+ })
165
+ })
166
+
167
+ yield * cids
168
+ }
169
+ }
@@ -0,0 +1,23 @@
1
+ import { Datastore, Key } from 'interface-datastore'
2
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
3
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
+
5
+ const DS_VERSION_KEY = new Key('/version')
6
+ const CURRENT_VERSION = 1
7
+
8
+ export async function assertDatastoreVersionIsCurrent (datastore: Datastore): Promise<void> {
9
+ if (!(await datastore.has(DS_VERSION_KEY))) {
10
+ await datastore.put(DS_VERSION_KEY, uint8ArrayFromString(`${CURRENT_VERSION}`))
11
+
12
+ return
13
+ }
14
+
15
+ const buf = await datastore.get(DS_VERSION_KEY)
16
+ const str = uint8ArrayToString(buf)
17
+ const version = parseInt(str, 10)
18
+
19
+ if (version !== CURRENT_VERSION) {
20
+ // TODO: write migrations when we break compatibility - for an example, see https://github.com/ipfs/js-ipfs-repo/tree/master/packages/ipfs-repo-migrations
21
+ throw new Error('Unknown datastore version, a datastore migration may be required')
22
+ }
23
+ }