@uwdata/mosaic-core
Version:
Scalable and extensible linked data views.
322 lines • 12.2 kB
JavaScript
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