@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.
Files changed (55) hide show
  1. package/LICENSE.md +232 -0
  2. package/README.md +84 -0
  3. package/cli.js +259 -0
  4. package/dist/src/api.d.ts +33 -0
  5. package/dist/src/api.d.ts.map +1 -0
  6. package/dist/src/batch/api.d.ts +33 -0
  7. package/dist/src/batch/api.d.ts.map +1 -0
  8. package/dist/src/batch/index.d.ts +74 -0
  9. package/dist/src/batch/index.d.ts.map +1 -0
  10. package/dist/src/batch/shard.d.ts +3 -0
  11. package/dist/src/batch/shard.d.ts.map +1 -0
  12. package/dist/src/block.d.ts +35 -0
  13. package/dist/src/block.d.ts.map +1 -0
  14. package/dist/src/clock/api.d.ts +10 -0
  15. package/dist/src/clock/api.d.ts.map +1 -0
  16. package/dist/src/clock/index.d.ts +48 -0
  17. package/dist/src/clock/index.d.ts.map +1 -0
  18. package/dist/src/crdt/api.d.ts +26 -0
  19. package/dist/src/crdt/api.d.ts.map +1 -0
  20. package/dist/src/crdt/batch/api.d.ts +11 -0
  21. package/dist/src/crdt/batch/api.d.ts.map +1 -0
  22. package/dist/src/crdt/batch/index.d.ts +5 -0
  23. package/dist/src/crdt/batch/index.d.ts.map +1 -0
  24. package/dist/src/crdt/index.d.ts +11 -0
  25. package/dist/src/crdt/index.d.ts.map +1 -0
  26. package/dist/src/diff.d.ts +13 -0
  27. package/dist/src/diff.d.ts.map +1 -0
  28. package/dist/src/index.d.ts +12 -0
  29. package/dist/src/index.d.ts.map +1 -0
  30. package/dist/src/merge.d.ts +5 -0
  31. package/dist/src/merge.d.ts.map +1 -0
  32. package/dist/src/shard.d.ts +51 -0
  33. package/dist/src/shard.d.ts.map +1 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -0
  35. package/package.json +174 -0
  36. package/src/api.js +1 -0
  37. package/src/api.ts +47 -0
  38. package/src/batch/api.js +1 -0
  39. package/src/batch/api.ts +61 -0
  40. package/src/batch/index.js +245 -0
  41. package/src/batch/shard.js +14 -0
  42. package/src/block.js +75 -0
  43. package/src/clock/api.js +1 -0
  44. package/src/clock/api.ts +12 -0
  45. package/src/clock/index.js +179 -0
  46. package/src/crdt/api.js +1 -0
  47. package/src/crdt/api.ts +33 -0
  48. package/src/crdt/batch/api.js +1 -0
  49. package/src/crdt/batch/api.ts +31 -0
  50. package/src/crdt/batch/index.js +156 -0
  51. package/src/crdt/index.js +355 -0
  52. package/src/diff.js +151 -0
  53. package/src/index.js +285 -0
  54. package/src/merge.js +43 -0
  55. 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
+ }