@atproto/repo 0.1.0 → 0.3.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 (49) hide show
  1. package/dist/block-map.d.ts +2 -0
  2. package/dist/data-diff.d.ts +12 -10
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +12388 -4431
  5. package/dist/index.js.map +4 -4
  6. package/dist/mst/mst.d.ts +19 -16
  7. package/dist/readable-repo.d.ts +4 -3
  8. package/dist/repo.d.ts +3 -2
  9. package/dist/storage/index.d.ts +0 -1
  10. package/dist/storage/memory-blockstore.d.ts +6 -10
  11. package/dist/storage/types.d.ts +29 -0
  12. package/dist/sync/consumer.d.ts +13 -16
  13. package/dist/sync/provider.d.ts +2 -6
  14. package/dist/types.d.ts +236 -48
  15. package/dist/util.d.ts +9 -7
  16. package/jest.bench.config.js +2 -1
  17. package/package.json +12 -7
  18. package/src/block-map.ts +8 -0
  19. package/src/data-diff.ts +47 -49
  20. package/src/index.ts +1 -1
  21. package/src/mst/diff.ts +14 -36
  22. package/src/mst/mst.ts +15 -14
  23. package/src/readable-repo.ts +5 -5
  24. package/src/repo.ts +50 -40
  25. package/src/storage/index.ts +0 -1
  26. package/src/storage/memory-blockstore.ts +19 -59
  27. package/src/storage/types.ts +30 -0
  28. package/src/sync/consumer.ts +170 -113
  29. package/src/sync/provider.ts +6 -44
  30. package/src/types.ts +49 -25
  31. package/src/util.ts +57 -91
  32. package/tests/_util.ts +38 -67
  33. package/tests/mst.test.ts +4 -1
  34. package/tests/{sync/narrow.test.ts → proofs.test.ts} +14 -21
  35. package/tests/repo.test.ts +5 -4
  36. package/tests/sync.test.ts +97 -0
  37. package/tests/util.test.ts +21 -0
  38. package/tsconfig.build.tsbuildinfo +1 -1
  39. package/tsconfig.json +1 -1
  40. package/dist/blockstore/index.d.ts +0 -2
  41. package/dist/blockstore/ipld-store.d.ts +0 -27
  42. package/dist/blockstore/memory-blockstore.d.ts +0 -13
  43. package/dist/storage/repo-storage.d.ts +0 -18
  44. package/dist/sync.d.ts +0 -9
  45. package/dist/verify.d.ts +0 -27
  46. package/src/storage/repo-storage.ts +0 -42
  47. package/src/verify.ts +0 -227
  48. package/tests/sync/checkout.test.ts +0 -57
  49. package/tests/sync/diff.test.ts +0 -87
package/src/util.ts CHANGED
@@ -4,36 +4,34 @@ import { CarReader } from '@ipld/car/reader'
4
4
  import { BlockWriter, CarWriter } from '@ipld/car/writer'
5
5
  import { Block as CarBlock } from '@ipld/car/api'
6
6
  import {
7
- streamToArray,
7
+ streamToBuffer,
8
8
  verifyCidForBytes,
9
9
  cborDecode,
10
10
  check,
11
11
  schema,
12
12
  cidForCbor,
13
+ byteIterableToStream,
14
+ TID,
13
15
  } from '@atproto/common'
14
16
  import { ipldToLex, lexToIpld, LexValue, RepoRecord } from '@atproto/lexicon'
15
17
 
16
18
  import * as crypto from '@atproto/crypto'
17
- import Repo from './repo'
18
- import { MST } from './mst'
19
19
  import DataDiff from './data-diff'
20
- import { RepoStorage } from './storage'
21
20
  import {
22
21
  Commit,
23
- DataStore,
22
+ LegacyV2Commit,
24
23
  RecordCreateDescript,
25
24
  RecordDeleteDescript,
26
25
  RecordPath,
27
26
  RecordUpdateDescript,
28
27
  RecordWriteDescript,
29
28
  UnsignedCommit,
30
- WriteLog,
31
29
  WriteOpAction,
32
30
  } from './types'
33
31
  import BlockMap from './block-map'
34
- import { MissingBlocksError } from './error'
35
32
  import * as parse from './parse'
36
33
  import { Keypair } from '@atproto/crypto'
34
+ import { Readable } from 'stream'
37
35
 
38
36
  export async function* verifyIncomingCarBlocks(
39
37
  car: AsyncIterable<CarBlock>,
@@ -44,25 +42,37 @@ export async function* verifyIncomingCarBlocks(
44
42
  }
45
43
  }
46
44
 
47
- export const writeCar = async (
45
+ // we have to turn the car writer output into a stream in order to properly handle errors
46
+ export function writeCarStream(
48
47
  root: CID | null,
49
48
  fn: (car: BlockWriter) => Promise<void>,
50
- ): Promise<Uint8Array> => {
49
+ ): Readable {
51
50
  const { writer, out } =
52
51
  root !== null ? CarWriter.create(root) : CarWriter.create()
53
- const bytes = streamToArray(out)
54
- try {
55
- await fn(writer)
56
- } finally {
57
- writer.close()
52
+
53
+ const stream = byteIterableToStream(out)
54
+ fn(writer)
55
+ .catch((err) => {
56
+ stream.destroy(err)
57
+ })
58
+ .finally(() => writer.close())
59
+ return stream
60
+ }
61
+
62
+ export async function* writeCar(
63
+ root: CID | null,
64
+ fn: (car: BlockWriter) => Promise<void>,
65
+ ): AsyncIterable<Uint8Array> {
66
+ const stream = writeCarStream(root, fn)
67
+ for await (const chunk of stream) {
68
+ yield chunk
58
69
  }
59
- return bytes
60
70
  }
61
71
 
62
- export const blocksToCar = async (
72
+ export const blocksToCarStream = (
63
73
  root: CID | null,
64
74
  blocks: BlockMap,
65
- ): Promise<Uint8Array> => {
75
+ ): AsyncIterable<Uint8Array> => {
66
76
  return writeCar(root, async (writer) => {
67
77
  for (const entry of blocks.entries()) {
68
78
  await writer.put(entry)
@@ -70,6 +80,14 @@ export const blocksToCar = async (
70
80
  })
71
81
  }
72
82
 
83
+ export const blocksToCarFile = (
84
+ root: CID | null,
85
+ blocks: BlockMap,
86
+ ): Promise<Uint8Array> => {
87
+ const carStream = blocksToCarStream(root, blocks)
88
+ return streamToBuffer(carStream)
89
+ }
90
+
73
91
  export const readCar = async (
74
92
  bytes: Uint8Array,
75
93
  ): Promise<{ roots: CID[]; blocks: BlockMap }> => {
@@ -99,33 +117,6 @@ export const readCarWithRoot = async (
99
117
  }
100
118
  }
101
119
 
102
- export const getWriteLog = async (
103
- storage: RepoStorage,
104
- latest: CID,
105
- earliest: CID | null,
106
- ): Promise<WriteLog> => {
107
- const commits = await storage.getCommitPath(latest, earliest)
108
- if (!commits) throw new Error('Could not find shared history')
109
- const heads = await Promise.all(commits.map((c) => Repo.load(storage, c)))
110
- // Turn commit path into list of diffs
111
- let prev: DataStore = await MST.create(storage) // Empty
112
- const msts = heads.map((h) => h.data)
113
- const diffs: DataDiff[] = []
114
- for (const mst of msts) {
115
- diffs.push(await DataDiff.of(mst, prev))
116
- prev = mst
117
- }
118
- const fullDiff = collapseDiffs(diffs)
119
- const diffBlocks = await storage.getBlocks(fullDiff.newCidList())
120
- if (diffBlocks.missing.length > 0) {
121
- throw new MissingBlocksError('write op log', diffBlocks.missing)
122
- }
123
- // Map MST diffs to write ops
124
- return Promise.all(
125
- diffs.map((diff) => diffToWriteDescripts(diff, diffBlocks.blocks)),
126
- )
127
- }
128
-
129
120
  export const diffToWriteDescripts = (
130
121
  diff: DataDiff,
131
122
  blocks: BlockMap,
@@ -166,55 +157,18 @@ export const diffToWriteDescripts = (
166
157
  ])
167
158
  }
168
159
 
169
- export const collapseWriteLog = (log: WriteLog): RecordWriteDescript[] => {
170
- const creates: Record<string, RecordCreateDescript> = {}
171
- const updates: Record<string, RecordUpdateDescript> = {}
172
- const deletes: Record<string, RecordDeleteDescript> = {}
173
- for (const commit of log) {
174
- for (const op of commit) {
175
- const key = op.collection + '/' + op.rkey
176
- if (op.action === WriteOpAction.Create) {
177
- const del = deletes[key]
178
- if (del) {
179
- if (del.cid !== op.cid) {
180
- updates[key] = {
181
- ...op,
182
- action: WriteOpAction.Update,
183
- prev: del.cid,
184
- }
185
- }
186
- delete deletes[key]
187
- } else {
188
- creates[key] = op
189
- }
190
- } else if (op.action === WriteOpAction.Update) {
191
- updates[key] = op
192
- delete creates[key]
193
- delete deletes[key]
194
- } else if (op.action === WriteOpAction.Delete) {
195
- if (creates[key]) {
196
- delete creates[key]
197
- } else {
198
- delete updates[key]
199
- deletes[key] = op
200
- }
201
- } else {
202
- throw new Error(`unknown action: ${op}`)
203
- }
160
+ export const ensureCreates = (
161
+ descripts: RecordWriteDescript[],
162
+ ): RecordCreateDescript[] => {
163
+ const creates: RecordCreateDescript[] = []
164
+ for (const descript of descripts) {
165
+ if (descript.action !== WriteOpAction.Create) {
166
+ throw new Error(`Unexpected action: ${descript.action}`)
167
+ } else {
168
+ creates.push(descript)
204
169
  }
205
170
  }
206
- return [
207
- ...Object.values(creates),
208
- ...Object.values(updates),
209
- ...Object.values(deletes),
210
- ]
211
- }
212
-
213
- export const collapseDiffs = (diffs: DataDiff[]): DataDiff => {
214
- return diffs.reduce((acc, cur) => {
215
- acc.addDiff(cur)
216
- return acc
217
- }, new DataDiff())
171
+ return creates
218
172
  }
219
173
 
220
174
  export const parseDataKey = (key: string): RecordPath => {
@@ -267,3 +221,15 @@ export const cborToLexRecord = (val: Uint8Array): RepoRecord => {
267
221
  export const cidForRecord = async (val: LexValue) => {
268
222
  return cidForCbor(lexToIpld(val))
269
223
  }
224
+
225
+ export const ensureV3Commit = (commit: LegacyV2Commit | Commit): Commit => {
226
+ if (commit.version === 3) {
227
+ return commit
228
+ } else {
229
+ return {
230
+ ...commit,
231
+ version: 3,
232
+ rev: commit.rev ?? TID.nextStr(),
233
+ }
234
+ }
235
+ }
package/tests/_util.ts CHANGED
@@ -7,15 +7,15 @@ import { RepoStorage } from '../src/storage'
7
7
  import { MST } from '../src/mst'
8
8
  import {
9
9
  BlockMap,
10
- collapseWriteLog,
11
10
  CollectionContents,
12
11
  RecordWriteOp,
13
12
  RepoContents,
14
13
  RecordPath,
15
- WriteLog,
16
14
  WriteOpAction,
17
15
  RecordClaim,
18
16
  Commit,
17
+ DataDiff,
18
+ CommitData,
19
19
  } from '../src'
20
20
  import { Keypair, randomBytes } from '@atproto/crypto'
21
21
 
@@ -109,7 +109,7 @@ export const fillRepo = async (
109
109
  }
110
110
  }
111
111
 
112
- export const editRepo = async (
112
+ export const formatEdit = async (
113
113
  repo: Repo,
114
114
  prevData: RepoContents,
115
115
  keypair: crypto.Keypair,
@@ -118,91 +118,58 @@ export const editRepo = async (
118
118
  updates?: number
119
119
  deletes?: number
120
120
  },
121
- ): Promise<{ repo: Repo; data: RepoContents }> => {
121
+ ): Promise<{ commit: CommitData; data: RepoContents }> => {
122
122
  const { adds = 0, updates = 0, deletes = 0 } = params
123
123
  const repoData: RepoContents = {}
124
+ const writes: RecordWriteOp[] = []
124
125
  for (const collName of testCollections) {
125
- const collData = prevData[collName]
126
+ const collData = { ...(prevData[collName] ?? {}) }
126
127
  const shuffled = shuffle(Object.entries(collData))
127
128
 
128
129
  for (let i = 0; i < adds; i++) {
129
130
  const object = generateObject()
130
131
  const rkey = TID.nextStr()
131
132
  collData[rkey] = object
132
- repo = await repo.applyWrites(
133
- {
134
- action: WriteOpAction.Create,
135
- collection: collName,
136
- rkey,
137
- record: object,
138
- },
139
- keypair,
140
- )
133
+ writes.push({
134
+ action: WriteOpAction.Create,
135
+ collection: collName,
136
+ rkey,
137
+ record: object,
138
+ })
141
139
  }
142
140
 
143
141
  const toUpdate = shuffled.slice(0, updates)
144
142
  for (let i = 0; i < toUpdate.length; i++) {
145
143
  const object = generateObject()
146
144
  const rkey = toUpdate[i][0]
147
- repo = await repo.applyWrites(
148
- {
149
- action: WriteOpAction.Update,
150
- collection: collName,
151
- rkey,
152
- record: object,
153
- },
154
- keypair,
155
- )
156
145
  collData[rkey] = object
146
+ writes.push({
147
+ action: WriteOpAction.Update,
148
+ collection: collName,
149
+ rkey,
150
+ record: object,
151
+ })
157
152
  }
158
153
 
159
154
  const toDelete = shuffled.slice(updates, deletes)
160
155
  for (let i = 0; i < toDelete.length; i++) {
161
156
  const rkey = toDelete[i][0]
162
- repo = await repo.applyWrites(
163
- {
164
- action: WriteOpAction.Delete,
165
- collection: collName,
166
- rkey,
167
- },
168
- keypair,
169
- )
170
157
  delete collData[rkey]
158
+ writes.push({
159
+ action: WriteOpAction.Delete,
160
+ collection: collName,
161
+ rkey,
162
+ })
171
163
  }
172
164
  repoData[collName] = collData
173
165
  }
166
+ const commit = await repo.formatCommit(writes, keypair)
174
167
  return {
175
- repo,
168
+ commit,
176
169
  data: repoData,
177
170
  }
178
171
  }
179
172
 
180
- export const verifyRepoDiff = async (
181
- writeLog: WriteLog,
182
- before: RepoContents,
183
- after: RepoContents,
184
- ): Promise<void> => {
185
- const getVal = (op: RecordWriteOp, data: RepoContents) => {
186
- return (data[op.collection] || {})[op.rkey]
187
- }
188
- const ops = await collapseWriteLog(writeLog)
189
-
190
- for (const op of ops) {
191
- if (op.action === WriteOpAction.Create) {
192
- expect(getVal(op, before)).toBeUndefined()
193
- expect(getVal(op, after)).toEqual(op.record)
194
- } else if (op.action === WriteOpAction.Update) {
195
- expect(getVal(op, before)).toBeDefined()
196
- expect(getVal(op, after)).toEqual(op.record)
197
- } else if (op.action === WriteOpAction.Delete) {
198
- expect(getVal(op, before)).toBeDefined()
199
- expect(getVal(op, after)).toBeUndefined()
200
- } else {
201
- throw new Error('unexpected op type')
202
- }
203
- }
204
- }
205
-
206
173
  export const contentsToClaims = (contents: RepoContents): RecordClaim[] => {
207
174
  const claims: RecordClaim[] = []
208
175
  for (const coll of Object.keys(contents)) {
@@ -233,23 +200,27 @@ export const addBadCommit = async (
233
200
  keypair: Keypair,
234
201
  ): Promise<Repo> => {
235
202
  const obj = generateObject()
236
- const blocks = new BlockMap()
237
- const cid = await blocks.add(obj)
203
+ const newBlocks = new BlockMap()
204
+ const cid = await newBlocks.add(obj)
238
205
  const updatedData = await repo.data.add(`com.example.test/${TID.next()}`, cid)
239
- const unstoredData = await updatedData.getUnstoredBlocks()
240
- blocks.addMap(unstoredData.blocks)
206
+ const dataCid = await updatedData.getPointer()
207
+ const diff = await DataDiff.of(updatedData, repo.data)
208
+ newBlocks.addMap(diff.newMstBlocks)
241
209
  // we generate a bad sig by signing some other data
210
+ const rev = TID.nextStr(repo.commit.rev)
242
211
  const commit: Commit = {
243
212
  ...repo.commit,
244
- prev: repo.cid,
245
- data: unstoredData.root,
213
+ rev,
214
+ data: dataCid,
246
215
  sig: await keypair.sign(randomBytes(256)),
247
216
  }
248
- const commitCid = await blocks.add(commit)
217
+ const commitCid = await newBlocks.add(commit)
249
218
  await repo.storage.applyCommit({
250
- commit: commitCid,
219
+ cid: commitCid,
220
+ rev,
251
221
  prev: repo.cid,
252
- blocks: blocks,
222
+ newBlocks,
223
+ removedCids: diff.removedCids,
253
224
  })
254
225
  return await Repo.load(repo.storage, commitCid)
255
226
  }
package/tests/mst.test.ts CHANGED
@@ -150,7 +150,10 @@ describe('Merkle Search Tree', () => {
150
150
  } else {
151
151
  cid = entry.value
152
152
  }
153
- const found = (await blockstore.has(cid)) || diff.newCids.has(cid)
153
+ const found =
154
+ (await blockstore.has(cid)) ||
155
+ diff.newMstBlocks.has(cid) ||
156
+ diff.newLeafCids.has(cid)
154
157
  expect(found).toBeTruthy()
155
158
  }
156
159
  })
@@ -1,13 +1,12 @@
1
- import { TID } from '@atproto/common'
1
+ import { TID, streamToBuffer } from '@atproto/common'
2
2
  import * as crypto from '@atproto/crypto'
3
- import { RecordClaim, Repo, RepoContents } from '../../src'
4
- import { MemoryBlockstore } from '../../src/storage'
5
- import * as verify from '../../src/verify'
6
- import * as sync from '../../src/sync'
3
+ import { RecordClaim, Repo, RepoContents } from '../src'
4
+ import { MemoryBlockstore } from '../src/storage'
5
+ import * as sync from '../src/sync'
7
6
 
8
- import * as util from '../_util'
7
+ import * as util from './_util'
9
8
 
10
- describe('Narrow Sync', () => {
9
+ describe('Repo Proofs', () => {
11
10
  let storage: MemoryBlockstore
12
11
  let repo: Repo
13
12
  let keypair: crypto.Keypair
@@ -25,11 +24,11 @@ describe('Narrow Sync', () => {
25
24
  })
26
25
 
27
26
  const getProofs = async (claims: RecordClaim[]) => {
28
- return sync.getRecords(storage, repo.cid, claims)
27
+ return streamToBuffer(sync.getRecords(storage, repo.cid, claims))
29
28
  }
30
29
 
31
30
  const doVerify = (proofs: Uint8Array, claims: RecordClaim[]) => {
32
- return verify.verifyProofs(proofs, claims, repoDid, keypair.did())
31
+ return sync.verifyProofs(proofs, claims, repoDid, keypair.did())
33
32
  }
34
33
 
35
34
  it('verifies valid records', async () => {
@@ -112,7 +111,7 @@ describe('Narrow Sync', () => {
112
111
  possible[8],
113
112
  ]
114
113
  const proofs = await getProofs(claims)
115
- const records = await verify.verifyRecords(proofs, repoDid, keypair.did())
114
+ const records = await sync.verifyRecords(proofs, repoDid, keypair.did())
116
115
  for (const record of records) {
117
116
  const foundClaim = claims.find(
118
117
  (claim) =>
@@ -127,19 +126,13 @@ describe('Narrow Sync', () => {
127
126
  }
128
127
  })
129
128
 
130
- it('verifyRecords throws on a bad signature', async () => {
131
- const badRepo = await util.addBadCommit(repo, keypair)
132
- const claims = util.contentsToClaims(repoData)
133
- const proofs = await sync.getRecords(storage, badRepo.cid, claims)
134
- const fn = verify.verifyRecords(proofs, repoDid, keypair.did())
135
- await expect(fn).rejects.toThrow(verify.RepoVerificationError)
136
- })
137
-
138
129
  it('verifyProofs throws on a bad signature', async () => {
139
130
  const badRepo = await util.addBadCommit(repo, keypair)
140
131
  const claims = util.contentsToClaims(repoData)
141
- const proofs = await sync.getRecords(storage, badRepo.cid, claims)
142
- const fn = verify.verifyProofs(proofs, claims, repoDid, keypair.did())
143
- await expect(fn).rejects.toThrow(verify.RepoVerificationError)
132
+ const proofs = await streamToBuffer(
133
+ sync.getRecords(storage, badRepo.cid, claims),
134
+ )
135
+ const fn = sync.verifyProofs(proofs, claims, repoDid, keypair.did())
136
+ await expect(fn).rejects.toThrow(sync.RepoVerificationError)
144
137
  })
145
138
  })
@@ -22,7 +22,7 @@ describe('Repo', () => {
22
22
 
23
23
  it('has proper metadata', async () => {
24
24
  expect(repo.did).toEqual(keypair.did())
25
- expect(repo.version).toBe(2)
25
+ expect(repo.version).toBe(3)
26
26
  })
27
27
 
28
28
  it('does basic operations', async () => {
@@ -75,12 +75,13 @@ describe('Repo', () => {
75
75
  })
76
76
 
77
77
  it('edits and deletes content', async () => {
78
- const edited = await util.editRepo(repo, repoData, keypair, {
78
+ const edit = await util.formatEdit(repo, repoData, keypair, {
79
79
  adds: 20,
80
80
  updates: 20,
81
81
  deletes: 20,
82
82
  })
83
- repo = edited.repo
83
+ repo = await repo.applyCommit(edit.commit)
84
+ repoData = edit.data
84
85
  const contents = await repo.getContents()
85
86
  expect(contents).toEqual(repoData)
86
87
  })
@@ -100,6 +101,6 @@ describe('Repo', () => {
100
101
  const contents = await reloadedRepo.getContents()
101
102
  expect(contents).toEqual(repoData)
102
103
  expect(repo.did).toEqual(keypair.did())
103
- expect(repo.version).toBe(2)
104
+ expect(repo.version).toBe(3)
104
105
  })
105
106
  })
@@ -0,0 +1,97 @@
1
+ import * as crypto from '@atproto/crypto'
2
+ import {
3
+ CidSet,
4
+ Repo,
5
+ RepoContents,
6
+ RepoVerificationError,
7
+ readCarWithRoot,
8
+ } from '../src'
9
+ import { MemoryBlockstore } from '../src/storage'
10
+ import * as sync from '../src/sync'
11
+
12
+ import * as util from './_util'
13
+ import { streamToBuffer } from '@atproto/common'
14
+ import { CarReader } from '@ipld/car/reader'
15
+
16
+ describe('Repo Sync', () => {
17
+ let storage: MemoryBlockstore
18
+ let repo: Repo
19
+ let keypair: crypto.Keypair
20
+ let repoData: RepoContents
21
+
22
+ const repoDid = 'did:example:test'
23
+
24
+ beforeAll(async () => {
25
+ storage = new MemoryBlockstore()
26
+ keypair = await crypto.Secp256k1Keypair.create()
27
+ repo = await Repo.create(storage, repoDid, keypair)
28
+ const filled = await util.fillRepo(repo, keypair, 20)
29
+ repo = filled.repo
30
+ repoData = filled.data
31
+ })
32
+
33
+ it('sync a full repo', async () => {
34
+ const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))
35
+ const car = await readCarWithRoot(carBytes)
36
+ const verified = await sync.verifyRepo(
37
+ car.blocks,
38
+ car.root,
39
+ repoDid,
40
+ keypair.did(),
41
+ )
42
+ const syncStorage = new MemoryBlockstore()
43
+ await syncStorage.applyCommit(verified.commit)
44
+ const loadedRepo = await Repo.load(syncStorage, car.root)
45
+ const contents = await loadedRepo.getContents()
46
+ expect(contents).toEqual(repoData)
47
+ const contentsFromOps: RepoContents = {}
48
+ for (const write of verified.creates) {
49
+ contentsFromOps[write.collection] ??= {}
50
+ contentsFromOps[write.collection][write.rkey] = write.record
51
+ }
52
+ expect(contentsFromOps).toEqual(repoData)
53
+ })
54
+
55
+ it('does not sync duplicate blocks', async () => {
56
+ const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))
57
+ const car = await CarReader.fromBytes(carBytes)
58
+ const cids = new CidSet()
59
+ for await (const block of car.blocks()) {
60
+ if (cids.has(block.cid)) {
61
+ throw new Error(`duplicate block: :${block.cid.toString()}`)
62
+ }
63
+ cids.add(block.cid)
64
+ }
65
+ })
66
+
67
+ it('syncs a repo that is behind', async () => {
68
+ // add more to providers's repo & have consumer catch up
69
+ const edit = await util.formatEdit(repo, repoData, keypair, {
70
+ adds: 10,
71
+ updates: 10,
72
+ deletes: 10,
73
+ })
74
+ const verified = await sync.verifyDiff(
75
+ repo,
76
+ edit.commit.newBlocks,
77
+ edit.commit.cid,
78
+ repoDid,
79
+ keypair.did(),
80
+ )
81
+ await storage.applyCommit(verified.commit)
82
+ repo = await Repo.load(storage, verified.commit.cid)
83
+ const contents = await repo.getContents()
84
+ expect(contents).toEqual(edit.data)
85
+ })
86
+
87
+ it('throws on a bad signature', async () => {
88
+ const badRepo = await util.addBadCommit(repo, keypair)
89
+ const carBytes = await streamToBuffer(
90
+ sync.getFullRepo(storage, badRepo.cid),
91
+ )
92
+ const car = await readCarWithRoot(carBytes)
93
+ await expect(
94
+ sync.verifyRepo(car.blocks, car.root, repoDid, keypair.did()),
95
+ ).rejects.toThrow(RepoVerificationError)
96
+ })
97
+ })
@@ -0,0 +1,21 @@
1
+ import { dataToCborBlock, wait } from '@atproto/common'
2
+ import { writeCar } from '../src'
3
+
4
+ describe('Utils', () => {
5
+ describe('writeCar()', () => {
6
+ it('propagates errors', async () => {
7
+ const iterate = async () => {
8
+ const iter = writeCar(null, async (car) => {
9
+ await wait(1)
10
+ const block = await dataToCborBlock({ test: 1 })
11
+ await car.put(block)
12
+ throw new Error('Oops!')
13
+ })
14
+ for await (const bytes of iter) {
15
+ // no-op
16
+ }
17
+ }
18
+ await expect(iterate).rejects.toThrow('Oops!')
19
+ })
20
+ })
21
+ })