@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1068cf700f

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 (199) hide show
  1. package/dist/lib/neutral/chunk-FJPXA75J.mjs +235 -0
  2. package/dist/lib/neutral/chunk-FJPXA75J.mjs.map +7 -0
  3. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-WIQUYO7H.mjs} +300 -213
  4. package/dist/lib/neutral/chunk-WIQUYO7H.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/{browser → neutral}/index.mjs +2832 -946
  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 +73 -224
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host.d.ts +53 -42
  12. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  13. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  14. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  15. package/dist/types/src/automerge/collection-synchronizer.d.ts +6 -4
  16. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  17. package/dist/types/src/automerge/echo-network-adapter.d.ts +22 -10
  18. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  19. package/dist/types/src/automerge/echo-replicator.d.ts +37 -15
  20. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  21. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  22. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  23. package/dist/types/src/automerge/index.d.ts +1 -1
  24. package/dist/types/src/automerge/index.d.ts.map +1 -1
  25. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +5 -5
  26. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  28. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  29. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +5 -5
  30. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  31. package/dist/types/src/common/codec.d.ts +1 -1
  32. package/dist/types/src/common/codec.d.ts.map +1 -1
  33. package/dist/types/src/db-host/automerge-data-source.d.ts +34 -0
  34. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  35. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  36. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  37. package/dist/types/src/db-host/data-service.d.ts +5 -4
  38. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  39. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  40. package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
  41. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  42. package/dist/types/src/db-host/echo-host.d.ts +44 -23
  43. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  44. package/dist/types/src/db-host/index.d.ts +1 -0
  45. package/dist/types/src/db-host/index.d.ts.map +1 -1
  46. package/dist/types/src/db-host/local-queue-service.d.ts +16 -0
  47. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  48. package/dist/types/src/db-host/query-service.d.ts +14 -9
  49. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  50. package/dist/types/src/db-host/queue-data-source.d.ts +28 -0
  51. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  52. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  53. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  54. package/dist/types/src/db-host/space-state-manager.d.ts +2 -1
  55. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  56. package/dist/types/src/db-host/stub.d.ts +10 -0
  57. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  58. package/dist/types/src/edge/echo-edge-replicator.d.ts +8 -6
  59. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  60. package/dist/types/src/filter/filter-match.d.ts +4 -1
  61. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  62. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  63. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  64. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  65. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  66. package/dist/types/src/query/errors.d.ts +21 -9
  67. package/dist/types/src/query/errors.d.ts.map +1 -1
  68. package/dist/types/src/query/plan.d.ts +57 -5
  69. package/dist/types/src/query/plan.d.ts.map +1 -1
  70. package/dist/types/src/query/query-executor.d.ts +49 -10
  71. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  72. package/dist/types/src/query/query-planner.d.ts +9 -0
  73. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  74. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  75. package/dist/types/src/space/auth.d.ts +3 -3
  76. package/dist/types/src/space/auth.d.ts.map +1 -1
  77. package/dist/types/src/space/control-pipeline.d.ts +3 -3
  78. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  79. package/dist/types/src/space/space-manager.d.ts +7 -7
  80. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  81. package/dist/types/src/space/space-protocol.d.ts +6 -6
  82. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  83. package/dist/types/src/space/space.d.ts +4 -4
  84. package/dist/types/src/space/space.d.ts.map +1 -1
  85. package/dist/types/src/testing/index.d.ts +0 -2
  86. package/dist/types/src/testing/index.d.ts.map +1 -1
  87. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  88. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  89. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  90. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  91. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  92. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  93. package/dist/types/src/util.d.ts +1 -1
  94. package/dist/types/src/util.d.ts.map +1 -1
  95. package/dist/types/tsconfig.tsbuildinfo +1 -1
  96. package/package.json +53 -45
  97. package/src/automerge/automerge-host.test.ts +55 -24
  98. package/src/automerge/automerge-host.ts +399 -126
  99. package/src/automerge/automerge-repo.test.ts +214 -46
  100. package/src/automerge/collection-synchronizer.test.ts +22 -15
  101. package/src/automerge/collection-synchronizer.ts +57 -22
  102. package/src/automerge/echo-data-monitor.ts +1 -1
  103. package/src/automerge/echo-network-adapter.test.ts +3 -3
  104. package/src/automerge/echo-network-adapter.ts +73 -28
  105. package/src/automerge/echo-replicator.ts +40 -15
  106. package/src/automerge/heads-store.ts +42 -3
  107. package/src/automerge/index.ts +1 -1
  108. package/src/automerge/leveldb-storage-adapter.ts +11 -11
  109. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  110. package/src/automerge/mesh-echo-replicator.ts +15 -11
  111. package/src/automerge/storage-adapter.test.ts +1 -1
  112. package/src/common/space-id.ts +1 -1
  113. package/src/db-host/automerge-data-source.test.ts +292 -0
  114. package/src/db-host/automerge-data-source.ts +144 -0
  115. package/src/db-host/data-service.ts +19 -20
  116. package/src/db-host/database-root.ts +2 -2
  117. package/src/db-host/documents-synchronizer.test.ts +73 -13
  118. package/src/db-host/documents-synchronizer.ts +58 -44
  119. package/src/db-host/echo-host.ts +194 -84
  120. package/src/db-host/index.ts +1 -0
  121. package/src/db-host/local-queue-service.ts +147 -0
  122. package/src/db-host/query-service.ts +28 -95
  123. package/src/db-host/queue-data-source.ts +132 -0
  124. package/src/db-host/queue-service.test.ts +145 -0
  125. package/src/db-host/space-state-manager.ts +6 -2
  126. package/src/db-host/stub.ts +28 -0
  127. package/src/edge/echo-edge-replicator.test.ts +8 -6
  128. package/src/edge/echo-edge-replicator.ts +124 -42
  129. package/src/filter/filter-match.test.ts +58 -32
  130. package/src/filter/filter-match.ts +163 -15
  131. package/src/metadata/metadata-store.ts +8 -5
  132. package/src/pipeline/pipeline-stress.test.ts +4 -2
  133. package/src/pipeline/pipeline.test.ts +3 -2
  134. package/src/pipeline/pipeline.ts +10 -7
  135. package/src/query/errors.ts +3 -1
  136. package/src/query/plan.ts +75 -5
  137. package/src/query/query-executor.ts +812 -117
  138. package/src/query/query-planner.test.ts +652 -34
  139. package/src/query/query-planner.ts +276 -13
  140. package/src/space/admission-discovery-extension.ts +3 -3
  141. package/src/space/auth.ts +6 -6
  142. package/src/space/control-pipeline.test.ts +4 -3
  143. package/src/space/control-pipeline.ts +11 -8
  144. package/src/space/space-manager.browser.test.ts +1 -1
  145. package/src/space/space-manager.ts +11 -10
  146. package/src/space/space-protocol.browser.test.ts +2 -2
  147. package/src/space/space-protocol.test.ts +3 -2
  148. package/src/space/space-protocol.ts +17 -14
  149. package/src/space/space.test.ts +1 -1
  150. package/src/space/space.ts +6 -5
  151. package/src/testing/index.ts +0 -2
  152. package/src/testing/test-agent-builder.ts +4 -3
  153. package/src/testing/test-network-adapter.ts +16 -3
  154. package/src/testing/test-replicator.ts +23 -18
  155. package/src/util.ts +7 -3
  156. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  157. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  158. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  159. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  160. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  161. package/dist/lib/browser/index.mjs.map +0 -7
  162. package/dist/lib/browser/meta.json +0 -1
  163. package/dist/lib/browser/testing/index.mjs.map +0 -7
  164. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  165. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  166. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  167. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  168. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  169. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  170. package/dist/lib/node/filter/index.cjs +0 -32
  171. package/dist/lib/node/filter/index.cjs.map +0 -7
  172. package/dist/lib/node/index.cjs +0 -4699
  173. package/dist/lib/node/index.cjs.map +0 -7
  174. package/dist/lib/node/meta.json +0 -1
  175. package/dist/lib/node/testing/index.cjs +0 -753
  176. package/dist/lib/node/testing/index.cjs.map +0 -7
  177. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  178. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  179. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  180. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  181. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  182. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  183. package/dist/lib/node-esm/filter/index.mjs +0 -11
  184. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  185. package/dist/lib/node-esm/index.mjs +0 -4743
  186. package/dist/lib/node-esm/index.mjs.map +0 -7
  187. package/dist/lib/node-esm/meta.json +0 -1
  188. package/dist/lib/node-esm/testing/index.mjs +0 -742
  189. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  190. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  191. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  192. package/dist/types/src/testing/test-data.d.ts +0 -18
  193. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  194. package/dist/types/src/testing/test-schema.d.ts +0 -37
  195. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  196. package/src/db-host/documents-iterator.ts +0 -74
  197. package/src/testing/test-data.ts +0 -127
  198. package/src/testing/test-schema.ts +0 -53
  199. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -2,6 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { Order } from '@dxos/echo';
5
6
  import { type QueryAST } from '@dxos/echo-protocol';
6
7
  import { invariant } from '@dxos/invariant';
7
8
  import type { DXN, SpaceId } from '@dxos/keys';
@@ -35,6 +36,8 @@ export class QueryPlanner {
35
36
  let plan = this._generate(query, { ...DEFAULT_CONTEXT, originalQuery: query });
36
37
  plan = this._optimizeEmptyFilters(plan);
37
38
  plan = this._optimizeSoloUnions(plan);
39
+ plan = this._ensureOrderStep(plan);
40
+ plan = this._optimizeLimits(plan);
38
41
  return plan;
39
42
  }
40
43
 
@@ -54,12 +57,19 @@ export class QueryPlanner {
54
57
  return this._generateRelationTraversalClause(query, context);
55
58
  case 'reference-traversal':
56
59
  return this._generateReferenceTraversalClause(query, context);
60
+ case 'hierarchy-traversal':
61
+ return this._generateHierarchyTraversalClause(query, context);
57
62
  case 'union':
58
63
  return this._generateUnionClause(query, context);
59
64
  case 'set-difference':
60
65
  return this._generateSetDifferenceClause(query, context);
66
+ case 'order':
67
+ return this._generateOrderClause(query, context);
68
+ case 'limit':
69
+ return this._generateLimitClause(query, context);
61
70
  default:
62
- throw new QueryError(`Unsupported query type: ${(query as any).type}`, {
71
+ throw new QueryError({
72
+ message: `Unsupported query type: ${(query as any).type}`,
63
73
  context: { query: context.originalQuery },
64
74
  });
65
75
  }
@@ -72,6 +82,12 @@ export class QueryPlanner {
72
82
  if (query.options.spaceIds) {
73
83
  newContext.selectionSpaces = query.options.spaceIds as readonly SpaceId[];
74
84
  }
85
+ if (query.options.allQueuesFromSpaces !== undefined) {
86
+ newContext.selectionAllQueuesFromSpaces = query.options.allQueuesFromSpaces;
87
+ }
88
+ if (query.options.queues) {
89
+ newContext.selectionQueues = query.options.queues as readonly DXN.String[];
90
+ }
75
91
  if (query.options.deleted) {
76
92
  newContext.deletedHandling = query.options.deleted;
77
93
  }
@@ -86,6 +102,7 @@ export class QueryPlanner {
86
102
  // 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
103
  private _generateSelectionFromFilter(filter: QueryAST.Filter, context: GenerationContext): QueryPlan.Plan {
88
104
  switch (filter.type) {
105
+ // Props
89
106
  case 'object': {
90
107
  if (
91
108
  context.selectionInverted &&
@@ -102,7 +119,10 @@ export class QueryPlanner {
102
119
  ]);
103
120
  }
104
121
  if (context.selectionInverted) {
105
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
122
+ throw new QueryError({
123
+ message: 'Query too complex',
124
+ context: { query: context.originalQuery },
125
+ });
106
126
  }
107
127
 
108
128
  // Try to utilize indexes during selection, prioritizing selecting by id, then by typename.
@@ -112,6 +132,8 @@ export class QueryPlanner {
112
132
  {
113
133
  _tag: 'SelectStep',
114
134
  spaces: context.selectionSpaces,
135
+ allQueuesFromSpaces: context.selectionAllQueuesFromSpaces,
136
+ queues: context.selectionQueues,
115
137
  selector: {
116
138
  _tag: 'IdSelector',
117
139
  objectIds: filter.id,
@@ -128,6 +150,8 @@ export class QueryPlanner {
128
150
  {
129
151
  _tag: 'SelectStep',
130
152
  spaces: context.selectionSpaces,
153
+ allQueuesFromSpaces: context.selectionAllQueuesFromSpaces,
154
+ queues: context.selectionQueues,
131
155
  selector: {
132
156
  _tag: 'TypeSelector',
133
157
  typename: [filter.typename as DXN.String],
@@ -135,9 +159,10 @@ export class QueryPlanner {
135
159
  },
136
160
  },
137
161
  ...this._generateDeletedHandlingSteps(context),
162
+ // 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
163
  {
139
164
  _tag: 'FilterStep',
140
- filter: { ...filter, typename: null },
165
+ filter: { ...filter },
141
166
  },
142
167
  ]);
143
168
  } else {
@@ -145,6 +170,8 @@ export class QueryPlanner {
145
170
  {
146
171
  _tag: 'SelectStep',
147
172
  spaces: context.selectionSpaces,
173
+ allQueuesFromSpaces: context.selectionAllQueuesFromSpaces,
174
+ queues: context.selectionQueues,
148
175
  selector: {
149
176
  _tag: 'WildcardSelector',
150
177
  },
@@ -157,44 +184,76 @@ export class QueryPlanner {
157
184
  ]);
158
185
  }
159
186
  }
187
+
188
+ // Tag
189
+ case 'tag': {
190
+ return QueryPlan.Plan.make([
191
+ {
192
+ _tag: 'SelectStep',
193
+ spaces: context.selectionSpaces,
194
+ allQueuesFromSpaces: context.selectionAllQueuesFromSpaces,
195
+ queues: context.selectionQueues,
196
+ selector: {
197
+ _tag: 'WildcardSelector',
198
+ },
199
+ },
200
+ ...this._generateDeletedHandlingSteps(context),
201
+ {
202
+ _tag: 'FilterStep',
203
+ filter: { ...filter },
204
+ },
205
+ ]);
206
+ }
207
+
208
+ // Text
160
209
  case 'text-search': {
161
210
  return QueryPlan.Plan.make([
162
211
  {
163
212
  _tag: 'SelectStep',
164
213
  spaces: context.selectionSpaces,
214
+ allQueuesFromSpaces: context.selectionAllQueuesFromSpaces,
215
+ queues: context.selectionQueues,
165
216
  selector: {
166
217
  _tag: 'TextSelector',
167
218
  text: filter.text,
168
219
  searchKind: filter.searchKind ?? this._options.defaultTextSearchKind,
220
+ typename: null,
169
221
  },
170
222
  },
171
223
  ...this._generateDeletedHandlingSteps(context),
172
224
  ]);
173
225
  }
226
+
227
+ // Compare
174
228
  case 'compare':
175
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
229
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
176
230
  case 'in':
177
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
231
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
178
232
  case 'range':
179
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
233
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
234
+
235
+ // Boolean
180
236
  case 'not':
181
237
  return this._generateSelectionFromFilter(filter.filter, {
182
238
  ...context,
183
239
  selectionInverted: !context.selectionInverted,
184
240
  });
185
241
  case 'and':
186
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
242
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
187
243
  case 'or':
188
244
  // Optimized case
189
245
  if (filter.filters.every(isTrivialTypenameFilter)) {
190
- const typenames = filter.filters.map((f) => {
191
- invariant(f.type === 'object' && f.typename !== null);
192
- return f.typename;
246
+ const typenames = filter.filters.map((filter) => {
247
+ invariant(filter.type === 'object' && filter.typename !== null);
248
+ return filter.typename;
193
249
  });
250
+
194
251
  return QueryPlan.Plan.make([
195
252
  {
196
253
  _tag: 'SelectStep',
197
254
  spaces: context.selectionSpaces,
255
+ allQueuesFromSpaces: context.selectionAllQueuesFromSpaces,
256
+ queues: context.selectionQueues,
198
257
  selector: {
199
258
  _tag: 'TypeSelector',
200
259
  typename: typenames as DXN.String[],
@@ -202,13 +261,18 @@ export class QueryPlanner {
202
261
  },
203
262
  },
204
263
  ...this._generateDeletedHandlingSteps(context),
264
+ {
265
+ _tag: 'FilterStep',
266
+ filter: { ...filter },
267
+ },
205
268
  ]);
206
269
  } else {
207
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
270
+ throw new QueryError({ message: 'Query too complex', context: { query: context.originalQuery } });
208
271
  }
209
272
 
210
273
  default:
211
- throw new QueryError(`Unsupported filter type: ${(filter as any).type}`, {
274
+ throw new QueryError({
275
+ message: `Unsupported filter type: ${(filter as any).type}`,
212
276
  context: { query: context.originalQuery },
213
277
  });
214
278
  }
@@ -286,7 +350,7 @@ export class QueryPlanner {
286
350
  traversal: {
287
351
  _tag: 'ReferenceTraversal',
288
352
  direction: 'incoming',
289
- property: query.property,
353
+ property: query.property ?? null,
290
354
  },
291
355
  },
292
356
  ...this._generateDeletedHandlingSteps(context),
@@ -392,6 +456,23 @@ export class QueryPlanner {
392
456
  ]);
393
457
  }
394
458
 
459
+ private _generateHierarchyTraversalClause(
460
+ query: QueryAST.QueryHierarchyTraversalClause,
461
+ context: GenerationContext,
462
+ ): QueryPlan.Plan {
463
+ return QueryPlan.Plan.make([
464
+ ...this._generate(query.anchor, context).steps,
465
+ {
466
+ _tag: 'TraverseStep',
467
+ traversal: {
468
+ _tag: 'HierarchyTraversal',
469
+ direction: query.direction,
470
+ },
471
+ },
472
+ ...this._generateDeletedHandlingSteps(context),
473
+ ]);
474
+ }
475
+
395
476
  /**
396
477
  * Removes filter steps that have no predicates.
397
478
  */
@@ -425,6 +506,176 @@ export class QueryPlanner {
425
506
  // TODO(dmaretskyi): Implement this.
426
507
  return plan;
427
508
  }
509
+
510
+ private _generateOrderClause(query: QueryAST.QueryOrderClause, context: GenerationContext): QueryPlan.Plan {
511
+ return QueryPlan.Plan.make([
512
+ ...this._generate(query.query, context).steps,
513
+ {
514
+ _tag: 'OrderStep',
515
+ order: query.order,
516
+ },
517
+ ]);
518
+ }
519
+
520
+ private _generateLimitClause(query: QueryAST.QueryLimitClause, context: GenerationContext): QueryPlan.Plan {
521
+ return QueryPlan.Plan.make([
522
+ ...this._generate(query.query, context).steps,
523
+ {
524
+ _tag: 'LimitStep',
525
+ limit: query.limit,
526
+ },
527
+ ]);
528
+ }
529
+
530
+ // After complete plan is built, inspect it from the end:
531
+ // - Walk backwards until hitting an object set changer.
532
+ // - If an order step is found, skip.
533
+ // - Otherwise insert natural order before any limit steps (since limit should be applied after ordering).
534
+ // This method is recursive and also processes sub-plans in unions and set differences.
535
+ private _ensureOrderStep(plan: QueryPlan.Plan): QueryPlan.Plan {
536
+ // First, recursively process any sub-plans.
537
+ const processedSteps = plan.steps.map((step): QueryPlan.Step => {
538
+ if (step._tag === 'UnionStep') {
539
+ return {
540
+ _tag: 'UnionStep',
541
+ plans: step.plans.map((subPlan) => this._ensureOrderStep(subPlan)),
542
+ };
543
+ } else if (step._tag === 'SetDifferenceStep') {
544
+ return {
545
+ _tag: 'SetDifferenceStep',
546
+ source: this._ensureOrderStep(step.source),
547
+ exclude: this._ensureOrderStep(step.exclude),
548
+ };
549
+ }
550
+ return step;
551
+ });
552
+
553
+ const processedPlan = QueryPlan.Plan.make(processedSteps);
554
+
555
+ const OBJECT_SET_CHANGERS = new Set(['SelectStep', 'TraverseStep', 'UnionStep', 'SetDifferenceStep']);
556
+ for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
557
+ const step = processedPlan.steps[i];
558
+ if (step._tag === 'OrderStep') {
559
+ return processedPlan;
560
+ }
561
+ if (OBJECT_SET_CHANGERS.has(step._tag)) {
562
+ break;
563
+ }
564
+ }
565
+
566
+ // Find the position to insert the order step (before any limit steps).
567
+ let insertIndex = processedPlan.steps.length;
568
+ for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
569
+ if (processedPlan.steps[i]._tag === 'LimitStep') {
570
+ insertIndex = i;
571
+ } else {
572
+ break;
573
+ }
574
+ }
575
+
576
+ const newSteps = [...processedPlan.steps];
577
+ newSteps.splice(insertIndex, 0, {
578
+ _tag: 'OrderStep',
579
+ order: [Order.natural.ast],
580
+ });
581
+
582
+ return QueryPlan.Plan.make(newSteps);
583
+ }
584
+
585
+ /**
586
+ * Propagates limits from LimitStep to SelectStep and OrderStep when possible.
587
+ * Limits can only be propagated if there are no unions or traversals between the LimitStep and SelectStep/OrderStep.
588
+ */
589
+ private _optimizeLimits(plan: QueryPlan.Plan): QueryPlan.Plan {
590
+ // First, recursively process any sub-plans.
591
+ const processedSteps = plan.steps.map((step): QueryPlan.Step => {
592
+ if (step._tag === 'UnionStep') {
593
+ return {
594
+ _tag: 'UnionStep',
595
+ plans: step.plans.map((subPlan) => this._optimizeLimits(subPlan)),
596
+ };
597
+ } else if (step._tag === 'SetDifferenceStep') {
598
+ return {
599
+ _tag: 'SetDifferenceStep',
600
+ source: this._optimizeLimits(step.source),
601
+ exclude: this._optimizeLimits(step.exclude),
602
+ };
603
+ }
604
+ return step;
605
+ });
606
+
607
+ // Find if there's a LimitStep at the end (possibly after OrderStep).
608
+ let limitStepIndex = -1;
609
+ let limitValue: number | undefined;
610
+
611
+ for (let i = processedSteps.length - 1; i >= 0; i--) {
612
+ const step = processedSteps[i];
613
+ if (step._tag === 'LimitStep') {
614
+ limitStepIndex = i;
615
+ limitValue = step.limit;
616
+ break;
617
+ }
618
+ // Only allow OrderStep after LimitStep when looking backwards.
619
+ if (step._tag !== 'OrderStep') {
620
+ break;
621
+ }
622
+ }
623
+
624
+ if (limitStepIndex === -1 || limitValue === undefined) {
625
+ return QueryPlan.Plan.make(processedSteps);
626
+ }
627
+
628
+ // Check if we can propagate the limit to a SelectStep and/or OrderStep.
629
+ // We can only do this if there are no unions, traversals, or set differences between them.
630
+ const BLOCKERS = new Set(['UnionStep', 'TraverseStep', 'SetDifferenceStep']);
631
+
632
+ let selectStepIndex = -1;
633
+ let orderStepIndex = -1;
634
+ for (let i = 0; i < limitStepIndex; i++) {
635
+ const step = processedSteps[i];
636
+ if (step._tag === 'SelectStep') {
637
+ selectStepIndex = i;
638
+ }
639
+ if (step._tag === 'OrderStep') {
640
+ orderStepIndex = i;
641
+ }
642
+ if (BLOCKERS.has(step._tag)) {
643
+ // Found a blocker after the select/order step - can't propagate.
644
+ selectStepIndex = -1;
645
+ orderStepIndex = -1;
646
+ }
647
+ }
648
+
649
+ if (selectStepIndex === -1 && orderStepIndex === -1) {
650
+ return QueryPlan.Plan.make(processedSteps);
651
+ }
652
+
653
+ // Create a mutable copy of steps to modify.
654
+ const newSteps = [...processedSteps];
655
+
656
+ // Propagate the limit to the SelectStep if found.
657
+ if (selectStepIndex !== -1) {
658
+ const selectStep = newSteps[selectStepIndex] as QueryPlan.SelectStep;
659
+ newSteps[selectStepIndex] = {
660
+ ...selectStep,
661
+ limit: limitValue,
662
+ };
663
+ }
664
+
665
+ // Propagate the limit to the OrderStep if found.
666
+ if (orderStepIndex !== -1) {
667
+ const orderStep = newSteps[orderStepIndex] as QueryPlan.OrderStep;
668
+ newSteps[orderStepIndex] = {
669
+ ...orderStep,
670
+ limit: limitValue,
671
+ };
672
+ }
673
+
674
+ // Remove the LimitStep since limit is now propagated.
675
+ newSteps.splice(limitStepIndex, 1);
676
+
677
+ return QueryPlan.Plan.make(newSteps);
678
+ }
428
679
  }
429
680
 
430
681
  /**
@@ -441,6 +692,16 @@ type GenerationContext = {
441
692
  */
442
693
  selectionSpaces: readonly SpaceId[];
443
694
 
695
+ /**
696
+ * Whether to select from all queues in the spaces specified by selectionSpaces.
697
+ */
698
+ selectionAllQueuesFromSpaces: boolean;
699
+
700
+ /**
701
+ * Which queues to select from.
702
+ */
703
+ selectionQueues: readonly DXN.String[];
704
+
444
705
  /**
445
706
  * How to handle deleted objects.
446
707
  */
@@ -455,6 +716,8 @@ type GenerationContext = {
455
716
  const DEFAULT_CONTEXT: GenerationContext = {
456
717
  originalQuery: null,
457
718
  selectionSpaces: [],
719
+ selectionAllQueuesFromSpaces: false,
720
+ selectionQueues: [],
458
721
  deletedHandling: 'exclude',
459
722
  selectionInverted: false,
460
723
  };
@@ -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
  },
package/src/space/auth.ts CHANGED
@@ -15,7 +15,7 @@ export type AuthProvider = (nonce: Uint8Array) => Promise<Uint8Array | undefined
15
15
 
16
16
  export type AuthVerifier = (nonce: Uint8Array, credential: Uint8Array) => Promise<boolean>;
17
17
 
18
- export type AuthExtensionParams = {
18
+ export type AuthExtensionProps = {
19
19
  provider: AuthProvider;
20
20
  verifier: AuthVerifier;
21
21
  onAuthSuccess: () => void;
@@ -29,7 +29,7 @@ export class AuthExtension extends RpcExtension<Services, Services> {
29
29
  },
30
30
  });
31
31
 
32
- constructor(private readonly _authParams: AuthExtensionParams) {
32
+ constructor(private readonly _authProps: AuthExtensionProps) {
33
33
  super({
34
34
  requested: {
35
35
  AuthService: schema.getService('dxos.mesh.teleport.auth.AuthService'),
@@ -46,7 +46,7 @@ export class AuthExtension extends RpcExtension<Services, Services> {
46
46
  AuthService: {
47
47
  authenticate: async ({ challenge }) => {
48
48
  try {
49
- const credential = await this._authParams.provider(challenge);
49
+ const credential = await this._authProps.provider(challenge);
50
50
  if (!credential) {
51
51
  throw new Error('auth rejected');
52
52
  }
@@ -67,13 +67,13 @@ export class AuthExtension extends RpcExtension<Services, Services> {
67
67
  const challenge = randomBytes(32);
68
68
  const { credential } = await this.rpc.AuthService.authenticate({ challenge });
69
69
  invariant(credential?.length > 0, 'invalid credential');
70
- const success = await this._authParams.verifier(challenge, credential);
70
+ const success = await this._authProps.verifier(challenge, credential);
71
71
  invariant(success, 'credential not verified');
72
- runInContext(this._ctx, () => this._authParams.onAuthSuccess());
72
+ runInContext(this._ctx, () => this._authProps.onAuthSuccess());
73
73
  } catch (err) {
74
74
  log('auth failed', err);
75
75
  this.close();
76
- this._authParams.onAuthFailure();
76
+ this._authProps.onAuthFailure();
77
77
  }
78
78
  });
79
79
  }
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { describe, expect, test, onTestFinished } from 'vitest';
5
+ import { describe, expect, onTestFinished, test } from 'vitest';
6
6
 
7
7
  import { CredentialGenerator, createCredential } from '@dxos/credentials';
8
8
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
@@ -11,13 +11,14 @@ import { type PublicKey } from '@dxos/keys';
11
11
  import { log } from '@dxos/log';
12
12
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
13
  import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
14
- import { createStorage, StorageType } from '@dxos/random-access-storage';
14
+ import { StorageType, createStorage } from '@dxos/random-access-storage';
15
15
  import { Timeframe } from '@dxos/timeframe';
16
16
 
17
- import { ControlPipeline } from './control-pipeline';
18
17
  import { valueEncoding } from '../common';
19
18
  import { MetadataStore } from '../metadata';
20
19
 
20
+ import { ControlPipeline } from './control-pipeline';
21
+
21
22
  describe('space/control-pipeline', () => {
22
23
  test('admits feeds', async () => {
23
24
  const keyring = new Keyring();
@@ -2,14 +2,14 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { DeferredTask, sleepWithContext, trackLeaks } from '@dxos/async';
5
+ import { DeferredTask, scheduleMicroTask, sleepWithContext, trackLeaks } from '@dxos/async';
6
6
  import { Context } from '@dxos/context';
7
7
  import {
8
- SpaceStateMachine,
9
- type SpaceState,
10
- type MemberInfo,
11
- type FeedInfo,
12
8
  type DelegateInvitationCredential,
9
+ type FeedInfo,
10
+ type MemberInfo,
11
+ type SpaceState,
12
+ SpaceStateMachine,
13
13
  } from '@dxos/credentials';
14
14
  import { type FeedWrapper } from '@dxos/feed-store';
15
15
  import { PublicKey } from '@dxos/keys';
@@ -25,7 +25,7 @@ import { type AsyncCallback, Callback, tracer } from '@dxos/util';
25
25
  import { type MetadataStore } from '../metadata';
26
26
  import { Pipeline, type PipelineAccessor } from '../pipeline';
27
27
 
28
- export type ControlPipelineParams = {
28
+ export type ControlPipelineProps = {
29
29
  spaceKey: PublicKey;
30
30
  genesisFeed: FeedWrapper<FeedMessage>;
31
31
  feedProvider: (feedKey: PublicKey) => Promise<FeedWrapper<FeedMessage>>;
@@ -70,7 +70,7 @@ export class ControlPipeline {
70
70
  await this._saveSnapshot();
71
71
  });
72
72
 
73
- constructor({ spaceKey, genesisFeed, feedProvider, metadataStore }: ControlPipelineParams) {
73
+ constructor({ spaceKey, genesisFeed, feedProvider, metadataStore }: ControlPipelineProps) {
74
74
  this._spaceKey = spaceKey;
75
75
  this._metadata = metadataStore;
76
76
  this._pipeline = new Pipeline();
@@ -83,9 +83,12 @@ export class ControlPipeline {
83
83
 
84
84
  // TODO(burdon): Check not stopping.
85
85
  if (info.assertion.designation === AdmittedFeed.Designation.CONTROL && !info.key.equals(genesisFeed.key)) {
86
- queueMicrotask(async () => {
86
+ scheduleMicroTask(this._ctx, async () => {
87
87
  try {
88
88
  const feed = await feedProvider(info.key);
89
+ if (this._ctx.disposed) {
90
+ return;
91
+ }
89
92
  if (!this._pipeline.hasFeed(feed.key)) {
90
93
  await this._pipeline.addFeed(feed);
91
94
  }
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { describe, test, onTestFinished } from 'vitest';
5
+ import { describe, onTestFinished, test } from 'vitest';
6
6
 
7
7
  import { createStorage } from '@dxos/random-access-storage';
8
8
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { type AutomergeUrl, parseAutomergeUrl } from '@automerge/automerge-repo';
6
6
 
7
- import { synchronized, trackLeaks, Trigger } from '@dxos/async';
8
- import { getCredentialAssertion, type DelegateInvitationCredential, type MemberInfo } from '@dxos/credentials';
7
+ import { Trigger, synchronized, trackLeaks } from '@dxos/async';
8
+ import { type DelegateInvitationCredential, type MemberInfo, getCredentialAssertion } from '@dxos/credentials';
9
9
  import { failUndefined } from '@dxos/debug';
10
10
  import { type FeedStore } from '@dxos/feed-store';
11
11
  import { PublicKey } from '@dxos/keys';
@@ -19,13 +19,14 @@ import { type Teleport } from '@dxos/teleport';
19
19
  import { type BlobStore } from '@dxos/teleport-extension-object-sync';
20
20
  import { ComplexMap } from '@dxos/util';
21
21
 
22
+ import { createIdFromSpaceKey } from '../common/space-id';
23
+ import { type MetadataStore } from '../metadata';
24
+
22
25
  import { CredentialRetrieverExtension } from './admission-discovery-extension';
23
26
  import { Space } from './space';
24
27
  import { SpaceProtocol, type SwarmIdentity } from './space-protocol';
25
- import { createIdFromSpaceKey } from '../common/space-id';
26
- import { type MetadataStore } from '../metadata';
27
28
 
28
- export type SpaceManagerParams = {
29
+ export type SpaceManagerProps = {
29
30
  feedStore: FeedStore<FeedMessage>;
30
31
  networkManager: SwarmNetworkManager;
31
32
  metadataStore: MetadataStore;
@@ -35,7 +36,7 @@ export type SpaceManagerParams = {
35
36
  disableP2pReplication?: boolean;
36
37
  };
37
38
 
38
- export type ConstructSpaceParams = {
39
+ export type ConstructSpaceProps = {
39
40
  metadata: SpaceMetadata;
40
41
  swarmIdentity: SwarmIdentity;
41
42
  memberKey: PublicKey;
@@ -48,7 +49,7 @@ export type ConstructSpaceParams = {
48
49
  onMemberRolesChanged: (member: MemberInfo[]) => Promise<void>;
49
50
  };
50
51
 
51
- export type RequestSpaceAdmissionCredentialParams = {
52
+ export type RequestSpaceAdmissionCredentialProps = {
52
53
  spaceKey: PublicKey;
53
54
  identityKey: PublicKey;
54
55
  swarmIdentity: SwarmIdentity;
@@ -68,7 +69,7 @@ export class SpaceManager {
68
69
  private readonly _instanceId = PublicKey.random().toHex();
69
70
  private readonly _disableP2pReplication: boolean;
70
71
 
71
- constructor({ feedStore, networkManager, metadataStore, blobStore, disableP2pReplication }: SpaceManagerParams) {
72
+ constructor({ feedStore, networkManager, metadataStore, blobStore, disableP2pReplication }: SpaceManagerProps) {
72
73
  // TODO(burdon): Assert.
73
74
  this._feedStore = feedStore;
74
75
  this._networkManager = networkManager;
@@ -98,7 +99,7 @@ export class SpaceManager {
98
99
  onDelegatedInvitationStatusChange,
99
100
  onMemberRolesChanged,
100
101
  memberKey,
101
- }: ConstructSpaceParams): Promise<Space> {
102
+ }: ConstructSpaceProps): Promise<Space> {
102
103
  log.trace('dxos.echo.space-manager.construct-space', trace.begin({ id: this._instanceId }));
103
104
  log('constructing space...', { spaceKey: metadata.genesisFeedKey });
104
105
 
@@ -134,7 +135,7 @@ export class SpaceManager {
134
135
  return space;
135
136
  }
136
137
 
137
- public async requestSpaceAdmissionCredential(params: RequestSpaceAdmissionCredentialParams): Promise<Credential> {
138
+ public async requestSpaceAdmissionCredential(params: RequestSpaceAdmissionCredentialProps): Promise<Credential> {
138
139
  const traceKey = 'dxos.echo.space-manager.request-space-admission';
139
140
  log.trace(traceKey, trace.begin({ id: this._instanceId }));
140
141
  log('requesting space admission credential...', { spaceKey: params.spaceKey });