@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Scalable and extensible linked data views.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mosaic",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"interface"
|
|
11
11
|
],
|
|
12
12
|
"license": "BSD-3-Clause",
|
|
13
|
-
"author": "Jeffrey Heer (
|
|
13
|
+
"author": "Jeffrey Heer (https://idl.uw.edu)",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "src/index.js",
|
|
16
16
|
"module": "src/index.js",
|
|
@@ -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.
|
|
33
|
-
"apache-arrow": "^
|
|
31
|
+
"@duckdb/duckdb-wasm": "^1.28.1-dev232.0",
|
|
32
|
+
"@uwdata/mosaic-sql": "^0.10.0",
|
|
33
|
+
"apache-arrow": "^16.1.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@uwdata/mosaic-duckdb": "^0.
|
|
36
|
+
"@uwdata/mosaic-duckdb": "^0.10.0"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "94fc4f0d4efc622001f6afd6714d1e9dda745be2"
|
|
39
39
|
}
|
package/src/Coordinator.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { socketConnector } from './connectors/socket.js';
|
|
2
|
-
import {
|
|
2
|
+
import { DataCubeIndexer } from './DataCubeIndexer.js';
|
|
3
3
|
import { QueryManager, Priority } from './QueryManager.js';
|
|
4
4
|
import { queryFieldInfo } from './util/field-info.js';
|
|
5
5
|
import { voidLogger } from './util/void-logger.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The singleton Coordinator instance.
|
|
9
|
+
* @type {Coordinator}
|
|
10
|
+
*/
|
|
7
11
|
let _instance;
|
|
8
12
|
|
|
9
13
|
/**
|
|
@@ -20,67 +24,112 @@ export function coordinator(instance) {
|
|
|
20
24
|
return _instance;
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A Mosaic Coordinator manages all database communication for clients and
|
|
29
|
+
* handles selection updates. The Coordinator also performs optimizations
|
|
30
|
+
* including query caching, consolidation, and data cube indexing.
|
|
31
|
+
* @param {*} [db] Database connector. Defaults to a web socket connection.
|
|
32
|
+
* @param {object} [options] Coordinator options.
|
|
33
|
+
* @param {*} [options.logger=console] The logger to use, defaults to `console`.
|
|
34
|
+
* @param {*} [options.manager] The query manager to use.
|
|
35
|
+
* @param {boolean} [options.cache=true] Boolean flag to enable/disable query caching.
|
|
36
|
+
* @param {boolean} [options.consolidate=true] Boolean flag to enable/disable query consolidation.
|
|
37
|
+
* @param {object} [options.indexes] Data cube indexer options.
|
|
38
|
+
*/
|
|
23
39
|
export class Coordinator {
|
|
24
|
-
constructor(db = socketConnector(),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
constructor(db = socketConnector(), {
|
|
41
|
+
logger = console,
|
|
42
|
+
manager = new QueryManager(),
|
|
43
|
+
cache = true,
|
|
44
|
+
consolidate = true,
|
|
45
|
+
indexes = {}
|
|
46
|
+
} = {}) {
|
|
29
47
|
this.manager = manager;
|
|
48
|
+
this.manager.cache(cache);
|
|
49
|
+
this.manager.consolidate(consolidate);
|
|
50
|
+
this.dataCubeIndexer = new DataCubeIndexer(this, indexes);
|
|
30
51
|
this.logger(logger);
|
|
31
|
-
this.configure(options);
|
|
32
52
|
this.databaseConnector(db);
|
|
33
53
|
this.clear();
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
logger(logger) {
|
|
37
|
-
if (arguments.length) {
|
|
38
|
-
this._logger = logger || voidLogger();
|
|
39
|
-
this.manager.logger(this._logger);
|
|
40
|
-
}
|
|
41
|
-
return this._logger;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
56
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {object} [options]
|
|
47
|
-
* @param {boolean} [options.
|
|
48
|
-
* @param {boolean} [options.
|
|
49
|
-
* @param {boolean|object} [options.indexes=true] Boolean flag to enable/disable
|
|
50
|
-
* automatic data cube indexes or an index options object.
|
|
57
|
+
* Clear the coordinator state.
|
|
58
|
+
* @param {object} [options] Options object.
|
|
59
|
+
* @param {boolean} [options.clients=true] If true, disconnect all clients.
|
|
60
|
+
* @param {boolean} [options.cache=true] If true, clear the query cache.
|
|
51
61
|
*/
|
|
52
|
-
configure({ cache = true, consolidate = true, indexes = true } = {}) {
|
|
53
|
-
this.manager.cache(cache);
|
|
54
|
-
this.manager.consolidate(consolidate);
|
|
55
|
-
this.indexes = indexes;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
62
|
clear({ clients = true, cache = true } = {}) {
|
|
59
63
|
this.manager.clear();
|
|
60
64
|
if (clients) {
|
|
65
|
+
this.filterGroups?.forEach(group => group.disconnect());
|
|
66
|
+
this.filterGroups = new Map;
|
|
61
67
|
this.clients?.forEach(client => this.disconnect(client));
|
|
62
|
-
this.filterGroups?.forEach(group => group.finalize());
|
|
63
68
|
this.clients = new Set;
|
|
64
|
-
this.filterGroups = new Map;
|
|
65
69
|
}
|
|
66
70
|
if (cache) this.manager.cache().clear();
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Get or set the database connector.
|
|
75
|
+
* @param {*} [db] The database connector to use.
|
|
76
|
+
* @returns The current database connector.
|
|
77
|
+
*/
|
|
69
78
|
databaseConnector(db) {
|
|
70
79
|
return this.manager.connector(db);
|
|
71
80
|
}
|
|
72
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Get or set the logger.
|
|
84
|
+
* @param {*} logger The logger to use.
|
|
85
|
+
* @returns The current logger
|
|
86
|
+
*/
|
|
87
|
+
logger(logger) {
|
|
88
|
+
if (arguments.length) {
|
|
89
|
+
this._logger = logger || voidLogger();
|
|
90
|
+
this.manager.logger(this._logger);
|
|
91
|
+
}
|
|
92
|
+
return this._logger;
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
// -- Query Management ----
|
|
74
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Cancel previosuly submitted query requests. These queries will be
|
|
99
|
+
* canceled if they are queued but have not yet been submitted.
|
|
100
|
+
* @param {import('./util/query-result.js').QueryResult[]} requests An array
|
|
101
|
+
* of query result objects, such as those returned by the `query` method.
|
|
102
|
+
*/
|
|
75
103
|
cancel(requests) {
|
|
76
104
|
this.manager.cancel(requests);
|
|
77
105
|
}
|
|
78
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Issue a query for which no result (return value) is needed.
|
|
109
|
+
* @param {import('@uwdata/mosaic-sql').Query | string} query The query.
|
|
110
|
+
* @param {object} [options] An options object.
|
|
111
|
+
* @param {number} [options.priority] The query priority, defaults to
|
|
112
|
+
* `Priority.Normal`.
|
|
113
|
+
* @returns {import('./util/query-result.js').QueryResult} A query result
|
|
114
|
+
* promise.
|
|
115
|
+
*/
|
|
79
116
|
exec(query, { priority = Priority.Normal } = {}) {
|
|
80
117
|
query = Array.isArray(query) ? query.join(';\n') : query;
|
|
81
118
|
return this.manager.request({ type: 'exec', query }, priority);
|
|
82
119
|
}
|
|
83
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Issue a query to the backing database. The submitted query may be
|
|
123
|
+
* consolidate with other queries and its results may be cached.
|
|
124
|
+
* @param {import('@uwdata/mosaic-sql').Query | string} query The query.
|
|
125
|
+
* @param {object} [options] An options object.
|
|
126
|
+
* @param {'arrow' | 'json'} [options.type] The query result format type.
|
|
127
|
+
* @param {boolean} [options.cache=true] If true, cache the query result.
|
|
128
|
+
* @param {number} [options.priority] The query priority, defaults to
|
|
129
|
+
* `Priority.Normal`.
|
|
130
|
+
* @returns {import('./util/query-result.js').QueryResult} A query result
|
|
131
|
+
* promise.
|
|
132
|
+
*/
|
|
84
133
|
query(query, {
|
|
85
134
|
type = 'arrow',
|
|
86
135
|
cache = true,
|
|
@@ -90,6 +139,15 @@ export class Coordinator {
|
|
|
90
139
|
return this.manager.request({ type, query, cache, options }, priority);
|
|
91
140
|
}
|
|
92
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Issue a query to prefetch data for later use. The query result is cached
|
|
144
|
+
* for efficient future access.
|
|
145
|
+
* @param {import('@uwdata/mosaic-sql').Query | string} query The query.
|
|
146
|
+
* @param {object} [options] An options object.
|
|
147
|
+
* @param {'arrow' | 'json'} [options.type] The query result format type.
|
|
148
|
+
* @returns {import('./util/query-result.js').QueryResult} A query result
|
|
149
|
+
* promise.
|
|
150
|
+
*/
|
|
93
151
|
prefetch(query, options = {}) {
|
|
94
152
|
return this.query(query, { ...options, cache: true, priority: Priority.Low });
|
|
95
153
|
}
|
|
@@ -106,16 +164,35 @@ export class Coordinator {
|
|
|
106
164
|
|
|
107
165
|
// -- Client Management ----
|
|
108
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Update client data by submitting the given query and returning the
|
|
169
|
+
* data (or error) to the client.
|
|
170
|
+
* @param {import('./MosaicClient.js').MosaicClient} client A Mosaic client.
|
|
171
|
+
* @param {import('@uwdata/mosaic-sql').Query | string} query The data query.
|
|
172
|
+
* @param {number} [priority] The query priority.
|
|
173
|
+
* @returns {Promise} A Promise that resolves upon completion of the update.
|
|
174
|
+
*/
|
|
109
175
|
updateClient(client, query, priority = Priority.Normal) {
|
|
110
176
|
client.queryPending();
|
|
111
|
-
return this.query(query, { priority })
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
177
|
+
return this.query(query, { priority })
|
|
178
|
+
.then(
|
|
179
|
+
data => client.queryResult(data).update(),
|
|
180
|
+
err => { this._logger.error(err); client.queryError(err); }
|
|
181
|
+
)
|
|
182
|
+
.catch(err => this._logger.error(err));
|
|
115
183
|
}
|
|
116
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Issue a query request for a client. If the query is null or undefined,
|
|
187
|
+
* the client is simply updated. Otherwise `updateClient` is called. As a
|
|
188
|
+
* side effect, this method clears the current data cube indexer state.
|
|
189
|
+
* @param {import('./MosaicClient.js').MosaicClient} client The client
|
|
190
|
+
* to update.
|
|
191
|
+
* @param {import('@uwdata/mosaic-sql').Query | string | null} [query]
|
|
192
|
+
* The query to issue.
|
|
193
|
+
*/
|
|
117
194
|
requestQuery(client, query) {
|
|
118
|
-
this.
|
|
195
|
+
this.dataCubeIndexer.clear();
|
|
119
196
|
return query
|
|
120
197
|
? this.updateClient(client, query)
|
|
121
198
|
: client.update();
|
|
@@ -123,10 +200,11 @@ export class Coordinator {
|
|
|
123
200
|
|
|
124
201
|
/**
|
|
125
202
|
* Connect a client to the coordinator.
|
|
126
|
-
* @param {import('./MosaicClient.js').MosaicClient} client
|
|
203
|
+
* @param {import('./MosaicClient.js').MosaicClient} client The Mosaic
|
|
204
|
+
* client to connect.
|
|
127
205
|
*/
|
|
128
206
|
async connect(client) {
|
|
129
|
-
const { clients
|
|
207
|
+
const { clients } = this;
|
|
130
208
|
|
|
131
209
|
if (clients.has(client)) {
|
|
132
210
|
throw new Error('Client already connected.');
|
|
@@ -140,30 +218,97 @@ export class Coordinator {
|
|
|
140
218
|
client.fieldInfo(await queryFieldInfo(this, fields));
|
|
141
219
|
}
|
|
142
220
|
|
|
143
|
-
// connect
|
|
144
|
-
|
|
145
|
-
if (filter) {
|
|
146
|
-
if (filterGroups.has(filter)) {
|
|
147
|
-
filterGroups.get(filter).add(client);
|
|
148
|
-
} else {
|
|
149
|
-
const group = new FilterGroup(this, filter, indexes);
|
|
150
|
-
filterGroups.set(filter, group.add(client));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
221
|
+
// connect filter selection
|
|
222
|
+
connectSelection(this, client.filterBy, client);
|
|
153
223
|
|
|
154
224
|
client.requestQuery();
|
|
155
225
|
}
|
|
156
226
|
|
|
157
227
|
/**
|
|
158
228
|
* Disconnect a client from the coordinator.
|
|
159
|
-
*
|
|
160
|
-
*
|
|
229
|
+
* @param {import('./MosaicClient.js').MosaicClient} client The Mosaic
|
|
230
|
+
* client to disconnect.
|
|
161
231
|
*/
|
|
162
232
|
disconnect(client) {
|
|
163
233
|
const { clients, filterGroups } = this;
|
|
164
234
|
if (!clients.has(client)) return;
|
|
165
235
|
clients.delete(client);
|
|
166
|
-
filterGroups.get(client.filterBy)?.remove(client);
|
|
167
236
|
client.coordinator = null;
|
|
237
|
+
|
|
238
|
+
const group = filterGroups.get(client.filterBy);
|
|
239
|
+
if (group) {
|
|
240
|
+
group.clients.delete(client);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Connect a selection-client pair to the coordinator to process updates.
|
|
247
|
+
* @param {Coordinator} mc The Mosaic coordinator.
|
|
248
|
+
* @param {import('./Selection.js').Selection} selection A selection.
|
|
249
|
+
* @param {import('./MosaicClient.js').MosaicClient} client A Mosiac
|
|
250
|
+
* client that is filtered by the given selection.
|
|
251
|
+
*/
|
|
252
|
+
function connectSelection(mc, selection, client) {
|
|
253
|
+
if (!selection) return;
|
|
254
|
+
let entry = mc.filterGroups.get(selection);
|
|
255
|
+
if (!entry) {
|
|
256
|
+
const activate = clause => activateSelection(mc, selection, clause);
|
|
257
|
+
const value = () => updateSelection(mc, selection);
|
|
258
|
+
|
|
259
|
+
selection.addEventListener('activate', activate);
|
|
260
|
+
selection.addEventListener('value', value);
|
|
261
|
+
|
|
262
|
+
entry = {
|
|
263
|
+
selection,
|
|
264
|
+
clients: new Set,
|
|
265
|
+
disconnect() {
|
|
266
|
+
selection.removeEventListener('activate', activate);
|
|
267
|
+
selection.removeEventListener('value', value);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
mc.filterGroups.set(selection, entry);
|
|
168
271
|
}
|
|
272
|
+
entry.clients.add(client);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Activate a selection, providing a clause indicative of potential
|
|
277
|
+
* next updates. Activation provides a preview of likely next events,
|
|
278
|
+
* enabling potential precomputation to optimize updates.
|
|
279
|
+
* @param {Coordinator} mc The Mosaic coordinator.
|
|
280
|
+
* @param {import('./Selection.js').Selection} selection A selection.
|
|
281
|
+
* @param {import('./util/selection-types.js').SelectionClause} clause A
|
|
282
|
+
* selection clause representative of the activation.
|
|
283
|
+
*/
|
|
284
|
+
function activateSelection(mc, selection, clause) {
|
|
285
|
+
const { dataCubeIndexer, filterGroups } = mc;
|
|
286
|
+
const { clients } = filterGroups.get(selection);
|
|
287
|
+
for (const client of clients) {
|
|
288
|
+
dataCubeIndexer.index(client, selection, clause);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Process an updated selection value, querying filtered data for any
|
|
294
|
+
* associated clients.
|
|
295
|
+
* @param {Coordinator} mc The Mosaic coordinator.
|
|
296
|
+
* @param {import('./Selection.js').Selection} selection A selection.
|
|
297
|
+
* @returns {Promise} A Promise that resolves when the update completes.
|
|
298
|
+
*/
|
|
299
|
+
function updateSelection(mc, selection) {
|
|
300
|
+
const { dataCubeIndexer, filterGroups } = mc;
|
|
301
|
+
const { clients } = filterGroups.get(selection);
|
|
302
|
+
const { active } = selection;
|
|
303
|
+
return Promise.allSettled(Array.from(clients, client => {
|
|
304
|
+
const info = dataCubeIndexer.index(client, selection, active);
|
|
305
|
+
const filter = info ? null : selection.predicate(client);
|
|
306
|
+
|
|
307
|
+
// skip due to cross-filtering
|
|
308
|
+
if (info?.skip || (!info && !filter)) return;
|
|
309
|
+
|
|
310
|
+
// @ts-ignore
|
|
311
|
+
const query = info?.query(active.predicate) ?? client.query(filter);
|
|
312
|
+
return mc.updateClient(client, query);
|
|
313
|
+
}));
|
|
169
314
|
}
|