@dxos/echo-pipeline 0.8.2-staging.7ac8446 → 0.8.2

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,470 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type QueryAST } from '@dxos/echo-protocol';
6
+ import { invariant } from '@dxos/invariant';
7
+ import type { DXN, SpaceId } from '@dxos/keys';
8
+
9
+ import { QueryError } from './errors';
10
+ import { QueryPlan } from './plan';
11
+
12
+ export type QueryPlannerOptions = {
13
+ defaultTextSearchKind: QueryPlan.TextSearchKind;
14
+ };
15
+
16
+ const DEFAULT_OPTIONS: QueryPlannerOptions = {
17
+ defaultTextSearchKind: 'full-text',
18
+ };
19
+
20
+ /**
21
+ * Constructs an optimized query plan.
22
+ */
23
+ // TODO(dmaretskyi): Implement inefficient versions of complex queries.
24
+ export class QueryPlanner {
25
+ private readonly _options: QueryPlannerOptions;
26
+
27
+ constructor(options?: Partial<QueryPlannerOptions>) {
28
+ this._options = {
29
+ ...DEFAULT_OPTIONS,
30
+ ...options,
31
+ };
32
+ }
33
+
34
+ createPlan(query: QueryAST.Query): QueryPlan.Plan {
35
+ let plan = this._generate(query, { ...DEFAULT_CONTEXT, originalQuery: query });
36
+ plan = this._optimizeEmptyFilters(plan);
37
+ plan = this._optimizeSoloUnions(plan);
38
+ return plan;
39
+ }
40
+
41
+ private _generate(query: QueryAST.Query, context: GenerationContext): QueryPlan.Plan {
42
+ switch (query.type) {
43
+ case 'options':
44
+ return this._generateOptionsClause(query, context);
45
+ case 'select':
46
+ return this._generateSelectClause(query, context);
47
+ case 'filter':
48
+ return this._generateFilterClause(query, context);
49
+ case 'incoming-references':
50
+ return this._generateIncomingReferencesClause(query, context);
51
+ case 'relation':
52
+ return this._generateRelationClause(query, context);
53
+ case 'relation-traversal':
54
+ return this._generateRelationTraversalClause(query, context);
55
+ case 'reference-traversal':
56
+ return this._generateReferenceTraversalClause(query, context);
57
+ case 'union':
58
+ return this._generateUnionClause(query, context);
59
+ default:
60
+ throw new QueryError(`Unsupported query type: ${(query as any).type}`, {
61
+ context: { query: context.originalQuery },
62
+ });
63
+ }
64
+ }
65
+
66
+ private _generateOptionsClause(query: QueryAST.QueryOptionsClause, context: GenerationContext): QueryPlan.Plan {
67
+ const newContext = {
68
+ ...context,
69
+ };
70
+ if (query.options.spaceIds) {
71
+ newContext.selectionSpaces = query.options.spaceIds as readonly SpaceId[];
72
+ }
73
+ if (query.options.deleted) {
74
+ newContext.deletedHandling = query.options.deleted;
75
+ }
76
+ return this._generate(query.query, newContext);
77
+ }
78
+
79
+ private _generateSelectClause(query: QueryAST.QuerySelectClause, context: GenerationContext): QueryPlan.Plan {
80
+ return this._generateSelectionFromFilter(query.filter, context);
81
+ }
82
+
83
+ // TODO(dmaretskyi): This can be rewritten as a function of (filter[]) -> (selection ? undefined, rest: filter[]) that recurses onto itself.
84
+ // 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.
85
+ private _generateSelectionFromFilter(filter: QueryAST.Filter, context: GenerationContext): QueryPlan.Plan {
86
+ switch (filter.type) {
87
+ case 'object': {
88
+ if (
89
+ context.selectionInverted &&
90
+ filter.id === undefined &&
91
+ filter.typename === null &&
92
+ Object.keys(filter.props).length === 0
93
+ ) {
94
+ // filter of nothing -> clear working set.
95
+ return QueryPlan.Plan.make([
96
+ {
97
+ _tag: 'ClearWorkingSetStep',
98
+ },
99
+ ...this._generateDeletedHandlingSteps(context),
100
+ ]);
101
+ }
102
+ if (context.selectionInverted) {
103
+ throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
104
+ }
105
+
106
+ // Try to utilize indexes during selection, prioritizing selecting by id, then by typename.
107
+ // After selection, filter out using the remaining predicates.
108
+ if (filter.id && filter.id?.length > 0) {
109
+ return QueryPlan.Plan.make([
110
+ {
111
+ _tag: 'SelectStep',
112
+ spaces: context.selectionSpaces,
113
+ selector: {
114
+ _tag: 'IdSelector',
115
+ objectIds: filter.id,
116
+ },
117
+ },
118
+ ...this._generateDeletedHandlingSteps(context),
119
+ {
120
+ _tag: 'FilterStep',
121
+ filter: { ...filter, id: undefined },
122
+ },
123
+ ]);
124
+ } else if (filter.typename) {
125
+ return QueryPlan.Plan.make([
126
+ {
127
+ _tag: 'SelectStep',
128
+ spaces: context.selectionSpaces,
129
+ selector: {
130
+ _tag: 'TypeSelector',
131
+ typename: [filter.typename as DXN.String],
132
+ inverted: false,
133
+ },
134
+ },
135
+ ...this._generateDeletedHandlingSteps(context),
136
+ {
137
+ _tag: 'FilterStep',
138
+ filter: { ...filter, typename: null },
139
+ },
140
+ ]);
141
+ } else {
142
+ return QueryPlan.Plan.make([
143
+ {
144
+ _tag: 'SelectStep',
145
+ spaces: context.selectionSpaces,
146
+ selector: {
147
+ _tag: 'WildcardSelector',
148
+ },
149
+ },
150
+ ...this._generateDeletedHandlingSteps(context),
151
+ {
152
+ _tag: 'FilterStep',
153
+ filter: { ...filter },
154
+ },
155
+ ]);
156
+ }
157
+ }
158
+ case 'text-search': {
159
+ return QueryPlan.Plan.make([
160
+ {
161
+ _tag: 'SelectStep',
162
+ spaces: context.selectionSpaces,
163
+ selector: {
164
+ _tag: 'TextSelector',
165
+ text: filter.text,
166
+ searchKind: filter.searchKind ?? this._options.defaultTextSearchKind,
167
+ },
168
+ },
169
+ ...this._generateDeletedHandlingSteps(context),
170
+ ]);
171
+ }
172
+ case 'compare':
173
+ throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
174
+ case 'in':
175
+ throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
176
+ case 'range':
177
+ throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
178
+ case 'not':
179
+ return this._generateSelectionFromFilter(filter.filter, {
180
+ ...context,
181
+ selectionInverted: !context.selectionInverted,
182
+ });
183
+ case 'and':
184
+ throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
185
+ case 'or':
186
+ // Optimized case
187
+ if (filter.filters.every(isTrivialTypenameFilter)) {
188
+ const typenames = filter.filters.map((f) => {
189
+ invariant(f.type === 'object' && f.typename !== null);
190
+ return f.typename;
191
+ });
192
+ return QueryPlan.Plan.make([
193
+ {
194
+ _tag: 'SelectStep',
195
+ spaces: context.selectionSpaces,
196
+ selector: {
197
+ _tag: 'TypeSelector',
198
+ typename: typenames as DXN.String[],
199
+ inverted: context.selectionInverted,
200
+ },
201
+ },
202
+ ...this._generateDeletedHandlingSteps(context),
203
+ ]);
204
+ } else {
205
+ throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
206
+ }
207
+
208
+ default:
209
+ throw new QueryError(`Unsupported filter type: ${(filter as any).type}`, {
210
+ context: { query: context.originalQuery },
211
+ });
212
+ }
213
+ }
214
+
215
+ private _generateDeletedHandlingSteps(context: GenerationContext): QueryPlan.Step[] {
216
+ switch (context.deletedHandling) {
217
+ case 'include':
218
+ return [];
219
+ case 'exclude':
220
+ return [
221
+ {
222
+ _tag: 'FilterDeletedStep',
223
+ mode: 'only-non-deleted',
224
+ },
225
+ ];
226
+ case 'only':
227
+ return [
228
+ {
229
+ _tag: 'FilterDeletedStep',
230
+ mode: 'only-deleted',
231
+ },
232
+ ];
233
+ }
234
+ }
235
+
236
+ private _generateUnionClause(query: QueryAST.QueryUnionClause, context: GenerationContext): QueryPlan.Plan {
237
+ return QueryPlan.Plan.make([
238
+ {
239
+ _tag: 'UnionStep',
240
+ plans: query.queries.map((query) => this._generate(query, context)),
241
+ },
242
+ ]);
243
+ }
244
+
245
+ private _generateReferenceTraversalClause(
246
+ query: QueryAST.QueryReferenceTraversalClause,
247
+ context: GenerationContext,
248
+ ): QueryPlan.Plan {
249
+ return QueryPlan.Plan.make([
250
+ ...this._generate(query.anchor, context).steps,
251
+ {
252
+ _tag: 'TraverseStep',
253
+ traversal: {
254
+ _tag: 'ReferenceTraversal',
255
+ direction: 'outgoing',
256
+ property: query.property,
257
+ },
258
+ },
259
+ ...this._generateDeletedHandlingSteps(context),
260
+ ]);
261
+ }
262
+
263
+ private _generateIncomingReferencesClause(
264
+ query: QueryAST.QueryIncomingReferencesClause,
265
+ context: GenerationContext,
266
+ ): QueryPlan.Plan {
267
+ return QueryPlan.Plan.make([
268
+ ...this._generate(query.anchor, context).steps,
269
+ {
270
+ _tag: 'TraverseStep',
271
+ traversal: {
272
+ _tag: 'ReferenceTraversal',
273
+ direction: 'incoming',
274
+ property: query.property,
275
+ },
276
+ },
277
+ ...this._generateDeletedHandlingSteps(context),
278
+ {
279
+ _tag: 'FilterStep',
280
+ filter: {
281
+ type: 'object',
282
+ typename: query.typename,
283
+ props: {},
284
+ },
285
+ },
286
+ ]);
287
+ }
288
+
289
+ private _generateRelationTraversalClause(
290
+ query: QueryAST.QueryRelationTraversalClause,
291
+ context: GenerationContext,
292
+ ): QueryPlan.Plan {
293
+ switch (query.direction) {
294
+ case 'source': {
295
+ return QueryPlan.Plan.make([
296
+ ...this._generate(query.anchor, context).steps,
297
+ createRelationTraversalStep('relation-to-source'),
298
+ ...this._generateDeletedHandlingSteps(context),
299
+ ]);
300
+ }
301
+ case 'target': {
302
+ return QueryPlan.Plan.make([
303
+ ...this._generate(query.anchor, context).steps,
304
+ createRelationTraversalStep('relation-to-target'),
305
+ ...this._generateDeletedHandlingSteps(context),
306
+ ]);
307
+ }
308
+ case 'both': {
309
+ const anchorPlan = this._generate(query.anchor, context);
310
+ return QueryPlan.Plan.make([
311
+ ...anchorPlan.steps,
312
+ {
313
+ _tag: 'UnionStep',
314
+ plans: [
315
+ QueryPlan.Plan.make([createRelationTraversalStep('relation-to-source')]),
316
+ QueryPlan.Plan.make([createRelationTraversalStep('relation-to-target')]),
317
+ ],
318
+ },
319
+ ...this._generateDeletedHandlingSteps(context),
320
+ ]);
321
+ }
322
+ }
323
+ }
324
+
325
+ private _generateRelationClause(query: QueryAST.QueryRelationClause, context: GenerationContext): QueryPlan.Plan {
326
+ switch (query.direction) {
327
+ case 'outgoing': {
328
+ return QueryPlan.Plan.make([
329
+ ...this._generate(query.anchor, context).steps,
330
+ createRelationTraversalStep('source-to-relation'),
331
+ ...this._generateDeletedHandlingSteps(context),
332
+ {
333
+ _tag: 'FilterStep',
334
+ filter: query.filter ?? NOOP_FILTER,
335
+ },
336
+ ]);
337
+ }
338
+ case 'incoming': {
339
+ return QueryPlan.Plan.make([
340
+ ...this._generate(query.anchor, context).steps,
341
+ createRelationTraversalStep('target-to-relation'),
342
+ ...this._generateDeletedHandlingSteps(context),
343
+ {
344
+ _tag: 'FilterStep',
345
+ filter: query.filter ?? NOOP_FILTER,
346
+ },
347
+ ]);
348
+ }
349
+ case 'both': {
350
+ const anchorPlan = this._generate(query.anchor, context);
351
+ return QueryPlan.Plan.make([
352
+ ...anchorPlan.steps,
353
+ {
354
+ _tag: 'UnionStep',
355
+ plans: [
356
+ QueryPlan.Plan.make([createRelationTraversalStep('source-to-relation')]),
357
+ QueryPlan.Plan.make([createRelationTraversalStep('target-to-relation')]),
358
+ ],
359
+ },
360
+ ...this._generateDeletedHandlingSteps(context),
361
+ {
362
+ _tag: 'FilterStep',
363
+ filter: query.filter ?? NOOP_FILTER,
364
+ },
365
+ ]);
366
+ }
367
+ }
368
+ }
369
+
370
+ private _generateFilterClause(query: QueryAST.QueryFilterClause, context: GenerationContext): QueryPlan.Plan {
371
+ return QueryPlan.Plan.make([
372
+ ...this._generate(query.selection, context).steps,
373
+ {
374
+ _tag: 'FilterStep',
375
+ filter: query.filter,
376
+ },
377
+ ]);
378
+ }
379
+
380
+ /**
381
+ * Removes filter steps that have no predicates.
382
+ */
383
+ private _optimizeEmptyFilters(plan: QueryPlan.Plan): QueryPlan.Plan {
384
+ return QueryPlan.Plan.make(
385
+ plan.steps
386
+ .filter((step) => {
387
+ if (step._tag === 'FilterStep') {
388
+ return !QueryPlan.FilterStep.isNoop(step);
389
+ } else {
390
+ return true;
391
+ }
392
+ })
393
+ .map((step) => {
394
+ if (step._tag === 'UnionStep') {
395
+ return {
396
+ _tag: 'UnionStep',
397
+ plans: step.plans.map((plan) => this._optimizeEmptyFilters(plan)),
398
+ };
399
+ } else {
400
+ return step;
401
+ }
402
+ }),
403
+ );
404
+ }
405
+
406
+ /**
407
+ * Removes union steps that have only one child.
408
+ */
409
+ private _optimizeSoloUnions(plan: QueryPlan.Plan): QueryPlan.Plan {
410
+ // TODO(dmaretskyi): Implement this.
411
+ return plan;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Context for query planning.
417
+ */
418
+ type GenerationContext = {
419
+ /**
420
+ * The original query.
421
+ */
422
+ originalQuery: QueryAST.Query | null;
423
+
424
+ /**
425
+ * Which spaces to select from.
426
+ */
427
+ selectionSpaces: readonly SpaceId[];
428
+
429
+ /**
430
+ * How to handle deleted objects.
431
+ */
432
+ deletedHandling: 'include' | 'exclude' | 'only';
433
+
434
+ /**
435
+ * When generating a selection clause, whether to invert the filter.
436
+ */
437
+ selectionInverted: boolean;
438
+ };
439
+
440
+ const DEFAULT_CONTEXT: GenerationContext = {
441
+ originalQuery: null,
442
+ selectionSpaces: [],
443
+ deletedHandling: 'exclude',
444
+ selectionInverted: false,
445
+ };
446
+
447
+ const NOOP_FILTER: QueryAST.Filter = {
448
+ type: 'object',
449
+ typename: null,
450
+ id: [],
451
+ props: {},
452
+ };
453
+
454
+ const createRelationTraversalStep = (direction: QueryPlan.RelationTraversal['direction']): QueryPlan.Step => ({
455
+ _tag: 'TraverseStep',
456
+ traversal: {
457
+ _tag: 'RelationTraversal',
458
+ direction,
459
+ },
460
+ });
461
+
462
+ const isTrivialTypenameFilter = (filter: QueryAST.Filter): boolean => {
463
+ return (
464
+ filter.type === 'object' &&
465
+ filter.typename !== null &&
466
+ Object.keys(filter.props).length === 0 &&
467
+ (filter.id === undefined || filter.id.length === 0) &&
468
+ (filter.foreignKeys === undefined || filter.foreignKeys.length === 0)
469
+ );
470
+ };
@@ -40,7 +40,7 @@ export class CredentialRetrieverExtension extends RpcExtension<
40
40
  return {};
41
41
  }
42
42
 
43
- override async onOpen(context: ExtensionContext) {
43
+ override async onOpen(context: ExtensionContext): Promise<void> {
44
44
  await super.onOpen(context);
45
45
  scheduleTask(this._ctx, async () => {
46
46
  try {
@@ -52,7 +52,7 @@ export class CredentialRetrieverExtension extends RpcExtension<
52
52
  });
53
53
  }
54
54
 
55
- override async onClose() {
55
+ override async onClose(): Promise<void> {
56
56
  await this._ctx.dispose();
57
57
  }
58
58
 
@@ -112,13 +112,13 @@ export class ControlPipeline {
112
112
  return this._pipeline;
113
113
  }
114
114
 
115
- async setWriteFeed(feed: FeedWrapper<FeedMessage>) {
115
+ async setWriteFeed(feed: FeedWrapper<FeedMessage>): Promise<void> {
116
116
  await this._pipeline.addFeed(feed);
117
117
  this._pipeline.setWriteFeed(feed);
118
118
  }
119
119
 
120
120
  @trace.span({ showInBrowserTimeline: true })
121
- async start() {
121
+ async start(): Promise<void> {
122
122
  const snapshot = this._metadata.getSpaceControlPipelineSnapshot(this._spaceKey);
123
123
  log('load snapshot', { key: this._spaceKey, present: !!snapshot, tf: snapshot?.timeframe });
124
124
  if (USE_SNAPSHOTS && snapshot) {
@@ -134,7 +134,7 @@ export class ControlPipeline {
134
134
  log('started');
135
135
  }
136
136
 
137
- private async _processSnapshot(snapshot: ControlPipelineSnapshot) {
137
+ private async _processSnapshot(snapshot: ControlPipelineSnapshot): Promise<void> {
138
138
  await this._pipeline.setCursor(snapshot.timeframe);
139
139
 
140
140
  for (const message of snapshot.messages ?? []) {
@@ -149,7 +149,7 @@ export class ControlPipeline {
149
149
  }
150
150
  }
151
151
 
152
- private async _saveSnapshot() {
152
+ private async _saveSnapshot(): Promise<void> {
153
153
  await this._pipeline.pause();
154
154
  const snapshot: ControlPipelineSnapshot = {
155
155
  timeframe: this._pipeline.state.timeframe,
@@ -165,7 +165,7 @@ export class ControlPipeline {
165
165
  }
166
166
 
167
167
  @trace.span()
168
- private async _consumePipeline(ctx: Context) {
168
+ private async _consumePipeline(ctx: Context): Promise<void> {
169
169
  for await (const msg of this._pipeline.consume()) {
170
170
  const span = this._usage.beginRecording();
171
171
  this._mutations.inc();
@@ -200,7 +200,7 @@ export class ControlPipeline {
200
200
  }
201
201
  }
202
202
 
203
- private async _noteTargetStateIfNeeded(timeframe: Timeframe) {
203
+ private async _noteTargetStateIfNeeded(timeframe: Timeframe): Promise<void> {
204
204
  // TODO(dmaretskyi): Replace this with a proper debounce/throttle.
205
205
 
206
206
  if (Date.now() - this._lastTimeframeSaveTime > TIMEFRAME_SAVE_DEBOUNCE_INTERVAL) {
@@ -210,7 +210,7 @@ export class ControlPipeline {
210
210
  }
211
211
  }
212
212
 
213
- async stop() {
213
+ async stop(): Promise<void> {
214
214
  log('stopping...');
215
215
  await this._ctx.dispose();
216
216
  await this._pipeline.stop();
@@ -218,7 +218,7 @@ export class ControlPipeline {
218
218
  log('stopped');
219
219
  }
220
220
 
221
- private async _saveTargetTimeframe(timeframe: Timeframe) {
221
+ private async _saveTargetTimeframe(timeframe: Timeframe): Promise<void> {
222
222
  try {
223
223
  const newTimeframe = Timeframe.merge(this._targetTimeframe ?? new Timeframe(), timeframe);
224
224
  await this._metadata.setSpaceControlLatestTimeframe(this._spaceKey, newTimeframe);
@@ -2,8 +2,9 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import { type AutomergeUrl, parseAutomergeUrl } from '@automerge/automerge-repo';
6
+
5
7
  import { synchronized, trackLeaks, Trigger } from '@dxos/async';
6
- import { type AutomergeUrl, parseAutomergeUrl } from '@dxos/automerge/automerge-repo';
7
8
  import { getCredentialAssertion, type DelegateInvitationCredential, type MemberInfo } from '@dxos/credentials';
8
9
  import { failUndefined } from '@dxos/debug';
9
10
  import { type FeedStore } from '@dxos/feed-store';
@@ -82,10 +83,10 @@ export class SpaceManager {
82
83
  }
83
84
 
84
85
  @synchronized
85
- async open() {}
86
+ async open(): Promise<void> {}
86
87
 
87
88
  @synchronized
88
- async close() {
89
+ async close(): Promise<void> {
89
90
  await Promise.all([...this._spaces.values()].map((space) => space.close()));
90
91
  }
91
92
 
@@ -97,7 +98,7 @@ export class SpaceManager {
97
98
  onDelegatedInvitationStatusChange,
98
99
  onMemberRolesChanged,
99
100
  memberKey,
100
- }: ConstructSpaceParams) {
101
+ }: ConstructSpaceParams): Promise<Space> {
101
102
  log.trace('dxos.echo.space-manager.construct-space', trace.begin({ id: this._instanceId }));
102
103
  log('constructing space...', { spaceKey: metadata.genesisFeedKey });
103
104
 
@@ -17,6 +17,7 @@ const port = process.env.SIGNAL_PORT ?? 4000;
17
17
  const SIGNAL_URL = `ws://localhost:${port}/.well-known/dx/signal`;
18
18
 
19
19
  describe('space/space-protocol', () => {
20
+ // TODO(dmaretskyi): Fails with the vscode test-runner for some reason.
20
21
  test('two peers discover each other', async () => {
21
22
  const builder = new TestAgentBuilder();
22
23
  onTestFinished(async () => {
@@ -15,6 +15,7 @@ import { AuthStatus, MOCK_AUTH_PROVIDER, MOCK_AUTH_VERIFIER, SpaceProtocol } fro
15
15
  import { TestAgentBuilder, TestFeedBuilder } from '../testing';
16
16
 
17
17
  describe('space/space-protocol', () => {
18
+ // Flaky.
18
19
  test('two peers discover each other via presence', async () => {
19
20
  const builder = new TestAgentBuilder();
20
21
  onTestFinished(async () => {
@@ -124,7 +124,7 @@ export class SpaceProtocol {
124
124
  }
125
125
 
126
126
  // TODO(burdon): Create abstraction for Space (e.g., add keys and have provider).
127
- async addFeed(feed: FeedWrapper<FeedMessage>) {
127
+ async addFeed(feed: FeedWrapper<FeedMessage>): Promise<void> {
128
128
  log('addFeed', { key: feed.key });
129
129
 
130
130
  this._feeds.add(feed);
@@ -136,7 +136,7 @@ export class SpaceProtocol {
136
136
  }
137
137
 
138
138
  // TODO(burdon): Rename open? Common open/close interfaces for all services?
139
- async start() {
139
+ async start(): Promise<void> {
140
140
  if (this._connection) {
141
141
  return;
142
142
  }
@@ -158,11 +158,11 @@ export class SpaceProtocol {
158
158
  log('started');
159
159
  }
160
160
 
161
- public updateTopology() {
161
+ public updateTopology(): void {
162
162
  this._topology.forceUpdate();
163
163
  }
164
164
 
165
- async stop() {
165
+ async stop(): Promise<void> {
166
166
  await this.blobSync.close();
167
167
 
168
168
  if (this._connection) {
@@ -151,14 +151,14 @@ export class Space extends Resource {
151
151
  return this._controlPipeline.pipeline;
152
152
  }
153
153
 
154
- async setControlFeed(feed: FeedWrapper<FeedMessage>) {
154
+ async setControlFeed(feed: FeedWrapper<FeedMessage>): Promise<this> {
155
155
  invariant(!this._controlFeed, 'Control feed already set.');
156
156
  this._controlFeed = feed;
157
157
  await this._controlPipeline.setWriteFeed(feed);
158
158
  return this;
159
159
  }
160
160
 
161
- async setDataFeed(feed: FeedWrapper<FeedMessage>) {
161
+ async setDataFeed(feed: FeedWrapper<FeedMessage>): Promise<this> {
162
162
  invariant(!this._dataFeed, 'Data feed already set.');
163
163
  this._dataFeed = feed;
164
164
  return this;
@@ -172,7 +172,7 @@ export class Space extends Resource {
172
172
  }
173
173
 
174
174
  @trace.span()
175
- protected override async _open(ctx: Context) {
175
+ protected override async _open(ctx: Context): Promise<void> {
176
176
  log('opening...');
177
177
 
178
178
  // Order is important.
@@ -182,14 +182,14 @@ export class Space extends Resource {
182
182
  }
183
183
 
184
184
  @synchronized
185
- public async startProtocol() {
185
+ public async startProtocol(): Promise<void> {
186
186
  invariant(this.isOpen);
187
187
  await this.protocol.start();
188
188
  await this.protocol.addFeed(await this._feedProvider(this._genesisFeedKey));
189
189
  }
190
190
 
191
191
  @synchronized
192
- protected override async _close() {
192
+ protected override async _close(): Promise<void> {
193
193
  log('closing...', { key: this._key });
194
194
 
195
195
  // Closes in reverse order to open.
@@ -7,3 +7,5 @@ export * from './test-agent-builder';
7
7
  export * from './test-feed-builder';
8
8
  export * from './test-network-adapter';
9
9
  export * from './test-replicator';
10
+ export * as TestSchema from './test-schema';
11
+ export * as TestData from './test-data';