UNPKG

@uwdata/mosaic-core

Version:

Scalable and extensible linked data views.

174 lines 6.05 kB
import { consolidator } from './QueryConsolidator.js'; import { lruCache, voidCache } from './util/cache.js'; import { PriorityQueue } from './util/priority-queue.js'; import { QueryResult, QueryState } from './util/query-result.js'; import { voidLogger } from './util/void-logger.js'; export const Priority = Object.freeze({ High: 0, Normal: 1, Low: 2 }); export class QueryManager { queue; db; clientCache; _logger; _logQueries; _consolidate; /** Requests pending with the query manager. */ pendingResults; maxConcurrentRequests; pendingExec; constructor(maxConcurrentRequests = 32) { this.queue = new PriorityQueue(3); this.db = null; this.clientCache = null; this._logger = voidLogger(); this._logQueries = false; this._consolidate = null; this.pendingResults = []; this.maxConcurrentRequests = maxConcurrentRequests; this.pendingExec = false; } next() { if (this.queue.isEmpty() || this.pendingResults.length > this.maxConcurrentRequests || this.pendingExec) { return; } const entry = this.queue.next(); if (!entry) return; const { request, result } = entry; this.pendingResults.push(result); if (request.type === 'exec') this.pendingExec = true; this.submit(request, result).finally(() => { // return from the queue all requests that are ready while (this.pendingResults.length && this.pendingResults[0].state !== QueryState.pending) { const result = this.pendingResults.shift(); if (result.state === QueryState.ready) { result.fulfill(); } else if (result.state === QueryState.done) { this._logger.warn('Found resolved query in pending results.'); } } if (request.type === 'exec') this.pendingExec = false; this.next(); }); } /** * Add an entry to the query queue with a priority. * @param entry The entry to add. * @param priority The query priority, defaults to `Priority.Normal`. */ enqueue(entry, priority = Priority.Normal) { this.queue.insert(entry, priority); this.next(); } /** * Submit the query to the connector. * @param request The request. * @param result The query result. */ async submit(request, result) { try { const { query, type, cache = false, options } = request; const sql = Array.isArray(query) ? query.filter(x => x).join(';\n') : query ? String(query) : null; // check query cache if (cache) { const cached = this.clientCache.get(sql); if (cached) { const data = await cached; this._logger.debug('Cache'); result.ready(data); return; } } // issue query, potentially cache result const t0 = performance.now(); if (this._logQueries) { this._logger.debug('Query', { type, sql, ...options }); } // @ts-expect-error type may be exec | json | arrow const promise = this.db.query({ type, sql: sql, ...options }); if (cache) this.clientCache.set(sql, promise); const data = await promise; if (cache) this.clientCache.set(sql, data); this._logger.debug(`Request: ${(performance.now() - t0).toFixed(1)}`); result.ready(type === 'exec' ? null : data); } catch (err) { result.reject(err); } } cache(value) { return value !== undefined ? (this.clientCache = value === true ? lruCache() : (value || voidCache())) : this.clientCache; } logger(value) { return value ? (this._logger = value) : this._logger; } logQueries(value) { return value !== undefined ? this._logQueries = !!value : this._logQueries; } connector(connector) { return connector ? (this.db = connector) : this.db; } /** * Indicate if query consolidation should be performed. * @param flag Whether to enable consolidation */ consolidate(flag) { if (flag && !this._consolidate) { this._consolidate = consolidator(this.enqueue.bind(this), this.clientCache); } else if (!flag && this._consolidate) { this._consolidate = null; } } /** * Request a query result. * @param request The request. * @param priority The query priority, defaults to `Priority.Normal`. * @returns A query result promise. */ request(request, priority = Priority.Normal) { const result = new QueryResult(); const entry = { request, result }; if (this._consolidate) { this._consolidate.add(entry, priority); } else { this.enqueue(entry, priority); } return result; } cancel(requests) { const set = new Set(requests); if (set.size) { this.queue.remove(({ result }) => { if (set.has(result)) { result.reject('Canceled'); return true; } return false; }); for (const result of this.pendingResults) { if (set.has(result)) { result.reject('Canceled'); } } } } clear() { this.queue.remove(({ result }) => { result.reject('Cleared'); return true; }); for (const result of this.pendingResults) { result.reject('Cleared'); } this.pendingResults = []; } } //# sourceMappingURL=QueryManager.js.map