@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.
- package/.eslintrc +1 -1
- package/dist/DocHandle.d.ts +8 -8
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +4 -8
- package/dist/DocUrl.d.ts.map +1 -1
- package/dist/Repo.d.ts +6 -3
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +20 -19
- package/dist/helpers/cbor.d.ts +2 -2
- package/dist/helpers/cbor.d.ts.map +1 -1
- package/dist/helpers/cbor.js +1 -1
- package/dist/helpers/debounce.d.ts +14 -0
- package/dist/helpers/debounce.d.ts.map +1 -0
- package/dist/helpers/debounce.js +21 -0
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/pause.js +3 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +2 -2
- package/dist/helpers/throttle.d.ts +28 -0
- package/dist/helpers/throttle.d.ts.map +1 -0
- package/dist/helpers/throttle.js +39 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/network/NetworkAdapter.d.ts +3 -3
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.d.ts +2 -2
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +30 -18
- package/dist/network/messages.d.ts +38 -68
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +13 -21
- package/dist/storage/StorageSubsystem.d.ts +1 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +9 -9
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -3
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +3 -3
- package/dist/synchronizer/DocSynchronizer.d.ts +4 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +25 -34
- package/dist/synchronizer/Synchronizer.d.ts +2 -2
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +8 -6
- package/fuzz/tsconfig.json +8 -0
- package/package.json +5 -13
- package/src/DocHandle.ts +12 -15
- package/src/DocUrl.ts +3 -1
- package/src/Repo.ts +36 -29
- package/src/helpers/cbor.ts +4 -4
- package/src/helpers/debounce.ts +25 -0
- package/src/helpers/headsAreSame.ts +1 -1
- package/src/helpers/pause.ts +7 -2
- package/src/helpers/tests/network-adapter-tests.ts +3 -3
- package/src/helpers/throttle.ts +43 -0
- package/src/helpers/withTimeout.ts +2 -2
- package/src/index.ts +36 -29
- package/src/network/NetworkAdapter.ts +7 -3
- package/src/network/NetworkSubsystem.ts +31 -23
- package/src/network/messages.ts +88 -151
- package/src/storage/StorageSubsystem.ts +12 -12
- package/src/synchronizer/CollectionSynchronizer.ts +7 -16
- package/src/synchronizer/DocSynchronizer.ts +42 -53
- package/src/synchronizer/Synchronizer.ts +2 -2
- package/src/types.ts +8 -3
- package/test/CollectionSynchronizer.test.ts +58 -53
- package/test/DocHandle.test.ts +35 -36
- package/test/DocSynchronizer.test.ts +5 -8
- package/test/Network.test.ts +1 -0
- package/test/Repo.test.ts +229 -158
- package/test/StorageSubsystem.test.ts +6 -9
- package/test/helpers/DummyNetworkAdapter.ts +9 -4
- package/test/helpers/DummyStorageAdapter.ts +4 -2
- package/test/tsconfig.json +8 -0
- package/typedoc.json +3 -3
- package/.mocharc.json +0 -5
- package/dist/EphemeralData.d.ts +0 -20
- package/dist/EphemeralData.d.ts.map +0 -1
- package/dist/EphemeralData.js +0 -1
- 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
|
|
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 {
|
|
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("
|
|
31
|
-
const setup = (
|
|
28
|
+
describe("local only", () => {
|
|
29
|
+
const setup = ({ startReady = true } = {}) => {
|
|
32
30
|
const storageAdapter = new DummyStorageAdapter()
|
|
33
|
-
const networkAdapter = new DummyNetworkAdapter(
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
d
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
306
|
-
|
|
297
|
+
repo.on("delete-document", ({ documentId }) => {
|
|
298
|
+
assert.equal(documentId, handle.documentId)
|
|
307
299
|
|
|
308
|
-
|
|
309
|
-
|
|
300
|
+
done()
|
|
301
|
+
})
|
|
310
302
|
|
|
311
|
-
|
|
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("
|
|
389
|
-
const
|
|
390
|
-
|
|
380
|
+
describe("with peers (linear network)", async () => {
|
|
381
|
+
const setup = async ({ connectAlice = true } = {}) => {
|
|
382
|
+
const charlieExcludedDocuments: DocumentId[] = []
|
|
383
|
+
const bobExcludedDocuments: DocumentId[] = []
|
|
391
384
|
|
|
392
|
-
|
|
393
|
-
|
|
385
|
+
const sharePolicy: SharePolicy = async (peerId, documentId) => {
|
|
386
|
+
if (documentId === undefined) return false
|
|
394
387
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
395
|
+
// make sure that bob never gets excluded documents
|
|
396
|
+
if (bobExcludedDocuments.includes(documentId) && peerId === "bob")
|
|
397
|
+
return false
|
|
402
398
|
|
|
403
|
-
|
|
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
|
|
410
|
-
const
|
|
404
|
+
const abChannel = new MessageChannel()
|
|
405
|
+
const bcChannel = new MessageChannel()
|
|
411
406
|
|
|
412
|
-
const { port1:
|
|
413
|
-
const { port1:
|
|
407
|
+
const { port1: ab, port2: ba } = abChannel
|
|
408
|
+
const { port1: bc, port2: cb } = bcChannel
|
|
414
409
|
|
|
415
|
-
const aliceNetworkAdapter = new MessageChannelNetworkAdapter(
|
|
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(
|
|
426
|
-
new MessageChannelNetworkAdapter(
|
|
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(
|
|
428
|
+
network: [new MessageChannelNetworkAdapter(cb)],
|
|
434
429
|
peerId: "charlie" as PeerId,
|
|
435
430
|
})
|
|
436
431
|
|
|
437
432
|
const teardown = () => {
|
|
438
|
-
|
|
439
|
-
|
|
433
|
+
abChannel.port1.close()
|
|
434
|
+
bcChannel.port1.close()
|
|
440
435
|
}
|
|
441
436
|
|
|
442
|
-
function
|
|
437
|
+
function connectAliceToBob() {
|
|
443
438
|
aliceRepo.networkSubsystem.addNetworkAdapter(
|
|
444
|
-
new MessageChannelNetworkAdapter(
|
|
439
|
+
new MessageChannelNetworkAdapter(ab)
|
|
445
440
|
)
|
|
446
|
-
//bobRepo.networkSubsystem.addNetworkAdapter(new MessageChannelNetworkAdapter(bobToAlice))
|
|
447
441
|
}
|
|
448
442
|
|
|
449
443
|
if (connectAlice) {
|
|
450
|
-
|
|
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
|
|
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
|
-
|
|
751
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
const
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
|
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
|
|
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 => {
|
|
797
|
-
|
|
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
|
|
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 => {
|
|
815
|
-
|
|
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 => {
|
|
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(
|
|
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
|
|
5
|
-
|
|
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(
|
|
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
|
|