@web3-storage/pail 0.4.0
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.md +232 -0
- package/README.md +84 -0
- package/cli.js +259 -0
- package/dist/src/api.d.ts +33 -0
- package/dist/src/api.d.ts.map +1 -0
- package/dist/src/batch/api.d.ts +33 -0
- package/dist/src/batch/api.d.ts.map +1 -0
- package/dist/src/batch/index.d.ts +74 -0
- package/dist/src/batch/index.d.ts.map +1 -0
- package/dist/src/batch/shard.d.ts +3 -0
- package/dist/src/batch/shard.d.ts.map +1 -0
- package/dist/src/block.d.ts +35 -0
- package/dist/src/block.d.ts.map +1 -0
- package/dist/src/clock/api.d.ts +10 -0
- package/dist/src/clock/api.d.ts.map +1 -0
- package/dist/src/clock/index.d.ts +48 -0
- package/dist/src/clock/index.d.ts.map +1 -0
- package/dist/src/crdt/api.d.ts +26 -0
- package/dist/src/crdt/api.d.ts.map +1 -0
- package/dist/src/crdt/batch/api.d.ts +11 -0
- package/dist/src/crdt/batch/api.d.ts.map +1 -0
- package/dist/src/crdt/batch/index.d.ts +5 -0
- package/dist/src/crdt/batch/index.d.ts.map +1 -0
- package/dist/src/crdt/index.d.ts +11 -0
- package/dist/src/crdt/index.d.ts.map +1 -0
- package/dist/src/diff.d.ts +13 -0
- package/dist/src/diff.d.ts.map +1 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/merge.d.ts +5 -0
- package/dist/src/merge.d.ts.map +1 -0
- package/dist/src/shard.d.ts +51 -0
- package/dist/src/shard.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +174 -0
- package/src/api.js +1 -0
- package/src/api.ts +47 -0
- package/src/batch/api.js +1 -0
- package/src/batch/api.ts +61 -0
- package/src/batch/index.js +245 -0
- package/src/batch/shard.js +14 -0
- package/src/block.js +75 -0
- package/src/clock/api.js +1 -0
- package/src/clock/api.ts +12 -0
- package/src/clock/index.js +179 -0
- package/src/crdt/api.js +1 -0
- package/src/crdt/api.ts +33 -0
- package/src/crdt/batch/api.js +1 -0
- package/src/crdt/batch/api.ts +31 -0
- package/src/crdt/batch/index.js +156 -0
- package/src/crdt/index.js +355 -0
- package/src/diff.js +151 -0
- package/src/index.js +285 -0
- package/src/merge.js +43 -0
- package/src/shard.js +248 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// eslint-disable-next-line no-unused-vars
|
|
2
|
+
import * as API from './api.js'
|
|
3
|
+
import { ShardFetcher } from './shard.js'
|
|
4
|
+
import * as Shard from './shard.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Put a value (a CID) for the given key. If the key exists it's value is
|
|
8
|
+
* overwritten.
|
|
9
|
+
*
|
|
10
|
+
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
11
|
+
* @param {API.ShardLink} root CID of the root node of the bucket.
|
|
12
|
+
* @param {string} key The key of the value to put.
|
|
13
|
+
* @param {API.UnknownLink} value The value to put.
|
|
14
|
+
* @returns {Promise<{ root: API.ShardLink } & API.ShardDiff>}
|
|
15
|
+
*/
|
|
16
|
+
export const put = async (blocks, root, key, value) => {
|
|
17
|
+
const shards = new ShardFetcher(blocks)
|
|
18
|
+
const rshard = await shards.get(root)
|
|
19
|
+
const path = await traverse(shards, rshard, key)
|
|
20
|
+
const target = path[path.length - 1]
|
|
21
|
+
const skey = key.slice(target.prefix.length) // key within the shard
|
|
22
|
+
|
|
23
|
+
/** @type {API.ShardEntry} */
|
|
24
|
+
let entry = [skey, value]
|
|
25
|
+
|
|
26
|
+
/** @type {API.ShardBlockView[]} */
|
|
27
|
+
const additions = []
|
|
28
|
+
|
|
29
|
+
// if the key in this shard is longer than allowed, then we need to make some
|
|
30
|
+
// intermediate shards.
|
|
31
|
+
if (skey.length > target.value.maxKeyLength) {
|
|
32
|
+
const pfxskeys = Array.from(Array(Math.ceil(skey.length / target.value.maxKeyLength)), (_, i) => {
|
|
33
|
+
const start = i * target.value.maxKeyLength
|
|
34
|
+
return {
|
|
35
|
+
prefix: target.prefix + skey.slice(0, start),
|
|
36
|
+
skey: skey.slice(start, start + target.value.maxKeyLength)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
let child = await Shard.encodeBlock(
|
|
41
|
+
Shard.withEntries([[pfxskeys[pfxskeys.length - 1].skey, value]], target.value),
|
|
42
|
+
pfxskeys[pfxskeys.length - 1].prefix
|
|
43
|
+
)
|
|
44
|
+
additions.push(child)
|
|
45
|
+
|
|
46
|
+
for (let i = pfxskeys.length - 2; i > 0; i--) {
|
|
47
|
+
child = await Shard.encodeBlock(
|
|
48
|
+
Shard.withEntries([[pfxskeys[i].skey, [child.cid]]], target.value),
|
|
49
|
+
pfxskeys[i].prefix
|
|
50
|
+
)
|
|
51
|
+
additions.push(child)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
entry = [pfxskeys[0].skey, [child.cid]]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let shard = Shard.withEntries(Shard.putEntry(target.value.entries, entry), target.value)
|
|
58
|
+
let child = await Shard.encodeBlock(shard, target.prefix)
|
|
59
|
+
|
|
60
|
+
if (child.bytes.length > shard.maxSize) {
|
|
61
|
+
const common = Shard.findCommonPrefix(shard.entries, entry[0])
|
|
62
|
+
if (!common) throw new Error('shard limit reached')
|
|
63
|
+
const { prefix, matches } = common
|
|
64
|
+
const block = await Shard.encodeBlock(
|
|
65
|
+
Shard.withEntries(
|
|
66
|
+
matches
|
|
67
|
+
.filter(([k]) => k !== prefix)
|
|
68
|
+
.map(([k, v]) => [k.slice(prefix.length), v]),
|
|
69
|
+
shard
|
|
70
|
+
),
|
|
71
|
+
target.prefix + prefix
|
|
72
|
+
)
|
|
73
|
+
additions.push(block)
|
|
74
|
+
|
|
75
|
+
/** @type {API.ShardEntryLinkValue | API.ShardEntryLinkAndValueValue} */
|
|
76
|
+
let value
|
|
77
|
+
const pfxmatch = matches.find(([k]) => k === prefix)
|
|
78
|
+
if (pfxmatch) {
|
|
79
|
+
if (Array.isArray(pfxmatch[1])) {
|
|
80
|
+
// should not happen! all entries with this prefix should have been
|
|
81
|
+
// placed within this shard already.
|
|
82
|
+
throw new Error(`expected "${prefix}" to be a shard value but found a shard link`)
|
|
83
|
+
}
|
|
84
|
+
value = [block.cid, pfxmatch[1]]
|
|
85
|
+
} else {
|
|
86
|
+
value = [block.cid]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
shard.entries = shard.entries.filter(e => matches.every(m => e[0] !== m[0]))
|
|
90
|
+
shard = Shard.withEntries(Shard.putEntry(shard.entries, [prefix, value]), shard)
|
|
91
|
+
child = await Shard.encodeBlock(shard, target.prefix)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// if no change in the target then we're done
|
|
95
|
+
if (child.cid.toString() === target.cid.toString()) {
|
|
96
|
+
return { root, additions: [], removals: [] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
additions.push(child)
|
|
100
|
+
|
|
101
|
+
// path is root -> shard, so work backwards, propagating the new shard CID
|
|
102
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
103
|
+
const parent = path[i]
|
|
104
|
+
const key = child.prefix.slice(parent.prefix.length)
|
|
105
|
+
const value = Shard.withEntries(
|
|
106
|
+
parent.value.entries.map((entry) => {
|
|
107
|
+
const [k, v] = entry
|
|
108
|
+
if (k !== key) return entry
|
|
109
|
+
if (!Array.isArray(v)) throw new Error(`"${key}" is not a shard link in: ${parent.cid}`)
|
|
110
|
+
return /** @type {API.ShardEntry} */(v[1] == null ? [k, [child.cid]] : [k, [child.cid, v[1]]])
|
|
111
|
+
}),
|
|
112
|
+
parent.value
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
child = await Shard.encodeBlock(value, parent.prefix)
|
|
116
|
+
additions.push(child)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { root: additions[additions.length - 1].cid, additions, removals: path }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the stored value for the given key from the bucket. If the key is not
|
|
124
|
+
* found, `undefined` is returned.
|
|
125
|
+
*
|
|
126
|
+
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
127
|
+
* @param {API.ShardLink} root CID of the root node of the bucket.
|
|
128
|
+
* @param {string} key The key of the value to get.
|
|
129
|
+
* @returns {Promise<API.UnknownLink | undefined>}
|
|
130
|
+
*/
|
|
131
|
+
export const get = async (blocks, root, key) => {
|
|
132
|
+
const shards = new ShardFetcher(blocks)
|
|
133
|
+
const rshard = await shards.get(root)
|
|
134
|
+
const path = await traverse(shards, rshard, key)
|
|
135
|
+
const target = path[path.length - 1]
|
|
136
|
+
const skey = key.slice(target.prefix.length) // key within the shard
|
|
137
|
+
const entry = target.value.entries.find(([k]) => k === skey)
|
|
138
|
+
if (!entry) return
|
|
139
|
+
return Array.isArray(entry[1]) ? entry[1][1] : entry[1]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Delete the value for the given key from the bucket. If the key is not found
|
|
144
|
+
* no operation occurs.
|
|
145
|
+
*
|
|
146
|
+
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
147
|
+
* @param {API.ShardLink} root CID of the root node of the bucket.
|
|
148
|
+
* @param {string} key The key of the value to delete.
|
|
149
|
+
* @returns {Promise<{ root: API.ShardLink } & API.ShardDiff>}
|
|
150
|
+
*/
|
|
151
|
+
export const del = async (blocks, root, key) => {
|
|
152
|
+
const shards = new ShardFetcher(blocks)
|
|
153
|
+
const rshard = await shards.get(root)
|
|
154
|
+
const path = await traverse(shards, rshard, key)
|
|
155
|
+
const target = path[path.length - 1]
|
|
156
|
+
const skey = key.slice(target.prefix.length) // key within the shard
|
|
157
|
+
|
|
158
|
+
const entryidx = target.value.entries.findIndex(([k]) => k === skey)
|
|
159
|
+
if (entryidx === -1) return { root, additions: [], removals: [] }
|
|
160
|
+
|
|
161
|
+
const entry = target.value.entries[entryidx]
|
|
162
|
+
// cannot delete a shard (without data)
|
|
163
|
+
if (Array.isArray(entry[1]) && entry[1][1] == null) {
|
|
164
|
+
return { root, additions: [], removals: [] }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @type {API.ShardBlockView[]} */
|
|
168
|
+
const additions = []
|
|
169
|
+
/** @type {API.ShardBlockView[]} */
|
|
170
|
+
const removals = [...path]
|
|
171
|
+
|
|
172
|
+
let shard = Shard.withEntries([...target.value.entries], target.value)
|
|
173
|
+
|
|
174
|
+
if (Array.isArray(entry[1])) {
|
|
175
|
+
// remove the value from this link+value
|
|
176
|
+
shard.entries[entryidx] = [entry[0], [entry[1][0]]]
|
|
177
|
+
} else {
|
|
178
|
+
shard.entries.splice(entryidx, 1)
|
|
179
|
+
// if now empty, remove from parent
|
|
180
|
+
while (!shard.entries.length) {
|
|
181
|
+
const child = path[path.length - 1]
|
|
182
|
+
const parent = path[path.length - 2]
|
|
183
|
+
if (!parent) break
|
|
184
|
+
path.pop()
|
|
185
|
+
shard = Shard.withEntries(
|
|
186
|
+
parent.value.entries.filter(e => {
|
|
187
|
+
if (!Array.isArray(e[1])) return true
|
|
188
|
+
return e[1][0].toString() !== child.cid.toString()
|
|
189
|
+
}),
|
|
190
|
+
parent.value
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let child = await Shard.encodeBlock(shard, path[path.length - 1].prefix)
|
|
196
|
+
additions.push(child)
|
|
197
|
+
|
|
198
|
+
// path is root -> shard, so work backwards, propagating the new shard CID
|
|
199
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
200
|
+
const parent = path[i]
|
|
201
|
+
const key = child.prefix.slice(parent.prefix.length)
|
|
202
|
+
const value = Shard.withEntries(
|
|
203
|
+
parent.value.entries.map((entry) => {
|
|
204
|
+
const [k, v] = entry
|
|
205
|
+
if (k !== key) return entry
|
|
206
|
+
if (!Array.isArray(v)) throw new Error(`"${key}" is not a shard link in: ${parent.cid}`)
|
|
207
|
+
return /** @type {API.ShardEntry} */(v[1] == null ? [k, [child.cid]] : [k, [child.cid, v[1]]])
|
|
208
|
+
}),
|
|
209
|
+
parent.value
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
child = await Shard.encodeBlock(value, parent.prefix)
|
|
213
|
+
additions.push(child)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { root: additions[additions.length - 1].cid, additions, removals }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* List entries in the bucket.
|
|
221
|
+
*
|
|
222
|
+
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
223
|
+
* @param {API.ShardLink} root CID of the root node of the bucket.
|
|
224
|
+
* @param {object} [options]
|
|
225
|
+
* @param {string} [options.prefix]
|
|
226
|
+
* @returns {AsyncIterableIterator<API.ShardValueEntry>}
|
|
227
|
+
*/
|
|
228
|
+
export const entries = async function * (blocks, root, options = {}) {
|
|
229
|
+
const { prefix } = options
|
|
230
|
+
const shards = new ShardFetcher(blocks)
|
|
231
|
+
const rshard = await shards.get(root)
|
|
232
|
+
|
|
233
|
+
yield * (
|
|
234
|
+
/** @returns {AsyncIterableIterator<API.ShardValueEntry>} */
|
|
235
|
+
async function * ents (shard) {
|
|
236
|
+
for (const entry of shard.value.entries) {
|
|
237
|
+
const key = shard.prefix + entry[0]
|
|
238
|
+
|
|
239
|
+
if (Array.isArray(entry[1])) {
|
|
240
|
+
if (entry[1][1]) {
|
|
241
|
+
if (!prefix || (prefix && key.startsWith(prefix))) {
|
|
242
|
+
yield [key, entry[1][1]]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (prefix) {
|
|
247
|
+
if (prefix.length <= key.length && !key.startsWith(prefix)) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
if (prefix.length > key.length && !prefix.startsWith(key)) {
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
yield * ents(await shards.get(entry[1][0], key))
|
|
255
|
+
} else {
|
|
256
|
+
if (prefix && !key.startsWith(prefix)) {
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
yield [key, entry[1]]
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
)(rshard)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Traverse from the passed shard block to the target shard block using the
|
|
268
|
+
* passed key. All traversed shards are returned, starting with the passed
|
|
269
|
+
* shard and ending with the target.
|
|
270
|
+
*
|
|
271
|
+
* @param {ShardFetcher} shards
|
|
272
|
+
* @param {API.ShardBlockView} shard
|
|
273
|
+
* @param {string} key
|
|
274
|
+
* @returns {Promise<[API.ShardBlockView, ...Array<API.ShardBlockView>]>}
|
|
275
|
+
*/
|
|
276
|
+
const traverse = async (shards, shard, key) => {
|
|
277
|
+
for (const [k, v] of shard.value.entries) {
|
|
278
|
+
if (key === k) return [shard]
|
|
279
|
+
if (key.startsWith(k) && Array.isArray(v)) {
|
|
280
|
+
const path = await traverse(shards, await shards.get(v[0], shard.prefix + k), key.slice(k.length))
|
|
281
|
+
return [shard, ...path]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return [shard]
|
|
285
|
+
}
|
package/src/merge.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// eslint-disable-next-line no-unused-vars
|
|
2
|
+
import * as API from './api.js'
|
|
3
|
+
import { difference } from './diff.js'
|
|
4
|
+
import { put, del } from './index.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
8
|
+
* @param {API.ShardLink} base Merge base. Common parent of target DAGs.
|
|
9
|
+
* @param {API.ShardLink[]} targets Target DAGs to merge.
|
|
10
|
+
* @returns {Promise<{ root: API.ShardLink } & API.ShardDiff>}
|
|
11
|
+
*/
|
|
12
|
+
export const merge = async (blocks, base, targets) => {
|
|
13
|
+
const diffs = await Promise.all(targets.map(t => difference(blocks, base, t)))
|
|
14
|
+
const additions = new Map()
|
|
15
|
+
const removals = new Map()
|
|
16
|
+
/** @type {API.BlockFetcher} */
|
|
17
|
+
const fetcher = { get: cid => additions.get(cid.toString()) ?? blocks.get(cid) }
|
|
18
|
+
|
|
19
|
+
let root = base
|
|
20
|
+
for (const { keys } of diffs) {
|
|
21
|
+
for (const [k, v] of keys) {
|
|
22
|
+
let res
|
|
23
|
+
if (v[1] == null) {
|
|
24
|
+
res = await del(fetcher, root, k)
|
|
25
|
+
} else {
|
|
26
|
+
res = await put(fetcher, root, k, v[1])
|
|
27
|
+
}
|
|
28
|
+
for (const blk of res.removals) {
|
|
29
|
+
if (additions.has(blk.cid.toString())) {
|
|
30
|
+
additions.delete(blk.cid.toString())
|
|
31
|
+
} else {
|
|
32
|
+
removals.set(blk.cid.toString(), blk)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const blk of res.additions) {
|
|
36
|
+
additions.set(blk.cid.toString(), blk)
|
|
37
|
+
}
|
|
38
|
+
root = res.root
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { root, additions: [...additions.values()], removals: [...removals.values()] }
|
|
43
|
+
}
|
package/src/shard.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as Link from 'multiformats/link'
|
|
2
|
+
import { Block, encode, decode } from 'multiformats/block'
|
|
3
|
+
import { sha256 } from 'multiformats/hashes/sha2'
|
|
4
|
+
import * as dagCBOR from '@ipld/dag-cbor'
|
|
5
|
+
import { tokensToLength } from 'cborg/length'
|
|
6
|
+
import { Token, Type } from 'cborg'
|
|
7
|
+
// eslint-disable-next-line no-unused-vars
|
|
8
|
+
import * as API from './api.js'
|
|
9
|
+
|
|
10
|
+
export const MaxKeyLength = 64
|
|
11
|
+
export const MaxShardSize = 512 * 1024
|
|
12
|
+
|
|
13
|
+
const CID_TAG = new Token(Type.tag, 42)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @extends {Block<API.Shard, typeof dagCBOR.code, typeof sha256.code, 1>}
|
|
17
|
+
* @implements {API.ShardBlockView}
|
|
18
|
+
*/
|
|
19
|
+
export class ShardBlock extends Block {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} config
|
|
22
|
+
* @param {API.ShardLink} config.cid
|
|
23
|
+
* @param {API.Shard} config.value
|
|
24
|
+
* @param {Uint8Array} config.bytes
|
|
25
|
+
* @param {string} config.prefix
|
|
26
|
+
*/
|
|
27
|
+
constructor ({ cid, value, bytes, prefix }) {
|
|
28
|
+
// @ts-expect-error
|
|
29
|
+
super({ cid, value, bytes })
|
|
30
|
+
this.prefix = prefix
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {API.ShardOptions} [options] */
|
|
34
|
+
static create (options) {
|
|
35
|
+
return encodeBlock(create(options))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {API.ShardOptions} [options]
|
|
41
|
+
* @returns {API.Shard}
|
|
42
|
+
*/
|
|
43
|
+
export const create = (options) => ({ entries: [], ...configure(options) })
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {API.ShardOptions} [options]
|
|
47
|
+
* @returns {API.ShardConfig}
|
|
48
|
+
*/
|
|
49
|
+
export const configure = (options) => ({
|
|
50
|
+
maxSize: options?.maxSize ?? MaxShardSize,
|
|
51
|
+
maxKeyLength: options?.maxKeyLength ?? MaxKeyLength
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {API.ShardEntry[]} entries
|
|
56
|
+
* @param {API.ShardOptions} [options]
|
|
57
|
+
* @returns {API.Shard}
|
|
58
|
+
*/
|
|
59
|
+
export const withEntries = (entries, options) => ({ ...create(options), entries })
|
|
60
|
+
|
|
61
|
+
/** @type {WeakMap<Uint8Array, API.ShardBlockView>} */
|
|
62
|
+
const decodeCache = new WeakMap()
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {API.Shard} value
|
|
66
|
+
* @param {string} [prefix]
|
|
67
|
+
* @returns {Promise<API.ShardBlockView>}
|
|
68
|
+
*/
|
|
69
|
+
export const encodeBlock = async (value, prefix) => {
|
|
70
|
+
const { cid, bytes } = await encode({ value, codec: dagCBOR, hasher: sha256 })
|
|
71
|
+
const block = new ShardBlock({ cid, value, bytes, prefix: prefix ?? '' })
|
|
72
|
+
decodeCache.set(block.bytes, block)
|
|
73
|
+
return block
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {Uint8Array} bytes
|
|
78
|
+
* @param {string} [prefix]
|
|
79
|
+
* @returns {Promise<API.ShardBlockView>}
|
|
80
|
+
*/
|
|
81
|
+
export const decodeBlock = async (bytes, prefix) => {
|
|
82
|
+
const block = decodeCache.get(bytes)
|
|
83
|
+
if (block) return block
|
|
84
|
+
const { cid, value } = await decode({ bytes, codec: dagCBOR, hasher: sha256 })
|
|
85
|
+
if (!isShard(value)) throw new Error(`invalid shard: ${cid}`)
|
|
86
|
+
return new ShardBlock({ cid, value, bytes, prefix: prefix ?? '' })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {any} value
|
|
91
|
+
* @returns {value is API.Shard}
|
|
92
|
+
*/
|
|
93
|
+
export const isShard = (value) =>
|
|
94
|
+
value != null &&
|
|
95
|
+
typeof value === 'object' &&
|
|
96
|
+
Array.isArray(value.entries) &&
|
|
97
|
+
typeof value.maxSize === 'number' &&
|
|
98
|
+
typeof value.maxKeyLength === 'number'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {any} value
|
|
102
|
+
* @returns {value is API.ShardLink}
|
|
103
|
+
*/
|
|
104
|
+
export const isShardLink = (value) =>
|
|
105
|
+
Link.isLink(value) &&
|
|
106
|
+
value.code === dagCBOR.code
|
|
107
|
+
|
|
108
|
+
export class ShardFetcher {
|
|
109
|
+
/** @param {API.BlockFetcher} blocks */
|
|
110
|
+
constructor (blocks) {
|
|
111
|
+
this._blocks = blocks
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {API.ShardLink} link
|
|
116
|
+
* @param {string} [prefix]
|
|
117
|
+
* @returns {Promise<API.ShardBlockView>}
|
|
118
|
+
*/
|
|
119
|
+
async get (link, prefix = '') {
|
|
120
|
+
const block = await this._blocks.get(link)
|
|
121
|
+
if (!block) throw new Error(`missing block: ${link}`)
|
|
122
|
+
return decodeBlock(block.bytes, prefix)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {API.ShardEntry[]} target Entries to insert into.
|
|
128
|
+
* @param {API.ShardEntry} newEntry
|
|
129
|
+
* @returns {API.ShardEntry[]}
|
|
130
|
+
*/
|
|
131
|
+
export const putEntry = (target, newEntry) => {
|
|
132
|
+
/** @type {API.ShardEntry[]} */
|
|
133
|
+
const entries = []
|
|
134
|
+
|
|
135
|
+
for (const [i, entry] of target.entries()) {
|
|
136
|
+
const [k, v] = entry
|
|
137
|
+
if (newEntry[0] === k) {
|
|
138
|
+
// if new value is link to shard...
|
|
139
|
+
if (Array.isArray(newEntry[1])) {
|
|
140
|
+
// and old value is link to shard
|
|
141
|
+
// and old value is _also_ link to data
|
|
142
|
+
// and new value does not have link to data
|
|
143
|
+
// then preserve old data
|
|
144
|
+
if (Array.isArray(v) && v[1] != null && newEntry[1][1] == null) {
|
|
145
|
+
entries.push([k, [newEntry[1][0], v[1]]])
|
|
146
|
+
} else {
|
|
147
|
+
entries.push(newEntry)
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// shard as well as value?
|
|
151
|
+
if (Array.isArray(v)) {
|
|
152
|
+
entries.push([k, [v[0], newEntry[1]]])
|
|
153
|
+
} else {
|
|
154
|
+
entries.push(newEntry)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (let j = i + 1; j < target.length; j++) {
|
|
158
|
+
entries.push(target[j])
|
|
159
|
+
}
|
|
160
|
+
return entries
|
|
161
|
+
}
|
|
162
|
+
if (i === 0 && newEntry[0] < k) {
|
|
163
|
+
entries.push(newEntry)
|
|
164
|
+
for (let j = i; j < target.length; j++) {
|
|
165
|
+
entries.push(target[j])
|
|
166
|
+
}
|
|
167
|
+
return entries
|
|
168
|
+
}
|
|
169
|
+
if (i > 0 && newEntry[0] > target[i - 1][0] && newEntry[0] < k) {
|
|
170
|
+
entries.push(newEntry)
|
|
171
|
+
for (let j = i; j < target.length; j++) {
|
|
172
|
+
entries.push(target[j])
|
|
173
|
+
}
|
|
174
|
+
return entries
|
|
175
|
+
}
|
|
176
|
+
entries.push(entry)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
entries.push(newEntry)
|
|
180
|
+
return entries
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {API.ShardEntry[]} entries
|
|
185
|
+
* @param {string} skey Shard key to use as a base.
|
|
186
|
+
*/
|
|
187
|
+
export const findCommonPrefix = (entries, skey) => {
|
|
188
|
+
const startidx = entries.findIndex(([k]) => skey === k)
|
|
189
|
+
if (startidx === -1) throw new Error(`key not found in shard: ${skey}`)
|
|
190
|
+
let i = startidx
|
|
191
|
+
/** @type {string} */
|
|
192
|
+
let pfx
|
|
193
|
+
while (true) {
|
|
194
|
+
pfx = entries[i][0].slice(0, -1)
|
|
195
|
+
if (pfx.length) {
|
|
196
|
+
while (true) {
|
|
197
|
+
const matches = entries.filter(entry => entry[0].startsWith(pfx))
|
|
198
|
+
if (matches.length > 1) return { prefix: pfx, matches }
|
|
199
|
+
pfx = pfx.slice(0, -1)
|
|
200
|
+
if (!pfx.length) break
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
i++
|
|
204
|
+
if (i >= entries.length) {
|
|
205
|
+
i = 0
|
|
206
|
+
}
|
|
207
|
+
if (i === startidx) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {API.Shard} shard */
|
|
214
|
+
export const encodedLength = (shard) => {
|
|
215
|
+
let entriesLength = 0
|
|
216
|
+
for (const entry of shard.entries) {
|
|
217
|
+
entriesLength += entryEncodedLength(entry)
|
|
218
|
+
}
|
|
219
|
+
const tokens = [
|
|
220
|
+
new Token(Type.map, 3),
|
|
221
|
+
new Token(Type.string, 'entries'),
|
|
222
|
+
new Token(Type.array, shard.entries.length),
|
|
223
|
+
new Token(Type.string, 'maxKeyLength'),
|
|
224
|
+
new Token(Type.uint, shard.maxKeyLength),
|
|
225
|
+
new Token(Type.string, 'maxSize'),
|
|
226
|
+
new Token(Type.uint, shard.maxSize)
|
|
227
|
+
]
|
|
228
|
+
return tokensToLength(tokens) + entriesLength
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** @param {API.ShardEntry} entry */
|
|
232
|
+
const entryEncodedLength = entry => {
|
|
233
|
+
const tokens = [
|
|
234
|
+
new Token(Type.array, entry.length),
|
|
235
|
+
new Token(Type.string, entry[0])
|
|
236
|
+
]
|
|
237
|
+
if (Array.isArray(entry[1])) {
|
|
238
|
+
tokens.push(new Token(Type.array, entry[1].length))
|
|
239
|
+
for (const link of entry[1]) {
|
|
240
|
+
tokens.push(CID_TAG)
|
|
241
|
+
tokens.push(new Token(Type.bytes, { length: link.byteLength + 1 }))
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
tokens.push(CID_TAG)
|
|
245
|
+
tokens.push(new Token(Type.bytes, { length: entry[1].byteLength + 1 }))
|
|
246
|
+
}
|
|
247
|
+
return tokensToLength(tokens)
|
|
248
|
+
}
|