@fireproof/vendor 2.0.0 → 2.0.2

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.
@@ -0,0 +1,356 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ import * as API from './api.js';
3
+ import { ShardFetcher, isPrintableASCII } from './shard.js';
4
+ import * as Shard from './shard.js';
5
+ /**
6
+ * Put a value (a CID) for the given key. If the key exists it's value is
7
+ * overwritten.
8
+ *
9
+ * @param {API.BlockFetcher} blocks Bucket block storage.
10
+ * @param {API.ShardLink} root CID of the root node of the bucket.
11
+ * @param {string} key The key of the value to put.
12
+ * @param {API.UnknownLink} value The value to put.
13
+ * @returns {Promise<{ root: API.ShardLink } & API.ShardDiff>}
14
+ */
15
+ export const put = async (blocks, root, key, value) => {
16
+ const shards = new ShardFetcher(blocks);
17
+ const rshard = await shards.get(root);
18
+ if (rshard.value.keyChars !== Shard.KeyCharsASCII) {
19
+ throw new Error(`unsupported key character set: ${rshard.value.keyChars}`);
20
+ }
21
+ if (!isPrintableASCII(key)) {
22
+ throw new Error('key contains non-ASCII characters');
23
+ }
24
+ // ensure utf8 encoded key is smaller than max
25
+ if (new TextEncoder().encode(key).length > rshard.value.maxKeySize) {
26
+ throw new Error(`UTF-8 encoded key exceeds max size of ${rshard.value.maxKeySize} bytes`);
27
+ }
28
+ const path = await traverse(shards, rshard, key);
29
+ const target = path[path.length - 1];
30
+ const skey = key.slice(target.value.prefix.length); // key within the shard
31
+ /** @type {API.ShardEntry} */
32
+ let entry = [skey, value];
33
+ let targetEntries = [...target.value.entries];
34
+ /** @type {API.ShardBlockView[]} */
35
+ const additions = [];
36
+ for (const [i, e] of targetEntries.entries()) {
37
+ const [k, v] = e;
38
+ // is this just a replace?
39
+ if (k === skey)
40
+ break;
41
+ // do we need to shard this entry?
42
+ const shortest = k.length < skey.length ? k : skey;
43
+ const other = shortest === k ? skey : k;
44
+ let common = '';
45
+ for (const char of shortest) {
46
+ const next = common + char;
47
+ if (!other.startsWith(next))
48
+ break;
49
+ common = next;
50
+ }
51
+ if (common.length) {
52
+ /** @type {API.ShardEntry[]} */
53
+ let entries = [];
54
+ // if the existing entry key or new key is equal to the common prefix,
55
+ // then the existing value / new value needs to persist in the parent
56
+ // shard. Otherwise they persist in this new shard.
57
+ if (common !== skey) {
58
+ entries = Shard.putEntry(entries, [skey.slice(common.length), value]);
59
+ }
60
+ if (common !== k) {
61
+ entries = Shard.putEntry(entries, [k.slice(common.length), v]);
62
+ }
63
+ let child = await Shard.encodeBlock(Shard.withEntries(entries, { ...target.value, prefix: target.value.prefix + common }));
64
+ additions.push(child);
65
+ // need to spread as access by index does not consider utf-16 surrogates
66
+ const commonChars = [...common];
67
+ // create parent shards for each character of the common prefix
68
+ for (let i = commonChars.length - 1; i > 0; i--) {
69
+ const parentConfig = { ...target.value, prefix: target.value.prefix + commonChars.slice(0, i).join('') };
70
+ /** @type {API.ShardEntryLinkValue | API.ShardEntryValueValue | API.ShardEntryLinkAndValueValue} */
71
+ let parentValue;
72
+ // if the first iteration and the existing entry key is equal to the
73
+ // common prefix, then existing value needs to persist in this parent
74
+ if (i === commonChars.length - 1 && common === k) {
75
+ if (Array.isArray(v))
76
+ throw new Error('found a shard link when expecting a value');
77
+ parentValue = [child.cid, v];
78
+ }
79
+ else if (i === commonChars.length - 1 && common === skey) {
80
+ parentValue = [child.cid, value];
81
+ }
82
+ else {
83
+ parentValue = [child.cid];
84
+ }
85
+ const parent = await Shard.encodeBlock(Shard.withEntries([[commonChars[i], parentValue]], parentConfig));
86
+ additions.push(parent);
87
+ child = parent;
88
+ }
89
+ // remove the sharded entry
90
+ targetEntries.splice(i, 1);
91
+ // create the entry that will be added to target
92
+ if (commonChars.length === 1 && common === k) {
93
+ if (Array.isArray(v))
94
+ throw new Error('found a shard link when expecting a value');
95
+ entry = [commonChars[0], [child.cid, v]];
96
+ }
97
+ else if (commonChars.length === 1 && common === skey) {
98
+ entry = [commonChars[0], [child.cid, value]];
99
+ }
100
+ else {
101
+ entry = [commonChars[0], [child.cid]];
102
+ }
103
+ break;
104
+ }
105
+ }
106
+ const shard = Shard.withEntries(Shard.putEntry(targetEntries, entry), target.value);
107
+ let child = await Shard.encodeBlock(shard);
108
+ // if no change in the target then we're done
109
+ if (child.cid.toString() === target.cid.toString()) {
110
+ return { root, additions: [], removals: [] };
111
+ }
112
+ additions.push(child);
113
+ // path is root -> target, so work backwards, propagating the new shard CID
114
+ for (let i = path.length - 2; i >= 0; i--) {
115
+ const parent = path[i];
116
+ const key = child.value.prefix.slice(parent.value.prefix.length);
117
+ const value = Shard.withEntries(parent.value.entries.map((entry) => {
118
+ const [k, v] = entry;
119
+ if (k !== key)
120
+ return entry;
121
+ if (!Array.isArray(v))
122
+ throw new Error(`"${key}" is not a shard link in: ${parent.cid}`);
123
+ return /** @type {API.ShardEntry} */ (v[1] == null ? [k, [child.cid]] : [k, [child.cid, v[1]]]);
124
+ }), parent.value);
125
+ child = await Shard.encodeBlock(value);
126
+ additions.push(child);
127
+ }
128
+ return { root: additions[additions.length - 1].cid, additions, removals: path };
129
+ };
130
+ /**
131
+ * Get the stored value for the given key from the bucket. If the key is not
132
+ * found, `undefined` is returned.
133
+ *
134
+ * @param {API.BlockFetcher} blocks Bucket block storage.
135
+ * @param {API.ShardLink} root CID of the root node of the bucket.
136
+ * @param {string} key The key of the value to get.
137
+ * @returns {Promise<API.UnknownLink | undefined>}
138
+ */
139
+ export const get = async (blocks, root, key) => {
140
+ const shards = new ShardFetcher(blocks);
141
+ const rshard = await shards.get(root);
142
+ const path = await traverse(shards, rshard, key);
143
+ const target = path[path.length - 1];
144
+ const skey = key.slice(target.value.prefix.length); // key within the shard
145
+ const entry = target.value.entries.find(([k]) => k === skey);
146
+ if (!entry)
147
+ return;
148
+ return Array.isArray(entry[1]) ? entry[1][1] : entry[1];
149
+ };
150
+ /**
151
+ * Delete the value for the given key from the bucket. If the key is not found
152
+ * no operation occurs.
153
+ *
154
+ * @param {API.BlockFetcher} blocks Bucket block storage.
155
+ * @param {API.ShardLink} root CID of the root node of the bucket.
156
+ * @param {string} key The key of the value to delete.
157
+ * @returns {Promise<{ root: API.ShardLink } & API.ShardDiff>}
158
+ */
159
+ export const del = async (blocks, root, key) => {
160
+ const shards = new ShardFetcher(blocks);
161
+ const rshard = await shards.get(root);
162
+ const path = await traverse(shards, rshard, key);
163
+ const target = path[path.length - 1];
164
+ const skey = key.slice(target.value.prefix.length); // key within the shard
165
+ const entryidx = target.value.entries.findIndex(([k]) => k === skey);
166
+ if (entryidx === -1)
167
+ return { root, additions: [], removals: [] };
168
+ const entry = target.value.entries[entryidx];
169
+ // cannot delete a shard (without data)
170
+ if (Array.isArray(entry[1]) && entry[1][1] == null) {
171
+ return { root, additions: [], removals: [] };
172
+ }
173
+ /** @type {API.ShardBlockView[]} */
174
+ const additions = [];
175
+ /** @type {API.ShardBlockView[]} */
176
+ const removals = [...path];
177
+ let shard = Shard.withEntries([...target.value.entries], target.value);
178
+ if (Array.isArray(entry[1])) {
179
+ // remove the value from this link+value
180
+ shard.entries[entryidx] = [entry[0], [entry[1][0]]];
181
+ }
182
+ else {
183
+ shard.entries.splice(entryidx, 1);
184
+ // if now empty, remove from parent
185
+ while (!shard.entries.length) {
186
+ const child = path[path.length - 1];
187
+ const parent = path[path.length - 2];
188
+ if (!parent)
189
+ break;
190
+ path.pop();
191
+ shard = Shard.withEntries(parent.value.entries.filter(e => {
192
+ if (!Array.isArray(e[1]))
193
+ return true;
194
+ return e[1][0].toString() !== child.cid.toString();
195
+ }), parent.value);
196
+ }
197
+ }
198
+ let child = await Shard.encodeBlock(shard);
199
+ additions.push(child);
200
+ // path is root -> shard, so work backwards, propagating the new shard CID
201
+ for (let i = path.length - 2; i >= 0; i--) {
202
+ const parent = path[i];
203
+ const key = child.value.prefix.slice(parent.value.prefix.length);
204
+ const value = Shard.withEntries(parent.value.entries.map((entry) => {
205
+ const [k, v] = entry;
206
+ if (k !== key)
207
+ return entry;
208
+ if (!Array.isArray(v))
209
+ throw new Error(`"${key}" is not a shard link in: ${parent.cid}`);
210
+ return /** @type {API.ShardEntry} */ (v[1] == null ? [k, [child.cid]] : [k, [child.cid, v[1]]]);
211
+ }), parent.value);
212
+ child = await Shard.encodeBlock(value);
213
+ additions.push(child);
214
+ }
215
+ return { root: additions[additions.length - 1].cid, additions, removals };
216
+ };
217
+ /**
218
+ * @param {API.EntriesOptions} [options]
219
+ * @returns {options is API.KeyPrefixOption}
220
+ */
221
+ const isKeyPrefixOption = options => {
222
+ const opts = options ?? {};
223
+ return 'prefix' in opts && Boolean(opts.prefix);
224
+ };
225
+ /**
226
+ * @param {API.EntriesOptions} [options]
227
+ * @returns {options is API.KeyRangeOption}
228
+ */
229
+ const isKeyRangeOption = options => {
230
+ const opts = options ?? {};
231
+ return ('gt' in opts && Boolean(opts.gt)) || ('gte' in opts && Boolean(opts.gte)) || ('lt' in opts && Boolean(opts.lt)) || ('lte' in opts && Boolean(opts.lte));
232
+ };
233
+ /**
234
+ * @param {API.KeyRangeOption} options
235
+ * @returns {options is API.KeyLowerBoundRangeOption}
236
+ */
237
+ const isKeyLowerBoundRangeOption = options => ('gt' in options && Boolean(options.gt)) || ('gte' in options && Boolean(options.gte));
238
+ /**
239
+ * @param {API.KeyLowerBoundRangeOption} options
240
+ * @returns {options is API.KeyLowerBoundRangeInclusiveOption}
241
+ */
242
+ const isKeyLowerBoundRangeInclusiveOption = options => 'gte' in options && Boolean(options.gte);
243
+ /**
244
+ * @param {API.KeyLowerBoundRangeOption} options
245
+ * @returns {options is API.KeyLowerBoundRangeExclusiveOption}
246
+ */
247
+ const isKeyLowerBoundRangeExclusiveOption = options => 'gt' in options && Boolean(options.gt);
248
+ /**
249
+ * @param {API.KeyRangeOption} options
250
+ * @returns {options is API.KeyUpperBoundRangeOption}
251
+ */
252
+ const isKeyUpperBoundRangeOption = options => ('lt' in options && Boolean(options.lt)) || ('lte' in options && Boolean(options.lte));
253
+ /**
254
+ * @param {API.KeyUpperBoundRangeOption} options
255
+ * @returns {options is API.KeyUpperBoundRangeInclusiveOption}
256
+ */
257
+ const isKeyUpperBoundRangeInclusiveOption = options => 'lte' in options && Boolean(options.lte);
258
+ /**
259
+ * @param {API.KeyUpperBoundRangeOption} options
260
+ * @returns {options is API.KeyUpperBoundRangeExclusiveOption}
261
+ */
262
+ const isKeyUpperBoundRangeExclusiveOption = options => 'lt' in options && Boolean(options.lt);
263
+ /**
264
+ * List entries in the bucket.
265
+ *
266
+ * @param {API.BlockFetcher} blocks Bucket block storage.
267
+ * @param {API.ShardLink} root CID of the root node of the bucket.
268
+ * @param {API.EntriesOptions} [options]
269
+ * @returns {AsyncIterableIterator<API.ShardValueEntry>}
270
+ */
271
+ export const entries = async function* (blocks, root, options) {
272
+ const hasKeyPrefix = isKeyPrefixOption(options);
273
+ const hasKeyRange = isKeyRangeOption(options);
274
+ const hasKeyLowerBoundRange = hasKeyRange && isKeyLowerBoundRangeOption(options);
275
+ const hasKeyLowerBoundRangeInclusive = hasKeyLowerBoundRange && isKeyLowerBoundRangeInclusiveOption(options);
276
+ const hasKeyLowerBoundRangeExclusive = hasKeyLowerBoundRange && isKeyLowerBoundRangeExclusiveOption(options);
277
+ const hasKeyUpperBoundRange = hasKeyRange && isKeyUpperBoundRangeOption(options);
278
+ const hasKeyUpperBoundRangeInclusive = hasKeyUpperBoundRange && isKeyUpperBoundRangeInclusiveOption(options);
279
+ const hasKeyUpperBoundRangeExclusive = hasKeyUpperBoundRange && isKeyUpperBoundRangeExclusiveOption(options);
280
+ const hasKeyUpperAndLowerBoundRange = hasKeyLowerBoundRange && hasKeyUpperBoundRange;
281
+ const shards = new ShardFetcher(blocks);
282
+ const rshard = await shards.get(root);
283
+ yield* (
284
+ /** @returns {AsyncIterableIterator<API.ShardValueEntry>} */
285
+ (async function* ents(shard) {
286
+ for (const entry of shard.value.entries) {
287
+ const key = shard.value.prefix + entry[0];
288
+ // if array, this is a link to a shard
289
+ if (Array.isArray(entry[1])) {
290
+ if (entry[1][1]) {
291
+ if ((hasKeyPrefix && key.startsWith(options.prefix)) ||
292
+ (hasKeyUpperAndLowerBoundRange && (((hasKeyLowerBoundRangeExclusive && key > options.gt) || (hasKeyLowerBoundRangeInclusive && key >= options.gte)) &&
293
+ ((hasKeyUpperBoundRangeExclusive && key < options.lt) || (hasKeyUpperBoundRangeInclusive && key <= options.lte)))) ||
294
+ (hasKeyLowerBoundRangeExclusive && key > options.gt) ||
295
+ (hasKeyLowerBoundRangeInclusive && key >= options.gte) ||
296
+ (hasKeyUpperBoundRangeExclusive && key < options.lt) ||
297
+ (hasKeyUpperBoundRangeInclusive && key <= options.lte) ||
298
+ (!hasKeyPrefix && !hasKeyRange)) {
299
+ yield [key, entry[1][1]];
300
+ }
301
+ }
302
+ if (hasKeyPrefix) {
303
+ if (options.prefix.length <= key.length && !key.startsWith(options.prefix)) {
304
+ continue;
305
+ }
306
+ if (options.prefix.length > key.length && !options.prefix.startsWith(key)) {
307
+ continue;
308
+ }
309
+ }
310
+ else if ((hasKeyLowerBoundRangeExclusive && (trunc(key, Math.min(key.length, options.gt.length)) < trunc(options.gt, Math.min(key.length, options.gt.length)))) ||
311
+ (hasKeyLowerBoundRangeInclusive && (trunc(key, Math.min(key.length, options.gte.length)) < trunc(options.gte, Math.min(key.length, options.gte.length)))) ||
312
+ (hasKeyUpperBoundRangeExclusive && (trunc(key, Math.min(key.length, options.lt.length)) > trunc(options.lt, Math.min(key.length, options.lt.length)))) ||
313
+ (hasKeyUpperBoundRangeInclusive && (trunc(key, Math.min(key.length, options.lte.length)) > trunc(options.lte, Math.min(key.length, options.lte.length))))) {
314
+ continue;
315
+ }
316
+ yield* ents(await shards.get(entry[1][0]));
317
+ }
318
+ else {
319
+ if ((hasKeyPrefix && key.startsWith(options.prefix)) ||
320
+ (hasKeyRange && hasKeyUpperAndLowerBoundRange && (((hasKeyLowerBoundRangeExclusive && key > options.gt) || (hasKeyLowerBoundRangeInclusive && key >= options.gte)) &&
321
+ ((hasKeyUpperBoundRangeExclusive && key < options.lt) || (hasKeyUpperBoundRangeInclusive && key <= options.lte)))) ||
322
+ (hasKeyRange && !hasKeyUpperAndLowerBoundRange && ((hasKeyLowerBoundRangeExclusive && key > options.gt) || (hasKeyLowerBoundRangeInclusive && key >= options.gte) ||
323
+ (hasKeyUpperBoundRangeExclusive && key < options.lt) || (hasKeyUpperBoundRangeInclusive && key <= options.lte))) ||
324
+ (!hasKeyPrefix && !hasKeyRange)) {
325
+ yield [key, entry[1]];
326
+ }
327
+ }
328
+ }
329
+ }))(rshard);
330
+ };
331
+ /**
332
+ * @param {string} str
333
+ * @param {number} len
334
+ */
335
+ const trunc = (str, len) => str.length <= len ? str : str.slice(0, len);
336
+ /**
337
+ * Traverse from the passed shard block to the target shard block using the
338
+ * passed key. All traversed shards are returned, starting with the passed
339
+ * shard and ending with the target.
340
+ *
341
+ * @param {ShardFetcher} shards
342
+ * @param {API.ShardBlockView} shard
343
+ * @param {string} key
344
+ * @returns {Promise<[API.ShardBlockView, ...Array<API.ShardBlockView>]>}
345
+ */
346
+ const traverse = async (shards, shard, key) => {
347
+ for (const [k, v] of shard.value.entries) {
348
+ if (key === k)
349
+ return [shard];
350
+ if (key.startsWith(k) && Array.isArray(v)) {
351
+ const path = await traverse(shards, await shards.get(v[0]), key.slice(k.length));
352
+ return [shard, ...path];
353
+ }
354
+ }
355
+ return [shard];
356
+ };
@@ -0,0 +1,42 @@
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
+ * @param {API.BlockFetcher} blocks Bucket block storage.
7
+ * @param {API.ShardLink} base Merge base. Common parent of target DAGs.
8
+ * @param {API.ShardLink[]} targets Target DAGs to merge.
9
+ * @returns {Promise<{ root: API.ShardLink } & API.ShardDiff>}
10
+ */
11
+ export const merge = async (blocks, base, targets) => {
12
+ const diffs = await Promise.all(targets.map(t => difference(blocks, base, t)));
13
+ const additions = new Map();
14
+ const removals = new Map();
15
+ /** @type {API.BlockFetcher} */
16
+ const fetcher = { get: cid => additions.get(cid.toString()) ?? blocks.get(cid) };
17
+ let root = base;
18
+ for (const { keys } of diffs) {
19
+ for (const [k, v] of keys) {
20
+ let res;
21
+ if (v[1] == null) {
22
+ res = await del(fetcher, root, k);
23
+ }
24
+ else {
25
+ res = await put(fetcher, root, k, v[1]);
26
+ }
27
+ for (const blk of res.removals) {
28
+ if (additions.has(blk.cid.toString())) {
29
+ additions.delete(blk.cid.toString());
30
+ }
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
+ return { root, additions: [...additions.values()], removals: [...removals.values()] };
42
+ };
@@ -0,0 +1,166 @@
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
+ // eslint-disable-next-line no-unused-vars
6
+ import * as API from './api.js';
7
+ export const KeyCharsASCII = 'ascii';
8
+ export const MaxKeySize = 4096;
9
+ /**
10
+ * @extends {Block<API.Shard, typeof dagCBOR.code, typeof sha256.code, 1>}
11
+ * @implements {API.ShardBlockView}
12
+ */
13
+ export class ShardBlock extends Block {
14
+ /**
15
+ * @param {object} config
16
+ * @param {API.ShardLink} config.cid
17
+ * @param {API.Shard} config.value
18
+ * @param {Uint8Array} config.bytes
19
+ */
20
+ constructor({ cid, value, bytes }) {
21
+ // @ts-expect-error
22
+ super({ cid, value, bytes });
23
+ }
24
+ /** @param {API.ShardOptions} [options] */
25
+ static create(options) {
26
+ return encodeBlock(create(options));
27
+ }
28
+ }
29
+ /**
30
+ * @param {API.ShardOptions} [options]
31
+ * @returns {API.Shard}
32
+ */
33
+ export const create = (options) => ({ entries: [], ...configure(options) });
34
+ /**
35
+ * @param {API.ShardOptions} [options]
36
+ * @returns {API.ShardConfig}
37
+ */
38
+ export const configure = (options) => ({
39
+ version: 1,
40
+ keyChars: options?.keyChars ?? KeyCharsASCII,
41
+ maxKeySize: options?.maxKeySize ?? MaxKeySize,
42
+ prefix: options?.prefix ?? ''
43
+ });
44
+ /**
45
+ * @param {API.ShardEntry[]} entries
46
+ * @param {API.ShardOptions} [options]
47
+ * @returns {API.Shard}
48
+ */
49
+ export const withEntries = (entries, options) => ({ ...create(options), entries });
50
+ /** @type {WeakMap<Uint8Array, API.ShardBlockView>} */
51
+ const decodeCache = new WeakMap();
52
+ /**
53
+ * @param {API.Shard} value
54
+ * @returns {Promise<API.ShardBlockView>}
55
+ */
56
+ export const encodeBlock = async (value) => {
57
+ const { cid, bytes } = await encode({ value, codec: dagCBOR, hasher: sha256 });
58
+ const block = new ShardBlock({ cid, value, bytes });
59
+ decodeCache.set(block.bytes, block);
60
+ return block;
61
+ };
62
+ /**
63
+ * @param {Uint8Array} bytes
64
+ * @returns {Promise<API.ShardBlockView>}
65
+ */
66
+ export const decodeBlock = async (bytes) => {
67
+ const block = decodeCache.get(bytes);
68
+ if (block)
69
+ return block;
70
+ const { cid, value } = await decode({ bytes, codec: dagCBOR, hasher: sha256 });
71
+ if (!isShard(value))
72
+ throw new Error(`invalid shard: ${cid}`);
73
+ return new ShardBlock({ cid, value, bytes });
74
+ };
75
+ /**
76
+ * @param {any} value
77
+ * @returns {value is API.Shard}
78
+ */
79
+ export const isShard = value => value != null &&
80
+ typeof value === 'object' &&
81
+ Array.isArray(value.entries) &&
82
+ value.version === 1 &&
83
+ typeof value.maxKeySize === 'number' &&
84
+ typeof value.keyChars === 'string' &&
85
+ typeof value.prefix === 'string';
86
+ /**
87
+ * @param {any} value
88
+ * @returns {value is API.ShardLink}
89
+ */
90
+ export const isShardLink = (value) => Link.isLink(value) &&
91
+ value.code === dagCBOR.code;
92
+ export class ShardFetcher {
93
+ /** @param {API.BlockFetcher} blocks */
94
+ constructor(blocks) {
95
+ this._blocks = blocks;
96
+ }
97
+ /**
98
+ * @param {API.ShardLink} link
99
+ * @returns {Promise<API.ShardBlockView>}
100
+ */
101
+ async get(link) {
102
+ const block = await this._blocks.get(link);
103
+ if (!block)
104
+ throw new Error(`missing block: ${link}`);
105
+ return decodeBlock(block.bytes);
106
+ }
107
+ }
108
+ /**
109
+ * @param {API.ShardEntry[]} target Entries to insert into.
110
+ * @param {API.ShardEntry} newEntry
111
+ * @returns {API.ShardEntry[]}
112
+ */
113
+ export const putEntry = (target, newEntry) => {
114
+ /** @type {API.ShardEntry[]} */
115
+ const entries = [];
116
+ for (const [i, entry] of target.entries()) {
117
+ const [k, v] = entry;
118
+ if (newEntry[0] === k) {
119
+ // if new value is link to shard...
120
+ if (Array.isArray(newEntry[1])) {
121
+ // and old value is link to shard
122
+ // and old value is _also_ link to data
123
+ // and new value does not have link to data
124
+ // then preserve old data
125
+ if (Array.isArray(v) && v[1] != null && newEntry[1][1] == null) {
126
+ entries.push([k, [newEntry[1][0], v[1]]]);
127
+ }
128
+ else {
129
+ entries.push(newEntry);
130
+ }
131
+ }
132
+ else {
133
+ // shard as well as value?
134
+ if (Array.isArray(v)) {
135
+ entries.push([k, [v[0], newEntry[1]]]);
136
+ }
137
+ else {
138
+ entries.push(newEntry);
139
+ }
140
+ }
141
+ for (let j = i + 1; j < target.length; j++) {
142
+ entries.push(target[j]);
143
+ }
144
+ return entries;
145
+ }
146
+ if (i === 0 && newEntry[0] < k) {
147
+ entries.push(newEntry);
148
+ for (let j = i; j < target.length; j++) {
149
+ entries.push(target[j]);
150
+ }
151
+ return entries;
152
+ }
153
+ if (i > 0 && newEntry[0] > target[i - 1][0] && newEntry[0] < k) {
154
+ entries.push(newEntry);
155
+ for (let j = i; j < target.length; j++) {
156
+ entries.push(target[j]);
157
+ }
158
+ return entries;
159
+ }
160
+ entries.push(entry);
161
+ }
162
+ entries.push(newEntry);
163
+ return entries;
164
+ };
165
+ /** @param {string} s */
166
+ export const isPrintableASCII = s => /^[\x20-\x7E]*$/.test(s);