@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1c7ec43d41

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 (239) hide show
  1. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-VIHYDCCC.mjs} +313 -709
  2. package/dist/lib/neutral/chunk-VIHYDCCC.mjs.map +7 -0
  3. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs +247 -0
  4. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/neutral/index.mjs +7065 -0
  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 +90 -311
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  12. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  13. package/dist/types/src/automerge/automerge-host.d.ts +99 -54
  14. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  15. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  16. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  17. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  18. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  19. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  20. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  21. package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
  22. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  23. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  24. package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
  25. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-replicator.d.ts +38 -15
  27. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  28. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  29. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  30. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  31. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  32. package/dist/types/src/automerge/index.d.ts +2 -1
  33. package/dist/types/src/automerge/index.d.ts.map +1 -1
  34. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +9 -5
  35. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  36. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  37. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  38. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  39. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  40. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  41. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  42. package/dist/types/src/common/codec.d.ts +1 -1
  43. package/dist/types/src/common/codec.d.ts.map +1 -1
  44. package/dist/types/src/common/feeds.d.ts.map +1 -1
  45. package/dist/types/src/common/space-id.d.ts.map +1 -1
  46. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  47. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  48. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  49. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  50. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  51. package/dist/types/src/db-host/data-service.d.ts +8 -7
  52. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  53. package/dist/types/src/db-host/database-root.d.ts +9 -3
  54. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  55. package/dist/types/src/db-host/documents-synchronizer.d.ts +9 -8
  56. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  57. package/dist/types/src/db-host/echo-host.d.ts +62 -27
  58. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  59. package/dist/types/src/db-host/index.d.ts +2 -0
  60. package/dist/types/src/db-host/index.d.ts.map +1 -1
  61. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  62. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  63. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  64. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  65. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  66. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  67. package/dist/types/src/db-host/query-service.d.ts +19 -11
  68. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  69. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  70. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  71. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  72. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  73. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  74. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  75. package/dist/types/src/db-host/stub.d.ts +11 -0
  76. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  77. package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
  78. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  79. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  80. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  81. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  82. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  83. package/dist/types/src/edge/index.d.ts +1 -0
  84. package/dist/types/src/edge/index.d.ts.map +1 -1
  85. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  86. package/dist/types/src/filter/filter-match.d.ts +4 -1
  87. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  88. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  89. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  90. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  91. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  92. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  93. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  94. package/dist/types/src/query/errors.d.ts +22 -10
  95. package/dist/types/src/query/errors.d.ts.map +1 -1
  96. package/dist/types/src/query/plan.d.ts +66 -8
  97. package/dist/types/src/query/plan.d.ts.map +1 -1
  98. package/dist/types/src/query/query-executor.d.ts +66 -11
  99. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  100. package/dist/types/src/query/query-planner.d.ts +12 -1
  101. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  102. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  103. package/dist/types/src/space/auth.d.ts +3 -3
  104. package/dist/types/src/space/auth.d.ts.map +1 -1
  105. package/dist/types/src/space/control-pipeline.d.ts +5 -4
  106. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  107. package/dist/types/src/space/space-manager.d.ts +8 -8
  108. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  109. package/dist/types/src/space/space-protocol.d.ts +10 -9
  110. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  111. package/dist/types/src/space/space.d.ts +6 -6
  112. package/dist/types/src/space/space.d.ts.map +1 -1
  113. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  114. package/dist/types/src/testing/index.d.ts +0 -2
  115. package/dist/types/src/testing/index.d.ts.map +1 -1
  116. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  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 +14 -13
  122. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  123. package/dist/types/src/util.d.ts +1 -1
  124. package/dist/types/src/util.d.ts.map +1 -1
  125. package/dist/types/tsconfig.tsbuildinfo +1 -1
  126. package/package.json +56 -54
  127. package/src/automerge/automerge-host-subduction.test.ts +247 -0
  128. package/src/automerge/automerge-host.test.ts +65 -39
  129. package/src/automerge/automerge-host.ts +589 -206
  130. package/src/automerge/automerge-repo-subduction.test.ts +1049 -0
  131. package/src/automerge/automerge-repo.test.ts +349 -91
  132. package/src/automerge/automerge-subduction.test.ts +330 -0
  133. package/src/automerge/collection-synchronizer.test.ts +53 -17
  134. package/src/automerge/collection-synchronizer.ts +119 -36
  135. package/src/automerge/echo-data-monitor.ts +1 -1
  136. package/src/automerge/echo-network-adapter.test.ts +5 -4
  137. package/src/automerge/echo-network-adapter.ts +74 -30
  138. package/src/automerge/echo-replicator.ts +41 -15
  139. package/src/automerge/handle-state.ts +62 -0
  140. package/src/automerge/heads-store.ts +42 -3
  141. package/src/automerge/index.ts +2 -1
  142. package/src/automerge/leveldb-storage-adapter.ts +40 -11
  143. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  144. package/src/automerge/mesh-echo-replicator.ts +15 -11
  145. package/src/automerge/storage-adapter.test.ts +1 -1
  146. package/src/common/space-id.ts +1 -1
  147. package/src/db-host/automerge-data-source.test.ts +299 -0
  148. package/src/db-host/automerge-data-source.ts +187 -0
  149. package/src/db-host/data-service.ts +25 -26
  150. package/src/db-host/database-root.ts +20 -8
  151. package/src/db-host/documents-synchronizer.test.ts +143 -14
  152. package/src/db-host/documents-synchronizer.ts +72 -55
  153. package/src/db-host/echo-host.ts +348 -96
  154. package/src/db-host/index.ts +2 -0
  155. package/src/db-host/invalidation-hint.ts +50 -0
  156. package/src/db-host/local-queue-service.ts +127 -0
  157. package/src/db-host/query-invalidation.test.ts +270 -0
  158. package/src/db-host/query-service.ts +111 -106
  159. package/src/db-host/queue-data-source.ts +160 -0
  160. package/src/db-host/queue-service.test.ts +145 -0
  161. package/src/db-host/space-state-manager.ts +12 -10
  162. package/src/db-host/stub.ts +33 -0
  163. package/src/edge/echo-edge-replicator.test.ts +12 -10
  164. package/src/edge/echo-edge-replicator.ts +144 -58
  165. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  166. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  167. package/src/edge/index.ts +1 -0
  168. package/src/filter/filter-match.test.ts +68 -32
  169. package/src/filter/filter-match.ts +184 -20
  170. package/src/metadata/metadata-store.ts +11 -8
  171. package/src/pipeline/pipeline-stress.test.ts +1 -1
  172. package/src/pipeline/pipeline.test.ts +2 -2
  173. package/src/pipeline/pipeline.ts +9 -7
  174. package/src/query/errors.ts +3 -1
  175. package/src/query/plan.ts +82 -8
  176. package/src/query/query-executor.ts +1199 -164
  177. package/src/query/query-planner.test.ts +993 -81
  178. package/src/query/query-planner.ts +467 -27
  179. package/src/space/admission-discovery-extension.ts +3 -3
  180. package/src/space/auth.ts +6 -6
  181. package/src/space/control-pipeline.test.ts +5 -4
  182. package/src/space/control-pipeline.ts +16 -13
  183. package/src/space/space-manager.browser.test.ts +1 -1
  184. package/src/space/space-manager.ts +16 -20
  185. package/src/space/space-protocol.browser.test.ts +15 -14
  186. package/src/space/space-protocol.test.ts +15 -14
  187. package/src/space/space-protocol.ts +22 -18
  188. package/src/space/space.test.ts +3 -3
  189. package/src/space/space.ts +11 -11
  190. package/src/testing/index.ts +0 -2
  191. package/src/testing/test-agent-builder.ts +3 -3
  192. package/src/testing/test-network-adapter.ts +24 -3
  193. package/src/testing/test-replicator.ts +19 -20
  194. package/src/util.ts +7 -3
  195. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  196. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  197. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  198. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  199. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  200. package/dist/lib/browser/index.mjs +0 -4743
  201. package/dist/lib/browser/index.mjs.map +0 -7
  202. package/dist/lib/browser/meta.json +0 -1
  203. package/dist/lib/browser/testing/index.mjs.map +0 -7
  204. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  205. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  206. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  207. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  208. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  209. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  210. package/dist/lib/node/filter/index.cjs +0 -32
  211. package/dist/lib/node/filter/index.cjs.map +0 -7
  212. package/dist/lib/node/index.cjs +0 -4699
  213. package/dist/lib/node/index.cjs.map +0 -7
  214. package/dist/lib/node/meta.json +0 -1
  215. package/dist/lib/node/testing/index.cjs +0 -753
  216. package/dist/lib/node/testing/index.cjs.map +0 -7
  217. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  218. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  219. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  220. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  221. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  222. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  223. package/dist/lib/node-esm/filter/index.mjs +0 -11
  224. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  225. package/dist/lib/node-esm/index.mjs +0 -4743
  226. package/dist/lib/node-esm/index.mjs.map +0 -7
  227. package/dist/lib/node-esm/meta.json +0 -1
  228. package/dist/lib/node-esm/testing/index.mjs +0 -742
  229. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  230. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  231. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  232. package/dist/types/src/testing/test-data.d.ts +0 -18
  233. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  234. package/dist/types/src/testing/test-schema.d.ts +0 -37
  235. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  236. package/src/db-host/documents-iterator.ts +0 -74
  237. package/src/testing/test-data.ts +0 -127
  238. package/src/testing/test-schema.ts +0 -53
  239. /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';
@@ -18,17 +18,20 @@ import {
18
18
  type AutomergeUrl,
19
19
  type DocHandle,
20
20
  type DocumentId,
21
- type HandleState,
22
- type StorageAdapterInterface,
21
+ type DocumentProgress,
22
+ type Message,
23
23
  type PeerId,
24
24
  Repo,
25
25
  type SharePolicy,
26
+ type StorageAdapterInterface,
26
27
  generateAutomergeUrl,
28
+ initSubduction,
27
29
  parseAutomergeUrl,
28
30
  } from '@automerge/automerge-repo';
29
- import { onTestFinished, describe, expect, 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';
@@ -36,15 +39,69 @@ import { TestBuilder as TeleportBuilder, TestPeer as TeleportPeer } from '@dxos/
36
39
  import { openAndClose } from '@dxos/test-utils';
37
40
  import { isNonNullable, range } from '@dxos/util';
38
41
 
39
- import { FIND_PARAMS } from './automerge-host';
42
+ import { TestAdapter, type TestConnectionStateProvider } from '../testing';
40
43
  import { EchoNetworkAdapter } from './echo-network-adapter';
44
+ import { type HandleQueryState } from './handle-state';
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
 
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
+
47
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
+
48
105
  test('change events', () => {
49
106
  const repo = new Repo({ network: [] });
50
107
  const handle = repo.create<{ field?: string }>();
@@ -111,13 +168,14 @@ describe('AutomergeRepo', () => {
111
168
  expect(equals(a, c)).to.be.true;
112
169
  });
113
170
 
114
- test('documents missing from local storage go to requesting state', async () => {
171
+ test('documents missing from local storage go to loading state', async () => {
115
172
  const { repos, adapters } = await createHostClientRepoTopology();
116
173
  const [host] = repos;
117
174
  await connectAdapters(adapters);
118
175
  const url = 'automerge:3JN8F3Z4dUWEEKKFN7WE9gEGvVUT';
119
- const handle = await host.find(url as AutomergeUrl, FIND_PARAMS);
120
- 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');
121
179
  });
122
180
 
123
181
  test('documents on disk go to ready state', async () => {
@@ -161,32 +219,6 @@ describe('AutomergeRepo', () => {
161
219
  expect((await asyncTimeout(client.find<any>(handle.url), 1000))!.doc().text).to.equal(text);
162
220
  });
163
221
 
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
222
  test('two documents and share policy switching', async () => {
191
223
  const allowedDocs: DocumentId[] = [];
192
224
 
@@ -236,15 +268,15 @@ describe('AutomergeRepo', () => {
236
268
  await connectAdapters(adapters);
237
269
 
238
270
  const handle = host.create();
239
- const docOnClient = await client.find<any>(handle.url, FIND_PARAMS);
271
+ const docOnClient = await findInStates<any>(client, handle.url, FIND_STATES);
240
272
  {
241
273
  const sanityText = 'Hello world';
242
274
  handle.change((doc: any) => {
243
275
  doc.sanityText = sanityText;
244
276
  });
245
- await asyncTimeout(docOnClient.whenReady(), 1000);
246
-
247
- 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);
248
280
  }
249
281
 
250
282
  // Disrupt connection.
@@ -270,9 +302,7 @@ describe('AutomergeRepo', () => {
270
302
  handle.change((doc: any) => {
271
303
  doc.onlineText = onlineText;
272
304
  });
273
- await sleep(100);
274
- await asyncTimeout(docOnClient.whenReady(), 1000);
275
- expect(docOnClient.doc().onlineText).to.equal(onlineText);
305
+ await expect.poll(() => docOnClient.doc()?.onlineText, { timeout: 1000 }).toEqual(onlineText);
276
306
  expect(docOnClient.doc().offlineText).to.equal(offlineText);
277
307
  }
278
308
  });
@@ -319,8 +349,8 @@ describe('AutomergeRepo', () => {
319
349
  doc.text = 'Hello world';
320
350
  });
321
351
 
322
- const _ = repoB.find(docA.url, FIND_PARAMS);
323
- 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);
324
354
  await asyncTimeout(docC.whenReady(), 1000);
325
355
  });
326
356
 
@@ -329,7 +359,6 @@ describe('AutomergeRepo', () => {
329
359
  const [host, client] = repos;
330
360
  const [[adapter1, adapter2]] = adapters;
331
361
 
332
- const unavailable: HandleState = 'unavailable';
333
362
  await connectAdapters(adapters, { noEmitPeerCandidate: true });
334
363
 
335
364
  const docA = host.create();
@@ -338,21 +367,21 @@ describe('AutomergeRepo', () => {
338
367
  doc.text = 'Hello world';
339
368
  });
340
369
 
341
- const docB = await client.find(docA.url, { allowableStates: ['unavailable'] });
342
-
343
- // Request document from repoB.
344
- await asyncTimeout(docB.whenReady([unavailable]), 1_000);
370
+ // Without peer candidates, the client has no source for this doc → unavailable.
371
+ const progress = client.findWithProgress(docA.url);
372
+ await waitForQueryState(progress, ['unavailable'], { timeout: 1_000 });
373
+ expect(progress.peek().state).to.equal('unavailable');
345
374
 
346
375
  adapter1.peerCandidate(adapter2.peerId!);
347
376
  await sleep(100);
348
377
  adapter2.peerCandidate(adapter1.peerId!);
349
378
 
350
- // Wait becomes unavailable.
351
- await asyncTimeout(docB.whenReady([unavailable]), 1_000);
352
-
353
- // Wait gets loaded after reconnect.
379
+ // Reconnecting cycles the peers and should re-trigger sync; the doc
380
+ // becomes available. Can't use `progress.whenReady()` here because
381
+ // the query is already in `'unavailable'` and `whenReady` rejects
382
+ // immediately for that state.
354
383
  await reconnectAdapters(adapters);
355
- await asyncTimeout(docB.whenReady(), 1_000);
384
+ await waitForQueryState(progress, ['ready'], { timeout: 1_000 });
356
385
  });
357
386
 
358
387
  test('documents loaded from disk get replicated', async () => {
@@ -385,9 +414,13 @@ describe('AutomergeRepo', () => {
385
414
  const repo = new Repo({ network: [] });
386
415
  const serverHandle = repo.create<{ field?: string }>();
387
416
 
388
- 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;
389
422
  const receiveByClient = (blob: Uint8Array) => {
390
- clientDoc = A.loadIncremental(clientDoc, blob);
423
+ clientDoc = clientDoc === undefined ? A.load(blob) : A.loadIncremental(clientDoc, blob);
391
424
  };
392
425
 
393
426
  // Sync handshake.
@@ -406,7 +439,7 @@ describe('AutomergeRepo', () => {
406
439
  serverHandle.change((doc: any) => {
407
440
  doc.field = value;
408
441
  });
409
- expect(clientDoc.field).to.deep.equal(value);
442
+ expect(clientDoc?.field).to.deep.equal(value);
410
443
  }
411
444
 
412
445
  {
@@ -414,19 +447,13 @@ describe('AutomergeRepo', () => {
414
447
  serverHandle.change((doc: any) => {
415
448
  doc.field = value;
416
449
  });
417
- expect(clientDoc.field).to.deep.equal(value);
450
+ expect(clientDoc?.field).to.deep.equal(value);
418
451
  }
419
452
  });
420
453
 
421
454
  test('client creates doc and syncs with a Repo', async () => {
422
455
  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
-
456
+ const receiveByServer = (blob: Uint8Array, docId: DocumentId) => repo.import<any>(blob, { docId });
430
457
  let clientDoc = A.from<{ field?: string }>({});
431
458
  const { documentId } = parseAutomergeUrl(generateAutomergeUrl());
432
459
  // Sync handshake.
@@ -434,7 +461,7 @@ describe('AutomergeRepo', () => {
434
461
 
435
462
  // Sync protocol.
436
463
  const sendDoc = async (doc: A.Doc<any>) => {
437
- await receiveByServer(saveSince(doc, sentHeads), documentId);
464
+ receiveByServer(saveSince(doc, sentHeads), documentId);
438
465
  sentHeads = getHeads(doc);
439
466
  };
440
467
 
@@ -456,10 +483,9 @@ describe('AutomergeRepo', () => {
456
483
 
457
484
  const repo = new Repo({ network: [], storage });
458
485
  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
- });
486
+ repo.import<any>(blob, { docId });
487
+ // TODO(mykola): This should not be required. Document is not persisted without it.
488
+ await repo.flush([docId]);
463
489
  };
464
490
 
465
491
  let clientDoc = A.from<{ field?: string }>({ field: 'foo' });
@@ -516,6 +542,149 @@ describe('AutomergeRepo', () => {
516
542
  await asyncTimeout(handleB.whenReady(), 1000);
517
543
  expect(handleB.doc()!.text).to.equal(text);
518
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
+ });
519
688
  });
520
689
 
521
690
  describe('teleport', () => {
@@ -602,11 +771,25 @@ describe('AutomergeRepo', () => {
602
771
  });
603
772
  await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
604
773
 
605
- const shouldNotFindDoc = await peer2.repo.find(docNotInRemoteCollection.url, FIND_PARAMS);
606
- const shouldFindDoc = await peer2.repo.find(docInRemoteCollection.url, FIND_PARAMS);
607
- 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']);
608
782
  expect(shouldFindDoc.doc()).to.deep.eq(docInRemoteCollection.doc());
609
- await asyncTimeout(shouldNotFindDoc.whenReady(['unavailable']), 1000);
783
+ expect(shouldFindDoc.doc()).to.deep.eq(docInRemoteCollection.doc());
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({});
610
793
  });
611
794
 
612
795
  /**
@@ -636,12 +819,23 @@ describe('AutomergeRepo', () => {
636
819
  await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
637
820
  await connectPeers(anotherSpaceKey, teleportBuilder, peer2, peerFromAnotherSpace);
638
821
 
639
- const loadedDocument = await peer2.repo.find(document.url, FIND_PARAMS);
640
- await asyncTimeout(loadedDocument.whenReady(), 1000);
641
-
642
- await sleep(200);
643
- const doc = await peerFromAnotherSpace.repo.find(document.url, { allowableStates: ['unavailable'] });
644
- 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({});
645
839
  });
646
840
 
647
841
  test('collection-sync with chain of peers', async () => {
@@ -670,17 +864,72 @@ describe('AutomergeRepo', () => {
670
864
  await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
671
865
  await connectPeers(spaceKey, teleportBuilder, peer2, peer3);
672
866
 
673
- const loadedDocument = await peer2.repo.find(document.url, FIND_PARAMS);
674
- 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());
675
869
 
676
- const doc = await peer3.repo.find(document.url, FIND_PARAMS);
677
- await doc.whenReady();
678
- 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());
872
+ });
873
+
874
+ test('document is passively replicated to connected peers', async () => {
875
+ const [spaceKey] = PublicKey.randomSequence();
876
+
877
+ const teleportBuilder = new TeleportBuilder();
878
+ onTestFinished(() => teleportBuilder.destroy());
879
+
880
+ const peer1 = await createTeleportTestPeer(teleportBuilder, spaceKey);
881
+ const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey);
882
+
883
+ const handle = peer1.repo.create();
884
+ handle.change((doc: any) => (doc.text = 'hello'));
885
+ await connectPeers(spaceKey, teleportBuilder, peer1, peer2);
886
+
887
+ await expect
888
+ .poll(async () => {
889
+ const doc = peer2.repo.handles[handle.documentId];
890
+ await doc.whenReady();
891
+ return doc.doc()!.text;
892
+ })
893
+ .toEqual('hello');
894
+ });
895
+
896
+ test('imported document is passively replicated to connected peers', async () => {
897
+ let blob: Uint8Array;
898
+ let documentId: DocumentId;
899
+ {
900
+ const repo = new Repo();
901
+ const handle = repo.create();
902
+ handle.change((doc: any) => (doc.text = 'hello'));
903
+ blob = A.save(handle.doc()!);
904
+ documentId = handle.documentId;
905
+ }
906
+
907
+ const [spaceKey] = PublicKey.randomSequence();
908
+
909
+ const teleportBuilder = new TeleportBuilder();
910
+ onTestFinished(() => teleportBuilder.destroy());
911
+
912
+ const peer1 = await createTeleportTestPeer(teleportBuilder, spaceKey);
913
+ const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey);
914
+ await connectPeers(spaceKey, teleportBuilder, peer1, peer2);
915
+
916
+ const handle = peer1.repo.import(blob, { docId: documentId });
917
+ await handle.whenReady();
918
+
919
+ await expect
920
+ .poll(
921
+ async () => {
922
+ const doc = peer2.repo.handles[handle.documentId];
923
+ await doc.whenReady();
924
+ return doc.doc()!.text;
925
+ },
926
+ { timeout: 1_000 },
927
+ )
928
+ .toEqual('hello');
679
929
  });
680
930
  });
681
931
 
682
- const createLevelAdapter = async () => {
683
- const level = createTestLevel();
932
+ const createLevelAdapter = async (level = createTestLevel()) => {
684
933
  const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
685
934
  await openAndClose(level, storage);
686
935
  return storage;
@@ -702,19 +951,23 @@ describe('AutomergeRepo', () => {
702
951
  };
703
952
  });
704
953
 
954
+ type ShareConfig = Exclude<ConstructorParameters<typeof Repo>[0], undefined>['shareConfig'];
955
+
705
956
  type ConnectedRepoOptions = {
706
957
  storages?: StorageAdapterInterface[];
707
958
  connectionStateProvider?: TestConnectionStateProvider;
708
959
  sharePolicy?: SharePolicy;
960
+ shareConfig?: ShareConfig;
709
961
  };
710
962
 
711
963
  const createRepoTopology = async <Peers extends string[], Peer extends string = Peers[number]>(args: {
712
964
  peers: Peers;
713
965
  connections: [Peer, Peer][];
714
966
  options?: ConnectedRepoOptions;
967
+ onMessage?: (message: Message) => void;
715
968
  }) => {
716
969
  const adapters = args.connections.map(
717
- () => TestAdapter.createPair(args.options?.connectionStateProvider) as [TestAdapter, TestAdapter],
970
+ () => TestAdapter.createPair(args.options?.connectionStateProvider, args.onMessage) as [TestAdapter, TestAdapter],
718
971
  );
719
972
  const repos = args.peers.map((peerId, peerIndex) => {
720
973
  const network = adapters
@@ -730,7 +983,8 @@ const createRepoTopology = async <Peers extends string[], Peer extends string =
730
983
  peerId: peerId as PeerId,
731
984
  storage: args.options?.storages?.[peerIndex],
732
985
  network,
733
- sharePolicy: args.options?.sharePolicy ?? (async () => true),
986
+ sharePolicy: (args.options?.sharePolicy ?? args.options?.shareConfig) ? undefined : async () => true,
987
+ shareConfig: args.options?.shareConfig,
734
988
  });
735
989
  });
736
990
  return { repos, adapters };
@@ -797,7 +1051,7 @@ const createTeleportTestPeer = async (
797
1051
  });
798
1052
  await echoAdapter.open();
799
1053
  await echoAdapter.whenConnected();
800
- await echoAdapter.addReplicator(meshAdapter);
1054
+ await echoAdapter.addReplicator(Context.default(), meshAdapter);
801
1055
  const [teleport] = teleportBuilder.createPeers({ factory: () => new TeleportPeer() });
802
1056
  return { repo, meshAdapter, teleport };
803
1057
  };
@@ -816,3 +1070,7 @@ const connectPeers = async (
816
1070
  };
817
1071
 
818
1072
  type TeleportTestPeer = { repo: Repo; meshAdapter: MeshEchoReplicator; teleport: TeleportPeer };
1073
+
1074
+ export const createTmpPath = (): string => {
1075
+ return `/tmp/dxos-${PublicKey.random().toHex()}`;
1076
+ };