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