@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/util.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import * as auth from '@atproto/auth'
3
+ import Repo from './repo'
4
+ import { DataDiff, MST } from './mst'
5
+ import { IpldStore } from './blockstore'
6
+ import { DataStore, RecordWriteOp, def } from './types'
7
+
8
+ export const ucanForOperation = async (
9
+ prevData: DataStore,
10
+ newData: DataStore,
11
+ rootDid: string,
12
+ authStore: auth.AuthStore,
13
+ ): Promise<string> => {
14
+ const diff = await prevData.diff(newData)
15
+ const neededCaps = diff.neededCapabilities(rootDid)
16
+ const ucanForOp = await authStore.createUcanForCaps(rootDid, neededCaps, 30)
17
+ return auth.encodeUcan(ucanForOp)
18
+ }
19
+
20
+ export const getCommitPath = async (
21
+ blockstore: IpldStore,
22
+ earliest: CID | null,
23
+ latest: CID,
24
+ ): Promise<CID[] | null> => {
25
+ let curr: CID | null = latest
26
+ const path: CID[] = []
27
+ while (curr !== null) {
28
+ path.push(curr)
29
+ const commit = await blockstore.get(curr, def.commit)
30
+ if (earliest && curr.equals(earliest)) {
31
+ return path.reverse()
32
+ }
33
+ const root = await blockstore.get(commit.root, def.repoRoot)
34
+ if (!earliest && root.prev === null) {
35
+ return path.reverse()
36
+ }
37
+ curr = root.prev
38
+ }
39
+ return null
40
+ }
41
+
42
+ export const getWriteOpLog = async (
43
+ blockstore: IpldStore,
44
+ earliest: CID | null,
45
+ latest: CID,
46
+ ): Promise<RecordWriteOp[][]> => {
47
+ const commits = await getCommitPath(blockstore, earliest, latest)
48
+ if (!commits) throw new Error('Could not find shared history')
49
+ const heads = await Promise.all(commits.map((c) => Repo.load(blockstore, c)))
50
+ // Turn commit path into list of diffs
51
+ let prev: DataStore = await MST.create(blockstore) // Empty
52
+ const msts = heads.map((h) => h.data)
53
+ const diffs: DataDiff[] = []
54
+ for (const mst of msts) {
55
+ diffs.push(await prev.diff(mst))
56
+ prev = mst
57
+ }
58
+ // Map MST diffs to write ops
59
+ return Promise.all(diffs.map((diff) => diffToWriteOps(blockstore, diff)))
60
+ }
61
+
62
+ export const diffToWriteOps = (
63
+ blockstore: IpldStore,
64
+ diff: DataDiff,
65
+ ): Promise<RecordWriteOp[]> => {
66
+ return Promise.all([
67
+ ...diff.addList().map(async (add) => {
68
+ const { collection, rkey } = parseRecordKey(add.key)
69
+ const value = await blockstore.getUnchecked(add.cid)
70
+ return { action: 'create' as const, collection, rkey, value }
71
+ }),
72
+ ...diff.updateList().map(async (upd) => {
73
+ const { collection, rkey } = parseRecordKey(upd.key)
74
+ const value = await blockstore.getUnchecked(upd.cid)
75
+ return { action: 'update' as const, collection, rkey, value }
76
+ }),
77
+ ...diff.deleteList().map((del) => {
78
+ const { collection, rkey } = parseRecordKey(del.key)
79
+ return { action: 'delete' as const, collection, rkey }
80
+ }),
81
+ ])
82
+ }
83
+
84
+ export const parseRecordKey = (key: string) => {
85
+ const parts = key.split('/')
86
+ if (parts.length !== 2) throw new Error(`Invalid record key: ${key}`)
87
+ return { collection: parts[0], rkey: parts[1] }
88
+ }
package/src/verify.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import * as auth from '@atproto/auth'
3
+ import { IpldStore } from './blockstore'
4
+ import Repo from './repo'
5
+ import { DataDiff } from './mst'
6
+ import * as util from './util'
7
+ import { def } from './types'
8
+
9
+ export const verifyUpdates = async (
10
+ blockstore: IpldStore,
11
+ earliest: CID | null,
12
+ latest: CID,
13
+ verifier: auth.Verifier,
14
+ ): Promise<DataDiff> => {
15
+ const commitPath = await util.getCommitPath(blockstore, earliest, latest)
16
+ if (commitPath === null) {
17
+ throw new Error('Could not find shared history')
18
+ }
19
+ const fullDiff = new DataDiff()
20
+ if (commitPath.length === 0) return fullDiff
21
+ let prevRepo = await Repo.load(blockstore, commitPath[0])
22
+ for (const commit of commitPath.slice(1)) {
23
+ const nextRepo = await Repo.load(blockstore, commit)
24
+ const diff = await prevRepo.data.diff(nextRepo.data)
25
+
26
+ if (!nextRepo.root.meta.equals(prevRepo.root.meta)) {
27
+ throw new Error('Not supported: repo metadata updated')
28
+ }
29
+
30
+ let didForSignature: string
31
+ if (nextRepo.root.auth_token) {
32
+ // verify auth token covers all necessary writes
33
+ const encodedToken = await blockstore.get(
34
+ nextRepo.root.auth_token,
35
+ def.string,
36
+ )
37
+ const token = await verifier.validateUcan(encodedToken)
38
+ const neededCaps = diff.neededCapabilities(prevRepo.did)
39
+ for (const cap of neededCaps) {
40
+ await verifier.verifyAtpUcan(token, prevRepo.did, cap)
41
+ }
42
+ didForSignature = token.payload.iss
43
+ } else {
44
+ didForSignature = prevRepo.did
45
+ }
46
+
47
+ // verify signature matches repo root + auth token
48
+ // const commit = await toRepo.getCommit()
49
+ const validSig = await verifier.verifySignature(
50
+ didForSignature,
51
+ nextRepo.commit.root.bytes,
52
+ nextRepo.commit.sig,
53
+ )
54
+ if (!validSig) {
55
+ throw new Error(`Invalid signature on commit: ${nextRepo.cid.toString()}`)
56
+ }
57
+
58
+ fullDiff.addDiff(diff)
59
+ prevRepo = nextRepo
60
+ }
61
+ return fullDiff
62
+ }
package/tests/_util.ts ADDED
@@ -0,0 +1,254 @@
1
+ import { CID } from 'multiformats'
2
+ import { TID } from '@atproto/common'
3
+ import * as auth from '@atproto/auth'
4
+ import IpldStore from '../src/blockstore/ipld-store'
5
+ import { Repo } from '../src/repo'
6
+ import { MemoryBlockstore } from '../src/blockstore'
7
+ import { DataDiff, MST } from '../src/mst'
8
+ import fs from 'fs'
9
+ import { RecordWriteOp } from '../src'
10
+
11
+ type IdMapping = Record<string, CID>
12
+
13
+ const fakeStore = new MemoryBlockstore()
14
+
15
+ export const randomCid = async (store: IpldStore = fakeStore): Promise<CID> => {
16
+ const str = randomStr(50)
17
+ return store.stage({ test: str })
18
+ }
19
+
20
+ export const generateBulkTids = (count: number): TID[] => {
21
+ const ids: TID[] = []
22
+ for (let i = 0; i < count; i++) {
23
+ ids.push(TID.next())
24
+ }
25
+ return ids
26
+ }
27
+
28
+ export const generateBulkTidMapping = async (
29
+ count: number,
30
+ blockstore: IpldStore = fakeStore,
31
+ ): Promise<IdMapping> => {
32
+ const ids = generateBulkTids(count)
33
+ const obj: IdMapping = {}
34
+ for (const id of ids) {
35
+ obj[id.toString()] = await randomCid(blockstore)
36
+ }
37
+ return obj
38
+ }
39
+
40
+ export const keysFromMapping = (mapping: IdMapping): TID[] => {
41
+ return Object.keys(mapping).map((id) => TID.fromStr(id))
42
+ }
43
+
44
+ export const keysFromMappings = (mappings: IdMapping[]): TID[] => {
45
+ return mappings.map(keysFromMapping).flat()
46
+ }
47
+
48
+ export const randomStr = (len: number): string => {
49
+ let result = ''
50
+ const CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
51
+ for (let i = 0; i < len; i++) {
52
+ result += CHARS.charAt(Math.floor(Math.random() * CHARS.length))
53
+ }
54
+ return result
55
+ }
56
+
57
+ export const shuffle = <T>(arr: T[]): T[] => {
58
+ const toShuffle = [...arr]
59
+ const shuffled: T[] = []
60
+ while (toShuffle.length > 0) {
61
+ const index = Math.floor(Math.random() * toShuffle.length)
62
+ shuffled.push(toShuffle[index])
63
+ toShuffle.splice(index, 1)
64
+ }
65
+ return shuffled
66
+ }
67
+
68
+ export const generateObject = (): Record<string, string> => {
69
+ return {
70
+ name: randomStr(100),
71
+ }
72
+ }
73
+
74
+ // Mass repo mutations & checking
75
+ // -------------------------------
76
+
77
+ export const testCollections = ['com.example.posts', 'com.example.likes']
78
+
79
+ export type CollectionData = Record<string, unknown>
80
+ export type RepoData = Record<string, CollectionData>
81
+
82
+ export const fillRepo = async (
83
+ repo: Repo,
84
+ authStore: auth.AuthStore,
85
+ itemsPerCollection: number,
86
+ ): Promise<{ repo: Repo; data: RepoData }> => {
87
+ const repoData: RepoData = {}
88
+ const writes: RecordWriteOp[] = []
89
+ for (const collName of testCollections) {
90
+ const collData: CollectionData = {}
91
+ for (let i = 0; i < itemsPerCollection; i++) {
92
+ const object = generateObject()
93
+ const rkey = TID.nextStr()
94
+ collData[rkey] = object
95
+ writes.push({
96
+ action: 'create',
97
+ collection: collName,
98
+ rkey,
99
+ value: object,
100
+ })
101
+ }
102
+ repoData[collName] = collData
103
+ }
104
+ const updated = await repo.stageUpdate(writes).createCommit(authStore)
105
+ return {
106
+ repo: updated,
107
+ data: repoData,
108
+ }
109
+ }
110
+
111
+ export const editRepo = async (
112
+ repo: Repo,
113
+ prevData: RepoData,
114
+ authStore: auth.AuthStore,
115
+ params: {
116
+ adds?: number
117
+ updates?: number
118
+ deletes?: number
119
+ },
120
+ ): Promise<{ repo: Repo; data: RepoData }> => {
121
+ const { adds = 0, updates = 0, deletes = 0 } = params
122
+ const repoData: RepoData = {}
123
+ const writes: RecordWriteOp[] = []
124
+ for (const collName of testCollections) {
125
+ const collData = prevData[collName]
126
+ const shuffled = shuffle(Object.entries(collData))
127
+
128
+ for (let i = 0; i < adds; i++) {
129
+ const object = generateObject()
130
+ const rkey = TID.nextStr()
131
+ collData[rkey] = object
132
+ writes.push({
133
+ action: 'create',
134
+ collection: collName,
135
+ rkey,
136
+ value: object,
137
+ })
138
+ }
139
+
140
+ const toUpdate = shuffled.slice(0, updates)
141
+ for (let i = 0; i < toUpdate.length; i++) {
142
+ const object = generateObject()
143
+ const rkey = toUpdate[i][0]
144
+ writes.push({
145
+ action: 'update',
146
+ collection: collName,
147
+ rkey,
148
+ value: object,
149
+ })
150
+ collData[rkey] = object
151
+ }
152
+
153
+ const toDelete = shuffled.slice(updates, deletes)
154
+ for (let i = 0; i < toDelete.length; i++) {
155
+ const rkey = toDelete[i][0]
156
+ writes.push({
157
+ action: 'delete',
158
+ collection: collName,
159
+ rkey,
160
+ })
161
+ delete collData[rkey]
162
+ }
163
+ repoData[collName] = collData
164
+ }
165
+ const updated = await repo.stageUpdate(writes).createCommit(authStore)
166
+ return {
167
+ repo: updated,
168
+ data: repoData,
169
+ }
170
+ }
171
+
172
+ export const checkRepo = async (repo: Repo, data: RepoData): Promise<void> => {
173
+ for (const collName of Object.keys(data)) {
174
+ const collData = data[collName]
175
+ for (const rkey of Object.keys(collData)) {
176
+ const record = await repo.getRecord(collName, rkey)
177
+ expect(record).toEqual(collData[rkey])
178
+ }
179
+ }
180
+ }
181
+
182
+ export const checkRepoDiff = async (
183
+ diff: DataDiff,
184
+ before: RepoData,
185
+ after: RepoData,
186
+ ): Promise<void> => {
187
+ const getObjectCid = async (
188
+ key: string,
189
+ data: RepoData,
190
+ ): Promise<CID | undefined> => {
191
+ const parts = key.split('/')
192
+ const collection = parts[0]
193
+ const obj = (data[collection] || {})[parts[1]]
194
+ return obj === undefined ? undefined : fakeStore.stage(obj)
195
+ }
196
+
197
+ for (const add of diff.addList()) {
198
+ const beforeCid = await getObjectCid(add.key, before)
199
+ const afterCid = await getObjectCid(add.key, after)
200
+
201
+ expect(beforeCid).toBeUndefined()
202
+ expect(afterCid).toEqual(add.cid)
203
+ }
204
+
205
+ for (const update of diff.updateList()) {
206
+ const beforeCid = await getObjectCid(update.key, before)
207
+ const afterCid = await getObjectCid(update.key, after)
208
+
209
+ expect(beforeCid).toEqual(update.prev)
210
+ expect(afterCid).toEqual(update.cid)
211
+ }
212
+
213
+ for (const del of diff.deleteList()) {
214
+ const beforeCid = await getObjectCid(del.key, before)
215
+ const afterCid = await getObjectCid(del.key, after)
216
+
217
+ expect(beforeCid).toEqual(del.cid)
218
+ expect(afterCid).toBeUndefined()
219
+ }
220
+ }
221
+
222
+ // Logging
223
+ // ----------------
224
+
225
+ export const writeMstLog = async (filename: string, tree: MST) => {
226
+ let log = ''
227
+ for await (const entry of tree.walk()) {
228
+ if (entry.isLeaf()) continue
229
+ const layer = await entry.getLayer()
230
+ log += `Layer ${layer}: ${entry.pointer}\n`
231
+ log += '--------------\n'
232
+ const entries = await entry.getEntries()
233
+ for (const e of entries) {
234
+ if (e.isLeaf()) {
235
+ log += `Key: ${e.key} (${e.value})\n`
236
+ } else {
237
+ log += `Subtree: ${e.pointer}\n`
238
+ }
239
+ }
240
+ log += '\n\n'
241
+ }
242
+ fs.writeFileSync(filename, log)
243
+ }
244
+
245
+ export const saveMstEntries = (filename: string, entries: [string, CID][]) => {
246
+ const writable = entries.map(([key, val]) => [key, val.toString()])
247
+ fs.writeFileSync(filename, JSON.stringify(writable))
248
+ }
249
+
250
+ export const loadMstEntries = (filename: string): [string, CID][] => {
251
+ const contents = fs.readFileSync(filename)
252
+ const parsed = JSON.parse(contents.toString())
253
+ return parsed.map(([key, value]) => [key, CID.parse(value)])
254
+ }
@@ -0,0 +1,280 @@
1
+ import { MST, DataAdd, DataUpdate, DataDelete } from '../src/mst'
2
+ import { countPrefixLen } from '../src/mst/util'
3
+
4
+ import { MemoryBlockstore } from '../src/blockstore'
5
+ import * as util from './_util'
6
+
7
+ import { CID } from 'multiformats'
8
+
9
+ describe('Merkle Search Tree', () => {
10
+ let blockstore: MemoryBlockstore
11
+ let mst: MST
12
+ let mapping: Record<string, CID>
13
+ let shuffled: [string, CID][]
14
+
15
+ beforeAll(async () => {
16
+ blockstore = new MemoryBlockstore()
17
+ mst = await MST.create(blockstore)
18
+ mapping = await util.generateBulkTidMapping(1000, blockstore)
19
+ shuffled = util.shuffle(Object.entries(mapping))
20
+ })
21
+
22
+ it('adds records', async () => {
23
+ for (const entry of shuffled) {
24
+ mst = await mst.add(entry[0], entry[1])
25
+ }
26
+ for (const entry of shuffled) {
27
+ const got = await mst.get(entry[0])
28
+ expect(entry[1].equals(got)).toBeTruthy()
29
+ }
30
+
31
+ const totalSize = await mst.leafCount()
32
+ expect(totalSize).toBe(1000)
33
+ })
34
+
35
+ it('edits records', async () => {
36
+ let editedMst = mst
37
+ const toEdit = shuffled.slice(0, 100)
38
+
39
+ const edited: [string, CID][] = []
40
+ for (const entry of toEdit) {
41
+ const newCid = await util.randomCid()
42
+ editedMst = await editedMst.update(entry[0], newCid)
43
+ edited.push([entry[0], newCid])
44
+ }
45
+
46
+ for (const entry of edited) {
47
+ const got = await editedMst.get(entry[0])
48
+ expect(entry[1].equals(got)).toBeTruthy()
49
+ }
50
+
51
+ const totalSize = await editedMst.leafCount()
52
+ expect(totalSize).toBe(1000)
53
+ })
54
+
55
+ it('deletes records', async () => {
56
+ let deletedMst = mst
57
+ const toDelete = shuffled.slice(0, 100)
58
+ const theRest = shuffled.slice(100)
59
+ for (const entry of toDelete) {
60
+ deletedMst = await deletedMst.delete(entry[0])
61
+ }
62
+
63
+ const totalSize = await deletedMst.leafCount()
64
+ expect(totalSize).toBe(900)
65
+
66
+ for (const entry of toDelete) {
67
+ const got = await deletedMst.get(entry[0])
68
+ expect(got).toBe(null)
69
+ }
70
+ for (const entry of theRest) {
71
+ const got = await deletedMst.get(entry[0])
72
+ expect(entry[1].equals(got)).toBeTruthy()
73
+ }
74
+ })
75
+
76
+ it('is order independent', async () => {
77
+ const allNodes = await mst.allNodes()
78
+
79
+ let recreated = await MST.create(blockstore)
80
+ const reshuffled = util.shuffle(Object.entries(mapping))
81
+ for (const entry of reshuffled) {
82
+ recreated = await recreated.add(entry[0], entry[1])
83
+ }
84
+ const allReshuffled = await recreated.allNodes()
85
+
86
+ expect(allNodes.length).toBe(allReshuffled.length)
87
+ for (let i = 0; i < allNodes.length; i++) {
88
+ expect(await allNodes[i].equals(allReshuffled[i])).toBeTruthy()
89
+ }
90
+ })
91
+
92
+ it('saves and loads from blockstore', async () => {
93
+ const cid = await mst.stage()
94
+ const loaded = await MST.load(blockstore, cid)
95
+ const origNodes = await mst.allNodes()
96
+ const loadedNodes = await loaded.allNodes()
97
+ expect(origNodes.length).toBe(loadedNodes.length)
98
+ for (let i = 0; i < origNodes.length; i++) {
99
+ expect(await origNodes[i].equals(loadedNodes[i])).toBeTruthy()
100
+ }
101
+ })
102
+
103
+ it('diffs', async () => {
104
+ let toDiff = mst
105
+
106
+ const toAdd = Object.entries(
107
+ await util.generateBulkTidMapping(100, blockstore),
108
+ )
109
+ const toEdit = shuffled.slice(500, 600)
110
+ const toDel = shuffled.slice(400, 500)
111
+
112
+ const expectedAdds: Record<string, DataAdd> = {}
113
+ const expectedUpdates: Record<string, DataUpdate> = {}
114
+ const expectedDels: Record<string, DataDelete> = {}
115
+
116
+ for (const entry of toAdd) {
117
+ toDiff = await toDiff.add(entry[0], entry[1])
118
+ expectedAdds[entry[0]] = { key: entry[0], cid: entry[1] }
119
+ }
120
+ for (const entry of toEdit) {
121
+ const updated = await util.randomCid()
122
+ toDiff = await toDiff.update(entry[0], updated)
123
+ expectedUpdates[entry[0]] = {
124
+ key: entry[0],
125
+ prev: entry[1],
126
+ cid: updated,
127
+ }
128
+ }
129
+ for (const entry of toDel) {
130
+ toDiff = await toDiff.delete(entry[0])
131
+ expectedDels[entry[0]] = { key: entry[0], cid: entry[1] }
132
+ }
133
+
134
+ const diff = await mst.diff(toDiff)
135
+
136
+ expect(diff.addList().length).toBe(100)
137
+ expect(diff.updateList().length).toBe(100)
138
+ expect(diff.deleteList().length).toBe(100)
139
+
140
+ expect(diff.adds).toEqual(expectedAdds)
141
+ expect(diff.updates).toEqual(expectedUpdates)
142
+ expect(diff.deletes).toEqual(expectedDels)
143
+
144
+ // ensure we correctly report all added CIDs
145
+ for await (const entry of toDiff.walk()) {
146
+ let cid: CID
147
+ if (entry.isTree()) {
148
+ cid = await entry.getPointer()
149
+ } else {
150
+ cid = entry.value
151
+ }
152
+ const found = (await blockstore.has(cid)) || diff.newCids.has(cid)
153
+ expect(found).toBeTruthy()
154
+ }
155
+ })
156
+
157
+ // Special Cases (these are made for fanout 32)
158
+ // ------------
159
+
160
+ it('trims the top of an MST on stage', async () => {
161
+ const layer0 = [
162
+ '3j6hnk65jis2t',
163
+ '3j6hnk65jit2t',
164
+ '3j6hnk65jiu2t',
165
+ '3j6hnk65jne2t',
166
+ '3j6hnk65jnm2t',
167
+ ]
168
+ const layer1 = '3j6hnk65jju2t'
169
+ mst = await MST.create(blockstore, [], { fanout: 32 })
170
+ const cid = await util.randomCid()
171
+ const tids = [...layer0, layer1]
172
+ for (const tid of tids) {
173
+ mst = await mst.add(tid, cid)
174
+ }
175
+ const layer = await mst.getLayer()
176
+ expect(layer).toBe(1)
177
+ mst = await mst.delete(layer1)
178
+ const root = await mst.stage()
179
+ const loaded = MST.load(blockstore, root)
180
+ const loadedLayer = await loaded.getLayer()
181
+ expect(loadedLayer).toBe(0)
182
+ })
183
+
184
+ // These are some tricky things that can come up that may not be included in a randomized tree
185
+
186
+ /**
187
+ * `f` gets added & it does two node splits (e is no longer grouped with g/h)
188
+ *
189
+ * * *
190
+ * _________|________ ____|_____
191
+ * | | | | | | | |
192
+ * * d * i * -> * f *
193
+ * __|__ __|__ __|__ __|__ __|___
194
+ * | | | | | | | | | | | | | | |
195
+ * a b c e g h j k l * d * * i *
196
+ * __|__ | _|_ __|__
197
+ * | | | | | | | | |
198
+ * a b c e g h j k l
199
+ *
200
+ */
201
+ it('handles splits that must go 2 deep', async () => {
202
+ const layer0 = [
203
+ '3j6hnk65jis2t',
204
+ '3j6hnk65jit2t',
205
+ '3j6hnk65jiu2t',
206
+ '3j6hnk65jne2t',
207
+ '3j6hnk65jnm2t',
208
+ '3j6hnk65jnn2t',
209
+ '3j6hnk65kvx2t',
210
+ '3j6hnk65kvy2t',
211
+ '3j6hnk65kvz2t',
212
+ ]
213
+ const layer1 = ['3j6hnk65jju2t', '3j6hnk65kve2t']
214
+ const layer2 = '3j6hnk65jng2t'
215
+ mst = await MST.create(blockstore, [], { fanout: 32 })
216
+ const cid = await util.randomCid()
217
+ for (const tid of layer0) {
218
+ mst = await mst.add(tid, cid)
219
+ }
220
+ for (const tid of layer1) {
221
+ mst = await mst.add(tid, cid)
222
+ }
223
+ mst = await mst.add(layer2, cid)
224
+ const layer = await mst.getLayer()
225
+ expect(layer).toBe(2)
226
+
227
+ const root = await mst.stage()
228
+ mst = MST.load(blockstore, root, { fanout: 32 })
229
+
230
+ const allTids = [...layer0, ...layer1, layer2]
231
+ for (const tid of allTids) {
232
+ const got = await mst.get(tid)
233
+ expect(cid.equals(got)).toBeTruthy()
234
+ }
235
+ })
236
+ /**
237
+ * `b` gets added & it hashes to 2 levels above any existing leaves
238
+ *
239
+ * * -> *
240
+ * __|__ __|__
241
+ * | | | | |
242
+ * a c * b *
243
+ * | |
244
+ * * *
245
+ * | |
246
+ * a c
247
+ *
248
+ */
249
+ it('handles new layers that are two higher than existing', async () => {
250
+ const layer0 = ['3j6hnk65jis2t', '3j6hnk65kvz2t']
251
+ const layer2 = '3j6hnk65jng2t'
252
+ mst = await MST.create(blockstore, [], { fanout: 32 })
253
+ const cid = await util.randomCid()
254
+ for (const tid of layer0) {
255
+ mst = await mst.add(tid, cid)
256
+ }
257
+ mst = await mst.add(layer2, cid)
258
+
259
+ const root = await mst.stage()
260
+ mst = MST.load(blockstore, root, { fanout: 32 })
261
+
262
+ const layer = await mst.getLayer()
263
+ expect(layer).toBe(2)
264
+ const allTids = [...layer0, layer2]
265
+ for (const tid of allTids) {
266
+ const got = await mst.get(tid)
267
+ expect(cid.equals(got)).toBeTruthy()
268
+ }
269
+ })
270
+ })
271
+
272
+ describe('utils', () => {
273
+ it('counts prefix length', () => {
274
+ expect(countPrefixLen('abc', 'abc')).toBe(3)
275
+ expect(countPrefixLen('', 'abc')).toBe(0)
276
+ expect(countPrefixLen('abc', '')).toBe(0)
277
+ expect(countPrefixLen('ab', 'abc')).toBe(2)
278
+ expect(countPrefixLen('abc', 'ab')).toBe(2)
279
+ })
280
+ })