@atproto/repo 0.0.1

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 (54) hide show
  1. package/README.md +3 -0
  2. package/babel.config.js +1 -0
  3. package/bench/mst.bench.ts +162 -0
  4. package/bench/repo.bench.ts +39 -0
  5. package/build.js +22 -0
  6. package/dist/blockstore/index.d.ts +2 -0
  7. package/dist/blockstore/ipld-store.d.ts +27 -0
  8. package/dist/blockstore/memory-blockstore.d.ts +13 -0
  9. package/dist/cid-set.d.ts +14 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.js +17731 -0
  12. package/dist/index.js.map +7 -0
  13. package/dist/logger.d.ts +2 -0
  14. package/dist/mst/diff.d.ts +33 -0
  15. package/dist/mst/index.d.ts +4 -0
  16. package/dist/mst/mst.d.ts +106 -0
  17. package/dist/mst/util.d.ts +9 -0
  18. package/dist/mst/walker.d.ts +22 -0
  19. package/dist/repo.d.ts +39 -0
  20. package/dist/storage/index.d.ts +1 -0
  21. package/dist/storage/types.d.ts +12 -0
  22. package/dist/sync.d.ts +9 -0
  23. package/dist/types.d.ts +368 -0
  24. package/dist/util.d.ts +13 -0
  25. package/dist/verify.d.ts +5 -0
  26. package/jest.bench.config.js +7 -0
  27. package/jest.config.js +6 -0
  28. package/package.json +34 -0
  29. package/src/blockstore/index.ts +2 -0
  30. package/src/blockstore/ipld-store.ts +103 -0
  31. package/src/blockstore/memory-blockstore.ts +49 -0
  32. package/src/cid-set.ts +50 -0
  33. package/src/index.ts +7 -0
  34. package/src/logger.ts +5 -0
  35. package/src/mst/diff.ts +106 -0
  36. package/src/mst/index.ts +4 -0
  37. package/src/mst/mst.ts +796 -0
  38. package/src/mst/util.ts +122 -0
  39. package/src/mst/walker.ts +120 -0
  40. package/src/repo.ts +312 -0
  41. package/src/storage/index.ts +1 -0
  42. package/src/storage/types.ts +12 -0
  43. package/src/sync.ts +38 -0
  44. package/src/types.ts +101 -0
  45. package/src/util.ts +88 -0
  46. package/src/verify.ts +62 -0
  47. package/tests/_util.ts +254 -0
  48. package/tests/mst.test.ts +280 -0
  49. package/tests/repo.test.ts +107 -0
  50. package/tests/sync.test.ts +129 -0
  51. package/tsconfig.build.json +4 -0
  52. package/tsconfig.build.tsbuildinfo +1 -0
  53. package/tsconfig.json +14 -0
  54. package/update-pkg.js +14 -0
package/src/mst/mst.ts ADDED
@@ -0,0 +1,796 @@
1
+ import z from 'zod'
2
+ import { CID } from 'multiformats'
3
+
4
+ import IpldStore from '../blockstore/ipld-store'
5
+ import { def, cidForData } from '@atproto/common'
6
+ import { DataDiff } from './diff'
7
+ import { DataStore } from '../types'
8
+ import { BlockWriter } from '@ipld/car/api'
9
+ import * as util from './util'
10
+ import MstWalker from './walker'
11
+
12
+ /**
13
+ * This is an implementation of a Merkle Search Tree (MST)
14
+ * The data structure is described here: https://hal.inria.fr/hal-02303490/document
15
+ * The MST is an ordered, insert-order-independent, deterministic tree.
16
+ * Keys are laid out in alphabetic order.
17
+ * The key insight of an MST is that each key is hashed and starting 0s are counted
18
+ * to determine which layer it falls on (5 zeros for ~32 fanout).
19
+ * This is a merkle tree, so each subtree is referred to by it's hash (CID).
20
+ * When a leaf is changed, ever tree on the path to that leaf is changed as well,
21
+ * thereby updating the root hash.
22
+ */
23
+
24
+ /**
25
+ * A couple notes on CBOR encoding:
26
+ *
27
+ * There are never two neighboring subtrees.
28
+ * Therefore, we can represent a node as an array of
29
+ * leaves & pointers to their right neighbor (possibly null),
30
+ * along with a pointer to the left-most subtree (also possibly null).
31
+ *
32
+ * Most keys in a subtree will have overlap.
33
+ * We do compression on prefixes by describing keys as:
34
+ * - the length of the prefix that it shares in common with the preceding key
35
+ * - the rest of the string
36
+ *
37
+ * For example:
38
+ * If the first leaf in a tree is `bsky/posts/abcdefg` and the second is `bsky/posts/abcdehi`
39
+ * Then the first will be described as `prefix: 0, key: 'bsky/posts/abcdefg'`,
40
+ * and the second will be described as `prefix: 16, key: 'hi'.`
41
+ */
42
+ const subTreePointer = z.nullable(def.cid)
43
+ const treeEntry = z.object({
44
+ p: z.number(), // prefix count of utf-8 chars that this key shares with the prev key
45
+ k: z.string(), // the rest of the key outside the shared prefix
46
+ v: def.cid, // value
47
+ t: subTreePointer, // next subtree (to the right of leaf)
48
+ })
49
+ export const nodeDataDef = z.object({
50
+ l: subTreePointer, // left-most subtree
51
+ e: z.array(treeEntry), //entries
52
+ })
53
+ export type NodeData = z.infer<typeof nodeDataDef>
54
+
55
+ export type NodeEntry = MST | Leaf
56
+
57
+ const DEFAULT_MST_FANOUT = 16
58
+ export type Fanout = 2 | 8 | 16 | 32 | 64
59
+ export type MstOpts = {
60
+ layer: number
61
+ fanout: Fanout
62
+ }
63
+
64
+ export class MST implements DataStore {
65
+ blockstore: IpldStore
66
+ fanout: Fanout
67
+ entries: NodeEntry[] | null
68
+ layer: number | null
69
+ pointer: CID
70
+ outdatedPointer = false
71
+
72
+ constructor(
73
+ blockstore: IpldStore,
74
+ fanout: Fanout,
75
+ pointer: CID,
76
+ entries: NodeEntry[] | null,
77
+ layer: number | null,
78
+ ) {
79
+ this.blockstore = blockstore
80
+ this.fanout = fanout
81
+ this.entries = entries
82
+ this.layer = layer
83
+ this.pointer = pointer
84
+ }
85
+
86
+ static async create(
87
+ blockstore: IpldStore,
88
+ entries: NodeEntry[] = [],
89
+ opts?: Partial<MstOpts>,
90
+ ): Promise<MST> {
91
+ const pointer = await util.cidForEntries(entries)
92
+ const { layer = 0, fanout = DEFAULT_MST_FANOUT } = opts || {}
93
+ return new MST(blockstore, fanout, pointer, entries, layer)
94
+ }
95
+
96
+ static async fromData(
97
+ blockstore: IpldStore,
98
+ data: NodeData,
99
+ opts?: Partial<MstOpts>,
100
+ ): Promise<MST> {
101
+ const { layer = null, fanout = DEFAULT_MST_FANOUT } = opts || {}
102
+ const entries = await util.deserializeNodeData(blockstore, data, opts)
103
+ const pointer = await cidForData(data)
104
+ return new MST(blockstore, fanout, pointer, entries, layer)
105
+ }
106
+
107
+ static load(blockstore: IpldStore, cid: CID, opts?: Partial<MstOpts>): MST {
108
+ const { layer = null, fanout = DEFAULT_MST_FANOUT } = opts || {}
109
+ return new MST(blockstore, fanout, cid, null, layer)
110
+ }
111
+
112
+ // Immutability
113
+ // -------------------
114
+
115
+ // We never mutate an MST, we just return a new MST with updated values
116
+ async newTree(entries: NodeEntry[]): Promise<MST> {
117
+ const mst = new MST(
118
+ this.blockstore,
119
+ this.fanout,
120
+ this.pointer,
121
+ entries,
122
+ this.layer,
123
+ )
124
+ mst.outdatedPointer = true
125
+ return mst
126
+ }
127
+
128
+ // Getters (lazy load)
129
+ // -------------------
130
+
131
+ // We don't want to load entries of every subtree, just the ones we need
132
+ async getEntries(): Promise<NodeEntry[]> {
133
+ if (this.entries) return [...this.entries]
134
+ if (this.pointer) {
135
+ const data = await this.blockstore.get(this.pointer, nodeDataDef)
136
+ const firstLeaf = data.e[0]
137
+ const layer =
138
+ firstLeaf !== undefined
139
+ ? await util.leadingZerosOnHash(firstLeaf.k, this.fanout)
140
+ : undefined
141
+ this.entries = await util.deserializeNodeData(this.blockstore, data, {
142
+ layer,
143
+ fanout: this.fanout,
144
+ })
145
+
146
+ return this.entries
147
+ }
148
+ throw new Error('No entries or CID provided')
149
+ }
150
+
151
+ // We don't hash the node on every mutation for performance reasons
152
+ // Instead we keep track of whether the pointer is outdated and only (recursively) calculate when needed
153
+ async getPointer(): Promise<CID> {
154
+ if (!this.outdatedPointer) return this.pointer
155
+ let entries = await this.getEntries()
156
+ const outdated = entries.filter(
157
+ (e) => e.isTree() && e.outdatedPointer,
158
+ ) as MST[]
159
+ if (outdated.length > 0) {
160
+ await Promise.all(outdated.map((e) => e.getPointer()))
161
+ entries = await this.getEntries()
162
+ }
163
+ this.pointer = await util.cidForEntries(entries)
164
+ this.outdatedPointer = false
165
+ return this.pointer
166
+ }
167
+
168
+ // In most cases, we get the layer of a node from a hint on creation
169
+ // In the case of the topmost node in the tree, we look for a key in the node & determine the layer
170
+ // In the case where we don't find one, we recurse down until we do.
171
+ // If we still can't find one, then we have an empty tree and the node is layer 0
172
+ async getLayer(): Promise<number> {
173
+ this.layer = await this.attemptGetLayer()
174
+ if (this.layer === null) this.layer = 0
175
+ return this.layer
176
+ }
177
+
178
+ async attemptGetLayer(): Promise<number | null> {
179
+ if (this.layer !== null) return this.layer
180
+ const entries = await this.getEntries()
181
+ let layer = await util.layerForEntries(entries, this.fanout)
182
+ if (layer === null) {
183
+ for (const entry of entries) {
184
+ if (entry.isTree()) {
185
+ const childLayer = await entry.attemptGetLayer()
186
+ if (childLayer !== null) {
187
+ layer = childLayer + 1
188
+ break
189
+ }
190
+ }
191
+ }
192
+ }
193
+ if (layer !== null) this.layer = layer
194
+ return layer
195
+ }
196
+
197
+ // Core functionality
198
+ // -------------------
199
+
200
+ // Persist the MST to the blockstore
201
+ // If the topmost tree only has one entry and it's a subtree, we can eliminate the topmost tree
202
+ // However, lower trees with only one entry must be preserved
203
+ async stage(): Promise<CID> {
204
+ return this.stageRecurse(true)
205
+ }
206
+
207
+ async stageRecurse(trimTop = false): Promise<CID> {
208
+ const pointer = await this.getPointer()
209
+ const alreadyHas = await this.blockstore.has(pointer)
210
+ if (alreadyHas) return pointer
211
+ const entries = await this.getEntries()
212
+ if (entries.length === 1 && trimTop) {
213
+ const node = entries[0]
214
+ if (node.isTree()) {
215
+ return node.stageRecurse(true)
216
+ }
217
+ }
218
+ const data = util.serializeNodeData(entries)
219
+ await this.blockstore.stage(data)
220
+ for (const entry of entries) {
221
+ if (entry.isTree()) {
222
+ await entry.stageRecurse(false)
223
+ }
224
+ }
225
+ return pointer
226
+ }
227
+
228
+ // Adds a new leaf for the given key/value pair
229
+ // Throws if a leaf with that key already exists
230
+ async add(key: string, value: CID, knownZeros?: number): Promise<MST> {
231
+ const keyZeros =
232
+ knownZeros ?? (await util.leadingZerosOnHash(key, this.fanout))
233
+ const layer = await this.getLayer()
234
+ const newLeaf = new Leaf(key, value)
235
+ if (keyZeros === layer) {
236
+ // it belongs in this layer
237
+ const index = await this.findGtOrEqualLeafIndex(key)
238
+ const found = await this.atIndex(index)
239
+ if (found?.isLeaf() && found.key === key) {
240
+ throw new Error(`There is already a value at key: ${key}`)
241
+ }
242
+ const prevNode = await this.atIndex(index - 1)
243
+ if (!prevNode || prevNode.isLeaf()) {
244
+ // if entry before is a leaf, (or we're on far left) we can just splice in
245
+ return this.spliceIn(newLeaf, index)
246
+ } else {
247
+ // else we try to split the subtree around the key
248
+ const splitSubTree = await prevNode.splitAround(key)
249
+ return this.replaceWithSplit(
250
+ index - 1,
251
+ splitSubTree[0],
252
+ newLeaf,
253
+ splitSubTree[1],
254
+ )
255
+ }
256
+ } else if (keyZeros < layer) {
257
+ // it belongs on a lower layer
258
+ const index = await this.findGtOrEqualLeafIndex(key)
259
+ const prevNode = await this.atIndex(index - 1)
260
+ if (prevNode && prevNode.isTree()) {
261
+ // if entry before is a tree, we add it to that tree
262
+ const newSubtree = await prevNode.add(key, value, keyZeros)
263
+ return this.updateEntry(index - 1, newSubtree)
264
+ } else {
265
+ const subTree = await this.createChild()
266
+ const newSubTree = await subTree.add(key, value, keyZeros)
267
+ return this.spliceIn(newSubTree, index)
268
+ }
269
+ } else {
270
+ // it belongs on a higher layer & we must push the rest of the tree down
271
+ const split = await this.splitAround(key)
272
+ // if the newly added key has >=2 more leading zeros than the current highest layer
273
+ // then we need to add in structural nodes in between as well
274
+ let left: MST | null = split[0]
275
+ let right: MST | null = split[1]
276
+ const layer = await this.getLayer()
277
+ const extraLayersToAdd = keyZeros - layer
278
+ // intentionally starting at 1, since first layer is taken care of by split
279
+ for (let i = 1; i < extraLayersToAdd; i++) {
280
+ if (left !== null) {
281
+ left = await left.createParent()
282
+ }
283
+ if (right !== null) {
284
+ right = await right.createParent()
285
+ }
286
+ }
287
+ const updated: NodeEntry[] = []
288
+ if (left) updated.push(left)
289
+ updated.push(new Leaf(key, value))
290
+ if (right) updated.push(right)
291
+ const newRoot = await MST.create(this.blockstore, updated, {
292
+ layer: keyZeros,
293
+ fanout: this.fanout,
294
+ })
295
+ newRoot.outdatedPointer = true
296
+ return newRoot
297
+ }
298
+ }
299
+
300
+ // Gets the value at the given key
301
+ async get(key: string): Promise<CID | null> {
302
+ const index = await this.findGtOrEqualLeafIndex(key)
303
+ const found = await this.atIndex(index)
304
+ if (found && found.isLeaf() && found.key === key) {
305
+ return found.value
306
+ }
307
+ const prev = await this.atIndex(index - 1)
308
+ if (prev && prev.isTree()) {
309
+ return prev.get(key)
310
+ }
311
+ return null
312
+ }
313
+
314
+ // Edits the value at the given key
315
+ // Throws if the given key does not exist
316
+ async update(key: string, value: CID): Promise<MST> {
317
+ const index = await this.findGtOrEqualLeafIndex(key)
318
+ const found = await this.atIndex(index)
319
+ if (found && found.isLeaf() && found.key === key) {
320
+ return this.updateEntry(index, new Leaf(key, value))
321
+ }
322
+ const prev = await this.atIndex(index - 1)
323
+ if (prev && prev.isTree()) {
324
+ const updatedTree = await prev.update(key, value)
325
+ return this.updateEntry(index - 1, updatedTree)
326
+ }
327
+ throw new Error(`Could not find a record with key: ${key}`)
328
+ }
329
+
330
+ // Deletes the value at the given key
331
+ async delete(key: string): Promise<MST> {
332
+ const index = await this.findGtOrEqualLeafIndex(key)
333
+ const found = await this.atIndex(index)
334
+ // if found, remove it on this level
335
+ if (found?.isLeaf() && found.key === key) {
336
+ const prev = await this.atIndex(index - 1)
337
+ const next = await this.atIndex(index + 1)
338
+ if (prev?.isTree() && next?.isTree()) {
339
+ const merged = await prev.appendMerge(next)
340
+ return this.newTree([
341
+ ...(await this.slice(0, index - 1)),
342
+ merged,
343
+ ...(await this.slice(index + 2)),
344
+ ])
345
+ } else {
346
+ return this.removeEntry(index)
347
+ }
348
+ }
349
+ // else recurse down to find it
350
+ const prev = await this.atIndex(index - 1)
351
+ if (prev?.isTree()) {
352
+ const subtree = await prev.delete(key)
353
+ const subTreeEntries = await subtree.getEntries()
354
+ if (subTreeEntries.length === 0) {
355
+ return this.removeEntry(index - 1)
356
+ } else {
357
+ return this.updateEntry(index - 1, subtree)
358
+ }
359
+ } else {
360
+ throw new Error(`Could not find a record with key: ${key}`)
361
+ }
362
+ }
363
+
364
+ // Walk two MSTs to find the semantic changes
365
+ async diff(other: MST): Promise<DataDiff> {
366
+ await this.getPointer()
367
+ await other.getPointer()
368
+ const diff = new DataDiff()
369
+
370
+ const leftWalker = new MstWalker(this)
371
+ const rightWalker = new MstWalker(other)
372
+ while (!leftWalker.status.done || !rightWalker.status.done) {
373
+ // if one walker is finished, continue walking the other & logging all nodes
374
+ if (leftWalker.status.done && !rightWalker.status.done) {
375
+ const node = rightWalker.status.curr
376
+ if (node.isLeaf()) {
377
+ diff.recordAdd(node.key, node.value)
378
+ } else {
379
+ diff.recordNewCid(node.pointer)
380
+ }
381
+ await rightWalker.advance()
382
+ continue
383
+ } else if (!leftWalker.status.done && rightWalker.status.done) {
384
+ const node = leftWalker.status.curr
385
+ if (node.isLeaf()) {
386
+ diff.recordDelete(node.key, node.value)
387
+ }
388
+ await leftWalker.advance()
389
+ continue
390
+ }
391
+ if (leftWalker.status.done || rightWalker.status.done) break
392
+ const left = leftWalker.status.curr
393
+ const right = rightWalker.status.curr
394
+ if (left === null || right === null) break
395
+
396
+ // if both pointers are leaves, record an update & advance both or record the lowest key and advance that pointer
397
+ if (left.isLeaf() && right.isLeaf()) {
398
+ if (left.key === right.key) {
399
+ if (!left.value.equals(right.value)) {
400
+ diff.recordUpdate(left.key, left.value, right.value)
401
+ }
402
+ await leftWalker.advance()
403
+ await rightWalker.advance()
404
+ } else if (left.key < right.key) {
405
+ diff.recordDelete(left.key, left.value)
406
+ await leftWalker.advance()
407
+ } else {
408
+ diff.recordAdd(right.key, right.value)
409
+ await rightWalker.advance()
410
+ }
411
+ continue
412
+ }
413
+
414
+ // next, ensure that we're on the same layer
415
+ // if one walker is at a higher layer than the other, we need to do one of two things
416
+ // if the higher walker is pointed at a tree, step into that tree to try to catch up with the lower
417
+ // if the higher walker is pointed at a leaf, then advance the lower walker to try to catch up the higher
418
+ if (leftWalker.layer() > rightWalker.layer()) {
419
+ if (left.isLeaf()) {
420
+ if (right.isLeaf()) {
421
+ diff.recordAdd(right.key, right.value)
422
+ } else {
423
+ diff.recordNewCid(right.pointer)
424
+ }
425
+ await rightWalker.advance()
426
+ } else {
427
+ await leftWalker.stepInto()
428
+ }
429
+ continue
430
+ } else if (leftWalker.layer() < rightWalker.layer()) {
431
+ if (right.isLeaf()) {
432
+ if (left.isLeaf()) {
433
+ diff.recordDelete(left.key, left.value)
434
+ }
435
+ await leftWalker.advance()
436
+ } else {
437
+ diff.recordNewCid(right.pointer)
438
+ await rightWalker.stepInto()
439
+ }
440
+ continue
441
+ }
442
+
443
+ // if we're on the same level, and both pointers are trees, do a comparison
444
+ // if they're the same, step over. if they're different, step in to find the subdiff
445
+ if (left.isTree() && right.isTree()) {
446
+ if (left.pointer.equals(right.pointer)) {
447
+ await leftWalker.stepOver()
448
+ await rightWalker.stepOver()
449
+ } else {
450
+ diff.recordNewCid(right.pointer)
451
+ await leftWalker.stepInto()
452
+ await rightWalker.stepInto()
453
+ }
454
+ continue
455
+ }
456
+
457
+ // finally, if one pointer is a tree and the other is a leaf, simply step into the tree
458
+ if (left.isLeaf() && right.isTree()) {
459
+ await diff.recordNewCid(right.pointer)
460
+ await rightWalker.stepInto()
461
+ continue
462
+ } else if (left.isTree() && right.isLeaf()) {
463
+ await leftWalker.stepInto()
464
+ continue
465
+ }
466
+
467
+ throw new Error('Unidentifiable case in diff walk')
468
+ }
469
+ return diff
470
+ }
471
+
472
+ // Simple Operations
473
+ // -------------------
474
+
475
+ // update entry in place
476
+ async updateEntry(index: number, entry: NodeEntry): Promise<MST> {
477
+ const update = [
478
+ ...(await this.slice(0, index)),
479
+ entry,
480
+ ...(await this.slice(index + 1)),
481
+ ]
482
+ return this.newTree(update)
483
+ }
484
+
485
+ // remove entry at index
486
+ async removeEntry(index: number): Promise<MST> {
487
+ const updated = [
488
+ ...(await this.slice(0, index)),
489
+ ...(await this.slice(index + 1)),
490
+ ]
491
+ return this.newTree(updated)
492
+ }
493
+
494
+ // append entry to end of the node
495
+ async append(entry: NodeEntry): Promise<MST> {
496
+ const entries = await this.getEntries()
497
+ return this.newTree([...entries, entry])
498
+ }
499
+
500
+ // prepend entry to start of the node
501
+ async prepend(entry: NodeEntry): Promise<MST> {
502
+ const entries = await this.getEntries()
503
+ return this.newTree([entry, ...entries])
504
+ }
505
+
506
+ // returns entry at index
507
+ async atIndex(index: number): Promise<NodeEntry | null> {
508
+ const entries = await this.getEntries()
509
+ return entries[index] ?? null
510
+ }
511
+
512
+ // returns a slice of the node (like array.slice)
513
+ async slice(
514
+ start?: number | undefined,
515
+ end?: number | undefined,
516
+ ): Promise<NodeEntry[]> {
517
+ const entries = await this.getEntries()
518
+ return entries.slice(start, end)
519
+ }
520
+
521
+ // inserts entry at index
522
+ async spliceIn(entry: NodeEntry, index: number): Promise<MST> {
523
+ const update = [
524
+ ...(await this.slice(0, index)),
525
+ entry,
526
+ ...(await this.slice(index)),
527
+ ]
528
+ return this.newTree(update)
529
+ }
530
+
531
+ // replaces an entry with [ Maybe(tree), Leaf, Maybe(tree) ]
532
+ async replaceWithSplit(
533
+ index: number,
534
+ left: MST | null,
535
+ leaf: Leaf,
536
+ right: MST | null,
537
+ ): Promise<MST> {
538
+ const update = await this.slice(0, index)
539
+ if (left) update.push(left)
540
+ update.push(leaf)
541
+ if (right) update.push(right)
542
+ update.push(...(await this.slice(index + 1)))
543
+ return this.newTree(update)
544
+ }
545
+
546
+ // Subtree & Splits
547
+ // -------------------
548
+
549
+ // Recursively splits a sub tree around a given key
550
+ async splitAround(key: string): Promise<[MST | null, MST | null]> {
551
+ const index = await this.findGtOrEqualLeafIndex(key)
552
+ // split tree around key
553
+ const leftData = await this.slice(0, index)
554
+ const rightData = await this.slice(index)
555
+ let left = await this.newTree(leftData)
556
+ let right = await this.newTree(rightData)
557
+
558
+ // if the far right of the left side is a subtree,
559
+ // we need to split it on the key as well
560
+ const lastInLeft = leftData[leftData.length - 1]
561
+ if (lastInLeft?.isTree()) {
562
+ left = await left.removeEntry(leftData.length - 1)
563
+ const split = await lastInLeft.splitAround(key)
564
+ if (split[0]) {
565
+ left = await left.append(split[0])
566
+ }
567
+ if (split[1]) {
568
+ right = await right.prepend(split[1])
569
+ }
570
+ }
571
+
572
+ return [
573
+ (await left.getEntries()).length > 0 ? left : null,
574
+ (await right.getEntries()).length > 0 ? right : null,
575
+ ]
576
+ }
577
+
578
+ // The simple merge case where every key in the right tree is greater than every key in the left tree
579
+ // (used primarily for deletes)
580
+ async appendMerge(toMerge: MST): Promise<MST> {
581
+ if ((await this.getLayer()) !== (await toMerge.getLayer())) {
582
+ throw new Error(
583
+ 'Trying to merge two nodes from different layers of the MST',
584
+ )
585
+ }
586
+ const thisEntries = await this.getEntries()
587
+ const toMergeEntries = await toMerge.getEntries()
588
+ const lastInLeft = thisEntries[thisEntries.length - 1]
589
+ const firstInRight = toMergeEntries[0]
590
+ if (lastInLeft?.isTree() && firstInRight?.isTree()) {
591
+ const merged = await lastInLeft.appendMerge(firstInRight)
592
+ return this.newTree([
593
+ ...thisEntries.slice(0, thisEntries.length - 1),
594
+ merged,
595
+ ...toMergeEntries.slice(1),
596
+ ])
597
+ } else {
598
+ return this.newTree([...thisEntries, ...toMergeEntries])
599
+ }
600
+ }
601
+
602
+ // Create relatives
603
+ // -------------------
604
+
605
+ async createChild(): Promise<MST> {
606
+ const layer = await this.getLayer()
607
+ return MST.create(this.blockstore, [], {
608
+ layer: layer - 1,
609
+ fanout: this.fanout,
610
+ })
611
+ }
612
+
613
+ async createParent(): Promise<MST> {
614
+ const layer = await this.getLayer()
615
+ const parent = await MST.create(this.blockstore, [this], {
616
+ layer: layer + 1,
617
+ fanout: this.fanout,
618
+ })
619
+ parent.outdatedPointer = true
620
+ return parent
621
+ }
622
+
623
+ // Finding insertion points
624
+ // -------------------
625
+
626
+ // finds index of first leaf node that is greater than or equal to the value
627
+ async findGtOrEqualLeafIndex(key: string): Promise<number> {
628
+ const entries = await this.getEntries()
629
+ const maybeIndex = entries.findIndex(
630
+ (entry) => entry.isLeaf() && entry.key >= key,
631
+ )
632
+ // if we can't find, we're on the end
633
+ return maybeIndex >= 0 ? maybeIndex : entries.length
634
+ }
635
+
636
+ // List operations (partial tree traversal)
637
+ // -------------------
638
+
639
+ // @TODO write tests for these
640
+
641
+ // Walk tree starting at key
642
+ async *walkLeavesFrom(key: string): AsyncIterable<Leaf> {
643
+ const index = await this.findGtOrEqualLeafIndex(key)
644
+ const entries = await this.getEntries()
645
+ const prev = entries[index - 1]
646
+ if (prev && prev.isTree()) {
647
+ for await (const e of prev.walkLeavesFrom(key)) {
648
+ yield e
649
+ }
650
+ }
651
+ for (let i = index; i < entries.length; i++) {
652
+ const entry = entries[i]
653
+ if (entry.isLeaf()) {
654
+ yield entry
655
+ } else {
656
+ for await (const e of entry.walkLeavesFrom(key)) {
657
+ yield e
658
+ }
659
+ }
660
+ }
661
+ }
662
+
663
+ async list(count: number, after?: string, before?: string): Promise<Leaf[]> {
664
+ const vals: Leaf[] = []
665
+ for await (const leaf of this.walkLeavesFrom(after || '')) {
666
+ if (leaf.key === after) continue
667
+ if (vals.length >= count) break
668
+ if (before && leaf.key >= before) break
669
+ vals.push(leaf)
670
+ }
671
+ return vals
672
+ }
673
+
674
+ async listWithPrefix(
675
+ prefix: string,
676
+ count = Number.MAX_SAFE_INTEGER,
677
+ ): Promise<Leaf[]> {
678
+ const vals: Leaf[] = []
679
+ for await (const leaf of this.walkLeavesFrom(prefix)) {
680
+ if (vals.length >= count || !leaf.key.startsWith(prefix)) break
681
+ vals.push(leaf)
682
+ }
683
+ return vals
684
+ }
685
+
686
+ // Full tree traversal
687
+ // -------------------
688
+
689
+ // Walk full tree & emit nodes, consumer can bail at any point by returning false
690
+ async *walk(): AsyncIterable<NodeEntry> {
691
+ yield this
692
+ const entries = await this.getEntries()
693
+ for (const entry of entries) {
694
+ if (entry.isTree()) {
695
+ for await (const e of entry.walk()) {
696
+ yield e
697
+ }
698
+ } else {
699
+ yield entry
700
+ }
701
+ }
702
+ }
703
+
704
+ // Walk full tree & emit nodes, consumer can bail at any point by returning false
705
+ async paths(): Promise<NodeEntry[][]> {
706
+ const entries = await this.getEntries()
707
+ let paths: NodeEntry[][] = []
708
+ for (const entry of entries) {
709
+ if (entry.isLeaf()) {
710
+ paths.push([entry])
711
+ }
712
+ if (entry.isTree()) {
713
+ const subPaths = await entry.paths()
714
+ paths = [...paths, ...subPaths.map((p) => [entry, ...p])]
715
+ }
716
+ }
717
+ return paths
718
+ }
719
+
720
+ // Walks tree & returns all nodes
721
+ async allNodes(): Promise<NodeEntry[]> {
722
+ const nodes: NodeEntry[] = []
723
+ for await (const entry of this.walk()) {
724
+ nodes.push(entry)
725
+ }
726
+ return nodes
727
+ }
728
+
729
+ // Walks tree & returns all leaves
730
+ async leaves() {
731
+ const leaves: Leaf[] = []
732
+ for await (const entry of this.walk()) {
733
+ if (entry.isLeaf()) leaves.push(entry)
734
+ }
735
+ return leaves
736
+ }
737
+
738
+ // Returns total leaf count
739
+ async leafCount(): Promise<number> {
740
+ const leaves = await this.leaves()
741
+ return leaves.length
742
+ }
743
+
744
+ // Sync Protocol
745
+
746
+ async writeToCarStream(car: BlockWriter): Promise<void> {
747
+ for await (const entry of this.walk()) {
748
+ if (entry.isTree()) {
749
+ const pointer = await entry.getPointer()
750
+ await this.blockstore.addToCar(car, pointer)
751
+ } else {
752
+ await this.blockstore.addToCar(car, entry.value)
753
+ }
754
+ }
755
+ }
756
+
757
+ // Matching Leaf interface
758
+ // -------------------
759
+
760
+ isTree(): this is MST {
761
+ return true
762
+ }
763
+
764
+ isLeaf(): this is Leaf {
765
+ return false
766
+ }
767
+
768
+ async equals(other: NodeEntry): Promise<boolean> {
769
+ if (other.isLeaf()) return false
770
+ const thisPointer = await this.getPointer()
771
+ const otherPointer = await other.getPointer()
772
+ return thisPointer.equals(otherPointer)
773
+ }
774
+ }
775
+
776
+ export class Leaf {
777
+ constructor(public key: string, public value: CID) {}
778
+
779
+ isTree(): this is MST {
780
+ return false
781
+ }
782
+
783
+ isLeaf(): this is Leaf {
784
+ return true
785
+ }
786
+
787
+ equals(entry: NodeEntry): boolean {
788
+ if (entry.isLeaf()) {
789
+ return this.key === entry.key && this.value.equals(entry.value)
790
+ } else {
791
+ return false
792
+ }
793
+ }
794
+ }
795
+
796
+ export default MST