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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-VIHYDCCC.mjs} +313 -709
  2. package/dist/lib/neutral/chunk-VIHYDCCC.mjs.map +7 -0
  3. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs +247 -0
  4. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/neutral/index.mjs +7065 -0
  7. package/dist/lib/neutral/index.mjs.map +7 -0
  8. package/dist/lib/neutral/meta.json +1 -0
  9. package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  12. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  13. package/dist/types/src/automerge/automerge-host.d.ts +99 -54
  14. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  15. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  16. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  17. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  18. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  19. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  20. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  21. package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
  22. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  23. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  24. package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
  25. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-replicator.d.ts +38 -15
  27. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  28. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  29. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  30. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  31. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  32. package/dist/types/src/automerge/index.d.ts +2 -1
  33. package/dist/types/src/automerge/index.d.ts.map +1 -1
  34. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +9 -5
  35. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  36. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  37. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  38. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  39. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  40. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  41. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  42. package/dist/types/src/common/codec.d.ts +1 -1
  43. package/dist/types/src/common/codec.d.ts.map +1 -1
  44. package/dist/types/src/common/feeds.d.ts.map +1 -1
  45. package/dist/types/src/common/space-id.d.ts.map +1 -1
  46. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  47. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  48. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  49. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  50. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  51. package/dist/types/src/db-host/data-service.d.ts +8 -7
  52. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  53. package/dist/types/src/db-host/database-root.d.ts +9 -3
  54. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  55. package/dist/types/src/db-host/documents-synchronizer.d.ts +9 -8
  56. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  57. package/dist/types/src/db-host/echo-host.d.ts +62 -27
  58. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  59. package/dist/types/src/db-host/index.d.ts +2 -0
  60. package/dist/types/src/db-host/index.d.ts.map +1 -1
  61. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  62. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  63. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  64. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  65. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  66. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  67. package/dist/types/src/db-host/query-service.d.ts +19 -11
  68. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  69. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  70. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  71. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  72. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  73. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  74. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  75. package/dist/types/src/db-host/stub.d.ts +11 -0
  76. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  77. package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
  78. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  79. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  80. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  81. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  82. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  83. package/dist/types/src/edge/index.d.ts +1 -0
  84. package/dist/types/src/edge/index.d.ts.map +1 -1
  85. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  86. package/dist/types/src/filter/filter-match.d.ts +4 -1
  87. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  88. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  89. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  90. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  91. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  92. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  93. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  94. package/dist/types/src/query/errors.d.ts +22 -10
  95. package/dist/types/src/query/errors.d.ts.map +1 -1
  96. package/dist/types/src/query/plan.d.ts +66 -8
  97. package/dist/types/src/query/plan.d.ts.map +1 -1
  98. package/dist/types/src/query/query-executor.d.ts +66 -11
  99. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  100. package/dist/types/src/query/query-planner.d.ts +12 -1
  101. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  102. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  103. package/dist/types/src/space/auth.d.ts +3 -3
  104. package/dist/types/src/space/auth.d.ts.map +1 -1
  105. package/dist/types/src/space/control-pipeline.d.ts +5 -4
  106. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  107. package/dist/types/src/space/space-manager.d.ts +8 -8
  108. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  109. package/dist/types/src/space/space-protocol.d.ts +10 -9
  110. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  111. package/dist/types/src/space/space.d.ts +6 -6
  112. package/dist/types/src/space/space.d.ts.map +1 -1
  113. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  114. package/dist/types/src/testing/index.d.ts +0 -2
  115. package/dist/types/src/testing/index.d.ts.map +1 -1
  116. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  117. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  118. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  119. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  120. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  121. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  122. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  123. package/dist/types/src/util.d.ts +1 -1
  124. package/dist/types/src/util.d.ts.map +1 -1
  125. package/dist/types/tsconfig.tsbuildinfo +1 -1
  126. package/package.json +56 -54
  127. package/src/automerge/automerge-host-subduction.test.ts +247 -0
  128. package/src/automerge/automerge-host.test.ts +65 -39
  129. package/src/automerge/automerge-host.ts +589 -206
  130. package/src/automerge/automerge-repo-subduction.test.ts +1049 -0
  131. package/src/automerge/automerge-repo.test.ts +349 -91
  132. package/src/automerge/automerge-subduction.test.ts +330 -0
  133. package/src/automerge/collection-synchronizer.test.ts +53 -17
  134. package/src/automerge/collection-synchronizer.ts +119 -36
  135. package/src/automerge/echo-data-monitor.ts +1 -1
  136. package/src/automerge/echo-network-adapter.test.ts +5 -4
  137. package/src/automerge/echo-network-adapter.ts +74 -30
  138. package/src/automerge/echo-replicator.ts +41 -15
  139. package/src/automerge/handle-state.ts +62 -0
  140. package/src/automerge/heads-store.ts +42 -3
  141. package/src/automerge/index.ts +2 -1
  142. package/src/automerge/leveldb-storage-adapter.ts +40 -11
  143. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  144. package/src/automerge/mesh-echo-replicator.ts +15 -11
  145. package/src/automerge/storage-adapter.test.ts +1 -1
  146. package/src/common/space-id.ts +1 -1
  147. package/src/db-host/automerge-data-source.test.ts +299 -0
  148. package/src/db-host/automerge-data-source.ts +187 -0
  149. package/src/db-host/data-service.ts +25 -26
  150. package/src/db-host/database-root.ts +20 -8
  151. package/src/db-host/documents-synchronizer.test.ts +143 -14
  152. package/src/db-host/documents-synchronizer.ts +72 -55
  153. package/src/db-host/echo-host.ts +348 -96
  154. package/src/db-host/index.ts +2 -0
  155. package/src/db-host/invalidation-hint.ts +50 -0
  156. package/src/db-host/local-queue-service.ts +127 -0
  157. package/src/db-host/query-invalidation.test.ts +270 -0
  158. package/src/db-host/query-service.ts +111 -106
  159. package/src/db-host/queue-data-source.ts +160 -0
  160. package/src/db-host/queue-service.test.ts +145 -0
  161. package/src/db-host/space-state-manager.ts +12 -10
  162. package/src/db-host/stub.ts +33 -0
  163. package/src/edge/echo-edge-replicator.test.ts +12 -10
  164. package/src/edge/echo-edge-replicator.ts +144 -58
  165. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  166. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  167. package/src/edge/index.ts +1 -0
  168. package/src/filter/filter-match.test.ts +68 -32
  169. package/src/filter/filter-match.ts +184 -20
  170. package/src/metadata/metadata-store.ts +11 -8
  171. package/src/pipeline/pipeline-stress.test.ts +1 -1
  172. package/src/pipeline/pipeline.test.ts +2 -2
  173. package/src/pipeline/pipeline.ts +9 -7
  174. package/src/query/errors.ts +3 -1
  175. package/src/query/plan.ts +82 -8
  176. package/src/query/query-executor.ts +1199 -164
  177. package/src/query/query-planner.test.ts +993 -81
  178. package/src/query/query-planner.ts +467 -27
  179. package/src/space/admission-discovery-extension.ts +3 -3
  180. package/src/space/auth.ts +6 -6
  181. package/src/space/control-pipeline.test.ts +5 -4
  182. package/src/space/control-pipeline.ts +16 -13
  183. package/src/space/space-manager.browser.test.ts +1 -1
  184. package/src/space/space-manager.ts +16 -20
  185. package/src/space/space-protocol.browser.test.ts +15 -14
  186. package/src/space/space-protocol.test.ts +15 -14
  187. package/src/space/space-protocol.ts +22 -18
  188. package/src/space/space.test.ts +3 -3
  189. package/src/space/space.ts +11 -11
  190. package/src/testing/index.ts +0 -2
  191. package/src/testing/test-agent-builder.ts +3 -3
  192. package/src/testing/test-network-adapter.ts +24 -3
  193. package/src/testing/test-replicator.ts +19 -20
  194. package/src/util.ts +7 -3
  195. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  196. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  197. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  198. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  199. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  200. package/dist/lib/browser/index.mjs +0 -4743
  201. package/dist/lib/browser/index.mjs.map +0 -7
  202. package/dist/lib/browser/meta.json +0 -1
  203. package/dist/lib/browser/testing/index.mjs.map +0 -7
  204. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  205. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  206. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  207. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  208. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  209. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  210. package/dist/lib/node/filter/index.cjs +0 -32
  211. package/dist/lib/node/filter/index.cjs.map +0 -7
  212. package/dist/lib/node/index.cjs +0 -4699
  213. package/dist/lib/node/index.cjs.map +0 -7
  214. package/dist/lib/node/meta.json +0 -1
  215. package/dist/lib/node/testing/index.cjs +0 -753
  216. package/dist/lib/node/testing/index.cjs.map +0 -7
  217. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  218. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  219. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  220. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  221. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  222. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  223. package/dist/lib/node-esm/filter/index.mjs +0 -11
  224. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  225. package/dist/lib/node-esm/index.mjs +0 -4743
  226. package/dist/lib/node-esm/index.mjs.map +0 -7
  227. package/dist/lib/node-esm/meta.json +0 -1
  228. package/dist/lib/node-esm/testing/index.mjs +0 -742
  229. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  230. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  231. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  232. package/dist/types/src/testing/test-data.d.ts +0 -18
  233. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  234. package/dist/types/src/testing/test-schema.d.ts +0 -37
  235. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  236. package/src/db-host/documents-iterator.ts +0 -74
  237. package/src/testing/test-data.ts +0 -127
  238. package/src/testing/test-schema.ts +0 -53
  239. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -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';
7
- import { Schema } from 'effect';
5
+ import type * as SqlClient from '@effect/sql/SqlClient';
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,
@@ -23,12 +21,14 @@ import {
23
21
  } from '@dxos/protocols/proto/dxos/echo/query';
24
22
  import { trace } from '@dxos/tracing';
25
23
 
26
- import type { SpaceStateManager } from './space-state-manager';
27
24
  import { type AutomergeHost } from '../automerge';
28
25
  import { QueryExecutor } from '../query';
26
+ import { type InvalidationHint, mergeHints } from './invalidation-hint';
27
+ import type { SpaceStateManager } from './space-state-manager';
29
28
 
30
- export type QueryServiceParams = {
31
- indexer: Indexer;
29
+ export type QueryServiceProps = {
30
+ indexEngine: IndexEngine;
31
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
32
32
  automergeHost: AutomergeHost;
33
33
  spaceStateManager: SpaceStateManager;
34
34
  };
@@ -53,14 +53,39 @@ type ActiveQuery = {
53
53
  close: () => Promise<void>;
54
54
  };
55
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
+
56
66
  @trace.resource()
57
67
  export class QueryServiceImpl extends Resource implements QueryService {
68
+ // TODO(dmaretskyi): We need to implement query deduping. Idle composer has 80 queries with only 10 being unique.
58
69
  private readonly _queries = new Set<ActiveQuery>();
59
70
 
60
71
  private _updateQueries!: DeferredTask;
61
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
+
62
87
  // TODO(burdon): OK for options, but not params. Pass separately and type readonly here.
63
- constructor(private readonly _params: QueryServiceParams) {
88
+ constructor(private readonly _params: QueryServiceProps) {
64
89
  super();
65
90
 
66
91
  trace.diagnostic({
@@ -76,12 +101,16 @@ export class QueryServiceImpl extends Resource implements QueryService {
76
101
  });
77
102
  },
78
103
  });
104
+
105
+ trace.diagnostic<QueryInvalidationStats>({
106
+ id: 'query-invalidation',
107
+ name: 'Query Invalidation',
108
+ fetch: async () => ({ ...this.#stats }),
109
+ });
79
110
  }
80
111
 
81
112
  override async _open(): Promise<void> {
82
- this._params.indexer.updated.on(this._ctx, () => this.invalidateQueries());
83
-
84
- this._updateQueries = new DeferredTask(this._ctx, this._executeQueries.bind(this));
113
+ this._updateQueries = new DeferredTask(this._ctx, () => this._executeQueries(this._ctx));
85
114
  }
86
115
 
87
116
  @synchronized
@@ -90,8 +119,19 @@ export class QueryServiceImpl extends Resource implements QueryService {
90
119
  await Promise.all(Array.from(this._queries).map((query) => query.close()));
91
120
  }
92
121
 
93
- async setConfig(config: IndexConfig): Promise<void> {
94
- 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');
95
135
  }
96
136
 
97
137
  execQuery(request: QueryRequest): Stream<QueryResponse> {
@@ -107,32 +147,20 @@ export class QueryServiceImpl extends Resource implements QueryService {
107
147
  }
108
148
 
109
149
  /**
110
- * 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).
111
152
  */
112
- async reindex(): Promise<void> {
113
- log('Reindexing all documents...');
114
- const iterator = createDocumentsIterator(this._params.automergeHost);
115
- const ids: IdToHeads = new Map();
116
- for await (const documents of iterator()) {
117
- for (const { id, heads } of documents) {
118
- ids.set(id, heads);
119
- }
120
- if (ids.size % 100 === 0) {
121
- 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;
122
162
  }
123
163
  }
124
-
125
- log('Marking all documents as dirty...', { count: ids.size });
126
- await this._params.indexer.reindex(ids);
127
- }
128
-
129
- /**
130
- * Schedule re-execution of all queries.
131
- */
132
- invalidateQueries() {
133
- for (const query of this._queries) {
134
- query.dirty = true;
135
- }
136
164
  this._updateQueries.schedule();
137
165
  }
138
166
 
@@ -146,7 +174,8 @@ export class QueryServiceImpl extends Resource implements QueryService {
146
174
  const parsedQuery = QueryAST.Query.pipe(Schema.decodeUnknownSync)(JSON.parse(request.query));
147
175
  const queryEntry: ActiveQuery = {
148
176
  executor: new QueryExecutor({
149
- indexer: this._params.indexer,
177
+ indexEngine: this._params.indexEngine,
178
+ runtime: this._params.runtime,
150
179
  automergeHost: this._params.automergeHost,
151
180
  queryId: request.queryId ?? raise(new Error('query id required')),
152
181
  query: parsedQuery,
@@ -173,17 +202,39 @@ export class QueryServiceImpl extends Resource implements QueryService {
173
202
  return queryEntry;
174
203
  }
175
204
 
176
- @trace.span({ showInBrowserTimeline: true })
177
- private async _executeQueries() {
178
- // 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
+
179
229
  const begin = performance.now();
180
- let count = 0;
230
+ let dirtyCount = 0;
231
+ const activeCount = this._queries.size;
181
232
  await Promise.all(
182
233
  Array.from(this._queries).map(async (query) => {
183
234
  if (!query.dirty || !query.open) {
184
235
  return;
185
236
  }
186
- count++;
237
+ dirtyCount++;
187
238
 
188
239
  try {
189
240
  const { changed } = await query.executor.execQuery();
@@ -193,68 +244,22 @@ export class QueryServiceImpl extends Resource implements QueryService {
193
244
  query.sendResults(query.executor.getResults());
194
245
  }
195
246
  } catch (err) {
196
- 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);
197
252
  }
198
253
  }),
199
254
  );
200
- log.verbose('executed queries', { count, duration: performance.now() - begin });
201
- }
202
- }
203
-
204
- /**
205
- * Factory for `getAllDocuments` iterator.
206
- */
207
- // TODO(dmaretskyi): Get roots from echo-host.
208
- const createDocumentsIterator = (automergeHost: AutomergeHost) =>
209
- /**
210
- * Recursively get all object data blobs from loaded documents from Automerge Repo.
211
- */
212
- // TODO(mykola): Unload automerge handles after usage.
213
- async function* getAllDocuments(): AsyncGenerator<ObjectSnapshot[], void, void> {
214
- /** visited automerge handles */
215
- const visited = new Set<string>();
216
-
217
- async function* getObjectsFromHandle(handle: DocHandle<DatabaseDirectory>): AsyncGenerator<ObjectSnapshot[]> {
218
- if (visited.has(handle.documentId) || !handle.isReady()) {
219
- return;
220
- }
221
-
222
- const doc = handle.doc()!;
223
- const spaceKey = DatabaseDirectory.getSpaceKey(doc) ?? undefined;
224
- if (doc.objects) {
225
- yield Object.entries(doc.objects as { [key: string]: any }).map(([objectId, object]) => {
226
- return {
227
- id: objectPointerCodec.encode({ documentId: handle.documentId, objectId, spaceKey }),
228
- object,
229
- heads: getHeads(doc),
230
- };
231
- });
232
- }
233
-
234
- if (doc.links) {
235
- for (const id of Object.values(doc.links as { [echoId: string]: string })) {
236
- const urlString = id.toString();
237
- if (visited.has(urlString)) {
238
- continue;
239
- }
240
- const linkHandle = await automergeHost.loadDoc<DatabaseDirectory>(Context.default(), urlString as DocumentId);
241
- for await (const result of getObjectsFromHandle(linkHandle)) {
242
- yield result;
243
- }
244
- }
245
- }
246
255
 
247
- visited.add(handle.documentId);
248
- }
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;
249
262
 
250
- // TODO(mykola): Use list of roots instead of iterating over all handles.
251
- for (const handle of Object.values(automergeHost.repo.handles)) {
252
- if (visited.has(handle.documentId)) {
253
- continue;
254
- }
255
- for await (const result of getObjectsFromHandle(handle)) {
256
- yield result;
257
- }
258
- visited.add(handle.documentId);
259
- }
260
- };
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,11 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { interpretAsDocumentId, type DocHandle, type DocumentId } 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
- import { Resource, Context, LifecycleState } 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 { type SpaceId } from '@dxos/keys';
@@ -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 () => {