@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-core",
3
- "version": "0.9.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 (http://idl.cs.washington.edu)",
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-dev195.0",
32
- "@uwdata/mosaic-sql": "^0.9.0",
33
- "apache-arrow": "^15.0.2"
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.9.0"
36
+ "@uwdata/mosaic-duckdb": "^0.10.0"
37
37
  },
38
- "gitHead": "89bb9b0dfa747aed691eaeba35379525a6764c61"
38
+ "gitHead": "94fc4f0d4efc622001f6afd6714d1e9dda745be2"
39
39
  }
@@ -1,9 +1,13 @@
1
1
  import { socketConnector } from './connectors/socket.js';
2
- import { FilterGroup } from './FilterGroup.js';
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(), options = {}) {
25
- const {
26
- logger = console,
27
- manager = new QueryManager()
28
- } = options;
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
- * Set configuration options for this coordinator.
46
- * @param {object} [options] Configration options.
47
- * @param {boolean} [options.cache=true] Boolean flag to enable/disable query caching.
48
- * @param {boolean} [options.consolidate=true] Boolean flag to enable/disable query consolidation.
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 }).then(
112
- data => client.queryResult(data).update(),
113
- err => { client.queryError(err); this._logger.error(err); }
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.filterGroups.get(client.filterBy)?.reset();
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 the client to disconnect
203
+ * @param {import('./MosaicClient.js').MosaicClient} client The Mosaic
204
+ * client to connect.
127
205
  */
128
206
  async connect(client) {
129
- const { clients, filterGroups, indexes } = this;
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 filters
144
- const filter = client.filterBy;
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
- * @param {import('./MosaicClient.js').MosaicClient} client the client to disconnect
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
  }