@uwdata/mosaic-core 0.9.0 → 0.10.0
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/mosaic-core.js +5768 -4799
- package/dist/mosaic-core.min.js +8 -8
- package/package.json +7 -7
- package/src/Coordinator.js +194 -49
- package/src/DataCubeIndexer.js +247 -142
- package/src/MosaicClient.js +3 -3
- package/src/QueryConsolidator.js +2 -2
- package/src/QueryManager.js +5 -3
- package/src/Selection.js +31 -10
- package/src/SelectionClause.js +22 -8
- package/src/index.js +10 -1
- package/src/util/AsyncDispatch.js +15 -5
- package/src/util/index-columns.js +10 -8
- package/src/util/query-result.js +41 -8
- package/src/util/to-data-columns.js +71 -0
- package/src/FilterGroup.js +0 -81
package/src/DataCubeIndexer.js
CHANGED
|
@@ -1,194 +1,211 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Query, and, asColumn, create, isBetween, scaleTransform, sql
|
|
3
|
+
} from '@uwdata/mosaic-sql';
|
|
3
4
|
import { indexColumns } from './util/index-columns.js';
|
|
5
|
+
import { fnv_hash } from './util/hash.js';
|
|
6
|
+
|
|
7
|
+
const Skip = { skip: true, result: null };
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Build and query optimized indices ("data cubes") for fast computation of
|
|
7
11
|
* groupby aggregate queries over compatible client queries and selections.
|
|
8
12
|
* A data cube contains pre-aggregated data for a Mosaic client, subdivided
|
|
9
|
-
* by possible query values from an active
|
|
10
|
-
* as
|
|
11
|
-
* Compatible client queries must
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* for an interval or point value predicate.
|
|
13
|
+
* by possible query values from an active selection clause. These cubes are
|
|
14
|
+
* realized as as database tables that can be queried for rapid updates.
|
|
15
|
+
* Compatible client queries must consist of only groupby dimensions and
|
|
16
|
+
* supported aggregate functions. Compatible selections must contain an active
|
|
17
|
+
* clause that exposes metadata for an interval or point value predicate.
|
|
15
18
|
*/
|
|
16
19
|
export class DataCubeIndexer {
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @param {import('./Coordinator.js').Coordinator}
|
|
20
|
-
* @param {
|
|
21
|
+
* Create a new data cube index table manager.
|
|
22
|
+
* @param {import('./Coordinator.js').Coordinator} coordinator A Mosaic coordinator.
|
|
23
|
+
* @param {object} [options] Indexer options.
|
|
24
|
+
* @param {boolean} [options.enabled=true] Flag to enable/disable indexer.
|
|
25
|
+
* @param {boolean} [options.temp=true] Flag to indicate if generated data
|
|
26
|
+
* cube index tables should be temporary tables.
|
|
21
27
|
*/
|
|
22
|
-
constructor(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
constructor(coordinator, {
|
|
29
|
+
enabled = true,
|
|
30
|
+
temp = true
|
|
31
|
+
} = {}) {
|
|
32
|
+
/** @type {Map<import('./MosaicClient.js').MosaicClient, DataCubeInfo | Skip | null>} */
|
|
33
|
+
this.indexes = new Map();
|
|
34
|
+
this.active = null;
|
|
26
35
|
this.temp = temp;
|
|
27
|
-
this.
|
|
36
|
+
this.mc = coordinator;
|
|
37
|
+
this._enabled = enabled;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Set the enabled state of this indexer. If false, any cached state is
|
|
42
|
+
* cleared and subsequent index calls will return null until re-enabled.
|
|
43
|
+
* @param {boolean} state The enabled state.
|
|
44
|
+
*/
|
|
45
|
+
enabled(state) {
|
|
46
|
+
if (state === undefined) {
|
|
47
|
+
return this._enabled;
|
|
48
|
+
} else if (this._enabled !== state) {
|
|
49
|
+
if (!state) this.clear();
|
|
50
|
+
this._enabled = state;
|
|
51
|
+
}
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Clear the cache of data cube index table entries for the current active
|
|
56
|
+
* selection clause. This method will also cancel any queued data cube table
|
|
57
|
+
* creation queries that have not yet been submitted to the database. This
|
|
58
|
+
* method does _not_ drop any existing data cube tables.
|
|
59
|
+
*/
|
|
37
60
|
clear() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
61
|
+
this.mc.cancel(Array.from(this.indexes.values(), info => info?.result));
|
|
62
|
+
this.indexes.clear();
|
|
63
|
+
this.active = null;
|
|
42
64
|
}
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Return data cube index table information for the active state of a
|
|
68
|
+
* client-selection pair, or null if the client is not indexable. This
|
|
69
|
+
* method has multiple possible side effects, including data cube table
|
|
70
|
+
* generation and updating internal caches.
|
|
71
|
+
* @param {import('./MosaicClient.js').MosaicClient} client A Mosaic client.
|
|
72
|
+
* @param {import('./Selection.js').Selection} selection A Mosaic selection
|
|
73
|
+
* to filter the client by.
|
|
74
|
+
* @param {import('./util/selection-types.js').SelectionClause} activeClause
|
|
75
|
+
* A representative active selection clause for which to (possibly) generate
|
|
76
|
+
* data cube index tables.
|
|
77
|
+
* @returns {DataCubeInfo | Skip | null} Data cube index table
|
|
78
|
+
* information and query generator, or null if the client is not indexable.
|
|
79
|
+
*/
|
|
80
|
+
index(client, selection, activeClause) {
|
|
81
|
+
// if not enabled, do nothing
|
|
82
|
+
if (!this._enabled) return null;
|
|
55
83
|
|
|
56
|
-
|
|
84
|
+
const { indexes, mc, temp } = this;
|
|
57
85
|
const { source } = activeClause;
|
|
58
|
-
|
|
59
|
-
if
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const sel = this.selection.remove(source);
|
|
71
|
-
|
|
72
|
-
// generate data cube indices
|
|
73
|
-
const indices = this.indices = new Map;
|
|
74
|
-
const { mc, temp } = this;
|
|
75
|
-
for (const client of clients) {
|
|
76
|
-
// determine if client should be skipped due to cross-filtering
|
|
77
|
-
if (sel.skip(client, activeClause)) {
|
|
78
|
-
indices.set(client, null);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// generate column definitions for data cube and cube queries
|
|
83
|
-
const index = indexColumns(client);
|
|
84
|
-
|
|
85
|
-
// skip if client is not indexable
|
|
86
|
-
if (!index) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// build index table construction query
|
|
91
|
-
const query = client.query(sel.predicate(client))
|
|
92
|
-
.select({ ...active.columns, ...index.aux })
|
|
93
|
-
.groupby(Object.keys(active.columns));
|
|
94
|
-
|
|
95
|
-
// ensure active view columns are selected by subqueries
|
|
96
|
-
const [subq] = query.subqueries;
|
|
97
|
-
if (subq) {
|
|
98
|
-
const cols = Object.values(active.columns).flatMap(c => c.columns);
|
|
99
|
-
subqueryPushdown(subq, cols);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// push orderby criteria to later cube queries
|
|
103
|
-
const order = query.orderby();
|
|
104
|
-
query.query.orderby = [];
|
|
105
|
-
|
|
106
|
-
const sql = query.toString();
|
|
107
|
-
const id = (fnv_hash(sql) >>> 0).toString(16);
|
|
108
|
-
const table = `cube_index_${id}`;
|
|
109
|
-
const result = mc.exec(create(table, sql, { temp }));
|
|
110
|
-
result.catch(e => logger.error(e));
|
|
111
|
-
indices.set(client, { table, result, order, ...index });
|
|
86
|
+
|
|
87
|
+
// if there is no clause source to track, do nothing
|
|
88
|
+
if (!source) return null;
|
|
89
|
+
|
|
90
|
+
// if we have cached active columns, check for updates or exit
|
|
91
|
+
if (this.active) {
|
|
92
|
+
// if the active clause source has changed, clear indexer state
|
|
93
|
+
// this cancels outstanding requests and clears the index cache
|
|
94
|
+
// a clear also sets this.active to null
|
|
95
|
+
if (this.active.source !== source) this.clear();
|
|
96
|
+
// if we've seen this source and it's not indexable, do nothing
|
|
97
|
+
if (this.active?.source === null) return null;
|
|
112
98
|
}
|
|
113
99
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
}
|
|
100
|
+
// the current active columns cache value
|
|
101
|
+
let { active } = this;
|
|
117
102
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
// if cached active columns are unset, analyze the active clause
|
|
104
|
+
if (!active) {
|
|
105
|
+
// generate active data cube dimension columns to select over
|
|
106
|
+
// will return an object with null source if not indexable
|
|
107
|
+
this.active = active = activeColumns(activeClause);
|
|
108
|
+
// if the active clause is not indexable, exit now
|
|
109
|
+
if (active.source === null) return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// if we have cached data cube index table info, return that
|
|
113
|
+
if (indexes.has(client)) {
|
|
114
|
+
return indexes.get(client);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// get non-active data cube index table columns
|
|
118
|
+
const indexCols = indexColumns(client);
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.select(dims, aggr)
|
|
144
|
-
.from(table)
|
|
145
|
-
.groupby(dims)
|
|
146
|
-
.where(filter)
|
|
147
|
-
.orderby(order);
|
|
148
|
-
return mc.updateClient(client, query);
|
|
120
|
+
let info;
|
|
121
|
+
if (!indexCols) {
|
|
122
|
+
// if client is not indexable, record null index
|
|
123
|
+
info = null;
|
|
124
|
+
} else if (selection.skip(client, activeClause)) {
|
|
125
|
+
// skip client if untouched by cross-filtering
|
|
126
|
+
info = Skip;
|
|
127
|
+
} else {
|
|
128
|
+
// generate data cube index table
|
|
129
|
+
const filter = selection.remove(source).predicate(client);
|
|
130
|
+
info = dataCubeInfo(client.query(filter), active, indexCols);
|
|
131
|
+
info.result = mc.exec(create(info.table, info.create, { temp }));
|
|
132
|
+
info.result.catch(e => mc.logger().error(e));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
indexes.set(client, info);
|
|
136
|
+
return info;
|
|
149
137
|
}
|
|
150
138
|
}
|
|
151
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Determines the active data cube dimension columns to select over. Returns
|
|
142
|
+
* an object with the clause source, column definitions, and a predicate
|
|
143
|
+
* generator function for the active dimensions of a data cube index table. If
|
|
144
|
+
* the active clause is not indexable or is missing metadata, this method
|
|
145
|
+
* returns an object with a null source property.
|
|
146
|
+
* @param {import('./util/selection-types.js').SelectionClause} clause The
|
|
147
|
+
* active selection clause to analyze.
|
|
148
|
+
*/
|
|
152
149
|
function activeColumns(clause) {
|
|
153
150
|
const { source, meta } = clause;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const { type, scales, bin, pixelSize = 1 } = meta;
|
|
151
|
+
const clausePred = clause.predicate;
|
|
152
|
+
const clauseCols = clausePred?.columns;
|
|
157
153
|
let predicate;
|
|
154
|
+
let columns;
|
|
155
|
+
|
|
156
|
+
if (!meta || !clauseCols) {
|
|
157
|
+
return { source: null, columns, predicate };
|
|
158
|
+
}
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
// @ts-ignore
|
|
161
|
+
const { type, scales, bin, pixelSize = 1 } = meta;
|
|
162
|
+
|
|
163
|
+
if (type === 'point') {
|
|
164
|
+
predicate = x => x;
|
|
165
|
+
columns = Object.fromEntries(
|
|
166
|
+
clauseCols.map(col => [`${col}`, asColumn(col)])
|
|
167
|
+
);
|
|
168
|
+
} else if (type === 'interval' && scales) {
|
|
160
169
|
// determine pixel-level binning
|
|
161
170
|
const bins = scales.map(s => binInterval(s, pixelSize, bin));
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (bins.length === 1) {
|
|
172
|
+
if (bins.some(b => !b)) {
|
|
173
|
+
// bail if a scale type is unsupported
|
|
174
|
+
} else if (bins.length === 1) {
|
|
167
175
|
// single interval selection
|
|
168
176
|
predicate = p => p ? isBetween('active0', p.range.map(bins[0])) : [];
|
|
169
|
-
|
|
177
|
+
// @ts-ignore
|
|
178
|
+
columns = { active0: bins[0](clausePred.field) };
|
|
170
179
|
} else {
|
|
171
180
|
// multiple interval selection
|
|
172
181
|
predicate = p => p
|
|
173
|
-
? and(p.children.map(
|
|
182
|
+
? and(p.children.map(
|
|
183
|
+
({ range }, i) => isBetween(`active${i}`, range.map(bins[i]))
|
|
184
|
+
))
|
|
174
185
|
: [];
|
|
175
186
|
columns = Object.fromEntries(
|
|
176
|
-
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
clausePred.children.map((p, i) => [`active${i}`, bins[i](p.field)])
|
|
177
189
|
);
|
|
178
190
|
}
|
|
179
|
-
} else if (type === 'point') {
|
|
180
|
-
predicate = x => x;
|
|
181
|
-
columns = Object.fromEntries(columns.map(col => [`${col}`, asColumn(col)]));
|
|
182
|
-
} else {
|
|
183
|
-
// unsupported selection type
|
|
184
|
-
return null;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
return { source, columns, predicate };
|
|
193
|
+
return { source: columns ? source : null, columns, predicate };
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
const BIN = { ceil: 'CEIL', round: 'ROUND' };
|
|
191
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Returns a bin function generator to discretize a selection interval domain.
|
|
200
|
+
* @param {import('./util/selection-types.js').Scale} scale A scale that maps
|
|
201
|
+
* domain values to the output range (typically pixels).
|
|
202
|
+
* @param {number} pixelSize The interactive pixel size. This value indicates
|
|
203
|
+
* the bin step size and may be greater than an actual screen pixel.
|
|
204
|
+
* @param {import('./util/selection-types.js').BinMethod} bin The binning
|
|
205
|
+
* method to apply, one of `floor`, `ceil', or `round`.
|
|
206
|
+
* @returns {(value: any) => import('@uwdata/mosaic-sql').SQLExpression}
|
|
207
|
+
* A bin function generator.
|
|
208
|
+
*/
|
|
192
209
|
function binInterval(scale, pixelSize, bin) {
|
|
193
210
|
const { type, domain, range, apply, sqlApply } = scaleTransform(scale);
|
|
194
211
|
if (!apply) return; // unsupported scale type
|
|
@@ -201,6 +218,51 @@ function binInterval(scale, pixelSize, bin) {
|
|
|
201
218
|
return value => sql`${fn}(${s}(${sqlApply(value)}${d}))::INTEGER`;
|
|
202
219
|
}
|
|
203
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Generate data cube table query information.
|
|
223
|
+
* @param {Query} clientQuery The original client query.
|
|
224
|
+
* @param {*} active Active (selected) column definitions.
|
|
225
|
+
* @param {*} indexCols Data cube index column definitions.
|
|
226
|
+
* @returns {DataCubeInfo}
|
|
227
|
+
*/
|
|
228
|
+
function dataCubeInfo(clientQuery, active, indexCols) {
|
|
229
|
+
const { dims, aggr, aux } = indexCols;
|
|
230
|
+
const { columns } = active;
|
|
231
|
+
|
|
232
|
+
// build index table construction query
|
|
233
|
+
const query = clientQuery
|
|
234
|
+
.select({ ...columns, ...aux })
|
|
235
|
+
.groupby(Object.keys(columns));
|
|
236
|
+
|
|
237
|
+
// ensure active clause columns are selected by subqueries
|
|
238
|
+
const [subq] = query.subqueries;
|
|
239
|
+
if (subq) {
|
|
240
|
+
const cols = Object.values(columns).flatMap(c => c.columns);
|
|
241
|
+
subqueryPushdown(subq, cols);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// push orderby criteria to later cube queries
|
|
245
|
+
const order = query.orderby();
|
|
246
|
+
query.query.orderby = [];
|
|
247
|
+
|
|
248
|
+
// generate creation query string and hash id
|
|
249
|
+
const create = query.toString();
|
|
250
|
+
const id = (fnv_hash(create) >>> 0).toString(16);
|
|
251
|
+
const table = `cube_index_${id}`;
|
|
252
|
+
|
|
253
|
+
// generate data cube select query
|
|
254
|
+
const select = Query
|
|
255
|
+
.select(dims, aggr)
|
|
256
|
+
.from(table)
|
|
257
|
+
.groupby(dims)
|
|
258
|
+
.orderby(order);
|
|
259
|
+
|
|
260
|
+
return new DataCubeInfo({ table, create, active, select });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Push column selections down to subqueries.
|
|
265
|
+
*/
|
|
204
266
|
function subqueryPushdown(query, cols) {
|
|
205
267
|
const memo = new Set;
|
|
206
268
|
const pushdown = q => {
|
|
@@ -213,3 +275,46 @@ function subqueryPushdown(query, cols) {
|
|
|
213
275
|
};
|
|
214
276
|
pushdown(query);
|
|
215
277
|
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Metadata and query generator for a data cube index table. This
|
|
281
|
+
* object provides the information needed to generate and query
|
|
282
|
+
* a data cube index table for a client-selection pair relative to
|
|
283
|
+
* a specific active clause and selection state.
|
|
284
|
+
*/
|
|
285
|
+
export class DataCubeInfo {
|
|
286
|
+
/**
|
|
287
|
+
* Create a new DataCubeInfo instance.
|
|
288
|
+
* @param {object} options
|
|
289
|
+
*/
|
|
290
|
+
constructor({ table, create, active, select } = {}) {
|
|
291
|
+
/** The name of the data cube index table. */
|
|
292
|
+
this.table = table;
|
|
293
|
+
/** The SQL query used to generate the data cube index table. */
|
|
294
|
+
this.create = create;
|
|
295
|
+
/** A result promise returned for the data cube creation query. */
|
|
296
|
+
this.result = null;
|
|
297
|
+
/**
|
|
298
|
+
* Definitions and predicate function for the active columns,
|
|
299
|
+
* which are dynamically filtered by the active clause.
|
|
300
|
+
*/
|
|
301
|
+
this.active = active;
|
|
302
|
+
/** Select query (sans where clause) for data cube tables. */
|
|
303
|
+
this.select = select;
|
|
304
|
+
/**
|
|
305
|
+
* Boolean flag indicating a client that should be skipped.
|
|
306
|
+
* This value is always false for completed data cube info.
|
|
307
|
+
*/
|
|
308
|
+
this.skip = false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate a data cube index table query for the given predicate.
|
|
313
|
+
* @param {import('@uwdata/mosaic-sql').SQLExpression} predicate The current
|
|
314
|
+
* active clause predicate.
|
|
315
|
+
* @returns {Query} A data cube index table query.
|
|
316
|
+
*/
|
|
317
|
+
query(predicate) {
|
|
318
|
+
return this.select.clone().where(this.active.predicate(predicate));
|
|
319
|
+
}
|
|
320
|
+
}
|
package/src/MosaicClient.js
CHANGED
|
@@ -94,8 +94,8 @@ export class MosaicClient {
|
|
|
94
94
|
* @param {*} error
|
|
95
95
|
* @returns {this}
|
|
96
96
|
*/
|
|
97
|
-
queryError(error) {
|
|
98
|
-
|
|
97
|
+
queryError(error) { // eslint-disable-line no-unused-vars
|
|
98
|
+
// do nothing, the coordinator logs the error
|
|
99
99
|
return this;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -122,7 +122,7 @@ export class MosaicClient {
|
|
|
122
122
|
/**
|
|
123
123
|
* Requests a client update.
|
|
124
124
|
* For example to (re-)render an interface component.
|
|
125
|
-
*
|
|
125
|
+
*
|
|
126
126
|
* @returns {this | Promise<any>}
|
|
127
127
|
*/
|
|
128
128
|
update() {
|
package/src/QueryConsolidator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Query, Ref, isDescribeQuery } from '@uwdata/mosaic-sql';
|
|
2
|
-
import {
|
|
2
|
+
import { QueryResult } from './util/query-result.js';
|
|
3
3
|
|
|
4
4
|
function wait(callback) {
|
|
5
5
|
const method = typeof requestAnimationFrame !== 'undefined'
|
|
@@ -133,7 +133,7 @@ function consolidate(group, enqueue, record) {
|
|
|
133
133
|
record: false,
|
|
134
134
|
query: (group.query = consolidatedQuery(group, record))
|
|
135
135
|
},
|
|
136
|
-
result: (group.result =
|
|
136
|
+
result: (group.result = new QueryResult())
|
|
137
137
|
});
|
|
138
138
|
} else {
|
|
139
139
|
// issue queries directly
|
package/src/QueryManager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { consolidator } from './QueryConsolidator.js';
|
|
2
2
|
import { lruCache, voidCache } from './util/cache.js';
|
|
3
3
|
import { priorityQueue } from './util/priority-queue.js';
|
|
4
|
-
import {
|
|
4
|
+
import { QueryResult } from './util/query-result.js';
|
|
5
5
|
|
|
6
6
|
export const Priority = { High: 0, Normal: 1, Low: 2 };
|
|
7
7
|
|
|
@@ -96,7 +96,7 @@ export class QueryManager {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
request(request, priority = Priority.Normal) {
|
|
99
|
-
const result =
|
|
99
|
+
const result = new QueryResult();
|
|
100
100
|
const entry = { request, result };
|
|
101
101
|
if (this._consolidate) {
|
|
102
102
|
this._consolidate.add(entry, priority);
|
|
@@ -108,7 +108,9 @@ export class QueryManager {
|
|
|
108
108
|
|
|
109
109
|
cancel(requests) {
|
|
110
110
|
const set = new Set(requests);
|
|
111
|
-
|
|
111
|
+
if (set.size) {
|
|
112
|
+
this.queue.remove(({ result }) => set.has(result));
|
|
113
|
+
}
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
clear() {
|
package/src/Selection.js
CHANGED
|
@@ -22,10 +22,13 @@ export class Selection extends Param {
|
|
|
22
22
|
* @param {boolean} [options.cross=false] Boolean flag indicating
|
|
23
23
|
* cross-filtered resolution. If true, selection clauses will not
|
|
24
24
|
* be applied to the clients they are associated with.
|
|
25
|
+
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
26
|
+
* of clauses should correspond to an empty selection with no records. This
|
|
27
|
+
* setting determines the default selection state.
|
|
25
28
|
* @returns {Selection} The new Selection instance.
|
|
26
29
|
*/
|
|
27
|
-
static intersect({ cross = false } = {}) {
|
|
28
|
-
return new Selection(new SelectionResolver({ cross }));
|
|
30
|
+
static intersect({ cross = false, empty = false } = {}) {
|
|
31
|
+
return new Selection(new SelectionResolver({ cross, empty }));
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -35,10 +38,13 @@ export class Selection extends Param {
|
|
|
35
38
|
* @param {boolean} [options.cross=false] Boolean flag indicating
|
|
36
39
|
* cross-filtered resolution. If true, selection clauses will not
|
|
37
40
|
* be applied to the clients they are associated with.
|
|
41
|
+
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
42
|
+
* of clauses should correspond to an empty selection with no records. This
|
|
43
|
+
* setting determines the default selection state.
|
|
38
44
|
* @returns {Selection} The new Selection instance.
|
|
39
45
|
*/
|
|
40
|
-
static union({ cross = false } = {}) {
|
|
41
|
-
return new Selection(new SelectionResolver({ cross, union: true }));
|
|
46
|
+
static union({ cross = false, empty = false } = {}) {
|
|
47
|
+
return new Selection(new SelectionResolver({ cross, empty, union: true }));
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -48,19 +54,26 @@ export class Selection extends Param {
|
|
|
48
54
|
* @param {boolean} [options.cross=false] Boolean flag indicating
|
|
49
55
|
* cross-filtered resolution. If true, selection clauses will not
|
|
50
56
|
* be applied to the clients they are associated with.
|
|
57
|
+
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
58
|
+
* of clauses should correspond to an empty selection with no records. This
|
|
59
|
+
* setting determines the default selection state.
|
|
51
60
|
* @returns {Selection} The new Selection instance.
|
|
52
61
|
*/
|
|
53
|
-
static single({ cross = false } = {}) {
|
|
54
|
-
return new Selection(new SelectionResolver({ cross, single: true }));
|
|
62
|
+
static single({ cross = false, empty = false } = {}) {
|
|
63
|
+
return new Selection(new SelectionResolver({ cross, empty, single: true }));
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* Create a new Selection instance with a
|
|
59
68
|
* cross-filtered intersect resolution strategy.
|
|
69
|
+
* @param {object} [options] The selection options.
|
|
70
|
+
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
71
|
+
* of clauses should correspond to an empty selection with no records. This
|
|
72
|
+
* setting determines the default selection state.
|
|
60
73
|
* @returns {Selection} The new Selection instance.
|
|
61
74
|
*/
|
|
62
|
-
static crossfilter() {
|
|
63
|
-
return new Selection(new SelectionResolver({ cross: true }));
|
|
75
|
+
static crossfilter({ empty = false } = {}) {
|
|
76
|
+
return new Selection(new SelectionResolver({ cross: true, empty }));
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
/**
|
|
@@ -232,11 +245,15 @@ export class SelectionResolver {
|
|
|
232
245
|
* If false, an intersection strategy is used.
|
|
233
246
|
* @param {boolean} [options.cross=false] Boolean flag to indicate cross-filtering.
|
|
234
247
|
* @param {boolean} [options.single=false] Boolean flag to indicate single clauses only.
|
|
248
|
+
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
249
|
+
* of clauses should correspond to an empty selection with no records. This
|
|
250
|
+
* setting determines the default selection state.
|
|
235
251
|
*/
|
|
236
|
-
constructor({ union, cross, single } = {}) {
|
|
252
|
+
constructor({ union, cross, single, empty } = {}) {
|
|
237
253
|
this.union = !!union;
|
|
238
254
|
this.cross = !!cross;
|
|
239
255
|
this.single = !!single;
|
|
256
|
+
this.empty = !!empty;
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
/**
|
|
@@ -274,7 +291,11 @@ export class SelectionResolver {
|
|
|
274
291
|
* based on the current state of this selection.
|
|
275
292
|
*/
|
|
276
293
|
predicate(clauseList, active, client) {
|
|
277
|
-
const { union } = this;
|
|
294
|
+
const { empty, union } = this;
|
|
295
|
+
|
|
296
|
+
if (empty && !clauseList.length) {
|
|
297
|
+
return ['FALSE'];
|
|
298
|
+
}
|
|
278
299
|
|
|
279
300
|
// do nothing if cross-filtering and client is currently active
|
|
280
301
|
if (this.skip(client, active)) return undefined;
|