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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/dist/lib/neutral/chunk-FJPXA75J.mjs +235 -0
  2. package/dist/lib/neutral/chunk-FJPXA75J.mjs.map +7 -0
  3. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-WIQUYO7H.mjs} +300 -213
  4. package/dist/lib/neutral/chunk-WIQUYO7H.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/{browser → neutral}/index.mjs +2832 -946
  7. package/dist/lib/neutral/index.mjs.map +7 -0
  8. package/dist/lib/neutral/meta.json +1 -0
  9. package/dist/lib/{browser → neutral}/testing/index.mjs +73 -224
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host.d.ts +53 -42
  12. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  13. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  14. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  15. package/dist/types/src/automerge/collection-synchronizer.d.ts +6 -4
  16. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  17. package/dist/types/src/automerge/echo-network-adapter.d.ts +22 -10
  18. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  19. package/dist/types/src/automerge/echo-replicator.d.ts +37 -15
  20. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  21. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  22. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  23. package/dist/types/src/automerge/index.d.ts +1 -1
  24. package/dist/types/src/automerge/index.d.ts.map +1 -1
  25. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +5 -5
  26. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  28. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  29. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +5 -5
  30. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  31. package/dist/types/src/common/codec.d.ts +1 -1
  32. package/dist/types/src/common/codec.d.ts.map +1 -1
  33. package/dist/types/src/db-host/automerge-data-source.d.ts +34 -0
  34. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  35. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  36. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  37. package/dist/types/src/db-host/data-service.d.ts +5 -4
  38. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  39. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  40. package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
  41. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  42. package/dist/types/src/db-host/echo-host.d.ts +44 -23
  43. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  44. package/dist/types/src/db-host/index.d.ts +1 -0
  45. package/dist/types/src/db-host/index.d.ts.map +1 -1
  46. package/dist/types/src/db-host/local-queue-service.d.ts +16 -0
  47. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  48. package/dist/types/src/db-host/query-service.d.ts +14 -9
  49. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  50. package/dist/types/src/db-host/queue-data-source.d.ts +28 -0
  51. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  52. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  53. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  54. package/dist/types/src/db-host/space-state-manager.d.ts +2 -1
  55. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  56. package/dist/types/src/db-host/stub.d.ts +10 -0
  57. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  58. package/dist/types/src/edge/echo-edge-replicator.d.ts +8 -6
  59. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  60. package/dist/types/src/filter/filter-match.d.ts +4 -1
  61. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  62. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  63. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  64. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  65. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  66. package/dist/types/src/query/errors.d.ts +21 -9
  67. package/dist/types/src/query/errors.d.ts.map +1 -1
  68. package/dist/types/src/query/plan.d.ts +57 -5
  69. package/dist/types/src/query/plan.d.ts.map +1 -1
  70. package/dist/types/src/query/query-executor.d.ts +49 -10
  71. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  72. package/dist/types/src/query/query-planner.d.ts +9 -0
  73. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  74. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  75. package/dist/types/src/space/auth.d.ts +3 -3
  76. package/dist/types/src/space/auth.d.ts.map +1 -1
  77. package/dist/types/src/space/control-pipeline.d.ts +3 -3
  78. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  79. package/dist/types/src/space/space-manager.d.ts +7 -7
  80. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  81. package/dist/types/src/space/space-protocol.d.ts +6 -6
  82. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  83. package/dist/types/src/space/space.d.ts +4 -4
  84. package/dist/types/src/space/space.d.ts.map +1 -1
  85. package/dist/types/src/testing/index.d.ts +0 -2
  86. package/dist/types/src/testing/index.d.ts.map +1 -1
  87. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  88. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  89. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  90. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  91. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  92. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  93. package/dist/types/src/util.d.ts +1 -1
  94. package/dist/types/src/util.d.ts.map +1 -1
  95. package/dist/types/tsconfig.tsbuildinfo +1 -1
  96. package/package.json +53 -45
  97. package/src/automerge/automerge-host.test.ts +55 -24
  98. package/src/automerge/automerge-host.ts +399 -126
  99. package/src/automerge/automerge-repo.test.ts +214 -46
  100. package/src/automerge/collection-synchronizer.test.ts +22 -15
  101. package/src/automerge/collection-synchronizer.ts +57 -22
  102. package/src/automerge/echo-data-monitor.ts +1 -1
  103. package/src/automerge/echo-network-adapter.test.ts +3 -3
  104. package/src/automerge/echo-network-adapter.ts +73 -28
  105. package/src/automerge/echo-replicator.ts +40 -15
  106. package/src/automerge/heads-store.ts +42 -3
  107. package/src/automerge/index.ts +1 -1
  108. package/src/automerge/leveldb-storage-adapter.ts +11 -11
  109. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  110. package/src/automerge/mesh-echo-replicator.ts +15 -11
  111. package/src/automerge/storage-adapter.test.ts +1 -1
  112. package/src/common/space-id.ts +1 -1
  113. package/src/db-host/automerge-data-source.test.ts +292 -0
  114. package/src/db-host/automerge-data-source.ts +144 -0
  115. package/src/db-host/data-service.ts +19 -20
  116. package/src/db-host/database-root.ts +2 -2
  117. package/src/db-host/documents-synchronizer.test.ts +73 -13
  118. package/src/db-host/documents-synchronizer.ts +58 -44
  119. package/src/db-host/echo-host.ts +194 -84
  120. package/src/db-host/index.ts +1 -0
  121. package/src/db-host/local-queue-service.ts +147 -0
  122. package/src/db-host/query-service.ts +28 -95
  123. package/src/db-host/queue-data-source.ts +132 -0
  124. package/src/db-host/queue-service.test.ts +145 -0
  125. package/src/db-host/space-state-manager.ts +6 -2
  126. package/src/db-host/stub.ts +28 -0
  127. package/src/edge/echo-edge-replicator.test.ts +8 -6
  128. package/src/edge/echo-edge-replicator.ts +124 -42
  129. package/src/filter/filter-match.test.ts +58 -32
  130. package/src/filter/filter-match.ts +163 -15
  131. package/src/metadata/metadata-store.ts +8 -5
  132. package/src/pipeline/pipeline-stress.test.ts +4 -2
  133. package/src/pipeline/pipeline.test.ts +3 -2
  134. package/src/pipeline/pipeline.ts +10 -7
  135. package/src/query/errors.ts +3 -1
  136. package/src/query/plan.ts +75 -5
  137. package/src/query/query-executor.ts +812 -117
  138. package/src/query/query-planner.test.ts +652 -34
  139. package/src/query/query-planner.ts +276 -13
  140. package/src/space/admission-discovery-extension.ts +3 -3
  141. package/src/space/auth.ts +6 -6
  142. package/src/space/control-pipeline.test.ts +4 -3
  143. package/src/space/control-pipeline.ts +11 -8
  144. package/src/space/space-manager.browser.test.ts +1 -1
  145. package/src/space/space-manager.ts +11 -10
  146. package/src/space/space-protocol.browser.test.ts +2 -2
  147. package/src/space/space-protocol.test.ts +3 -2
  148. package/src/space/space-protocol.ts +17 -14
  149. package/src/space/space.test.ts +1 -1
  150. package/src/space/space.ts +6 -5
  151. package/src/testing/index.ts +0 -2
  152. package/src/testing/test-agent-builder.ts +4 -3
  153. package/src/testing/test-network-adapter.ts +16 -3
  154. package/src/testing/test-replicator.ts +23 -18
  155. package/src/util.ts +7 -3
  156. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  157. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  158. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  159. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  160. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  161. package/dist/lib/browser/index.mjs.map +0 -7
  162. package/dist/lib/browser/meta.json +0 -1
  163. package/dist/lib/browser/testing/index.mjs.map +0 -7
  164. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  165. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  166. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  167. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  168. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  169. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  170. package/dist/lib/node/filter/index.cjs +0 -32
  171. package/dist/lib/node/filter/index.cjs.map +0 -7
  172. package/dist/lib/node/index.cjs +0 -4699
  173. package/dist/lib/node/index.cjs.map +0 -7
  174. package/dist/lib/node/meta.json +0 -1
  175. package/dist/lib/node/testing/index.cjs +0 -753
  176. package/dist/lib/node/testing/index.cjs.map +0 -7
  177. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  178. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  179. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  180. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  181. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  182. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  183. package/dist/lib/node-esm/filter/index.mjs +0 -11
  184. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  185. package/dist/lib/node-esm/index.mjs +0 -4743
  186. package/dist/lib/node-esm/index.mjs.map +0 -7
  187. package/dist/lib/node-esm/meta.json +0 -1
  188. package/dist/lib/node-esm/testing/index.mjs +0 -742
  189. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  190. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  191. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  192. package/dist/types/src/testing/test-data.d.ts +0 -18
  193. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  194. package/dist/types/src/testing/test-schema.d.ts +0 -37
  195. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  196. package/src/db-host/documents-iterator.ts +0 -74
  197. package/src/testing/test-data.ts +0 -127
  198. package/src/testing/test-schema.ts +0 -53
  199. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -3,21 +3,22 @@
3
3
  //
4
4
 
5
5
  import { next as A, type Heads } from '@automerge/automerge';
6
- import { type Repo, type DocHandle, type DocumentId } from '@automerge/automerge-repo';
6
+ import { type DocHandle, type DocumentId } from '@automerge/automerge-repo';
7
7
 
8
8
  import { UpdateScheduler } from '@dxos/async';
9
- import { Resource } from '@dxos/context';
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';
14
15
 
15
- import { FIND_PARAMS } from '../automerge';
16
+ import type { AutomergeHost } from '../automerge';
16
17
 
17
18
  const MAX_UPDATE_FREQ = 10; // [updates/sec]
18
19
 
19
- export type DocumentsSynchronizerParams = {
20
- repo: Repo;
20
+ export type DocumentsSynchronizerProps = {
21
+ automergeHost: AutomergeHost;
21
22
  sendUpdates: (updates: BatchedDocumentUpdates) => void;
22
23
  };
23
24
 
@@ -27,6 +28,9 @@ interface DocSyncState {
27
28
  clearSubscriptions?: () => void;
28
29
  }
29
30
 
31
+ const WRAP_AROUND_RETRY_LIMIT = 3;
32
+ const WRAP_AROUND_RETRY_INITIAL_DELAY = 100; // [ms]
33
+
30
34
  /**
31
35
  * Manages a connection and replication between worker's Automerge Repo and the client's Repo.
32
36
  */
@@ -43,30 +47,38 @@ export class DocumentsSynchronizer extends Resource {
43
47
  */
44
48
  private _sendUpdatesJob?: UpdateScheduler = undefined;
45
49
 
46
- constructor(private readonly _params: DocumentsSynchronizerParams) {
50
+ constructor(private readonly _params: DocumentsSynchronizerProps) {
47
51
  super();
48
52
  }
49
53
 
50
- addDocuments(documentIds: DocumentId[], retryCounter = 0): void {
51
- if (retryCounter > 3) {
52
- log.warn('Failed to load document, retry limit reached', { documentIds });
53
- return;
54
- }
55
-
56
- for (const documentId of documentIds) {
57
- this._params.repo
58
- .find<DatabaseDirectory>(documentId as DocumentId)
59
- .then(async (doc) => {
60
- await doc.whenReady();
61
- this._startSync(doc);
62
- this._pendingUpdates.add(doc.documentId);
63
- this._sendUpdatesJob!.trigger();
64
- })
65
- .catch((error) => {
66
- log.warn('Failed to load document, wraparound', { documentId, error });
67
- this.addDocuments([documentId], retryCounter + 1);
68
- });
69
- }
54
+ async addDocuments(documentIds: DocumentId[]): Promise<void> {
55
+ await Promise.all(
56
+ documentIds.map(async (documentId) => {
57
+ try {
58
+ await retry(
59
+ { count: WRAP_AROUND_RETRY_LIMIT, delayMs: WRAP_AROUND_RETRY_INITIAL_DELAY, exponent: 2 },
60
+ async () => {
61
+ try {
62
+ log('loading document', { documentId });
63
+ const doc = await this._params.automergeHost.loadDoc<DatabaseDirectory>(
64
+ Context.default(),
65
+ documentId as DocumentId,
66
+ { fetchFromNetwork: true },
67
+ );
68
+ this._startSync(doc);
69
+ this._pendingUpdates.add(doc.documentId);
70
+ this._sendUpdatesJob!.trigger();
71
+ } catch (err) {
72
+ log.warn('failed to load document', { err });
73
+ throw err;
74
+ }
75
+ },
76
+ );
77
+ } catch (err) {
78
+ log.catch(err);
79
+ }
80
+ }),
81
+ );
70
82
  }
71
83
 
72
84
  removeDocuments(documentIds: DocumentId[]): void {
@@ -89,18 +101,14 @@ export class DocumentsSynchronizer extends Resource {
89
101
  }
90
102
 
91
103
  async update(updates: DocumentUpdate[]): Promise<void> {
92
- for (const { documentId, mutation, isNew } of updates) {
93
- if (isNew) {
94
- const doc = await this._params.repo.find<DatabaseDirectory>(documentId as DocumentId, FIND_PARAMS);
95
- doc.update((doc) => A.loadIncremental(doc, mutation));
96
- this._startSync(doc);
97
- } else {
98
- this._writeMutation(documentId as DocumentId, mutation);
99
- }
104
+ for (const { documentId, mutation } of updates) {
105
+ await this._writeMutation(documentId as DocumentId, mutation);
100
106
  }
107
+ // TODO(mykola): This should not be required.
108
+ await this._params.automergeHost.flush({ documentIds: updates.map(({ documentId }) => documentId as DocumentId) });
101
109
  }
102
110
 
103
- private _startSync(doc: DocHandle<DatabaseDirectory>): void {
111
+ private _startSync(doc: DocHandle<DatabaseDirectory>) {
104
112
  if (this._syncStates.has(doc.documentId)) {
105
113
  log('Document already being synced', { documentId: doc.documentId });
106
114
  return;
@@ -109,6 +117,7 @@ export class DocumentsSynchronizer extends Resource {
109
117
  const syncState: DocSyncState = { handle: doc };
110
118
  this._subscribeForChanges(syncState);
111
119
  this._syncStates.set(doc.documentId, syncState);
120
+ return syncState;
112
121
  }
113
122
 
114
123
  _subscribeForChanges(syncState: DocSyncState): void {
@@ -157,16 +166,21 @@ export class DocumentsSynchronizer extends Resource {
157
166
  return mutation;
158
167
  }
159
168
 
160
- private _writeMutation(documentId: DocumentId, mutation: Uint8Array): void {
169
+ private async _writeMutation(documentId: DocumentId, mutation: Uint8Array): Promise<void> {
170
+ if (this._lifecycleState === LifecycleState.CLOSED) {
171
+ return;
172
+ }
173
+ log('write mutation', { documentId });
174
+
161
175
  const syncState = this._syncStates.get(documentId);
162
176
  invariant(syncState, 'Sync state for document not found');
163
- syncState.handle.update((doc) => {
164
- const headsBefore = A.getHeads(doc);
165
- const newDoc = A.loadIncremental(doc, mutation);
166
- if (A.equals(headsBefore, syncState.lastSentHead)) {
167
- syncState.lastSentHead = A.getHeads(newDoc);
168
- }
169
- return newDoc;
170
- });
177
+ const headsBefore = A.getHeads(syncState.handle.doc());
178
+ // This will update corresponding handle in the repo.
179
+ await this._params.automergeHost.createDoc(mutation, { documentId, preserveHistory: true });
180
+
181
+ if (A.equals(headsBefore, syncState.lastSentHead)) {
182
+ // No new mutations were discovered on network, so we do not need to send updates from worker to client.
183
+ syncState.lastSentHead = A.getHeads(syncState.handle.doc());
184
+ }
171
185
  }
172
186
  }
@@ -2,66 +2,63 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import {
6
- type AnyDocumentId,
7
- type AutomergeUrl,
8
- type DocHandle,
9
- type DocumentId,
10
- type Repo,
11
- } from '@automerge/automerge-repo';
12
-
13
- import { LifecycleState, Resource, type Context } from '@dxos/context';
5
+ import { type AnyDocumentId, type AutomergeUrl, type DocHandle, type DocumentId } from '@automerge/automerge-repo';
6
+ import type * as SqlClient from '@effect/sql/SqlClient';
7
+
8
+ import { DeferredTask, sleep } from '@dxos/async';
9
+ import { Context, LifecycleState, Resource } from '@dxos/context';
14
10
  import { todo } from '@dxos/debug';
15
- import { createIdFromSpaceKey, SpaceDocVersion, type DatabaseDirectory } from '@dxos/echo-protocol';
16
- import { IndexMetadataStore, IndexStore, Indexer } from '@dxos/indexing';
11
+ import { type DatabaseDirectory, SpaceDocVersion, createIdFromSpaceKey } from '@dxos/echo-protocol';
12
+ import { RuntimeProvider } from '@dxos/effect';
13
+ import { FeedStore } from '@dxos/feed';
14
+ import { IndexEngine } from '@dxos/index-core';
17
15
  import { invariant } from '@dxos/invariant';
18
16
  import { type PublicKey, type SpaceId } from '@dxos/keys';
19
17
  import { type LevelDB } from '@dxos/kv-store';
20
- import { IndexKind } from '@dxos/protocols/proto/dxos/echo/indexing';
18
+ import { log } from '@dxos/log';
19
+ import { type FeedProtocol } from '@dxos/protocols';
20
+ import type * as SqlTransaction from '@dxos/sql-sqlite/SqlTransaction';
21
21
  import { trace } from '@dxos/tracing';
22
22
 
23
- import { DataServiceImpl } from './data-service';
24
- import { type DatabaseRoot } from './database-root';
25
- import { createSelectedDocumentsIterator } from './documents-iterator';
26
- import { QueryServiceImpl } from './query-service';
27
- import { SpaceStateManager } from './space-state-manager';
28
23
  import {
29
24
  AutomergeHost,
30
- FIND_PARAMS,
31
- EchoDataMonitor,
32
- deriveCollectionIdFromSpaceId,
33
- type LoadDocOptions,
25
+ type AutomergeReplicator,
34
26
  type CreateDocOptions,
35
- type EchoReplicator,
27
+ EchoDataMonitor,
36
28
  type EchoDataStats,
29
+ type LoadDocOptions,
37
30
  type PeerIdProvider,
38
31
  type RootDocumentSpaceKeyProvider,
32
+ deriveCollectionIdFromSpaceId,
39
33
  } from '../automerge';
40
34
 
41
- export interface EchoHostIndexingConfig {
35
+ import { AutomergeDataSource } from './automerge-data-source';
36
+ import { DataServiceImpl } from './data-service';
37
+ import { type DatabaseRoot } from './database-root';
38
+ import { LocalQueueServiceImpl } from './local-queue-service';
39
+ import { QueryServiceImpl } from './query-service';
40
+ import { QueueDataSource } from './queue-data-source';
41
+ import { SpaceStateManager } from './space-state-manager';
42
+ import { QueueServiceStub } from './stub';
43
+
44
+ export type EchoHostProps = {
45
+ kv: LevelDB;
46
+ peerIdProvider?: PeerIdProvider;
47
+ getSpaceKeyByRootDocumentId?: RootDocumentSpaceKeyProvider;
48
+
49
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
50
+
42
51
  /**
43
- * @default true
52
+ * Use SQLite-backed local-first durable queues.
53
+ * Otherwise defaults to in-memory impl.
44
54
  */
45
- fullText: boolean;
55
+ localQueues?: boolean;
46
56
 
47
57
  /**
58
+ * This peer is allowed to assign positions (global-order) to items appended to the queue.
48
59
  * @default false
49
60
  */
50
- vector: boolean;
51
- }
52
-
53
- const DEFAULT_INDEXING_CONFIG: EchoHostIndexingConfig = {
54
- // TODO(dmaretskyi): Disabled by default since embedding generation is expensive.
55
- fullText: false,
56
- vector: false,
57
- };
58
-
59
- export type EchoHostParams = {
60
- kv: LevelDB;
61
- peerIdProvider?: PeerIdProvider;
62
- getSpaceKeyByRootDocumentId?: RootDocumentSpaceKeyProvider;
63
-
64
- indexing?: Partial<EchoHostIndexingConfig>;
61
+ assignQueuePositions?: boolean;
65
62
  };
66
63
 
67
64
  /**
@@ -71,51 +68,64 @@ export type EchoHostParams = {
71
68
  * Can sync with pluggable data replicators.
72
69
  */
73
70
  export class EchoHost extends Resource {
74
- private readonly _indexMetadataStore: IndexMetadataStore;
75
- private readonly _indexer: Indexer;
76
71
  private readonly _automergeHost: AutomergeHost;
77
72
  private readonly _queryService: QueryServiceImpl;
78
73
  private readonly _dataService: DataServiceImpl;
79
74
  private readonly _spaceStateManager = new SpaceStateManager();
80
75
  private readonly _echoDataMonitor: EchoDataMonitor;
81
76
 
82
- constructor({ kv, indexing = {}, peerIdProvider, getSpaceKeyByRootDocumentId }: EchoHostParams) {
83
- super();
77
+ private readonly _automergeDataSource: AutomergeDataSource;
78
+ private readonly _indexEngine: IndexEngine;
79
+ private readonly _runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
80
+ private readonly _feedStore?: FeedStore;
81
+ private readonly _queueDataSource?: QueueDataSource;
82
+
83
+ private _updateIndexes!: DeferredTask;
84
+
85
+ private _queuesService: FeedProtocol.QueueService;
86
+
87
+ private _indexesUpToDate = false;
84
88
 
85
- const indexingConfig = { ...DEFAULT_INDEXING_CONFIG, ...indexing };
89
+ constructor({
90
+ kv,
91
+ localQueues,
92
+ peerIdProvider,
93
+ getSpaceKeyByRootDocumentId,
94
+ runtime,
95
+ assignQueuePositions = false,
96
+ }: EchoHostProps) {
97
+ super();
86
98
 
87
- this._indexMetadataStore = new IndexMetadataStore({ db: kv.sublevel('index-metadata') });
88
99
  this._echoDataMonitor = new EchoDataMonitor();
89
100
  this._automergeHost = new AutomergeHost({
90
101
  db: kv,
91
102
  dataMonitor: this._echoDataMonitor,
92
- indexMetadataStore: this._indexMetadataStore,
93
103
  peerIdProvider,
94
104
  getSpaceKeyByRootDocumentId,
95
105
  });
96
106
 
97
- this._indexer = new Indexer({
98
- db: kv,
99
- indexStore: new IndexStore({ db: kv.sublevel('index-storage') }),
100
- metadataStore: this._indexMetadataStore,
101
- loadDocuments: createSelectedDocumentsIterator(this._automergeHost),
102
- indexCooldownTime: process.env.NODE_ENV === 'test' ? 0 : undefined,
103
- });
104
- void this._indexer.setConfig({
105
- enabled: true,
106
- indexes: [
107
- //
108
- { kind: IndexKind.Kind.SCHEMA_MATCH },
109
- { kind: IndexKind.Kind.GRAPH },
110
-
111
- ...(indexingConfig.fullText ? [{ kind: IndexKind.Kind.FULL_TEXT }] : []),
112
- ...(indexingConfig.vector ? [{ kind: IndexKind.Kind.VECTOR }] : []),
113
- ],
114
- });
107
+ this._runtime = runtime;
108
+ this._automergeDataSource = new AutomergeDataSource(this._automergeHost);
109
+
110
+ if (localQueues) {
111
+ this._feedStore = new FeedStore({ assignPositions: assignQueuePositions, localActorId: crypto.randomUUID() });
112
+ this._queueDataSource = new QueueDataSource({
113
+ feedStore: this._feedStore,
114
+ runtime: this._runtime,
115
+ getSpaceIds: () => this._spaceStateManager.spaceIds,
116
+ });
117
+ this._queuesService = new LocalQueueServiceImpl(runtime, this._feedStore);
118
+ } else {
119
+ this._queuesService = new QueueServiceStub();
120
+ }
121
+
122
+ // SQLite-based index engine for all queries.
123
+ this._indexEngine = new IndexEngine();
115
124
 
116
125
  this._queryService = new QueryServiceImpl({
117
126
  automergeHost: this._automergeHost,
118
- indexer: this._indexer,
127
+ indexEngine: this._indexEngine,
128
+ runtime: this._runtime,
119
129
  spaceStateManager: this._spaceStateManager,
120
130
  });
121
131
 
@@ -123,7 +133,9 @@ export class EchoHost extends Resource {
123
133
  automergeHost: this._automergeHost,
124
134
  spaceStateManager: this._spaceStateManager,
125
135
  updateIndexes: async () => {
126
- await this._indexer.updateIndexes();
136
+ do {
137
+ await this._updateIndexes.runBlocking();
138
+ } while (!this._indexesUpToDate);
127
139
  },
128
140
  });
129
141
 
@@ -168,6 +180,10 @@ export class EchoHost extends Resource {
168
180
  });
169
181
  }
170
182
 
183
+ get spaceIds(): SpaceId[] {
184
+ return this._spaceStateManager.spaceIds;
185
+ }
186
+
171
187
  get queryService(): QueryServiceImpl {
172
188
  return this._queryService;
173
189
  }
@@ -176,29 +192,45 @@ export class EchoHost extends Resource {
176
192
  return this._dataService;
177
193
  }
178
194
 
179
- /**
180
- * @deprecated To be abstracted away.
181
- */
182
- get automergeRepo(): Repo {
183
- return this._automergeHost.repo;
195
+ get queuesService(): FeedProtocol.QueueService {
196
+ return this._queuesService;
184
197
  }
185
198
 
186
199
  get roots(): ReadonlyMap<DocumentId, DatabaseRoot> {
187
200
  return this._spaceStateManager.roots;
188
201
  }
189
202
 
203
+ get feedStore(): FeedStore | undefined {
204
+ return this._feedStore;
205
+ }
206
+
207
+ /**
208
+ * Index engine for queries.
209
+ */
210
+ get indexEngine(): IndexEngine {
211
+ return this._indexEngine;
212
+ }
213
+
190
214
  protected override async _open(ctx: Context): Promise<void> {
191
215
  await this._automergeHost.open();
192
- await this._indexer.open(ctx);
193
216
  await this._queryService.open(ctx);
194
217
  await this._spaceStateManager.open(ctx);
195
218
 
219
+ if (this._feedStore) {
220
+ await RuntimeProvider.runPromise(this._runtime)(this._feedStore.migrate());
221
+ this._feedStore.onNewBlocks.on(this._ctx, () => {
222
+ this._queryService.invalidateQueries();
223
+ this._updateIndexes.schedule();
224
+ });
225
+ }
226
+
227
+ await RuntimeProvider.runPromise(this._runtime)(this._indexEngine.migrate());
228
+ this._updateIndexes = new DeferredTask(this._ctx, this._runUpdateIndexes);
229
+
196
230
  this._spaceStateManager.spaceDocumentListUpdated.on(this._ctx, (e) => {
197
231
  if (e.previousRootId) {
198
232
  void this._automergeHost.clearLocalCollectionState(deriveCollectionIdFromSpaceId(e.spaceId, e.previousRootId));
199
233
  }
200
- // TODO(yaroslav): remove collection without spaceRootId after release (production<->staging interop)
201
- void this._automergeHost.updateLocalCollectionState(deriveCollectionIdFromSpaceId(e.spaceId), e.documentIds);
202
234
  void this._automergeHost.updateLocalCollectionState(
203
235
  deriveCollectionIdFromSpaceId(e.spaceId, e.spaceRootId),
204
236
  e.documentIds,
@@ -206,13 +238,14 @@ export class EchoHost extends Resource {
206
238
  });
207
239
  this._automergeHost.documentsSaved.on(this._ctx, () => {
208
240
  this._queryService.invalidateQueries();
241
+ this._updateIndexes.schedule();
209
242
  });
243
+ this._updateIndexes.schedule();
210
244
  }
211
245
 
212
246
  protected override async _close(ctx: Context): Promise<void> {
213
247
  await this._queryService.close(ctx);
214
248
  await this._spaceStateManager.close(ctx);
215
- await this._indexer.close(ctx);
216
249
  await this._automergeHost.close();
217
250
  }
218
251
 
@@ -220,14 +253,16 @@ export class EchoHost extends Resource {
220
253
  * Flush all pending writes to the underlying storage.
221
254
  */
222
255
  async flush(): Promise<void> {
223
- await this._automergeHost.repo.flush();
256
+ await this._automergeHost.flush();
224
257
  }
225
258
 
226
259
  /**
227
260
  * Perform any pending index updates.
228
261
  */
229
262
  async updateIndexes(): Promise<void> {
230
- await this._indexer.updateIndexes();
263
+ do {
264
+ await this._updateIndexes.runBlocking();
265
+ } while (!this._indexesUpToDate);
231
266
  }
232
267
 
233
268
  /**
@@ -244,7 +279,7 @@ export class EchoHost extends Resource {
244
279
  /**
245
280
  * Create new persisted document.
246
281
  */
247
- createDoc<T>(initialValue?: T, opts?: CreateDocOptions): DocHandle<T> {
282
+ async createDoc<T>(initialValue?: T, opts?: CreateDocOptions): Promise<DocHandle<T>> {
248
283
  return this._automergeHost.createDoc(initialValue, opts);
249
284
  }
250
285
 
@@ -255,7 +290,7 @@ export class EchoHost extends Resource {
255
290
  invariant(this._lifecycleState === LifecycleState.OPEN);
256
291
  const spaceId = await createIdFromSpaceKey(spaceKey);
257
292
 
258
- const automergeRoot = this._automergeHost.createDoc<DatabaseDirectory>({
293
+ const automergeRoot = await this._automergeHost.createDoc<DatabaseDirectory>({
259
294
  version: SpaceDocVersion.CURRENT,
260
295
  access: { spaceKey: spaceKey.toHex() },
261
296
 
@@ -272,30 +307,105 @@ export class EchoHost extends Resource {
272
307
  // TODO(dmaretskyi): Change to document id.
273
308
  async openSpaceRoot(spaceId: SpaceId, automergeUrl: AutomergeUrl): Promise<DatabaseRoot> {
274
309
  invariant(this._lifecycleState === LifecycleState.OPEN);
275
- const handle = await this._automergeHost.repo.find<DatabaseDirectory>(automergeUrl, FIND_PARAMS);
310
+ const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), automergeUrl, {
311
+ fetchFromNetwork: true,
312
+ });
276
313
  await handle.whenReady();
277
314
 
278
315
  return this._spaceStateManager.assignRootToSpace(spaceId, handle);
279
316
  }
280
317
 
281
318
  // TODO(dmaretskyi): Change to document id.
282
- async closeSpaceRoot(automergeUrl: AutomergeUrl): Promise<void> {
319
+ async closeSpaceRoot(_automergeUrl: AutomergeUrl): Promise<void> {
283
320
  todo();
284
321
  }
285
322
 
286
323
  /**
287
324
  * Install data replicator.
288
325
  */
289
- async addReplicator(replicator: EchoReplicator): Promise<void> {
326
+ async addReplicator(replicator: AutomergeReplicator): Promise<void> {
290
327
  await this._automergeHost.addReplicator(replicator);
291
328
  }
292
329
 
293
330
  /**
294
331
  * Remove data replicator.
295
332
  */
296
- async removeReplicator(replicator: EchoReplicator): Promise<void> {
333
+ async removeReplicator(replicator: AutomergeReplicator): Promise<void> {
297
334
  await this._automergeHost.removeReplicator(replicator);
298
335
  }
336
+
337
+ /**
338
+ * Run collection sync for the given space.
339
+ * Does not wait for the sync to complete.
340
+ */
341
+ async runCollectionSync(spaceId: SpaceId) {
342
+ const root = this._spaceStateManager.getRootBySpaceId(spaceId);
343
+ if (!root) {
344
+ throw new Error(`Space not found: ${spaceId}`);
345
+ }
346
+ this._automergeHost.refreshCollection(deriveCollectionIdFromSpaceId(spaceId, root.documentId));
347
+ }
348
+
349
+ private _runUpdateIndexes = async (): Promise<void> => {
350
+ let totalUpdated = 0;
351
+ let totalDone = true;
352
+
353
+ {
354
+ performance.mark('indexEngine.update.automerge:start');
355
+ const { updated, done } = await this._indexEngine
356
+ .update(this._automergeDataSource, { spaceId: null, limit: 50 })
357
+ .pipe(RuntimeProvider.runPromise(this._runtime));
358
+ totalUpdated += updated;
359
+ totalDone &&= done;
360
+ performance.measure('Index Automerge', {
361
+ start: 'indexEngine.update.automerge:start',
362
+ detail: {
363
+ devtools: {
364
+ dataType: 'track-entry',
365
+ track: 'Indexing',
366
+ trackGroup: 'ECHO', // Group related tracks together
367
+ color: 'tertiary-dark',
368
+ properties: [['count', updated]],
369
+ },
370
+ },
371
+ });
372
+ }
373
+
374
+ if (this._queueDataSource) {
375
+ performance.mark('indexEngine.update.queue:start');
376
+ const { updated, done } = await this._indexEngine
377
+ .update(this._queueDataSource, { spaceId: null, limit: 50 })
378
+ .pipe(RuntimeProvider.runPromise(this._runtime));
379
+ totalUpdated += updated;
380
+ totalDone &&= done;
381
+ performance.measure('Index Queues', {
382
+ start: 'indexEngine.update.queue:start',
383
+ detail: {
384
+ devtools: {
385
+ dataType: 'track-entry',
386
+ track: 'Indexing',
387
+ trackGroup: 'ECHO',
388
+ color: 'tertiary-dark',
389
+ properties: [['count', updated]],
390
+ },
391
+ },
392
+ });
393
+ }
394
+
395
+ log.verbose('indexEngine update completed', { updated: totalUpdated, done: totalDone });
396
+ await sleep(1);
397
+ if (!totalDone) {
398
+ this._indexesUpToDate = false;
399
+ this._updateIndexes!.schedule();
400
+ } else {
401
+ this._indexesUpToDate = true;
402
+ }
403
+
404
+ // Invalidate queries after index update completes so queries can see newly indexed data.
405
+ if (totalUpdated > 0) {
406
+ this._queryService.invalidateQueries();
407
+ }
408
+ };
299
409
  }
300
410
 
301
411
  export type { EchoDataStats };
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './automerge-data-source';
5
6
  export * from './data-service';
6
7
  export * from './documents-synchronizer';
7
8
  export * from './echo-host';