@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.
- package/dist/lib/browser/{chunk-32WDI3LB.mjs → chunk-3XSXS5EX.mjs} +15 -21
- package/dist/lib/browser/chunk-3XSXS5EX.mjs.map +7 -0
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +11 -0
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +7 -0
- package/dist/lib/browser/chunk-TQJTKNMS.mjs +126 -0
- package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +7 -0
- package/dist/lib/browser/filter/index.mjs +11 -0
- package/dist/lib/browser/filter/index.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1380 -516
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +202 -22
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/chunk-HOPOFWAL.cjs +147 -0
- package/dist/lib/node/chunk-HOPOFWAL.cjs.map +7 -0
- package/dist/lib/node/chunk-Q7SFCCGT.cjs +33 -0
- package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +7 -0
- package/dist/lib/node/{chunk-TC2PRBEU.cjs → chunk-SG2PL5RH.cjs} +18 -24
- package/dist/lib/node/chunk-SG2PL5RH.cjs.map +7 -0
- package/dist/lib/node/filter/index.cjs +32 -0
- package/dist/lib/node/filter/index.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1381 -525
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +207 -31
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/lib/node-esm/{chunk-UKOLB3LW.mjs → chunk-3BZP75TJ.mjs} +15 -21
- package/dist/lib/node-esm/chunk-3BZP75TJ.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs +126 -0
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +7 -0
- package/dist/lib/node-esm/filter/index.mjs +11 -0
- package/dist/lib/node-esm/filter/index.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1380 -516
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +202 -22
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/automerge/automerge-host.d.ts +6 -4
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/collection-synchronizer.d.ts +1 -1
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-data-monitor.d.ts +6 -6
- package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +4 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/heads-store.d.ts +2 -2
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
- package/dist/types/src/automerge/space-collection.d.ts +1 -1
- package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/common/space-id.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-metrics.d.ts +1 -1
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +7 -7
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-iterator.d.ts +1 -1
- package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +4 -4
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +13 -2
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +0 -1
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/query-service.d.ts +2 -0
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
- package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
- package/dist/types/src/filter/filter-match.d.ts +13 -0
- package/dist/types/src/filter/filter-match.d.ts.map +1 -0
- package/dist/types/src/filter/filter-match.test.d.ts +2 -0
- package/dist/types/src/filter/filter-match.test.d.ts.map +1 -0
- package/dist/types/src/filter/index.d.ts +2 -0
- package/dist/types/src/filter/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +23 -0
- package/dist/types/src/query/errors.d.ts.map +1 -0
- package/dist/types/src/query/index.d.ts +5 -0
- package/dist/types/src/query/index.d.ts.map +1 -0
- package/dist/types/src/query/plan.d.ts +132 -0
- package/dist/types/src/query/plan.d.ts.map +1 -0
- package/dist/types/src/query/query-executor.d.ts +83 -0
- package/dist/types/src/query/query-executor.d.ts.map +1 -0
- package/dist/types/src/query/query-planner.d.ts +33 -0
- package/dist/types/src/query/query-planner.d.ts.map +1 -0
- package/dist/types/src/query/query-planner.test.d.ts +2 -0
- package/dist/types/src/query/query-planner.test.d.ts.map +1 -0
- package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-data.d.ts +18 -0
- package/dist/types/src/testing/test-data.d.ts.map +1 -0
- package/dist/types/src/testing/test-network-adapter.d.ts +3 -2
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
- package/dist/types/src/testing/test-schema.d.ts +39 -0
- package/dist/types/src/testing/test-schema.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +2 -2
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +43 -34
- package/src/automerge/automerge-host.test.ts +7 -7
- package/src/automerge/automerge-host.ts +58 -60
- package/src/automerge/automerge-repo.test.ts +65 -65
- package/src/automerge/collection-synchronizer.test.ts +1 -1
- package/src/automerge/collection-synchronizer.ts +11 -10
- package/src/automerge/echo-data-monitor.ts +21 -20
- package/src/automerge/echo-network-adapter.test.ts +1 -1
- package/src/automerge/echo-network-adapter.ts +25 -18
- package/src/automerge/heads-store.ts +4 -3
- package/src/automerge/leveldb-storage-adapter.ts +1 -1
- package/src/automerge/mesh-echo-replicator-connection.ts +6 -5
- package/src/automerge/mesh-echo-replicator.ts +2 -2
- package/src/automerge/network-protocol.ts +2 -1
- package/src/automerge/space-collection.ts +2 -1
- package/src/db-host/automerge-metrics.ts +2 -1
- package/src/db-host/data-service.ts +4 -3
- package/src/db-host/database-root.ts +17 -22
- package/src/db-host/documents-iterator.ts +9 -8
- package/src/db-host/documents-synchronizer.test.ts +2 -2
- package/src/db-host/documents-synchronizer.ts +20 -18
- package/src/db-host/echo-host.ts +44 -15
- package/src/db-host/index.ts +0 -1
- package/src/db-host/query-service.ts +43 -37
- package/src/db-host/space-state-manager.ts +14 -4
- package/src/edge/echo-edge-replicator.test.ts +3 -3
- package/src/edge/echo-edge-replicator.ts +9 -8
- package/src/edge/inflight-request-limiter.ts +4 -4
- package/src/filter/filter-match.test.ts +101 -0
- package/src/filter/filter-match.ts +174 -0
- package/src/filter/index.ts +5 -0
- package/src/index.ts +1 -0
- package/src/metadata/metadata-store.ts +13 -13
- package/src/pipeline/pipeline-stress.test.ts +9 -9
- package/src/pipeline/pipeline.ts +13 -13
- package/src/pipeline/timeframe-clock.ts +5 -5
- package/src/query/errors.ts +7 -0
- package/src/query/index.ts +8 -0
- package/src/query/plan.ts +179 -0
- package/src/query/query-executor.ts +648 -0
- package/src/query/query-planner.test.ts +613 -0
- package/src/query/query-planner.ts +470 -0
- package/src/space/admission-discovery-extension.ts +2 -2
- package/src/space/control-pipeline.ts +8 -8
- package/src/space/space-manager.ts +5 -4
- package/src/space/space-protocol.browser.test.ts +1 -0
- package/src/space/space-protocol.test.ts +1 -0
- package/src/space/space-protocol.ts +4 -4
- package/src/space/space.ts +5 -5
- package/src/testing/index.ts +2 -0
- package/src/testing/test-agent-builder.ts +6 -6
- package/src/testing/test-data.ts +127 -0
- package/src/testing/test-network-adapter.ts +15 -12
- package/src/testing/test-replicator.ts +2 -2
- package/src/testing/test-schema.ts +53 -0
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-32WDI3LB.mjs.map +0 -7
- package/dist/lib/node/chunk-TC2PRBEU.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-UKOLB3LW.mjs.map +0 -7
- package/dist/types/src/db-host/query-state.d.ts +0 -41
- package/dist/types/src/db-host/query-state.d.ts.map +0 -1
- 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) {
|
package/src/space/space.ts
CHANGED
|
@@ -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.
|
package/src/testing/index.ts
CHANGED