@atproto/repo 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/bench/mst.bench.ts +7 -4
  2. package/bench/repo.bench.ts +25 -16
  3. package/dist/block-map.d.ts +27 -0
  4. package/dist/data-diff.d.ts +36 -0
  5. package/dist/error.d.ts +20 -0
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.js +22870 -12456
  8. package/dist/index.js.map +4 -4
  9. package/dist/mst/diff.d.ts +4 -33
  10. package/dist/mst/mst.d.ts +73 -31
  11. package/dist/mst/util.d.ts +13 -5
  12. package/dist/parse.d.ts +16 -0
  13. package/dist/readable-repo.d.ts +23 -0
  14. package/dist/repo.d.ts +19 -31
  15. package/dist/src/block-map.d.ts +23 -0
  16. package/dist/src/blockstore/persistent-blockstore.d.ts +12 -0
  17. package/dist/src/cid-set.d.ts +14 -0
  18. package/dist/src/collection.d.ts +22 -0
  19. package/dist/src/data-diff.d.ts +34 -0
  20. package/dist/src/error.d.ts +21 -0
  21. package/dist/src/index.d.ts +7 -0
  22. package/dist/src/logger.d.ts +2 -0
  23. package/dist/src/mst/diff.d.ts +33 -0
  24. package/dist/src/mst/index.d.ts +4 -0
  25. package/dist/src/mst/mst.d.ts +106 -0
  26. package/dist/src/mst/util.d.ts +9 -0
  27. package/dist/src/mst/walker.d.ts +22 -0
  28. package/dist/src/parse.d.ts +11 -0
  29. package/dist/src/readable-repo.d.ts +25 -0
  30. package/dist/src/repo.d.ts +39 -0
  31. package/dist/src/storage/error.d.ts +22 -0
  32. package/dist/src/storage/index.d.ts +1 -0
  33. package/dist/src/storage/memory-blobstore.d.ts +1 -0
  34. package/dist/src/storage/memory-blockstore.d.ts +28 -0
  35. package/dist/src/storage/readable-blockstore.d.ts +21 -0
  36. package/dist/src/storage/repo-storage.d.ts +18 -0
  37. package/dist/src/storage/sync-storage.d.ts +15 -0
  38. package/dist/src/storage/types.d.ts +12 -0
  39. package/dist/src/storage/util.d.ts +17 -0
  40. package/dist/src/structure.d.ts +39 -0
  41. package/dist/src/sync/consumer.d.ts +19 -0
  42. package/dist/src/sync/index.d.ts +2 -0
  43. package/dist/src/sync/producer.d.ts +13 -0
  44. package/dist/src/sync/provider.d.ts +11 -0
  45. package/dist/src/types.d.ts +368 -0
  46. package/dist/src/util.d.ts +13 -0
  47. package/dist/src/verify.d.ts +5 -0
  48. package/dist/storage/index.d.ts +4 -0
  49. package/dist/storage/memory-blockstore.d.ts +29 -0
  50. package/dist/storage/readable-blockstore.d.ts +24 -0
  51. package/dist/storage/repo-storage.d.ts +19 -0
  52. package/dist/storage/sync-storage.d.ts +15 -0
  53. package/dist/storage/types.d.ts +4 -0
  54. package/dist/sync/consumer.d.ts +19 -0
  55. package/dist/sync/index.d.ts +2 -0
  56. package/dist/sync/provider.d.ts +9 -0
  57. package/dist/tsconfig.build.tsbuildinfo +1 -0
  58. package/dist/types.d.ts +137 -331
  59. package/dist/util.d.ts +35 -12
  60. package/dist/verify.d.ts +31 -4
  61. package/jest.bench.config.js +2 -1
  62. package/package.json +13 -6
  63. package/src/block-map.ts +103 -0
  64. package/src/cid-set.ts +1 -2
  65. package/src/data-diff.ts +117 -0
  66. package/src/error.ts +31 -0
  67. package/src/index.ts +4 -1
  68. package/src/mst/diff.ts +120 -90
  69. package/src/mst/mst.ts +179 -187
  70. package/src/mst/util.ts +54 -31
  71. package/src/parse.ts +44 -0
  72. package/src/readable-repo.ts +75 -0
  73. package/src/repo.ts +145 -244
  74. package/src/storage/index.ts +4 -0
  75. package/src/storage/memory-blockstore.ts +133 -0
  76. package/src/storage/readable-blockstore.ts +56 -0
  77. package/src/storage/repo-storage.ts +43 -0
  78. package/src/storage/sync-storage.ts +35 -0
  79. package/src/storage/types.ts +4 -0
  80. package/src/sync/consumer.ts +140 -0
  81. package/src/sync/index.ts +2 -0
  82. package/src/sync/provider.ts +91 -0
  83. package/src/types.ts +110 -73
  84. package/src/util.ts +258 -56
  85. package/src/verify.ts +248 -42
  86. package/tests/_util.ts +132 -97
  87. package/tests/mst.test.ts +269 -122
  88. package/tests/rebase.test.ts +37 -0
  89. package/tests/repo.test.ts +48 -50
  90. package/tests/sync/checkout.test.ts +75 -0
  91. package/tests/sync/diff.test.ts +92 -0
  92. package/tests/sync/narrow.test.ts +149 -0
  93. package/tests/util.test.ts +21 -0
  94. package/tsconfig.build.tsbuildinfo +1 -1
  95. package/tsconfig.json +2 -1
  96. package/src/blockstore/index.ts +0 -2
  97. package/src/blockstore/ipld-store.ts +0 -103
  98. package/src/blockstore/memory-blockstore.ts +0 -49
  99. package/src/sync.ts +0 -38
  100. package/tests/sync.test.ts +0 -129
  101. /package/dist/{blockstore → src/blockstore}/index.d.ts +0 -0
  102. /package/dist/{blockstore → src/blockstore}/ipld-store.d.ts +0 -0
  103. /package/dist/{blockstore → src/blockstore}/memory-blockstore.d.ts +0 -0
  104. /package/dist/{sync.d.ts → src/sync.d.ts} +0 -0
package/dist/util.d.ts CHANGED
@@ -1,13 +1,36 @@
1
+ /// <reference types="node" />
1
2
  import { CID } from 'multiformats/cid';
2
- import * as auth from '@atproto/auth';
3
- import { DataDiff } from './mst';
4
- import { IpldStore } from './blockstore';
5
- import { DataStore, RecordWriteOp } from './types';
6
- export declare const ucanForOperation: (prevData: DataStore, newData: DataStore, rootDid: string, authStore: auth.AuthStore) => Promise<string>;
7
- export declare const getCommitPath: (blockstore: IpldStore, earliest: CID | null, latest: CID) => Promise<CID[] | null>;
8
- export declare const getWriteOpLog: (blockstore: IpldStore, earliest: CID | null, latest: CID) => Promise<RecordWriteOp[][]>;
9
- export declare const diffToWriteOps: (blockstore: IpldStore, diff: DataDiff) => Promise<RecordWriteOp[]>;
10
- export declare const parseRecordKey: (key: string) => {
11
- collection: string;
12
- rkey: string;
13
- };
3
+ import { BlockWriter } from '@ipld/car/writer';
4
+ import { Block as CarBlock } from '@ipld/car/api';
5
+ import { LexValue, RepoRecord } from '@atproto/lexicon';
6
+ import DataDiff from './data-diff';
7
+ import { RepoStorage } from './storage';
8
+ import { Commit, RecordPath, RecordWriteDescript, UnsignedCommit, WriteLog } from './types';
9
+ import BlockMap from './block-map';
10
+ import { Keypair } from '@atproto/crypto';
11
+ import { Readable } from 'stream';
12
+ export declare function verifyIncomingCarBlocks(car: AsyncIterable<CarBlock>): AsyncIterable<CarBlock>;
13
+ export declare function writeCarStream(root: CID | null, fn: (car: BlockWriter) => Promise<void>): Readable;
14
+ export declare function writeCar(root: CID | null, fn: (car: BlockWriter) => Promise<void>): AsyncIterable<Uint8Array>;
15
+ export declare const blocksToCarStream: (root: CID | null, blocks: BlockMap) => AsyncIterable<Uint8Array>;
16
+ export declare const blocksToCarFile: (root: CID | null, blocks: BlockMap) => Promise<Uint8Array>;
17
+ export declare const readCar: (bytes: Uint8Array) => Promise<{
18
+ roots: CID[];
19
+ blocks: BlockMap;
20
+ }>;
21
+ export declare const readCarWithRoot: (bytes: Uint8Array) => Promise<{
22
+ root: CID;
23
+ blocks: BlockMap;
24
+ }>;
25
+ export declare const getWriteLog: (storage: RepoStorage, latest: CID, earliest: CID | null) => Promise<WriteLog>;
26
+ export declare const diffToWriteDescripts: (diff: DataDiff, blocks: BlockMap) => Promise<RecordWriteDescript[]>;
27
+ export declare const collapseWriteLog: (log: WriteLog) => RecordWriteDescript[];
28
+ export declare const collapseDiffs: (diffs: DataDiff[]) => DataDiff;
29
+ export declare const parseDataKey: (key: string) => RecordPath;
30
+ export declare const formatDataKey: (collection: string, rkey: string) => string;
31
+ export declare const metaEqual: (a: Commit, b: Commit) => boolean;
32
+ export declare const signCommit: (unsigned: UnsignedCommit, keypair: Keypair) => Promise<Commit>;
33
+ export declare const verifyCommitSig: (commit: Commit, didKey: string) => Promise<boolean>;
34
+ export declare const cborToLex: (val: Uint8Array) => LexValue;
35
+ export declare const cborToLexRecord: (val: Uint8Array) => RepoRecord;
36
+ export declare const cidForRecord: (val: LexValue) => Promise<CID>;
package/dist/verify.d.ts CHANGED
@@ -1,5 +1,32 @@
1
1
  import { CID } from 'multiformats/cid';
2
- import * as auth from '@atproto/auth';
3
- import { IpldStore } from './blockstore';
4
- import { DataDiff } from './mst';
5
- export declare const verifyUpdates: (blockstore: IpldStore, earliest: CID | null, latest: CID, verifier: auth.Verifier) => Promise<DataDiff>;
2
+ import { ReadableBlockstore, RepoStorage } from './storage';
3
+ import DataDiff from './data-diff';
4
+ import ReadableRepo from './readable-repo';
5
+ import CidSet from './cid-set';
6
+ import { RecordClaim, RepoContents, RepoContentsWithCids } from './types';
7
+ export declare type VerifiedCheckout = {
8
+ contents: RepoContents;
9
+ newCids: CidSet;
10
+ };
11
+ export declare type VerifiedCheckoutWithCids = {
12
+ commit: CID;
13
+ contents: RepoContentsWithCids;
14
+ };
15
+ export declare const verifyCheckout: (storage: ReadableBlockstore, head: CID, did: string, signingKey: string) => Promise<VerifiedCheckout>;
16
+ export declare const verifyCheckoutWithCids: (storage: ReadableBlockstore, head: CID, did: string, signingKey: string) => Promise<VerifiedCheckoutWithCids>;
17
+ export declare type VerifiedUpdate = {
18
+ commit: CID;
19
+ prev: CID | null;
20
+ diff: DataDiff;
21
+ newCids: CidSet;
22
+ };
23
+ export declare const verifyFullHistory: (storage: RepoStorage, head: CID, did: string, signingKey: string) => Promise<VerifiedUpdate[]>;
24
+ export declare const verifyUpdates: (repo: ReadableRepo, updateStorage: RepoStorage, updateRoot: CID, did: string, signingKey: string) => Promise<VerifiedUpdate[]>;
25
+ export declare const verifyCommitPath: (baseRepo: ReadableRepo, storage: ReadableBlockstore, commitPath: CID[], did: string, signingKey: string) => Promise<VerifiedUpdate[]>;
26
+ export declare const verifyProofs: (proofs: Uint8Array, claims: RecordClaim[], did: string, didKey: string) => Promise<{
27
+ verified: RecordClaim[];
28
+ unverified: RecordClaim[];
29
+ }>;
30
+ export declare const verifyRecords: (proofs: Uint8Array, did: string, signingKey: string) => Promise<RecordClaim[]>;
31
+ export declare class RepoVerificationError extends Error {
32
+ }
@@ -2,6 +2,7 @@ const base = require('./jest.config')
2
2
 
3
3
  module.exports = {
4
4
  ...base,
5
- testRegex: '(/bench/.*.bench)',
5
+ roots: ['<rootDir>/bench'],
6
+ testRegex: '(.*.bench)',
6
7
  testTimeout: 3000000,
7
8
  }
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "@atproto/repo",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/bluesky-social/atproto.git",
9
+ "directory": "packages/repo"
10
+ },
6
11
  "scripts": {
7
12
  "test": "jest",
8
13
  "test:profile": "node --inspect ../../node_modules/.bin/jest",
9
14
  "bench": "jest --config jest.bench.config.js",
10
- "bench:profile": "node --inspect ../../node_modules/.bin/jest --config jest.bench.config.js",
11
- "prettier": "prettier --check src/",
12
- "prettier:fix": "prettier --write src/",
15
+ "bench:profile": "node --inspect-brk ../../node_modules/.bin/jest --config jest.bench.config.js",
16
+ "prettier": "prettier --check src/ tests/",
17
+ "prettier:fix": "prettier --write src/ tests/",
13
18
  "lint": "eslint . --ext .ts,.tsx",
14
19
  "lint:fix": "yarn lint --fix",
15
20
  "verify": "run-p prettier lint",
@@ -22,13 +27,15 @@
22
27
  "postpublish": "npm run update-main-to-src"
23
28
  },
24
29
  "dependencies": {
25
- "@atproto/auth": "*",
26
30
  "@atproto/common": "*",
31
+ "@atproto/crypto": "*",
32
+ "@atproto/identity": "*",
33
+ "@atproto/lexicon": "*",
27
34
  "@atproto/nsid": "*",
28
35
  "@ipld/car": "^3.2.3",
29
36
  "@ipld/dag-cbor": "^7.0.0",
30
37
  "multiformats": "^9.6.4",
31
38
  "uint8arrays": "3.0.0",
32
- "zod": "^3.14.2"
39
+ "zod": "^3.21.4"
33
40
  }
34
41
  }
@@ -0,0 +1,103 @@
1
+ import { lexToIpld, LexValue } from '@atproto/lexicon'
2
+ import { dataToCborBlock } from '@atproto/common'
3
+ import { CID } from 'multiformats/cid'
4
+ import * as uint8arrays from 'uint8arrays'
5
+
6
+ export class BlockMap {
7
+ private map: Map<string, Uint8Array> = new Map()
8
+
9
+ async add(value: LexValue): Promise<CID> {
10
+ const block = await dataToCborBlock(lexToIpld(value))
11
+ this.set(block.cid, block.bytes)
12
+ return block.cid
13
+ }
14
+
15
+ set(cid: CID, bytes: Uint8Array) {
16
+ this.map.set(cid.toString(), bytes)
17
+ }
18
+
19
+ get(cid: CID): Uint8Array | undefined {
20
+ return this.map.get(cid.toString())
21
+ }
22
+
23
+ delete(cid: CID) {
24
+ this.map.delete(cid.toString())
25
+ }
26
+
27
+ getMany(cids: CID[]): { blocks: BlockMap; missing: CID[] } {
28
+ const missing: CID[] = []
29
+ const blocks = new BlockMap()
30
+ for (const cid of cids) {
31
+ const got = this.map.get(cid.toString())
32
+ if (got) {
33
+ blocks.set(cid, got)
34
+ } else {
35
+ missing.push(cid)
36
+ }
37
+ }
38
+ return { blocks, missing }
39
+ }
40
+
41
+ has(cid: CID): boolean {
42
+ return this.map.has(cid.toString())
43
+ }
44
+
45
+ clear(): void {
46
+ this.map.clear()
47
+ }
48
+
49
+ forEach(cb: (bytes: Uint8Array, cid: CID) => void): void {
50
+ this.map.forEach((val, key) => cb(val, CID.parse(key)))
51
+ }
52
+
53
+ entries(): Entry[] {
54
+ const entries: Entry[] = []
55
+ this.forEach((bytes, cid) => {
56
+ entries.push({ cid, bytes })
57
+ })
58
+ return entries
59
+ }
60
+
61
+ cids(): CID[] {
62
+ return this.entries().map((e) => e.cid)
63
+ }
64
+
65
+ addMap(toAdd: BlockMap) {
66
+ toAdd.forEach((bytes, cid) => {
67
+ this.set(cid, bytes)
68
+ })
69
+ }
70
+
71
+ get size(): number {
72
+ return this.map.size
73
+ }
74
+
75
+ get byteSize(): number {
76
+ let size = 0
77
+ this.forEach((bytes) => {
78
+ size += bytes.length
79
+ })
80
+ return size
81
+ }
82
+
83
+ equals(other: BlockMap): boolean {
84
+ if (this.size !== other.size) {
85
+ return false
86
+ }
87
+ for (const entry of this.entries()) {
88
+ const otherBytes = other.get(entry.cid)
89
+ if (!otherBytes) return false
90
+ if (!uint8arrays.equals(entry.bytes, otherBytes)) {
91
+ return false
92
+ }
93
+ }
94
+ return true
95
+ }
96
+ }
97
+
98
+ type Entry = {
99
+ cid: CID
100
+ bytes: Uint8Array
101
+ }
102
+
103
+ export default BlockMap
package/src/cid-set.ts CHANGED
@@ -42,8 +42,7 @@ export class CidSet {
42
42
  }
43
43
 
44
44
  toList(): CID[] {
45
- const arr = [...this.set]
46
- return arr.map((c) => CID.parse(c))
45
+ return [...this.set].map((c) => CID.parse(c))
47
46
  }
48
47
  }
49
48
 
@@ -0,0 +1,117 @@
1
+ import { CID } from 'multiformats'
2
+ import CidSet from './cid-set'
3
+ import { MST, mstDiff } from './mst'
4
+
5
+ export class DataDiff {
6
+ adds: Record<string, DataAdd> = {}
7
+ updates: Record<string, DataUpdate> = {}
8
+ deletes: Record<string, DataDelete> = {}
9
+
10
+ newCids: CidSet = new CidSet()
11
+ removedCids: CidSet = new CidSet()
12
+
13
+ static async of(curr: MST, prev: MST | null): Promise<DataDiff> {
14
+ return mstDiff(curr, prev)
15
+ }
16
+
17
+ recordAdd(key: string, cid: CID): void {
18
+ this.adds[key] = { key, cid }
19
+ this.newCids.add(cid)
20
+ }
21
+
22
+ recordUpdate(key: string, prev: CID, cid: CID): void {
23
+ this.updates[key] = { key, prev, cid }
24
+ this.newCids.add(cid)
25
+ }
26
+
27
+ recordDelete(key: string, cid: CID): void {
28
+ this.deletes[key] = { key, cid }
29
+ }
30
+
31
+ recordNewCid(cid: CID): void {
32
+ if (this.removedCids.has(cid)) {
33
+ this.removedCids.delete(cid)
34
+ } else {
35
+ this.newCids.add(cid)
36
+ }
37
+ }
38
+
39
+ recordRemovedCid(cid: CID): void {
40
+ if (this.newCids.has(cid)) {
41
+ this.newCids.delete(cid)
42
+ } else {
43
+ this.removedCids.add(cid)
44
+ }
45
+ }
46
+
47
+ addDiff(diff: DataDiff) {
48
+ for (const add of diff.addList()) {
49
+ if (this.deletes[add.key]) {
50
+ const del = this.deletes[add.key]
51
+ if (del.cid !== add.cid) {
52
+ this.recordUpdate(add.key, del.cid, add.cid)
53
+ }
54
+ delete this.deletes[add.key]
55
+ } else {
56
+ this.recordAdd(add.key, add.cid)
57
+ }
58
+ }
59
+ for (const update of diff.updateList()) {
60
+ this.recordUpdate(update.key, update.prev, update.cid)
61
+ delete this.adds[update.key]
62
+ delete this.deletes[update.key]
63
+ }
64
+ for (const del of diff.deleteList()) {
65
+ if (this.adds[del.key]) {
66
+ delete this.adds[del.key]
67
+ } else {
68
+ delete this.updates[del.key]
69
+ this.recordDelete(del.key, del.cid)
70
+ }
71
+ }
72
+ this.newCids.addSet(diff.newCids)
73
+ }
74
+
75
+ addList(): DataAdd[] {
76
+ return Object.values(this.adds)
77
+ }
78
+
79
+ updateList(): DataUpdate[] {
80
+ return Object.values(this.updates)
81
+ }
82
+
83
+ deleteList(): DataDelete[] {
84
+ return Object.values(this.deletes)
85
+ }
86
+
87
+ newCidList(): CID[] {
88
+ return this.newCids.toList()
89
+ }
90
+
91
+ updatedKeys(): string[] {
92
+ const keys = [
93
+ ...Object.keys(this.adds),
94
+ ...Object.keys(this.updates),
95
+ ...Object.keys(this.deletes),
96
+ ]
97
+ return [...new Set(keys)]
98
+ }
99
+ }
100
+
101
+ export type DataAdd = {
102
+ key: string
103
+ cid: CID
104
+ }
105
+
106
+ export type DataUpdate = {
107
+ key: string
108
+ prev: CID
109
+ cid: CID
110
+ }
111
+
112
+ export type DataDelete = {
113
+ key: string
114
+ cid: CID
115
+ }
116
+
117
+ export default DataDiff
package/src/error.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { CID } from 'multiformats/cid'
2
+
3
+ export class MissingBlockError extends Error {
4
+ constructor(public cid: CID, def?: string) {
5
+ let msg = `block not found: ${cid.toString()}`
6
+ if (def) {
7
+ msg += `, expected type: ${def}`
8
+ }
9
+ super(msg)
10
+ }
11
+ }
12
+
13
+ export class MissingBlocksError extends Error {
14
+ constructor(public context: string, public cids: CID[]) {
15
+ const cidStr = cids.map((c) => c.toString())
16
+ super(`missing ${context} blocks: ${cidStr}`)
17
+ }
18
+ }
19
+
20
+ export class MissingCommitBlocksError extends Error {
21
+ constructor(public commit: CID, public cids: CID[]) {
22
+ const cidStr = cids.map((c) => c.toString())
23
+ super(`missing blocks for commit ${commit.toString()}: ${cidStr}`)
24
+ }
25
+ }
26
+
27
+ export class UnexpectedObjectError extends Error {
28
+ constructor(public cid: CID, public def: string) {
29
+ super(`unexpected object at ${cid.toString()}, expected: ${def}`)
30
+ }
31
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,10 @@
1
- export * from './blockstore'
1
+ export * from './block-map'
2
+ export * from './cid-set'
2
3
  export * from './repo'
3
4
  export * from './mst'
4
5
  export * from './storage'
6
+ export * from './sync'
5
7
  export * from './types'
6
8
  export * from './verify'
9
+ export * from './data-diff'
7
10
  export * from './util'
package/src/mst/diff.ts CHANGED
@@ -1,106 +1,136 @@
1
- import * as auth from '@atproto/auth'
2
- import { CID } from 'multiformats'
3
- import CidSet from '../cid-set'
4
- import { parseRecordKey } from '../util'
5
-
6
- export class DataDiff {
7
- adds: Record<string, DataAdd> = {}
8
- updates: Record<string, DataUpdate> = {}
9
- deletes: Record<string, DataDelete> = {}
10
-
11
- newCids: CidSet = new CidSet()
12
-
13
- recordAdd(key: string, cid: CID): void {
14
- this.adds[key] = { key, cid }
15
- this.newCids.add(cid)
16
- }
17
-
18
- recordUpdate(key: string, prev: CID, cid: CID): void {
19
- this.updates[key] = { key, prev, cid }
20
- this.newCids.add(cid)
21
- }
22
-
23
- recordDelete(key: string, cid: CID): void {
24
- this.deletes[key] = { key, cid }
1
+ import { DataDiff } from '../data-diff'
2
+ import MST from './mst'
3
+ import MstWalker from './walker'
4
+
5
+ export const nullDiff = async (tree: MST): Promise<DataDiff> => {
6
+ const diff = new DataDiff()
7
+ for await (const entry of tree.walk()) {
8
+ if (entry.isLeaf()) {
9
+ diff.recordAdd(entry.key, entry.value)
10
+ } else {
11
+ diff.recordNewCid(entry.pointer)
12
+ }
25
13
  }
14
+ return diff
15
+ }
26
16
 
27
- recordNewCid(cid: CID): void {
28
- this.newCids.add(cid)
17
+ export const mstDiff = async (
18
+ curr: MST,
19
+ prev: MST | null,
20
+ ): Promise<DataDiff> => {
21
+ await curr.getPointer()
22
+ if (prev === null) {
23
+ return nullDiff(curr)
29
24
  }
30
25
 
31
- addDiff(diff: DataDiff) {
32
- for (const add of diff.addList()) {
33
- if (this.deletes[add.key]) {
34
- const del = this.deletes[add.key]
35
- if (del.cid !== add.cid) {
36
- this.recordUpdate(add.key, del.cid, add.cid)
37
- }
38
- delete this.deletes[add.key]
26
+ await prev.getPointer()
27
+ const diff = new DataDiff()
28
+
29
+ const leftWalker = new MstWalker(prev)
30
+ const rightWalker = new MstWalker(curr)
31
+ while (!leftWalker.status.done || !rightWalker.status.done) {
32
+ // if one walker is finished, continue walking the other & logging all nodes
33
+ if (leftWalker.status.done && !rightWalker.status.done) {
34
+ const node = rightWalker.status.curr
35
+ if (node.isLeaf()) {
36
+ diff.recordAdd(node.key, node.value)
39
37
  } else {
40
- this.recordAdd(add.key, add.cid)
38
+ diff.recordNewCid(node.pointer)
41
39
  }
40
+ await rightWalker.advance()
41
+ continue
42
+ } else if (!leftWalker.status.done && rightWalker.status.done) {
43
+ const node = leftWalker.status.curr
44
+ if (node.isLeaf()) {
45
+ diff.recordDelete(node.key, node.value)
46
+ } else {
47
+ diff.recordRemovedCid(node.pointer)
48
+ }
49
+ await leftWalker.advance()
50
+ continue
42
51
  }
43
- for (const update of diff.updateList()) {
44
- this.recordUpdate(update.key, update.prev, update.cid)
45
- delete this.adds[update.key]
46
- delete this.deletes[update.key]
47
- }
48
- for (const del of diff.deleteList()) {
49
- if (this.adds[del.key]) {
50
- delete this.adds[del.key]
52
+ if (leftWalker.status.done || rightWalker.status.done) break
53
+ const left = leftWalker.status.curr
54
+ const right = rightWalker.status.curr
55
+ if (left === null || right === null) break
56
+
57
+ // if both pointers are leaves, record an update & advance both or record the lowest key and advance that pointer
58
+ if (left.isLeaf() && right.isLeaf()) {
59
+ if (left.key === right.key) {
60
+ if (!left.value.equals(right.value)) {
61
+ diff.recordUpdate(left.key, left.value, right.value)
62
+ }
63
+ await leftWalker.advance()
64
+ await rightWalker.advance()
65
+ } else if (left.key < right.key) {
66
+ diff.recordDelete(left.key, left.value)
67
+ await leftWalker.advance()
51
68
  } else {
52
- delete this.updates[del.key]
53
- this.recordDelete(del.key, del.cid)
69
+ diff.recordAdd(right.key, right.value)
70
+ await rightWalker.advance()
54
71
  }
72
+ continue
55
73
  }
56
- this.newCids.addSet(diff.newCids)
57
- }
58
74
 
59
- addList(): DataAdd[] {
60
- return Object.values(this.adds)
61
- }
62
-
63
- updateList(): DataUpdate[] {
64
- return Object.values(this.updates)
65
- }
66
-
67
- deleteList(): DataDelete[] {
68
- return Object.values(this.deletes)
69
- }
75
+ // next, ensure that we're on the same layer
76
+ // if one walker is at a higher layer than the other, we need to do one of two things
77
+ // if the higher walker is pointed at a tree, step into that tree to try to catch up with the lower
78
+ // if the higher walker is pointed at a leaf, then advance the lower walker to try to catch up the higher
79
+ if (leftWalker.layer() > rightWalker.layer()) {
80
+ if (left.isLeaf()) {
81
+ if (right.isLeaf()) {
82
+ diff.recordAdd(right.key, right.value)
83
+ } else {
84
+ diff.recordNewCid(right.pointer)
85
+ }
86
+ await rightWalker.advance()
87
+ } else {
88
+ diff.recordRemovedCid(left.pointer)
89
+ await leftWalker.stepInto()
90
+ }
91
+ continue
92
+ } else if (leftWalker.layer() < rightWalker.layer()) {
93
+ if (right.isLeaf()) {
94
+ if (left.isLeaf()) {
95
+ diff.recordDelete(left.key, left.value)
96
+ } else {
97
+ diff.recordRemovedCid(left.pointer)
98
+ }
99
+ await leftWalker.advance()
100
+ } else {
101
+ diff.recordNewCid(right.pointer)
102
+ await rightWalker.stepInto()
103
+ }
104
+ continue
105
+ }
70
106
 
71
- newCidList(): CID[] {
72
- return this.newCids.toList()
73
- }
107
+ // if we're on the same level, and both pointers are trees, do a comparison
108
+ // if they're the same, step over. if they're different, step in to find the subdiff
109
+ if (left.isTree() && right.isTree()) {
110
+ if (left.pointer.equals(right.pointer)) {
111
+ await leftWalker.stepOver()
112
+ await rightWalker.stepOver()
113
+ } else {
114
+ diff.recordNewCid(right.pointer)
115
+ diff.recordRemovedCid(left.pointer)
116
+ await leftWalker.stepInto()
117
+ await rightWalker.stepInto()
118
+ }
119
+ continue
120
+ }
74
121
 
75
- updatedKeys(): string[] {
76
- const keys = [
77
- ...Object.keys(this.adds),
78
- ...Object.keys(this.updates),
79
- ...Object.keys(this.deletes),
80
- ]
81
- return [...new Set(keys)]
82
- }
122
+ // finally, if one pointer is a tree and the other is a leaf, simply step into the tree
123
+ if (left.isLeaf() && right.isTree()) {
124
+ diff.recordNewCid(right.pointer)
125
+ await rightWalker.stepInto()
126
+ continue
127
+ } else if (left.isTree() && right.isLeaf()) {
128
+ diff.recordRemovedCid(left.pointer)
129
+ await leftWalker.stepInto()
130
+ continue
131
+ }
83
132
 
84
- neededCapabilities(rootDid: string): auth.ucans.Capability[] {
85
- return this.updatedKeys().map((key) => {
86
- const { collection, rkey } = parseRecordKey(key)
87
- return auth.writeCap(rootDid, collection, rkey)
88
- })
133
+ throw new Error('Unidentifiable case in diff walk')
89
134
  }
90
- }
91
-
92
- export type DataAdd = {
93
- key: string
94
- cid: CID
95
- }
96
-
97
- export type DataUpdate = {
98
- key: string
99
- prev: CID
100
- cid: CID
101
- }
102
-
103
- export type DataDelete = {
104
- key: string
105
- cid: CID
135
+ return diff
106
136
  }