@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/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
|
+
})
|