@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.
- package/README.md +3 -0
- package/babel.config.js +1 -0
- package/bench/mst.bench.ts +162 -0
- package/bench/repo.bench.ts +39 -0
- package/build.js +22 -0
- package/dist/blockstore/index.d.ts +2 -0
- package/dist/blockstore/ipld-store.d.ts +27 -0
- package/dist/blockstore/memory-blockstore.d.ts +13 -0
- package/dist/cid-set.d.ts +14 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +17731 -0
- package/dist/index.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/mst/diff.d.ts +33 -0
- package/dist/mst/index.d.ts +4 -0
- package/dist/mst/mst.d.ts +106 -0
- package/dist/mst/util.d.ts +9 -0
- package/dist/mst/walker.d.ts +22 -0
- package/dist/repo.d.ts +39 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/types.d.ts +12 -0
- package/dist/sync.d.ts +9 -0
- package/dist/types.d.ts +368 -0
- package/dist/util.d.ts +13 -0
- package/dist/verify.d.ts +5 -0
- package/jest.bench.config.js +7 -0
- package/jest.config.js +6 -0
- package/package.json +34 -0
- package/src/blockstore/index.ts +2 -0
- package/src/blockstore/ipld-store.ts +103 -0
- package/src/blockstore/memory-blockstore.ts +49 -0
- package/src/cid-set.ts +50 -0
- package/src/index.ts +7 -0
- package/src/logger.ts +5 -0
- package/src/mst/diff.ts +106 -0
- package/src/mst/index.ts +4 -0
- package/src/mst/mst.ts +796 -0
- package/src/mst/util.ts +122 -0
- package/src/mst/walker.ts +120 -0
- package/src/repo.ts +312 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/types.ts +12 -0
- package/src/sync.ts +38 -0
- package/src/types.ts +101 -0
- package/src/util.ts +88 -0
- package/src/verify.ts +62 -0
- package/tests/_util.ts +254 -0
- package/tests/mst.test.ts +280 -0
- package/tests/repo.test.ts +107 -0
- package/tests/sync.test.ts +129 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +14 -0
- 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
|