@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/storage.ts ADDED
@@ -0,0 +1,172 @@
1
+ import { start, stop } from '@libp2p/interface'
2
+ import createMortice from 'mortice'
3
+ import type { Blocks, Pair, DeleteManyBlocksProgressEvents, DeleteBlockProgressEvents, GetBlockProgressEvents, GetManyBlocksProgressEvents, PutManyBlocksProgressEvents, PutBlockProgressEvents, GetAllBlocksProgressEvents, GetOfflineOptions } from '@helia/interface/blocks'
4
+ import type { Pins } from '@helia/interface/pins'
5
+ import type { AbortOptions, Startable } from '@libp2p/interface'
6
+ import type { Blockstore } from 'interface-blockstore'
7
+ import type { AwaitIterable } from 'interface-store'
8
+ import type { Mortice } from 'mortice'
9
+ import type { CID } from 'multiformats/cid'
10
+ import type { ProgressOptions } from 'progress-events'
11
+
12
+ export interface BlockStorageInit {
13
+ holdGcLock?: boolean
14
+ }
15
+
16
+ export interface GetOptions extends AbortOptions {
17
+ progress?(evt: Event): void
18
+ }
19
+
20
+ /**
21
+ * BlockStorage is a hybrid blockstore that puts/gets blocks from a configured
22
+ * blockstore (that may be on disk, s3, or something else). If the blocks are
23
+ * not present Bitswap will be used to fetch them from network peers.
24
+ */
25
+ export class BlockStorage implements Blocks, Startable {
26
+ public lock: Mortice
27
+ private readonly child: Blockstore
28
+ private readonly pins: Pins
29
+ private started: boolean
30
+
31
+ /**
32
+ * Create a new BlockStorage
33
+ */
34
+ constructor (blockstore: Blockstore, pins: Pins, options: BlockStorageInit = {}) {
35
+ this.child = blockstore
36
+ this.pins = pins
37
+ this.lock = createMortice({
38
+ singleProcess: options.holdGcLock
39
+ })
40
+ this.started = false
41
+ }
42
+
43
+ isStarted (): boolean {
44
+ return this.started
45
+ }
46
+
47
+ async start (): Promise<void> {
48
+ await start(this.child)
49
+ this.started = true
50
+ }
51
+
52
+ async stop (): Promise<void> {
53
+ await stop(this.child)
54
+ this.started = false
55
+ }
56
+
57
+ unwrap (): Blockstore {
58
+ return this.child
59
+ }
60
+
61
+ /**
62
+ * Put a block to the underlying datastore
63
+ */
64
+ async put (cid: CID, block: Uint8Array, options: AbortOptions & ProgressOptions<PutBlockProgressEvents> = {}): Promise<CID> {
65
+ const releaseLock = await this.lock.readLock()
66
+
67
+ try {
68
+ return await this.child.put(cid, block, options)
69
+ } finally {
70
+ releaseLock()
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Put a multiple blocks to the underlying datastore
76
+ */
77
+ async * putMany (blocks: AwaitIterable<{ cid: CID, block: Uint8Array }>, options: AbortOptions & ProgressOptions<PutManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
78
+ const releaseLock = await this.lock.readLock()
79
+
80
+ try {
81
+ yield * this.child.putMany(blocks, options)
82
+ } finally {
83
+ releaseLock()
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get a block by cid
89
+ */
90
+ async get (cid: CID, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetBlockProgressEvents> = {}): Promise<Uint8Array> {
91
+ const releaseLock = await this.lock.readLock()
92
+
93
+ try {
94
+ return await this.child.get(cid, options)
95
+ } finally {
96
+ releaseLock()
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get multiple blocks back from an (async) iterable of cids
102
+ */
103
+ async * getMany (cids: AwaitIterable<CID>, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetManyBlocksProgressEvents> = {}): AsyncIterable<Pair> {
104
+ const releaseLock = await this.lock.readLock()
105
+
106
+ try {
107
+ yield * this.child.getMany(cids, options)
108
+ } finally {
109
+ releaseLock()
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Delete a block from the blockstore
115
+ */
116
+ async delete (cid: CID, options: AbortOptions & ProgressOptions<DeleteBlockProgressEvents> = {}): Promise<void> {
117
+ const releaseLock = await this.lock.writeLock()
118
+
119
+ try {
120
+ if (await this.pins.isPinned(cid)) {
121
+ throw new Error('CID was pinned')
122
+ }
123
+
124
+ await this.child.delete(cid, options)
125
+ } finally {
126
+ releaseLock()
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Delete multiple blocks from the blockstore
132
+ */
133
+ async * deleteMany (cids: AwaitIterable<CID>, options: AbortOptions & ProgressOptions<DeleteManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
134
+ const releaseLock = await this.lock.writeLock()
135
+
136
+ try {
137
+ const storage = this
138
+
139
+ yield * this.child.deleteMany((async function * (): AsyncGenerator<CID> {
140
+ for await (const cid of cids) {
141
+ if (await storage.pins.isPinned(cid)) {
142
+ throw new Error('CID was pinned')
143
+ }
144
+
145
+ yield cid
146
+ }
147
+ }()), options)
148
+ } finally {
149
+ releaseLock()
150
+ }
151
+ }
152
+
153
+ async has (cid: CID, options: AbortOptions = {}): Promise<boolean> {
154
+ const releaseLock = await this.lock.readLock()
155
+
156
+ try {
157
+ return await this.child.has(cid, options)
158
+ } finally {
159
+ releaseLock()
160
+ }
161
+ }
162
+
163
+ async * getAll (options: AbortOptions & ProgressOptions<GetAllBlocksProgressEvents> = {}): AsyncIterable<Pair> {
164
+ const releaseLock = await this.lock.readLock()
165
+
166
+ try {
167
+ yield * this.child.getAll(options)
168
+ } finally {
169
+ releaseLock()
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,198 @@
1
+ /* eslint max-depth: ["error", 7] */
2
+
3
+ import * as dagCbor from '@ipld/dag-cbor'
4
+ import * as dagJson from '@ipld/dag-json'
5
+ import * as dagPb from '@ipld/dag-pb'
6
+ import * as cborg from 'cborg'
7
+ import { Type, Token } from 'cborg'
8
+ import * as cborgJson from 'cborg/json'
9
+ import { CID } from 'multiformats'
10
+ import { base64 } from 'multiformats/bases/base64'
11
+ import * as json from 'multiformats/codecs/json'
12
+ import * as raw from 'multiformats/codecs/raw'
13
+ import type { DAGWalker } from '@helia/interface'
14
+
15
+ /**
16
+ * Dag walker for dag-pb CIDs
17
+ */
18
+ export const dagPbWalker: DAGWalker = {
19
+ codec: dagPb.code,
20
+ * walk (block) {
21
+ const node = dagPb.decode(block)
22
+
23
+ yield * node.Links.map(l => l.Hash)
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Dag walker for raw CIDs
29
+ */
30
+ export const rawWalker: DAGWalker = {
31
+ codec: raw.code,
32
+ * walk () {
33
+ // no embedded CIDs in a raw block
34
+ }
35
+ }
36
+
37
+ // https://github.com/ipfs/go-ipfs/issues/3570#issuecomment-273931692
38
+ const CID_TAG = 42
39
+
40
+ /**
41
+ * Dag walker for dag-cbor CIDs. Does not actually use dag-cbor since
42
+ * all we are interested in is extracting the the CIDs from the block
43
+ * so we can just use cborg for that.
44
+ */
45
+ export const dagCborWalker: DAGWalker = {
46
+ codec: dagCbor.code,
47
+ * walk (block) {
48
+ const cids: CID[] = []
49
+ const tags: cborg.TagDecoder[] = []
50
+ tags[CID_TAG] = (bytes) => {
51
+ if (bytes[0] !== 0) {
52
+ throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')
53
+ }
54
+
55
+ const cid = CID.decode(bytes.subarray(1)) // ignore leading 0x00
56
+
57
+ cids.push(cid)
58
+
59
+ return cid
60
+ }
61
+
62
+ cborg.decode(block, {
63
+ tags
64
+ })
65
+
66
+ yield * cids
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Borrowed from @ipld/dag-json
72
+ */
73
+ class DagJsonTokenizer extends cborgJson.Tokenizer {
74
+ private readonly tokenBuffer: cborg.Token[]
75
+
76
+ constructor (data: Uint8Array, options?: cborg.DecodeOptions) {
77
+ super(data, options)
78
+
79
+ this.tokenBuffer = []
80
+ }
81
+
82
+ done (): boolean {
83
+ return this.tokenBuffer.length === 0 && super.done()
84
+ }
85
+
86
+ _next (): cborg.Token {
87
+ if (this.tokenBuffer.length > 0) {
88
+ // @ts-expect-error https://github.com/Microsoft/TypeScript/issues/30406
89
+ return this.tokenBuffer.pop()
90
+ }
91
+ return super.next()
92
+ }
93
+
94
+ /**
95
+ * Implements rules outlined in https://github.com/ipld/specs/pull/356
96
+ */
97
+ next (): cborg.Token {
98
+ const token = this._next()
99
+
100
+ if (token.type === Type.map) {
101
+ const keyToken = this._next()
102
+ if (keyToken.type === Type.string && keyToken.value === '/') {
103
+ const valueToken = this._next()
104
+ if (valueToken.type === Type.string) { // *must* be a CID
105
+ const breakToken = this._next() // swallow the end-of-map token
106
+ if (breakToken.type !== Type.break) {
107
+ throw new Error('Invalid encoded CID form')
108
+ }
109
+ this.tokenBuffer.push(valueToken) // CID.parse will pick this up after our tag token
110
+ return new Token(Type.tag, 42, 0)
111
+ }
112
+ if (valueToken.type === Type.map) {
113
+ const innerKeyToken = this._next()
114
+ if (innerKeyToken.type === Type.string && innerKeyToken.value === 'bytes') {
115
+ const innerValueToken = this._next()
116
+ if (innerValueToken.type === Type.string) { // *must* be Bytes
117
+ for (let i = 0; i < 2; i++) {
118
+ const breakToken = this._next() // swallow two end-of-map tokens
119
+ if (breakToken.type !== Type.break) {
120
+ throw new Error('Invalid encoded Bytes form')
121
+ }
122
+ }
123
+ const bytes = base64.decode(`m${innerValueToken.value}`)
124
+ return new Token(Type.bytes, bytes, innerValueToken.value.length)
125
+ }
126
+ this.tokenBuffer.push(innerValueToken) // bail
127
+ }
128
+ this.tokenBuffer.push(innerKeyToken) // bail
129
+ }
130
+ this.tokenBuffer.push(valueToken) // bail
131
+ }
132
+ this.tokenBuffer.push(keyToken) // bail
133
+ }
134
+ return token
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Dag walker for dag-json CIDs. Does not actually use dag-json since
140
+ * all we are interested in is extracting the the CIDs from the block
141
+ * so we can just use cborg/json for that.
142
+ */
143
+ export const dagJsonWalker: DAGWalker = {
144
+ codec: dagJson.code,
145
+ * walk (block) {
146
+ const cids: CID[] = []
147
+ const tags: cborg.TagDecoder[] = []
148
+ tags[CID_TAG] = (string) => {
149
+ const cid = CID.parse(string)
150
+
151
+ cids.push(cid)
152
+
153
+ return cid
154
+ }
155
+
156
+ cborgJson.decode(block, {
157
+ tags,
158
+ tokenizer: new DagJsonTokenizer(block, {
159
+ tags,
160
+ allowIndefinite: true,
161
+ allowUndefined: true,
162
+ allowNaN: true,
163
+ allowInfinity: true,
164
+ allowBigInt: true,
165
+ strict: false,
166
+ rejectDuplicateMapKeys: false
167
+ })
168
+ })
169
+
170
+ yield * cids
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Dag walker for json CIDs. JSON has no facility for linking to
176
+ * external blocks so the walker is a no-op.
177
+ */
178
+ export const jsonWalker: DAGWalker = {
179
+ codec: json.code,
180
+ * walk () {}
181
+ }
182
+
183
+ export function defaultDagWalkers (walkers: DAGWalker[] = []): Record<number, DAGWalker> {
184
+ const output: Record<number, DAGWalker> = {}
185
+
186
+ ;[
187
+ dagPbWalker,
188
+ rawWalker,
189
+ dagCborWalker,
190
+ dagJsonWalker,
191
+ jsonWalker,
192
+ ...walkers
193
+ ].forEach(dagWalker => {
194
+ output[dagWalker.codec] = dagWalker
195
+ })
196
+
197
+ return output
198
+ }
@@ -0,0 +1,23 @@
1
+ import { type 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
+ }
@@ -0,0 +1,18 @@
1
+ import { identity } from 'multiformats/hashes/identity'
2
+ import { sha256, sha512 } from 'multiformats/hashes/sha2'
3
+ import type { MultihashHasher } from 'multiformats/hashes/interface'
4
+
5
+ export function defaultHashers (hashers: MultihashHasher[] = []): Record<number, MultihashHasher> {
6
+ const output: Record<number, MultihashHasher> = {}
7
+
8
+ ;[
9
+ sha256,
10
+ sha512,
11
+ identity,
12
+ ...hashers
13
+ ].forEach(hasher => {
14
+ output[hasher.code] = hasher
15
+ })
16
+
17
+ return output
18
+ }
@@ -0,0 +1,261 @@
1
+ import { CodeError, start, stop } from '@libp2p/interface'
2
+ import { anySignal } from 'any-signal'
3
+ import filter from 'it-filter'
4
+ import forEach from 'it-foreach'
5
+ import { CustomProgressEvent, type ProgressOptions } from 'progress-events'
6
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
7
+ import type { BlockBroker, Blocks, Pair, DeleteManyBlocksProgressEvents, DeleteBlockProgressEvents, GetBlockProgressEvents, GetManyBlocksProgressEvents, PutManyBlocksProgressEvents, PutBlockProgressEvents, GetAllBlocksProgressEvents, GetOfflineOptions, BlockRetriever, BlockAnnouncer, BlockRetrievalOptions } from '@helia/interface/blocks'
8
+ import type { AbortOptions, ComponentLogger, Logger, LoggerOptions, Startable } from '@libp2p/interface'
9
+ import type { Blockstore } from 'interface-blockstore'
10
+ import type { AwaitIterable } from 'interface-store'
11
+ import type { CID } from 'multiformats/cid'
12
+ import type { MultihashHasher } from 'multiformats/hashes/interface'
13
+
14
+ export interface GetOptions extends AbortOptions {
15
+ progress?(evt: Event): void
16
+ }
17
+
18
+ function isBlockRetriever (b: any): b is BlockRetriever {
19
+ return typeof b.retrieve === 'function'
20
+ }
21
+
22
+ function isBlockAnnouncer (b: any): b is BlockAnnouncer {
23
+ return typeof b.announce === 'function'
24
+ }
25
+
26
+ export interface NetworkedStorageComponents {
27
+ blockstore: Blockstore
28
+ logger: ComponentLogger
29
+ blockBrokers?: BlockBroker[]
30
+ hashers?: Record<number, MultihashHasher>
31
+ }
32
+
33
+ /**
34
+ * Networked storage wraps a regular blockstore - when getting blocks if the
35
+ * blocks are not present Bitswap will be used to fetch them from network peers.
36
+ */
37
+ export class NetworkedStorage implements Blocks, Startable {
38
+ private readonly child: Blockstore
39
+ private readonly blockRetrievers: BlockRetriever[]
40
+ private readonly blockAnnouncers: BlockAnnouncer[]
41
+ private readonly hashers: Record<number, MultihashHasher>
42
+ private started: boolean
43
+ private readonly log: Logger
44
+
45
+ /**
46
+ * Create a new BlockStorage
47
+ */
48
+ constructor (components: NetworkedStorageComponents) {
49
+ this.log = components.logger.forComponent('helia:networked-storage')
50
+ this.child = components.blockstore
51
+ this.blockRetrievers = (components.blockBrokers ?? []).filter(isBlockRetriever)
52
+ this.blockAnnouncers = (components.blockBrokers ?? []).filter(isBlockAnnouncer)
53
+ this.hashers = components.hashers ?? {}
54
+ this.started = false
55
+ }
56
+
57
+ isStarted (): boolean {
58
+ return this.started
59
+ }
60
+
61
+ async start (): Promise<void> {
62
+ await start(this.child, ...new Set([...this.blockRetrievers, ...this.blockAnnouncers]))
63
+ this.started = true
64
+ }
65
+
66
+ async stop (): Promise<void> {
67
+ await stop(this.child, ...new Set([...this.blockRetrievers, ...this.blockAnnouncers]))
68
+ this.started = false
69
+ }
70
+
71
+ unwrap (): Blockstore {
72
+ return this.child
73
+ }
74
+
75
+ /**
76
+ * Put a block to the underlying datastore
77
+ */
78
+ async put (cid: CID, block: Uint8Array, options: AbortOptions & ProgressOptions<PutBlockProgressEvents> = {}): Promise<CID> {
79
+ if (await this.child.has(cid)) {
80
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:put:duplicate', cid))
81
+ return cid
82
+ }
83
+
84
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:put:providers:notify', cid))
85
+
86
+ this.blockAnnouncers.forEach(provider => {
87
+ provider.announce(cid, block, options)
88
+ })
89
+
90
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:put:blockstore:put', cid))
91
+
92
+ return this.child.put(cid, block, options)
93
+ }
94
+
95
+ /**
96
+ * Put a multiple blocks to the underlying datastore
97
+ */
98
+ async * putMany (blocks: AwaitIterable<{ cid: CID, block: Uint8Array }>, options: AbortOptions & ProgressOptions<PutManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
99
+ const missingBlocks = filter(blocks, async ({ cid }): Promise<boolean> => {
100
+ const has = await this.child.has(cid)
101
+
102
+ if (has) {
103
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:put-many:duplicate', cid))
104
+ }
105
+
106
+ return !has
107
+ })
108
+
109
+ const notifyEach = forEach(missingBlocks, ({ cid, block }): void => {
110
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:put-many:providers:notify', cid))
111
+ this.blockAnnouncers.forEach(provider => {
112
+ provider.announce(cid, block, options)
113
+ })
114
+ })
115
+
116
+ options.onProgress?.(new CustomProgressEvent('blocks:put-many:blockstore:put-many'))
117
+ yield * this.child.putMany(notifyEach, options)
118
+ }
119
+
120
+ /**
121
+ * Get a block by cid
122
+ */
123
+ async get (cid: CID, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetBlockProgressEvents> = {}): Promise<Uint8Array> {
124
+ if (options.offline !== true && !(await this.child.has(cid))) {
125
+ // we do not have the block locally, get it from a block provider
126
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:providers:get', cid))
127
+ const block = await raceBlockRetrievers(cid, this.blockRetrievers, this.hashers[cid.multihash.code], {
128
+ ...options,
129
+ log: this.log
130
+ })
131
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:blockstore:put', cid))
132
+ await this.child.put(cid, block, options)
133
+
134
+ // notify other block providers of the new block
135
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:providers:notify', cid))
136
+ this.blockAnnouncers.forEach(provider => {
137
+ provider.announce(cid, block, options)
138
+ })
139
+
140
+ return block
141
+ }
142
+
143
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:blockstore:get', cid))
144
+
145
+ return this.child.get(cid, options)
146
+ }
147
+
148
+ /**
149
+ * Get multiple blocks back from an (async) iterable of cids
150
+ */
151
+ async * getMany (cids: AwaitIterable<CID>, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetManyBlocksProgressEvents> = {}): AsyncIterable<Pair> {
152
+ options.onProgress?.(new CustomProgressEvent('blocks:get-many:blockstore:get-many'))
153
+
154
+ yield * this.child.getMany(forEach(cids, async (cid): Promise<void> => {
155
+ if (options.offline !== true && !(await this.child.has(cid))) {
156
+ // we do not have the block locally, get it from a block provider
157
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:get-many:providers:get', cid))
158
+ const block = await raceBlockRetrievers(cid, this.blockRetrievers, this.hashers[cid.multihash.code], {
159
+ ...options,
160
+ log: this.log
161
+ })
162
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:get-many:blockstore:put', cid))
163
+ await this.child.put(cid, block, options)
164
+
165
+ // notify other block providers of the new block
166
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:get-many:providers:notify', cid))
167
+ this.blockAnnouncers.forEach(provider => {
168
+ provider.announce(cid, block, options)
169
+ })
170
+ }
171
+ }))
172
+ }
173
+
174
+ /**
175
+ * Delete a block from the blockstore
176
+ */
177
+ async delete (cid: CID, options: AbortOptions & ProgressOptions<DeleteBlockProgressEvents> = {}): Promise<void> {
178
+ options.onProgress?.(new CustomProgressEvent<CID>('blocks:delete:blockstore:delete', cid))
179
+
180
+ await this.child.delete(cid, options)
181
+ }
182
+
183
+ /**
184
+ * Delete multiple blocks from the blockstore
185
+ */
186
+ async * deleteMany (cids: AwaitIterable<CID>, options: AbortOptions & ProgressOptions<DeleteManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
187
+ options.onProgress?.(new CustomProgressEvent('blocks:delete-many:blockstore:delete-many'))
188
+ yield * this.child.deleteMany((async function * (): AsyncGenerator<CID> {
189
+ for await (const cid of cids) {
190
+ yield cid
191
+ }
192
+ }()), options)
193
+ }
194
+
195
+ async has (cid: CID, options: AbortOptions = {}): Promise<boolean> {
196
+ return this.child.has(cid, options)
197
+ }
198
+
199
+ async * getAll (options: AbortOptions & ProgressOptions<GetAllBlocksProgressEvents> = {}): AwaitIterable<Pair> {
200
+ options.onProgress?.(new CustomProgressEvent('blocks:get-all:blockstore:get-many'))
201
+ yield * this.child.getAll(options)
202
+ }
203
+ }
204
+
205
+ export const getCidBlockVerifierFunction = (cid: CID, hasher: MultihashHasher): Required<BlockRetrievalOptions>['validateFn'] => {
206
+ if (hasher == null) {
207
+ throw new CodeError(`No hasher configured for multihash code 0x${cid.multihash.code.toString(16)}, please configure one. You can look up which hash this is at https://github.com/multiformats/multicodec/blob/master/table.csv`, 'ERR_UNKNOWN_HASH_ALG')
208
+ }
209
+
210
+ return async (block: Uint8Array): Promise<void> => {
211
+ // verify block
212
+ const hash = await hasher.digest(block)
213
+
214
+ if (!uint8ArrayEquals(hash.digest, cid.multihash.digest)) {
215
+ // if a hash mismatch occurs for a TrustlessGatewayBlockBroker, we should try another gateway
216
+ throw new CodeError('Hash of downloaded block did not match multihash from passed CID', 'ERR_HASH_MISMATCH')
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Race block providers cancelling any pending requests once the block has been
223
+ * found.
224
+ */
225
+ async function raceBlockRetrievers (cid: CID, providers: BlockRetriever[], hasher: MultihashHasher, options: AbortOptions & LoggerOptions): Promise<Uint8Array> {
226
+ const validateFn = getCidBlockVerifierFunction(cid, hasher)
227
+
228
+ const controller = new AbortController()
229
+ const signal = anySignal([controller.signal, options.signal])
230
+
231
+ try {
232
+ return await Promise.any(
233
+ providers.map(async provider => {
234
+ try {
235
+ let blocksWereValidated = false
236
+ const block = await provider.retrieve(cid, {
237
+ ...options,
238
+ signal,
239
+ validateFn: async (block: Uint8Array): Promise<void> => {
240
+ await validateFn(block)
241
+ blocksWereValidated = true
242
+ }
243
+ })
244
+
245
+ if (!blocksWereValidated) {
246
+ // the blockBroker either did not throw an error when attempting to validate the block
247
+ // or did not call the validateFn at all. We should validate the block ourselves
248
+ await validateFn(block)
249
+ }
250
+
251
+ return block
252
+ } catch (err) {
253
+ options.log.error('could not retrieve verified block for %c', cid, err)
254
+ throw err
255
+ }
256
+ })
257
+ )
258
+ } finally {
259
+ signal.clear()
260
+ }
261
+ }