@fireproof/vendor 2.0.1 → 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,140 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ import * as API from './api.js';
3
+ import * as Shard from '../../shard.js';
4
+ import { ShardFetcher, ShardBlock } from '../../shard.js';
5
+ import * as Batch from '../../batch/index.js';
6
+ import { BatchCommittedError } from '../../batch/index.js';
7
+ import * as CRDT from '../index.js';
8
+ import * as Clock from '../../clock/index.js';
9
+ import { EventBlock } from '../../clock/index.js';
10
+ import { MemoryBlockstore, MultiBlockFetcher } from '../../block.js';
11
+ export { BatchCommittedError };
12
+ /** @implements {API.CRDTBatcher} */
13
+ class Batcher {
14
+ #committed = false;
15
+ /**
16
+ * @param {object} init
17
+ * @param {API.BlockFetcher} init.blocks Block storage.
18
+ * @param {API.EventLink<API.Operation>[]} init.head Merkle clock head.
19
+ * @param {API.BatcherShardEntry[]} init.entries The entries in this shard.
20
+ * @param {string} init.prefix Key prefix.
21
+ * @param {number} init.version Shard compatibility version.
22
+ * @param {string} init.keyChars Characters allowed in keys, referring to a known character set.
23
+ * @param {number} init.maxKeySize Max key size in bytes.
24
+ * @param {API.ShardBlockView} init.base Original shard this batcher is based on.
25
+ * @param {API.ShardBlockView[]} init.additions Additions to include in the committed batch.
26
+ * @param {API.ShardBlockView[]} init.removals Removals to include in the committed batch.
27
+ */
28
+ constructor({ blocks, head, entries, prefix, version, keyChars, maxKeySize, base, additions, removals }) {
29
+ this.blocks = blocks;
30
+ this.head = head;
31
+ this.prefix = prefix;
32
+ this.entries = [...entries];
33
+ this.base = base;
34
+ this.version = version;
35
+ this.keyChars = keyChars;
36
+ this.maxKeySize = maxKeySize;
37
+ this.additions = additions;
38
+ this.removals = removals;
39
+ /** @type {API.BatchOperation['ops']} */
40
+ this.ops = [];
41
+ }
42
+ /**
43
+ * @param {string} key The key of the value to put.
44
+ * @param {API.UnknownLink} value The value to put.
45
+ * @returns {Promise<void>}
46
+ */
47
+ async put(key, value) {
48
+ if (this.#committed)
49
+ throw new BatchCommittedError();
50
+ await Batch.put(this.blocks, this, key, value);
51
+ this.ops.push({ type: 'put', key, value });
52
+ }
53
+ async commit() {
54
+ if (this.#committed)
55
+ throw new BatchCommittedError();
56
+ this.#committed = true;
57
+ const res = await Batch.commit(this);
58
+ /** @type {API.Operation} */
59
+ const data = { type: 'batch', ops: this.ops, root: res.root };
60
+ const event = await EventBlock.create(data, this.head);
61
+ const mblocks = new MemoryBlockstore();
62
+ const blocks = new MultiBlockFetcher(mblocks, this.blocks);
63
+ mblocks.putSync(event.cid, event.bytes);
64
+ const head = await Clock.advance(blocks, this.head, event.cid);
65
+ /** @type {Map<string, API.ShardBlockView>} */
66
+ const additions = new Map();
67
+ /** @type {Map<string, API.ShardBlockView>} */
68
+ const removals = new Map();
69
+ for (const a of this.additions) {
70
+ additions.set(a.cid.toString(), a);
71
+ }
72
+ for (const r of this.removals) {
73
+ removals.set(r.cid.toString(), r);
74
+ }
75
+ for (const a of res.additions) {
76
+ if (removals.has(a.cid.toString())) {
77
+ removals.delete(a.cid.toString());
78
+ }
79
+ additions.set(a.cid.toString(), a);
80
+ }
81
+ for (const r of res.removals) {
82
+ if (additions.has(r.cid.toString())) {
83
+ additions.delete(r.cid.toString());
84
+ }
85
+ else {
86
+ removals.set(r.cid.toString(), r);
87
+ }
88
+ }
89
+ return {
90
+ head,
91
+ event,
92
+ root: res.root,
93
+ additions: [...additions.values()],
94
+ removals: [...removals.values()]
95
+ };
96
+ }
97
+ /**
98
+ * @param {object} init
99
+ * @param {API.BlockFetcher} init.blocks Block storage.
100
+ * @param {API.EventLink<API.Operation>[]} init.head Merkle clock head.
101
+ */
102
+ static async create({ blocks, head }) {
103
+ const mblocks = new MemoryBlockstore();
104
+ blocks = new MultiBlockFetcher(mblocks, blocks);
105
+ if (!head.length) {
106
+ const base = await ShardBlock.create();
107
+ mblocks.putSync(base.cid, base.bytes);
108
+ return new Batcher({
109
+ blocks,
110
+ head,
111
+ entries: [],
112
+ base,
113
+ additions: [base],
114
+ removals: [],
115
+ ...Shard.configure(base.value)
116
+ });
117
+ }
118
+ const { root, additions, removals } = await CRDT.root(blocks, head);
119
+ for (const a of additions) {
120
+ mblocks.putSync(a.cid, a.bytes);
121
+ }
122
+ const shards = new ShardFetcher(blocks);
123
+ const base = await shards.get(root);
124
+ return new Batcher({
125
+ blocks,
126
+ head,
127
+ entries: base.value.entries,
128
+ base,
129
+ additions,
130
+ removals,
131
+ ...Shard.configure(base.value)
132
+ });
133
+ }
134
+ }
135
+ /**
136
+ * @param {API.BlockFetcher} blocks Bucket block storage.
137
+ * @param {API.EventLink<API.Operation>[]} head Merkle clock head.
138
+ * @returns {Promise<API.CRDTBatcher>}
139
+ */
140
+ export const create = (blocks, head) => Batcher.create({ blocks, head });
@@ -0,0 +1,344 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ import * as API from './api.js';
3
+ import * as Clock from '../clock/index.js';
4
+ import { EventFetcher, EventBlock } from '../clock/index.js';
5
+ import * as Pail from '../index.js';
6
+ import { ShardBlock } from '../shard.js';
7
+ import { MemoryBlockstore, MultiBlockFetcher } from '../block.js';
8
+ import * as Batch from '../batch/index.js';
9
+ /**
10
+ * Put a value (a CID) for the given key. If the key exists it's value is
11
+ * overwritten.
12
+ *
13
+ * @param {API.BlockFetcher} blocks Bucket block storage.
14
+ * @param {API.EventLink<API.Operation>[]} head Merkle clock head.
15
+ * @param {string} key The key of the value to put.
16
+ * @param {API.UnknownLink} value The value to put.
17
+ * @returns {Promise<API.Result>}
18
+ */
19
+ export const put = async (blocks, head, key, value) => {
20
+ const mblocks = new MemoryBlockstore();
21
+ blocks = new MultiBlockFetcher(mblocks, blocks);
22
+ if (!head.length) {
23
+ const shard = await ShardBlock.create();
24
+ mblocks.putSync(shard.cid, shard.bytes);
25
+ const result = await Pail.put(blocks, shard.cid, key, value);
26
+ /** @type {API.Operation} */
27
+ const data = { type: 'put', root: result.root, key, value };
28
+ const event = await EventBlock.create(data, head);
29
+ head = await Clock.advance(blocks, head, event.cid);
30
+ return {
31
+ root: result.root,
32
+ additions: [shard, ...result.additions],
33
+ removals: result.removals,
34
+ head,
35
+ event
36
+ };
37
+ }
38
+ /** @type {EventFetcher<API.Operation>} */
39
+ const events = new EventFetcher(blocks);
40
+ const ancestor = await findCommonAncestor(events, head);
41
+ if (!ancestor)
42
+ throw new Error('failed to find common ancestor event');
43
+ const aevent = await events.get(ancestor);
44
+ let { root } = aevent.value.data;
45
+ const sorted = await findSortedEvents(events, head, ancestor);
46
+ /** @type {Map<string, API.ShardBlockView>} */
47
+ const additions = new Map();
48
+ /** @type {Map<string, API.ShardBlockView>} */
49
+ const removals = new Map();
50
+ for (const { value: event } of sorted) {
51
+ let result;
52
+ if (event.data.type === 'put') {
53
+ result = await Pail.put(blocks, root, event.data.key, event.data.value);
54
+ }
55
+ else if (event.data.type === 'del') {
56
+ result = await Pail.del(blocks, root, event.data.key);
57
+ }
58
+ else if (event.data.type === 'batch') {
59
+ const batch = await Batch.create(blocks, root);
60
+ for (const op of event.data.ops) {
61
+ if (op.type !== 'put')
62
+ throw new Error(`unsupported batch operation: ${op.type}`);
63
+ await batch.put(op.key, op.value);
64
+ }
65
+ result = await batch.commit();
66
+ }
67
+ else {
68
+ // @ts-expect-error type does not exist on never
69
+ throw new Error(`unknown operation: ${event.data.type}`);
70
+ }
71
+ root = result.root;
72
+ for (const a of result.additions) {
73
+ mblocks.putSync(a.cid, a.bytes);
74
+ additions.set(a.cid.toString(), a);
75
+ }
76
+ for (const r of result.removals) {
77
+ removals.set(r.cid.toString(), r);
78
+ }
79
+ }
80
+ const result = await Pail.put(blocks, root, key, value);
81
+ // if we didn't change the pail we're done
82
+ if (result.root.toString() === root.toString()) {
83
+ return { root, additions: [], removals: [], head };
84
+ }
85
+ for (const a of result.additions) {
86
+ mblocks.putSync(a.cid, a.bytes);
87
+ additions.set(a.cid.toString(), a);
88
+ }
89
+ for (const r of result.removals) {
90
+ removals.set(r.cid.toString(), r);
91
+ }
92
+ /** @type {API.Operation} */
93
+ const data = { type: 'put', root: result.root, key, value };
94
+ const event = await EventBlock.create(data, head);
95
+ mblocks.putSync(event.cid, event.bytes);
96
+ head = await Clock.advance(blocks, head, event.cid);
97
+ // filter blocks that were added _and_ removed
98
+ for (const k of removals.keys()) {
99
+ if (additions.has(k)) {
100
+ additions.delete(k);
101
+ removals.delete(k);
102
+ }
103
+ }
104
+ return {
105
+ root: result.root,
106
+ additions: [...additions.values()],
107
+ removals: [...removals.values()],
108
+ head,
109
+ event
110
+ };
111
+ };
112
+ /**
113
+ * Delete the value for the given key from the bucket. If the key is not found
114
+ * no operation occurs.
115
+ *
116
+ * @param {API.BlockFetcher} blocks Bucket block storage.
117
+ * @param {API.EventLink<API.Operation>[]} head Merkle clock head.
118
+ * @param {string} key The key of the value to delete.
119
+ * @param {object} [options]
120
+ * @returns {Promise<API.Result>}
121
+ */
122
+ export const del = async (blocks, head, key, options) => {
123
+ throw new Error('not implemented');
124
+ };
125
+ /**
126
+ * Determine the effective pail root given the current merkle clock head.
127
+ *
128
+ * Clocks with multiple head events may return blocks that were added or
129
+ * removed while playing forward events from their common ancestor.
130
+ *
131
+ * @param {API.BlockFetcher} blocks Bucket block storage.
132
+ * @param {API.EventLink<API.Operation>[]} head Merkle clock head.
133
+ * @returns {Promise<{ root: API.ShardLink } & API.ShardDiff>}
134
+ */
135
+ export const root = async (blocks, head) => {
136
+ if (!head.length)
137
+ throw new Error('cannot determine root of headless clock');
138
+ const mblocks = new MemoryBlockstore();
139
+ blocks = new MultiBlockFetcher(mblocks, blocks);
140
+ /** @type {EventFetcher<API.Operation>} */
141
+ const events = new EventFetcher(blocks);
142
+ if (head.length === 1) {
143
+ const event = await events.get(head[0]);
144
+ const { root } = event.value.data;
145
+ return { root, additions: [], removals: [] };
146
+ }
147
+ const ancestor = await findCommonAncestor(events, head);
148
+ if (!ancestor)
149
+ throw new Error('failed to find common ancestor event');
150
+ const aevent = await events.get(ancestor);
151
+ let { root } = aevent.value.data;
152
+ const sorted = await findSortedEvents(events, head, ancestor);
153
+ /** @type {Map<string, API.ShardBlockView>} */
154
+ const additions = new Map();
155
+ /** @type {Map<string, API.ShardBlockView>} */
156
+ const removals = new Map();
157
+ for (const { value: event } of sorted) {
158
+ let result;
159
+ if (event.data.type === 'put') {
160
+ result = await Pail.put(blocks, root, event.data.key, event.data.value);
161
+ }
162
+ else if (event.data.type === 'del') {
163
+ result = await Pail.del(blocks, root, event.data.key);
164
+ }
165
+ else if (event.data.type === 'batch') {
166
+ const batch = await Batch.create(blocks, root);
167
+ for (const op of event.data.ops) {
168
+ if (op.type !== 'put')
169
+ throw new Error(`unsupported batch operation: ${op.type}`);
170
+ await batch.put(op.key, op.value);
171
+ }
172
+ result = await batch.commit();
173
+ }
174
+ else {
175
+ // @ts-expect-error type does not exist on never
176
+ throw new Error(`unknown operation: ${event.data.type}`);
177
+ }
178
+ root = result.root;
179
+ for (const a of result.additions) {
180
+ mblocks.putSync(a.cid, a.bytes);
181
+ additions.set(a.cid.toString(), a);
182
+ }
183
+ for (const r of result.removals) {
184
+ removals.set(r.cid.toString(), r);
185
+ }
186
+ }
187
+ // filter blocks that were added _and_ removed
188
+ for (const k of removals.keys()) {
189
+ if (additions.has(k)) {
190
+ additions.delete(k);
191
+ removals.delete(k);
192
+ }
193
+ }
194
+ return {
195
+ root,
196
+ additions: [...additions.values()],
197
+ removals: [...removals.values()]
198
+ };
199
+ };
200
+ /**
201
+ * @param {API.BlockFetcher} blocks Bucket block storage.
202
+ * @param {API.EventLink<API.Operation>[]} head Merkle clock head.
203
+ * @param {string} key The key of the value to retrieve.
204
+ */
205
+ export const get = async (blocks, head, key) => {
206
+ if (!head.length)
207
+ return;
208
+ const result = await root(blocks, head);
209
+ if (result.additions.length) {
210
+ blocks = new MultiBlockFetcher(new MemoryBlockstore(result.additions), blocks);
211
+ }
212
+ return Pail.get(blocks, result.root, key);
213
+ };
214
+ /**
215
+ * @param {API.BlockFetcher} blocks Bucket block storage.
216
+ * @param {API.EventLink<API.Operation>[]} head Merkle clock head.
217
+ * @param {API.EntriesOptions} [options]
218
+ */
219
+ export const entries = async function* (blocks, head, options) {
220
+ if (!head.length)
221
+ return;
222
+ const result = await root(blocks, head);
223
+ if (result.additions.length) {
224
+ blocks = new MultiBlockFetcher(new MemoryBlockstore(result.additions), blocks);
225
+ }
226
+ yield* Pail.entries(blocks, result.root, options);
227
+ };
228
+ /**
229
+ * Find the common ancestor event of the passed children. A common ancestor is
230
+ * the first single event in the DAG that _all_ paths from children lead to.
231
+ *
232
+ * @param {EventFetcher<API.Operation>} events
233
+ * @param {API.EventLink<API.Operation>[]} children
234
+ */
235
+ const findCommonAncestor = async (events, children) => {
236
+ if (!children.length)
237
+ return;
238
+ const candidates = children.map(c => [c]);
239
+ while (true) {
240
+ let changed = false;
241
+ for (const c of candidates) {
242
+ const candidate = await findAncestorCandidate(events, c[c.length - 1]);
243
+ if (!candidate)
244
+ continue;
245
+ changed = true;
246
+ c.push(candidate);
247
+ const ancestor = findCommonString(candidates);
248
+ if (ancestor)
249
+ return ancestor;
250
+ }
251
+ if (!changed)
252
+ return;
253
+ }
254
+ };
255
+ /**
256
+ * @param {EventFetcher<API.Operation>} events
257
+ * @param {API.EventLink<API.Operation>} root
258
+ */
259
+ const findAncestorCandidate = async (events, root) => {
260
+ const { value: event } = await events.get(root);
261
+ if (!event.parents.length)
262
+ return root;
263
+ return event.parents.length === 1
264
+ ? event.parents[0]
265
+ : findCommonAncestor(events, event.parents);
266
+ };
267
+ /**
268
+ * @template {{ toString: () => string }} T
269
+ * @param {Array<T[]>} arrays
270
+ */
271
+ const findCommonString = (arrays) => {
272
+ arrays = arrays.map(a => [...a]);
273
+ for (const arr of arrays) {
274
+ for (const item of arr) {
275
+ let matched = true;
276
+ for (const other of arrays) {
277
+ if (arr === other)
278
+ continue;
279
+ matched = other.some(i => String(i) === String(item));
280
+ if (!matched)
281
+ break;
282
+ }
283
+ if (matched)
284
+ return item;
285
+ }
286
+ }
287
+ };
288
+ /**
289
+ * Find and sort events between the head(s) and the tail.
290
+ * @param {EventFetcher<API.Operation>} events
291
+ * @param {API.EventLink<API.Operation>[]} head
292
+ * @param {API.EventLink<API.Operation>} tail
293
+ */
294
+ const findSortedEvents = async (events, head, tail) => {
295
+ if (head.length === 1 && head[0].toString() === tail.toString()) {
296
+ return [];
297
+ }
298
+ // get weighted events - heavier events happened first
299
+ /** @type {Map<string, { event: API.EventBlockView<API.Operation>, weight: number }>} */
300
+ const weights = new Map();
301
+ const all = await Promise.all(head.map(h => findEvents(events, h, tail)));
302
+ for (const arr of all) {
303
+ for (const { event, depth } of arr) {
304
+ const info = weights.get(event.cid.toString());
305
+ if (info) {
306
+ info.weight += depth;
307
+ }
308
+ else {
309
+ weights.set(event.cid.toString(), { event, weight: depth });
310
+ }
311
+ }
312
+ }
313
+ // group events into buckets by weight
314
+ /** @type {Map<number, API.EventBlockView<API.Operation>[]>} */
315
+ const buckets = new Map();
316
+ for (const { event, weight } of weights.values()) {
317
+ const bucket = buckets.get(weight);
318
+ if (bucket) {
319
+ bucket.push(event);
320
+ }
321
+ else {
322
+ buckets.set(weight, [event]);
323
+ }
324
+ }
325
+ // sort by weight, and by CID within weight
326
+ return Array.from(buckets)
327
+ .sort((a, b) => b[0] - a[0])
328
+ .flatMap(([, es]) => es.sort((a, b) => String(a.cid) < String(b.cid) ? -1 : 1));
329
+ };
330
+ /**
331
+ * @param {EventFetcher<API.Operation>} events
332
+ * @param {API.EventLink<API.Operation>} start
333
+ * @param {API.EventLink<API.Operation>} end
334
+ * @returns {Promise<Array<{ event: API.EventBlockView<API.Operation>, depth: number }>>}
335
+ */
336
+ const findEvents = async (events, start, end, depth = 0) => {
337
+ const event = await events.get(start);
338
+ const acc = [{ event, depth }];
339
+ const { parents } = event.value;
340
+ if (parents.length === 1 && String(parents[0]) === String(end))
341
+ return acc;
342
+ const rest = await Promise.all(parents.map(p => findEvents(events, p, end, depth + 1)));
343
+ return acc.concat(...rest);
344
+ };
@@ -0,0 +1,151 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ import * as API from './api.js';
3
+ import { ShardFetcher } from './shard.js';
4
+ /**
5
+ * @typedef {string} K
6
+ * @typedef {[before: null, after: API.UnknownLink]} AddV
7
+ * @typedef {[before: API.UnknownLink, after: API.UnknownLink]} UpdateV
8
+ * @typedef {[before: API.UnknownLink, after: null]} DeleteV
9
+ * @typedef {[key: K, value: AddV|UpdateV|DeleteV]} KV
10
+ * @typedef {KV[]} KeysDiff
11
+ * @typedef {{ keys: KeysDiff, shards: API.ShardDiff }} CombinedDiff
12
+ */
13
+ /**
14
+ * @param {API.BlockFetcher} blocks Bucket block storage.
15
+ * @param {API.ShardLink} a Base DAG.
16
+ * @param {API.ShardLink} b Comparison DAG.
17
+ * @returns {Promise<CombinedDiff>}
18
+ */
19
+ export const difference = async (blocks, a, b) => {
20
+ if (isEqual(a, b))
21
+ return { keys: [], shards: { additions: [], removals: [] } };
22
+ const shards = new ShardFetcher(blocks);
23
+ const [ashard, bshard] = await Promise.all([shards.get(a), shards.get(b)]);
24
+ const aents = new Map(ashard.value.entries);
25
+ const bents = new Map(bshard.value.entries);
26
+ const keys = /** @type {Map<K, AddV|UpdateV|DeleteV>} */ (new Map());
27
+ const additions = new Map([[bshard.cid.toString(), bshard]]);
28
+ const removals = new Map([[ashard.cid.toString(), ashard]]);
29
+ // find shards removed in B
30
+ for (const [akey, aval] of ashard.value.entries) {
31
+ const bval = bents.get(akey);
32
+ if (bval)
33
+ continue;
34
+ if (!Array.isArray(aval)) {
35
+ keys.set(`${ashard.value.prefix}${akey}`, [aval, null]);
36
+ continue;
37
+ }
38
+ // if shard link _with_ value
39
+ if (aval[1] != null) {
40
+ keys.set(`${ashard.value.prefix}${akey}`, [aval[1], null]);
41
+ }
42
+ for await (const s of collect(shards, aval[0])) {
43
+ for (const [k, v] of s.value.entries) {
44
+ if (!Array.isArray(v)) {
45
+ keys.set(`${s.value.prefix}${k}`, [v, null]);
46
+ }
47
+ else if (v[1] != null) {
48
+ keys.set(`${s.value.prefix}${k}`, [v[1], null]);
49
+ }
50
+ }
51
+ removals.set(s.cid.toString(), s);
52
+ }
53
+ }
54
+ // find shards added or updated in B
55
+ for (const [bkey, bval] of bshard.value.entries) {
56
+ const aval = aents.get(bkey);
57
+ if (!Array.isArray(bval)) {
58
+ if (!aval) {
59
+ keys.set(`${bshard.value.prefix}${bkey}`, [null, bval]);
60
+ }
61
+ else if (Array.isArray(aval)) {
62
+ keys.set(`${bshard.value.prefix}${bkey}`, [aval[1] ?? null, bval]);
63
+ }
64
+ else if (!isEqual(aval, bval)) {
65
+ keys.set(`${bshard.value.prefix}${bkey}`, [aval, bval]);
66
+ }
67
+ continue;
68
+ }
69
+ if (aval && Array.isArray(aval)) { // updated in B
70
+ if (isEqual(aval[0], bval[0])) {
71
+ if (bval[1] != null && (aval[1] == null || !isEqual(aval[1], bval[1]))) {
72
+ keys.set(`${bshard.value.prefix}${bkey}`, [aval[1] ?? null, bval[1]]);
73
+ }
74
+ continue; // updated value?
75
+ }
76
+ const res = await difference(blocks, aval[0], bval[0]);
77
+ for (const shard of res.shards.additions) {
78
+ additions.set(shard.cid.toString(), shard);
79
+ }
80
+ for (const shard of res.shards.removals) {
81
+ removals.set(shard.cid.toString(), shard);
82
+ }
83
+ for (const [k, v] of res.keys) {
84
+ keys.set(k, v);
85
+ }
86
+ }
87
+ else if (aval) { // updated in B value => link+value
88
+ if (bval[1] == null) {
89
+ keys.set(`${bshard.value.prefix}${bkey}`, [aval, null]);
90
+ }
91
+ else if (!isEqual(aval, bval[1])) {
92
+ keys.set(`${bshard.value.prefix}${bkey}`, [aval, bval[1]]);
93
+ }
94
+ for await (const s of collect(shards, bval[0])) {
95
+ for (const [k, v] of s.value.entries) {
96
+ if (!Array.isArray(v)) {
97
+ keys.set(`${s.value.prefix}${k}`, [null, v]);
98
+ }
99
+ else if (v[1] != null) {
100
+ keys.set(`${s.value.prefix}${k}`, [null, v[1]]);
101
+ }
102
+ }
103
+ additions.set(s.cid.toString(), s);
104
+ }
105
+ }
106
+ else { // added in B
107
+ keys.set(`${bshard.value.prefix}${bkey}`, [null, bval[0]]);
108
+ for await (const s of collect(shards, bval[0])) {
109
+ for (const [k, v] of s.value.entries) {
110
+ if (!Array.isArray(v)) {
111
+ keys.set(`${s.value.prefix}${k}`, [null, v]);
112
+ }
113
+ else if (v[1] != null) {
114
+ keys.set(`${s.value.prefix}${k}`, [null, v[1]]);
115
+ }
116
+ }
117
+ additions.set(s.cid.toString(), s);
118
+ }
119
+ }
120
+ }
121
+ // filter blocks that were added _and_ removed from B
122
+ for (const k of removals.keys()) {
123
+ if (additions.has(k)) {
124
+ additions.delete(k);
125
+ removals.delete(k);
126
+ }
127
+ }
128
+ return {
129
+ keys: [...keys.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1),
130
+ shards: { additions: [...additions.values()], removals: [...removals.values()] }
131
+ };
132
+ };
133
+ /**
134
+ * @param {API.UnknownLink} a
135
+ * @param {API.UnknownLink} b
136
+ */
137
+ const isEqual = (a, b) => a.toString() === b.toString();
138
+ /**
139
+ * @param {import('./shard.js').ShardFetcher} shards
140
+ * @param {API.ShardLink} root
141
+ * @returns {AsyncIterableIterator<API.ShardBlockView>}
142
+ */
143
+ async function* collect(shards, root) {
144
+ const shard = await shards.get(root);
145
+ yield shard;
146
+ for (const [, v] of shard.value.entries) {
147
+ if (!Array.isArray(v))
148
+ continue;
149
+ yield* collect(shards, v[0]);
150
+ }
151
+ }