UNPKG

@uwdata/mosaic-core

Version:

Scalable and extensible linked data views.

322 lines 12.2 kB
import { literal, or } from '@uwdata/mosaic-sql'; import { Param } from './Param.js'; /** * Test if a value is a Selection instance. * @param x The value to test. * @returns True if the input is a Selection, false otherwise. */ export function isSelection(x) { return x instanceof Selection; } function create(options, include) { return new Selection(new SelectionResolver(options), include ? [include].flat() : undefined); } /** * Represents a dynamic set of query filter predicates. */ export class Selection extends Param { _resolved; _resolver; _relay; /** * Create a new Selection instance with an * intersect (conjunction) resolution strategy. * @param options The selection options. * @returns The new Selection instance. */ static intersect({ cross = false, empty = false, include = [] } = {}) { return create({ cross, empty }, include); } /** * Create a new Selection instance with a * union (disjunction) resolution strategy. * @param options The selection options. * @returns The new Selection instance. */ static union({ cross = false, empty = false, include = [] } = {}) { return create({ cross, empty, union: true }, include); } /** * Create a new Selection instance with a singular resolution strategy * that keeps only the most recent selection clause. * @param options The selection options. * @returns The new Selection instance. */ static single({ cross = false, empty = false, include = [] } = {}) { return create({ cross, empty, single: true }, include); } /** * Create a new Selection instance with a * cross-filtered intersect resolution strategy. * @param options The selection options. * @returns The new Selection instance. */ static crossfilter({ empty = false, include = [] } = {}) { return create({ cross: true, empty }, include); } /** * Create a new Selection instance. * @param resolver The selection resolution * strategy to apply. * @param include Upstream selections whose clauses * should be included as part of this selection. Any clauses published * to these upstream selections will be relayed to this selection. */ constructor(resolver = new SelectionResolver(), include = []) { super([]); this._resolved = this._value; this._resolver = resolver; this._relay = new Set(); if (Array.isArray(include)) { for (const sel of include) { sel._relay.add(this); } } } /** * Create a cloned copy of this Selection instance. * @returns A clone of this selection. */ clone() { const s = new Selection(this._resolver); s._value = s._resolved = this._value; return s; } /** * Create a clone of this Selection with clauses corresponding * to the provided source removed. * @param source The clause source to remove. * @returns A cloned and updated Selection. */ remove(source) { const s = this.clone(); s._value = s._resolved = s._resolver.resolve(this._resolved, { source }); s._value.active = { source }; return s; } /** * The selection clause resolver. */ get resolver() { return this._resolver; } /** * Indicate if this selection has a single resolution strategy. */ get single() { return this._resolver.single; } /** * The current array of selection clauses. */ get clauses() { return super.value; } /** * The current active (most recently updated) selection clause. */ get active() { return this.clauses.active; } /** * The value corresponding to the current active selection clause. * This method ensures compatibility where a normal Param is expected. */ // @ts-expect-error return type differs from Param parent class get value() { return this.active?.value; } /** * The value corresponding to a given source. Returns undefined if * this selection does not include a clause from this source. * @param source The clause source to look up the value for. */ valueFor(source) { return this.clauses.find(c => c.source === source)?.value; } /** * Emit an activate event with the given selection clause. * @param clause The clause representing the potential activation. */ activate(clause) { // @ts-expect-error selection operates differently than scalar param this.emit('activate', clause); this._relay.forEach(sel => sel.activate(clause)); } /** * Update the selection with a new selection clause. * @param clause The selection clause to add. * @returns This Selection instance. */ // @ts-expect-error selection and param use differing value types update(clause) { // we maintain an up-to-date list of all resolved clauses // this ensures consistent clause state across unemitted event values this._resolved = this._resolver.resolve(this._resolved, clause, true); this._resolved.active = clause; this._relay.forEach(sel => sel.update(clause)); return super.update(this._resolved); } /** * Reset the selection state by removing all provided clauses. If no clause * array is provided as an argument, all current clauses are removed. The * reset method (if defined) is invoked on all corresponding clause sources. * The reset is relayed to downstream selections that include this selection. * @param clauses The clauses to remove. If unspecified, all current clauses are removed. * @returns This selection instance. */ reset(clauses) { clauses ??= this._resolved; clauses.forEach(c => c.source?.reset?.()); this._resolved = this._resolved.filter(c => !clauses.includes(c)); this._relay.forEach(sel => sel.reset(clauses)); return super.update(this._resolved); } /** * Upon value-typed updates, sets the current clause list to the * input value and returns the active clause value. * @param type The event type. * @param value The input event value. * @returns For value-typed events, returns the active clause * values. Otherwise returns the input event value as-is. */ // @ts-expect-error selection and param use differing value types willEmit(type, value) { if (type === 'value') { this._value = value; return this.value; } return value; } /** * Upon value-typed updates, returns a dispatch queue filter function. * The return value depends on the selection resolution strategy. * @param type The event type. * @param value The new event value that will be enqueued. * @returns A dispatch queue filter function. For non-value events, * returns a function that always returns null (no filtering). */ // @ts-expect-error selection and param use differing value types emitQueueFilter(type, value) { return type === 'value' ? this._resolver.queueFilter(value) : null; } /** * Indicates if a selection clause should not be applied to a given client. * The return value depends on the selection resolution strategy. * @param client The client to test. If null or undefined, return false. * @param clause The selection clause. * @returns True if the client should be skipped, false otherwise. */ skip(client, clause) { return this._resolver.skip(client, clause); } /** * Return a selection query predicate for the given client. * @param client The client whose data may be filtered. If null or undefined, return a predicate with all clauses. * @param noSkip Disable skipping of active * cross-filtered sources. If set true, the source of the active * clause in a cross-filtered selection will not be skipped. * @returns The query predicate for filtering client data, * based on the current state of this selection. */ predicate(client = undefined, noSkip = false) { const { clauses } = this; const active = noSkip ? null : clauses.active; return this._resolver.predicate(clauses, active, client); } } /** * Implements selection clause resolution strategies. */ export class SelectionResolver { union; cross; single; empty; /** * Create a new selection resolved instance. * @param options The resolution strategy options. * @param options.union Boolean flag to indicate a union strategy. * If false, an intersection strategy is used. * @param options.cross Boolean flag to indicate cross-filtering. * @param options.single Boolean flag to indicate single clauses only. * @param options.empty Boolean flag indicating if a lack * of clauses should correspond to an empty selection with no records. This * setting determines the default selection state. */ constructor({ union, cross, single, empty } = {}) { this.union = !!union; this.cross = !!cross; this.single = !!single; this.empty = !!empty; } /** * Resolve a list of selection clauses according to the resolution strategy. * @param clauseList An array of selection clauses. * @param clause A new selection clause to add. * @returns An updated array of selection clauses. */ resolve(clauseList, clause, reset = false) { const { source, predicate } = clause; const filtered = clauseList.filter(c => source !== c.source); const clauses = this.single ? [] : filtered; if (this.single && reset) filtered.forEach(c => c.source?.reset?.()); if (predicate) clauses.push(clause); return clauses; } /** * Indicates if a selection clause should not be applied to a given client. * The return value depends on the resolution strategy. * @param client The client to test. If null or undefined, return false. * @param clause The selection clause. * @returns True if the client should be skipped, false otherwise. */ skip(client, clause) { if (client == null) { return false; } return Boolean(this.cross && clause?.clients?.has(client)); } /** * Return a selection query predicate for the given client. * @param clauseList An array of selection clauses. * @param active The current active selection clause. * @param client The client whose data may be filtered. If null or undefined, return all clauses. * @returns The query predicate for filtering client data, * based on the current state of this selection. */ predicate(clauseList, active, client) { const { empty, union } = this; if (empty && !clauseList.length) { return [literal(false)]; } // do nothing if cross-filtering and client is currently active if (this.skip(client, active)) return undefined; // remove client-specific predicates if cross-filtering const predicates = clauseList .filter(clause => !this.skip(client, clause)) .map(clause => clause.predicate); // return appropriate conjunction or disjunction // an array of predicates is implicitly conjunctive return union && predicates.length > 1 ? or(predicates) : predicates; } /** * Returns a filter function for queued selection updates. * @param value The new event value that will be enqueued. * @returns A dispatch queue filter * function, or null if all unemitted event values should be filtered. */ queueFilter(value) { if (this.cross) { const source = value.active?.source; return value => value.active?.source !== source; } return null; } } //# sourceMappingURL=Selection.js.map