@atproto/repo 0.0.1 → 0.1.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 (63) 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 +25 -0
  4. package/dist/data-diff.d.ts +36 -0
  5. package/dist/error.d.ts +20 -0
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.js +11605 -10399
  8. package/dist/index.js.map +4 -4
  9. package/dist/mst/diff.d.ts +4 -33
  10. package/dist/mst/mst.d.ts +68 -25
  11. package/dist/mst/util.d.ts +13 -5
  12. package/dist/parse.d.ts +16 -0
  13. package/dist/readable-repo.d.ts +22 -0
  14. package/dist/repo.d.ts +14 -30
  15. package/dist/storage/index.d.ts +4 -0
  16. package/dist/storage/memory-blockstore.d.ts +28 -0
  17. package/dist/storage/readable-blockstore.d.ts +24 -0
  18. package/dist/storage/repo-storage.d.ts +18 -0
  19. package/dist/storage/sync-storage.d.ts +15 -0
  20. package/dist/storage/types.d.ts +3 -0
  21. package/dist/sync/consumer.d.ts +18 -0
  22. package/dist/sync/index.d.ts +2 -0
  23. package/dist/sync/provider.d.ts +9 -0
  24. package/dist/types.d.ts +124 -317
  25. package/dist/util.d.ts +31 -12
  26. package/dist/verify.d.ts +26 -4
  27. package/package.json +4 -2
  28. package/src/block-map.ts +95 -0
  29. package/src/cid-set.ts +1 -2
  30. package/src/data-diff.ts +121 -0
  31. package/src/error.ts +31 -0
  32. package/src/index.ts +3 -1
  33. package/src/mst/diff.ts +120 -90
  34. package/src/mst/mst.ts +185 -184
  35. package/src/mst/util.ts +54 -31
  36. package/src/parse.ts +44 -0
  37. package/src/readable-repo.ts +75 -0
  38. package/src/repo.ts +119 -249
  39. package/src/storage/index.ts +4 -0
  40. package/src/storage/memory-blockstore.ts +114 -0
  41. package/src/storage/readable-blockstore.ts +56 -0
  42. package/src/storage/repo-storage.ts +42 -0
  43. package/src/storage/sync-storage.ts +35 -0
  44. package/src/storage/types.ts +3 -0
  45. package/src/sync/consumer.ts +137 -0
  46. package/src/sync/index.ts +2 -0
  47. package/src/sync/provider.ts +91 -0
  48. package/src/types.ts +101 -62
  49. package/src/util.ts +237 -56
  50. package/src/verify.ts +207 -42
  51. package/tests/_util.ts +132 -97
  52. package/tests/mst.test.ts +269 -122
  53. package/tests/repo.test.ts +48 -50
  54. package/tests/sync/checkout.test.ts +57 -0
  55. package/tests/sync/diff.test.ts +87 -0
  56. package/tests/sync/narrow.test.ts +145 -0
  57. package/tsconfig.build.tsbuildinfo +1 -1
  58. package/tsconfig.json +2 -1
  59. package/src/blockstore/index.ts +0 -2
  60. package/src/blockstore/ipld-store.ts +0 -103
  61. package/src/blockstore/memory-blockstore.ts +0 -49
  62. package/src/sync.ts +0 -38
  63. package/tests/sync.test.ts +0 -129
@@ -0,0 +1,56 @@
1
+ import { check } from '@atproto/common'
2
+ import { RepoRecord } from '@atproto/lexicon'
3
+ import { CID } from 'multiformats/cid'
4
+ import BlockMap from '../block-map'
5
+ import { MissingBlockError } from '../error'
6
+ import * as parse from '../parse'
7
+ import { cborToLexRecord } from '../util'
8
+
9
+ export abstract class ReadableBlockstore {
10
+ abstract getBytes(cid: CID): Promise<Uint8Array | null>
11
+ abstract has(cid: CID): Promise<boolean>
12
+ abstract getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }>
13
+
14
+ async attemptRead<T>(
15
+ cid: CID,
16
+ def: check.Def<T>,
17
+ ): Promise<{ obj: T; bytes: Uint8Array } | null> {
18
+ const bytes = await this.getBytes(cid)
19
+ if (!bytes) return null
20
+ return parse.parseObjByDef(bytes, cid, def)
21
+ }
22
+
23
+ async readObjAndBytes<T>(
24
+ cid: CID,
25
+ def: check.Def<T>,
26
+ ): Promise<{ obj: T; bytes: Uint8Array }> {
27
+ const read = await this.attemptRead(cid, def)
28
+ if (!read) {
29
+ throw new MissingBlockError(cid, def.name)
30
+ }
31
+ return read
32
+ }
33
+
34
+ async readObj<T>(cid: CID, def: check.Def<T>): Promise<T> {
35
+ const obj = await this.readObjAndBytes(cid, def)
36
+ return obj.obj
37
+ }
38
+
39
+ async attemptReadRecord(cid: CID): Promise<RepoRecord | null> {
40
+ try {
41
+ return await this.readRecord(cid)
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ async readRecord(cid: CID): Promise<RepoRecord> {
48
+ const bytes = await this.getBytes(cid)
49
+ if (!bytes) {
50
+ throw new MissingBlockError(cid)
51
+ }
52
+ return cborToLexRecord(bytes)
53
+ }
54
+ }
55
+
56
+ export default ReadableBlockstore
@@ -0,0 +1,42 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import BlockMap from '../block-map'
3
+ import { CommitBlockData, CommitData } from '../types'
4
+ import ReadableBlockstore from './readable-blockstore'
5
+
6
+ export abstract class RepoStorage extends ReadableBlockstore {
7
+ abstract getHead(forUpdate?: boolean): Promise<CID | null>
8
+ abstract getCommitPath(
9
+ latest: CID,
10
+ earliest: CID | null,
11
+ ): Promise<CID[] | null>
12
+ abstract getBlocksForCommits(
13
+ commits: CID[],
14
+ ): Promise<{ [commit: string]: BlockMap }>
15
+
16
+ abstract putBlock(cid: CID, block: Uint8Array): Promise<void>
17
+ abstract putMany(blocks: BlockMap): Promise<void>
18
+ abstract updateHead(cid: CID, prev: CID | null): Promise<void>
19
+ abstract indexCommits(commit: CommitData[]): Promise<void>
20
+
21
+ async applyCommit(commit: CommitData): Promise<void> {
22
+ await Promise.all([
23
+ this.indexCommits([commit]),
24
+ this.updateHead(commit.commit, commit.prev),
25
+ ])
26
+ }
27
+
28
+ async getCommits(
29
+ latest: CID,
30
+ earliest: CID | null,
31
+ ): Promise<CommitBlockData[] | null> {
32
+ const commitPath = await this.getCommitPath(latest, earliest)
33
+ if (!commitPath) return null
34
+ const blocksByCommit = await this.getBlocksForCommits(commitPath)
35
+ return commitPath.map((commit) => ({
36
+ commit,
37
+ blocks: blocksByCommit[commit.toString()] || new BlockMap(),
38
+ }))
39
+ }
40
+ }
41
+
42
+ export default RepoStorage
@@ -0,0 +1,35 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import BlockMap from '../block-map'
3
+ import ReadableBlockstore from './readable-blockstore'
4
+
5
+ export class SyncStorage extends ReadableBlockstore {
6
+ constructor(
7
+ public staged: ReadableBlockstore,
8
+ public saved: ReadableBlockstore,
9
+ ) {
10
+ super()
11
+ }
12
+
13
+ async getBytes(cid: CID): Promise<Uint8Array | null> {
14
+ const got = await this.staged.getBytes(cid)
15
+ if (got) return got
16
+ return this.saved.getBytes(cid)
17
+ }
18
+
19
+ async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> {
20
+ const fromStaged = await this.staged.getBlocks(cids)
21
+ const fromSaved = await this.saved.getBlocks(fromStaged.missing)
22
+ const blocks = fromStaged.blocks
23
+ blocks.addMap(fromSaved.blocks)
24
+ return {
25
+ blocks,
26
+ missing: fromSaved.missing,
27
+ }
28
+ }
29
+
30
+ async has(cid: CID): Promise<boolean> {
31
+ return (await this.staged.has(cid)) || (await this.saved.has(cid))
32
+ }
33
+ }
34
+
35
+ export default SyncStorage
@@ -5,8 +5,11 @@ export interface BlobStore {
5
5
  putTemp(bytes: Uint8Array | stream.Readable): Promise<string>
6
6
  makePermanent(key: string, cid: CID): Promise<void>
7
7
  putPermanent(cid: CID, bytes: Uint8Array | stream.Readable): Promise<void>
8
+ quarantine(cid: CID): Promise<void>
9
+ unquarantine(cid: CID): Promise<void>
8
10
  getBytes(cid: CID): Promise<Uint8Array>
9
11
  getStream(cid: CID): Promise<stream.Readable>
12
+ delete(cid: CID): Promise<void>
10
13
  }
11
14
 
12
15
  export class BlobNotFoundError extends Error {}
@@ -0,0 +1,137 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import { MemoryBlockstore, RepoStorage } from '../storage'
3
+ import Repo from '../repo'
4
+ import * as verify from '../verify'
5
+ import * as util from '../util'
6
+ import { CommitData, RepoContents, WriteLog } from '../types'
7
+ import CidSet from '../cid-set'
8
+ import { MissingBlocksError } from '../error'
9
+
10
+ // Checkouts
11
+ // -------------
12
+
13
+ export const loadCheckout = async (
14
+ storage: RepoStorage,
15
+ repoCar: Uint8Array,
16
+ did: string,
17
+ signingKey: string,
18
+ ): Promise<{ root: CID; contents: RepoContents }> => {
19
+ const { root, blocks } = await util.readCarWithRoot(repoCar)
20
+ const updateStorage = new MemoryBlockstore(blocks)
21
+ const checkout = await verify.verifyCheckout(
22
+ updateStorage,
23
+ root,
24
+ did,
25
+ signingKey,
26
+ )
27
+
28
+ const checkoutBlocks = await updateStorage.getBlocks(
29
+ checkout.newCids.toList(),
30
+ )
31
+ if (checkoutBlocks.missing.length > 0) {
32
+ throw new MissingBlocksError('sync', checkoutBlocks.missing)
33
+ }
34
+ await Promise.all([
35
+ storage.putMany(checkoutBlocks.blocks),
36
+ storage.updateHead(root, null),
37
+ ])
38
+
39
+ return {
40
+ root,
41
+ contents: checkout.contents,
42
+ }
43
+ }
44
+
45
+ // Diffs
46
+ // -------------
47
+
48
+ export const loadFullRepo = async (
49
+ storage: RepoStorage,
50
+ repoCar: Uint8Array,
51
+ did: string,
52
+ signingKey: string,
53
+ ): Promise<{ root: CID; writeLog: WriteLog }> => {
54
+ const { root, blocks } = await util.readCarWithRoot(repoCar)
55
+ const updateStorage = new MemoryBlockstore(blocks)
56
+ const updates = await verify.verifyFullHistory(
57
+ updateStorage,
58
+ root,
59
+ did,
60
+ signingKey,
61
+ )
62
+
63
+ const [writeLog] = await Promise.all([
64
+ persistUpdates(storage, updateStorage, updates),
65
+ storage.updateHead(root, null),
66
+ ])
67
+
68
+ return {
69
+ root,
70
+ writeLog,
71
+ }
72
+ }
73
+
74
+ export const loadDiff = async (
75
+ repo: Repo,
76
+ diffCar: Uint8Array,
77
+ did: string,
78
+ signingKey: string,
79
+ ): Promise<{ root: CID; writeLog: WriteLog }> => {
80
+ const { root, blocks } = await util.readCarWithRoot(diffCar)
81
+ const updateStorage = new MemoryBlockstore(blocks)
82
+ const updates = await verify.verifyUpdates(
83
+ repo,
84
+ updateStorage,
85
+ root,
86
+ did,
87
+ signingKey,
88
+ )
89
+
90
+ const [writeLog] = await Promise.all([
91
+ persistUpdates(repo.storage, updateStorage, updates),
92
+ repo.storage.updateHead(root, repo.cid),
93
+ ])
94
+
95
+ return {
96
+ root,
97
+ writeLog,
98
+ }
99
+ }
100
+
101
+ // Helpers
102
+ // -------------
103
+
104
+ export const persistUpdates = async (
105
+ storage: RepoStorage,
106
+ updateStorage: RepoStorage,
107
+ updates: verify.VerifiedUpdate[],
108
+ ): Promise<WriteLog> => {
109
+ const newCids = new CidSet()
110
+ for (const update of updates) {
111
+ newCids.addSet(update.newCids)
112
+ }
113
+
114
+ const diffBlocks = await updateStorage.getBlocks(newCids.toList())
115
+ if (diffBlocks.missing.length > 0) {
116
+ throw new MissingBlocksError('sync', diffBlocks.missing)
117
+ }
118
+ const commits: CommitData[] = updates.map((update) => {
119
+ const forCommit = diffBlocks.blocks.getMany(update.newCids.toList())
120
+ if (forCommit.missing.length > 0) {
121
+ throw new MissingBlocksError('sync', forCommit.missing)
122
+ }
123
+ return {
124
+ commit: update.commit,
125
+ prev: update.prev,
126
+ blocks: forCommit.blocks,
127
+ }
128
+ })
129
+
130
+ await storage.indexCommits(commits)
131
+
132
+ return Promise.all(
133
+ updates.map((upd) =>
134
+ util.diffToWriteDescripts(upd.diff, diffBlocks.blocks),
135
+ ),
136
+ )
137
+ }
@@ -0,0 +1,2 @@
1
+ export * from './consumer'
2
+ export * from './provider'
@@ -0,0 +1,91 @@
1
+ import { Commit, def, RecordPath } from '../types'
2
+ import { BlockWriter } from '@ipld/car/writer'
3
+ import { CID } from 'multiformats/cid'
4
+ import CidSet from '../cid-set'
5
+ import { MissingBlocksError } from '../error'
6
+ import { RepoStorage } from '../storage'
7
+ import * as util from '../util'
8
+ import { MST } from '../mst'
9
+
10
+ // Checkouts
11
+ // -------------
12
+
13
+ export const getCheckout = async (
14
+ storage: RepoStorage,
15
+ commitCid: CID,
16
+ ): Promise<Uint8Array> => {
17
+ return util.writeCar(commitCid, async (car: BlockWriter) => {
18
+ const commit = await storage.readObjAndBytes(commitCid, def.commit)
19
+ await car.put({ cid: commitCid, bytes: commit.bytes })
20
+ const mst = MST.load(storage, commit.obj.data)
21
+ await mst.writeToCarStream(car)
22
+ })
23
+ }
24
+
25
+ // Diffs
26
+ // -------------
27
+
28
+ export const getDiff = async (
29
+ storage: RepoStorage,
30
+ latest: CID,
31
+ earliest: CID | null,
32
+ ): Promise<Uint8Array> => {
33
+ return util.writeCar(latest, (car: BlockWriter) => {
34
+ return writeCommitsToCarStream(storage, car, latest, earliest)
35
+ })
36
+ }
37
+
38
+ export const getFullRepo = async (
39
+ storage: RepoStorage,
40
+ cid: CID,
41
+ ): Promise<Uint8Array> => {
42
+ return getDiff(storage, cid, null)
43
+ }
44
+
45
+ export const writeCommitsToCarStream = async (
46
+ storage: RepoStorage,
47
+ car: BlockWriter,
48
+ latest: CID,
49
+ earliest: CID | null,
50
+ ): Promise<void> => {
51
+ const commits = await storage.getCommits(latest, earliest)
52
+ if (commits === null) {
53
+ throw new Error('Could not find shared history')
54
+ }
55
+ if (commits.length === 0) return
56
+ for (const commit of commits) {
57
+ for (const entry of commit.blocks.entries()) {
58
+ await car.put(entry)
59
+ }
60
+ }
61
+ }
62
+
63
+ // Narrow slices
64
+ // -------------
65
+
66
+ export const getRecords = async (
67
+ storage: RepoStorage,
68
+ commitCid: CID,
69
+ paths: RecordPath[],
70
+ ): Promise<Uint8Array> => {
71
+ return util.writeCar(commitCid, async (car: BlockWriter) => {
72
+ const commit = await storage.readObjAndBytes(commitCid, def.commit)
73
+ await car.put({ cid: commitCid, bytes: commit.bytes })
74
+ const mst = MST.load(storage, commit.obj.data)
75
+ const cidsForPaths = await Promise.all(
76
+ paths.map((p) =>
77
+ mst.cidsForPath(util.formatDataKey(p.collection, p.rkey)),
78
+ ),
79
+ )
80
+ const allCids = cidsForPaths.reduce((acc, cur) => {
81
+ return acc.addSet(new CidSet(cur))
82
+ }, new CidSet())
83
+ const found = await storage.getBlocks(allCids.toList())
84
+ if (found.missing.length > 0) {
85
+ throw new MissingBlocksError('writeRecordsToCarStream', found.missing)
86
+ }
87
+ for (const block of found.blocks.entries()) {
88
+ await car.put(block)
89
+ }
90
+ })
91
+ }
package/src/types.ts CHANGED
@@ -1,88 +1,127 @@
1
1
  import { z } from 'zod'
2
2
  import { BlockWriter } from '@ipld/car/writer'
3
- import { def as common } from '@atproto/common'
3
+ import { schema as common, def as commonDef } from '@atproto/common'
4
4
  import { CID } from 'multiformats'
5
- import { DataDiff } from './mst'
5
+ import BlockMap from './block-map'
6
+ import { RepoRecord } from '@atproto/lexicon'
6
7
 
7
- const repoMeta = z.object({
8
+ // Repo nodes
9
+ // ---------------
10
+
11
+ const unsignedCommit = z.object({
8
12
  did: z.string(),
9
13
  version: z.number(),
10
- datastore: z.string(),
11
- })
12
- export type RepoMeta = z.infer<typeof repoMeta>
13
-
14
- const repoRoot = z.object({
15
- meta: common.cid,
16
14
  prev: common.cid.nullable(),
17
- auth_token: common.cid.nullable(),
18
15
  data: common.cid,
19
16
  })
20
- export type RepoRoot = z.infer<typeof repoRoot>
17
+ export type UnsignedCommit = z.infer<typeof unsignedCommit> & { sig?: never }
21
18
 
22
19
  const commit = z.object({
23
- root: common.cid,
20
+ did: z.string(),
21
+ version: z.number(),
22
+ prev: common.cid.nullable(),
23
+ data: common.cid,
24
24
  sig: common.bytes,
25
25
  })
26
26
  export type Commit = z.infer<typeof commit>
27
27
 
28
- export const cidCreateOp = z.object({
29
- action: z.literal('create'),
30
- collection: z.string(),
31
- rkey: z.string(),
32
- cid: common.cid,
33
- })
34
- export type CidCreateOp = z.infer<typeof cidCreateOp>
28
+ export const schema = {
29
+ ...common,
30
+ commit,
31
+ }
35
32
 
36
- export const cidUpdateOp = z.object({
37
- action: z.literal('update'),
38
- collection: z.string(),
39
- rkey: z.string(),
40
- cid: common.cid,
41
- })
42
- export type CidUpdateOp = z.infer<typeof cidUpdateOp>
33
+ export const def = {
34
+ ...commonDef,
35
+ commit: {
36
+ name: 'commit',
37
+ schema: schema.commit,
38
+ },
39
+ }
43
40
 
44
- export const deleteOp = z.object({
45
- action: z.literal('delete'),
46
- collection: z.string(),
47
- rkey: z.string(),
48
- })
49
- export type DeleteOp = z.infer<typeof deleteOp>
41
+ // Repo Operations
42
+ // ---------------
50
43
 
51
- export const cidWriteOp = z.union([cidCreateOp, cidUpdateOp, deleteOp])
52
- export type CidWriteOp = z.infer<typeof cidWriteOp>
44
+ export enum WriteOpAction {
45
+ Create = 'create',
46
+ Update = 'update',
47
+ Delete = 'delete',
48
+ }
53
49
 
54
- export const recordCreateOp = z.object({
55
- action: z.literal('create'),
56
- collection: z.string(),
57
- rkey: z.string(),
58
- value: z.any(),
59
- })
60
- export type RecordCreateOp = z.infer<typeof recordCreateOp>
50
+ export type RecordCreateOp = {
51
+ action: WriteOpAction.Create
52
+ collection: string
53
+ rkey: string
54
+ record: RepoRecord
55
+ }
61
56
 
62
- export const recordUpdateOp = z.object({
63
- action: z.literal('update'),
64
- collection: z.string(),
65
- rkey: z.string(),
66
- value: z.any(),
67
- })
68
- export type RecordUpdateOp = z.infer<typeof recordUpdateOp>
57
+ export type RecordUpdateOp = {
58
+ action: WriteOpAction.Update
59
+ collection: string
60
+ rkey: string
61
+ record: RepoRecord
62
+ }
69
63
 
70
- export const recordWriteOp = z.union([recordCreateOp, recordUpdateOp, deleteOp])
71
- export type RecordWriteOp = z.infer<typeof recordWriteOp>
64
+ export type RecordDeleteOp = {
65
+ action: WriteOpAction.Delete
66
+ collection: string
67
+ rkey: string
68
+ }
72
69
 
73
- export const def = {
74
- ...common,
75
- repoMeta,
76
- repoRoot,
77
- commit,
78
- cidWriteOp,
79
- recordWriteOp,
70
+ export type RecordWriteOp = RecordCreateOp | RecordUpdateOp | RecordDeleteOp
71
+
72
+ export type RecordCreateDescript = RecordCreateOp & {
73
+ cid: CID
80
74
  }
81
75
 
82
- export interface CarStreamable {
83
- writeToCarStream(car: BlockWriter): Promise<void>
76
+ export type RecordUpdateDescript = RecordUpdateOp & {
77
+ prev: CID
78
+ cid: CID
79
+ }
80
+
81
+ export type RecordDeleteDescript = RecordDeleteOp & {
82
+ cid: CID
83
+ }
84
+
85
+ export type RecordWriteDescript =
86
+ | RecordCreateDescript
87
+ | RecordUpdateDescript
88
+ | RecordDeleteDescript
89
+
90
+ export type WriteLog = RecordWriteDescript[][]
91
+
92
+ // Updates/Commits
93
+ // ---------------
94
+
95
+ export type CommitBlockData = {
96
+ commit: CID
97
+ blocks: BlockMap
84
98
  }
85
99
 
100
+ export type CommitData = CommitBlockData & {
101
+ prev: CID | null
102
+ }
103
+
104
+ export type RepoUpdate = CommitData & {
105
+ ops: RecordWriteOp[]
106
+ }
107
+
108
+ export type CollectionContents = Record<string, RepoRecord>
109
+ export type RepoContents = Record<string, CollectionContents>
110
+
111
+ export type RecordPath = {
112
+ collection: string
113
+ rkey: string
114
+ }
115
+
116
+ export type RecordClaim = {
117
+ collection: string
118
+ rkey: string
119
+ record: RepoRecord | null
120
+ }
121
+
122
+ // DataStores
123
+ // ---------------
124
+
86
125
  export type DataValue = {
87
126
  key: string
88
127
  value: CID
@@ -93,9 +132,9 @@ export interface DataStore {
93
132
  update(key: string, value: CID): Promise<DataStore>
94
133
  delete(key: string): Promise<DataStore>
95
134
  get(key: string): Promise<CID | null>
96
- list(count: number, after?: string, before?: string): Promise<DataValue[]>
135
+ list(count?: number, after?: string, before?: string): Promise<DataValue[]>
97
136
  listWithPrefix(prefix: string, count?: number): Promise<DataValue[]>
98
- diff(other: DataStore): Promise<DataDiff>
99
- stage(): Promise<CID>
137
+ getUnstoredBlocks(): Promise<{ root: CID; blocks: BlockMap }>
100
138
  writeToCarStream(car: BlockWriter): Promise<void>
139
+ cidsForPath(key: string): Promise<CID[]>
101
140
  }