@automerge/automerge-repo 1.1.4 → 1.1.8

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 (37) hide show
  1. package/README.md +3 -22
  2. package/dist/DocHandle.d.ts +124 -100
  3. package/dist/DocHandle.d.ts.map +1 -1
  4. package/dist/DocHandle.js +239 -231
  5. package/dist/Repo.d.ts +10 -3
  6. package/dist/Repo.d.ts.map +1 -1
  7. package/dist/Repo.js +22 -1
  8. package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
  9. package/dist/helpers/debounce.d.ts.map +1 -1
  10. package/dist/helpers/tests/network-adapter-tests.d.ts +1 -1
  11. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  12. package/dist/helpers/tests/network-adapter-tests.js +2 -2
  13. package/dist/helpers/tests/storage-adapter-tests.d.ts +7 -0
  14. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -0
  15. package/dist/helpers/tests/storage-adapter-tests.js +128 -0
  16. package/dist/helpers/throttle.d.ts.map +1 -1
  17. package/dist/helpers/withTimeout.d.ts.map +1 -1
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -0
  21. package/dist/synchronizer/DocSynchronizer.js +1 -1
  22. package/package.json +4 -4
  23. package/src/DocHandle.ts +325 -375
  24. package/src/Repo.ts +35 -8
  25. package/src/helpers/tests/network-adapter-tests.ts +4 -2
  26. package/src/helpers/tests/storage-adapter-tests.ts +193 -0
  27. package/src/index.ts +43 -0
  28. package/src/synchronizer/DocSynchronizer.ts +1 -1
  29. package/test/CollectionSynchronizer.test.ts +1 -3
  30. package/test/DocHandle.test.ts +19 -1
  31. package/test/DocSynchronizer.test.ts +1 -4
  32. package/test/DummyStorageAdapter.test.ts +11 -0
  33. package/test/Repo.test.ts +179 -53
  34. package/test/helpers/DummyNetworkAdapter.ts +20 -18
  35. package/test/helpers/DummyStorageAdapter.ts +5 -1
  36. package/test/remoteHeads.test.ts +1 -1
  37. package/tsconfig.json +1 -0
package/test/Repo.test.ts CHANGED
@@ -3,7 +3,6 @@ import { MessageChannelNetworkAdapter } from "../../automerge-repo-network-messa
3
3
  import assert from "assert"
4
4
  import * as Uuid from "uuid"
5
5
  import { describe, expect, it } from "vitest"
6
- import { HandleState, READY } from "../src/DocHandle.js"
7
6
  import { parseAutomergeUrl } from "../src/AutomergeUrl.js"
8
7
  import {
9
8
  generateAutomergeUrl,
@@ -29,14 +28,12 @@ import {
29
28
  } from "./helpers/generate-large-object.js"
30
29
  import { getRandomItem } from "./helpers/getRandomItem.js"
31
30
  import { TestDoc } from "./types.js"
32
- import { StorageId } from "../src/storage/types.js"
31
+ import { StorageId, StorageKey } from "../src/storage/types.js"
33
32
 
34
33
  describe("Repo", () => {
35
34
  describe("constructor", () => {
36
- it("can be instantiated without network adapters", () => {
37
- const repo = new Repo({
38
- network: [],
39
- })
35
+ it("can be instantiated without any configuration", () => {
36
+ const repo = new Repo()
40
37
  expect(repo).toBeInstanceOf(Repo)
41
38
  })
42
39
  })
@@ -126,8 +123,7 @@ describe("Repo", () => {
126
123
  })
127
124
  const v = await handle.doc()
128
125
  assert.equal(handle.isReady(), true)
129
-
130
- assert.equal(v?.foo, "bar")
126
+ assert.equal(v.foo, "bar")
131
127
  })
132
128
 
133
129
  it("can clone a document", () => {
@@ -207,12 +203,11 @@ describe("Repo", () => {
207
203
  assert.equal(doc, undefined)
208
204
  })
209
205
 
210
- it("fires an 'unavailable' event when you don't have the document locally and network to connect to", async () => {
206
+ it("emits an unavailable event when you don't have the document locally and are not connected to anyone", async () => {
211
207
  const { repo } = setup()
212
208
  const url = generateAutomergeUrl()
213
209
  const handle = repo.find<TestDoc>(url)
214
210
  assert.equal(handle.isReady(), false)
215
-
216
211
  await eventPromise(handle, "unavailable")
217
212
  })
218
213
 
@@ -255,7 +250,6 @@ describe("Repo", () => {
255
250
 
256
251
  const repo2 = new Repo({
257
252
  storage: storageAdapter,
258
- network: [],
259
253
  })
260
254
 
261
255
  const bobHandle = repo2.find<TestDoc>(handle.url)
@@ -277,7 +271,6 @@ describe("Repo", () => {
277
271
 
278
272
  const repo2 = new Repo({
279
273
  storage: storageAdapter,
280
- network: [],
281
274
  })
282
275
 
283
276
  const bobHandle = repo2.find<TestDoc>(handle.url)
@@ -364,7 +357,6 @@ describe("Repo", () => {
364
357
 
365
358
  const repo = new Repo({
366
359
  storage,
367
- network: [],
368
360
  })
369
361
 
370
362
  const handle = repo.create<{ count: number }>()
@@ -376,14 +368,12 @@ describe("Repo", () => {
376
368
  d.count = 1
377
369
  })
378
370
 
379
- // wait because storage id is not initialized immediately
380
- await pause()
371
+ await repo.flush()
381
372
 
382
373
  const initialKeys = storage.keys()
383
374
 
384
375
  const repo2 = new Repo({
385
376
  storage,
386
- network: [],
387
377
  })
388
378
  const handle2 = repo2.find(handle.url)
389
379
  await handle2.doc()
@@ -396,7 +386,6 @@ describe("Repo", () => {
396
386
 
397
387
  const repo = new Repo({
398
388
  storage,
399
- network: [],
400
389
  })
401
390
 
402
391
  const handle = repo.create<{ count: number }>()
@@ -411,7 +400,6 @@ describe("Repo", () => {
411
400
  for (let i = 0; i < 3; i++) {
412
401
  const repo2 = new Repo({
413
402
  storage,
414
- network: [],
415
403
  })
416
404
  const handle2 = repo2.find(handle.url)
417
405
  await handle2.doc()
@@ -452,12 +440,159 @@ describe("Repo", () => {
452
440
  expect(A.getHistory(v)).toEqual(A.getHistory(updatedDoc))
453
441
  })
454
442
 
455
- it("throws an error if we try to import an invalid document", async () => {
443
+ it("throws an error if we try to import a nonsensical byte array", async () => {
456
444
  const { repo } = setup()
457
445
  expect(() => {
458
- repo.import<TestDoc>(A.init<TestDoc> as unknown as Uint8Array)
446
+ repo.import<TestDoc>(new Uint8Array([1, 2, 3]))
459
447
  }).toThrow()
460
448
  })
449
+
450
+ // TODO: not sure if this is the desired behavior from `import`.
451
+
452
+ it("makes an empty document if we try to import an automerge doc", async () => {
453
+ const { repo } = setup()
454
+ // @ts-ignore - passing something other than UInt8Array
455
+ const handle = repo.import<TestDoc>(A.from({ foo: 123 }))
456
+ const doc = await handle.doc()
457
+ expect(doc).toEqual({})
458
+ })
459
+
460
+ it("makes an empty document if we try to import a plain object", async () => {
461
+ const { repo } = setup()
462
+ // @ts-ignore - passing something other than UInt8Array
463
+ const handle = repo.import<TestDoc>({ foo: 123 })
464
+ const doc = await handle.doc()
465
+ expect(doc).toEqual({})
466
+ })
467
+ })
468
+
469
+ describe("flush behaviour", () => {
470
+ const setup = () => {
471
+ let blockedSaves = new Set<{ path: StorageKey; resolve: () => void }>()
472
+ let resume = (documentIds?: DocumentId[]) => {
473
+ const savesToUnblock = documentIds
474
+ ? Array.from(blockedSaves).filter(({ path }) =>
475
+ documentIds.some(documentId => path.includes(documentId))
476
+ )
477
+ : Array.from(blockedSaves)
478
+ savesToUnblock.forEach(({ resolve }) => resolve())
479
+ }
480
+ const pausedStorage = new DummyStorageAdapter()
481
+ {
482
+ const originalSave = pausedStorage.save.bind(pausedStorage)
483
+ pausedStorage.save = async (...args) => {
484
+ await new Promise<void>(resolve => {
485
+ const blockedSave = {
486
+ path: args[0],
487
+ resolve: () => {
488
+ resolve()
489
+ blockedSaves.delete(blockedSave)
490
+ },
491
+ }
492
+ blockedSaves.add(blockedSave)
493
+ })
494
+ await pause(0)
495
+ // otherwise all the save promises resolve together
496
+ // which prevents testing flushing a single docID
497
+ return originalSave(...args)
498
+ }
499
+ }
500
+
501
+ const repo = new Repo({
502
+ storage: pausedStorage,
503
+ })
504
+
505
+ // Create a pair of handles
506
+ const handle = repo.create<{ foo: string }>({ foo: "first" })
507
+ const handle2 = repo.create<{ foo: string }>({ foo: "second" })
508
+ return { resume, pausedStorage, repo, handle, handle2 }
509
+ }
510
+
511
+ it("should not be in a new repo yet because the storage is slow", async () => {
512
+ const { pausedStorage, repo, handle, handle2 } = setup()
513
+ expect((await handle.doc()).foo).toEqual("first")
514
+ expect((await handle2.doc()).foo).toEqual("second")
515
+
516
+ // Reload repo
517
+ const repo2 = new Repo({
518
+ storage: pausedStorage,
519
+ })
520
+
521
+ // Could not find the document that is not yet saved because of slow storage.
522
+ const reloadedHandle = repo2.find<{ foo: string }>(handle.url)
523
+ expect(pausedStorage.keys()).to.deep.equal([])
524
+ expect(await reloadedHandle.doc()).toEqual(undefined)
525
+ })
526
+
527
+ it("should be visible to a new repo after flush()", async () => {
528
+ const { resume, pausedStorage, repo, handle, handle2 } = setup()
529
+
530
+ const flushPromise = repo.flush()
531
+ resume()
532
+ await flushPromise
533
+
534
+ // Check that the data is now saved.
535
+ expect(pausedStorage.keys().length).toBeGreaterThan(0)
536
+
537
+ {
538
+ // Reload repo
539
+ const repo = new Repo({
540
+ storage: pausedStorage,
541
+ })
542
+
543
+ expect(
544
+ (await repo.find<{ foo: string }>(handle.documentId).doc()).foo
545
+ ).toEqual("first")
546
+ expect(
547
+ (await repo.find<{ foo: string }>(handle2.documentId).doc()).foo
548
+ ).toEqual("second")
549
+ }
550
+ })
551
+
552
+ it("should only block on flushing requested documents", async () => {
553
+ const { resume, pausedStorage, repo, handle, handle2 } = setup()
554
+
555
+ const flushPromise = repo.flush([handle.documentId])
556
+ resume([handle.documentId])
557
+ await flushPromise
558
+
559
+ // Check that the data is now saved.
560
+ expect(pausedStorage.keys().length).toBeGreaterThan(0)
561
+
562
+ {
563
+ // Reload repo
564
+ const repo = new Repo({
565
+ storage: pausedStorage,
566
+ })
567
+
568
+ expect(
569
+ (await repo.find<{ foo: string }>(handle.documentId).doc()).foo
570
+ ).toEqual("first")
571
+ // Really, it's okay if the second one is also flushed but I'm forcing the issue
572
+ // in the test storage engine above to make sure the behaviour is as documented
573
+ expect(
574
+ await repo.find<{ foo: string }>(handle2.documentId).doc()
575
+ ).toEqual(undefined)
576
+ }
577
+ })
578
+
579
+ it("flush right before change should resolve correctly", async () => {
580
+ const repo = new Repo({
581
+ network: [],
582
+ storage: new DummyStorageAdapter(),
583
+ })
584
+ const handle = repo.create<{ field?: string }>()
585
+
586
+ for (let i = 0; i < 10; i++) {
587
+ const flushPromise = repo.flush([handle.documentId])
588
+ handle.change((doc: any) => {
589
+ doc.field += Array(1024)
590
+ .fill(Math.random() * 10)
591
+ .join("")
592
+ })
593
+ await flushPromise
594
+ }
595
+ })
461
596
  })
462
597
 
463
598
  describe("with peers (linear network)", async () => {
@@ -695,9 +830,7 @@ describe("Repo", () => {
695
830
  it("charlieRepo can request a document not initially shared with it", async () => {
696
831
  const { charlieRepo, notForCharlie, teardown } = await setup()
697
832
 
698
- const handle = charlieRepo.find<TestDoc>(
699
- stringifyAutomergeUrl({ documentId: notForCharlie })
700
- )
833
+ const handle = charlieRepo.find<TestDoc>(notForCharlie)
701
834
 
702
835
  await pause(50)
703
836
 
@@ -711,9 +844,7 @@ describe("Repo", () => {
711
844
  it("charlieRepo can request a document across a network of multiple peers", async () => {
712
845
  const { charlieRepo, notForBob, teardown } = await setup()
713
846
 
714
- const handle = charlieRepo.find<TestDoc>(
715
- stringifyAutomergeUrl({ documentId: notForBob })
716
- )
847
+ const handle = charlieRepo.find<TestDoc>(notForBob)
717
848
 
718
849
  await pause(50)
719
850
 
@@ -735,7 +866,16 @@ describe("Repo", () => {
735
866
  teardown()
736
867
  })
737
868
 
738
- it("fires an 'unavailable' event when a document is not available on the network", async () => {
869
+ it("emits an unavailable event when it's not found on the network", async () => {
870
+ const { aliceRepo, teardown } = await setup()
871
+ const url = generateAutomergeUrl()
872
+ const handle = aliceRepo.find(url)
873
+ assert.equal(handle.isReady(), false)
874
+ await eventPromise(handle, "unavailable")
875
+ teardown()
876
+ })
877
+
878
+ it("emits an unavailable event every time an unavailable doc is requested", async () => {
739
879
  const { charlieRepo, teardown } = await setup()
740
880
  const url = generateAutomergeUrl()
741
881
  const handle = charlieRepo.find<TestDoc>(url)
@@ -746,10 +886,13 @@ describe("Repo", () => {
746
886
  eventPromise(charlieRepo, "unavailable-document"),
747
887
  ])
748
888
 
749
- // make sure it fires a second time if the doc is still unavailable
889
+ // make sure it emits a second time if the doc is still unavailable
750
890
  const handle2 = charlieRepo.find<TestDoc>(url)
751
891
  assert.equal(handle2.isReady(), false)
752
- await eventPromise(handle2, "unavailable")
892
+ await Promise.all([
893
+ eventPromise(handle, "unavailable"),
894
+ eventPromise(charlieRepo, "unavailable-document"),
895
+ ])
753
896
 
754
897
  teardown()
755
898
  })
@@ -773,7 +916,7 @@ describe("Repo", () => {
773
916
 
774
917
  await eventPromise(aliceRepo.networkSubsystem, "peer")
775
918
 
776
- const doc = await handle.doc([READY])
919
+ const doc = await handle.doc(["ready"])
777
920
  assert.deepStrictEqual(doc, { foo: "baz" })
778
921
 
779
922
  // an additional find should also return the correct resolved document
@@ -797,7 +940,6 @@ describe("Repo", () => {
797
940
  // we have a storage containing the document to pass to a new repo later
798
941
  const storage = new DummyStorageAdapter()
799
942
  const isolatedRepo = new Repo({
800
- network: [],
801
943
  storage,
802
944
  })
803
945
  const unsyncedHandle = isolatedRepo.create<TestDoc>()
@@ -855,17 +997,6 @@ describe("Repo", () => {
855
997
  teardown()
856
998
  })
857
999
 
858
- it("can emit an 'unavailable' event when it's not found on the network", async () => {
859
- const { charlieRepo, teardown } = await setup()
860
-
861
- const url = generateAutomergeUrl()
862
- const handle = charlieRepo.find<TestDoc>(url)
863
- assert.equal(handle.isReady(), false)
864
-
865
- await eventPromise(handle, "unavailable")
866
- teardown()
867
- })
868
-
869
1000
  it("syncs a bunch of changes", async () => {
870
1001
  const { aliceRepo, bobRepo, charlieRepo, teardown } = await setup()
871
1002
 
@@ -949,8 +1080,7 @@ describe("Repo", () => {
949
1080
  bobHandle.documentId,
950
1081
  await charlieRepo!.storageSubsystem.id()
951
1082
  )
952
- const docHeads = A.getHeads(bobHandle.docSync())
953
- assert.deepStrictEqual(storedSyncState.sharedHeads, docHeads)
1083
+ assert.deepStrictEqual(storedSyncState.sharedHeads, bobHandle.heads())
954
1084
 
955
1085
  teardown()
956
1086
  })
@@ -1003,7 +1133,6 @@ describe("Repo", () => {
1003
1133
  // setup new repo which uses bob's storage
1004
1134
  const bob2Repo = new Repo({
1005
1135
  storage: bobStorage,
1006
- network: [],
1007
1136
  peerId: "bob-2" as PeerId,
1008
1137
  })
1009
1138
 
@@ -1067,18 +1196,15 @@ describe("Repo", () => {
1067
1196
  // pause to let the sync happen
1068
1197
  await pause(100)
1069
1198
 
1070
- const charlieHeads = A.getHeads(charlieHandle.docSync())
1071
- const bobHeads = A.getHeads(handle.docSync())
1072
-
1073
- assert.deepStrictEqual(charlieHeads, bobHeads)
1199
+ assert.deepStrictEqual(charlieHandle.heads(), handle.heads())
1074
1200
 
1075
1201
  const nextRemoteHeads = await nextRemoteHeadsPromise
1076
1202
  assert.deepStrictEqual(nextRemoteHeads.storageId, charliedStorageId)
1077
- assert.deepStrictEqual(nextRemoteHeads.heads, charlieHeads)
1203
+ assert.deepStrictEqual(nextRemoteHeads.heads, charlieHandle.heads())
1078
1204
 
1079
1205
  assert.deepStrictEqual(
1080
1206
  handle.getRemoteHeads(charliedStorageId),
1081
- A.getHeads(charlieHandle.docSync())
1207
+ charlieHandle.heads()
1082
1208
  )
1083
1209
 
1084
1210
  teardown()
@@ -1115,14 +1241,14 @@ describe("Repo", () => {
1115
1241
 
1116
1242
  const bobDoc = bobRepo.find(aliceDoc.url)
1117
1243
  bobDoc.unavailable()
1118
- await bobDoc.whenReady([HandleState.UNAVAILABLE])
1244
+ await eventPromise(bobDoc, "unavailable")
1119
1245
 
1120
1246
  aliceAdapter.peerCandidate(bob)
1121
1247
  // Bob isn't yet connected to Alice and can't respond to her sync message
1122
1248
  await pause(100)
1123
1249
  bobAdapter.peerCandidate(alice)
1124
1250
 
1125
- await bobDoc.whenReady([HandleState.READY])
1251
+ await bobDoc.whenReady()
1126
1252
 
1127
1253
  assert.equal(bobDoc.isReady(), true)
1128
1254
  })
@@ -1,18 +1,18 @@
1
- import { pause } from "../../src/helpers/pause.js";
1
+ import { pause } from "../../src/helpers/pause.js"
2
2
  import { Message, NetworkAdapter, PeerId } from "../../src/index.js"
3
3
 
4
4
  export class DummyNetworkAdapter extends NetworkAdapter {
5
5
  #startReady: boolean
6
- #sendMessage?: SendMessageFn;
6
+ #sendMessage?: SendMessageFn
7
7
 
8
- constructor(opts: Options = {startReady: true}) {
8
+ constructor(opts: Options = { startReady: true }) {
9
9
  super()
10
- this.#startReady = opts.startReady;
11
- this.#sendMessage = opts.sendMessage;
10
+ this.#startReady = opts.startReady
11
+ this.#sendMessage = opts.sendMessage
12
12
  }
13
13
 
14
14
  connect(peerId: PeerId) {
15
- this.peerId = peerId;
15
+ this.peerId = peerId
16
16
  if (this.#startReady) {
17
17
  this.emit("ready", { network: this })
18
18
  }
@@ -21,34 +21,36 @@ export class DummyNetworkAdapter extends NetworkAdapter {
21
21
  disconnect() {}
22
22
 
23
23
  peerCandidate(peerId: PeerId) {
24
- this.emit('peer-candidate', { peerId, peerMetadata: {} });
24
+ this.emit("peer-candidate", { peerId, peerMetadata: {} })
25
25
  }
26
26
 
27
27
  override send(message: Message) {
28
- this.#sendMessage?.(message);
28
+ this.#sendMessage?.(message)
29
29
  }
30
30
 
31
31
  receive(message: Message) {
32
- this.emit('message', message);
32
+ this.emit("message", message)
33
33
  }
34
34
 
35
- static createConnectedPair({ latency = 10 }: { latency?: number} = {}) {
35
+ static createConnectedPair({ latency = 10 }: { latency?: number } = {}) {
36
36
  const adapter1: DummyNetworkAdapter = new DummyNetworkAdapter({
37
37
  startReady: true,
38
- sendMessage: (message: Message) => pause(latency).then(() => adapter2.receive(message)),
39
- });
38
+ sendMessage: (message: Message) =>
39
+ pause(latency).then(() => adapter2.receive(message)),
40
+ })
40
41
  const adapter2: DummyNetworkAdapter = new DummyNetworkAdapter({
41
42
  startReady: true,
42
- sendMessage: (message: Message) => pause(latency).then(() => adapter1.receive(message)),
43
- });
43
+ sendMessage: (message: Message) =>
44
+ pause(latency).then(() => adapter1.receive(message)),
45
+ })
44
46
 
45
- return [adapter1, adapter2];
47
+ return [adapter1, adapter2]
46
48
  }
47
49
  }
48
50
 
49
- type SendMessageFn = (message: Message) => void;
51
+ type SendMessageFn = (message: Message) => void
50
52
 
51
53
  type Options = {
52
- startReady?: boolean;
53
- sendMessage?: SendMessageFn;
54
+ startReady?: boolean
55
+ sendMessage?: SendMessageFn
54
56
  }
@@ -1,4 +1,8 @@
1
- import { Chunk, StorageAdapterInterface, type StorageKey } from "../../src/index.js"
1
+ import {
2
+ Chunk,
3
+ StorageAdapterInterface,
4
+ type StorageKey,
5
+ } from "../../src/index.js"
2
6
 
3
7
  export class DummyStorageAdapter implements StorageAdapterInterface {
4
8
  #data: Record<string, Uint8Array> = {}
@@ -152,7 +152,7 @@ describe("DocHandle.remoteHeads", () => {
152
152
  // wait for alice's service worker to acknowledge the change
153
153
  const { heads } = await aliceSeenByBobPromise
154
154
 
155
- assert.deepStrictEqual(heads, A.getHeads(aliceServiceWorkerDoc.docSync()))
155
+ assert.deepStrictEqual(heads, aliceServiceWorkerDoc.heads())
156
156
  })
157
157
 
158
158
  it("should report remoteHeads only for documents the subscriber has open", async () => {
package/tsconfig.json CHANGED
@@ -10,6 +10,7 @@
10
10
  "esModuleInterop": true,
11
11
  "forceConsistentCasingInFileNames": true,
12
12
  "strict": true,
13
+ "strictNullChecks": true,
13
14
  "skipLibCheck": true
14
15
  },
15
16
  "include": ["src/**/*.ts"],