@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
@@ -2,19 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { getHeads } from '@automerge/automerge';
6
- import { type DocHandle, type DocumentId } from '@automerge/automerge-repo';
5
+ import type * as SqlClient from '@effect/sql/SqlClient';
7
6
  import * as Schema from 'effect/Schema';
8
7
 
9
8
  import { DeferredTask, scheduleMicroTask, synchronized } from '@dxos/async';
10
9
  import { Stream } from '@dxos/codec-protobuf/stream';
11
- import { Context, Resource } from '@dxos/context';
10
+ import { type Context, Resource } from '@dxos/context';
12
11
  import { raise } from '@dxos/debug';
13
- import { DatabaseDirectory, QueryAST } from '@dxos/echo-protocol';
14
- import { type IdToHeads, type Indexer, type ObjectSnapshot } from '@dxos/indexing';
12
+ import { QueryAST } from '@dxos/echo-protocol';
13
+ import { type RuntimeProvider } from '@dxos/effect';
14
+ import { type IndexEngine } from '@dxos/index-core';
15
15
  import { log } from '@dxos/log';
16
- import { objectPointerCodec } from '@dxos/protocols';
17
- import { type IndexConfig } from '@dxos/protocols/proto/dxos/echo/indexing';
18
16
  import {
19
17
  type QueryRequest,
20
18
  type QueryResponse,
@@ -25,11 +23,12 @@ import { trace } from '@dxos/tracing';
25
23
 
26
24
  import { type AutomergeHost } from '../automerge';
27
25
  import { QueryExecutor } from '../query';
28
-
26
+ import { type InvalidationHint, mergeHints } from './invalidation-hint';
29
27
  import type { SpaceStateManager } from './space-state-manager';
30
28
 
31
- export type QueryServiceParams = {
32
- indexer: Indexer;
29
+ export type QueryServiceProps = {
30
+ indexEngine: IndexEngine;
31
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
33
32
  automergeHost: AutomergeHost;
34
33
  spaceStateManager: SpaceStateManager;
35
34
  };
@@ -54,6 +53,16 @@ type ActiveQuery = {
54
53
  close: () => Promise<void>;
55
54
  };
56
55
 
56
+ type QueryInvalidationStats = {
57
+ totalInvalidations: number;
58
+ catchAllInvalidations: number;
59
+ hintedInvalidations: number;
60
+ totalDirtyQueriesExecuted: number;
61
+ totalExecutionBatches: number;
62
+ averageDirtyPerBatch: number;
63
+ averageQueriesActive: number;
64
+ };
65
+
57
66
  @trace.resource()
58
67
  export class QueryServiceImpl extends Resource implements QueryService {
59
68
  // TODO(dmaretskyi): We need to implement query deduping. Idle composer has 80 queries with only 10 being unique.
@@ -61,8 +70,22 @@ export class QueryServiceImpl extends Resource implements QueryService {
61
70
 
62
71
  private _updateQueries!: DeferredTask;
63
72
 
73
+ // 'all' = catch-all; null = no pending hint.
74
+ #pendingHint: InvalidationHint | 'all' | null = null;
75
+
76
+ // Diagnostic counters.
77
+ #stats: QueryInvalidationStats = {
78
+ totalInvalidations: 0,
79
+ catchAllInvalidations: 0,
80
+ hintedInvalidations: 0,
81
+ totalDirtyQueriesExecuted: 0,
82
+ totalExecutionBatches: 0,
83
+ averageDirtyPerBatch: 0,
84
+ averageQueriesActive: 0,
85
+ };
86
+
64
87
  // TODO(burdon): OK for options, but not params. Pass separately and type readonly here.
65
- constructor(private readonly _params: QueryServiceParams) {
88
+ constructor(private readonly _params: QueryServiceProps) {
66
89
  super();
67
90
 
68
91
  trace.diagnostic({
@@ -78,12 +101,16 @@ export class QueryServiceImpl extends Resource implements QueryService {
78
101
  });
79
102
  },
80
103
  });
104
+
105
+ trace.diagnostic<QueryInvalidationStats>({
106
+ id: 'query-invalidation',
107
+ name: 'Query Invalidation',
108
+ fetch: async () => ({ ...this.#stats }),
109
+ });
81
110
  }
82
111
 
83
112
  override async _open(): Promise<void> {
84
- this._params.indexer.updated.on(this._ctx, () => this.invalidateQueries());
85
-
86
- this._updateQueries = new DeferredTask(this._ctx, this._executeQueries.bind(this));
113
+ this._updateQueries = new DeferredTask(this._ctx, () => this._executeQueries(this._ctx));
87
114
  }
88
115
 
89
116
  @synchronized
@@ -92,18 +119,23 @@ export class QueryServiceImpl extends Resource implements QueryService {
92
119
  await Promise.all(Array.from(this._queries).map((query) => query.close()));
93
120
  }
94
121
 
95
- async setConfig(config: IndexConfig): Promise<void> {
96
- await this._params.indexer.setConfig(config);
122
+ /**
123
+ * @deprecated No longer needed with SQL-based indexing.
124
+ */
125
+ async setConfig(): Promise<void> {
126
+ // No-op: SQL indexer doesn't need explicit configuration.
127
+ }
128
+
129
+ /**
130
+ * @deprecated No longer needed with SQL-based indexing.
131
+ */
132
+ async reindex(): Promise<void> {
133
+ // No-op: SQL indexer handles re-indexing automatically.
134
+ log.warn('reindex() is deprecated and no longer has any effect');
97
135
  }
98
136
 
99
137
  execQuery(request: QueryRequest): Stream<QueryResponse> {
100
138
  return new Stream<QueryResponse>(({ next, close, ctx }) => {
101
- if (this._params.indexer.config?.enabled !== true) {
102
- log.error('indexer is disabled', { config: this._params.indexer.config });
103
- close();
104
- return;
105
- }
106
-
107
139
  const queryEntry = this._createQuery(ctx, request, next, close, close);
108
140
  scheduleMicroTask(ctx, async () => {
109
141
  await queryEntry.executor.open();
@@ -115,32 +147,20 @@ export class QueryServiceImpl extends Resource implements QueryService {
115
147
  }
116
148
 
117
149
  /**
118
- * Re-index all loaded documents.
150
+ * Schedule re-execution of queries, optionally guided by a targeted hint.
151
+ * When called without a hint, all queries are marked dirty (catch-all invalidation).
119
152
  */
120
- async reindex(): Promise<void> {
121
- log('Reindexing all documents...');
122
- const iterator = createDocumentsIterator(this._params.automergeHost);
123
- const ids: IdToHeads = new Map();
124
- for await (const documents of iterator()) {
125
- for (const { id, heads } of documents) {
126
- ids.set(id, heads);
127
- }
128
- if (ids.size % 100 === 0) {
129
- log('Collected documents...', { count: ids.size });
153
+ invalidateQueries(hint?: InvalidationHint): void {
154
+ this.#stats.totalInvalidations++;
155
+ if (!hint) {
156
+ this.#pendingHint = 'all';
157
+ this.#stats.catchAllInvalidations++;
158
+ } else {
159
+ this.#stats.hintedInvalidations++;
160
+ if (this.#pendingHint !== 'all') {
161
+ this.#pendingHint = this.#pendingHint ? mergeHints(this.#pendingHint, hint) : hint;
130
162
  }
131
163
  }
132
-
133
- log('Marking all documents as dirty...', { count: ids.size });
134
- await this._params.indexer.reindex(ids);
135
- }
136
-
137
- /**
138
- * Schedule re-execution of all queries.
139
- */
140
- invalidateQueries() {
141
- for (const query of this._queries) {
142
- query.dirty = true;
143
- }
144
164
  this._updateQueries.schedule();
145
165
  }
146
166
 
@@ -154,7 +174,8 @@ export class QueryServiceImpl extends Resource implements QueryService {
154
174
  const parsedQuery = QueryAST.Query.pipe(Schema.decodeUnknownSync)(JSON.parse(request.query));
155
175
  const queryEntry: ActiveQuery = {
156
176
  executor: new QueryExecutor({
157
- indexer: this._params.indexer,
177
+ indexEngine: this._params.indexEngine,
178
+ runtime: this._params.runtime,
158
179
  automergeHost: this._params.automergeHost,
159
180
  queryId: request.queryId ?? raise(new Error('query id required')),
160
181
  query: parsedQuery,
@@ -181,17 +202,39 @@ export class QueryServiceImpl extends Resource implements QueryService {
181
202
  return queryEntry;
182
203
  }
183
204
 
184
- @trace.span({ showInBrowserTimeline: true })
185
- private async _executeQueries() {
186
- // TODO(dmaretskyi): How do we integrate this tracing info into the tracing API.
205
+ @trace.span({ showInBrowserTimeline: true, showInRemoteTracing: false })
206
+ private async _executeQueries(_ctx: Context) {
207
+ const hint = this.#pendingHint;
208
+ this.#pendingHint = null;
209
+
210
+ // Apply hint to determine which queries need re-execution.
211
+ for (const query of this._queries) {
212
+ if (!query.open) {
213
+ continue;
214
+ }
215
+ if (query.firstResult) {
216
+ // First run is always executed regardless of hint.
217
+ query.dirty = true;
218
+ continue;
219
+ }
220
+ if (hint === 'all') {
221
+ query.dirty = true;
222
+ continue;
223
+ }
224
+ if (hint && query.executor.matchesHint(hint)) {
225
+ query.dirty = true;
226
+ }
227
+ }
228
+
187
229
  const begin = performance.now();
188
- let count = 0;
230
+ let dirtyCount = 0;
231
+ const activeCount = this._queries.size;
189
232
  await Promise.all(
190
233
  Array.from(this._queries).map(async (query) => {
191
234
  if (!query.dirty || !query.open) {
192
235
  return;
193
236
  }
194
- count++;
237
+ dirtyCount++;
195
238
 
196
239
  try {
197
240
  const { changed } = await query.executor.execQuery();
@@ -201,68 +244,22 @@ export class QueryServiceImpl extends Resource implements QueryService {
201
244
  query.sendResults(query.executor.getResults());
202
245
  }
203
246
  } catch (err) {
204
- log.catch(err);
247
+ log.catch(err, {
248
+ queryId: query.executor.queryId,
249
+ query: JSON.stringify(query.executor.query),
250
+ });
251
+ query.onError(err as Error);
205
252
  }
206
253
  }),
207
254
  );
208
- log.verbose('executed queries', { count, duration: performance.now() - begin });
209
- }
210
- }
211
-
212
- /**
213
- * Factory for `getAllDocuments` iterator.
214
- */
215
- // TODO(dmaretskyi): Get roots from echo-host.
216
- const createDocumentsIterator = (automergeHost: AutomergeHost) =>
217
- /**
218
- * Recursively get all object data blobs from loaded documents from Automerge Repo.
219
- */
220
- // TODO(mykola): Unload automerge handles after usage.
221
- async function* getAllDocuments(): AsyncGenerator<ObjectSnapshot[], void, void> {
222
- /** visited automerge handles */
223
- const visited = new Set<string>();
224
-
225
- async function* getObjectsFromHandle(handle: DocHandle<DatabaseDirectory>): AsyncGenerator<ObjectSnapshot[]> {
226
- if (visited.has(handle.documentId) || !handle.isReady()) {
227
- return;
228
- }
229
-
230
- const doc = handle.doc()!;
231
- const spaceKey = DatabaseDirectory.getSpaceKey(doc) ?? undefined;
232
- if (doc.objects) {
233
- yield Object.entries(doc.objects as { [key: string]: any }).map(([objectId, object]) => {
234
- return {
235
- id: objectPointerCodec.encode({ documentId: handle.documentId, objectId, spaceKey }),
236
- object,
237
- heads: getHeads(doc),
238
- };
239
- });
240
- }
241
-
242
- if (doc.links) {
243
- for (const id of Object.values(doc.links as { [echoId: string]: string })) {
244
- const urlString = id.toString();
245
- if (visited.has(urlString)) {
246
- continue;
247
- }
248
- const linkHandle = await automergeHost.loadDoc<DatabaseDirectory>(Context.default(), urlString as DocumentId);
249
- for await (const result of getObjectsFromHandle(linkHandle)) {
250
- yield result;
251
- }
252
- }
253
- }
254
255
 
255
- visited.add(handle.documentId);
256
- }
256
+ this.#stats.totalExecutionBatches++;
257
+ this.#stats.totalDirtyQueriesExecuted += dirtyCount;
258
+ this.#stats.averageDirtyPerBatch = this.#stats.totalDirtyQueriesExecuted / this.#stats.totalExecutionBatches;
259
+ this.#stats.averageQueriesActive =
260
+ (this.#stats.averageQueriesActive * (this.#stats.totalExecutionBatches - 1) + activeCount) /
261
+ this.#stats.totalExecutionBatches;
257
262
 
258
- // TODO(mykola): Use list of roots instead of iterating over all handles.
259
- for (const handle of Object.values(automergeHost.repo.handles)) {
260
- if (visited.has(handle.documentId)) {
261
- continue;
262
- }
263
- for await (const result of getObjectsFromHandle(handle)) {
264
- yield result;
265
- }
266
- visited.add(handle.documentId);
267
- }
268
- };
263
+ log.verbose('executed queries', { dirty: dirtyCount, active: activeCount, duration: performance.now() - begin });
264
+ }
265
+ }
@@ -0,0 +1,160 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import type * as SqlClient from '@effect/sql/SqlClient';
6
+ import * as Effect from 'effect/Effect';
7
+
8
+ import { type Context } from '@dxos/context';
9
+ import { EchoFeedCodec } from '@dxos/echo-protocol';
10
+ import { type ObjectJSON } from '@dxos/echo/internal';
11
+ import { RuntimeProvider } from '@dxos/effect';
12
+ import { type FeedStore } from '@dxos/feed';
13
+ import { type DataSourceCursor, type IndexDataSource, type IndexerObject } from '@dxos/index-core';
14
+ import { failedInvariant } from '@dxos/invariant';
15
+ import type { SpaceId } from '@dxos/keys';
16
+ import { log } from '@dxos/log';
17
+ import { FeedProtocol } from '@dxos/protocols';
18
+
19
+ export type QueueDataSourceOptions = {
20
+ feedStore: FeedStore;
21
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
22
+ /**
23
+ * Callback to get the list of space IDs that should be indexed.
24
+ */
25
+ getSpaceIds: () => SpaceId[];
26
+
27
+ /**
28
+ * Feed namespaces to index.
29
+ * @default [WellKnownNamespaces.data, WellKnownNamespaces.trace]
30
+ */
31
+ feedNamespaces?: string[];
32
+ };
33
+
34
+ export class QueueDataSource implements IndexDataSource {
35
+ readonly sourceName = 'queue';
36
+
37
+ /**
38
+ * Feed namespaces to index.
39
+ */
40
+ private readonly _feedNamespaces: string[];
41
+ private readonly _feedStore: FeedStore;
42
+ private readonly _runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
43
+ private readonly _getSpaceIds: () => SpaceId[];
44
+
45
+ constructor(options: QueueDataSourceOptions) {
46
+ this._feedStore = options.feedStore;
47
+ this._runtime = options.runtime;
48
+ this._getSpaceIds = options.getSpaceIds;
49
+ this._feedNamespaces = options.feedNamespaces ?? [
50
+ FeedProtocol.WellKnownNamespaces.data,
51
+ FeedProtocol.WellKnownNamespaces.trace,
52
+ ];
53
+ }
54
+
55
+ getChangedObjects(
56
+ _ctx: Context,
57
+ cursors: DataSourceCursor[],
58
+ opts?: { limit?: number },
59
+ ): Effect.Effect<{ objects: IndexerObject[]; cursors: DataSourceCursor[] }> {
60
+ // For queue, the cursor is assumed to have:
61
+ // spaceId = set
62
+ // resourceId = null
63
+ // cursor = feed cursor
64
+
65
+ // We also add new cursors from all previously unindexed spaces.
66
+
67
+ return Effect.gen(this, function* () {
68
+ const objects: IndexerObject[] = [];
69
+ const updatedCursors: DataSourceCursor[] = [];
70
+
71
+ // Delete old cursors that did not have feed namespace set.
72
+ let augmentedCursors = [...cursors].filter((c) => c.resourceId !== null);
73
+ for (const spaceId of this._getSpaceIds()) {
74
+ for (const feedNamespace of this._feedNamespaces) {
75
+ if (!augmentedCursors.some((c) => c.spaceId === spaceId && c.resourceId === feedNamespace)) {
76
+ // Add a new cursor for this space starting from the beginning.
77
+ // Empty string cursor means "start from beginning".
78
+ augmentedCursors.push({
79
+ spaceId,
80
+ resourceId: feedNamespace,
81
+ cursor: '',
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ // Limit per call, but we might have multiple spaces.
88
+ // We should distribute limit or just fill up to limit.
89
+ let remainingLimit = opts?.limit ?? Infinity;
90
+
91
+ for (const cursor of augmentedCursors) {
92
+ if (remainingLimit <= 0) {
93
+ // Remaining cursors are not processed in this call.
94
+ updatedCursors.push(cursor);
95
+ continue;
96
+ }
97
+ if (!cursor.spaceId) {
98
+ // Ignore cursors without spaceId.
99
+ updatedCursors.push(cursor);
100
+ continue;
101
+ }
102
+ if (
103
+ FeedProtocol.WellKnownNamespaces.data !== cursor.resourceId &&
104
+ FeedProtocol.WellKnownNamespaces.trace !== cursor.resourceId
105
+ ) {
106
+ // TODO(dmaretskyi): Update/remove this check when adding new feed namespaces.
107
+ log.warn('Ignoring cursor with invaliding feed namespace', { namespace: cursor.resourceId });
108
+ // Ignore cursors with unknown resourceId.
109
+ updatedCursors.push(cursor);
110
+ continue;
111
+ }
112
+
113
+ // Empty string cursor means "start from beginning".
114
+ const currentCursor =
115
+ typeof cursor.cursor === 'string' && cursor.cursor !== ''
116
+ ? FeedProtocol.FeedCursor.make(cursor.cursor)
117
+ : undefined;
118
+
119
+ try {
120
+ const result = yield* this._feedStore.query({
121
+ spaceId: cursor.spaceId,
122
+ feedNamespace: cursor.resourceId,
123
+ cursor: currentCursor,
124
+ limit: remainingLimit,
125
+ });
126
+
127
+ // Process blocks
128
+ for (const block of result.blocks) {
129
+ try {
130
+ const data = EchoFeedCodec.decode(block.data) as ObjectJSON;
131
+
132
+ objects.push({
133
+ spaceId: cursor.spaceId,
134
+ queueId: block.feedId ?? failedInvariant(),
135
+ queueNamespace: cursor.resourceId,
136
+ documentId: null,
137
+ recordId: null,
138
+ data,
139
+ updatedAt: block.timestamp,
140
+ });
141
+ } catch (err) {
142
+ log.warn('Failed to parse block data for indexing', { spaceId: cursor.spaceId, err });
143
+ }
144
+ }
145
+
146
+ remainingLimit -= result.blocks.length;
147
+ updatedCursors.push({
148
+ spaceId: cursor.spaceId,
149
+ resourceId: cursor.resourceId,
150
+ cursor: result.nextCursor,
151
+ });
152
+ } catch (error) {
153
+ log.error('Error querying queue for indexing', { spaceId: cursor.spaceId, error });
154
+ }
155
+ }
156
+
157
+ return { objects, cursors: updatedCursors };
158
+ }).pipe(RuntimeProvider.provide(this._runtime), Effect.withSpan('QueueDataSource.getChangedObjects'), Effect.orDie);
159
+ }
160
+ }
@@ -0,0 +1,145 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as SqliteClient from '@effect/sql-sqlite-node/SqliteClient';
6
+ import type * as SqlClient from '@effect/sql/SqlClient';
7
+ import { describe, expect, it } from '@effect/vitest';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Layer from 'effect/Layer';
10
+
11
+ import { RuntimeProvider } from '@dxos/effect';
12
+ import { FeedStore } from '@dxos/feed';
13
+ import { ObjectId, SpaceId } from '@dxos/keys';
14
+ import { FeedProtocol } from '@dxos/protocols';
15
+ import { SqlTransaction } from '@dxos/sql-sqlite';
16
+
17
+ import { LocalQueueServiceImpl } from './local-queue-service';
18
+
19
+ const TestLayer = SqlTransaction.layer.pipe(
20
+ Layer.provideMerge(
21
+ SqliteClient.layer({
22
+ filename: ':memory:',
23
+ }),
24
+ ),
25
+ );
26
+
27
+ describe('LocalQueueServiceImpl', () => {
28
+ it.effect('should insert and query items', () =>
29
+ Effect.gen(function* () {
30
+ const feedStore = new FeedStore({ localActorId: 'actor-id', assignPositions: true });
31
+ const runtime = yield* RuntimeProvider.currentRuntime<SqlClient.SqlClient | SqlTransaction.SqlTransaction>();
32
+ const service = new LocalQueueServiceImpl(runtime, feedStore);
33
+ yield* feedStore.migrate();
34
+
35
+ const spaceId = SpaceId.random();
36
+ const queueId = ObjectId.random();
37
+ const object1 = { id: 'obj1', data: 'test1' };
38
+ const object2 = { id: 'obj2', data: 'test2' };
39
+
40
+ yield* Effect.promise(() =>
41
+ service.insertIntoQueue({
42
+ subspaceTag: FeedProtocol.WellKnownNamespaces.data,
43
+ spaceId,
44
+ queueId,
45
+ objects: [object1, object2].map((obj) => JSON.stringify(obj)),
46
+ }),
47
+ );
48
+
49
+ const result = yield* Effect.promise(() =>
50
+ service.queryQueue({
51
+ query: { spaceId, queueIds: [queueId] },
52
+ }),
53
+ );
54
+ expect(JSON.parse(result.objects![0])).toMatchObject(object1);
55
+ expect(JSON.parse(result.objects![1])).toMatchObject(object2);
56
+ }).pipe(Effect.provide(TestLayer)),
57
+ );
58
+
59
+ it.effect('should delete items', () =>
60
+ Effect.gen(function* () {
61
+ const runtime = Effect.succeed(yield* Effect.runtime<any>());
62
+ const feedStore = new FeedStore({ localActorId: 'actor-id', assignPositions: true });
63
+ const service = new LocalQueueServiceImpl(runtime, feedStore);
64
+ yield* feedStore.migrate();
65
+
66
+ const spaceId = SpaceId.random();
67
+ const queueId = ObjectId.random();
68
+ const object1Id = ObjectId.random();
69
+ const object1 = { id: object1Id, data: 'test1' };
70
+
71
+ yield* Effect.promise(() =>
72
+ service.insertIntoQueue({
73
+ subspaceTag: FeedProtocol.WellKnownNamespaces.data,
74
+ spaceId,
75
+ queueId,
76
+ objects: [JSON.stringify(object1)],
77
+ }),
78
+ );
79
+ yield* Effect.promise(() =>
80
+ service.deleteFromQueue({
81
+ subspaceTag: FeedProtocol.WellKnownNamespaces.data,
82
+ spaceId,
83
+ queueId,
84
+ objectIds: [object1Id],
85
+ }),
86
+ );
87
+
88
+ const result = yield* Effect.promise(() =>
89
+ service.queryQueue({
90
+ query: { spaceId, queueIds: [queueId] },
91
+ }),
92
+ );
93
+ expect(result.objects).toHaveLength(2);
94
+ expect(JSON.parse(result.objects![1])).toMatchObject({ id: object1Id, '@deleted': true });
95
+ }).pipe(Effect.provide(TestLayer)),
96
+ );
97
+
98
+ it.effect('should support pagination', () =>
99
+ Effect.gen(function* () {
100
+ const runtime = Effect.succeed(yield* Effect.runtime<any>());
101
+ const feedStore = new FeedStore({ localActorId: 'actor-id', assignPositions: true });
102
+ yield* feedStore.migrate();
103
+ const service = new LocalQueueServiceImpl(runtime, feedStore);
104
+ const spaceId = 'space-1' as SpaceId;
105
+ const queueId = ObjectId.random();
106
+
107
+ // Insert 10 items
108
+ const items = Array.from({ length: 10 }, (_, i) => ({ id: `obj${i}`, data: `test${i}` }));
109
+ yield* Effect.promise(() =>
110
+ service.insertIntoQueue({
111
+ subspaceTag: FeedProtocol.WellKnownNamespaces.data,
112
+ spaceId,
113
+ queueId,
114
+ objects: items.map((item) => JSON.stringify(item)),
115
+ }),
116
+ );
117
+
118
+ // Query first 5
119
+ const page1 = yield* Effect.promise(() =>
120
+ service.queryQueue({
121
+ query: { spaceId, queueIds: [queueId], limit: 5 },
122
+ }),
123
+ );
124
+ expect(page1.objects).toHaveLength(5);
125
+ expect(JSON.parse(page1.objects![0])).toMatchObject(items[0]);
126
+ expect(JSON.parse(page1.objects![4])).toMatchObject(items[4]);
127
+ expect(page1.nextCursor).toBeDefined();
128
+
129
+ // Query next 5
130
+ const page2 = yield* Effect.promise(() =>
131
+ service.queryQueue({
132
+ query: {
133
+ spaceId,
134
+ queueIds: [queueId],
135
+ limit: 5,
136
+ after: page1.nextCursor!,
137
+ },
138
+ }),
139
+ );
140
+ expect(page2.objects).toHaveLength(5);
141
+ expect(JSON.parse(page2.objects![0])).toMatchObject(items[5]);
142
+ expect(JSON.parse(page2.objects![4])).toMatchObject(items[9]);
143
+ }).pipe(Effect.provide(TestLayer)),
144
+ );
145
+ });
@@ -2,8 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type DocHandle, type DocumentId, interpretAsDocumentId } from '@automerge/automerge-repo';
6
- import isEqual from 'lodash.isequal';
5
+ import { type DocumentId, type DocumentQuery, interpretAsDocumentId } from '@automerge/automerge-repo';
6
+ import isEqual from 'fast-deep-equal';
7
7
 
8
8
  import { Event, UpdateScheduler } from '@dxos/async';
9
9
  import { Context, LifecycleState, Resource } from '@dxos/context';
@@ -32,6 +32,10 @@ export class SpaceStateManager extends Resource {
32
32
  return this._roots;
33
33
  }
34
34
 
35
+ get spaceIds(): SpaceId[] {
36
+ return Array.from(this._rootBySpace.keys());
37
+ }
38
+
35
39
  getRootByDocumentId(documentId: DocumentId): DatabaseRoot | undefined {
36
40
  return this._roots.get(documentId);
37
41
  }
@@ -49,13 +53,13 @@ export class SpaceStateManager extends Resource {
49
53
  return this._roots.get(documentId);
50
54
  }
51
55
 
52
- async assignRootToSpace(spaceId: SpaceId, handle: DocHandle<DatabaseDirectory>): Promise<DatabaseRoot> {
56
+ async assignRootToSpace(spaceId: SpaceId, query: DocumentQuery<DatabaseDirectory>): Promise<DatabaseRoot> {
53
57
  let root: DatabaseRoot;
54
- if (this._roots.has(handle.documentId)) {
55
- root = this._roots.get(handle.documentId)!;
58
+ if (this._roots.has(query.documentId)) {
59
+ root = this._roots.get(query.documentId)!;
56
60
  } else {
57
- root = new DatabaseRoot(handle);
58
- this._roots.set(handle.documentId, root);
61
+ root = new DatabaseRoot(query);
62
+ this._roots.set(query.documentId, root);
59
63
  }
60
64
 
61
65
  if (this._rootBySpace.get(spaceId) === root.handle.documentId) {
@@ -73,8 +77,6 @@ export class SpaceStateManager extends Resource {
73
77
 
74
78
  this._perRootContext.set(root.handle.documentId, ctx);
75
79
 
76
- await root.handle.whenReady();
77
-
78
80
  const documentListCheckScheduler = new UpdateScheduler(
79
81
  ctx,
80
82
  async () => {