@dxos/echo-pipeline 0.8.1 → 0.8.2-main.10c050d

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 (182) hide show
  1. package/dist/lib/browser/{chunk-32WDI3LB.mjs → chunk-3XSXS5EX.mjs} +15 -21
  2. package/dist/lib/browser/chunk-3XSXS5EX.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-CGS2ULMK.mjs +11 -0
  4. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-TQJTKNMS.mjs +126 -0
  6. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +7 -0
  7. package/dist/lib/browser/filter/index.mjs +11 -0
  8. package/dist/lib/browser/filter/index.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +1380 -516
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/testing/index.mjs +202 -22
  13. package/dist/lib/browser/testing/index.mjs.map +4 -4
  14. package/dist/lib/node/chunk-HOPOFWAL.cjs +147 -0
  15. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +7 -0
  16. package/dist/lib/node/chunk-Q7SFCCGT.cjs +33 -0
  17. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-TC2PRBEU.cjs → chunk-SG2PL5RH.cjs} +18 -24
  19. package/dist/lib/node/chunk-SG2PL5RH.cjs.map +7 -0
  20. package/dist/lib/node/filter/index.cjs +32 -0
  21. package/dist/lib/node/filter/index.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +1381 -525
  23. package/dist/lib/node/index.cjs.map +4 -4
  24. package/dist/lib/node/meta.json +1 -1
  25. package/dist/lib/node/testing/index.cjs +207 -31
  26. package/dist/lib/node/testing/index.cjs.map +4 -4
  27. package/dist/lib/node-esm/{chunk-UKOLB3LW.mjs → chunk-3BZP75TJ.mjs} +15 -21
  28. package/dist/lib/node-esm/chunk-3BZP75TJ.mjs.map +7 -0
  29. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  30. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  31. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +126 -0
  32. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +7 -0
  33. package/dist/lib/node-esm/filter/index.mjs +11 -0
  34. package/dist/lib/node-esm/filter/index.mjs.map +7 -0
  35. package/dist/lib/node-esm/index.mjs +1380 -516
  36. package/dist/lib/node-esm/index.mjs.map +4 -4
  37. package/dist/lib/node-esm/meta.json +1 -1
  38. package/dist/lib/node-esm/testing/index.mjs +202 -22
  39. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  40. package/dist/types/src/automerge/automerge-host.d.ts +6 -4
  41. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  42. package/dist/types/src/automerge/collection-synchronizer.d.ts +1 -1
  43. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  44. package/dist/types/src/automerge/echo-data-monitor.d.ts +6 -6
  45. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  46. package/dist/types/src/automerge/echo-network-adapter.d.ts +4 -1
  47. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  48. package/dist/types/src/automerge/heads-store.d.ts +2 -2
  49. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  50. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +1 -1
  51. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  52. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  53. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  54. package/dist/types/src/automerge/network-protocol.d.ts +1 -1
  55. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  56. package/dist/types/src/automerge/space-collection.d.ts +1 -1
  57. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  58. package/dist/types/src/common/feeds.d.ts.map +1 -1
  59. package/dist/types/src/common/space-id.d.ts.map +1 -1
  60. package/dist/types/src/db-host/automerge-metrics.d.ts +1 -1
  61. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  62. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  63. package/dist/types/src/db-host/database-root.d.ts +7 -7
  64. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  65. package/dist/types/src/db-host/documents-iterator.d.ts +1 -1
  66. package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -1
  67. package/dist/types/src/db-host/documents-synchronizer.d.ts +4 -4
  68. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  69. package/dist/types/src/db-host/echo-host.d.ts +13 -2
  70. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  71. package/dist/types/src/db-host/index.d.ts +0 -1
  72. package/dist/types/src/db-host/index.d.ts.map +1 -1
  73. package/dist/types/src/db-host/query-service.d.ts +2 -0
  74. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  75. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  76. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  77. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  78. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  79. package/dist/types/src/filter/filter-match.d.ts +13 -0
  80. package/dist/types/src/filter/filter-match.d.ts.map +1 -0
  81. package/dist/types/src/filter/filter-match.test.d.ts +2 -0
  82. package/dist/types/src/filter/filter-match.test.d.ts.map +1 -0
  83. package/dist/types/src/filter/index.d.ts +2 -0
  84. package/dist/types/src/filter/index.d.ts.map +1 -0
  85. package/dist/types/src/index.d.ts +1 -0
  86. package/dist/types/src/index.d.ts.map +1 -1
  87. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  88. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  89. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  90. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  91. package/dist/types/src/query/errors.d.ts +23 -0
  92. package/dist/types/src/query/errors.d.ts.map +1 -0
  93. package/dist/types/src/query/index.d.ts +5 -0
  94. package/dist/types/src/query/index.d.ts.map +1 -0
  95. package/dist/types/src/query/plan.d.ts +132 -0
  96. package/dist/types/src/query/plan.d.ts.map +1 -0
  97. package/dist/types/src/query/query-executor.d.ts +83 -0
  98. package/dist/types/src/query/query-executor.d.ts.map +1 -0
  99. package/dist/types/src/query/query-planner.d.ts +33 -0
  100. package/dist/types/src/query/query-planner.d.ts.map +1 -0
  101. package/dist/types/src/query/query-planner.test.d.ts +2 -0
  102. package/dist/types/src/query/query-planner.test.d.ts.map +1 -0
  103. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  104. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  105. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  106. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  107. package/dist/types/src/space/space.d.ts.map +1 -1
  108. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  109. package/dist/types/src/testing/index.d.ts +2 -0
  110. package/dist/types/src/testing/index.d.ts.map +1 -1
  111. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  112. package/dist/types/src/testing/test-data.d.ts +18 -0
  113. package/dist/types/src/testing/test-data.d.ts.map +1 -0
  114. package/dist/types/src/testing/test-network-adapter.d.ts +3 -2
  115. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  116. package/dist/types/src/testing/test-schema.d.ts +39 -0
  117. package/dist/types/src/testing/test-schema.d.ts.map +1 -0
  118. package/dist/types/src/util.d.ts +2 -2
  119. package/dist/types/src/util.d.ts.map +1 -1
  120. package/dist/types/tsconfig.tsbuildinfo +1 -1
  121. package/package.json +43 -34
  122. package/src/automerge/automerge-host.test.ts +7 -7
  123. package/src/automerge/automerge-host.ts +58 -60
  124. package/src/automerge/automerge-repo.test.ts +65 -65
  125. package/src/automerge/collection-synchronizer.test.ts +1 -1
  126. package/src/automerge/collection-synchronizer.ts +11 -10
  127. package/src/automerge/echo-data-monitor.ts +21 -20
  128. package/src/automerge/echo-network-adapter.test.ts +1 -1
  129. package/src/automerge/echo-network-adapter.ts +25 -18
  130. package/src/automerge/heads-store.ts +4 -3
  131. package/src/automerge/leveldb-storage-adapter.ts +1 -1
  132. package/src/automerge/mesh-echo-replicator-connection.ts +6 -5
  133. package/src/automerge/mesh-echo-replicator.ts +2 -2
  134. package/src/automerge/network-protocol.ts +2 -1
  135. package/src/automerge/space-collection.ts +2 -1
  136. package/src/db-host/automerge-metrics.ts +2 -1
  137. package/src/db-host/data-service.ts +4 -3
  138. package/src/db-host/database-root.ts +17 -22
  139. package/src/db-host/documents-iterator.ts +9 -8
  140. package/src/db-host/documents-synchronizer.test.ts +2 -2
  141. package/src/db-host/documents-synchronizer.ts +20 -18
  142. package/src/db-host/echo-host.ts +44 -15
  143. package/src/db-host/index.ts +0 -1
  144. package/src/db-host/query-service.ts +43 -37
  145. package/src/db-host/space-state-manager.ts +14 -4
  146. package/src/edge/echo-edge-replicator.test.ts +3 -3
  147. package/src/edge/echo-edge-replicator.ts +9 -8
  148. package/src/edge/inflight-request-limiter.ts +4 -4
  149. package/src/filter/filter-match.test.ts +101 -0
  150. package/src/filter/filter-match.ts +174 -0
  151. package/src/filter/index.ts +5 -0
  152. package/src/index.ts +1 -0
  153. package/src/metadata/metadata-store.ts +13 -13
  154. package/src/pipeline/pipeline-stress.test.ts +9 -9
  155. package/src/pipeline/pipeline.ts +13 -13
  156. package/src/pipeline/timeframe-clock.ts +5 -5
  157. package/src/query/errors.ts +7 -0
  158. package/src/query/index.ts +8 -0
  159. package/src/query/plan.ts +179 -0
  160. package/src/query/query-executor.ts +648 -0
  161. package/src/query/query-planner.test.ts +613 -0
  162. package/src/query/query-planner.ts +470 -0
  163. package/src/space/admission-discovery-extension.ts +2 -2
  164. package/src/space/control-pipeline.ts +8 -8
  165. package/src/space/space-manager.ts +5 -4
  166. package/src/space/space-protocol.browser.test.ts +1 -0
  167. package/src/space/space-protocol.test.ts +1 -0
  168. package/src/space/space-protocol.ts +4 -4
  169. package/src/space/space.ts +5 -5
  170. package/src/testing/index.ts +2 -0
  171. package/src/testing/test-agent-builder.ts +6 -6
  172. package/src/testing/test-data.ts +127 -0
  173. package/src/testing/test-network-adapter.ts +15 -12
  174. package/src/testing/test-replicator.ts +2 -2
  175. package/src/testing/test-schema.ts +53 -0
  176. package/src/util.ts +7 -3
  177. package/dist/lib/browser/chunk-32WDI3LB.mjs.map +0 -7
  178. package/dist/lib/node/chunk-TC2PRBEU.cjs.map +0 -7
  179. package/dist/lib/node-esm/chunk-UKOLB3LW.mjs.map +0 -7
  180. package/dist/types/src/db-host/query-state.d.ts +0 -41
  181. package/dist/types/src/db-host/query-state.d.ts.map +0 -1
  182. package/src/db-host/query-state.ts +0 -217
@@ -0,0 +1,648 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type { AutomergeUrl, DocumentId } from '@automerge/automerge-repo';
6
+ import { Match } from 'effect';
7
+
8
+ import { Context, ContextDisposedError, LifecycleState, Resource } from '@dxos/context';
9
+ import { DatabaseDirectory, isEncodedReference, ObjectStructure, type QueryAST } from '@dxos/echo-protocol';
10
+ import { EscapedPropPath, type FindResult, type Indexer } from '@dxos/indexing';
11
+ import { invariant } from '@dxos/invariant';
12
+ import { DXN, type ObjectId, PublicKey, type SpaceId } from '@dxos/keys';
13
+ import { log } from '@dxos/log';
14
+ import { objectPointerCodec } from '@dxos/protocols';
15
+ import { type QueryReactivity, type QueryResult } from '@dxos/protocols/proto/dxos/echo/query';
16
+ import { getDeep, isNonNullable } from '@dxos/util';
17
+
18
+ import type { QueryPlan } from './plan';
19
+ import { QueryPlanner } from './query-planner';
20
+ import type { AutomergeHost } from '../automerge';
21
+ import { createIdFromSpaceKey } from '../common';
22
+ import type { SpaceStateManager } from '../db-host';
23
+ import { filterMatchObject } from '../filter';
24
+
25
+ type QueryExecutorOptions = {
26
+ indexer: Indexer;
27
+ automergeHost: AutomergeHost;
28
+ spaceStateManager: SpaceStateManager;
29
+
30
+ queryId: string;
31
+ query: QueryAST.Query;
32
+ reactivity: QueryReactivity;
33
+ };
34
+
35
+ type QueryExecutionResult = {
36
+ /**
37
+ * Whether the query results have changed since the last execution.
38
+ */
39
+ changed: boolean;
40
+ };
41
+
42
+ /**
43
+ * Represents an item in the query working set during execution.
44
+ */
45
+ type QueryItem = {
46
+ objectId: ObjectId;
47
+ documentId: DocumentId;
48
+ spaceId: SpaceId;
49
+ doc: ObjectStructure;
50
+ };
51
+
52
+ /**
53
+ * Recursive data structure that represents the execution trace of a query.
54
+ */
55
+ export type ExecutionTrace = {
56
+ name: string;
57
+ details: string;
58
+
59
+ objectCount: number;
60
+ documentsLoaded: number;
61
+ indexHits: number;
62
+
63
+ executionTime: number;
64
+ indexQueryTime: number;
65
+ documentLoadTime: number;
66
+
67
+ children: ExecutionTrace[];
68
+ };
69
+
70
+ export const ExecutionTrace = Object.freeze({
71
+ makeEmpty: (): ExecutionTrace => ({
72
+ name: 'Empty',
73
+ details: '',
74
+ objectCount: 0,
75
+ documentsLoaded: 0,
76
+ indexHits: 0,
77
+ indexQueryTime: 0,
78
+ documentLoadTime: 0,
79
+ executionTime: 0,
80
+ children: [],
81
+ }),
82
+ format: (trace: ExecutionTrace): string => {
83
+ const go = (trace: ExecutionTrace, indent: number): string => {
84
+ return [
85
+ `${' '.repeat(indent)} - ${trace.name}(${trace.details})`,
86
+ `${' '.repeat(indent)} objects: ${trace.objectCount} docs: ${trace.documentsLoaded} index hits: ${trace.indexHits} | total: ${trace.executionTime.toFixed(0)}ms index: ${trace.indexQueryTime.toFixed(0)}ms load: ${trace.documentLoadTime.toFixed(0)}ms`,
87
+ '',
88
+ ...trace.children.map((child) => go(child, indent + 2)),
89
+ ].join('\n');
90
+ };
91
+ return go(trace, 0);
92
+ },
93
+ });
94
+
95
+ type StepExecutionResult = {
96
+ workingSet: QueryItem[];
97
+ trace: ExecutionTrace;
98
+ };
99
+
100
+ /**
101
+ * Executes query plans against the Indexer and AutomergeHost.
102
+ *
103
+ * The QueryExecutor is responsible for:
104
+ * - Executing query plans step by step
105
+ * - Managing the working set of query results
106
+ * - Loading documents from the database
107
+ * - Tracking execution performance metrics
108
+ * - Handling different types of query operations (select, filter, traverse, etc.)
109
+ */
110
+ export class QueryExecutor extends Resource {
111
+ private readonly _indexer: Indexer;
112
+ private readonly _automergeHost: AutomergeHost;
113
+ private readonly _spaceStateManager: SpaceStateManager;
114
+ /**
115
+ * Id of this query.
116
+ */
117
+ private readonly _id: string;
118
+ private readonly _query: QueryAST.Query;
119
+ // TODO(dmaretskyi): Might be used in the future.
120
+ private readonly _reactivity: QueryReactivity;
121
+
122
+ private _plan: QueryPlan.Plan;
123
+ private _trace: ExecutionTrace = ExecutionTrace.makeEmpty();
124
+ private _lastResultSet: QueryItem[] = [];
125
+
126
+ constructor(options: QueryExecutorOptions) {
127
+ super();
128
+
129
+ this._indexer = options.indexer;
130
+ this._automergeHost = options.automergeHost;
131
+ this._spaceStateManager = options.spaceStateManager;
132
+
133
+ this._id = options.queryId;
134
+ this._query = options.query;
135
+ this._reactivity = options.reactivity;
136
+
137
+ const queryPlanner = new QueryPlanner();
138
+ this._plan = queryPlanner.createPlan(this._query);
139
+ }
140
+
141
+ get query(): QueryAST.Query {
142
+ return this._query;
143
+ }
144
+
145
+ get plan(): QueryPlan.Plan {
146
+ return this._plan;
147
+ }
148
+
149
+ get trace(): ExecutionTrace {
150
+ return this._trace;
151
+ }
152
+
153
+ protected override async _open(ctx: Context): Promise<void> {}
154
+
155
+ protected override async _close(ctx: Context): Promise<void> {}
156
+
157
+ getResults(): QueryResult[] {
158
+ return this._lastResultSet.map(
159
+ (item): QueryResult => ({
160
+ id: item.objectId,
161
+ documentId: item.documentId,
162
+ spaceId: item.spaceId,
163
+
164
+ // TODO(dmaretskyi): Plumb through the rank.
165
+ rank: 0,
166
+ }),
167
+ );
168
+ }
169
+
170
+ async execQuery(): Promise<QueryExecutionResult> {
171
+ invariant(this._lifecycleState === LifecycleState.OPEN);
172
+ const prevResultSet = this._lastResultSet;
173
+
174
+ const { workingSet, trace } = await this._execPlan(this._plan, this._lastResultSet);
175
+ this._lastResultSet = workingSet;
176
+ trace.name = 'Root';
177
+ trace.details = JSON.stringify({ id: this._id });
178
+ this._trace = trace;
179
+
180
+ const changed =
181
+ prevResultSet.length !== workingSet.length ||
182
+ prevResultSet.some(
183
+ (item, index) =>
184
+ workingSet[index].objectId !== item.objectId ||
185
+ workingSet[index].spaceId !== item.spaceId ||
186
+ workingSet[index].documentId !== item.documentId,
187
+ );
188
+
189
+ // log.info('Query execution result', {
190
+ // changed,
191
+ // trace: ExecutionTrace.format(trace),
192
+ // });
193
+ // eslint-disable-next-line no-console
194
+ // console.log(ExecutionTrace.format(trace));
195
+
196
+ return {
197
+ changed,
198
+ };
199
+ }
200
+
201
+ private async _execPlan(plan: QueryPlan.Plan, workingSet: QueryItem[]): Promise<StepExecutionResult> {
202
+ const trace = ExecutionTrace.makeEmpty();
203
+ for (const step of plan.steps) {
204
+ if (this._ctx.disposed) {
205
+ throw new ContextDisposedError();
206
+ }
207
+
208
+ const result = await this._execStep(step, workingSet);
209
+ workingSet = result.workingSet;
210
+ trace.children.push(result.trace);
211
+ }
212
+ return { workingSet, trace };
213
+ }
214
+
215
+ private async _execStep(step: QueryPlan.Step, workingSet: QueryItem[]): Promise<StepExecutionResult> {
216
+ if (this._ctx.disposed) {
217
+ return { workingSet, trace: ExecutionTrace.makeEmpty() };
218
+ }
219
+ let newWorkingSet: QueryItem[], trace: ExecutionTrace;
220
+
221
+ const begin = performance.now();
222
+ switch (step._tag) {
223
+ case 'ClearWorkingSetStep':
224
+ newWorkingSet = [];
225
+ trace = ExecutionTrace.makeEmpty();
226
+ break;
227
+ case 'SelectStep':
228
+ ({ workingSet: newWorkingSet, trace } = await this._execSelectStep(step, workingSet));
229
+ break;
230
+ case 'FilterStep':
231
+ ({ workingSet: newWorkingSet, trace } = await this._execFilterStep(step, workingSet));
232
+ break;
233
+ case 'FilterDeletedStep':
234
+ ({ workingSet: newWorkingSet, trace } = await this._execFilterDeletedStep(step, workingSet));
235
+ break;
236
+ case 'UnionStep':
237
+ ({ workingSet: newWorkingSet, trace } = await this._execUnionStep(step, workingSet));
238
+ break;
239
+ case 'TraverseStep':
240
+ ({ workingSet: newWorkingSet, trace } = await this._execTraverseStep(step, workingSet));
241
+ break;
242
+ default:
243
+ throw new Error(`Unknown step type: ${(step as any)._tag}`);
244
+ }
245
+ trace.executionTime = performance.now() - begin;
246
+
247
+ return { workingSet: newWorkingSet, trace };
248
+ }
249
+
250
+ private async _execSelectStep(step: QueryPlan.SelectStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
251
+ workingSet = [...workingSet];
252
+
253
+ const trace: ExecutionTrace = {
254
+ ...ExecutionTrace.makeEmpty(),
255
+ name: 'Select',
256
+ details: JSON.stringify(step.selector),
257
+ };
258
+
259
+ switch (step.selector._tag) {
260
+ case 'WildcardSelector': {
261
+ const beginIndexQuery = performance.now();
262
+ const indexHits = await this._indexer.execQuery({
263
+ typenames: [],
264
+ inverted: false,
265
+ });
266
+ trace.indexHits = +indexHits.length;
267
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
268
+
269
+ if (this._ctx.disposed) {
270
+ return { workingSet, trace };
271
+ }
272
+
273
+ const documentLoadStart = performance.now();
274
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
275
+ trace.documentsLoaded += results.length;
276
+ trace.documentLoadTime += performance.now() - documentLoadStart;
277
+
278
+ workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
279
+ trace.objectCount = workingSet.length;
280
+
281
+ break;
282
+ }
283
+ case 'IdSelector': {
284
+ const beginLoad = performance.now();
285
+ const items = await Promise.all(
286
+ step.selector.objectIds.map((id) =>
287
+ this._loadFromDXN(DXN.fromLocalObjectId(id), { sourceSpaceId: step.spaces[0] }),
288
+ ),
289
+ );
290
+ trace.documentLoadTime += performance.now() - beginLoad;
291
+
292
+ workingSet.push(...items.filter(isNonNullable));
293
+ trace.objectCount = workingSet.length;
294
+ break;
295
+ }
296
+ case 'TypeSelector': {
297
+ const beginIndexQuery = performance.now();
298
+ const indexHits = await this._indexer.execQuery({
299
+ typenames: step.selector.typename,
300
+ inverted: step.selector.inverted,
301
+ });
302
+ trace.indexHits = +indexHits.length;
303
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
304
+
305
+ if (this._ctx.disposed) {
306
+ return { workingSet, trace };
307
+ }
308
+
309
+ const documentLoadStart = performance.now();
310
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
311
+ trace.documentsLoaded += results.length;
312
+ trace.documentLoadTime += performance.now() - documentLoadStart;
313
+
314
+ workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
315
+ trace.objectCount = workingSet.length;
316
+
317
+ break;
318
+ }
319
+ case 'TextSelector': {
320
+ const beginIndexQuery = performance.now();
321
+ const indexHits = await this._indexer.execQuery({
322
+ typenames: [],
323
+ text: {
324
+ query: step.selector.text,
325
+ kind: Match.type<QueryPlan.TextSearchKind>().pipe(
326
+ Match.withReturnType<'text' | 'vector'>(),
327
+ Match.when('full-text', () => 'text'),
328
+ Match.when('vector', () => 'vector'),
329
+ Match.orElseAbsurd,
330
+ )(step.selector.searchKind),
331
+ },
332
+ });
333
+ trace.indexHits = +indexHits.length;
334
+ trace.indexQueryTime += performance.now() - beginIndexQuery;
335
+
336
+ if (this._ctx.disposed) {
337
+ return { workingSet, trace };
338
+ }
339
+
340
+ const documentLoadStart = performance.now();
341
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
342
+ trace.documentsLoaded += results.length;
343
+ trace.documentLoadTime += performance.now() - documentLoadStart;
344
+
345
+ workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
346
+ trace.objectCount = workingSet.length;
347
+ break;
348
+ }
349
+ default:
350
+ throw new Error(`Unknown selector type: ${(step.selector as any)._tag}`);
351
+ }
352
+
353
+ return { workingSet, trace };
354
+ }
355
+
356
+ private async _execFilterStep(step: QueryPlan.FilterStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
357
+ const result = workingSet.filter((item) =>
358
+ filterMatchObject(step.filter, {
359
+ id: item.objectId,
360
+ spaceId: item.spaceId,
361
+ doc: item.doc,
362
+ }),
363
+ );
364
+ return {
365
+ workingSet: result,
366
+ trace: {
367
+ ...ExecutionTrace.makeEmpty(),
368
+ name: 'Filter',
369
+ details: JSON.stringify(step.filter),
370
+ objectCount: result.length,
371
+ },
372
+ };
373
+ }
374
+
375
+ private async _execFilterDeletedStep(
376
+ step: QueryPlan.FilterDeletedStep,
377
+ workingSet: QueryItem[],
378
+ ): Promise<StepExecutionResult> {
379
+ const expected = step.mode === 'only-deleted';
380
+ const result = workingSet.filter((item) => ObjectStructure.isDeleted(item.doc) === expected);
381
+ return {
382
+ workingSet: result,
383
+ trace: {
384
+ ...ExecutionTrace.makeEmpty(),
385
+ name: 'FilterDeleted',
386
+ details: step.mode,
387
+ objectCount: result.length,
388
+ },
389
+ };
390
+ }
391
+
392
+ // TODO(dmaretskyi): This needs to be completed.
393
+ private async _execTraverseStep(step: QueryPlan.TraverseStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
394
+ const trace: ExecutionTrace = {
395
+ ...ExecutionTrace.makeEmpty(),
396
+ name: 'Traverse',
397
+ details: JSON.stringify(step.traversal),
398
+ };
399
+
400
+ const newWorkingSet: QueryItem[] = [];
401
+
402
+ switch (step.traversal._tag) {
403
+ case 'ReferenceTraversal': {
404
+ switch (step.traversal.direction) {
405
+ case 'outgoing': {
406
+ const property = EscapedPropPath.unescape(step.traversal.property);
407
+
408
+ const refs = workingSet
409
+ .map((item) => {
410
+ const ref = getDeep(item.doc.data, property);
411
+ if (!isEncodedReference(ref)) {
412
+ return null;
413
+ }
414
+
415
+ try {
416
+ return {
417
+ ref: DXN.parse(ref['/']),
418
+ spaceId: item.spaceId,
419
+ };
420
+ } catch {
421
+ log.warn('Invalid reference', { ref: ref['/'] });
422
+ return null;
423
+ }
424
+ })
425
+ .filter(isNonNullable);
426
+
427
+ const beginLoad = performance.now();
428
+ const items = await Promise.all(
429
+ refs.map(({ ref, spaceId }) => this._loadFromDXN(ref, { sourceSpaceId: spaceId })),
430
+ );
431
+ trace.documentLoadTime += performance.now() - beginLoad;
432
+
433
+ newWorkingSet.push(...items.filter(isNonNullable));
434
+ trace.objectCount = newWorkingSet.length;
435
+
436
+ break;
437
+ }
438
+ case 'incoming': {
439
+ const indexHits = await this._indexer.execQuery({
440
+ typenames: [],
441
+ inverted: false,
442
+ graph: {
443
+ kind: 'inbound-reference',
444
+ property: step.traversal.property,
445
+ anchors: workingSet.map((item) => item.objectId),
446
+ },
447
+ });
448
+ trace.indexHits += indexHits.length;
449
+
450
+ const documentLoadStart = performance.now();
451
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
452
+ trace.documentsLoaded += results.length;
453
+ trace.documentLoadTime += performance.now() - documentLoadStart;
454
+
455
+ newWorkingSet.push(...results.filter(isNonNullable));
456
+ trace.objectCount = newWorkingSet.length;
457
+
458
+ break;
459
+ }
460
+ }
461
+ break;
462
+ }
463
+ case 'RelationTraversal': {
464
+ switch (step.traversal.direction) {
465
+ case 'relation-to-source':
466
+ case 'relation-to-target': {
467
+ const refs = workingSet
468
+ .map((item) => {
469
+ const ref =
470
+ step.traversal.direction === 'relation-to-source'
471
+ ? ObjectStructure.getRelationSource(item.doc)
472
+ : ObjectStructure.getRelationTarget(item.doc);
473
+
474
+ if (!isEncodedReference(ref)) {
475
+ return null;
476
+ }
477
+ try {
478
+ return {
479
+ ref: DXN.parse(ref['/']),
480
+ spaceId: item.spaceId,
481
+ };
482
+ } catch {
483
+ log.warn('Invalid reference', { ref: ref['/'] });
484
+ return null;
485
+ }
486
+ })
487
+ .filter(isNonNullable);
488
+
489
+ const beginLoad = performance.now();
490
+ const items = await Promise.all(
491
+ refs.map(({ ref, spaceId }) => this._loadFromDXN(ref, { sourceSpaceId: spaceId })),
492
+ );
493
+ trace.documentLoadTime += performance.now() - beginLoad;
494
+
495
+ newWorkingSet.push(...items.filter(isNonNullable));
496
+ trace.objectCount = newWorkingSet.length;
497
+
498
+ break;
499
+ }
500
+
501
+ case 'source-to-relation':
502
+ case 'target-to-relation': {
503
+ const indexHits = await this._indexer.execQuery({
504
+ typenames: [],
505
+ inverted: false,
506
+ graph: {
507
+ kind: step.traversal.direction === 'source-to-relation' ? 'relation-source' : 'relation-target',
508
+ anchors: workingSet.map((item) => item.objectId),
509
+ property: null,
510
+ },
511
+ });
512
+
513
+ trace.indexHits += indexHits.length;
514
+
515
+ const documentLoadStart = performance.now();
516
+ const results = await this._loadDocumentsAfterIndexQuery(indexHits);
517
+ trace.documentsLoaded += results.length;
518
+ trace.documentLoadTime += performance.now() - documentLoadStart;
519
+
520
+ newWorkingSet.push(...results.filter(isNonNullable));
521
+ trace.objectCount = newWorkingSet.length;
522
+
523
+ break;
524
+ }
525
+ }
526
+ break;
527
+ }
528
+ default:
529
+ throw new Error(`Unknown traversal type: ${(step.traversal as any)._tag}`);
530
+ }
531
+
532
+ return { workingSet: newWorkingSet, trace };
533
+ }
534
+
535
+ private async _execUnionStep(step: QueryPlan.UnionStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
536
+ const results = new Map<ObjectId, QueryItem>();
537
+
538
+ const resultSets = await Promise.all(step.plans.map((plan) => this._execPlan(plan, [...workingSet])));
539
+
540
+ const trace: ExecutionTrace = {
541
+ ...ExecutionTrace.makeEmpty(),
542
+ name: 'Union',
543
+ };
544
+
545
+ // NOTE: Doing insertion after execution to ensure deterministic results. Probably not needed.
546
+ for (const resultSet of resultSets) {
547
+ for (const item of resultSet.workingSet) {
548
+ // Could be duplicate object ids in different spaces or in different epochs of the same space.
549
+ results.set(`${item.spaceId}:${item.documentId}:${item.objectId}`, item);
550
+ }
551
+ trace.children.push(resultSet.trace);
552
+ }
553
+
554
+ return {
555
+ workingSet: [...results.values()],
556
+ trace,
557
+ };
558
+ }
559
+
560
+ private async _loadDocumentsAfterIndexQuery(indexHits: FindResult[]): Promise<(QueryItem | null)[]> {
561
+ return Promise.all(
562
+ indexHits.map(async (hit): Promise<QueryItem | null> => {
563
+ return this._loadFromIndexHit(hit);
564
+ }),
565
+ );
566
+ }
567
+
568
+ private async _loadFromIndexHit(hit: FindResult): Promise<QueryItem | null> {
569
+ const { objectId, documentId, spaceKey: spaceKeyInIndex } = objectPointerCodec.decode(hit.id);
570
+
571
+ const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), documentId as DocumentId);
572
+ const doc = handle.doc();
573
+ if (!doc) {
574
+ return null;
575
+ }
576
+
577
+ const spaceKey = spaceKeyInIndex ?? DatabaseDirectory.getSpaceKey(doc);
578
+ if (!spaceKey) {
579
+ return null;
580
+ }
581
+
582
+ const object = DatabaseDirectory.getInlineObject(doc, objectId);
583
+ if (!object) {
584
+ return null;
585
+ }
586
+
587
+ return {
588
+ objectId,
589
+ documentId: documentId as DocumentId,
590
+ spaceId: await createIdFromSpaceKey(PublicKey.from(spaceKey)),
591
+ doc: object,
592
+ };
593
+ }
594
+
595
+ private async _loadFromDXN(dxn: DXN, { sourceSpaceId }: { sourceSpaceId: SpaceId }): Promise<QueryItem | null> {
596
+ const echoDxn = dxn.asEchoDXN();
597
+ if (!echoDxn) {
598
+ log.warn('unable to resolve DXN', { dxn });
599
+ return null;
600
+ }
601
+
602
+ const spaceId = echoDxn.spaceId ?? sourceSpaceId;
603
+
604
+ const spaceRoot = this._spaceStateManager.getRootBySpaceId(spaceId);
605
+ if (!spaceRoot) {
606
+ log.warn('no space state found for', { spaceId });
607
+ return null;
608
+ }
609
+ const dbDirectory = spaceRoot.doc();
610
+ if (!dbDirectory) {
611
+ log.warn('no space state found for', { spaceId });
612
+ return null;
613
+ }
614
+
615
+ const inlineObject = DatabaseDirectory.getInlineObject(dbDirectory, echoDxn.echoId);
616
+ if (inlineObject) {
617
+ return {
618
+ objectId: echoDxn.echoId,
619
+ documentId: spaceRoot.documentId,
620
+ spaceId,
621
+ doc: inlineObject,
622
+ };
623
+ }
624
+
625
+ const link = DatabaseDirectory.getLink(dbDirectory, echoDxn.echoId);
626
+ if (!link) {
627
+ return null;
628
+ }
629
+
630
+ const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), link as AutomergeUrl);
631
+ const doc = handle.doc();
632
+ if (!doc) {
633
+ return null;
634
+ }
635
+
636
+ const object = DatabaseDirectory.getInlineObject(doc, echoDxn.echoId);
637
+ if (!object) {
638
+ return null;
639
+ }
640
+
641
+ return {
642
+ objectId: echoDxn.echoId,
643
+ documentId: handle.documentId,
644
+ spaceId,
645
+ doc: object,
646
+ };
647
+ }
648
+ }