@uwdata/mosaic-core 0.8.0 → 0.9.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 +1588 -1273
- package/dist/mosaic-core.min.js +8 -8
- package/package.json +5 -5
- package/src/Coordinator.js +1 -1
- package/src/DataCubeIndexer.js +74 -120
- package/src/FilterGroup.js +20 -9
- package/src/QueryManager.js +101 -93
- package/src/Selection.js +26 -11
- package/src/SelectionClause.js +147 -0
- package/src/connectors/rest.js +7 -0
- package/src/connectors/socket.js +7 -0
- package/src/connectors/wasm.js +2 -2
- package/src/index.js +1 -0
- package/src/util/convert-arrow.js +14 -5
- package/src/util/index-columns.js +538 -0
- package/src/util/selection-types.ts +137 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Scalable and extensible linked data views.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mosaic",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"prepublishOnly": "npm run test && npm run lint && npm run build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@duckdb/duckdb-wasm": "^1.28.1-
|
|
32
|
-
"@uwdata/mosaic-sql": "^0.
|
|
31
|
+
"@duckdb/duckdb-wasm": "^1.28.1-dev195.0",
|
|
32
|
+
"@uwdata/mosaic-sql": "^0.9.0",
|
|
33
33
|
"apache-arrow": "^15.0.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@uwdata/mosaic-duckdb": "^0.
|
|
36
|
+
"@uwdata/mosaic-duckdb": "^0.9.0"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "89bb9b0dfa747aed691eaeba35379525a6764c61"
|
|
39
39
|
}
|
package/src/Coordinator.js
CHANGED
package/src/DataCubeIndexer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Query, and, create, isBetween, scaleTransform, sql } from '@uwdata/mosaic-sql';
|
|
1
|
+
import { Query, and, asColumn, create, isBetween, scaleTransform, sql } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { fnv_hash } from './util/hash.js';
|
|
3
|
+
import { indexColumns } from './util/index-columns.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Build and query optimized indices ("data cubes") for fast computation of
|
|
@@ -9,7 +10,7 @@ import { fnv_hash } from './util/hash.js';
|
|
|
9
10
|
* as temporary database tables that can be queried for rapid updates.
|
|
10
11
|
* Compatible client queries must pull data from the same backing table and
|
|
11
12
|
* must consist of only groupby dimensions and supported aggregates.
|
|
12
|
-
* Compatible selections must contain an active clause that exposes
|
|
13
|
+
* Compatible selections must contain an active clause that exposes metadata
|
|
13
14
|
* for an interval or point value predicate.
|
|
14
15
|
*/
|
|
15
16
|
export class DataCubeIndexer {
|
|
@@ -30,39 +31,40 @@ export class DataCubeIndexer {
|
|
|
30
31
|
this.enabled = false;
|
|
31
32
|
this.clients = null;
|
|
32
33
|
this.indices = null;
|
|
33
|
-
this.
|
|
34
|
+
this.active = null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
clear() {
|
|
37
38
|
if (this.indices) {
|
|
38
|
-
this.mc.cancel(Array.from(this.indices.values(), index => index
|
|
39
|
+
this.mc.cancel(Array.from(this.indices.values(), index => index?.result));
|
|
39
40
|
this.indices = null;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
index(clients,
|
|
44
|
+
index(clients, activeClause) {
|
|
44
45
|
if (this.clients !== clients) {
|
|
45
46
|
// test client views for compatibility
|
|
46
|
-
const cols = Array.from(clients,
|
|
47
|
+
const cols = Array.from(clients, indexColumns).filter(x => x);
|
|
47
48
|
const from = cols[0]?.from;
|
|
48
|
-
this.enabled = cols.every(c => c
|
|
49
|
+
this.enabled = cols.length && cols.every(c => c.from === from);
|
|
49
50
|
this.clients = clients;
|
|
50
|
-
this.
|
|
51
|
+
this.active = null;
|
|
51
52
|
this.clear();
|
|
52
53
|
}
|
|
53
54
|
if (!this.enabled) return false; // client views are not indexable
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
const { source } =
|
|
56
|
+
activeClause = activeClause || this.selection.active;
|
|
57
|
+
const { source } = activeClause;
|
|
57
58
|
// exit early if indexes already set up for active view
|
|
58
|
-
if (source && source === this.
|
|
59
|
+
if (source && source === this.active?.source) return true;
|
|
59
60
|
|
|
60
61
|
this.clear();
|
|
61
62
|
if (!source) return false; // nothing to work with
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
63
|
+
const active = this.active = activeColumns(activeClause);
|
|
64
|
+
if (!active) return false; // active selection clause not compatible
|
|
64
65
|
|
|
65
|
-
this.mc.logger()
|
|
66
|
+
const logger = this.mc.logger();
|
|
67
|
+
logger.warn('DATA CUBE INDEX CONSTRUCTION');
|
|
66
68
|
|
|
67
69
|
// create a selection with the active source removed
|
|
68
70
|
const sel = this.selection.remove(source);
|
|
@@ -71,18 +73,29 @@ export class DataCubeIndexer {
|
|
|
71
73
|
const indices = this.indices = new Map;
|
|
72
74
|
const { mc, temp } = this;
|
|
73
75
|
for (const client of clients) {
|
|
74
|
-
if
|
|
75
|
-
|
|
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
|
+
}
|
|
76
89
|
|
|
77
|
-
// build index construction query
|
|
90
|
+
// build index table construction query
|
|
78
91
|
const query = client.query(sel.predicate(client))
|
|
79
|
-
.select({ ...
|
|
80
|
-
.groupby(Object.keys(
|
|
92
|
+
.select({ ...active.columns, ...index.aux })
|
|
93
|
+
.groupby(Object.keys(active.columns));
|
|
81
94
|
|
|
82
95
|
// ensure active view columns are selected by subqueries
|
|
83
96
|
const [subq] = query.subqueries;
|
|
84
97
|
if (subq) {
|
|
85
|
-
const cols = Object.values(
|
|
98
|
+
const cols = Object.values(active.columns).flatMap(c => c.columns);
|
|
86
99
|
subqueryPushdown(subq, cols);
|
|
87
100
|
}
|
|
88
101
|
|
|
@@ -94,6 +107,7 @@ export class DataCubeIndexer {
|
|
|
94
107
|
const id = (fnv_hash(sql) >>> 0).toString(16);
|
|
95
108
|
const table = `cube_index_${id}`;
|
|
96
109
|
const result = mc.exec(create(table, sql, { temp }));
|
|
110
|
+
result.catch(e => logger.error(e));
|
|
97
111
|
indices.set(client, { table, result, order, ...index });
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -102,21 +116,28 @@ export class DataCubeIndexer {
|
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
async update() {
|
|
105
|
-
const { clients, selection,
|
|
106
|
-
const filter =
|
|
119
|
+
const { clients, selection, active } = this;
|
|
120
|
+
const filter = active.predicate(selection.active.predicate);
|
|
107
121
|
return Promise.all(
|
|
108
122
|
Array.from(clients).map(client => this.updateClient(client, filter))
|
|
109
123
|
);
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
async updateClient(client, filter) {
|
|
127
|
+
const { mc, indices, selection } = this;
|
|
128
|
+
|
|
129
|
+
// if client has no index, perform a standard update
|
|
130
|
+
if (!indices.has(client)) {
|
|
131
|
+
filter = selection.predicate(client);
|
|
132
|
+
return mc.updateClient(client, client.query(filter));
|
|
133
|
+
};
|
|
134
|
+
|
|
113
135
|
const index = this.indices.get(client);
|
|
114
|
-
if (!index) return;
|
|
115
136
|
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
}
|
|
137
|
+
// skip update if cross-filtered
|
|
138
|
+
if (!index) return;
|
|
119
139
|
|
|
140
|
+
// otherwise, query a data cube index table
|
|
120
141
|
const { table, dims, aggr, order = [] } = index;
|
|
121
142
|
const query = Query
|
|
122
143
|
.select(dims, aggr)
|
|
@@ -124,25 +145,30 @@ export class DataCubeIndexer {
|
|
|
124
145
|
.groupby(dims)
|
|
125
146
|
.where(filter)
|
|
126
147
|
.orderby(order);
|
|
127
|
-
return
|
|
148
|
+
return mc.updateClient(client, query);
|
|
128
149
|
}
|
|
129
150
|
}
|
|
130
151
|
|
|
131
|
-
function
|
|
132
|
-
const { source,
|
|
152
|
+
function activeColumns(clause) {
|
|
153
|
+
const { source, meta } = clause;
|
|
133
154
|
let columns = clause.predicate?.columns;
|
|
134
|
-
if (!
|
|
135
|
-
const { type, scales, pixelSize = 1 } =
|
|
155
|
+
if (!meta || !columns) return null;
|
|
156
|
+
const { type, scales, bin, pixelSize = 1 } = meta;
|
|
136
157
|
let predicate;
|
|
137
158
|
|
|
138
159
|
if (type === 'interval' && scales) {
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
// determine pixel-level binning
|
|
161
|
+
const bins = scales.map(s => binInterval(s, pixelSize, bin));
|
|
162
|
+
|
|
163
|
+
// bail if the scale type is unsupported
|
|
164
|
+
if (bins.some(b => b == null)) return null;
|
|
141
165
|
|
|
142
166
|
if (bins.length === 1) {
|
|
167
|
+
// single interval selection
|
|
143
168
|
predicate = p => p ? isBetween('active0', p.range.map(bins[0])) : [];
|
|
144
169
|
columns = { active0: bins[0](clause.predicate.field) };
|
|
145
170
|
} else {
|
|
171
|
+
// multiple interval selection
|
|
146
172
|
predicate = p => p
|
|
147
173
|
? and(p.children.map(({ range }, i) => isBetween(`active${i}`, range.map(bins[i]))))
|
|
148
174
|
: [];
|
|
@@ -152,99 +178,27 @@ function getActiveView(clause) {
|
|
|
152
178
|
}
|
|
153
179
|
} else if (type === 'point') {
|
|
154
180
|
predicate = x => x;
|
|
155
|
-
columns = Object.fromEntries(columns.map(col => [col
|
|
181
|
+
columns = Object.fromEntries(columns.map(col => [`${col}`, asColumn(col)]));
|
|
156
182
|
} else {
|
|
157
|
-
|
|
183
|
+
// unsupported selection type
|
|
184
|
+
return null;
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
return { source, columns, predicate };
|
|
161
188
|
}
|
|
162
189
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const NO_INDEX = { from: NaN };
|
|
176
|
-
|
|
177
|
-
function getIndexColumns(client) {
|
|
178
|
-
if (!client.filterIndexable) return NO_INDEX;
|
|
179
|
-
const q = client.query();
|
|
180
|
-
const from = getBaseTable(q);
|
|
181
|
-
if (!from || !q.groupby) return NO_INDEX;
|
|
182
|
-
const g = new Set(q.groupby().map(c => c.column));
|
|
183
|
-
|
|
184
|
-
const aggr = [];
|
|
185
|
-
const dims = [];
|
|
186
|
-
const aux = {}; // auxiliary columns needed by aggregates
|
|
187
|
-
let auxAs;
|
|
188
|
-
|
|
189
|
-
for (const entry of q.select()) {
|
|
190
|
-
const { as, expr: { aggregate, args } } = entry;
|
|
191
|
-
const op = aggregate?.toUpperCase?.();
|
|
192
|
-
switch (op) {
|
|
193
|
-
case 'COUNT':
|
|
194
|
-
case 'SUM':
|
|
195
|
-
aggr.push({ [as]: sql`SUM("${as}")::DOUBLE` });
|
|
196
|
-
break;
|
|
197
|
-
case 'AVG':
|
|
198
|
-
aux[auxAs = '__count__'] = sql`COUNT(*)`;
|
|
199
|
-
aggr.push({ [as]: sql`(SUM("${as}" * ${auxAs}) / SUM(${auxAs}))::DOUBLE` });
|
|
200
|
-
break;
|
|
201
|
-
case 'ARG_MAX':
|
|
202
|
-
aux[auxAs = `__max_${as}__`] = sql`MAX(${args[1]})`;
|
|
203
|
-
aggr.push({ [as]: sql`ARG_MAX("${as}", ${auxAs})` });
|
|
204
|
-
break;
|
|
205
|
-
case 'ARG_MIN':
|
|
206
|
-
aux[auxAs = `__min_${as}__`] = sql`MIN(${args[1]})`;
|
|
207
|
-
aggr.push({ [as]: sql`ARG_MIN("${as}", ${auxAs})` });
|
|
208
|
-
break;
|
|
209
|
-
|
|
210
|
-
// aggregates that commute directly
|
|
211
|
-
case 'MAX':
|
|
212
|
-
case 'MIN':
|
|
213
|
-
case 'BIT_AND':
|
|
214
|
-
case 'BIT_OR':
|
|
215
|
-
case 'BIT_XOR':
|
|
216
|
-
case 'BOOL_AND':
|
|
217
|
-
case 'BOOL_OR':
|
|
218
|
-
case 'PRODUCT':
|
|
219
|
-
aggr.push({ [as]: sql`${op}("${as}")` });
|
|
220
|
-
break;
|
|
221
|
-
default:
|
|
222
|
-
if (g.has(as)) dims.push(as);
|
|
223
|
-
else return null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return { aggr, dims, aux, from };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function getBaseTable(query) {
|
|
231
|
-
const subq = query.subqueries;
|
|
232
|
-
|
|
233
|
-
// select query
|
|
234
|
-
if (query.select) {
|
|
235
|
-
const from = query.from();
|
|
236
|
-
if (!from.length) return undefined;
|
|
237
|
-
if (subq.length === 0) return from[0].from.table;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// handle set operations / subqueries
|
|
241
|
-
const base = getBaseTable(subq[0]);
|
|
242
|
-
for (let i = 1; i < subq.length; ++i) {
|
|
243
|
-
const from = getBaseTable(subq[i]);
|
|
244
|
-
if (from === undefined) continue;
|
|
245
|
-
if (from !== base) return NaN;
|
|
246
|
-
}
|
|
247
|
-
return base;
|
|
190
|
+
const BIN = { ceil: 'CEIL', round: 'ROUND' };
|
|
191
|
+
|
|
192
|
+
function binInterval(scale, pixelSize, bin) {
|
|
193
|
+
const { type, domain, range, apply, sqlApply } = scaleTransform(scale);
|
|
194
|
+
if (!apply) return; // unsupported scale type
|
|
195
|
+
const fn = BIN[`${bin}`.toLowerCase()] || 'FLOOR';
|
|
196
|
+
const lo = apply(Math.min(...domain));
|
|
197
|
+
const hi = apply(Math.max(...domain));
|
|
198
|
+
const a = type === 'identity' ? 1 : Math.abs(range[1] - range[0]) / (hi - lo);
|
|
199
|
+
const s = a / pixelSize === 1 ? '' : `${a / pixelSize}::DOUBLE * `;
|
|
200
|
+
const d = lo === 0 ? '' : ` - ${lo}::DOUBLE`;
|
|
201
|
+
return value => sql`${fn}(${s}(${sqlApply(value)}${d}))::INTEGER`;
|
|
248
202
|
}
|
|
249
203
|
|
|
250
204
|
function subqueryPushdown(query, cols) {
|
package/src/FilterGroup.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
|
+
import { Coordinator } from './Coordinator.js';
|
|
1
2
|
import { DataCubeIndexer } from './DataCubeIndexer.js';
|
|
3
|
+
import { MosaicClient } from './MosaicClient.js';
|
|
4
|
+
import { Selection } from './Selection.js';
|
|
2
5
|
|
|
3
6
|
export class FilterGroup {
|
|
4
7
|
/**
|
|
5
|
-
* @param {
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {
|
|
8
|
-
* Falsy values disable indexing.
|
|
8
|
+
* @param {Coordinator} coordinator The Mosaic coordinator.
|
|
9
|
+
* @param {Selection} selection The shared filter selection.
|
|
10
|
+
* @param {object|boolean} index Boolean flag or options hash for
|
|
11
|
+
* a data cube indexer. Falsy values disable indexing.
|
|
9
12
|
*/
|
|
10
13
|
constructor(coordinator, selection, index = true) {
|
|
11
|
-
/** @type import('./Coordinator.js').Coordinator */
|
|
12
14
|
this.mc = coordinator;
|
|
13
15
|
this.selection = selection;
|
|
16
|
+
/** @type {Set<MosaicClient>} */
|
|
14
17
|
this.clients = new Set();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
/** @type {DataCubeIndexer | null} */
|
|
19
|
+
this.indexer = null;
|
|
20
|
+
this.index(index);
|
|
18
21
|
|
|
19
22
|
const { value, activate } = this.handlers = {
|
|
20
23
|
value: () => this.update(),
|
|
21
|
-
activate: clause => this.indexer?.index(this.clients, clause)
|
|
24
|
+
activate: clause => { this.indexer?.index(this.clients, clause); }
|
|
22
25
|
};
|
|
23
26
|
selection.addEventListener('value', value);
|
|
24
27
|
selection.addEventListener('activate', activate);
|
|
@@ -30,6 +33,14 @@ export class FilterGroup {
|
|
|
30
33
|
this.selection.removeEventListener('activate', activate);
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
index(state) {
|
|
37
|
+
const { selection } = this;
|
|
38
|
+
const { resolver } = selection;
|
|
39
|
+
this.indexer = state && (resolver.single || !resolver.union)
|
|
40
|
+
? new DataCubeIndexer(this.mc, { ...state, selection })
|
|
41
|
+
: null;
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
reset() {
|
|
34
45
|
this.indexer?.reset();
|
|
35
46
|
}
|
package/src/QueryManager.js
CHANGED
|
@@ -5,48 +5,51 @@ import { queryResult } from './util/query-result.js';
|
|
|
5
5
|
|
|
6
6
|
export const Priority = { High: 0, Normal: 1, Low: 2 };
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (pending || queue.isEmpty()) return;
|
|
19
|
-
const { request, result } = queue.next();
|
|
20
|
-
pending = submit(request, result);
|
|
21
|
-
pending.finally(() => { pending = null; next(); });
|
|
8
|
+
export class QueryManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.queue = priorityQueue(3);
|
|
11
|
+
this.db = null;
|
|
12
|
+
this.clientCache = null;
|
|
13
|
+
this._logger = null;
|
|
14
|
+
this._logQueries = false;
|
|
15
|
+
this.recorders = [];
|
|
16
|
+
this.pending = null;
|
|
17
|
+
this._consolidate = null;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
queue.
|
|
26
|
-
next();
|
|
20
|
+
next() {
|
|
21
|
+
if (this.pending || this.queue.isEmpty()) return;
|
|
22
|
+
const { request, result } = this.queue.next();
|
|
23
|
+
this.pending = this.submit(request, result);
|
|
24
|
+
this.pending.finally(() => { this.pending = null; this.next(); });
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
enqueue(entry, priority = Priority.Normal) {
|
|
28
|
+
this.queue.insert(entry, priority);
|
|
29
|
+
this.next();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
recordQuery(sql) {
|
|
33
|
+
if (this.recorders.length && sql) {
|
|
34
|
+
this.recorders.forEach(rec => rec.add(sql));
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
async
|
|
38
|
+
async submit(request, result) {
|
|
36
39
|
try {
|
|
37
40
|
const { query, type, cache = false, record = true, options } = request;
|
|
38
41
|
const sql = query ? `${query}` : null;
|
|
39
42
|
|
|
40
43
|
// update recorders
|
|
41
44
|
if (record) {
|
|
42
|
-
recordQuery(sql);
|
|
45
|
+
this.recordQuery(sql);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// check query cache
|
|
46
49
|
if (cache) {
|
|
47
|
-
const cached = clientCache.get(sql);
|
|
50
|
+
const cached = this.clientCache.get(sql);
|
|
48
51
|
if (cached) {
|
|
49
|
-
|
|
52
|
+
this._logger.debug('Cache');
|
|
50
53
|
result.fulfill(cached);
|
|
51
54
|
return;
|
|
52
55
|
}
|
|
@@ -54,80 +57,85 @@ export function QueryManager() {
|
|
|
54
57
|
|
|
55
58
|
// issue query, potentially cache result
|
|
56
59
|
const t0 = performance.now();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
if (this._logQueries) {
|
|
61
|
+
this._logger.debug('Query', { type, sql, ...options });
|
|
62
|
+
}
|
|
63
|
+
const data = await this.db.query({ type, sql, ...options });
|
|
64
|
+
if (cache) this.clientCache.set(sql, data);
|
|
65
|
+
this._logger.debug(`Request: ${(performance.now() - t0).toFixed(1)}`);
|
|
60
66
|
result.fulfill(data);
|
|
61
67
|
} catch (err) {
|
|
62
68
|
result.reject(err);
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
cancel(requests) {
|
|
101
|
-
const set = new Set(requests);
|
|
102
|
-
queue.remove(({ result }) => set.has(result));
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
clear() {
|
|
106
|
-
queue.remove(({ result }) => {
|
|
107
|
-
result.reject('Cleared');
|
|
108
|
-
return true;
|
|
109
|
-
});
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
record() {
|
|
113
|
-
let state = [];
|
|
114
|
-
const recorder = {
|
|
115
|
-
add(query) {
|
|
116
|
-
state.push(query);
|
|
117
|
-
},
|
|
118
|
-
reset() {
|
|
119
|
-
state = [];
|
|
120
|
-
},
|
|
121
|
-
snapshot() {
|
|
122
|
-
return state.slice();
|
|
123
|
-
},
|
|
124
|
-
stop() {
|
|
125
|
-
recorders = recorders.filter(x => x !== recorder);
|
|
126
|
-
return state;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
recorders.push(recorder);
|
|
130
|
-
return recorder;
|
|
72
|
+
cache(value) {
|
|
73
|
+
return value !== undefined
|
|
74
|
+
? (this.clientCache = value === true ? lruCache() : (value || voidCache()))
|
|
75
|
+
: this.clientCache;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
logger(value) {
|
|
79
|
+
return value ? (this._logger = value) : this._logger;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
logQueries(value) {
|
|
83
|
+
return value !== undefined ? this._logQueries = !!value : this._logQueries;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
connector(connector) {
|
|
87
|
+
return connector ? (this.db = connector) : this.db;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
consolidate(flag) {
|
|
91
|
+
if (flag && !this._consolidate) {
|
|
92
|
+
this._consolidate = consolidator(this.enqueue.bind(this), this.clientCache, this.recordQuery.bind(this));
|
|
93
|
+
} else if (!flag && this._consolidate) {
|
|
94
|
+
this._consolidate = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
request(request, priority = Priority.Normal) {
|
|
99
|
+
const result = queryResult();
|
|
100
|
+
const entry = { request, result };
|
|
101
|
+
if (this._consolidate) {
|
|
102
|
+
this._consolidate.add(entry, priority);
|
|
103
|
+
} else {
|
|
104
|
+
this.enqueue(entry, priority);
|
|
131
105
|
}
|
|
132
|
-
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cancel(requests) {
|
|
110
|
+
const set = new Set(requests);
|
|
111
|
+
this.queue.remove(({ result }) => set.has(result));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
clear() {
|
|
115
|
+
this.queue.remove(({ result }) => {
|
|
116
|
+
result.reject('Cleared');
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
record() {
|
|
122
|
+
let state = [];
|
|
123
|
+
const recorder = {
|
|
124
|
+
add(query) {
|
|
125
|
+
state.push(query);
|
|
126
|
+
},
|
|
127
|
+
reset() {
|
|
128
|
+
state = [];
|
|
129
|
+
},
|
|
130
|
+
snapshot() {
|
|
131
|
+
return state.slice();
|
|
132
|
+
},
|
|
133
|
+
stop() {
|
|
134
|
+
this.recorders = this.recorders.filter(x => x !== recorder);
|
|
135
|
+
return state;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
this.recorders.push(recorder);
|
|
139
|
+
return recorder;
|
|
140
|
+
}
|
|
133
141
|
}
|
package/src/Selection.js
CHANGED
|
@@ -98,19 +98,17 @@ export class Selection extends Param {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* The
|
|
101
|
+
* The selection clause resolver.
|
|
102
102
|
*/
|
|
103
|
-
get
|
|
104
|
-
return this.
|
|
103
|
+
get resolver() {
|
|
104
|
+
return this._resolver;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
* This method ensures compatibility where a normal Param is expected.
|
|
108
|
+
* Indicate if this selection has a single resolution strategy.
|
|
110
109
|
*/
|
|
111
|
-
get
|
|
112
|
-
|
|
113
|
-
return this.active?.value;
|
|
110
|
+
get single() {
|
|
111
|
+
return this._resolver.single;
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
/**
|
|
@@ -121,10 +119,27 @@ export class Selection extends Param {
|
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
/**
|
|
124
|
-
*
|
|
122
|
+
* The current active (most recently updated) selection clause.
|
|
125
123
|
*/
|
|
126
|
-
get
|
|
127
|
-
return this.
|
|
124
|
+
get active() {
|
|
125
|
+
return this.clauses.active;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The value corresponding to the current active selection clause.
|
|
130
|
+
* This method ensures compatibility where a normal Param is expected.
|
|
131
|
+
*/
|
|
132
|
+
get value() {
|
|
133
|
+
return this.active?.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The value corresponding to a given source. Returns undefined if
|
|
138
|
+
* this selection does not include a clause from this source.
|
|
139
|
+
* @param {*} source The clause source to look up the value for.
|
|
140
|
+
*/
|
|
141
|
+
valueFor(source) {
|
|
142
|
+
return this.clauses.find(c => c.source === source)?.value;
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
/**
|