@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc

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 (221) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/{browser/chunk-WJJXJTNS.mjs → neutral/chunk-46DCRDA7.mjs} +44 -13
  4. package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
  5. package/dist/lib/{browser/chunk-FR6GJ4S4.mjs → neutral/chunk-LR7UFVQX.mjs} +145 -618
  6. package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
  7. package/dist/lib/{browser → neutral}/filter/index.mjs +1 -2
  8. package/dist/lib/neutral/index.mjs +7332 -0
  9. package/dist/lib/neutral/index.mjs.map +7 -0
  10. package/dist/lib/neutral/meta.json +1 -0
  11. package/dist/lib/{browser → neutral}/testing/index.mjs +49 -292
  12. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  13. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  14. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  15. package/dist/types/src/automerge/automerge-host.d.ts +105 -27
  16. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  17. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  18. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  19. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  20. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  21. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  22. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  23. package/dist/types/src/automerge/collection-synchronizer.d.ts +22 -2
  24. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  25. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-network-adapter.d.ts +17 -11
  27. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  28. package/dist/types/src/automerge/echo-replicator.d.ts +28 -16
  29. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  30. package/dist/types/src/automerge/errors.d.ts +35 -0
  31. package/dist/types/src/automerge/errors.d.ts.map +1 -0
  32. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  33. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  34. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  35. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  36. package/dist/types/src/automerge/index.d.ts +2 -0
  37. package/dist/types/src/automerge/index.d.ts.map +1 -1
  38. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +10 -4
  39. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  40. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +8 -8
  41. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  42. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  43. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  44. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  45. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  46. package/dist/types/src/common/feeds.d.ts.map +1 -1
  47. package/dist/types/src/common/space-id.d.ts.map +1 -1
  48. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  49. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  50. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  51. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  52. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  53. package/dist/types/src/db-host/data-service.d.ts +12 -6
  54. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  55. package/dist/types/src/db-host/database-root.d.ts +9 -3
  56. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  57. package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
  58. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  59. package/dist/types/src/db-host/echo-host.d.ts +74 -28
  60. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  61. package/dist/types/src/db-host/index.d.ts +2 -0
  62. package/dist/types/src/db-host/index.d.ts.map +1 -1
  63. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  64. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  65. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  66. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  67. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  68. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  69. package/dist/types/src/db-host/query-service.d.ts +18 -10
  70. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  71. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  72. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  73. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  74. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  75. package/dist/types/src/db-host/space-state-manager.d.ts +3 -2
  76. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  77. package/dist/types/src/db-host/stub.d.ts +11 -0
  78. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  79. package/dist/types/src/edge/echo-edge-replicator.d.ts +7 -6
  80. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  81. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  82. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  83. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  84. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  85. package/dist/types/src/edge/index.d.ts +1 -0
  86. package/dist/types/src/edge/index.d.ts.map +1 -1
  87. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  88. package/dist/types/src/filter/filter-match.d.ts +1 -1
  89. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  90. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  91. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  92. package/dist/types/src/pipeline/pipeline.d.ts +2 -2
  93. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  94. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  95. package/dist/types/src/query/errors.d.ts +16 -20
  96. package/dist/types/src/query/errors.d.ts.map +1 -1
  97. package/dist/types/src/query/plan.d.ts +59 -8
  98. package/dist/types/src/query/plan.d.ts.map +1 -1
  99. package/dist/types/src/query/query-executor.d.ts +55 -12
  100. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  101. package/dist/types/src/query/query-planner.d.ts +10 -1
  102. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  103. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  104. package/dist/types/src/space/auth.d.ts +3 -3
  105. package/dist/types/src/space/auth.d.ts.map +1 -1
  106. package/dist/types/src/space/control-pipeline.d.ts +4 -3
  107. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  108. package/dist/types/src/space/space-manager.d.ts +7 -7
  109. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  110. package/dist/types/src/space/space-protocol.d.ts +9 -8
  111. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  112. package/dist/types/src/space/space.d.ts +5 -5
  113. package/dist/types/src/space/space.d.ts.map +1 -1
  114. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  115. package/dist/types/src/testing/index.d.ts +0 -2
  116. package/dist/types/src/testing/index.d.ts.map +1 -1
  117. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  118. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  119. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  120. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  121. package/dist/types/src/testing/test-replicator.d.ts +13 -13
  122. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  123. package/dist/types/src/util.d.ts.map +1 -1
  124. package/dist/types/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +56 -54
  126. package/src/automerge/automerge-host-subduction.test.ts +263 -0
  127. package/src/automerge/automerge-host.test.ts +145 -32
  128. package/src/automerge/automerge-host.ts +442 -163
  129. package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
  130. package/src/automerge/automerge-repo.test.ts +284 -77
  131. package/src/automerge/automerge-subduction.test.ts +330 -0
  132. package/src/automerge/collection-synchronizer.test.ts +96 -16
  133. package/src/automerge/collection-synchronizer.ts +125 -26
  134. package/src/automerge/echo-network-adapter.test.ts +2 -1
  135. package/src/automerge/echo-network-adapter.ts +44 -32
  136. package/src/automerge/echo-replicator.ts +30 -16
  137. package/src/automerge/errors.ts +10 -0
  138. package/src/automerge/handle-state.ts +62 -0
  139. package/src/automerge/heads-store.ts +42 -3
  140. package/src/automerge/index.ts +2 -0
  141. package/src/automerge/leveldb-storage-adapter.ts +70 -4
  142. package/src/automerge/mesh-echo-replicator-connection.ts +8 -8
  143. package/src/automerge/mesh-echo-replicator.ts +14 -11
  144. package/src/db-host/automerge-data-source.test.ts +299 -0
  145. package/src/db-host/automerge-data-source.ts +187 -0
  146. package/src/db-host/data-service.ts +27 -10
  147. package/src/db-host/database-root.ts +19 -7
  148. package/src/db-host/documents-synchronizer.test.ts +145 -14
  149. package/src/db-host/documents-synchronizer.ts +158 -59
  150. package/src/db-host/echo-host.ts +376 -90
  151. package/src/db-host/index.ts +2 -0
  152. package/src/db-host/invalidation-hint.ts +50 -0
  153. package/src/db-host/local-queue-service.ts +127 -0
  154. package/src/db-host/query-invalidation.test.ts +270 -0
  155. package/src/db-host/query-service.ts +108 -111
  156. package/src/db-host/queue-data-source.ts +160 -0
  157. package/src/db-host/queue-service.test.ts +145 -0
  158. package/src/db-host/space-state-manager.ts +11 -9
  159. package/src/db-host/stub.ts +33 -0
  160. package/src/edge/echo-edge-replicator.test.ts +9 -9
  161. package/src/edge/echo-edge-replicator.ts +50 -42
  162. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  163. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  164. package/src/edge/index.ts +1 -0
  165. package/src/filter/filter-match.test.ts +41 -25
  166. package/src/filter/filter-match.ts +75 -20
  167. package/src/metadata/metadata-store.ts +8 -5
  168. package/src/pipeline/pipeline-stress.test.ts +1 -3
  169. package/src/pipeline/pipeline.test.ts +0 -1
  170. package/src/pipeline/pipeline.ts +2 -3
  171. package/src/query/errors.ts +2 -2
  172. package/src/query/plan.ts +73 -9
  173. package/src/query/query-executor.ts +1064 -172
  174. package/src/query/query-planner.test.ts +854 -86
  175. package/src/query/query-planner.ts +444 -38
  176. package/src/space/admission-discovery-extension.ts +1 -1
  177. package/src/space/auth.ts +6 -6
  178. package/src/space/control-pipeline.test.ts +2 -2
  179. package/src/space/control-pipeline.ts +7 -7
  180. package/src/space/space-manager.ts +12 -17
  181. package/src/space/space-protocol.browser.test.ts +13 -12
  182. package/src/space/space-protocol.test.ts +13 -13
  183. package/src/space/space-protocol.ts +16 -15
  184. package/src/space/space.test.ts +2 -2
  185. package/src/space/space.ts +9 -10
  186. package/src/testing/index.ts +0 -2
  187. package/src/testing/test-agent-builder.ts +0 -1
  188. package/src/testing/test-network-adapter.ts +24 -3
  189. package/src/testing/test-replicator.ts +15 -20
  190. package/src/util.ts +7 -3
  191. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  192. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-FR6GJ4S4.mjs.map +0 -7
  194. package/dist/lib/browser/chunk-WJJXJTNS.mjs.map +0 -7
  195. package/dist/lib/browser/index.mjs +0 -5492
  196. package/dist/lib/browser/index.mjs.map +0 -7
  197. package/dist/lib/browser/meta.json +0 -1
  198. package/dist/lib/browser/testing/index.mjs.map +0 -7
  199. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  200. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  201. package/dist/lib/node-esm/chunk-JQCCVDLT.mjs +0 -2144
  202. package/dist/lib/node-esm/chunk-JQCCVDLT.mjs.map +0 -7
  203. package/dist/lib/node-esm/chunk-LEQ77KAT.mjs +0 -240
  204. package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
  205. package/dist/lib/node-esm/filter/index.mjs +0 -13
  206. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  207. package/dist/lib/node-esm/index.mjs +0 -5492
  208. package/dist/lib/node-esm/index.mjs.map +0 -7
  209. package/dist/lib/node-esm/meta.json +0 -1
  210. package/dist/lib/node-esm/testing/index.mjs +0 -764
  211. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  212. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  213. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  214. package/dist/types/src/testing/test-data.d.ts +0 -18
  215. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  216. package/dist/types/src/testing/test-schema.d.ts +0 -37
  217. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  218. package/src/db-host/documents-iterator.ts +0 -74
  219. package/src/testing/test-data.ts +0 -127
  220. package/src/testing/test-schema.ts +0 -53
  221. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -18,17 +18,20 @@ import {
18
18
  type AutomergeUrl,
19
19
  type DocHandle,
20
20
  type DocumentId,
21
- type HandleState,
21
+ type DocumentProgress,
22
+ type Message,
22
23
  type PeerId,
23
24
  Repo,
24
25
  type SharePolicy,
25
26
  type StorageAdapterInterface,
26
27
  generateAutomergeUrl,
28
+ initSubduction,
27
29
  parseAutomergeUrl,
28
30
  } from '@automerge/automerge-repo';
29
- import { describe, expect, onTestFinished, test } from 'vitest';
31
+ import { beforeAll, describe, expect, onTestFinished, test } from 'vitest';
30
32
 
31
- import { asyncTimeout, sleep } from '@dxos/async';
33
+ import { Trigger, asyncTimeout, sleep } from '@dxos/async';
34
+ import { Context } from '@dxos/context';
32
35
  import { randomBytes } from '@dxos/crypto';
33
36
  import { PublicKey } from '@dxos/keys';
34
37
  import { createTestLevel } from '@dxos/kv-store/testing';
@@ -37,15 +40,68 @@ import { openAndClose } from '@dxos/test-utils';
37
40
  import { isNonNullable, range } from '@dxos/util';
38
41
 
39
42
  import { TestAdapter, type TestConnectionStateProvider } from '../testing';
40
-
41
- import { FIND_PARAMS } from './automerge-host';
42
43
  import { EchoNetworkAdapter } from './echo-network-adapter';
44
+ import { type HandleQueryState } from './handle-state';
43
45
  import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
44
46
  import { MeshEchoReplicator } from './mesh-echo-replicator';
45
47
 
46
48
  const HOST_AND_CLIENT: [string, string] = ['host', 'client'];
47
49
 
50
+ /**
51
+ * Block until the {@link DocumentProgress} reports a state in `awaitStates`.
52
+ */
53
+ const waitForQueryState = async (
54
+ progress: DocumentProgress<unknown>,
55
+ awaitStates: readonly HandleQueryState[],
56
+ { timeout }: { timeout?: number } = {},
57
+ ): Promise<void> => {
58
+ if (awaitStates.includes(progress.peek().state)) {
59
+ return;
60
+ }
61
+ const trigger = new Trigger();
62
+ const unsubscribe = progress.subscribe((state) => {
63
+ if (awaitStates.includes(state.state)) {
64
+ trigger.wake();
65
+ } else if (state.state === 'failed' && !awaitStates.includes('failed')) {
66
+ trigger.throw(state.error);
67
+ }
68
+ });
69
+ try {
70
+ await trigger.wait({ timeout });
71
+ } finally {
72
+ unsubscribe();
73
+ }
74
+ };
75
+
76
+ /**
77
+ * Multi-state `Repo.find` replacement for the pre-subduction
78
+ * `Repo.find(url, { allowableStates })` ergonomics. */
79
+ const findInStates = async <T>(
80
+ repo: Repo,
81
+ url: AutomergeUrl,
82
+ awaitStates: readonly HandleQueryState[],
83
+ ): Promise<DocHandle<T>> => {
84
+ const { documentId } = parseAutomergeUrl(url);
85
+ const progress = repo.findWithProgress<T>(url);
86
+ await waitForQueryState(progress, awaitStates);
87
+ return repo.handles[documentId] as DocHandle<T>;
88
+ };
89
+
90
+ /**
91
+ * Default state set for `findInStates`. Equivalent to the pre-subduction
92
+ * `FIND_PARAMS = { allowableStates: ['ready', 'requesting'] }` — i.e. "give
93
+ * me the handle as soon as we have it OR are still trying to fetch it".
94
+ * `'loading'` is the {@link QueryState} equivalent of the old `'requesting'`.
95
+ */
96
+ const FIND_STATES: readonly HandleQueryState[] = ['ready', 'loading'];
97
+
48
98
  describe('AutomergeRepo', () => {
99
+ // Subduction-fork `Repo` constructs a `MemorySigner` internally; WASM must be
100
+ // initialized first or the constructor throws on `memorysigner_new`.
101
+ beforeAll(async () => {
102
+ await initSubduction();
103
+ });
104
+
49
105
  test('change events', () => {
50
106
  const repo = new Repo({ network: [] });
51
107
  const handle = repo.create<{ field?: string }>();
@@ -112,13 +168,14 @@ describe('AutomergeRepo', () => {
112
168
  expect(equals(a, c)).to.be.true;
113
169
  });
114
170
 
115
- test('documents missing from local storage go to requesting state', async () => {
171
+ test('documents missing from local storage go to loading state', async () => {
116
172
  const { repos, adapters } = await createHostClientRepoTopology();
117
173
  const [host] = repos;
118
174
  await connectAdapters(adapters);
119
175
  const url = 'automerge:3JN8F3Z4dUWEEKKFN7WE9gEGvVUT';
120
- const handle = await host.find(url as AutomergeUrl, FIND_PARAMS);
121
- await handle.whenReady(['requesting']);
176
+ // `'loading'` is the equivalent of the legacy `'requesting'` handle state.
177
+ const progress = host.findWithProgress(url as AutomergeUrl);
178
+ expect(progress.peek().state).to.equal('loading');
122
179
  });
123
180
 
124
181
  test('documents on disk go to ready state', async () => {
@@ -162,32 +219,6 @@ describe('AutomergeRepo', () => {
162
219
  expect((await asyncTimeout(client.find<any>(handle.url), 1000))!.doc().text).to.equal(text);
163
220
  });
164
221
 
165
- test('share policy gets enabled afterwards', async () => {
166
- let sharePolicy = false;
167
-
168
- const { repos, adapters } = await createHostClientRepoTopology({
169
- sharePolicy: async () => sharePolicy,
170
- });
171
- const [host, client] = repos;
172
- await connectAdapters(adapters);
173
-
174
- const handle = host.create();
175
- const text = 'Hello world';
176
- handle.change((doc: any) => {
177
- doc.text = text;
178
- });
179
-
180
- {
181
- await client.find(handle.url, { allowableStates: ['unavailable'] });
182
- }
183
-
184
- sharePolicy = true;
185
-
186
- {
187
- await client.find(handle.url, { allowableStates: ['unavailable'] });
188
- }
189
- });
190
-
191
222
  test('two documents and share policy switching', async () => {
192
223
  const allowedDocs: DocumentId[] = [];
193
224
 
@@ -237,15 +268,15 @@ describe('AutomergeRepo', () => {
237
268
  await connectAdapters(adapters);
238
269
 
239
270
  const handle = host.create();
240
- const docOnClient = await client.find<any>(handle.url, FIND_PARAMS);
271
+ const docOnClient = await findInStates<any>(client, handle.url, FIND_STATES);
241
272
  {
242
273
  const sanityText = 'Hello world';
243
274
  handle.change((doc: any) => {
244
275
  doc.sanityText = sanityText;
245
276
  });
246
- await asyncTimeout(docOnClient.whenReady(), 1000);
247
-
248
- expect(docOnClient.doc().sanityText).to.equal(sanityText);
277
+ // Subduction-fork: `whenReady()` resolves on the handle's `ready` state
278
+ // (no longer waits for new data). Poll for the actual change to propagate.
279
+ await expect.poll(() => docOnClient.doc()?.sanityText, { timeout: 1000 }).toEqual(sanityText);
249
280
  }
250
281
 
251
282
  // Disrupt connection.
@@ -271,9 +302,7 @@ describe('AutomergeRepo', () => {
271
302
  handle.change((doc: any) => {
272
303
  doc.onlineText = onlineText;
273
304
  });
274
- await sleep(100);
275
- await asyncTimeout(docOnClient.whenReady(), 1000);
276
- expect(docOnClient.doc().onlineText).to.equal(onlineText);
305
+ await expect.poll(() => docOnClient.doc()?.onlineText, { timeout: 1000 }).toEqual(onlineText);
277
306
  expect(docOnClient.doc().offlineText).to.equal(offlineText);
278
307
  }
279
308
  });
@@ -320,8 +349,8 @@ describe('AutomergeRepo', () => {
320
349
  doc.text = 'Hello world';
321
350
  });
322
351
 
323
- const _ = repoB.find(docA.url, FIND_PARAMS);
324
- const docC = await repoC.find(docA.url, FIND_PARAMS);
352
+ const _ = findInStates(repoB, docA.url, FIND_STATES);
353
+ const docC = await findInStates(repoC, docA.url, FIND_STATES);
325
354
  await asyncTimeout(docC.whenReady(), 1000);
326
355
  });
327
356
 
@@ -330,7 +359,6 @@ describe('AutomergeRepo', () => {
330
359
  const [host, client] = repos;
331
360
  const [[adapter1, adapter2]] = adapters;
332
361
 
333
- const unavailable: HandleState = 'unavailable';
334
362
  await connectAdapters(adapters, { noEmitPeerCandidate: true });
335
363
 
336
364
  const docA = host.create();
@@ -339,21 +367,21 @@ describe('AutomergeRepo', () => {
339
367
  doc.text = 'Hello world';
340
368
  });
341
369
 
342
- const docB = await client.find(docA.url, { allowableStates: ['unavailable'] });
343
-
344
- // Request document from repoB.
345
- await asyncTimeout(docB.whenReady([unavailable]), 1_000);
370
+ // Without peer candidates, the patched subduction source keeps the
371
+ // query pending (`'loading'`) instead of driving it to
372
+ // `'unavailable'` it will retry on the next connection-state
373
+ // change.
374
+ const progress = client.findWithProgress(docA.url);
375
+ await expect.poll(() => progress.peek().state, { timeout: 500, interval: 50 }).toEqual('loading');
346
376
 
347
377
  adapter1.peerCandidate(adapter2.peerId!);
348
378
  await sleep(100);
349
379
  adapter2.peerCandidate(adapter1.peerId!);
350
380
 
351
- // Wait becomes unavailable.
352
- await asyncTimeout(docB.whenReady([unavailable]), 1_000);
353
-
354
- // Wait gets loaded after reconnect.
381
+ // Reconnecting cycles the peers and re-triggers sync; the doc
382
+ // becomes available.
355
383
  await reconnectAdapters(adapters);
356
- await asyncTimeout(docB.whenReady(), 1_000);
384
+ await waitForQueryState(progress, ['ready'], { timeout: 1_000 });
357
385
  });
358
386
 
359
387
  test('documents loaded from disk get replicated', async () => {
@@ -386,9 +414,13 @@ describe('AutomergeRepo', () => {
386
414
  const repo = new Repo({ network: [] });
387
415
  const serverHandle = repo.create<{ field?: string }>();
388
416
 
389
- let clientDoc = A.from<{ field?: string }>({});
417
+ // The client must be bootstrapped via `A.load` on the first message
418
+ // (full save) — using `A.from({})` + `A.loadIncremental` panics
419
+ // automerge@3.2.5 with `PatchLogMismatch` when the donor history shares
420
+ // no lineage with the receiver's local actor.
421
+ let clientDoc: A.Doc<{ field?: string }> | undefined;
390
422
  const receiveByClient = (blob: Uint8Array) => {
391
- clientDoc = A.loadIncremental(clientDoc, blob);
423
+ clientDoc = clientDoc === undefined ? A.load(blob) : A.loadIncremental(clientDoc, blob);
392
424
  };
393
425
 
394
426
  // Sync handshake.
@@ -407,7 +439,7 @@ describe('AutomergeRepo', () => {
407
439
  serverHandle.change((doc: any) => {
408
440
  doc.field = value;
409
441
  });
410
- expect(clientDoc.field).to.deep.equal(value);
442
+ expect(clientDoc?.field).to.deep.equal(value);
411
443
  }
412
444
 
413
445
  {
@@ -415,7 +447,7 @@ describe('AutomergeRepo', () => {
415
447
  serverHandle.change((doc: any) => {
416
448
  doc.field = value;
417
449
  });
418
- expect(clientDoc.field).to.deep.equal(value);
450
+ expect(clientDoc?.field).to.deep.equal(value);
419
451
  }
420
452
  });
421
453
 
@@ -510,6 +542,149 @@ describe('AutomergeRepo', () => {
510
542
  await asyncTimeout(handleB.whenReady(), 1000);
511
543
  expect(handleB.doc()!.text).to.equal(text);
512
544
  });
545
+
546
+ test('retry share config', async () => {
547
+ let announce = false;
548
+
549
+ const { repos, adapters } = await createRepoTopology({
550
+ peers: ['A', 'B'],
551
+ connections: [['A', 'B']],
552
+ options: {
553
+ shareConfig: {
554
+ access: async () => true,
555
+ announce: async () => announce,
556
+ },
557
+ },
558
+ });
559
+ const [repoA, repoB] = repos;
560
+ await connectAdapters(adapters);
561
+
562
+ const docA = repoA.create({ text: 'Hello world' });
563
+
564
+ // TODO(mykola): The classical-network `DocSynchronizer` doesn't drive
565
+ // the query to `'unavailable'` while the dormant subduction source is
566
+ // still attached — the query stays in `'loading'` until something
567
+ // actually delivers data. Once the fork drives source state
568
+ // correctly, switch this to a positive `'unavailable'` assertion.
569
+ // For now we assert "stuck in loading" + empty doc body.
570
+ const progress = repoB.findWithProgress<{ text: string }>(docA.url);
571
+ await expect.poll(() => progress.peek().state, { timeout: 500, interval: 50 }).toEqual('loading');
572
+ const docB = repoB.handles[parseAutomergeUrl(docA.url).documentId] as DocHandle<{ text: string }>;
573
+ expect(docB.doc()).to.deep.equal({});
574
+
575
+ // Flip announce on and kick the share-config retry; the query
576
+ // transitions to `'ready'` once the doc syncs through.
577
+ announce = true;
578
+ repoB.shareConfigChanged();
579
+ // Use `waitForQueryState` instead of `progress.whenReady()` so that a
580
+ // transient `'unavailable'` from the dormant subduction source
581
+ // doesn't reject the wait before classical sync delivers.
582
+ await waitForQueryState(progress, ['ready'], { timeout: 1_000 });
583
+ expect(progress.peek().state).to.equal('ready');
584
+ expect(docB.doc()).to.deep.equal({ text: 'Hello world' });
585
+ });
586
+ });
587
+
588
+ describe('create2 tests', () => {
589
+ test('basic document creation', async () => {
590
+ const repo = new Repo({ network: [] });
591
+
592
+ const handle = await repo.create2<{ text: string }>();
593
+ expect(handle.doc()).to.deep.equal({});
594
+
595
+ handle.change((doc: any) => {
596
+ doc.text = 'Hello World';
597
+ });
598
+
599
+ expect(handle.doc()?.text).to.equal('Hello World');
600
+ });
601
+
602
+ test('document mutation', async () => {
603
+ const repo = new Repo({ network: [] });
604
+
605
+ const handle = await repo.create2<{ counter: number }>({ counter: 0 });
606
+
607
+ for (let i = 1; i <= 10; i++) {
608
+ handle.change((doc: any) => {
609
+ doc.counter = i;
610
+ });
611
+ }
612
+
613
+ expect(handle.doc()?.counter).to.equal(10);
614
+ });
615
+
616
+ test('import document and mutate', async () => {
617
+ const repo = new Repo({ network: [] });
618
+
619
+ const original = await repo.create2<{ text: string }>({ text: 'original' });
620
+ const blob = A.save(original.doc()!);
621
+
622
+ const imported = repo.import<{ text: string }>(blob);
623
+ expect(imported.doc()?.text).to.equal('original');
624
+
625
+ imported.change((doc: any) => {
626
+ doc.text = 'mutated';
627
+ });
628
+ expect(imported.doc()?.text).to.equal('mutated');
629
+ });
630
+
631
+ test('reload document with flush', async () => {
632
+ const path = createTmpPath();
633
+ const text = 'Hello World!';
634
+ let url: AutomergeUrl;
635
+
636
+ {
637
+ const level = createTestLevel(path);
638
+ const storage = await createLevelAdapter(level);
639
+ const repo = new Repo({ network: [], storage });
640
+ const handle = await repo.create2<{ text: string }>();
641
+ url = handle.url;
642
+ handle.change((doc: any) => {
643
+ doc.text = text;
644
+ });
645
+ await repo.flush([handle.documentId]);
646
+ await level.close();
647
+ }
648
+
649
+ {
650
+ const level = createTestLevel(path);
651
+ const storage = await createLevelAdapter(level);
652
+ const repo = new Repo({ network: [], storage });
653
+ const handle = await repo.find<{ text: string }>(url);
654
+ await handle.whenReady();
655
+ expect(handle.doc()?.text).to.equal(text);
656
+ await level.close();
657
+ }
658
+ });
659
+
660
+ test('reload document without flush', async () => {
661
+ const path = createTmpPath();
662
+ const text = 'Hello World!';
663
+ let url: AutomergeUrl;
664
+
665
+ {
666
+ const level = createTestLevel(path);
667
+ const storage = await createLevelAdapter(level);
668
+ const repo = new Repo({ network: [], storage });
669
+ const handle = await repo.create2<{ text: string }>();
670
+ url = handle.url;
671
+ handle.change((doc: any) => {
672
+ doc.text = text;
673
+ });
674
+ // No explicit flush - rely on auto-save.
675
+ await sleep(200);
676
+ await level.close();
677
+ }
678
+
679
+ {
680
+ const level = createTestLevel(path);
681
+ const storage = await createLevelAdapter(level);
682
+ const repo = new Repo({ network: [], storage });
683
+ const handle = await repo.find<{ text: string }>(url);
684
+ await handle.whenReady();
685
+ expect(handle.doc()?.text).to.equal(text);
686
+ }
687
+ });
513
688
  });
514
689
 
515
690
  describe('teleport', () => {
@@ -596,11 +771,25 @@ describe('AutomergeRepo', () => {
596
771
  });
597
772
  await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
598
773
 
599
- const shouldNotFindDoc = await peer2.repo.find(docNotInRemoteCollection.url, FIND_PARAMS);
600
- const shouldFindDoc = await peer2.repo.find(docInRemoteCollection.url, FIND_PARAMS);
601
- await shouldFindDoc.whenReady();
774
+ const shouldNotFindProgress = peer2.repo.findWithProgress(docNotInRemoteCollection.url);
775
+ // The doc in the remote collection should sync. Can't use
776
+ // `repo.find()` / `progress.whenReady()` here: the dormant
777
+ // subduction source transitions to `'unavailable'` mid-flight
778
+ // (before classical-network sync delivers the doc), which both APIs
779
+ // surface as a rejection. `findInStates` ignores transient
780
+ // `'unavailable'` and only rejects on `'failed'`.
781
+ const shouldFindDoc = await findInStates<any>(peer2.repo, docInRemoteCollection.url, ['ready']);
782
+ expect(shouldFindDoc.doc()).to.deep.eq(docInRemoteCollection.doc());
602
783
  expect(shouldFindDoc.doc()).to.deep.eq(docInRemoteCollection.doc());
603
- await asyncTimeout(shouldNotFindDoc.whenReady(['unavailable']), 1000);
784
+ // The doc that's NOT in any remote collection must remain unsynced.
785
+ // TODO(mykola): Fork's classical-network sync doesn't transition the
786
+ // query to `'unavailable'` while a dormant subduction source is
787
+ // attached, so we can only assert "stays in loading" rather than
788
+ // "reaches unavailable". Re-enable a positive `'unavailable'`
789
+ // assertion when the fork drives the source state correctly.
790
+ await expect.poll(() => shouldNotFindProgress.peek().state, { timeout: 500, interval: 50 }).toEqual('loading');
791
+ const shouldNotFindDoc = peer2.repo.handles[parseAutomergeUrl(docNotInRemoteCollection.url).documentId];
792
+ expect(shouldNotFindDoc.doc()).to.deep.equal({});
604
793
  });
605
794
 
606
795
  /**
@@ -630,12 +819,23 @@ describe('AutomergeRepo', () => {
630
819
  await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
631
820
  await connectPeers(anotherSpaceKey, teleportBuilder, peer2, peerFromAnotherSpace);
632
821
 
633
- const loadedDocument = await peer2.repo.find(document.url, FIND_PARAMS);
634
- await asyncTimeout(loadedDocument.whenReady(), 1000);
635
-
636
- await sleep(200);
637
- const doc = await peerFromAnotherSpace.repo.find(document.url, { allowableStates: ['unavailable'] });
638
- await asyncTimeout(doc.whenReady(['unavailable']), 1000);
822
+ // Can't use `repo.find()` / `progress.whenReady()`: the dormant
823
+ // subduction source can transition to `'unavailable'` mid-flight
824
+ // (before classical sync delivers), which both APIs surface as a
825
+ // rejection. `findInStates` ignores transient `'unavailable'`.
826
+ const loaded = await findInStates<any>(peer2.repo, document.url, ['ready']);
827
+ expect(loaded.doc()).to.deep.equal(document.doc());
828
+
829
+ // peerFromAnotherSpace is connected to peer2 over a different spaceKey,
830
+ // so it must NOT receive the doc.
831
+ // TODO(mykola): Fork's classical-network query doesn't reach
832
+ // `'unavailable'`; assert "stays in loading" instead. Switch to a
833
+ // positive `'unavailable'` assertion once the fork drives source
834
+ // state correctly.
835
+ const otherSpaceProgress = peerFromAnotherSpace.repo.findWithProgress(document.url);
836
+ await expect.poll(() => otherSpaceProgress.peek().state, { timeout: 500, interval: 50 }).toEqual('loading');
837
+ const otherSpaceDoc = peerFromAnotherSpace.repo.handles[parseAutomergeUrl(document.url).documentId];
838
+ expect(otherSpaceDoc.doc()).to.deep.equal({});
639
839
  });
640
840
 
641
841
  test('collection-sync with chain of peers', async () => {
@@ -664,12 +864,11 @@ describe('AutomergeRepo', () => {
664
864
  await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
665
865
  await connectPeers(spaceKey, teleportBuilder, peer2, peer3);
666
866
 
667
- const loadedDocument = await peer2.repo.find(document.url, FIND_PARAMS);
668
- await loadedDocument.whenReady();
867
+ const loadedDocument = await findInStates(peer2.repo, document.url, FIND_STATES);
868
+ await expect.poll(() => loadedDocument.doc(), { timeout: 1000 }).toEqual(document.doc());
669
869
 
670
- const doc = await peer3.repo.find(document.url, FIND_PARAMS);
671
- await doc.whenReady();
672
- expect(doc.doc()).to.deep.eq(document.doc());
870
+ const doc = await findInStates(peer3.repo, document.url, FIND_STATES);
871
+ await expect.poll(() => doc.doc(), { timeout: 1000 }).toEqual(document.doc());
673
872
  });
674
873
 
675
874
  test('document is passively replicated to connected peers', async () => {
@@ -730,8 +929,7 @@ describe('AutomergeRepo', () => {
730
929
  });
731
930
  });
732
931
 
733
- const createLevelAdapter = async () => {
734
- const level = createTestLevel();
932
+ const createLevelAdapter = async (level = createTestLevel()) => {
735
933
  const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
736
934
  await openAndClose(level, storage);
737
935
  return storage;
@@ -753,19 +951,23 @@ describe('AutomergeRepo', () => {
753
951
  };
754
952
  });
755
953
 
954
+ type ShareConfig = Exclude<ConstructorParameters<typeof Repo>[0], undefined>['shareConfig'];
955
+
756
956
  type ConnectedRepoOptions = {
757
957
  storages?: StorageAdapterInterface[];
758
958
  connectionStateProvider?: TestConnectionStateProvider;
759
959
  sharePolicy?: SharePolicy;
960
+ shareConfig?: ShareConfig;
760
961
  };
761
962
 
762
963
  const createRepoTopology = async <Peers extends string[], Peer extends string = Peers[number]>(args: {
763
964
  peers: Peers;
764
965
  connections: [Peer, Peer][];
765
966
  options?: ConnectedRepoOptions;
967
+ onMessage?: (message: Message) => void;
766
968
  }) => {
767
969
  const adapters = args.connections.map(
768
- () => TestAdapter.createPair(args.options?.connectionStateProvider) as [TestAdapter, TestAdapter],
970
+ () => TestAdapter.createPair(args.options?.connectionStateProvider, args.onMessage) as [TestAdapter, TestAdapter],
769
971
  );
770
972
  const repos = args.peers.map((peerId, peerIndex) => {
771
973
  const network = adapters
@@ -781,7 +983,8 @@ const createRepoTopology = async <Peers extends string[], Peer extends string =
781
983
  peerId: peerId as PeerId,
782
984
  storage: args.options?.storages?.[peerIndex],
783
985
  network,
784
- sharePolicy: args.options?.sharePolicy ?? (async () => true),
986
+ sharePolicy: (args.options?.sharePolicy ?? args.options?.shareConfig) ? undefined : async () => true,
987
+ shareConfig: args.options?.shareConfig,
785
988
  });
786
989
  });
787
990
  return { repos, adapters };
@@ -848,7 +1051,7 @@ const createTeleportTestPeer = async (
848
1051
  });
849
1052
  await echoAdapter.open();
850
1053
  await echoAdapter.whenConnected();
851
- await echoAdapter.addReplicator(meshAdapter);
1054
+ await echoAdapter.addReplicator(Context.default(), meshAdapter);
852
1055
  const [teleport] = teleportBuilder.createPeers({ factory: () => new TeleportPeer() });
853
1056
  return { repo, meshAdapter, teleport };
854
1057
  };
@@ -867,3 +1070,7 @@ const connectPeers = async (
867
1070
  };
868
1071
 
869
1072
  type TeleportTestPeer = { repo: Repo; meshAdapter: MeshEchoReplicator; teleport: TeleportPeer };
1073
+
1074
+ export const createTmpPath = (): string => {
1075
+ return `/tmp/dxos-${PublicKey.random().toHex()}`;
1076
+ };