@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
@@ -3,24 +3,27 @@
3
3
  //
4
4
 
5
5
  import { next as A, type Heads } from '@automerge/automerge';
6
- import { type DocHandle, type DocumentId, type Repo } from '@automerge/automerge-repo';
6
+ import { type DocumentId, type DocumentQuery } from '@automerge/automerge-repo';
7
7
 
8
- import { UpdateScheduler, sleep } from '@dxos/async';
9
- import { LifecycleState, Resource, cancelWithContext } from '@dxos/context';
8
+ import { UpdateScheduler } from '@dxos/async';
9
+ import { Context, LifecycleState, Resource } from '@dxos/context';
10
10
  import { type DatabaseDirectory } from '@dxos/echo-protocol';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { log } from '@dxos/log';
13
13
  import { type BatchedDocumentUpdates, type DocumentUpdate } from '@dxos/protocols/proto/dxos/echo/service';
14
+ import { retry } from '@dxos/util';
15
+
16
+ import type { AutomergeHost } from '../automerge';
14
17
 
15
18
  const MAX_UPDATE_FREQ = 10; // [updates/sec]
16
19
 
17
- export type DocumentsSynchronizerParams = {
18
- repo: Repo;
20
+ export type DocumentsSynchronizerProps = {
21
+ automergeHost: AutomergeHost;
19
22
  sendUpdates: (updates: BatchedDocumentUpdates) => void;
20
23
  };
21
24
 
22
25
  interface DocSyncState {
23
- handle: DocHandle<DatabaseDirectory>;
26
+ docQuery: DocumentQuery<DatabaseDirectory>;
24
27
  lastSentHead?: Heads;
25
28
  clearSubscriptions?: () => void;
26
29
  }
@@ -39,41 +42,108 @@ export class DocumentsSynchronizer extends Resource {
39
42
  */
40
43
  private readonly _pendingUpdates = new Set<DocumentId>();
41
44
 
45
+ /**
46
+ * Documents whose on-disk probe completed negative and that need a
47
+ * mutation-less `requesting: true` transition update sent to the client.
48
+ * Cleared per-flush in `_checkAndSendUpdates`.
49
+ */
50
+ private readonly _pendingRequesting = new Set<DocumentId>();
51
+
42
52
  /**
43
53
  * Job that schedules if there are pending updates.
44
54
  */
45
55
  private _sendUpdatesJob?: UpdateScheduler = undefined;
46
56
 
47
- constructor(private readonly _params: DocumentsSynchronizerParams) {
57
+ /**
58
+ * Test affordance: when true, `_checkAndSendUpdates` is a no-op so no
59
+ * document state is flushed to the client. `addDocuments` still loads
60
+ * documents on the worker side and accumulates `_pendingUpdates`, which
61
+ * flush automatically on resume. From the client's perspective every
62
+ * `RepoProxy.find()` made while paused returns a handle that stays
63
+ * `pending` until resume — used to deterministically reproduce
64
+ * worker-side hangs in query-pipeline tests.
65
+ */
66
+ #sendUpdatesPaused = false;
67
+
68
+ constructor(private readonly _params: DocumentsSynchronizerProps) {
48
69
  super();
49
70
  }
50
71
 
51
- addDocuments(
52
- documentIds: DocumentId[],
53
- retryCounter = 0,
54
- wrapAroundRetryDelay = WRAP_AROUND_RETRY_INITIAL_DELAY,
55
- ): void {
56
- if (retryCounter > WRAP_AROUND_RETRY_LIMIT) {
57
- log.warn('Failed to load document, retry limit reached', { documentIds });
72
+ /**
73
+ * Test affordance: pause/resume flushing of document updates to the client.
74
+ * See `#sendUpdatesPaused` for details. Idempotent.
75
+ */
76
+ setSendUpdatesPaused(paused: boolean): void {
77
+ if (this.#sendUpdatesPaused === paused) {
58
78
  return;
59
79
  }
80
+ this.#sendUpdatesPaused = paused;
81
+ if (!paused) {
82
+ this._sendUpdatesJob?.trigger();
83
+ }
84
+ }
60
85
 
61
- for (const documentId of documentIds) {
62
- this._params.repo
63
- .find<DatabaseDirectory>(documentId as DocumentId)
64
- .then(async (doc) => {
65
- await doc.whenReady();
66
- this._startSync(doc);
67
- this._pendingUpdates.add(doc.documentId);
68
- this._sendUpdatesJob!.trigger();
69
- })
70
- .catch((error) => {
71
- log.warn('Failed to load document, wraparound', { documentId, error });
72
- void cancelWithContext(this._ctx, sleep(wrapAroundRetryDelay)).then(() =>
73
- this.addDocuments([documentId], retryCounter + 1, wrapAroundRetryDelay * 2),
86
+ async addDocuments(documentIds: DocumentId[]): Promise<void> {
87
+ await Promise.all(
88
+ documentIds.map(async (documentId) => {
89
+ try {
90
+ await retry(
91
+ { count: WRAP_AROUND_RETRY_LIMIT, delayMs: WRAP_AROUND_RETRY_INITIAL_DELAY, exponent: 2 },
92
+ async () => {
93
+ try {
94
+ log('loading document', { documentId });
95
+ const docQuery = this._params.automergeHost.findWithProgress<DatabaseDirectory>(
96
+ documentId as DocumentId,
97
+ );
98
+ this._startSync(docQuery);
99
+ this._pendingUpdates.add(docQuery.documentId);
100
+ this._sendUpdatesJob!.trigger();
101
+ // Background disk probe so the client can distinguish
102
+ // "not on disk, waiting for network" from "still loading".
103
+ // Fire-and-forget; the result feeds `_pendingRequesting`.
104
+ this._scheduleDiskProbe(docQuery.documentId);
105
+ } catch (err) {
106
+ log.warn('failed to load document', { err });
107
+ throw err;
108
+ }
109
+ },
74
110
  );
75
- });
76
- }
111
+ } catch (err) {
112
+ log.catch(err);
113
+ }
114
+ }),
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Probe local storage for the document; if not present, enqueue a
120
+ * `requesting: true` transition update so the client moves the handle
121
+ * from `'pending'` to `'requesting'` and disk-only callers can give up
122
+ * without waiting on the network. If the doc became `ready` before the
123
+ * probe completes (e.g. delivered concurrently by network/peer), no
124
+ * transition update is sent.
125
+ */
126
+ private _scheduleDiskProbe(documentId: DocumentId): void {
127
+ void Promise.resolve().then(async () => {
128
+ try {
129
+ const onDisk = await this._params.automergeHost.hasDocOnDisk(documentId);
130
+ if (onDisk) {
131
+ // Doc is on disk; the existing `heads-changed` flow will deliver
132
+ // a normal mutation update once the load completes.
133
+ return;
134
+ }
135
+ // Skip the transition signal if the doc has since become `ready`
136
+ // via the network/peer race.
137
+ const syncState = this._syncStates.get(documentId);
138
+ if (!syncState || syncState.docQuery.peek().state === 'ready') {
139
+ return;
140
+ }
141
+ this._pendingRequesting.add(documentId);
142
+ this._sendUpdatesJob?.trigger();
143
+ } catch (err) {
144
+ log.warn('disk probe failed', { documentId, err });
145
+ }
146
+ });
77
147
  }
78
148
 
79
149
  removeDocuments(documentIds: DocumentId[]): void {
@@ -81,6 +151,7 @@ export class DocumentsSynchronizer extends Resource {
81
151
  this._syncStates.get(documentId)?.clearSubscriptions?.();
82
152
  this._syncStates.delete(documentId);
83
153
  this._pendingUpdates.delete(documentId);
154
+ this._pendingRequesting.delete(documentId);
84
155
  }
85
156
  }
86
157
 
@@ -95,36 +166,48 @@ export class DocumentsSynchronizer extends Resource {
95
166
  this._syncStates.clear();
96
167
  }
97
168
 
98
- async update(updates: DocumentUpdate[]): Promise<void> {
99
- for (const { documentId, mutation, isNew } of updates) {
100
- this._writeMutation(documentId as DocumentId, mutation, isNew);
169
+ async update(ctx: Context, updates: DocumentUpdate[]): Promise<void> {
170
+ for (const { documentId, mutation } of updates) {
171
+ // Inbound (client -> worker) updates are always mutation-bearing; the
172
+ // `mutation`-less variant is only used for worker -> client transition
173
+ // signals (e.g. `requesting`). Defensive skip here to satisfy the
174
+ // optional proto field.
175
+ if (!mutation) {
176
+ continue;
177
+ }
178
+ await this._writeMutation(documentId as DocumentId, mutation);
101
179
  }
102
180
  // TODO(mykola): This should not be required.
103
- await this._params.repo.flush(updates.map(({ documentId }) => documentId as DocumentId));
181
+ await this._params.automergeHost.flush(ctx, {
182
+ documentIds: updates.map(({ documentId }) => documentId as DocumentId),
183
+ });
104
184
  }
105
185
 
106
- private _startSync(doc: DocHandle<DatabaseDirectory>) {
107
- if (this._syncStates.has(doc.documentId)) {
108
- log('Document already being synced', { documentId: doc.documentId });
186
+ private _startSync(docQuery: DocumentQuery<DatabaseDirectory>) {
187
+ if (this._syncStates.has(docQuery.documentId)) {
188
+ log('Document already being synced', { documentId: docQuery.documentId });
109
189
  return;
110
190
  }
111
191
 
112
- const syncState: DocSyncState = { handle: doc };
192
+ const syncState: DocSyncState = { docQuery };
113
193
  this._subscribeForChanges(syncState);
114
- this._syncStates.set(doc.documentId, syncState);
194
+ this._syncStates.set(docQuery.documentId, syncState);
115
195
  return syncState;
116
196
  }
117
197
 
118
198
  _subscribeForChanges(syncState: DocSyncState): void {
119
199
  const handler = () => {
120
- this._pendingUpdates.add(syncState.handle.documentId);
200
+ this._pendingUpdates.add(syncState.docQuery.documentId);
121
201
  this._sendUpdatesJob!.trigger();
122
202
  };
123
- syncState.handle.on('heads-changed', handler);
124
- syncState.clearSubscriptions = () => syncState.handle.off('heads-changed', handler);
203
+ syncState.docQuery.handle.on('heads-changed', handler);
204
+ syncState.clearSubscriptions = () => syncState.docQuery.handle.off('heads-changed', handler);
125
205
  }
126
206
 
127
207
  private async _checkAndSendUpdates(): Promise<void> {
208
+ if (this.#sendUpdatesPaused) {
209
+ return;
210
+ }
128
211
  const updates: DocumentUpdate[] = [];
129
212
 
130
213
  const docsWithPendingUpdates = Array.from(this._pendingUpdates);
@@ -140,6 +223,23 @@ export class DocumentsSynchronizer extends Resource {
140
223
  }
141
224
  }
142
225
 
226
+ // Mutation-less transition updates: tell the client `requesting: true`
227
+ // for any documents whose disk probe completed negative. Skip docs that
228
+ // are already being delivered as a real mutation in this same batch
229
+ // (those go directly to `ready` on the client and a subsequent
230
+ // `requesting` would be ignored anyway).
231
+ const docsRequesting = Array.from(this._pendingRequesting);
232
+ this._pendingRequesting.clear();
233
+ if (docsRequesting.length > 0) {
234
+ const docsBeingFlushed = new Set(updates.map((update) => update.documentId));
235
+ for (const documentId of docsRequesting) {
236
+ if (docsBeingFlushed.has(documentId)) {
237
+ continue;
238
+ }
239
+ updates.push({ documentId, requesting: true });
240
+ }
241
+ }
242
+
143
243
  if (updates.length > 0) {
144
244
  this._params.sendUpdates({ updates });
145
245
  }
@@ -148,11 +248,14 @@ export class DocumentsSynchronizer extends Resource {
148
248
  private _getPendingChanges(documentId: DocumentId): Uint8Array | void {
149
249
  const syncState = this._syncStates.get(documentId);
150
250
  invariant(syncState, 'Sync state for document not found');
151
- const handle = syncState.handle;
152
- if (!handle || !handle.isReady() || !handle.doc()) {
251
+ const handle = syncState.docQuery.handle;
252
+ if (!handle || syncState.docQuery.peek().state !== 'ready') {
153
253
  return;
154
254
  }
155
255
  const doc = handle.doc();
256
+ if (!doc) {
257
+ return;
258
+ }
156
259
  const mutation = syncState.lastSentHead ? A.saveSince(doc, syncState.lastSentHead) : A.save(doc);
157
260
  if (mutation.length === 0) {
158
261
  return;
@@ -161,25 +264,21 @@ export class DocumentsSynchronizer extends Resource {
161
264
  return mutation;
162
265
  }
163
266
 
164
- private _writeMutation(documentId: DocumentId, mutation: Uint8Array, isNew?: boolean): void {
267
+ private async _writeMutation(documentId: DocumentId, mutation: Uint8Array): Promise<void> {
165
268
  if (this._lifecycleState === LifecycleState.CLOSED) {
166
269
  return;
167
270
  }
168
- if (isNew) {
169
- const newHandle = this._params.repo.import<DatabaseDirectory>(mutation, { docId: documentId });
170
- const syncState = this._startSync(newHandle);
171
- syncState!.lastSentHead = A.getHeads(newHandle.doc());
172
- } else {
173
- const syncState = this._syncStates.get(documentId);
174
- invariant(syncState, 'Sync state for document not found');
175
- const headsBefore = A.getHeads(syncState.handle.doc());
176
- // This will update corresponding handle in the repo.
177
- this._params.repo.import(mutation, { docId: documentId });
178
-
179
- if (A.equals(headsBefore, syncState!.lastSentHead)) {
180
- // No new mutations were discovered on network, so we do not need to send updates from worker to client.
181
- syncState!.lastSentHead = A.getHeads(syncState.handle.doc());
182
- }
271
+ log('write mutation', { documentId });
272
+
273
+ const syncState = this._syncStates.get(documentId);
274
+ invariant(syncState, 'Sync state for document not found');
275
+ const headsBefore = A.getHeads(syncState.docQuery.handle.doc());
276
+ // This will update corresponding handle in the repo.
277
+ await this._params.automergeHost.createDoc(mutation, { documentId, preserveHistory: true });
278
+
279
+ if (A.equals(headsBefore, syncState.lastSentHead)) {
280
+ // No new mutations were discovered on network, so we do not need to send updates from worker to client.
281
+ syncState.lastSentHead = A.getHeads(syncState.docQuery.handle.doc());
183
282
  }
184
283
  }
185
284
  }