@uwdata/mosaic-core 0.1.0 → 0.2.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 +2675 -1198
- package/dist/mosaic-core.min.js +7 -7
- package/package.json +5 -5
- package/src/Catalog.js +13 -1
- package/src/Coordinator.js +54 -50
- package/src/DataTileIndexer.js +49 -50
- package/src/FilterGroup.js +3 -6
- package/src/Param.js +67 -21
- package/src/QueryManager.js +109 -0
- package/src/Selection.js +229 -33
- package/src/{clients → connectors}/rest.js +1 -1
- package/src/{clients → connectors}/socket.js +1 -1
- package/src/{clients → connectors}/wasm.js +1 -1
- package/src/index.js +7 -4
- package/src/util/AsyncDispatch.js +180 -0
- package/src/util/cache.js +58 -0
- package/src/util/priority-queue.js +85 -0
- package/src/util/synchronizer.js +47 -0
- package/src/QueryCache.js +0 -65
- package/src/util/skip-client.js +0 -3
- package/src/util/sql-from.js +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scalable and extensible linked data views.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mosaic",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"prepublishOnly": "npm run test && npm run lint && npm run build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@duckdb/duckdb-wasm": "^1.
|
|
32
|
-
"@uwdata/mosaic-sql": "^0.
|
|
33
|
-
"apache-arrow": "^
|
|
31
|
+
"@duckdb/duckdb-wasm": "^1.25.0",
|
|
32
|
+
"@uwdata/mosaic-sql": "^0.2.0",
|
|
33
|
+
"apache-arrow": "^12.0.0"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "e53cd914c807f99aabe78dcbe618dd9543e2f438"
|
|
36
36
|
}
|
package/src/Catalog.js
CHANGED
|
@@ -49,8 +49,20 @@ export class Catalog {
|
|
|
49
49
|
// no need for summary statistics
|
|
50
50
|
if (!stats?.length) return colInfo;
|
|
51
51
|
|
|
52
|
-
const result = await this.mc.query(
|
|
52
|
+
const result = await this.mc.query(
|
|
53
|
+
summarize(colInfo, stats),
|
|
54
|
+
{ persist: true }
|
|
55
|
+
);
|
|
53
56
|
const info = { ...colInfo, ...(Array.from(result)[0]) };
|
|
57
|
+
|
|
58
|
+
// coerce bigint to number
|
|
59
|
+
for (const key in info) {
|
|
60
|
+
const value = info[key];
|
|
61
|
+
if (typeof value === 'bigint') {
|
|
62
|
+
info[key] = Number(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
return info;
|
|
55
67
|
}
|
|
56
68
|
|
package/src/Coordinator.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { socketConnector } from './connectors/socket.js';
|
|
2
2
|
import { Catalog } from './Catalog.js';
|
|
3
3
|
import { FilterGroup } from './FilterGroup.js';
|
|
4
|
-
import {
|
|
4
|
+
import { QueryManager, Priority } from './QueryManager.js';
|
|
5
5
|
import { voidLogger } from './util/void-logger.js';
|
|
6
6
|
|
|
7
7
|
let _instance;
|
|
@@ -16,22 +16,26 @@ export function coordinator(instance) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export class Coordinator {
|
|
19
|
-
constructor(db =
|
|
19
|
+
constructor(db = socketConnector(), options = {}) {
|
|
20
20
|
this.catalog = new Catalog(this);
|
|
21
|
+
this.manager = options.manager || QueryManager();
|
|
21
22
|
this.logger(options.logger || console);
|
|
22
23
|
this.configure(options);
|
|
23
|
-
this.
|
|
24
|
+
this.databaseConnector(db);
|
|
24
25
|
this.clear();
|
|
26
|
+
this._recorders = [];
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
logger(logger) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
if (arguments.length) {
|
|
31
|
+
this._logger = logger || voidLogger();
|
|
32
|
+
this.manager.logger(this._logger);
|
|
33
|
+
}
|
|
34
|
+
return this._logger;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
configure({ cache = true, indexes = true }) {
|
|
34
|
-
this.cache
|
|
38
|
+
this.manager.cache(cache);
|
|
35
39
|
this.indexes = indexes;
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -42,58 +46,58 @@ export class Coordinator {
|
|
|
42
46
|
this.clients = new Set;
|
|
43
47
|
this.filterGroups = new Map;
|
|
44
48
|
}
|
|
45
|
-
if (cache) this.cache.clear();
|
|
49
|
+
if (cache) this.manager.cache().clear();
|
|
46
50
|
if (catalog) this.catalog.clear();
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.db = db;
|
|
52
|
-
}
|
|
53
|
-
return this.db;
|
|
53
|
+
databaseConnector(db) {
|
|
54
|
+
return this.manager.connector(db);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this._logger.error(err);
|
|
61
|
-
}
|
|
57
|
+
// -- Query Management ----
|
|
58
|
+
|
|
59
|
+
cancel(requests) {
|
|
60
|
+
this.manager.cancel(requests);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const t0 = performance.now();
|
|
67
|
-
const cached = this.cache.get(sql);
|
|
68
|
-
if (cached) {
|
|
69
|
-
this._logger.debug('Cache');
|
|
70
|
-
return cached;
|
|
71
|
-
} else {
|
|
72
|
-
const request = this.db.query({ type, sql });
|
|
73
|
-
const result = cache ? this.cache.set(sql, request) : request;
|
|
74
|
-
result.then(() => this._logger.debug(`Query: ${performance.now() - t0}`));
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
63
|
+
exec(query, { priority = Priority.Normal } = {}) {
|
|
64
|
+
return this.manager.request({ type: 'exec', query }, priority);
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
67
|
+
query(query, {
|
|
68
|
+
type = 'arrow',
|
|
69
|
+
cache = true,
|
|
70
|
+
priority = Priority.Normal,
|
|
71
|
+
...options
|
|
72
|
+
} = {}) {
|
|
73
|
+
return this.manager.request({ type, query, cache, options }, priority);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
prefetch(query, options = {}) {
|
|
77
|
+
return this.query(query, { ...options, cache: true, priority: Priority.Low });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
createBundle(name, queries, priority = Priority.Low) {
|
|
81
|
+
const options = { name, queries };
|
|
82
|
+
return this.manager.request({ type: 'create-bundle', options }, priority);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
loadBundle(name, priority = Priority.High) {
|
|
86
|
+
const options = { name };
|
|
87
|
+
return this.manager.request({ type: 'load-bundle', options }, priority);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// -- Client Management ----
|
|
91
|
+
|
|
92
|
+
updateClient(client, query, priority = Priority.Normal) {
|
|
93
|
+
client.queryPending();
|
|
94
|
+
return this.query(query, { priority }).then(
|
|
95
|
+
data => client.queryResult(data).update(),
|
|
96
|
+
err => { client.queryError(err); this._logger.error(err); }
|
|
97
|
+
);
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
requestQuery(client, query) {
|
|
97
101
|
this.filterGroups.get(client.filterBy)?.reset();
|
|
98
102
|
return query
|
|
99
103
|
? this.updateClient(client, query)
|
|
@@ -111,7 +115,7 @@ export class Coordinator {
|
|
|
111
115
|
// retrieve field statistics
|
|
112
116
|
const fields = client.fields();
|
|
113
117
|
if (fields?.length) {
|
|
114
|
-
client.
|
|
118
|
+
client.fieldInfo(await catalog.queryFields(fields));
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
// connect filters
|
package/src/DataTileIndexer.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Query,
|
|
1
|
+
import { Query, and, asColumn, epoch_ms, isBetween, sql } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { fnv_hash } from './util/hash.js';
|
|
3
|
-
import { skipClient } from './util/skip-client.js';
|
|
4
3
|
|
|
5
4
|
const identity = x => x;
|
|
6
5
|
|
|
@@ -30,35 +29,43 @@ export class DataTileIndexer {
|
|
|
30
29
|
this.activeView = null;
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
clear() {
|
|
33
|
+
if (this.indices) {
|
|
34
|
+
this.mc.cancel(Array.from(this.indices.values(), index => index.result));
|
|
35
|
+
this.indices = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
index(clients, active) {
|
|
34
40
|
if (this.clients !== clients) {
|
|
35
41
|
// test client views for compatibility
|
|
36
|
-
const cols = Array.from(clients
|
|
42
|
+
const cols = Array.from(clients, getIndexColumns);
|
|
37
43
|
const from = cols[0]?.from;
|
|
38
44
|
this.enabled = cols.every(c => c && c.from === from);
|
|
39
45
|
this.clients = clients;
|
|
40
|
-
this.indices = null;
|
|
41
46
|
this.activeView = null;
|
|
47
|
+
this.clear();
|
|
42
48
|
}
|
|
43
49
|
if (!this.enabled) return false; // client views are not indexable
|
|
44
50
|
|
|
45
51
|
active = active || this.selection.active;
|
|
46
52
|
const { source } = active;
|
|
53
|
+
if (source && source === this.activeView?.source) return true; // we're good!
|
|
54
|
+
|
|
55
|
+
this.clear();
|
|
47
56
|
if (!source) return false; // nothing to work with
|
|
48
|
-
if (source === this.activeView?.source) return true; // we're good!
|
|
49
57
|
const activeView = this.activeView = getActiveView(active);
|
|
50
58
|
if (!activeView) return false; // active selection clause not compatible
|
|
51
59
|
|
|
52
60
|
this.mc.logger().warn('DATA TILE INDEX CONSTRUCTION');
|
|
53
61
|
|
|
54
|
-
// create a selection with the active
|
|
55
|
-
const sel = this.selection.
|
|
62
|
+
// create a selection with the active source removed
|
|
63
|
+
const sel = this.selection.remove(source);
|
|
56
64
|
|
|
57
65
|
// generate data tile indices
|
|
58
66
|
const indices = this.indices = new Map;
|
|
59
|
-
const promises = [];
|
|
60
67
|
for (const client of clients) {
|
|
61
|
-
if (sel.
|
|
68
|
+
if (sel.skip(client, active)) continue;
|
|
62
69
|
const index = getIndexColumns(client);
|
|
63
70
|
|
|
64
71
|
// build index construction query
|
|
@@ -76,11 +83,9 @@ export class DataTileIndexer {
|
|
|
76
83
|
const sql = query.toString();
|
|
77
84
|
const id = (fnv_hash(sql) >>> 0).toString(16);
|
|
78
85
|
const table = `tile_index_${id}`;
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
const result = createIndex(this.mc, table, sql);
|
|
87
|
+
indices.set(client, { table, result, ...index });
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
return promises;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
async update() {
|
|
@@ -100,12 +105,12 @@ export class DataTileIndexer {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const { table, dims, aggr } = index;
|
|
103
|
-
|
|
108
|
+
const query = Query
|
|
104
109
|
.select(dims, aggr)
|
|
105
110
|
.from(table)
|
|
106
111
|
.groupby(dims)
|
|
107
|
-
.where(filter)
|
|
108
|
-
);
|
|
112
|
+
.where(filter);
|
|
113
|
+
return this.mc.updateClient(client, query);
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -113,22 +118,22 @@ function getActiveView(clause) {
|
|
|
113
118
|
const { source, schema } = clause;
|
|
114
119
|
let columns = clause.predicate?.columns;
|
|
115
120
|
if (!schema || !columns) return null;
|
|
116
|
-
const { type, scales } = schema;
|
|
121
|
+
const { type, scales, pixelSize = 1 } = schema;
|
|
117
122
|
let predicate;
|
|
118
123
|
|
|
119
124
|
if (type === 'interval' && scales) {
|
|
120
|
-
const bins = scales.map(s => binInterval(s));
|
|
125
|
+
const bins = scales.map(s => binInterval(s, pixelSize));
|
|
121
126
|
if (bins.some(b => b == null)) return null; // unsupported scale type
|
|
122
127
|
|
|
123
128
|
if (bins.length === 1) {
|
|
124
|
-
predicate = p => p ? isBetween('active0', p.
|
|
125
|
-
columns = { active0: bins[0](clause.predicate.
|
|
129
|
+
predicate = p => p ? isBetween('active0', p.range.map(bins[0])) : [];
|
|
130
|
+
columns = { active0: bins[0](clause.predicate.field) };
|
|
126
131
|
} else {
|
|
127
132
|
predicate = p => p
|
|
128
|
-
? and(p.
|
|
133
|
+
? and(p.children.map(({ range }, i) => isBetween(`active${i}`, range.map(bins[i]))))
|
|
129
134
|
: [];
|
|
130
135
|
columns = Object.fromEntries(
|
|
131
|
-
clause.predicate.
|
|
136
|
+
clause.predicate.children.map((p, i) => [`active${i}`, bins[i](p.field)])
|
|
132
137
|
);
|
|
133
138
|
}
|
|
134
139
|
} else if (type === 'point') {
|
|
@@ -141,53 +146,47 @@ function getActiveView(clause) {
|
|
|
141
146
|
return { source, columns, predicate };
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
function binInterval(scale) {
|
|
149
|
+
function binInterval(scale, pixelSize) {
|
|
145
150
|
const { type, domain, range } = scale;
|
|
146
|
-
let lift,
|
|
151
|
+
let lift, toSql;
|
|
147
152
|
|
|
148
153
|
switch (type) {
|
|
149
154
|
case 'linear':
|
|
150
155
|
lift = identity;
|
|
151
|
-
|
|
156
|
+
toSql = asColumn;
|
|
152
157
|
break;
|
|
153
158
|
case 'log':
|
|
154
159
|
lift = Math.log;
|
|
155
|
-
|
|
160
|
+
toSql = c => sql`LN(${asColumn(c)})`;
|
|
156
161
|
break;
|
|
157
162
|
case 'symlog':
|
|
158
163
|
// TODO: support log constants other than 1?
|
|
159
164
|
lift = x => Math.sign(x) * Math.log1p(Math.abs(x));
|
|
160
|
-
|
|
165
|
+
toSql = c => (c = asColumn(c), sql`SIGN(${c}) * LN(1 + ABS(${c}))`);
|
|
161
166
|
break;
|
|
162
167
|
case 'sqrt':
|
|
163
168
|
lift = Math.sqrt;
|
|
164
|
-
|
|
169
|
+
toSql = c => sql`SQRT(${asColumn(c)})`;
|
|
165
170
|
break;
|
|
166
171
|
case 'utc':
|
|
167
172
|
case 'time':
|
|
168
173
|
lift = x => +x;
|
|
169
|
-
|
|
174
|
+
toSql = c => c instanceof Date ? +c : epoch_ms(asColumn(c));
|
|
170
175
|
break;
|
|
171
176
|
}
|
|
172
|
-
return lift ? binFunction(domain, range, lift,
|
|
177
|
+
return lift ? binFunction(domain, range, pixelSize, lift, toSql) : null;
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
function binFunction(domain, range, lift,
|
|
180
|
+
function binFunction(domain, range, pixelSize, lift, toSql) {
|
|
176
181
|
const lo = lift(Math.min(domain[0], domain[1]));
|
|
177
182
|
const hi = lift(Math.max(domain[0], domain[1]));
|
|
178
|
-
const a = Math.abs(lift(range[1]) - lift(range[0])) / (hi - lo);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
asColumn(value).columns
|
|
182
|
-
);
|
|
183
|
+
const a = (Math.abs(lift(range[1]) - lift(range[0])) / (hi - lo)) / pixelSize;
|
|
184
|
+
const s = pixelSize === 1 ? '' : `${pixelSize}::INTEGER * `;
|
|
185
|
+
return value => sql`${s}FLOOR(${a}::DOUBLE * (${toSql(value)} - ${lo}::DOUBLE))::INTEGER`;
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
await mc.exec(`CREATE TEMP TABLE IF NOT EXISTS ${table} AS ${query}`);
|
|
188
|
-
} catch (err) {
|
|
189
|
-
mc.logger().error(err);
|
|
190
|
-
}
|
|
188
|
+
function createIndex(mc, table, query) {
|
|
189
|
+
return mc.exec(`CREATE TEMP TABLE IF NOT EXISTS ${table} AS ${query}`);
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
const NO_INDEX = { from: NaN };
|
|
@@ -199,25 +198,25 @@ function getIndexColumns(client) {
|
|
|
199
198
|
if (!from || !q.groupby) return NO_INDEX;
|
|
200
199
|
const g = new Set(q.groupby().map(c => c.column));
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
const aggr = [];
|
|
202
|
+
const dims = [];
|
|
204
203
|
let count;
|
|
205
204
|
|
|
206
205
|
for (const { as, expr: { aggregate } } of q.select()) {
|
|
207
206
|
switch (aggregate?.toUpperCase()) {
|
|
208
207
|
case 'COUNT':
|
|
209
208
|
case 'SUM':
|
|
210
|
-
aggr.push({ [as]:
|
|
209
|
+
aggr.push({ [as]: sql`SUM("${as}")::DOUBLE` });
|
|
211
210
|
break;
|
|
212
211
|
case 'AVG':
|
|
213
212
|
count = '_count_';
|
|
214
|
-
aggr.push({ [as]:
|
|
213
|
+
aggr.push({ [as]: sql`(SUM("${as}" * ${count}) / SUM(${count}))::DOUBLE` });
|
|
215
214
|
break;
|
|
216
215
|
case 'MAX':
|
|
217
|
-
aggr.push({ [as]:
|
|
216
|
+
aggr.push({ [as]: sql`MAX("${as}")` });
|
|
218
217
|
break;
|
|
219
218
|
case 'MIN':
|
|
220
|
-
aggr.push({ [as]:
|
|
219
|
+
aggr.push({ [as]: sql`MIN("${as}")` });
|
|
221
220
|
break;
|
|
222
221
|
default:
|
|
223
222
|
if (g.has(as)) dims.push(as);
|
|
@@ -228,7 +227,7 @@ function getIndexColumns(client) {
|
|
|
228
227
|
return {
|
|
229
228
|
aggr,
|
|
230
229
|
dims,
|
|
231
|
-
count: count ? { [count]:
|
|
230
|
+
count: count ? { [count]: sql`COUNT(*)` } : {},
|
|
232
231
|
from
|
|
233
232
|
};
|
|
234
233
|
}
|
|
@@ -244,7 +243,7 @@ function getBaseTable(query) {
|
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
// handle set operations / subqueries
|
|
247
|
-
|
|
246
|
+
const base = getBaseTable(subq[0]);
|
|
248
247
|
for (let i = 1; i < subq.length; ++i) {
|
|
249
248
|
const from = getBaseTable(subq[i]);
|
|
250
249
|
if (from === undefined) continue;
|
package/src/FilterGroup.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { DataTileIndexer } from './DataTileIndexer.js';
|
|
2
|
-
import { throttle } from './util/throttle.js';
|
|
3
2
|
|
|
4
3
|
export class FilterGroup {
|
|
5
4
|
constructor(coordinator, selection, index = true) {
|
|
@@ -9,10 +8,8 @@ export class FilterGroup {
|
|
|
9
8
|
this.indexer = index ? new DataTileIndexer(this.mc, selection) : null;
|
|
10
9
|
|
|
11
10
|
const { value, activate } = this.handlers = {
|
|
12
|
-
value:
|
|
13
|
-
activate: clause =>
|
|
14
|
-
this.indexer?.index(this.clients, clause);
|
|
15
|
-
}
|
|
11
|
+
value: () => this.update(),
|
|
12
|
+
activate: clause => this.indexer?.index(this.clients, clause)
|
|
16
13
|
};
|
|
17
14
|
selection.addEventListener('value', value);
|
|
18
15
|
selection.addEventListener('activate', activate);
|
|
@@ -40,7 +37,7 @@ export class FilterGroup {
|
|
|
40
37
|
return this;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
update() {
|
|
44
41
|
const { mc, indexer, clients, selection } = this;
|
|
45
42
|
return indexer?.index(clients)
|
|
46
43
|
? indexer.update()
|
package/src/Param.js
CHANGED
|
@@ -1,46 +1,92 @@
|
|
|
1
|
+
import { AsyncDispatch } from './util/AsyncDispatch.js';
|
|
1
2
|
import { distinct } from './util/distinct.js';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Test if a value is a Param instance.
|
|
6
|
+
* @param {*} x The value to test.
|
|
7
|
+
* @returns {boolean} True if the input is a Param, false otherwise.
|
|
8
|
+
*/
|
|
3
9
|
export function isParam(x) {
|
|
4
10
|
return x instanceof Param;
|
|
5
11
|
}
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Represents a dynamic parameter that dispatches updates
|
|
15
|
+
* upon parameter changes.
|
|
16
|
+
*/
|
|
17
|
+
export class Param extends AsyncDispatch {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a new Param instance.
|
|
21
|
+
* @param {*} value The initial value of the Param.
|
|
22
|
+
*/
|
|
8
23
|
constructor(value) {
|
|
24
|
+
super();
|
|
9
25
|
this._value = value;
|
|
10
|
-
this._listeners = new Map;
|
|
11
26
|
}
|
|
12
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Create a new Param instance with the given initial value.
|
|
30
|
+
* @param {*} value The initial value of the Param.
|
|
31
|
+
* @returns {Param} The new Param instance.
|
|
32
|
+
*/
|
|
13
33
|
static value(value) {
|
|
14
34
|
return new Param(value);
|
|
15
35
|
}
|
|
16
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Create a new Param instance over an array of initial values,
|
|
39
|
+
* which may contain nested Params.
|
|
40
|
+
* @param {*} values The initial values of the Param.
|
|
41
|
+
* @returns {Param} The new Param instance.
|
|
42
|
+
*/
|
|
43
|
+
static array(values) {
|
|
44
|
+
if (values.some(v => isParam(v))) {
|
|
45
|
+
const p = new Param();
|
|
46
|
+
const update = () => p.update(values.map(v => isParam(v) ? v.value : v));
|
|
47
|
+
update();
|
|
48
|
+
values.forEach(v => isParam(v) ? v.addEventListener('value', update) : 0);
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
return new Param(values);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The current value of the Param.
|
|
56
|
+
*/
|
|
17
57
|
get value() {
|
|
18
58
|
return this._value;
|
|
19
59
|
}
|
|
20
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Update the Param value
|
|
63
|
+
* @param {*} value The new value of the Param.
|
|
64
|
+
* @param {object} [options] The update options.
|
|
65
|
+
* @param {boolean} [options.force] A boolean flag indicating if the Param
|
|
66
|
+
* should emit a 'value' event even if the internal value is unchanged.
|
|
67
|
+
* @returns {this} This Param instance.
|
|
68
|
+
*/
|
|
21
69
|
update(value, { force } = {}) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
addEventListener(type, callback) {
|
|
29
|
-
let list = this._listeners.get(type) || [];
|
|
30
|
-
if (!list.includes(callback)) {
|
|
31
|
-
list = list.concat(callback);
|
|
70
|
+
const shouldEmit = distinct(this._value, value) || force;
|
|
71
|
+
if (shouldEmit) {
|
|
72
|
+
this.emit('value', value);
|
|
73
|
+
} else {
|
|
74
|
+
this.cancel('value');
|
|
32
75
|
}
|
|
33
|
-
this
|
|
76
|
+
return this;
|
|
34
77
|
}
|
|
35
78
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Upon value-typed updates, sets the current value to the input value
|
|
81
|
+
* immediately prior to the event value being emitted to listeners.
|
|
82
|
+
* @param {string} type The event type.
|
|
83
|
+
* @param {*} value The input event value.
|
|
84
|
+
* @returns {*} The input event value.
|
|
85
|
+
*/
|
|
86
|
+
willEmit(type, value) {
|
|
87
|
+
if (type === 'value') {
|
|
88
|
+
this._value = value;
|
|
40
89
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
emit(type, event) {
|
|
44
|
-
this._listeners.get(type)?.forEach(l => l(event));
|
|
90
|
+
return value;
|
|
45
91
|
}
|
|
46
92
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { lruCache, voidCache } from './util/cache.js';
|
|
2
|
+
import { priorityQueue } from './util/priority-queue.js';
|
|
3
|
+
|
|
4
|
+
export const Priority = { High: 0, Normal: 1, Low: 2 };
|
|
5
|
+
|
|
6
|
+
export function QueryManager() {
|
|
7
|
+
const queue = priorityQueue(3);
|
|
8
|
+
let db;
|
|
9
|
+
let clientCache;
|
|
10
|
+
let logger;
|
|
11
|
+
let recorders = [];
|
|
12
|
+
let pending = null;
|
|
13
|
+
|
|
14
|
+
function next() {
|
|
15
|
+
if (pending || queue.isEmpty()) return;
|
|
16
|
+
const { request, result } = queue.next();
|
|
17
|
+
pending = submit(request, result);
|
|
18
|
+
pending.finally(() => { pending = null; next(); });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function submit(request, result) {
|
|
22
|
+
try {
|
|
23
|
+
const { query, type, cache = false, options } = request;
|
|
24
|
+
const sql = query ? String(query) : null;
|
|
25
|
+
|
|
26
|
+
// update recorders
|
|
27
|
+
if (recorders.length && sql) {
|
|
28
|
+
recorders.forEach(rec => rec.add(sql));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// check query cache
|
|
32
|
+
if (cache) {
|
|
33
|
+
const cached = clientCache.get(sql);
|
|
34
|
+
if (cached) {
|
|
35
|
+
logger.debug('Cache');
|
|
36
|
+
result.fulfill(cached);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// issue query, potentially cache result
|
|
42
|
+
const t0 = performance.now();
|
|
43
|
+
const data = await db.query({ type, sql, ...options });
|
|
44
|
+
if (cache) clientCache.set(sql, data);
|
|
45
|
+
logger.debug(`Request: ${(performance.now() - t0).toFixed(1)}`);
|
|
46
|
+
result.fulfill(data);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
result.reject(err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
cache(value) {
|
|
54
|
+
return value !== undefined
|
|
55
|
+
? (clientCache = value === true ? lruCache() : (value || voidCache()))
|
|
56
|
+
: clientCache;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
logger(value) {
|
|
60
|
+
return value ? (logger = value) : logger;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
connector(connector) {
|
|
64
|
+
return connector ? (db = connector) : db;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
request(request, priority = Priority.Normal) {
|
|
68
|
+
const result = queryResult();
|
|
69
|
+
queue.insert({ request, result }, priority);
|
|
70
|
+
next();
|
|
71
|
+
return result;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
cancel(requests) {
|
|
75
|
+
const set = new Set(requests);
|
|
76
|
+
queue.remove(({ result }) => set.has(result));
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
record() {
|
|
80
|
+
let state = [];
|
|
81
|
+
const recorder = {
|
|
82
|
+
add(query) {
|
|
83
|
+
state.push(query);
|
|
84
|
+
},
|
|
85
|
+
reset() {
|
|
86
|
+
state = [];
|
|
87
|
+
},
|
|
88
|
+
snapshot() {
|
|
89
|
+
return state.slice();
|
|
90
|
+
},
|
|
91
|
+
stop() {
|
|
92
|
+
recorders = recorders.filter(x => x !== recorder);
|
|
93
|
+
return state;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
recorders.push(recorder);
|
|
97
|
+
return recorder;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function queryResult() {
|
|
103
|
+
let resolve;
|
|
104
|
+
let reject;
|
|
105
|
+
const p = new Promise((r, e) => { resolve = r; reject = e; });
|
|
106
|
+
p.fulfill = value => (resolve(value), p);
|
|
107
|
+
p.reject = err => (reject(err), p);
|
|
108
|
+
return p;
|
|
109
|
+
}
|