@dxos/echo-pipeline 0.8.3 → 0.8.4-main.03d5cd7b56

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,13 +2,31 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type QueryAST } from '@dxos/echo-protocol';
5
+ import { Order, Query } from '@dxos/echo';
6
+ import { QueryAST } from '@dxos/echo-protocol';
6
7
  import { invariant } from '@dxos/invariant';
7
- import type { DXN, SpaceId } from '@dxos/keys';
8
+ import type { DXN } from '@dxos/keys';
8
9
 
9
10
  import { QueryError } from './errors';
10
11
  import { QueryPlan } from './plan';
11
12
 
13
+ /**
14
+ * Creates a QueryError with "Query too complex" message and includes the prettified query in the context.
15
+ */
16
+ const queryTooComplexError = (query: QueryAST.Query | null): QueryError => {
17
+ if (query === null) {
18
+ return new QueryError({
19
+ message: 'Query too complex',
20
+ context: { query: null },
21
+ });
22
+ }
23
+ const prettyQuery = Query.pretty(Query.fromAst(query));
24
+ return new QueryError({
25
+ message: `Query too complex: ${prettyQuery}`,
26
+ context: { query, prettyQuery },
27
+ });
28
+ };
29
+
12
30
  export type QueryPlannerOptions = {
13
31
  defaultTextSearchKind: QueryPlan.TextSearchKind;
14
32
  };
@@ -32,9 +50,12 @@ export class QueryPlanner {
32
50
  }
33
51
 
34
52
  createPlan(query: QueryAST.Query): QueryPlan.Plan {
53
+ this._validateQueryScoped(query);
35
54
  let plan = this._generate(query, { ...DEFAULT_CONTEXT, originalQuery: query });
36
55
  plan = this._optimizeEmptyFilters(plan);
37
56
  plan = this._optimizeSoloUnions(plan);
57
+ plan = this._ensureOrderStep(plan);
58
+ plan = this._optimizeLimits(plan);
38
59
  return plan;
39
60
  }
40
61
 
@@ -54,12 +75,21 @@ export class QueryPlanner {
54
75
  return this._generateRelationTraversalClause(query, context);
55
76
  case 'reference-traversal':
56
77
  return this._generateReferenceTraversalClause(query, context);
78
+ case 'hierarchy-traversal':
79
+ return this._generateHierarchyTraversalClause(query, context);
57
80
  case 'union':
58
81
  return this._generateUnionClause(query, context);
59
82
  case 'set-difference':
60
83
  return this._generateSetDifferenceClause(query, context);
84
+ case 'order':
85
+ return this._generateOrderClause(query, context);
86
+ case 'limit':
87
+ return this._generateLimitClause(query, context);
88
+ case 'from':
89
+ return this._generateFromClause(query, context);
61
90
  default:
62
- throw new QueryError(`Unsupported query type: ${(query as any).type}`, {
91
+ throw new QueryError({
92
+ message: `Unsupported query type: ${(query as any).type}`,
63
93
  context: { query: context.originalQuery },
64
94
  });
65
95
  }
@@ -69,15 +99,25 @@ export class QueryPlanner {
69
99
  const newContext = {
70
100
  ...context,
71
101
  };
72
- if (query.options.spaceIds) {
73
- newContext.selectionSpaces = query.options.spaceIds as readonly SpaceId[];
74
- }
75
102
  if (query.options.deleted) {
76
103
  newContext.deletedHandling = query.options.deleted;
77
104
  }
78
105
  return this._generate(query.query, newContext);
79
106
  }
80
107
 
108
+ private _generateFromClause(query: QueryAST.QueryFromClause, context: GenerationContext): QueryPlan.Plan {
109
+ if (query.from._tag === 'scope') {
110
+ return this._generate(query.query, { ...context, scope: query.from.scope });
111
+ }
112
+
113
+ // Subquery from: flatten by rewriting the outer query's leaf select into a filter on the subquery.
114
+ const subquery = query.from.query;
115
+ const rewritten = QueryAST.map(query.query, (node) =>
116
+ node.type === 'select' ? { type: 'filter', selection: subquery, filter: node.filter } : node,
117
+ );
118
+ return this._generate(rewritten, context);
119
+ }
120
+
81
121
  private _generateSelectClause(query: QueryAST.QuerySelectClause, context: GenerationContext): QueryPlan.Plan {
82
122
  return this._generateSelectionFromFilter(query.filter, context);
83
123
  }
@@ -86,6 +126,7 @@ export class QueryPlanner {
86
126
  // TODO(dmaretskyi): If the tip of the query ast is a [select, ...filter] shape we can reorder the filters so the query is most efficient.
87
127
  private _generateSelectionFromFilter(filter: QueryAST.Filter, context: GenerationContext): QueryPlan.Plan {
88
128
  switch (filter.type) {
129
+ // Props
89
130
  case 'object': {
90
131
  if (
91
132
  context.selectionInverted &&
@@ -102,7 +143,7 @@ export class QueryPlanner {
102
143
  ]);
103
144
  }
104
145
  if (context.selectionInverted) {
105
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
146
+ throw queryTooComplexError(context.originalQuery);
106
147
  }
107
148
 
108
149
  // Try to utilize indexes during selection, prioritizing selecting by id, then by typename.
@@ -111,7 +152,7 @@ export class QueryPlanner {
111
152
  return QueryPlan.Plan.make([
112
153
  {
113
154
  _tag: 'SelectStep',
114
- spaces: context.selectionSpaces,
155
+ scope: context.scope,
115
156
  selector: {
116
157
  _tag: 'IdSelector',
117
158
  objectIds: filter.id,
@@ -127,7 +168,7 @@ export class QueryPlanner {
127
168
  return QueryPlan.Plan.make([
128
169
  {
129
170
  _tag: 'SelectStep',
130
- spaces: context.selectionSpaces,
171
+ scope: context.scope,
131
172
  selector: {
132
173
  _tag: 'TypeSelector',
133
174
  typename: [filter.typename as DXN.String],
@@ -135,16 +176,17 @@ export class QueryPlanner {
135
176
  },
136
177
  },
137
178
  ...this._generateDeletedHandlingSteps(context),
179
+ // TODO(dmaretskyi): Normally we could skip filtering by typename here, but since the index does not separate schema versions, we need to do additional filter to only select the correct version.
138
180
  {
139
181
  _tag: 'FilterStep',
140
- filter: { ...filter, typename: null },
182
+ filter: { ...filter },
141
183
  },
142
184
  ]);
143
185
  } else {
144
186
  return QueryPlan.Plan.make([
145
187
  {
146
188
  _tag: 'SelectStep',
147
- spaces: context.selectionSpaces,
189
+ scope: context.scope,
148
190
  selector: {
149
191
  _tag: 'WildcardSelector',
150
192
  },
@@ -157,44 +199,187 @@ export class QueryPlanner {
157
199
  ]);
158
200
  }
159
201
  }
202
+
203
+ // Tag
204
+ case 'tag': {
205
+ return QueryPlan.Plan.make([
206
+ {
207
+ _tag: 'SelectStep',
208
+ scope: context.scope,
209
+ selector: {
210
+ _tag: 'WildcardSelector',
211
+ },
212
+ },
213
+ ...this._generateDeletedHandlingSteps(context),
214
+ {
215
+ _tag: 'FilterStep',
216
+ filter: { ...filter },
217
+ },
218
+ ]);
219
+ }
220
+
221
+ // Text
160
222
  case 'text-search': {
161
223
  return QueryPlan.Plan.make([
162
224
  {
163
225
  _tag: 'SelectStep',
164
- spaces: context.selectionSpaces,
226
+ scope: context.scope,
165
227
  selector: {
166
228
  _tag: 'TextSelector',
167
229
  text: filter.text,
168
230
  searchKind: filter.searchKind ?? this._options.defaultTextSearchKind,
231
+ typename: null,
169
232
  },
170
233
  },
171
234
  ...this._generateDeletedHandlingSteps(context),
172
235
  ]);
173
236
  }
237
+
238
+ // Timestamp
239
+ case 'timestamp': {
240
+ if (context.selectionInverted) {
241
+ throw new QueryError({
242
+ message:
243
+ 'Negated timestamp filters are not supported. Use Filter.updated/Filter.created with the inverted range instead.',
244
+ context: { query: context.originalQuery },
245
+ });
246
+ }
247
+ return QueryPlan.Plan.make([
248
+ {
249
+ _tag: 'SelectStep',
250
+ scope: context.scope,
251
+ selector: _timestampFilterToSelector(filter),
252
+ },
253
+ ...this._generateDeletedHandlingSteps(context),
254
+ ]);
255
+ }
256
+
257
+ // ChildOf
258
+ case 'child-of': {
259
+ return QueryPlan.Plan.make([
260
+ {
261
+ _tag: 'SelectStep',
262
+ scope: context.scope,
263
+ selector: {
264
+ _tag: 'WildcardSelector',
265
+ },
266
+ },
267
+ ...this._generateDeletedHandlingSteps(context),
268
+ {
269
+ _tag: 'FilterStep',
270
+ filter: { ...filter },
271
+ },
272
+ ]);
273
+ }
274
+
275
+ // Compare
174
276
  case 'compare':
175
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
277
+ throw queryTooComplexError(context.originalQuery);
176
278
  case 'in':
177
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
279
+ throw queryTooComplexError(context.originalQuery);
178
280
  case 'range':
179
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
281
+ throw queryTooComplexError(context.originalQuery);
282
+
283
+ // Boolean
180
284
  case 'not':
181
285
  return this._generateSelectionFromFilter(filter.filter, {
182
286
  ...context,
183
287
  selectionInverted: !context.selectionInverted,
184
288
  });
185
- case 'and':
186
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
289
+ case 'and': {
290
+ const flatFilters = _flattenAnd(filter.filters);
291
+ const timestampFilters = flatFilters.filter((f): f is QueryAST.FilterTimestamp => f.type === 'timestamp');
292
+ const childOfFilters = flatFilters.filter((f): f is QueryAST.FilterChildOf => f.type === 'child-of');
293
+ const otherFilters = flatFilters.filter((f) => f.type !== 'timestamp' && f.type !== 'child-of');
294
+
295
+ if (timestampFilters.length > 0 && context.selectionInverted) {
296
+ throw new QueryError({
297
+ message:
298
+ 'Negated timestamp filters are not supported. Use Filter.updated/Filter.created with the inverted range instead.',
299
+ context: { query: context.originalQuery },
300
+ });
301
+ }
302
+
303
+ if (timestampFilters.length > 0 && otherFilters.length <= 1 && childOfFilters.length === 0) {
304
+ const innerFilter = otherFilters[0];
305
+ const innerPlan = innerFilter
306
+ ? this._generateSelectionFromFilter(innerFilter, context)
307
+ : QueryPlan.Plan.make([
308
+ {
309
+ _tag: 'SelectStep',
310
+ scope: context.scope,
311
+ selector: { _tag: 'WildcardSelector' },
312
+ },
313
+ ...this._generateDeletedHandlingSteps(context),
314
+ ]);
315
+
316
+ const selectIdx = innerPlan.steps.findIndex((s) => s._tag === 'SelectStep');
317
+ if (selectIdx !== -1) {
318
+ const newSteps = [...innerPlan.steps];
319
+ newSteps.splice(selectIdx + 1, 0, {
320
+ _tag: 'FilterStep',
321
+ filter: { type: 'and', filters: timestampFilters },
322
+ });
323
+ return QueryPlan.Plan.make(newSteps);
324
+ }
325
+
326
+ const selector = _mergeTimestampSelectors(timestampFilters.map(_timestampFilterToSelector));
327
+ return QueryPlan.Plan.make([
328
+ {
329
+ _tag: 'SelectStep',
330
+ scope: context.scope,
331
+ selector,
332
+ },
333
+ ...this._generateDeletedHandlingSteps(context),
334
+ ]);
335
+ }
336
+
337
+ if (timestampFilters.length > 0 && childOfFilters.length === 0) {
338
+ throw new QueryError({
339
+ message:
340
+ 'Timestamp filters can only be combined with a single type or property filter via AND. Split complex filters into a subquery.',
341
+ context: { query: context.originalQuery },
342
+ });
343
+ }
344
+
345
+ if (childOfFilters.length > 0) {
346
+ const remainingFilters = flatFilters.filter((f) => f.type !== 'child-of');
347
+ const innerPlan =
348
+ remainingFilters.length === 1
349
+ ? this._generateSelectionFromFilter(remainingFilters[0], context)
350
+ : remainingFilters.length > 1
351
+ ? this._generateSelectionFromFilter({ type: 'and', filters: remainingFilters }, context)
352
+ : QueryPlan.Plan.make([
353
+ {
354
+ _tag: 'SelectStep',
355
+ scope: context.scope,
356
+ selector: { _tag: 'WildcardSelector' },
357
+ },
358
+ ...this._generateDeletedHandlingSteps(context),
359
+ ]);
360
+
361
+ const childOfSteps: QueryPlan.Step[] = childOfFilters.map((f) => ({
362
+ _tag: 'FilterStep' as const,
363
+ filter: f,
364
+ }));
365
+
366
+ return QueryPlan.Plan.make([...innerPlan.steps, ...childOfSteps]);
367
+ }
368
+
369
+ throw queryTooComplexError(context.originalQuery);
370
+ }
187
371
  case 'or':
188
372
  // Optimized case
189
373
  if (filter.filters.every(isTrivialTypenameFilter)) {
190
- const typenames = filter.filters.map((f) => {
191
- invariant(f.type === 'object' && f.typename !== null);
192
- return f.typename;
374
+ const typenames = filter.filters.map((filter) => {
375
+ invariant(filter.type === 'object' && filter.typename !== null);
376
+ return filter.typename;
193
377
  });
378
+
194
379
  return QueryPlan.Plan.make([
195
380
  {
196
381
  _tag: 'SelectStep',
197
- spaces: context.selectionSpaces,
382
+ scope: context.scope,
198
383
  selector: {
199
384
  _tag: 'TypeSelector',
200
385
  typename: typenames as DXN.String[],
@@ -202,13 +387,18 @@ export class QueryPlanner {
202
387
  },
203
388
  },
204
389
  ...this._generateDeletedHandlingSteps(context),
390
+ {
391
+ _tag: 'FilterStep',
392
+ filter: { ...filter },
393
+ },
205
394
  ]);
206
395
  } else {
207
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
396
+ throw queryTooComplexError(context.originalQuery);
208
397
  }
209
398
 
210
399
  default:
211
- throw new QueryError(`Unsupported filter type: ${(filter as any).type}`, {
400
+ throw new QueryError({
401
+ message: `Unsupported filter type: ${(filter as any).type}`,
212
402
  context: { query: context.originalQuery },
213
403
  });
214
404
  }
@@ -286,7 +476,7 @@ export class QueryPlanner {
286
476
  traversal: {
287
477
  _tag: 'ReferenceTraversal',
288
478
  direction: 'incoming',
289
- property: query.property,
479
+ property: query.property ?? null,
290
480
  },
291
481
  },
292
482
  ...this._generateDeletedHandlingSteps(context),
@@ -392,6 +582,38 @@ export class QueryPlanner {
392
582
  ]);
393
583
  }
394
584
 
585
+ private _generateHierarchyTraversalClause(
586
+ query: QueryAST.QueryHierarchyTraversalClause,
587
+ context: GenerationContext,
588
+ ): QueryPlan.Plan {
589
+ return QueryPlan.Plan.make([
590
+ ...this._generate(query.anchor, context).steps,
591
+ {
592
+ _tag: 'TraverseStep',
593
+ traversal: {
594
+ _tag: 'HierarchyTraversal',
595
+ direction: query.direction,
596
+ },
597
+ },
598
+ ...this._generateDeletedHandlingSteps(context),
599
+ ]);
600
+ }
601
+
602
+ private _validateQueryScoped(query: QueryAST.Query): void {
603
+ let hasScope = false;
604
+ QueryAST.visit(query, (node) => {
605
+ if (node.type === 'from') {
606
+ hasScope = true;
607
+ }
608
+ });
609
+ if (!hasScope) {
610
+ throw new QueryError({
611
+ message: 'Query must be scoped with a from() clause',
612
+ context: { query },
613
+ });
614
+ }
615
+ }
616
+
395
617
  /**
396
618
  * Removes filter steps that have no predicates.
397
619
  */
@@ -425,6 +647,176 @@ export class QueryPlanner {
425
647
  // TODO(dmaretskyi): Implement this.
426
648
  return plan;
427
649
  }
650
+
651
+ private _generateOrderClause(query: QueryAST.QueryOrderClause, context: GenerationContext): QueryPlan.Plan {
652
+ return QueryPlan.Plan.make([
653
+ ...this._generate(query.query, context).steps,
654
+ {
655
+ _tag: 'OrderStep',
656
+ order: query.order,
657
+ },
658
+ ]);
659
+ }
660
+
661
+ private _generateLimitClause(query: QueryAST.QueryLimitClause, context: GenerationContext): QueryPlan.Plan {
662
+ return QueryPlan.Plan.make([
663
+ ...this._generate(query.query, context).steps,
664
+ {
665
+ _tag: 'LimitStep',
666
+ limit: query.limit,
667
+ },
668
+ ]);
669
+ }
670
+
671
+ // After complete plan is built, inspect it from the end:
672
+ // - Walk backwards until hitting an object set changer.
673
+ // - If an order step is found, skip.
674
+ // - Otherwise insert natural order before any limit steps (since limit should be applied after ordering).
675
+ // This method is recursive and also processes sub-plans in unions and set differences.
676
+ private _ensureOrderStep(plan: QueryPlan.Plan): QueryPlan.Plan {
677
+ // First, recursively process any sub-plans.
678
+ const processedSteps = plan.steps.map((step): QueryPlan.Step => {
679
+ if (step._tag === 'UnionStep') {
680
+ return {
681
+ _tag: 'UnionStep',
682
+ plans: step.plans.map((subPlan) => this._ensureOrderStep(subPlan)),
683
+ };
684
+ } else if (step._tag === 'SetDifferenceStep') {
685
+ return {
686
+ _tag: 'SetDifferenceStep',
687
+ source: this._ensureOrderStep(step.source),
688
+ exclude: this._ensureOrderStep(step.exclude),
689
+ };
690
+ }
691
+ return step;
692
+ });
693
+
694
+ const processedPlan = QueryPlan.Plan.make(processedSteps);
695
+
696
+ const OBJECT_SET_CHANGERS = new Set(['SelectStep', 'TraverseStep', 'UnionStep', 'SetDifferenceStep']);
697
+ for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
698
+ const step = processedPlan.steps[i];
699
+ if (step._tag === 'OrderStep') {
700
+ return processedPlan;
701
+ }
702
+ if (OBJECT_SET_CHANGERS.has(step._tag)) {
703
+ break;
704
+ }
705
+ }
706
+
707
+ // Find the position to insert the order step (before any limit steps).
708
+ let insertIndex = processedPlan.steps.length;
709
+ for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
710
+ if (processedPlan.steps[i]._tag === 'LimitStep') {
711
+ insertIndex = i;
712
+ } else {
713
+ break;
714
+ }
715
+ }
716
+
717
+ const newSteps = [...processedPlan.steps];
718
+ newSteps.splice(insertIndex, 0, {
719
+ _tag: 'OrderStep',
720
+ order: [Order.natural.ast],
721
+ });
722
+
723
+ return QueryPlan.Plan.make(newSteps);
724
+ }
725
+
726
+ /**
727
+ * Propagates limits from LimitStep to SelectStep and OrderStep when possible.
728
+ * Limits can only be propagated if there are no unions or traversals between the LimitStep and SelectStep/OrderStep.
729
+ */
730
+ private _optimizeLimits(plan: QueryPlan.Plan): QueryPlan.Plan {
731
+ // First, recursively process any sub-plans.
732
+ const processedSteps = plan.steps.map((step): QueryPlan.Step => {
733
+ if (step._tag === 'UnionStep') {
734
+ return {
735
+ _tag: 'UnionStep',
736
+ plans: step.plans.map((subPlan) => this._optimizeLimits(subPlan)),
737
+ };
738
+ } else if (step._tag === 'SetDifferenceStep') {
739
+ return {
740
+ _tag: 'SetDifferenceStep',
741
+ source: this._optimizeLimits(step.source),
742
+ exclude: this._optimizeLimits(step.exclude),
743
+ };
744
+ }
745
+ return step;
746
+ });
747
+
748
+ // Find if there's a LimitStep at the end (possibly after OrderStep).
749
+ let limitStepIndex = -1;
750
+ let limitValue: number | undefined;
751
+
752
+ for (let i = processedSteps.length - 1; i >= 0; i--) {
753
+ const step = processedSteps[i];
754
+ if (step._tag === 'LimitStep') {
755
+ limitStepIndex = i;
756
+ limitValue = step.limit;
757
+ break;
758
+ }
759
+ // Only allow OrderStep after LimitStep when looking backwards.
760
+ if (step._tag !== 'OrderStep') {
761
+ break;
762
+ }
763
+ }
764
+
765
+ if (limitStepIndex === -1 || limitValue === undefined) {
766
+ return QueryPlan.Plan.make(processedSteps);
767
+ }
768
+
769
+ // Check if we can propagate the limit to a SelectStep and/or OrderStep.
770
+ // We can only do this if there are no unions, traversals, or set differences between them.
771
+ const BLOCKERS = new Set(['UnionStep', 'TraverseStep', 'SetDifferenceStep']);
772
+
773
+ let selectStepIndex = -1;
774
+ let orderStepIndex = -1;
775
+ for (let i = 0; i < limitStepIndex; i++) {
776
+ const step = processedSteps[i];
777
+ if (step._tag === 'SelectStep') {
778
+ selectStepIndex = i;
779
+ }
780
+ if (step._tag === 'OrderStep') {
781
+ orderStepIndex = i;
782
+ }
783
+ if (BLOCKERS.has(step._tag)) {
784
+ // Found a blocker after the select/order step - can't propagate.
785
+ selectStepIndex = -1;
786
+ orderStepIndex = -1;
787
+ }
788
+ }
789
+
790
+ if (selectStepIndex === -1 && orderStepIndex === -1) {
791
+ return QueryPlan.Plan.make(processedSteps);
792
+ }
793
+
794
+ // Create a mutable copy of steps to modify.
795
+ const newSteps = [...processedSteps];
796
+
797
+ // Propagate the limit to the SelectStep if found.
798
+ if (selectStepIndex !== -1) {
799
+ const selectStep = newSteps[selectStepIndex] as QueryPlan.SelectStep;
800
+ newSteps[selectStepIndex] = {
801
+ ...selectStep,
802
+ limit: limitValue,
803
+ };
804
+ }
805
+
806
+ // Propagate the limit to the OrderStep if found.
807
+ if (orderStepIndex !== -1) {
808
+ const orderStep = newSteps[orderStepIndex] as QueryPlan.OrderStep;
809
+ newSteps[orderStepIndex] = {
810
+ ...orderStep,
811
+ limit: limitValue,
812
+ };
813
+ }
814
+
815
+ // Remove the LimitStep since limit is now propagated.
816
+ newSteps.splice(limitStepIndex, 1);
817
+
818
+ return QueryPlan.Plan.make(newSteps);
819
+ }
428
820
  }
429
821
 
430
822
  /**
@@ -437,9 +829,9 @@ type GenerationContext = {
437
829
  originalQuery: QueryAST.Query | null;
438
830
 
439
831
  /**
440
- * Which spaces to select from.
832
+ * The scope to select from (space IDs, queues, etc.).
441
833
  */
442
- selectionSpaces: readonly SpaceId[];
834
+ scope: QueryAST.Scope;
443
835
 
444
836
  /**
445
837
  * How to handle deleted objects.
@@ -454,7 +846,7 @@ type GenerationContext = {
454
846
 
455
847
  const DEFAULT_CONTEXT: GenerationContext = {
456
848
  originalQuery: null,
457
- selectionSpaces: [],
849
+ scope: {},
458
850
  deletedHandling: 'exclude',
459
851
  selectionInverted: false,
460
852
  };
@@ -474,6 +866,54 @@ const createRelationTraversalStep = (direction: QueryPlan.RelationTraversal['dir
474
866
  },
475
867
  });
476
868
 
869
+ /**
870
+ * Recursively flattens nested `and` filters into a single list.
871
+ */
872
+ const _flattenAnd = (filters: readonly QueryAST.Filter[]): QueryAST.Filter[] => {
873
+ const result: QueryAST.Filter[] = [];
874
+ for (const f of filters) {
875
+ if (f.type === 'and') {
876
+ result.push(..._flattenAnd(f.filters));
877
+ } else {
878
+ result.push(f);
879
+ }
880
+ }
881
+ return result;
882
+ };
883
+
884
+ const _timestampFilterToSelector = (filter: QueryAST.FilterTimestamp): QueryPlan.TimestampSelector => {
885
+ const selector: QueryPlan.TimestampSelector = { _tag: 'TimestampSelector' };
886
+ const key =
887
+ filter.field === 'updatedAt'
888
+ ? filter.operator === 'gte' || filter.operator === 'gt'
889
+ ? 'updatedAfter'
890
+ : 'updatedBefore'
891
+ : filter.operator === 'gte' || filter.operator === 'gt'
892
+ ? 'createdAfter'
893
+ : 'createdBefore';
894
+ selector[key] = filter.value;
895
+ return selector;
896
+ };
897
+
898
+ const _mergeTimestampSelectors = (selectors: QueryPlan.TimestampSelector[]): QueryPlan.TimestampSelector => {
899
+ const merged: QueryPlan.TimestampSelector = { _tag: 'TimestampSelector' };
900
+ for (const s of selectors) {
901
+ if (s.updatedAfter != null) {
902
+ merged.updatedAfter = Math.max(merged.updatedAfter ?? 0, s.updatedAfter);
903
+ }
904
+ if (s.updatedBefore != null) {
905
+ merged.updatedBefore = Math.min(merged.updatedBefore ?? Infinity, s.updatedBefore);
906
+ }
907
+ if (s.createdAfter != null) {
908
+ merged.createdAfter = Math.max(merged.createdAfter ?? 0, s.createdAfter);
909
+ }
910
+ if (s.createdBefore != null) {
911
+ merged.createdBefore = Math.min(merged.createdBefore ?? Infinity, s.createdBefore);
912
+ }
913
+ }
914
+ return merged;
915
+ };
916
+
477
917
  const isTrivialTypenameFilter = (filter: QueryAST.Filter): boolean => {
478
918
  return (
479
919
  filter.type === 'object' &&
@@ -2,15 +2,15 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { scheduleTask, type Trigger } from '@dxos/async';
5
+ import { type Trigger, scheduleTask } from '@dxos/async';
6
6
  import { Context } from '@dxos/context';
7
7
  import { ProtocolError } from '@dxos/protocols';
8
8
  import { schema } from '@dxos/protocols/proto';
9
9
  import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
10
10
  import {
11
11
  type AdmissionDiscoveryService,
12
- type GetAdmissionCredentialResponse,
13
12
  type GetAdmissionCredentialRequest,
13
+ type GetAdmissionCredentialResponse,
14
14
  } from '@dxos/protocols/proto/dxos/mesh/teleport';
15
15
  import { type ExtensionContext, RpcExtension } from '@dxos/teleport';
16
16
 
@@ -81,7 +81,7 @@ export class CredentialServerExtension extends RpcExtension<
81
81
  ): Promise<GetAdmissionCredentialResponse> => {
82
82
  const memberInfo = this._space.spaceState.members.get(request.memberKey);
83
83
  if (!memberInfo?.credential) {
84
- throw new ProtocolError('Space member not found.', request);
84
+ throw new ProtocolError({ message: 'Space member not found.', context: { ...request } });
85
85
  }
86
86
  return { admissionCredential: memberInfo.credential };
87
87
  },