@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/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
|
+
}
|