@automerge/automerge-repo 1.0.6 → 1.0.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 (82) hide show
  1. package/.eslintrc +1 -1
  2. package/dist/DocHandle.d.ts +8 -8
  3. package/dist/DocHandle.d.ts.map +1 -1
  4. package/dist/DocHandle.js +4 -8
  5. package/dist/DocUrl.d.ts.map +1 -1
  6. package/dist/Repo.d.ts +6 -3
  7. package/dist/Repo.d.ts.map +1 -1
  8. package/dist/Repo.js +20 -19
  9. package/dist/helpers/cbor.d.ts +2 -2
  10. package/dist/helpers/cbor.d.ts.map +1 -1
  11. package/dist/helpers/cbor.js +1 -1
  12. package/dist/helpers/debounce.d.ts +14 -0
  13. package/dist/helpers/debounce.d.ts.map +1 -0
  14. package/dist/helpers/debounce.js +21 -0
  15. package/dist/helpers/pause.d.ts.map +1 -1
  16. package/dist/helpers/pause.js +3 -1
  17. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  18. package/dist/helpers/tests/network-adapter-tests.js +2 -2
  19. package/dist/helpers/throttle.d.ts +28 -0
  20. package/dist/helpers/throttle.d.ts.map +1 -0
  21. package/dist/helpers/throttle.js +39 -0
  22. package/dist/index.d.ts +11 -9
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -4
  25. package/dist/network/NetworkAdapter.d.ts +3 -3
  26. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  27. package/dist/network/NetworkSubsystem.d.ts +2 -2
  28. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  29. package/dist/network/NetworkSubsystem.js +30 -18
  30. package/dist/network/messages.d.ts +38 -68
  31. package/dist/network/messages.d.ts.map +1 -1
  32. package/dist/network/messages.js +13 -21
  33. package/dist/storage/StorageSubsystem.d.ts +1 -1
  34. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  35. package/dist/storage/StorageSubsystem.js +9 -9
  36. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -3
  37. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  38. package/dist/synchronizer/CollectionSynchronizer.js +3 -3
  39. package/dist/synchronizer/DocSynchronizer.d.ts +4 -3
  40. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  41. package/dist/synchronizer/DocSynchronizer.js +25 -34
  42. package/dist/synchronizer/Synchronizer.d.ts +2 -2
  43. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  44. package/dist/types.d.ts +5 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/fuzz/fuzz.ts +8 -6
  47. package/fuzz/tsconfig.json +8 -0
  48. package/package.json +5 -13
  49. package/src/DocHandle.ts +12 -15
  50. package/src/DocUrl.ts +3 -1
  51. package/src/Repo.ts +36 -29
  52. package/src/helpers/cbor.ts +4 -4
  53. package/src/helpers/debounce.ts +25 -0
  54. package/src/helpers/headsAreSame.ts +1 -1
  55. package/src/helpers/pause.ts +7 -2
  56. package/src/helpers/tests/network-adapter-tests.ts +3 -3
  57. package/src/helpers/throttle.ts +43 -0
  58. package/src/helpers/withTimeout.ts +2 -2
  59. package/src/index.ts +36 -29
  60. package/src/network/NetworkAdapter.ts +7 -3
  61. package/src/network/NetworkSubsystem.ts +31 -23
  62. package/src/network/messages.ts +88 -151
  63. package/src/storage/StorageSubsystem.ts +12 -12
  64. package/src/synchronizer/CollectionSynchronizer.ts +7 -16
  65. package/src/synchronizer/DocSynchronizer.ts +42 -53
  66. package/src/synchronizer/Synchronizer.ts +2 -2
  67. package/src/types.ts +8 -3
  68. package/test/CollectionSynchronizer.test.ts +58 -53
  69. package/test/DocHandle.test.ts +35 -36
  70. package/test/DocSynchronizer.test.ts +5 -8
  71. package/test/Network.test.ts +1 -0
  72. package/test/Repo.test.ts +229 -158
  73. package/test/StorageSubsystem.test.ts +6 -9
  74. package/test/helpers/DummyNetworkAdapter.ts +9 -4
  75. package/test/helpers/DummyStorageAdapter.ts +4 -2
  76. package/test/tsconfig.json +8 -0
  77. package/typedoc.json +3 -3
  78. package/.mocharc.json +0 -5
  79. package/dist/EphemeralData.d.ts +0 -20
  80. package/dist/EphemeralData.d.ts.map +0 -1
  81. package/dist/EphemeralData.js +0 -1
  82. package/src/EphemeralData.ts +0 -17
package/test/Repo.test.ts CHANGED
@@ -1,7 +1,13 @@
1
- import assert from "assert"
2
1
  import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
3
- import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel"
4
-
2
+ import assert from "assert"
3
+ import * as Uuid from "uuid"
4
+ import { describe, it } from "vitest"
5
+ import { parseAutomergeUrl } from "../dist/DocUrl.js"
6
+ import { READY } from "../src/DocHandle.js"
7
+ import { generateAutomergeUrl, stringifyAutomergeUrl } from "../src/DocUrl.js"
8
+ import { Repo } from "../src/Repo.js"
9
+ import { eventPromise } from "../src/helpers/eventPromise.js"
10
+ import { pause } from "../src/helpers/pause.js"
5
11
  import {
6
12
  AutomergeUrl,
7
13
  DocHandle,
@@ -9,28 +15,20 @@ import {
9
15
  PeerId,
10
16
  SharePolicy,
11
17
  } from "../src/index.js"
12
- import { eventPromise } from "../src/helpers/eventPromise.js"
13
- import { pause, rejectOnTimeout } from "../src/helpers/pause.js"
14
- import { Repo } from "../src/Repo.js"
15
18
  import { DummyNetworkAdapter } from "./helpers/DummyNetworkAdapter.js"
16
19
  import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
17
- import { getRandomItem } from "./helpers/getRandomItem.js"
18
- import { TestDoc } from "./types.js"
19
- import { generateAutomergeUrl, stringifyAutomergeUrl } from "../src/DocUrl.js"
20
- import { READY, AWAITING_NETWORK } from "../src/DocHandle.js"
21
20
  import {
22
- generateLargeObject,
23
21
  LargeObject,
22
+ generateLargeObject,
24
23
  } from "./helpers/generate-large-object.js"
25
- import { parseAutomergeUrl } from "../dist/DocUrl.js"
26
-
27
- import * as Uuid from "uuid"
24
+ import { getRandomItem } from "./helpers/getRandomItem.js"
25
+ import { TestDoc } from "./types.js"
28
26
 
29
27
  describe("Repo", () => {
30
- describe("single repo", () => {
31
- const setup = (networkReady = true) => {
28
+ describe("local only", () => {
29
+ const setup = ({ startReady = true } = {}) => {
32
30
  const storageAdapter = new DummyStorageAdapter()
33
- const networkAdapter = new DummyNetworkAdapter(networkReady)
31
+ const networkAdapter = new DummyNetworkAdapter({ startReady })
34
32
 
35
33
  const repo = new Repo({
36
34
  storage: storageAdapter,
@@ -66,6 +64,8 @@ describe("Repo", () => {
66
64
  })
67
65
 
68
66
  it("can find a document using a legacy UUID (for now)", () => {
67
+ disableConsoleWarn()
68
+
69
69
  const { repo } = setup()
70
70
  const handle = repo.create<TestDoc>()
71
71
  handle.change((d: TestDoc) => {
@@ -79,6 +79,8 @@ describe("Repo", () => {
79
79
  const handle2 = repo.find(legacyDocumentId)
80
80
  assert.equal(handle, handle2)
81
81
  assert.deepEqual(handle2.docSync(), { foo: "bar" })
82
+
83
+ reenableConsoleWarn()
82
84
  })
83
85
 
84
86
  it("can change a document", async () => {
@@ -180,7 +182,7 @@ describe("Repo", () => {
180
182
  })
181
183
 
182
184
  it("doesn't mark a document as unavailable until network adapters are ready", async () => {
183
- const { repo, networkAdapter } = setup(false)
185
+ const { repo, networkAdapter } = setup({ startReady: false })
184
186
  const url = generateAutomergeUrl()
185
187
  const handle = repo.find<TestDoc>(url)
186
188
 
@@ -236,7 +238,7 @@ describe("Repo", () => {
236
238
 
237
239
  assert.equal(handle.isReady(), true)
238
240
 
239
- await pause()
241
+ await pause(150)
240
242
 
241
243
  const repo2 = new Repo({
242
244
  storage: storageAdapter,
@@ -257,19 +259,14 @@ describe("Repo", () => {
257
259
  })
258
260
  // we now have a snapshot and an incremental change in storage
259
261
  assert.equal(handle.isReady(), true)
260
- await handle.doc()
262
+ const foo = await handle.doc()
263
+ assert.equal(foo?.foo, "bar")
264
+
265
+ await pause()
261
266
  repo.delete(handle.documentId)
262
267
 
263
268
  assert(handle.isDeleted())
264
269
  assert.equal(repo.handles[handle.documentId], undefined)
265
-
266
- const bobHandle = repo.find<TestDoc>(handle.url)
267
- await assert.rejects(
268
- rejectOnTimeout(bobHandle.doc(), 10),
269
- "document should have been deleted"
270
- )
271
-
272
- assert(!bobHandle.isReady())
273
270
  })
274
271
 
275
272
  it("can delete an existing document by url", async () => {
@@ -280,36 +277,31 @@ describe("Repo", () => {
280
277
  })
281
278
  assert.equal(handle.isReady(), true)
282
279
  await handle.doc()
280
+
281
+ await pause()
283
282
  repo.delete(handle.url)
284
283
 
285
284
  assert(handle.isDeleted())
286
285
  assert.equal(repo.handles[handle.documentId], undefined)
287
-
288
- const bobHandle = repo.find<TestDoc>(handle.url)
289
- await assert.rejects(
290
- rejectOnTimeout(bobHandle.doc(), 10),
291
- "document should have been deleted"
292
- )
293
-
294
- assert(!bobHandle.isReady())
295
286
  })
296
287
 
297
- it("deleting a document emits an event", async done => {
298
- const { repo } = setup()
299
- const handle = repo.create<TestDoc>()
300
- handle.change(d => {
301
- d.foo = "bar"
302
- })
303
- assert.equal(handle.isReady(), true)
288
+ it("deleting a document emits an event", async () =>
289
+ new Promise<void>(done => {
290
+ const { repo } = setup()
291
+ const handle = repo.create<TestDoc>()
292
+ handle.change(d => {
293
+ d.foo = "bar"
294
+ })
295
+ assert.equal(handle.isReady(), true)
304
296
 
305
- repo.on("delete-document", ({ documentId }) => {
306
- assert.equal(documentId, handle.documentId)
297
+ repo.on("delete-document", ({ documentId }) => {
298
+ assert.equal(documentId, handle.documentId)
307
299
 
308
- done()
309
- })
300
+ done()
301
+ })
310
302
 
311
- repo.delete(handle.documentId)
312
- })
303
+ repo.delete(handle.documentId)
304
+ }))
313
305
 
314
306
  it("storage state doesn't change across reloads when the document hasn't changed", async () => {
315
307
  const storage = new DummyStorageAdapter()
@@ -385,34 +377,37 @@ describe("Repo", () => {
385
377
  })
386
378
  })
387
379
 
388
- describe("sync", async () => {
389
- const charlieExcludedDocuments: DocumentId[] = []
390
- const bobExcludedDocuments: DocumentId[] = []
380
+ describe("with peers (linear network)", async () => {
381
+ const setup = async ({ connectAlice = true } = {}) => {
382
+ const charlieExcludedDocuments: DocumentId[] = []
383
+ const bobExcludedDocuments: DocumentId[] = []
391
384
 
392
- const sharePolicy: SharePolicy = async (peerId, documentId) => {
393
- if (documentId === undefined) return false
385
+ const sharePolicy: SharePolicy = async (peerId, documentId) => {
386
+ if (documentId === undefined) return false
394
387
 
395
- // make sure that charlie never gets excluded documents
396
- if (charlieExcludedDocuments.includes(documentId) && peerId === "charlie")
397
- return false
388
+ // make sure that charlie never gets excluded documents
389
+ if (
390
+ charlieExcludedDocuments.includes(documentId) &&
391
+ peerId === "charlie"
392
+ )
393
+ return false
398
394
 
399
- // make sure that bob never gets excluded documents
400
- if (bobExcludedDocuments.includes(documentId) && peerId === "bob")
401
- return false
395
+ // make sure that bob never gets excluded documents
396
+ if (bobExcludedDocuments.includes(documentId) && peerId === "bob")
397
+ return false
402
398
 
403
- return true
404
- }
399
+ return true
400
+ }
405
401
 
406
- const setupRepos = (connectAlice = true) => {
407
402
  // Set up three repos; connect Alice to Bob, and Bob to Charlie
408
403
 
409
- const aliceBobChannel = new MessageChannel()
410
- const bobCharlieChannel = new MessageChannel()
404
+ const abChannel = new MessageChannel()
405
+ const bcChannel = new MessageChannel()
411
406
 
412
- const { port1: aliceToBob, port2: bobToAlice } = aliceBobChannel
413
- const { port1: bobToCharlie, port2: charlieToBob } = bobCharlieChannel
407
+ const { port1: ab, port2: ba } = abChannel
408
+ const { port1: bc, port2: cb } = bcChannel
414
409
 
415
- const aliceNetworkAdapter = new MessageChannelNetworkAdapter(aliceToBob)
410
+ const aliceNetworkAdapter = new MessageChannelNetworkAdapter(ab)
416
411
 
417
412
  const aliceRepo = new Repo({
418
413
  network: connectAlice ? [aliceNetworkAdapter] : [],
@@ -422,46 +417,32 @@ describe("Repo", () => {
422
417
 
423
418
  const bobRepo = new Repo({
424
419
  network: [
425
- new MessageChannelNetworkAdapter(bobToAlice),
426
- new MessageChannelNetworkAdapter(bobToCharlie),
420
+ new MessageChannelNetworkAdapter(ba),
421
+ new MessageChannelNetworkAdapter(bc),
427
422
  ],
428
423
  peerId: "bob" as PeerId,
429
424
  sharePolicy,
430
425
  })
431
426
 
432
427
  const charlieRepo = new Repo({
433
- network: [new MessageChannelNetworkAdapter(charlieToBob)],
428
+ network: [new MessageChannelNetworkAdapter(cb)],
434
429
  peerId: "charlie" as PeerId,
435
430
  })
436
431
 
437
432
  const teardown = () => {
438
- aliceBobChannel.port1.close()
439
- bobCharlieChannel.port1.close()
433
+ abChannel.port1.close()
434
+ bcChannel.port1.close()
440
435
  }
441
436
 
442
- function doConnectAlice() {
437
+ function connectAliceToBob() {
443
438
  aliceRepo.networkSubsystem.addNetworkAdapter(
444
- new MessageChannelNetworkAdapter(aliceToBob)
439
+ new MessageChannelNetworkAdapter(ab)
445
440
  )
446
- //bobRepo.networkSubsystem.addNetworkAdapter(new MessageChannelNetworkAdapter(bobToAlice))
447
441
  }
448
442
 
449
443
  if (connectAlice) {
450
- doConnectAlice()
451
- }
452
-
453
- return {
454
- teardown,
455
- aliceRepo,
456
- bobRepo,
457
- charlieRepo,
458
- connectAliceToBob: doConnectAlice,
444
+ connectAliceToBob()
459
445
  }
460
- }
461
-
462
- const setup = async (connectAlice = true) => {
463
- const { teardown, aliceRepo, bobRepo, charlieRepo, connectAliceToBob } =
464
- setupRepos(connectAlice)
465
446
 
466
447
  const aliceHandle = aliceRepo.create<TestDoc>()
467
448
  aliceHandle.change(d => {
@@ -564,17 +545,19 @@ describe("Repo", () => {
564
545
  })
565
546
 
566
547
  it("doesn't find a document which doesn't exist anywhere on the network", async () => {
567
- const { charlieRepo } = await setup()
548
+ const { charlieRepo, teardown } = await setup()
568
549
  const url = generateAutomergeUrl()
569
550
  const handle = charlieRepo.find<TestDoc>(url)
570
551
  assert.equal(handle.isReady(), false)
571
552
 
572
553
  const doc = await handle.doc()
573
554
  assert.equal(doc, undefined)
555
+
556
+ teardown()
574
557
  })
575
558
 
576
559
  it("fires an 'unavailable' event when a document is not available on the network", async () => {
577
- const { charlieRepo } = await setup()
560
+ const { charlieRepo, teardown } = await setup()
578
561
  const url = generateAutomergeUrl()
579
562
  const handle = charlieRepo.find<TestDoc>(url)
580
563
  assert.equal(handle.isReady(), false)
@@ -588,6 +571,8 @@ describe("Repo", () => {
588
571
  const handle2 = charlieRepo.find<TestDoc>(url)
589
572
  assert.equal(handle2.isReady(), false)
590
573
  await eventPromise(handle2, "unavailable")
574
+
575
+ teardown()
591
576
  })
592
577
 
593
578
  it("a previously unavailable document syncs over the network if a peer with it connects", async () => {
@@ -597,7 +582,7 @@ describe("Repo", () => {
597
582
  aliceRepo,
598
583
  teardown,
599
584
  connectAliceToBob,
600
- } = await setup(false)
585
+ } = await setup({ connectAlice: false })
601
586
 
602
587
  const url = stringifyAutomergeUrl({ documentId: notForCharlie })
603
588
  const handle = charlieRepo.find<TestDoc>(url)
@@ -620,6 +605,55 @@ describe("Repo", () => {
620
605
  teardown()
621
606
  })
622
607
 
608
+ it("a previously unavailable document becomes available if the network adapter initially has no peers", async () => {
609
+ // It is possible for a network adapter to be ready without any peer
610
+ // being announced (e.g. the BroadcastChannelNetworkAdapter). In this
611
+ // case attempting to `Repo.find` a document which is not in the storage
612
+ // will result in an unavailable document. If a peer is later announced
613
+ // on the NetworkAdapter we should attempt to sync with the new peer and
614
+ // if the new peer has the document, the DocHandle should eventually
615
+ // transition to "ready"
616
+
617
+ // first create a repo with no network adapter and add a document so that
618
+ // we have a storage containing the document to pass to a new repo later
619
+ const storage = new DummyStorageAdapter()
620
+ const isolatedRepo = new Repo({
621
+ network: [],
622
+ storage,
623
+ })
624
+ const unsyncedHandle = isolatedRepo.create<TestDoc>()
625
+ const url = unsyncedHandle.url
626
+
627
+ // Now create a message channel to connect two repos
628
+ const abChannel = new MessageChannel()
629
+ const { port1: ab, port2: ba } = abChannel
630
+
631
+ // Create an empty repo to request the document from
632
+ const a = new Repo({
633
+ network: [new MessageChannelNetworkAdapter(ab)],
634
+ peerId: "a" as PeerId,
635
+ sharePolicy: async () => true,
636
+ })
637
+
638
+ const handle = a.find(url)
639
+
640
+ // We expect this to be unavailable as there is no connected peer and
641
+ // the repo has no storage.
642
+ await eventPromise(handle, "unavailable")
643
+
644
+ // Now create a repo pointing at the storage containing the document and
645
+ // connect it to the other end of the MessageChannel
646
+ const b = new Repo({
647
+ storage,
648
+ peerId: "b" as PeerId,
649
+ network: [new MessageChannelNetworkAdapter(ba)],
650
+ })
651
+
652
+ // The empty repo should be notified of the new peer, send it a request
653
+ // and eventually resolve the handle to "READY"
654
+ await handle.whenReady()
655
+ })
656
+
623
657
  it("a deleted document from charlieRepo can be refetched", async () => {
624
658
  const { charlieRepo, aliceHandle, teardown } = await setup()
625
659
 
@@ -642,40 +676,15 @@ describe("Repo", () => {
642
676
  teardown()
643
677
  })
644
678
 
645
- const setupMeshNetwork = async () => {
646
- const aliceRepo = new Repo({
647
- network: [new BroadcastChannelNetworkAdapter()],
648
- peerId: "alice" as PeerId,
649
- })
650
-
651
- const bobRepo = new Repo({
652
- network: [new BroadcastChannelNetworkAdapter()],
653
- peerId: "bob" as PeerId,
654
- })
655
-
656
- const charlieRepo = new Repo({
657
- network: [new BroadcastChannelNetworkAdapter()],
658
- peerId: "charlie" as PeerId,
659
- })
660
-
661
- // pause to let the network set up
662
- await pause(50)
663
-
664
- return {
665
- aliceRepo,
666
- bobRepo,
667
- charlieRepo,
668
- }
669
- }
670
-
671
679
  it("can emit an 'unavailable' event when it's not found on the network", async () => {
672
- const { charlieRepo } = await setupMeshNetwork()
680
+ const { charlieRepo, teardown } = await setup()
673
681
 
674
682
  const url = generateAutomergeUrl()
675
683
  const handle = charlieRepo.find<TestDoc>(url)
676
684
  assert.equal(handle.isReady(), false)
677
685
 
678
686
  await eventPromise(handle, "unavailable")
687
+ teardown()
679
688
  })
680
689
 
681
690
  it("syncs a bunch of changes", async () => {
@@ -705,7 +714,6 @@ describe("Repo", () => {
705
714
  d.foo = Math.random().toString()
706
715
  })
707
716
  }
708
- await pause(500)
709
717
 
710
718
  teardown()
711
719
  })
@@ -723,8 +731,6 @@ describe("Repo", () => {
723
731
  stringifyAutomergeUrl({ documentId: notForCharlie })
724
732
  )
725
733
 
726
- await pause(50)
727
-
728
734
  const charliePromise = new Promise<void>((resolve, reject) => {
729
735
  charlieRepo.networkSubsystem.on("message", message => {
730
736
  if (
@@ -746,55 +752,104 @@ describe("Repo", () => {
746
752
  await charliePromise
747
753
  teardown()
748
754
  })
755
+ })
749
756
 
750
- it("can broadcast a message without entering into an infinite loop", async () => {
751
- const { aliceRepo, bobRepo, charlieRepo } = await setupMeshNetwork()
757
+ describe("with peers (mesh network)", () => {
758
+ const setup = async () => {
759
+ // Set up three repos; connect Alice to Bob, Bob to Charlie, and Alice to Charlie
752
760
 
753
- // pause to let the network set up
754
- await pause(50)
755
- const message = { presence: "alex" }
761
+ const abChannel = new MessageChannel()
762
+ const bcChannel = new MessageChannel()
763
+ const acChannel = new MessageChannel()
764
+
765
+ const { port1: ab, port2: ba } = abChannel
766
+ const { port1: bc, port2: cb } = bcChannel
767
+ const { port1: ac, port2: ca } = acChannel
768
+
769
+ const aliceRepo = new Repo({
770
+ network: [
771
+ new MessageChannelNetworkAdapter(ab),
772
+ new MessageChannelNetworkAdapter(ac),
773
+ ],
774
+ peerId: "alice" as PeerId,
775
+ })
776
+
777
+ const bobRepo = new Repo({
778
+ network: [
779
+ new MessageChannelNetworkAdapter(ba),
780
+ new MessageChannelNetworkAdapter(bc),
781
+ ],
782
+ peerId: "bob" as PeerId,
783
+ })
784
+
785
+ const charlieRepo = new Repo({
786
+ network: [
787
+ new MessageChannelNetworkAdapter(ca),
788
+ new MessageChannelNetworkAdapter(cb),
789
+ ],
790
+ peerId: "charlie" as PeerId,
791
+ })
792
+
793
+ const teardown = () => {
794
+ abChannel.port1.close()
795
+ bcChannel.port1.close()
796
+ acChannel.port1.close()
797
+ }
798
+
799
+ await Promise.all([
800
+ eventPromise(aliceRepo.networkSubsystem, "peer"),
801
+ eventPromise(bobRepo.networkSubsystem, "peer"),
802
+ eventPromise(charlieRepo.networkSubsystem, "peer"),
803
+ ])
804
+
805
+ return {
806
+ teardown,
807
+ aliceRepo,
808
+ bobRepo,
809
+ charlieRepo,
810
+ }
811
+ }
812
+
813
+ it("can broadcast a message without entering into an infinite loop", async () => {
814
+ const { aliceRepo, bobRepo, charlieRepo, teardown } = await setup()
756
815
 
757
816
  const aliceHandle = aliceRepo.create<TestDoc>()
758
817
 
759
818
  const bobHandle = bobRepo.find(aliceHandle.url)
760
819
  const charlieHandle = charlieRepo.find(aliceHandle.url)
761
820
 
762
- const aliceDoesntGetIt = new Promise<void>((resolve, reject) => {
763
- setTimeout(() => {
764
- resolve()
765
- }, 100)
766
-
767
- aliceHandle.on("ephemeral-message", () => {
768
- reject("alice got the message")
769
- })
821
+ // Alice should not receive her own ephemeral message
822
+ aliceHandle.on("ephemeral-message", () => {
823
+ throw new Error("Alice should not receive her own ephemeral message")
770
824
  })
771
825
 
826
+ // Bob and Charlie should receive Alice's ephemeral message
772
827
  const bobGotIt = eventPromise(bobHandle, "ephemeral-message")
773
828
  const charlieGotIt = eventPromise(charlieHandle, "ephemeral-message")
774
829
 
775
- // let things get in sync and peers meet one another
830
+ // let peers meet and sync up
776
831
  await pause(50)
832
+
833
+ // Alice sends an ephemeral message
834
+ const message = { foo: "bar" }
777
835
  aliceHandle.broadcast(message)
778
836
 
779
- const [bob, charlie] = await Promise.all([
780
- bobGotIt,
781
- charlieGotIt,
782
- aliceDoesntGetIt,
783
- ])
837
+ const [bob, charlie] = await Promise.all([bobGotIt, charlieGotIt])
784
838
 
785
839
  assert.deepStrictEqual(bob.message, message)
786
840
  assert.deepStrictEqual(charlie.message, message)
841
+
842
+ teardown()
787
843
  })
788
844
 
789
845
  it("notifies peers when a document is cloned", async () => {
790
- const { bobRepo, charlieRepo } = await setupMeshNetwork()
791
-
792
- // pause to let the network set up
793
- await pause(50)
846
+ const { bobRepo, charlieRepo, teardown } = await setup()
794
847
 
795
848
  const handle = bobRepo.create<TestDoc>()
796
- handle.change(d => { d.foo = "bar" })
797
- const handle2 = bobRepo.clone(handle)
849
+ handle.change(d => {
850
+ d.foo = "bar"
851
+ })
852
+ const handle2 = bobRepo.clone(handle)
798
853
 
799
854
  // pause to let the sync happen
800
855
  await pause(50)
@@ -802,17 +857,18 @@ describe("Repo", () => {
802
857
  const charlieHandle = charlieRepo.find(handle2.url)
803
858
  await charlieHandle.doc()
804
859
  assert.deepStrictEqual(charlieHandle.docSync(), { foo: "bar" })
860
+
861
+ teardown()
805
862
  })
806
863
 
807
864
  it("notifies peers when a document is merged", async () => {
808
- const { bobRepo, charlieRepo } = await setupMeshNetwork()
809
-
810
- // pause to let the network set up
811
- await pause(50)
865
+ const { bobRepo, charlieRepo, teardown } = await setup()
812
866
 
813
867
  const handle = bobRepo.create<TestDoc>()
814
- handle.change(d => { d.foo = "bar" })
815
- const handle2 = bobRepo.clone(handle)
868
+ handle.change(d => {
869
+ d.foo = "bar"
870
+ })
871
+ const handle2 = bobRepo.clone(handle)
816
872
 
817
873
  // pause to let the sync happen
818
874
  await pause(50)
@@ -822,14 +878,29 @@ describe("Repo", () => {
822
878
  assert.deepStrictEqual(charlieHandle.docSync(), { foo: "bar" })
823
879
 
824
880
  // now make a change to doc2 on bobs side and merge it into doc1
825
- handle2.change(d => { d.foo = "baz" })
881
+ handle2.change(d => {
882
+ d.foo = "baz"
883
+ })
826
884
  handle.merge(handle2)
827
-
885
+
828
886
  // wait for the network to do it's thang
829
- await pause(50)
887
+ await pause(350)
830
888
 
831
- await charlieHandle.doc()
889
+ await charlieHandle.doc()
832
890
  assert.deepStrictEqual(charlieHandle.docSync(), { foo: "baz" })
891
+
892
+ teardown()
833
893
  })
834
894
  })
835
895
  })
896
+
897
+ const warn = console.warn
898
+ const NO_OP = () => {}
899
+
900
+ const disableConsoleWarn = () => {
901
+ console.warn = NO_OP
902
+ }
903
+
904
+ const reenableConsoleWarn = () => {
905
+ console.warn = warn
906
+ }
@@ -1,16 +1,13 @@
1
+ import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs"
2
+ import * as A from "@automerge/automerge/next"
3
+ import assert from "assert"
1
4
  import fs from "fs"
2
5
  import os from "os"
3
6
  import path from "path"
4
-
5
- import assert from "assert"
6
-
7
- import * as A from "@automerge/automerge/next"
8
-
9
- import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
10
- import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs"
11
-
12
- import { StorageSubsystem } from "../src/storage/StorageSubsystem.js"
7
+ import { describe, it } from "vitest"
13
8
  import { generateAutomergeUrl, parseAutomergeUrl } from "../src/DocUrl.js"
9
+ import { StorageSubsystem } from "../src/storage/StorageSubsystem.js"
10
+ import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
14
11
 
15
12
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "automerge-repo-tests"))
16
13
 
@@ -1,16 +1,21 @@
1
1
  import { NetworkAdapter } from "../../src/index.js"
2
2
 
3
3
  export class DummyNetworkAdapter extends NetworkAdapter {
4
- #startReady = true
5
- constructor(startReady: boolean) {
4
+ #startReady: boolean
5
+
6
+ constructor({ startReady = true }: Options = {}) {
6
7
  super()
7
8
  this.#startReady = startReady
8
9
  }
9
- send() { }
10
+ send() {}
10
11
  connect(_: string) {
11
12
  if (this.#startReady) {
12
13
  this.emit("ready", { network: this })
13
14
  }
14
15
  }
15
- disconnect() { }
16
+ disconnect() {}
17
+ }
18
+
19
+ type Options = {
20
+ startReady?: boolean
16
21
  }
@@ -11,10 +11,12 @@ export class DummyStorageAdapter implements StorageAdapter {
11
11
  return key.split(".")
12
12
  }
13
13
 
14
- async loadRange(keyPrefix: StorageKey): Promise<{data: Uint8Array, key: StorageKey}[]> {
14
+ async loadRange(
15
+ keyPrefix: StorageKey
16
+ ): Promise<{ data: Uint8Array; key: StorageKey }[]> {
15
17
  const range = Object.entries(this.#data)
16
18
  .filter(([key, _]) => key.startsWith(this.#keyToString(keyPrefix)))
17
- .map(([key, data]) => ({key: this.#stringToKey(key), data}))
19
+ .map(([key, data]) => ({ key: this.#stringToKey(key), data }))
18
20
  return Promise.resolve(range)
19
21
  }
20
22
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "NodeNext",
4
+ "moduleResolution": "Node16",
5
+ "noEmit": true
6
+ },
7
+ "include": ["**/*.ts"]
8
+ }