UNPKG

@uwdata/mosaic-core

Version:

Scalable and extensible linked data views.

269 lines 10.5 kB
import { SocketConnector } from './connectors/socket.js'; import { PreAggregator } from './preagg/PreAggregator.js'; import { voidLogger } from './util/void-logger.js'; import { QueryManager, Priority } from './QueryManager.js'; /** * The singleton Coordinator instance. */ let _instance; /** * Set or retrieve the coordinator instance. * @param instance The coordinator instance to set * @returns The coordinator instance */ export function coordinator(instance) { if (instance) { _instance = instance; } else if (_instance == null) { _instance = new Coordinator(); } return _instance; } /** * A Mosaic Coordinator manages all database communication for clients and * handles selection updates. The Coordinator also performs optimizations * including query caching, consolidation, and pre-aggregation. */ export class Coordinator { manager; preaggregator; clients = new Set; filterGroups = new Map; _logger = voidLogger(); /** * @param db Database connector. Defaults to a web socket connection. * @param options Coordinator options. * @param options.logger The logger to use, defaults to `console`. * @param options.manager The query manager to use. * @param options.cache Boolean flag to enable/disable query caching. * @param options.consolidate Boolean flag to enable/disable query consolidation. * @param options.preagg Options for the Pre-aggregator. */ constructor(db = new SocketConnector(), options = {}) { const { logger = console, manager = new QueryManager(), cache = true, consolidate = true, preagg = {} } = options; this.manager = manager; this.manager.cache(cache); this.manager.consolidate(consolidate); this.databaseConnector(db); this.logger(logger); this.clear(); this.preaggregator = new PreAggregator(this, preagg); } /** * Clear the coordinator state. * @param options Options object. * @param options.clients If true, disconnect all clients. * @param options.cache If true, clear the query cache. */ clear(options = {}) { const { clients = true, cache = true } = options; this.manager.clear(); if (clients) { this.filterGroups?.forEach(group => group.disconnect()); this.filterGroups = new Map; this.clients?.forEach(client => this.disconnect(client)); this.clients = new Set; } if (cache) this.manager.cache().clear(); } databaseConnector(db) { return db ? this.manager.connector(db) : this.manager.connector(); } /** * Get or set the logger. * @param logger The logger to use. * @returns The current logger */ logger(logger) { if (arguments.length) { this._logger = logger || voidLogger(); this.manager.logger(this._logger); } return this._logger; } // -- Query Management ---- /** * Cancel previously submitted query requests. These queries will be * canceled if they are queued but have not yet been submitted. * @param requests An array of query result objects, such as those returned by the `query` method. */ cancel(requests) { this.manager.cancel(requests); } /** * Issue a query for which no result (return value) is needed. * @param query The query or an array of queries. Each query should be either a Query builder object or a SQL string. * @param options An options object. * @param options.priority The query priority, defaults to `Priority.Normal`. * @returns A query result promise. */ exec(query, options = {}) { const { priority = Priority.Normal } = options; query = Array.isArray(query) ? query.filter(x => x).join(';\n') : query; return this.manager.request({ type: 'exec', query }, priority); } /** * Issue a query to the backing database. The submitted query may be * consolidate with other queries and its results may be cached. * @param query The query as either a Query builder object or a SQL string. * @param options An options object. * @param options.type The query result format type. * @param options.cache If true, cache the query result client-side within the QueryManager. * @param options.persist If true, request the database server to persist a cached query server-side. * @param options.priority The query priority, defaults to `Priority.Normal`. * @returns A query result promise. */ query(query, options = {}) { const { type = 'arrow', cache = true, priority = Priority.Normal, ...otherOptions } = options; return this.manager.request({ type, query, cache, options: otherOptions }, priority); } /** * Issue a query to prefetch data for later use. The query result is cached * for efficient future access. * @param query The query as either a Query builder object or a SQL string. * @param options An options object. * @param options.type The query result format type. * @returns A query result promise. */ prefetch(query, options = {}) { return this.query(query, { ...options, cache: true, priority: Priority.Low }); } // -- Client Management ---- /** * Update client data by submitting the given query and returning the() * data (or error) to the client. * @param client A Mosaic client. * @param query The data query. * @param priority The query priority. * @returns A Promise that resolves upon completion of the update. */ updateClient(client, query, priority = Priority.Normal) { client.queryPending(); return client._pending = this.query(query, { priority }) .then(data => client.queryResult(data).update(), err => { this._logger?.error(err); client.queryError(err); }) .catch(err => this._logger?.error(err)); } /** * Issue a query request for a client. If the query is null or undefined, * the client is simply updated. Otherwise `updateClient` is called. As a * side effect, this method clears the current preaggregator state. * @param client The client to update. * @param query The query to issue. */ requestQuery(client, query) { this.preaggregator.clear(); return query ? this.updateClient(client, query) : Promise.resolve(client.update()); } /** * Connect a client to the coordinator. * Throws an error if the client is already connected. * @param client The Mosaic client to connect. */ connect(client) { const { clients } = this; if (clients?.has(client)) { throw new Error('Client already connected.'); } // add client to client set clients?.add(client); // register coordinator on client instance client.coordinator = this; // initialize client lifecycle client.initialize(); // connect filter selection connectSelection(this, client.filterBy, client); } /** * Disconnect a client from the coordinator. * This method has no effect if the client is already disconnected. * @param client The Mosaic client to disconnect. */ disconnect(client) { const { clients, filterGroups } = this; if (!clients?.has(client)) return; clients.delete(client); client.coordinator = null; const group = filterGroups?.get(client.filterBy); if (group) { group.clients.delete(client); } } } /** * Connect a selection-client pair to the coordinator to process updates. * @param mc The Mosaic coordinator. * @param selection A selection. * @param client A Mosaic client that is filtered by the given selection. */ function connectSelection(mc, selection, client) { if (!selection) return; let entry = mc.filterGroups?.get(selection); if (!entry) { const activate = (clause) => activateSelection(mc, selection, clause); const value = () => updateSelection(mc, selection); // @ts-expect-error todo: update selection dispatch types selection.addEventListener('activate', activate); selection.addEventListener('value', value); entry = { selection, clients: new Set, disconnect() { // @ts-expect-error todo: update selection dispatch types selection.removeEventListener('activate', activate); selection.removeEventListener('value', value); } }; mc.filterGroups?.set(selection, entry); } entry.clients.add(client); } /** * Activate a selection, providing a clause indicative of potential * next updates. Activation provides a preview of likely next events, * enabling potential precomputation to optimize updates. * @param mc The Mosaic coordinator. * @param selection A selection. * @param clause A selection clause for the activation. */ function activateSelection(mc, selection, clause) { const { preaggregator, filterGroups } = mc; const { clients } = filterGroups.get(selection); for (const client of clients) { if (client.enabled) { preaggregator.request(client, selection, clause); } } } /** * Process an updated selection value, querying filtered data for any * associated clients. * @param mc The Mosaic coordinator. * @param selection A selection. * @returns A Promise that resolves when the update completes. */ function updateSelection(mc, selection) { const { preaggregator, filterGroups } = mc; const { clients } = filterGroups.get(selection); const { active } = selection; return Promise.allSettled(Array.from(clients, (client) => { if (!client.enabled) return client.requestQuery(); const info = preaggregator.request(client, selection, active); const filter = info ? null : selection.predicate(client); // skip due to cross-filtering if (info?.skip || (!info && !filter)) return; // @ts-expect-error FIXME const query = info?.query(active.predicate) ?? client.query(filter); return mc.updateClient(client, query); })); } //# sourceMappingURL=Coordinator.js.map