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

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 (154) hide show
  1. package/dist/lib/browser/{chunk-WJJXJTNS.mjs → chunk-FJPXA75J.mjs} +7 -12
  2. package/dist/lib/browser/chunk-FJPXA75J.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-FR6GJ4S4.mjs → chunk-WIQUYO7H.mjs} +39 -29
  4. package/dist/lib/browser/chunk-WIQUYO7H.mjs.map +7 -0
  5. package/dist/lib/browser/filter/index.mjs +1 -2
  6. package/dist/lib/browser/index.mjs +1933 -620
  7. package/dist/lib/browser/index.mjs.map +4 -4
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/testing/index.mjs +28 -201
  10. package/dist/lib/browser/testing/index.mjs.map +4 -4
  11. package/dist/lib/node-esm/{chunk-JQCCVDLT.mjs → chunk-7PL6UK4B.mjs} +39 -29
  12. package/dist/lib/node-esm/chunk-7PL6UK4B.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-LEQ77KAT.mjs → chunk-S5KGVQFZ.mjs} +7 -12
  14. package/dist/lib/node-esm/chunk-S5KGVQFZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/filter/index.mjs +1 -2
  16. package/dist/lib/node-esm/index.mjs +1933 -620
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/testing/index.mjs +28 -201
  20. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  21. package/dist/types/src/automerge/automerge-host.d.ts +36 -9
  22. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  23. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  24. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  25. package/dist/types/src/automerge/collection-synchronizer.d.ts +3 -2
  26. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  27. package/dist/types/src/automerge/echo-network-adapter.d.ts +11 -6
  28. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  29. package/dist/types/src/automerge/echo-replicator.d.ts +9 -6
  30. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  31. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  32. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  33. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +4 -4
  34. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  35. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +7 -7
  36. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  37. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  38. package/dist/types/src/db-host/automerge-data-source.d.ts +34 -0
  39. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  40. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  41. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  42. package/dist/types/src/db-host/data-service.d.ts +4 -3
  43. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  44. package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -1
  45. package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
  46. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  47. package/dist/types/src/db-host/echo-host.d.ts +43 -11
  48. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  49. package/dist/types/src/db-host/index.d.ts +1 -0
  50. package/dist/types/src/db-host/index.d.ts.map +1 -1
  51. package/dist/types/src/db-host/local-queue-service.d.ts +15 -0
  52. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  53. package/dist/types/src/db-host/query-service.d.ts +7 -2
  54. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  55. package/dist/types/src/db-host/queue-data-source.d.ts +28 -0
  56. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  57. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  58. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  59. package/dist/types/src/db-host/space-state-manager.d.ts +1 -0
  60. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  61. package/dist/types/src/db-host/stub.d.ts +10 -0
  62. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  63. package/dist/types/src/edge/echo-edge-replicator.d.ts +2 -2
  64. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  65. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  66. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  67. package/dist/types/src/pipeline/pipeline.d.ts +2 -2
  68. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  69. package/dist/types/src/query/errors.d.ts +11 -15
  70. package/dist/types/src/query/errors.d.ts.map +1 -1
  71. package/dist/types/src/query/plan.d.ts +37 -3
  72. package/dist/types/src/query/plan.d.ts.map +1 -1
  73. package/dist/types/src/query/query-executor.d.ts +39 -3
  74. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  75. package/dist/types/src/query/query-planner.d.ts +6 -0
  76. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  77. package/dist/types/src/space/auth.d.ts +3 -3
  78. package/dist/types/src/space/auth.d.ts.map +1 -1
  79. package/dist/types/src/space/control-pipeline.d.ts +2 -2
  80. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  81. package/dist/types/src/space/space-manager.d.ts +6 -6
  82. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  83. package/dist/types/src/space/space-protocol.d.ts +5 -5
  84. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  85. package/dist/types/src/space/space.d.ts +3 -3
  86. package/dist/types/src/space/space.d.ts.map +1 -1
  87. package/dist/types/src/testing/index.d.ts +0 -2
  88. package/dist/types/src/testing/index.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 +6 -6
  92. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  93. package/dist/types/src/util.d.ts.map +1 -1
  94. package/dist/types/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +48 -38
  96. package/src/automerge/automerge-host.test.ts +37 -14
  97. package/src/automerge/automerge-host.ts +130 -36
  98. package/src/automerge/automerge-repo.test.ts +147 -30
  99. package/src/automerge/collection-synchronizer.test.ts +16 -10
  100. package/src/automerge/collection-synchronizer.ts +41 -11
  101. package/src/automerge/echo-network-adapter.ts +23 -11
  102. package/src/automerge/echo-replicator.ts +10 -6
  103. package/src/automerge/heads-store.ts +14 -2
  104. package/src/automerge/leveldb-storage-adapter.ts +4 -4
  105. package/src/automerge/mesh-echo-replicator-connection.ts +7 -7
  106. package/src/automerge/mesh-echo-replicator.ts +2 -2
  107. package/src/db-host/automerge-data-source.test.ts +294 -0
  108. package/src/db-host/automerge-data-source.ts +144 -0
  109. package/src/db-host/data-service.ts +10 -3
  110. package/src/db-host/documents-iterator.ts +3 -1
  111. package/src/db-host/documents-synchronizer.test.ts +77 -13
  112. package/src/db-host/documents-synchronizer.ts +51 -50
  113. package/src/db-host/echo-host.ts +191 -28
  114. package/src/db-host/index.ts +1 -0
  115. package/src/db-host/local-queue-service.ts +127 -0
  116. package/src/db-host/query-service.ts +17 -4
  117. package/src/db-host/queue-data-source.ts +132 -0
  118. package/src/db-host/queue-service.test.ts +141 -0
  119. package/src/db-host/space-state-manager.ts +4 -0
  120. package/src/db-host/stub.ts +28 -0
  121. package/src/edge/echo-edge-replicator.ts +12 -8
  122. package/src/filter/filter-match.test.ts +31 -25
  123. package/src/filter/filter-match.ts +8 -14
  124. package/src/metadata/metadata-store.ts +5 -2
  125. package/src/pipeline/pipeline.ts +2 -2
  126. package/src/query/errors.ts +2 -2
  127. package/src/query/plan.ts +48 -3
  128. package/src/query/query-executor.ts +612 -131
  129. package/src/query/query-planner.test.ts +509 -35
  130. package/src/query/query-planner.ts +187 -13
  131. package/src/space/admission-discovery-extension.ts +1 -1
  132. package/src/space/auth.ts +6 -6
  133. package/src/space/control-pipeline.ts +2 -2
  134. package/src/space/space-manager.ts +6 -6
  135. package/src/space/space-protocol.ts +11 -11
  136. package/src/space/space.ts +3 -3
  137. package/src/testing/index.ts +0 -2
  138. package/src/testing/test-network-adapter.ts +16 -3
  139. package/src/testing/test-replicator.ts +8 -7
  140. package/src/util.ts +7 -3
  141. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  142. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  143. package/dist/lib/browser/chunk-FR6GJ4S4.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-WJJXJTNS.mjs.map +0 -7
  145. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  146. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  147. package/dist/lib/node-esm/chunk-JQCCVDLT.mjs.map +0 -7
  148. package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
  149. package/dist/types/src/testing/test-data.d.ts +0 -18
  150. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  151. package/dist/types/src/testing/test-schema.d.ts +0 -37
  152. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  153. package/src/testing/test-data.ts +0 -127
  154. package/src/testing/test-schema.ts +0 -53
@@ -3,19 +3,22 @@
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 DocHandle, type DocumentId } 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
 
@@ -44,36 +47,38 @@ export class DocumentsSynchronizer extends Resource {
44
47
  */
45
48
  private _sendUpdatesJob?: UpdateScheduler = undefined;
46
49
 
47
- constructor(private readonly _params: DocumentsSynchronizerParams) {
50
+ constructor(private readonly _params: DocumentsSynchronizerProps) {
48
51
  super();
49
52
  }
50
53
 
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 });
58
- return;
59
- }
60
-
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),
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
+ },
74
76
  );
75
- });
76
- }
77
+ } catch (err) {
78
+ log.catch(err);
79
+ }
80
+ }),
81
+ );
77
82
  }
78
83
 
79
84
  removeDocuments(documentIds: DocumentId[]): void {
@@ -96,11 +101,11 @@ export class DocumentsSynchronizer extends Resource {
96
101
  }
97
102
 
98
103
  async update(updates: DocumentUpdate[]): Promise<void> {
99
- for (const { documentId, mutation, isNew } of updates) {
100
- this._writeMutation(documentId as DocumentId, mutation, isNew);
104
+ for (const { documentId, mutation } of updates) {
105
+ await this._writeMutation(documentId as DocumentId, mutation);
101
106
  }
102
107
  // TODO(mykola): This should not be required.
103
- await this._params.repo.flush(updates.map(({ documentId }) => documentId as DocumentId));
108
+ await this._params.automergeHost.flush({ documentIds: updates.map(({ documentId }) => documentId as DocumentId) });
104
109
  }
105
110
 
106
111
  private _startSync(doc: DocHandle<DatabaseDirectory>) {
@@ -161,25 +166,21 @@ export class DocumentsSynchronizer extends Resource {
161
166
  return mutation;
162
167
  }
163
168
 
164
- private _writeMutation(documentId: DocumentId, mutation: Uint8Array, isNew?: boolean): void {
169
+ private async _writeMutation(documentId: DocumentId, mutation: Uint8Array): Promise<void> {
165
170
  if (this._lifecycleState === LifecycleState.CLOSED) {
166
171
  return;
167
172
  }
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
- }
173
+ log('write mutation', { documentId });
174
+
175
+ const syncState = this._syncStates.get(documentId);
176
+ invariant(syncState, 'Sync state for document not found');
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());
183
184
  }
184
185
  }
185
186
  }
@@ -2,22 +2,24 @@
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 { type Context, LifecycleState, Resource } 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
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';
16
15
  import { IndexMetadataStore, IndexStore, Indexer } from '@dxos/indexing';
17
16
  import { invariant } from '@dxos/invariant';
18
17
  import { type PublicKey, type SpaceId } from '@dxos/keys';
19
18
  import { type LevelDB } from '@dxos/kv-store';
19
+ import { log } from '@dxos/log';
20
+ import type { QueueService } from '@dxos/protocols';
20
21
  import { IndexKind } from '@dxos/protocols/proto/dxos/echo/indexing';
22
+ import type * as SqlTransaction from '@dxos/sql-sqlite/SqlTransaction';
21
23
  import { trace } from '@dxos/tracing';
22
24
 
23
25
  import {
@@ -26,24 +28,27 @@ import {
26
28
  EchoDataMonitor,
27
29
  type EchoDataStats,
28
30
  type EchoReplicator,
29
- FIND_PARAMS,
30
31
  type LoadDocOptions,
31
32
  type PeerIdProvider,
32
33
  type RootDocumentSpaceKeyProvider,
33
34
  deriveCollectionIdFromSpaceId,
34
35
  } from '../automerge';
35
36
 
37
+ import { AutomergeDataSource } from './automerge-data-source';
36
38
  import { DataServiceImpl } from './data-service';
37
39
  import { type DatabaseRoot } from './database-root';
38
40
  import { createSelectedDocumentsIterator } from './documents-iterator';
41
+ import { LocalQueueServiceImpl } from './local-queue-service';
39
42
  import { QueryServiceImpl } from './query-service';
43
+ import { QueueDataSource } from './queue-data-source';
40
44
  import { SpaceStateManager } from './space-state-manager';
45
+ import { QueueServiceStub } from './stub';
41
46
 
42
47
  export interface EchoHostIndexingConfig {
43
48
  /**
44
- * @default true
49
+ * @default false
45
50
  */
46
- fullText: boolean;
51
+ sqlIndex: boolean;
47
52
 
48
53
  /**
49
54
  * @default false
@@ -52,17 +57,30 @@ export interface EchoHostIndexingConfig {
52
57
  }
53
58
 
54
59
  const DEFAULT_INDEXING_CONFIG: EchoHostIndexingConfig = {
55
- // TODO(dmaretskyi): Disabled by default since embedding generation is expensive.
56
- fullText: false,
60
+ // TODO(mykola): Enable by default when SQLite indexer is stable.
61
+ sqlIndex: false,
57
62
  vector: false,
58
63
  };
59
64
 
60
- export type EchoHostParams = {
65
+ export type EchoHostProps = {
61
66
  kv: LevelDB;
62
67
  peerIdProvider?: PeerIdProvider;
63
68
  getSpaceKeyByRootDocumentId?: RootDocumentSpaceKeyProvider;
64
69
 
65
70
  indexing?: Partial<EchoHostIndexingConfig>;
71
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
72
+
73
+ /**
74
+ * Use SQLite-backed local-first durable queues.
75
+ * Otherwise defaults to in-memory impl.
76
+ */
77
+ localQueues?: boolean;
78
+
79
+ /**
80
+ * This peer is allowed to assign positions (global-order) to items appended to the queue.
81
+ * @default false
82
+ */
83
+ assignQueuePositions?: boolean;
66
84
  };
67
85
 
68
86
  /**
@@ -80,10 +98,31 @@ export class EchoHost extends Resource {
80
98
  private readonly _spaceStateManager = new SpaceStateManager();
81
99
  private readonly _echoDataMonitor: EchoDataMonitor;
82
100
 
83
- constructor({ kv, indexing = {}, peerIdProvider, getSpaceKeyByRootDocumentId }: EchoHostParams) {
101
+ private readonly _automergeDataSource: AutomergeDataSource;
102
+ private readonly _indexer2: IndexEngine;
103
+ private readonly _indexConfig: EchoHostIndexingConfig;
104
+ private readonly _runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
105
+ private readonly _feedStore?: FeedStore;
106
+ private readonly _queueDataSource?: QueueDataSource;
107
+
108
+ private _updateIndexes!: DeferredTask;
109
+
110
+ private _queuesService: QueueService;
111
+
112
+ private _indexesUpToDate = false;
113
+
114
+ constructor({
115
+ kv,
116
+ localQueues,
117
+ indexing = {},
118
+ peerIdProvider,
119
+ getSpaceKeyByRootDocumentId,
120
+ runtime,
121
+ assignQueuePositions = false,
122
+ }: EchoHostProps) {
84
123
  super();
85
124
 
86
- const indexingConfig = { ...DEFAULT_INDEXING_CONFIG, ...indexing };
125
+ this._indexConfig = { ...DEFAULT_INDEXING_CONFIG, ...indexing };
87
126
 
88
127
  this._indexMetadataStore = new IndexMetadataStore({ db: kv.sublevel('index-metadata') });
89
128
  this._echoDataMonitor = new EchoDataMonitor();
@@ -95,6 +134,21 @@ export class EchoHost extends Resource {
95
134
  getSpaceKeyByRootDocumentId,
96
135
  });
97
136
 
137
+ this._runtime = runtime;
138
+ this._automergeDataSource = new AutomergeDataSource(this._automergeHost);
139
+
140
+ if (localQueues) {
141
+ this._feedStore = new FeedStore({ assignPositions: assignQueuePositions, localActorId: crypto.randomUUID() });
142
+ this._queueDataSource = new QueueDataSource({
143
+ feedStore: this._feedStore,
144
+ runtime: this._runtime,
145
+ getSpaceIds: () => this._spaceStateManager.spaceIds,
146
+ });
147
+ this._queuesService = new LocalQueueServiceImpl(runtime, this._feedStore);
148
+ } else {
149
+ this._queuesService = new QueueServiceStub();
150
+ }
151
+
98
152
  this._indexer = new Indexer({
99
153
  db: kv,
100
154
  indexStore: new IndexStore({ db: kv.sublevel('index-storage') }),
@@ -109,14 +163,18 @@ export class EchoHost extends Resource {
109
163
  { kind: IndexKind.Kind.SCHEMA_MATCH },
110
164
  { kind: IndexKind.Kind.GRAPH },
111
165
 
112
- ...(indexingConfig.fullText ? [{ kind: IndexKind.Kind.FULL_TEXT }] : []),
113
- ...(indexingConfig.vector ? [{ kind: IndexKind.Kind.VECTOR }] : []),
166
+ ...(this._indexConfig.vector ? [{ kind: IndexKind.Kind.VECTOR }] : []),
114
167
  ],
115
168
  });
116
169
 
170
+ // New index-core Indexer for text queries.
171
+ this._indexer2 = new IndexEngine();
172
+
117
173
  this._queryService = new QueryServiceImpl({
118
174
  automergeHost: this._automergeHost,
119
175
  indexer: this._indexer,
176
+ indexer2: this._indexConfig.sqlIndex ? this._indexer2 : undefined,
177
+ runtime: this._indexConfig.sqlIndex ? this._runtime : undefined,
120
178
  spaceStateManager: this._spaceStateManager,
121
179
  });
122
180
 
@@ -125,6 +183,11 @@ export class EchoHost extends Resource {
125
183
  spaceStateManager: this._spaceStateManager,
126
184
  updateIndexes: async () => {
127
185
  await this._indexer.updateIndexes();
186
+ if (this._indexConfig.sqlIndex) {
187
+ do {
188
+ await this._updateIndexes.runBlocking();
189
+ } while (!this._indexesUpToDate);
190
+ }
128
191
  },
129
192
  });
130
193
 
@@ -177,23 +240,42 @@ export class EchoHost extends Resource {
177
240
  return this._dataService;
178
241
  }
179
242
 
180
- /**
181
- * @deprecated To be abstracted away.
182
- */
183
- get automergeRepo(): Repo {
184
- return this._automergeHost.repo;
243
+ get queuesService(): QueueService {
244
+ return this._queuesService;
185
245
  }
186
246
 
187
247
  get roots(): ReadonlyMap<DocumentId, DatabaseRoot> {
188
248
  return this._spaceStateManager.roots;
189
249
  }
190
250
 
251
+ /**
252
+ * New index-core indexer for text and other queries.
253
+ */
254
+ get indexer2(): IndexEngine {
255
+ return this._indexer2;
256
+ }
257
+
191
258
  protected override async _open(ctx: Context): Promise<void> {
192
259
  await this._automergeHost.open();
193
260
  await this._indexer.open(ctx);
194
261
  await this._queryService.open(ctx);
195
262
  await this._spaceStateManager.open(ctx);
196
263
 
264
+ if (this._feedStore) {
265
+ await RuntimeProvider.runPromise(this._runtime)(this._feedStore.migrate());
266
+ this._feedStore.onNewBlocks.on(this._ctx, () => {
267
+ this._queryService.invalidateQueries();
268
+ if (this._indexConfig.sqlIndex) {
269
+ this._updateIndexes.schedule();
270
+ }
271
+ });
272
+ }
273
+
274
+ if (this._indexConfig.sqlIndex) {
275
+ await RuntimeProvider.runPromise(this._runtime)(this._indexer2.migrate());
276
+ this._updateIndexes = new DeferredTask(this._ctx, this._runUpdateIndexes);
277
+ }
278
+
197
279
  this._spaceStateManager.spaceDocumentListUpdated.on(this._ctx, (e) => {
198
280
  if (e.previousRootId) {
199
281
  void this._automergeHost.clearLocalCollectionState(deriveCollectionIdFromSpaceId(e.spaceId, e.previousRootId));
@@ -205,7 +287,13 @@ export class EchoHost extends Resource {
205
287
  });
206
288
  this._automergeHost.documentsSaved.on(this._ctx, () => {
207
289
  this._queryService.invalidateQueries();
290
+ if (this._indexConfig.sqlIndex) {
291
+ this._updateIndexes.schedule();
292
+ }
208
293
  });
294
+ if (this._indexConfig.sqlIndex) {
295
+ this._updateIndexes.schedule();
296
+ }
209
297
  }
210
298
 
211
299
  protected override async _close(ctx: Context): Promise<void> {
@@ -243,7 +331,7 @@ export class EchoHost extends Resource {
243
331
  /**
244
332
  * Create new persisted document.
245
333
  */
246
- createDoc<T>(initialValue?: T, opts?: CreateDocOptions): DocHandle<T> {
334
+ async createDoc<T>(initialValue?: T, opts?: CreateDocOptions): Promise<DocHandle<T>> {
247
335
  return this._automergeHost.createDoc(initialValue, opts);
248
336
  }
249
337
 
@@ -254,7 +342,7 @@ export class EchoHost extends Resource {
254
342
  invariant(this._lifecycleState === LifecycleState.OPEN);
255
343
  const spaceId = await createIdFromSpaceKey(spaceKey);
256
344
 
257
- const automergeRoot = this._automergeHost.createDoc<DatabaseDirectory>({
345
+ const automergeRoot = await this._automergeHost.createDoc<DatabaseDirectory>({
258
346
  version: SpaceDocVersion.CURRENT,
259
347
  access: { spaceKey: spaceKey.toHex() },
260
348
 
@@ -271,14 +359,16 @@ export class EchoHost extends Resource {
271
359
  // TODO(dmaretskyi): Change to document id.
272
360
  async openSpaceRoot(spaceId: SpaceId, automergeUrl: AutomergeUrl): Promise<DatabaseRoot> {
273
361
  invariant(this._lifecycleState === LifecycleState.OPEN);
274
- const handle = await this._automergeHost.repo.find<DatabaseDirectory>(automergeUrl, FIND_PARAMS);
362
+ const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), automergeUrl, {
363
+ fetchFromNetwork: true,
364
+ });
275
365
  await handle.whenReady();
276
366
 
277
367
  return this._spaceStateManager.assignRootToSpace(spaceId, handle);
278
368
  }
279
369
 
280
370
  // TODO(dmaretskyi): Change to document id.
281
- async closeSpaceRoot(automergeUrl: AutomergeUrl): Promise<void> {
371
+ async closeSpaceRoot(_automergeUrl: AutomergeUrl): Promise<void> {
282
372
  todo();
283
373
  }
284
374
 
@@ -295,6 +385,79 @@ export class EchoHost extends Resource {
295
385
  async removeReplicator(replicator: EchoReplicator): Promise<void> {
296
386
  await this._automergeHost.removeReplicator(replicator);
297
387
  }
388
+
389
+ /**
390
+ * Run collection sync for the given space.
391
+ * Does not wait for the sync to complete.
392
+ */
393
+ async runCollectionSync(spaceId: SpaceId) {
394
+ const root = this._spaceStateManager.getRootBySpaceId(spaceId);
395
+ if (!root) {
396
+ throw new Error(`Space not found: ${spaceId}`);
397
+ }
398
+ this._automergeHost.refreshCollection(deriveCollectionIdFromSpaceId(spaceId, root.documentId));
399
+ }
400
+
401
+ private _runUpdateIndexes = async (): Promise<void> => {
402
+ if (!this._indexer2) {
403
+ // Indexer not initialized yet, skip this update cycle.
404
+ return;
405
+ }
406
+
407
+ let totalUpdated = 0;
408
+ let totalDone = true;
409
+
410
+ {
411
+ performance.mark('indexer2.update.automerge:start');
412
+ const { updated, done } = await this._indexer2
413
+ .update(this._automergeDataSource, { spaceId: null, limit: 50 })
414
+ .pipe(RuntimeProvider.runPromise(this._runtime));
415
+ totalUpdated += updated;
416
+ totalDone &&= done;
417
+ performance.measure('Index Automerge', {
418
+ start: 'indexer2.update.automerge:start',
419
+ detail: {
420
+ devtools: {
421
+ dataType: 'track-entry',
422
+ track: 'Indexing',
423
+ trackGroup: 'ECHO', // Group related tracks together
424
+ color: 'tertiary-dark',
425
+ properties: [['count', updated]],
426
+ },
427
+ },
428
+ });
429
+ }
430
+
431
+ if (this._queueDataSource) {
432
+ performance.mark('indexer2.update.queue:start');
433
+ const { updated, done } = await this._indexer2
434
+ .update(this._queueDataSource, { spaceId: null, limit: 50 })
435
+ .pipe(RuntimeProvider.runPromise(this._runtime));
436
+ totalUpdated += updated;
437
+ totalDone &&= done;
438
+ performance.measure('Index Queues', {
439
+ start: 'indexer2.update.queue:start',
440
+ detail: {
441
+ devtools: {
442
+ dataType: 'track-entry',
443
+ track: 'Indexing',
444
+ trackGroup: 'ECHO',
445
+ color: 'tertiary-dark',
446
+ properties: [['count', updated]],
447
+ },
448
+ },
449
+ });
450
+ }
451
+
452
+ log.verbose('indexer2 update completed', { updated: totalUpdated, done: totalDone });
453
+ await sleep(1);
454
+ if (!totalDone) {
455
+ this._indexesUpToDate = false;
456
+ this._updateIndexes!.schedule();
457
+ } else {
458
+ this._indexesUpToDate = true;
459
+ }
460
+ };
298
461
  }
299
462
 
300
463
  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';
@@ -0,0 +1,127 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type * as SqlClient from '@effect/sql/SqlClient';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Function from 'effect/Function';
8
+
9
+ import { ATTR_META, type ObjectJSON } from '@dxos/echo/internal';
10
+ import type { ForeignKey } from '@dxos/echo-protocol';
11
+ import { RuntimeProvider } from '@dxos/effect';
12
+ import { type Block, type FeedStore } from '@dxos/feed';
13
+ import { invariant } from '@dxos/invariant';
14
+ import { KEY_QUEUE_POSITION } from '@dxos/protocols';
15
+ import {
16
+ type DeleteFromQueueRequest,
17
+ type InsertIntoQueueRequest,
18
+ type QueryQueueRequest,
19
+ type QueueQueryResult,
20
+ type QueueService,
21
+ } from '@dxos/protocols/proto/dxos/client/services';
22
+
23
+ /**
24
+ * Writes queue data to a local FeedStore.
25
+ */
26
+ export class LocalQueueServiceImpl implements QueueService {
27
+ #runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
28
+ #feedStore: FeedStore;
29
+
30
+ constructor(runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>, feedStore: FeedStore) {
31
+ this.#runtime = runtime;
32
+ this.#feedStore = feedStore;
33
+ }
34
+
35
+ queryQueue(request: QueryQueueRequest): Promise<QueueQueryResult> {
36
+ const { query } = request;
37
+ invariant(query, 'query is required');
38
+ const { spaceId, queueIds } = query;
39
+ return RuntimeProvider.runPromise(this.#runtime)(
40
+ Effect.gen(this, function* () {
41
+ const cursor = query.after ? parseInt(query.after) : -1;
42
+ const result = yield* this.#feedStore.query({
43
+ requestId: crypto.randomUUID(),
44
+ spaceId: spaceId!,
45
+ query: { feedIds: queueIds ?? [] },
46
+ position: cursor,
47
+ limit: query.limit,
48
+ });
49
+
50
+ const objects = result.blocks.map((block: Block) => {
51
+ const data = JSON.parse(new TextDecoder().decode(block.data));
52
+ if (block.position !== null) {
53
+ setQueuePosition(data, block.position);
54
+ }
55
+ return data;
56
+ });
57
+
58
+ const lastBlock = result.blocks[result.blocks.length - 1];
59
+ const nextCursor = lastBlock && lastBlock.position != null ? String(lastBlock.position) : null;
60
+
61
+ return Function.identity<QueueQueryResult>({
62
+ objects,
63
+
64
+ // TODO(dmaretskyi): This is wrong, fix later - cursors should come directly from the feed.
65
+ nextCursor: nextCursor?.toString() ?? '',
66
+ prevCursor: '',
67
+ });
68
+ }),
69
+ );
70
+ }
71
+
72
+ insertIntoQueue(request: InsertIntoQueueRequest): Promise<void> {
73
+ const { subspaceTag, spaceId, queueId, objects } = request;
74
+ return RuntimeProvider.runPromise(this.#runtime)(
75
+ Effect.gen(this, function* () {
76
+ const messages = objects!.map((obj) => {
77
+ const data = structuredClone(obj);
78
+ if (data[ATTR_META]?.keys?.find((key: ForeignKey) => key.source === KEY_QUEUE_POSITION)) {
79
+ data[ATTR_META].keys = data[ATTR_META].keys.filter((key: ForeignKey) => key.source !== KEY_QUEUE_POSITION);
80
+ }
81
+
82
+ return {
83
+ spaceId: spaceId,
84
+ feedId: queueId!,
85
+ feedNamespace: subspaceTag,
86
+ data: new TextEncoder().encode(JSON.stringify(obj)),
87
+ };
88
+ });
89
+
90
+ yield* this.#feedStore.appendLocal(messages);
91
+ }),
92
+ );
93
+ }
94
+
95
+ deleteFromQueue(request: DeleteFromQueueRequest): Promise<void> {
96
+ const { subspaceTag, spaceId, queueId, objectIds } = request;
97
+ return RuntimeProvider.runPromise(this.#runtime)(
98
+ Effect.gen(this, function* () {
99
+ const messages = objectIds!.map((id) => ({
100
+ spaceId: spaceId,
101
+ feedId: queueId!,
102
+ feedNamespace: subspaceTag,
103
+ data: new TextEncoder().encode(JSON.stringify({ id, '@deleted': true })),
104
+ }));
105
+
106
+ yield* this.#feedStore.appendLocal(messages);
107
+ }),
108
+ );
109
+ }
110
+ }
111
+
112
+ // TODO(dmaretskyi): Duplicated code.
113
+ const setQueuePosition = (obj: ObjectJSON, position: number) => {
114
+ obj[ATTR_META] ??= { keys: [] };
115
+ obj[ATTR_META].keys ??= [];
116
+ for (let i = 0; i < obj[ATTR_META].keys.length; i++) {
117
+ const key = obj[ATTR_META].keys[i];
118
+ if (key.source === KEY_QUEUE_POSITION) {
119
+ obj[ATTR_META].keys.splice(i, 1);
120
+ i--;
121
+ }
122
+ }
123
+ obj[ATTR_META].keys.push({
124
+ source: KEY_QUEUE_POSITION,
125
+ id: position.toString(),
126
+ });
127
+ };