@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1068cf700f

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 (199) hide show
  1. package/dist/lib/neutral/chunk-FJPXA75J.mjs +235 -0
  2. package/dist/lib/neutral/chunk-FJPXA75J.mjs.map +7 -0
  3. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-WIQUYO7H.mjs} +300 -213
  4. package/dist/lib/neutral/chunk-WIQUYO7H.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/{browser → neutral}/index.mjs +2832 -946
  7. package/dist/lib/neutral/index.mjs.map +7 -0
  8. package/dist/lib/neutral/meta.json +1 -0
  9. package/dist/lib/{browser → neutral}/testing/index.mjs +73 -224
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host.d.ts +53 -42
  12. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  13. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  14. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  15. package/dist/types/src/automerge/collection-synchronizer.d.ts +6 -4
  16. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  17. package/dist/types/src/automerge/echo-network-adapter.d.ts +22 -10
  18. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  19. package/dist/types/src/automerge/echo-replicator.d.ts +37 -15
  20. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  21. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  22. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  23. package/dist/types/src/automerge/index.d.ts +1 -1
  24. package/dist/types/src/automerge/index.d.ts.map +1 -1
  25. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +5 -5
  26. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  28. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  29. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +5 -5
  30. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  31. package/dist/types/src/common/codec.d.ts +1 -1
  32. package/dist/types/src/common/codec.d.ts.map +1 -1
  33. package/dist/types/src/db-host/automerge-data-source.d.ts +34 -0
  34. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  35. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  36. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  37. package/dist/types/src/db-host/data-service.d.ts +5 -4
  38. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  39. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  40. package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
  41. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  42. package/dist/types/src/db-host/echo-host.d.ts +44 -23
  43. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  44. package/dist/types/src/db-host/index.d.ts +1 -0
  45. package/dist/types/src/db-host/index.d.ts.map +1 -1
  46. package/dist/types/src/db-host/local-queue-service.d.ts +16 -0
  47. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  48. package/dist/types/src/db-host/query-service.d.ts +14 -9
  49. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  50. package/dist/types/src/db-host/queue-data-source.d.ts +28 -0
  51. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  52. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  53. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  54. package/dist/types/src/db-host/space-state-manager.d.ts +2 -1
  55. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  56. package/dist/types/src/db-host/stub.d.ts +10 -0
  57. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  58. package/dist/types/src/edge/echo-edge-replicator.d.ts +8 -6
  59. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  60. package/dist/types/src/filter/filter-match.d.ts +4 -1
  61. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  62. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  63. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  64. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  65. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  66. package/dist/types/src/query/errors.d.ts +21 -9
  67. package/dist/types/src/query/errors.d.ts.map +1 -1
  68. package/dist/types/src/query/plan.d.ts +57 -5
  69. package/dist/types/src/query/plan.d.ts.map +1 -1
  70. package/dist/types/src/query/query-executor.d.ts +49 -10
  71. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  72. package/dist/types/src/query/query-planner.d.ts +9 -0
  73. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  74. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  75. package/dist/types/src/space/auth.d.ts +3 -3
  76. package/dist/types/src/space/auth.d.ts.map +1 -1
  77. package/dist/types/src/space/control-pipeline.d.ts +3 -3
  78. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  79. package/dist/types/src/space/space-manager.d.ts +7 -7
  80. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  81. package/dist/types/src/space/space-protocol.d.ts +6 -6
  82. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  83. package/dist/types/src/space/space.d.ts +4 -4
  84. package/dist/types/src/space/space.d.ts.map +1 -1
  85. package/dist/types/src/testing/index.d.ts +0 -2
  86. package/dist/types/src/testing/index.d.ts.map +1 -1
  87. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  88. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  89. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  90. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  91. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  92. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  93. package/dist/types/src/util.d.ts +1 -1
  94. package/dist/types/src/util.d.ts.map +1 -1
  95. package/dist/types/tsconfig.tsbuildinfo +1 -1
  96. package/package.json +53 -45
  97. package/src/automerge/automerge-host.test.ts +55 -24
  98. package/src/automerge/automerge-host.ts +399 -126
  99. package/src/automerge/automerge-repo.test.ts +214 -46
  100. package/src/automerge/collection-synchronizer.test.ts +22 -15
  101. package/src/automerge/collection-synchronizer.ts +57 -22
  102. package/src/automerge/echo-data-monitor.ts +1 -1
  103. package/src/automerge/echo-network-adapter.test.ts +3 -3
  104. package/src/automerge/echo-network-adapter.ts +73 -28
  105. package/src/automerge/echo-replicator.ts +40 -15
  106. package/src/automerge/heads-store.ts +42 -3
  107. package/src/automerge/index.ts +1 -1
  108. package/src/automerge/leveldb-storage-adapter.ts +11 -11
  109. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  110. package/src/automerge/mesh-echo-replicator.ts +15 -11
  111. package/src/automerge/storage-adapter.test.ts +1 -1
  112. package/src/common/space-id.ts +1 -1
  113. package/src/db-host/automerge-data-source.test.ts +292 -0
  114. package/src/db-host/automerge-data-source.ts +144 -0
  115. package/src/db-host/data-service.ts +19 -20
  116. package/src/db-host/database-root.ts +2 -2
  117. package/src/db-host/documents-synchronizer.test.ts +73 -13
  118. package/src/db-host/documents-synchronizer.ts +58 -44
  119. package/src/db-host/echo-host.ts +194 -84
  120. package/src/db-host/index.ts +1 -0
  121. package/src/db-host/local-queue-service.ts +147 -0
  122. package/src/db-host/query-service.ts +28 -95
  123. package/src/db-host/queue-data-source.ts +132 -0
  124. package/src/db-host/queue-service.test.ts +145 -0
  125. package/src/db-host/space-state-manager.ts +6 -2
  126. package/src/db-host/stub.ts +28 -0
  127. package/src/edge/echo-edge-replicator.test.ts +8 -6
  128. package/src/edge/echo-edge-replicator.ts +124 -42
  129. package/src/filter/filter-match.test.ts +58 -32
  130. package/src/filter/filter-match.ts +163 -15
  131. package/src/metadata/metadata-store.ts +8 -5
  132. package/src/pipeline/pipeline-stress.test.ts +4 -2
  133. package/src/pipeline/pipeline.test.ts +3 -2
  134. package/src/pipeline/pipeline.ts +10 -7
  135. package/src/query/errors.ts +3 -1
  136. package/src/query/plan.ts +75 -5
  137. package/src/query/query-executor.ts +812 -117
  138. package/src/query/query-planner.test.ts +652 -34
  139. package/src/query/query-planner.ts +276 -13
  140. package/src/space/admission-discovery-extension.ts +3 -3
  141. package/src/space/auth.ts +6 -6
  142. package/src/space/control-pipeline.test.ts +4 -3
  143. package/src/space/control-pipeline.ts +11 -8
  144. package/src/space/space-manager.browser.test.ts +1 -1
  145. package/src/space/space-manager.ts +11 -10
  146. package/src/space/space-protocol.browser.test.ts +2 -2
  147. package/src/space/space-protocol.test.ts +3 -2
  148. package/src/space/space-protocol.ts +17 -14
  149. package/src/space/space.test.ts +1 -1
  150. package/src/space/space.ts +6 -5
  151. package/src/testing/index.ts +0 -2
  152. package/src/testing/test-agent-builder.ts +4 -3
  153. package/src/testing/test-network-adapter.ts +16 -3
  154. package/src/testing/test-replicator.ts +23 -18
  155. package/src/util.ts +7 -3
  156. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  157. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  158. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  159. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  160. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  161. package/dist/lib/browser/index.mjs.map +0 -7
  162. package/dist/lib/browser/meta.json +0 -1
  163. package/dist/lib/browser/testing/index.mjs.map +0 -7
  164. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  165. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  166. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  167. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  168. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  169. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  170. package/dist/lib/node/filter/index.cjs +0 -32
  171. package/dist/lib/node/filter/index.cjs.map +0 -7
  172. package/dist/lib/node/index.cjs +0 -4699
  173. package/dist/lib/node/index.cjs.map +0 -7
  174. package/dist/lib/node/meta.json +0 -1
  175. package/dist/lib/node/testing/index.cjs +0 -753
  176. package/dist/lib/node/testing/index.cjs.map +0 -7
  177. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  178. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  179. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  180. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  181. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  182. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  183. package/dist/lib/node-esm/filter/index.mjs +0 -11
  184. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  185. package/dist/lib/node-esm/index.mjs +0 -4743
  186. package/dist/lib/node-esm/index.mjs.map +0 -7
  187. package/dist/lib/node-esm/meta.json +0 -1
  188. package/dist/lib/node-esm/testing/index.mjs +0 -742
  189. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  190. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  191. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  192. package/dist/types/src/testing/test-data.d.ts +0 -18
  193. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  194. package/dist/types/src/testing/test-schema.d.ts +0 -37
  195. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  196. package/src/db-host/documents-iterator.ts +0 -74
  197. package/src/testing/test-data.ts +0 -127
  198. package/src/testing/test-schema.ts +0 -53
  199. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  import {
6
+ next as A,
6
7
  type Heads,
7
8
  change,
8
9
  clone,
@@ -10,7 +11,6 @@ import {
10
11
  from,
11
12
  getBackend,
12
13
  getHeads,
13
- next as A,
14
14
  save,
15
15
  saveSince,
16
16
  } from '@automerge/automerge';
@@ -19,28 +19,31 @@ import {
19
19
  type DocHandle,
20
20
  type DocumentId,
21
21
  type HandleState,
22
- type StorageAdapterInterface,
22
+ type Message,
23
23
  type PeerId,
24
24
  Repo,
25
25
  type SharePolicy,
26
+ type StorageAdapterInterface,
26
27
  generateAutomergeUrl,
27
28
  parseAutomergeUrl,
28
29
  } from '@automerge/automerge-repo';
29
- import { onTestFinished, describe, expect, test } from 'vitest';
30
+ import { describe, expect, onTestFinished, test } from 'vitest';
30
31
 
31
32
  import { asyncTimeout, sleep } from '@dxos/async';
32
33
  import { randomBytes } from '@dxos/crypto';
33
34
  import { PublicKey } from '@dxos/keys';
34
35
  import { createTestLevel } from '@dxos/kv-store/testing';
36
+ import { log } from '@dxos/log';
35
37
  import { TestBuilder as TeleportBuilder, TestPeer as TeleportPeer } from '@dxos/teleport/testing';
36
38
  import { openAndClose } from '@dxos/test-utils';
37
39
  import { isNonNullable, range } from '@dxos/util';
38
40
 
41
+ import { TestAdapter, type TestConnectionStateProvider } from '../testing';
42
+
39
43
  import { FIND_PARAMS } from './automerge-host';
40
44
  import { EchoNetworkAdapter } from './echo-network-adapter';
41
45
  import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
42
46
  import { MeshEchoReplicator } from './mesh-echo-replicator';
43
- import { TestAdapter, type TestConnectionStateProvider } from '../testing';
44
47
 
45
48
  const HOST_AND_CLIENT: [string, string] = ['host', 'client'];
46
49
 
@@ -161,32 +164,6 @@ describe('AutomergeRepo', () => {
161
164
  expect((await asyncTimeout(client.find<any>(handle.url), 1000))!.doc().text).to.equal(text);
162
165
  });
163
166
 
164
- test('share policy gets enabled afterwards', async () => {
165
- let sharePolicy = false;
166
-
167
- const { repos, adapters } = await createHostClientRepoTopology({
168
- sharePolicy: async () => sharePolicy,
169
- });
170
- const [host, client] = repos;
171
- await connectAdapters(adapters);
172
-
173
- const handle = host.create();
174
- const text = 'Hello world';
175
- handle.change((doc: any) => {
176
- doc.text = text;
177
- });
178
-
179
- {
180
- await client.find(handle.url, { allowableStates: ['unavailable'] });
181
- }
182
-
183
- sharePolicy = true;
184
-
185
- {
186
- await client.find(handle.url, { allowableStates: ['unavailable'] });
187
- }
188
- });
189
-
190
167
  test('two documents and share policy switching', async () => {
191
168
  const allowedDocs: DocumentId[] = [];
192
169
 
@@ -420,13 +397,7 @@ describe('AutomergeRepo', () => {
420
397
 
421
398
  test('client creates doc and syncs with a Repo', async () => {
422
399
  const repo = new Repo({ network: [] });
423
- const receiveByServer = async (blob: Uint8Array, docId: DocumentId) => {
424
- const serverHandle = await repo.find(docId, FIND_PARAMS);
425
- serverHandle.update((doc) => {
426
- return A.loadIncremental(doc, blob);
427
- });
428
- };
429
-
400
+ const receiveByServer = (blob: Uint8Array, docId: DocumentId) => repo.import<any>(blob, { docId });
430
401
  let clientDoc = A.from<{ field?: string }>({});
431
402
  const { documentId } = parseAutomergeUrl(generateAutomergeUrl());
432
403
  // Sync handshake.
@@ -434,7 +405,7 @@ describe('AutomergeRepo', () => {
434
405
 
435
406
  // Sync protocol.
436
407
  const sendDoc = async (doc: A.Doc<any>) => {
437
- await receiveByServer(saveSince(doc, sentHeads), documentId);
408
+ receiveByServer(saveSince(doc, sentHeads), documentId);
438
409
  sentHeads = getHeads(doc);
439
410
  };
440
411
 
@@ -456,10 +427,9 @@ describe('AutomergeRepo', () => {
456
427
 
457
428
  const repo = new Repo({ network: [], storage });
458
429
  const receiveByServer = async (blob: Uint8Array, docId: DocumentId) => {
459
- const serverHandle = await repo.find(docId, FIND_PARAMS);
460
- serverHandle.update((doc) => {
461
- return A.loadIncremental(doc, blob);
462
- });
430
+ repo.import<any>(blob, { docId });
431
+ // TODO(mykola): This should not be required. Document is not persisted without it.
432
+ await repo.flush([docId]);
463
433
  };
464
434
 
465
435
  let clientDoc = A.from<{ field?: string }>({ field: 'foo' });
@@ -516,6 +486,139 @@ describe('AutomergeRepo', () => {
516
486
  await asyncTimeout(handleB.whenReady(), 1000);
517
487
  expect(handleB.doc()!.text).to.equal(text);
518
488
  });
489
+
490
+ test('retry share config', async () => {
491
+ let announce = false;
492
+
493
+ const { repos, adapters } = await createRepoTopology({
494
+ peers: ['A', 'B'],
495
+ connections: [['A', 'B']],
496
+ options: {
497
+ shareConfig: {
498
+ access: async () => true,
499
+ announce: async () => announce,
500
+ },
501
+ },
502
+ onMessage: (message) => {
503
+ console.log(`${message.senderId} -> ${message.targetId}: ${message.type} ${message.documentId ?? ''}`);
504
+ },
505
+ });
506
+ const [repoA, repoB] = repos;
507
+ await connectAdapters(adapters);
508
+ console.log({ peersA: repoA.peers, peersB: repoB.peers });
509
+
510
+ const docA = repoA.create({ text: 'Hello world' });
511
+
512
+ const docB = await repoB.find(docA.url, { allowableStates: ['ready', 'unavailable'] });
513
+ expect(docB.state).to.equal('unavailable');
514
+ log.info('unavailable');
515
+
516
+ announce = true;
517
+ repoB.shareConfigChanged();
518
+ await docB.whenReady();
519
+ });
520
+ });
521
+
522
+ describe('create2 tests', () => {
523
+ test('basic document creation', async () => {
524
+ const repo = new Repo({ network: [] });
525
+
526
+ const handle = await repo.create2<{ text: string }>();
527
+ expect(handle.doc()).to.deep.equal({});
528
+
529
+ handle.change((doc: any) => {
530
+ doc.text = 'Hello World';
531
+ });
532
+
533
+ expect(handle.doc()?.text).to.equal('Hello World');
534
+ });
535
+
536
+ test('document mutation', async () => {
537
+ const repo = new Repo({ network: [] });
538
+
539
+ const handle = await repo.create2<{ counter: number }>({ counter: 0 });
540
+
541
+ for (let i = 1; i <= 10; i++) {
542
+ handle.change((doc: any) => {
543
+ doc.counter = i;
544
+ });
545
+ }
546
+
547
+ expect(handle.doc()?.counter).to.equal(10);
548
+ });
549
+
550
+ test('import document and mutate', async () => {
551
+ const repo = new Repo({ network: [] });
552
+
553
+ const original = await repo.create2<{ text: string }>({ text: 'original' });
554
+ const blob = A.save(original.doc()!);
555
+
556
+ const imported = repo.import<{ text: string }>(blob);
557
+ expect(imported.doc()?.text).to.equal('original');
558
+
559
+ imported.change((doc: any) => {
560
+ doc.text = 'mutated';
561
+ });
562
+ expect(imported.doc()?.text).to.equal('mutated');
563
+ });
564
+
565
+ test('reload document with flush', async () => {
566
+ const path = createTmpPath();
567
+ const text = 'Hello World!';
568
+ let url: AutomergeUrl;
569
+
570
+ {
571
+ const level = createTestLevel(path);
572
+ const storage = await createLevelAdapter(level);
573
+ const repo = new Repo({ network: [], storage });
574
+ const handle = await repo.create2<{ text: string }>();
575
+ url = handle.url;
576
+ handle.change((doc: any) => {
577
+ doc.text = text;
578
+ });
579
+ await repo.flush([handle.documentId]);
580
+ await level.close();
581
+ }
582
+
583
+ {
584
+ const level = createTestLevel(path);
585
+ const storage = await createLevelAdapter(level);
586
+ const repo = new Repo({ network: [], storage });
587
+ const handle = await repo.find<{ text: string }>(url);
588
+ await handle.whenReady();
589
+ expect(handle.doc()?.text).to.equal(text);
590
+ await level.close();
591
+ }
592
+ });
593
+
594
+ test('reload document without flush', async () => {
595
+ const path = createTmpPath();
596
+ const text = 'Hello World!';
597
+ let url: AutomergeUrl;
598
+
599
+ {
600
+ const level = createTestLevel(path);
601
+ const storage = await createLevelAdapter(level);
602
+ const repo = new Repo({ network: [], storage });
603
+ const handle = await repo.create2<{ text: string }>();
604
+ url = handle.url;
605
+ handle.change((doc: any) => {
606
+ doc.text = text;
607
+ });
608
+ // No explicit flush - rely on auto-save.
609
+ await sleep(200);
610
+ await level.close();
611
+ }
612
+
613
+ {
614
+ const level = createTestLevel(path);
615
+ const storage = await createLevelAdapter(level);
616
+ const repo = new Repo({ network: [], storage });
617
+ const handle = await repo.find<{ text: string }>(url);
618
+ await handle.whenReady();
619
+ expect(handle.doc()?.text).to.equal(text);
620
+ }
621
+ });
519
622
  });
520
623
 
521
624
  describe('teleport', () => {
@@ -677,10 +780,66 @@ describe('AutomergeRepo', () => {
677
780
  await doc.whenReady();
678
781
  expect(doc.doc()).to.deep.eq(document.doc());
679
782
  });
783
+
784
+ test('document is passively replicated to connected peers', async () => {
785
+ const [spaceKey] = PublicKey.randomSequence();
786
+
787
+ const teleportBuilder = new TeleportBuilder();
788
+ onTestFinished(() => teleportBuilder.destroy());
789
+
790
+ const peer1 = await createTeleportTestPeer(teleportBuilder, spaceKey);
791
+ const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey);
792
+
793
+ const handle = peer1.repo.create();
794
+ handle.change((doc: any) => (doc.text = 'hello'));
795
+ await connectPeers(spaceKey, teleportBuilder, peer1, peer2);
796
+
797
+ await expect
798
+ .poll(async () => {
799
+ const doc = peer2.repo.handles[handle.documentId];
800
+ await doc.whenReady();
801
+ return doc.doc()!.text;
802
+ })
803
+ .toEqual('hello');
804
+ });
805
+
806
+ test('imported document is passively replicated to connected peers', async () => {
807
+ let blob: Uint8Array;
808
+ let documentId: DocumentId;
809
+ {
810
+ const repo = new Repo();
811
+ const handle = repo.create();
812
+ handle.change((doc: any) => (doc.text = 'hello'));
813
+ blob = A.save(handle.doc()!);
814
+ documentId = handle.documentId;
815
+ }
816
+
817
+ const [spaceKey] = PublicKey.randomSequence();
818
+
819
+ const teleportBuilder = new TeleportBuilder();
820
+ onTestFinished(() => teleportBuilder.destroy());
821
+
822
+ const peer1 = await createTeleportTestPeer(teleportBuilder, spaceKey);
823
+ const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey);
824
+ await connectPeers(spaceKey, teleportBuilder, peer1, peer2);
825
+
826
+ const handle = peer1.repo.import(blob, { docId: documentId });
827
+ await handle.whenReady();
828
+
829
+ await expect
830
+ .poll(
831
+ async () => {
832
+ const doc = peer2.repo.handles[handle.documentId];
833
+ await doc.whenReady();
834
+ return doc.doc()!.text;
835
+ },
836
+ { timeout: 1_000 },
837
+ )
838
+ .toEqual('hello');
839
+ });
680
840
  });
681
841
 
682
- const createLevelAdapter = async () => {
683
- const level = createTestLevel();
842
+ const createLevelAdapter = async (level = createTestLevel()) => {
684
843
  const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
685
844
  await openAndClose(level, storage);
686
845
  return storage;
@@ -702,19 +861,23 @@ describe('AutomergeRepo', () => {
702
861
  };
703
862
  });
704
863
 
864
+ type ShareConfig = Exclude<ConstructorParameters<typeof Repo>[0], undefined>['shareConfig'];
865
+
705
866
  type ConnectedRepoOptions = {
706
867
  storages?: StorageAdapterInterface[];
707
868
  connectionStateProvider?: TestConnectionStateProvider;
708
869
  sharePolicy?: SharePolicy;
870
+ shareConfig?: ShareConfig;
709
871
  };
710
872
 
711
873
  const createRepoTopology = async <Peers extends string[], Peer extends string = Peers[number]>(args: {
712
874
  peers: Peers;
713
875
  connections: [Peer, Peer][];
714
876
  options?: ConnectedRepoOptions;
877
+ onMessage?: (message: Message) => void;
715
878
  }) => {
716
879
  const adapters = args.connections.map(
717
- () => TestAdapter.createPair(args.options?.connectionStateProvider) as [TestAdapter, TestAdapter],
880
+ () => TestAdapter.createPair(args.options?.connectionStateProvider, args.onMessage) as [TestAdapter, TestAdapter],
718
881
  );
719
882
  const repos = args.peers.map((peerId, peerIndex) => {
720
883
  const network = adapters
@@ -730,7 +893,8 @@ const createRepoTopology = async <Peers extends string[], Peer extends string =
730
893
  peerId: peerId as PeerId,
731
894
  storage: args.options?.storages?.[peerIndex],
732
895
  network,
733
- sharePolicy: args.options?.sharePolicy ?? (async () => true),
896
+ sharePolicy: (args.options?.sharePolicy ?? args.options?.shareConfig) ? undefined : async () => true,
897
+ shareConfig: args.options?.shareConfig,
734
898
  });
735
899
  });
736
900
  return { repos, adapters };
@@ -816,3 +980,7 @@ const connectPeers = async (
816
980
  };
817
981
 
818
982
  type TeleportTestPeer = { repo: Repo; meshAdapter: MeshEchoReplicator; teleport: TeleportPeer };
983
+
984
+ export const createTmpPath = (): string => {
985
+ return `/tmp/dxos-${PublicKey.random().toHex()}`;
986
+ };
@@ -2,12 +2,15 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import type { PeerId } from '@automerge/automerge-repo';
6
- import { onTestFinished, describe, expect, test } from 'vitest';
5
+ import { type Heads } from '@automerge/automerge';
6
+ import * as Automerge from '@automerge/automerge';
7
+ import type { DocumentId, PeerId } from '@automerge/automerge-repo';
8
+ import { describe, expect, onTestFinished, test } from 'vitest';
7
9
 
8
10
  import { sleep } from '@dxos/async';
11
+ import { range } from '@dxos/util';
9
12
 
10
- import { CollectionSynchronizer, diffCollectionState, type CollectionState } from './collection-synchronizer';
13
+ import { type CollectionState, CollectionSynchronizer, diffCollectionState } from './collection-synchronizer';
11
14
 
12
15
  describe('CollectionSynchronizer', () => {
13
16
  test('sync two peers', async () => {
@@ -53,16 +56,18 @@ describe('CollectionSynchronizer', () => {
53
56
  peer1.onConnectionOpen(peerId2);
54
57
  peer2.onConnectionOpen(peerId1);
55
58
 
59
+ const updated = Promise.all([
60
+ peer1.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId2),
61
+ peer2.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId1),
62
+ ]);
63
+
56
64
  peer1.setLocalCollectionState(collectionId, STATE_1);
57
65
  peer2.setLocalCollectionState(collectionId, STATE_2);
58
66
 
59
67
  peer1.refreshCollection(collectionId);
60
68
  peer2.refreshCollection(collectionId);
61
69
 
62
- await Promise.all([
63
- peer1.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId2),
64
- peer2.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId1),
65
- ]);
70
+ await updated;
66
71
 
67
72
  expect(peer1.getRemoteCollectionStates(collectionId).get(peerId2)).to.deep.equal(STATE_2);
68
73
  expect(peer2.getRemoteCollectionStates(collectionId).get(peerId1)).to.deep.equal(STATE_1);
@@ -79,18 +84,20 @@ describe('CollectionSynchronizer', () => {
79
84
  });
80
85
  });
81
86
 
87
+ const TEST_HEADS = range(4).map((i) => Automerge.getHeads(Automerge.from({ i: i.toString() })));
88
+
82
89
  const STATE_1: CollectionState = {
83
90
  documents: {
84
- a: ['1'],
85
- b: ['2'],
86
- c: ['3'],
87
- },
91
+ a: TEST_HEADS[0],
92
+ b: TEST_HEADS[1],
93
+ c: TEST_HEADS[2],
94
+ } as Record<DocumentId, Heads>,
88
95
  };
89
96
 
90
97
  const STATE_2: CollectionState = {
91
98
  documents: {
92
- a: ['1'],
93
- b: ['4'],
94
- d: ['3'],
95
- },
99
+ a: TEST_HEADS[0],
100
+ b: TEST_HEADS[3],
101
+ d: TEST_HEADS[2],
102
+ } as Record<DocumentId, Heads>,
96
103
  };
@@ -2,20 +2,22 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { next as am } from '@automerge/automerge';
5
+ import { next as A, type Heads } from '@automerge/automerge';
6
6
  import type { DocumentId, PeerId } from '@automerge/automerge-repo';
7
+ import * as Array from 'effect/Array';
8
+ import * as Record from 'effect/Record';
7
9
 
8
- import { asyncReturn, Event, scheduleTask, scheduleTaskInterval } from '@dxos/async';
9
- import { Resource, type Context } from '@dxos/context';
10
+ import { Event, asyncReturn, scheduleTask, scheduleTaskInterval } from '@dxos/async';
11
+ import { type Context, Resource } from '@dxos/context';
10
12
  import { log } from '@dxos/log';
11
13
  import { trace } from '@dxos/tracing';
12
14
  import { defaultMap } from '@dxos/util';
13
15
 
14
16
  const MIN_QUERY_INTERVAL = 5_000;
15
17
 
16
- const POLL_INTERVAL = 30_000;
18
+ const POLL_INTERVAL = 10_000;
17
19
 
18
- export type CollectionSynchronizerParams = {
20
+ export type CollectionSynchronizerProps = {
19
21
  sendCollectionState: (collectionId: string, peerId: PeerId, state: CollectionState) => void;
20
22
  queryCollectionState: (collectionId: string, peerId: PeerId) => void;
21
23
  shouldSyncCollection: (collectionId: string, peerId: PeerId) => boolean;
@@ -26,9 +28,9 @@ export type CollectionSynchronizerParams = {
26
28
  */
27
29
  @trace.resource()
28
30
  export class CollectionSynchronizer extends Resource {
29
- private readonly _sendCollectionState: CollectionSynchronizerParams['sendCollectionState'];
30
- private readonly _queryCollectionState: CollectionSynchronizerParams['queryCollectionState'];
31
- private readonly _shouldSyncCollection: CollectionSynchronizerParams['shouldSyncCollection'];
31
+ private readonly _sendCollectionState: CollectionSynchronizerProps['sendCollectionState'];
32
+ private readonly _queryCollectionState: CollectionSynchronizerProps['queryCollectionState'];
33
+ private readonly _shouldSyncCollection: CollectionSynchronizerProps['shouldSyncCollection'];
32
34
 
33
35
  /**
34
36
  * CollectionId -> State.
@@ -40,7 +42,7 @@ export class CollectionSynchronizer extends Resource {
40
42
 
41
43
  public readonly remoteStateUpdated = new Event<{ collectionId: string; peerId: PeerId; newDocsAppeared: boolean }>();
42
44
 
43
- constructor(params: CollectionSynchronizerParams) {
45
+ constructor(params: CollectionSynchronizerProps) {
44
46
  super();
45
47
  this._sendCollectionState = params.sendCollectionState;
46
48
  this._queryCollectionState = params.queryCollectionState;
@@ -76,6 +78,10 @@ export class CollectionSynchronizer extends Resource {
76
78
  log('setLocalCollectionState', { collectionId, state });
77
79
  this._getOrCreatePerCollectionState(collectionId).localState = state;
78
80
 
81
+ for (const peerId of this._connectedPeers) {
82
+ this._diffCollectionState(collectionId, peerId);
83
+ }
84
+
79
85
  queueMicrotask(async () => {
80
86
  if (!this._ctx.disposed && this._activeCollections.has(collectionId)) {
81
87
  this._refreshInterestedPeers(collectionId);
@@ -117,6 +123,7 @@ export class CollectionSynchronizer extends Resource {
117
123
  * Callback when a connection to a peer is established.
118
124
  */
119
125
  onConnectionOpen(peerId: PeerId): void {
126
+ log('onConnectionOpen', { peerId });
120
127
  const spanId = getSpanName(peerId);
121
128
  trace.spanStart({
122
129
  id: spanId,
@@ -146,6 +153,8 @@ export class CollectionSynchronizer extends Resource {
146
153
  * Callback when a connection to a peer is closed.
147
154
  */
148
155
  onConnectionClosed(peerId: PeerId): void {
156
+ log('onConnectionClosed', { peerId });
157
+
149
158
  this._connectedPeers.delete(peerId);
150
159
 
151
160
  for (const perCollectionState of this._perCollectionStates.values()) {
@@ -171,8 +180,20 @@ export class CollectionSynchronizer extends Resource {
171
180
  log('onRemoteStateReceived', { collectionId, peerId, state });
172
181
  validateCollectionState(state);
173
182
  const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
174
- const existingState = perCollectionState.remoteStates.get(peerId) ?? { documents: {} };
175
- const diff = diffCollectionState(existingState, state);
183
+ perCollectionState.remoteStates.set(peerId, state);
184
+ this._diffCollectionState(collectionId, peerId);
185
+ }
186
+
187
+ private _diffCollectionState(collectionId: string, peerId: PeerId) {
188
+ const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
189
+ const remoteState = perCollectionState.remoteStates.get(peerId);
190
+ if (!remoteState) {
191
+ return;
192
+ }
193
+
194
+ log('diffCollectionState', { collectionId, peerId });
195
+ const localState = perCollectionState.localState ?? { documents: {} };
196
+ const diff = diffCollectionState(localState, remoteState);
176
197
  const spanId = getSpanName(peerId);
177
198
  if (diff.different.length === 0) {
178
199
  trace.spanEnd(spanId);
@@ -186,9 +207,20 @@ export class CollectionSynchronizer extends Resource {
186
207
  attributes: { peerId },
187
208
  });
188
209
  }
189
- if (diff.missingOnLocal.length > 0 || diff.different.length > 0) {
190
- perCollectionState.remoteStates.set(peerId, state);
191
- this.remoteStateUpdated.emit({ peerId, collectionId, newDocsAppeared: diff.missingOnLocal.length > 0 });
210
+ log('diff', {
211
+ localState: localState.documents,
212
+ remoteState: remoteState.documents,
213
+ missingOnLocal: diff.missingOnLocal,
214
+ missingOnRemote: diff.missingOnRemote,
215
+ different: diff.different,
216
+ });
217
+ if (diff.missingOnLocal.length > 0 || diff.different.length > 0 || diff.missingOnRemote.length > 0) {
218
+ log('emit remote state update');
219
+ this.remoteStateUpdated.emit({
220
+ peerId,
221
+ collectionId,
222
+ newDocsAppeared: diff.missingOnLocal.length > 0,
223
+ });
192
224
  }
193
225
  }
194
226
 
@@ -223,7 +255,7 @@ export type CollectionState = {
223
255
  /**
224
256
  * DocumentId -> Heads.
225
257
  */
226
- documents: Record<string, string[]>;
258
+ documents: Record<DocumentId, Heads>;
227
259
  };
228
260
 
229
261
  export type CollectionStateDiff = {
@@ -233,18 +265,21 @@ export type CollectionStateDiff = {
233
265
  };
234
266
 
235
267
  export const diffCollectionState = (local: CollectionState, remote: CollectionState): CollectionStateDiff => {
236
- const allDocuments = new Set<DocumentId>([...Object.keys(local.documents), ...Object.keys(remote.documents)] as any);
268
+ const localDocuments = Record.filter(local.documents, (heads) => heads.length > 0);
269
+ const remoteDocuments = Record.filter(remote.documents, (heads) => heads.length > 0);
270
+ // NOTE: Using `Array.union` is slow.
271
+ const allDocuments = [...new Set([...Record.keys(localDocuments), ...Record.keys(remoteDocuments)])] as DocumentId[];
237
272
 
238
273
  const missingOnRemote: DocumentId[] = [];
239
274
  const missingOnLocal: DocumentId[] = [];
240
275
  const different: DocumentId[] = [];
241
276
  for (const documentId of allDocuments) {
242
- if (!local.documents[documentId]) {
243
- missingOnLocal.push(documentId as DocumentId);
244
- } else if (!remote.documents[documentId]) {
245
- missingOnRemote.push(documentId as DocumentId);
246
- } else if (!am.equals(local.documents[documentId], remote.documents[documentId])) {
247
- different.push(documentId as DocumentId);
277
+ if (!localDocuments[documentId]) {
278
+ missingOnLocal.push(documentId);
279
+ } else if (!remoteDocuments[documentId]) {
280
+ missingOnRemote.push(documentId);
281
+ } else if (!A.equals(local.documents[documentId], remote.documents[documentId])) {
282
+ different.push(documentId);
248
283
  }
249
284
  }
250
285
 
@@ -5,7 +5,7 @@
5
5
  import { type Message } from '@automerge/automerge-repo';
6
6
 
7
7
  import { type TimeAware, trace } from '@dxos/tracing';
8
- import { CircularBuffer, mapValues, SlidingWindowSummary, type SlidingWindowSummaryConfig } from '@dxos/util';
8
+ import { CircularBuffer, SlidingWindowSummary, type SlidingWindowSummaryConfig, mapValues } from '@dxos/util';
9
9
 
10
10
  import { type NetworkDataMonitor } from './echo-network-adapter';
11
11
  import { type StorageAdapterDataMonitor } from './leveldb-storage-adapter';
@@ -2,10 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { cbor, type PeerId } from '@automerge/automerge-repo';
6
- import { onTestFinished, describe, expect, test } from 'vitest';
5
+ import { type PeerId, cbor } from '@automerge/automerge-repo';
6
+ import { describe, expect, onTestFinished, test } from 'vitest';
7
7
 
8
- import { sleep, Trigger, waitForCondition } from '@dxos/async';
8
+ import { Trigger, sleep, waitForCondition } from '@dxos/async';
9
9
  import { invariant } from '@dxos/invariant';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { type SyncMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';