UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

497 lines (496 loc) • 16.1 kB
import { SortedMap } from "../SortedMap.js"; import { CleanupQueue } from "../collection/cleanup-queue.js"; import { UNSUBSCRIBED_GC_FLOOR_MS } from "../collection/lifecycle.js"; import { makeComparator } from "../utils/comparison.js"; import { isVirtualPropName } from "../virtual-props.js"; import { getPersistedReadinessSource } from "../persisted-readiness.js"; import { getWhereExpression } from "./ir.js"; import { equalityKey, readPath, equalityConjunct } from "./equality-conjunct.js"; import { compileExpression, toBooleanPredicate } from "./compiler/evaluators.js"; import { buildCompareOptions } from "./compiler/order-by.js"; import { createLiveQueryCollection } from "./live-query-collection.js"; const partitionsBySource = /* @__PURE__ */ new WeakMap(); const EMPTY_GROUP = { key: ``, rows: new SortedMap(), listeners: /* @__PURE__ */ new Set(), revision: 0, layoutRevision: 0 }; function appendGroupKeyPart(groupKey, part) { return `${groupKey}${part.length}:${part}`; } class Partition { constructor(source, paths, compareRows, registry) { this.source = source; this.paths = paths; this.gg = compareRows; this.gf = registry; this.groups = /* @__PURE__ */ new Map(); this.gj = 0; this.statusListeners = /* @__PURE__ */ new Set(); this.listenerCount = 0; this.gcTime = 0; this.terminated = false; this.gc = () => { if (this.listenerCount > 0) return; this.f5?.(); this.f5 = void 0; this.groups.clear(); this.release(); }; } groupKeyOf(row) { if (row === void 0) return void 0; let groupKey = ``; for (const path of this.paths) { const key = equalityKey(readPath(row, path)); if (key === void 0) return void 0; groupKey = appendGroupKeyPart(groupKey, key); } return groupKey; } // Whether two versions of a row hold the same value in every field. gh(a, b) { return this.paths.every( (path) => Object.is(readPath(a, path), readPath(b, path)) ); } group(key) { let group = this.groups.get(key); if (!group) { const revision = ++this.gj; group = { key, rows: new SortedMap(this.gg), listeners: /* @__PURE__ */ new Set(), revision, layoutRevision: revision }; this.groups.set(key, group); } return group; } /** A group for reading, without creating one. */ peek(key) { return this.groups.get(key) ?? EMPTY_GROUP; } // A group with no rows and no watchers holds nothing anyone can read. gi(group) { if (group.rows.size === 0 && group.listeners.size === 0) { this.groups.delete(group.key); } } /** * Keep the shared source subscription open. Each view brings its query's * `gcTime`; the partition keeps the longest, so it never releases before * one of its views' own live-query Collection would have. */ retain(gcTime) { if (gcTime !== void 0) { const delay = gcTime > 0 && Number.isFinite(gcTime) ? gcTime : Infinity; this.gcTime = Math.max(this.gcTime, delay); } if (this.terminated) return; this.subscribe(); this.f9(UNSUBSCRIBED_GC_FLOOR_MS); } subscribe() { if (!this.terminated && !this.subscription) { this.gf.add(); this.subscription = this.source.subscribeChanges( (changes) => this.apply(changes), { includeInitialState: true } ); const stopStatus = this.source.on(`status:change`, (event) => { this.gb(event); }); const stopCleanupStart = this.source._onCleanupStart(() => { const previousStatus = this.source.status; this.terminate(); this.gb({ type: `status:change`, collection: this.source, previousStatus, status: `error` }); }); this.f5 = () => { stopStatus(); stopCleanupStart(); }; } } gb(event) { const delivered = this.terminated ? { ...event, status: `error` } : event; for (const listener of [...this.statusListeners]) listener(delivered); if (this.terminated) { this.f5?.(); this.f5 = void 0; } } addListener(group, listener) { this.subscribe(); group.listeners.add(listener); this.listenerCount++; } removeListener(group, listener) { if (!group.listeners.delete(listener)) return; this.listenerCount--; this.gi(group); this.f9(0); } terminate() { this.terminated = true; CleanupQueue.getInstance().cancel(this); this.release(); } release() { this.subscription?.unsubscribe(); this.subscription = void 0; this.gf.remove(); } // Releases on the Collections' shared GC queue, after the longest // `gcTime` of this partition's views. f9(minDelay) { if (this.listenerCount > 0 || !Number.isFinite(this.gcTime)) return; const delay = Math.max(this.gcTime, minDelay); CleanupQueue.getInstance().schedule(this, delay, this.gc); } apply(changes) { const touched = /* @__PURE__ */ new Map(); const record = (group, change) => { const list = touched.get(group); if (list) list.push(change); else touched.set(group, [change]); if (change.type !== `update`) group.layoutRevision = ++this.gj; }; for (const change of changes) { const next = change.type === `delete` ? void 0 : this.groupKeyOf(change.value); const previous = change.type === `insert` ? void 0 : change.type === `update` && change.previousValue !== void 0 && this.gh(change.value, change.previousValue) ? next : this.groupKeyOf( change.type === `delete` ? change.value : change.previousValue ); if (previous !== void 0 && previous !== next) { const group = this.group(previous); const old = group.rows.get(change.key); if (group.rows.delete(change.key)) { record(group, { type: `delete`, key: change.key, value: old }); } } if (next !== void 0) { const group = this.group(next); const existed = group.rows.has(change.key); group.rows.set(change.key, change.value); record( group, existed ? change.type === `update` ? change : { ...change, type: `update` } : { type: `insert`, key: change.key, value: change.value } ); } } for (const [group, groupChanges] of touched) { group.revision = ++this.gj; for (const listener of [...group.listeners]) listener(groupChanges); this.gi(group); } } } function readsOnlyRow(expression, alias) { if (expression.type === `val`) return true; if (expression.type === `ref`) { const [root, field] = expression.path; return root === alias && field !== void 0 && !isVirtualPropName(field); } return expression.args.every((arg) => readsOnlyRow(arg, alias)); } function collectConjuncts(expression, alias, out, residual) { if (expression.type === `func` && expression.name === `and`) { for (const arg of expression.args) { if (!collectConjuncts(arg, alias, out, residual)) return false; } return true; } const conjunct = equalityConjunctOf(expression, alias); if (conjunct) out.push(conjunct); else if (readsOnlyRow(expression, alias)) residual.push(expression); else return false; return true; } function equalityConjunctOf(expression, alias) { const conjunct = equalityConjunct( expression, (ref) => ref.path[0] === alias ? ref.path.slice(1) : void 0 ); return conjunct && { ...conjunct, pathKey: JSON.stringify(conjunct.path) }; } function rowPredicate(residual, alias) { const conjuncts = residual.map((expression) => compileExpression(expression)); const namespaced = {}; return (row) => { namespaced[alias] = row; return conjuncts.every( (conjunct) => toBooleanPredicate(conjunct(namespaced)) ); }; } function poolableShape(query) { if (query.from.type !== `collectionRef` || query.select || query.join || query.groupBy || query.having || query.limit !== void 0 || query.offset !== void 0 || query.distinct || query.singleResult || query.fnSelect || query.fnWhere?.length || query.fnHaving?.length || !query.where?.length) { return void 0; } const conjuncts = []; const residual = []; for (const where of query.where) { if (!collectConjuncts( getWhereExpression(where), query.from.alias, conjuncts, residual )) { return void 0; } } if (conjuncts.length === 0) return void 0; const orderBy = query.orderBy?.length ? query.orderBy : void 0; const orderKey = orderBy ? orderByKey(orderBy, query.from.alias, query.from.collection) : ``; if (orderKey === void 0) return void 0; if (conjuncts.length === 2) { if (conjuncts[1].pathKey < conjuncts[0].pathKey) conjuncts.reverse(); } else if (conjuncts.length > 2) { conjuncts.sort((a, b) => a.pathKey < b.pathKey ? -1 : 1); } const paths = []; let shapeKey = ``; let groupKey = ``; for (const { path, pathKey, literalKey } of conjuncts) { paths.push(path); shapeKey += pathKey; groupKey = appendGroupKeyPart(groupKey, literalKey); } if (orderKey) shapeKey += `|${orderKey}`; return { paths, shapeKey, groupKey, residual, orderBy }; } function orderByKey(orderBy, alias, source) { let key = ``; for (const clause of orderBy) { const { expression } = clause; if (expression.type !== `ref` || !readsOnlyRow(expression, alias) || expression.path.length < 2) { return void 0; } const options = buildCompareOptions(clause, source); if (options.stringSort === `custom`) return void 0; key += JSON.stringify([expression.path.slice(1), options]); } return key; } function rowComparator(orderBy, alias, source) { const terms = orderBy.map((clause) => ({ read: compileExpression(clause.expression), compare: makeComparator(buildCompareOptions(clause, source)) })); const namespaced = {}; return (a, b) => { for (const { read, compare } of terms) { namespaced[alias] = a; const left = read(namespaced); namespaced[alias] = b; const result = compare(left, read(namespaced)); if (result !== 0) return result; } return 0; }; } class PooledLiveQuery { constructor(source, query, partition, groupKey, gcTime, passes) { this.source = source; this.query = query; this.partition = partition; this.groupKey = groupKey; this.gcTime = gcTime; this.ge = passes; this.isLoadingSubset = false; this.config = void 0; this._subscribeLayoutChanges = void 0; this.collection = void 0; this.listenerCount = 0; this.f8 = void 0; this.proxy = void 0; partition.retain(gcTime); } get status() { return this.partition.terminated ? `error` : this.source.status; } // Read by key: the partition drops a group nobody watches once it empties. get group() { return this.partition.peek(this.groupKey); } get _stateRevision() { return this.group.revision; } get _layoutRevision() { return this.group.layoutRevision; } entries() { const rows = this.group.rows.entries(); const passes = this.ge; return passes ? [...rows].filter(([, row]) => passes(row)) : rows; } subscribeChanges(callback, options = {}) { this.partition.subscribe(); const listener = this.ge ? this.gd(callback) : callback; const group = this.partition.group(this.groupKey); this.partition.addListener(group, listener); this.listenerCount++; this.ga(); if (options.includeInitialState) { callback( Array.from(this.entries(), ([key, value]) => ({ type: `insert`, key, value })) ); } let subscribed = true; return { unsubscribe: () => { if (!subscribed) return; subscribed = false; this.partition.removeListener(group, listener); if (--this.listenerCount === 0) { this.f8?.unsubscribe(); this.f8 = void 0; } } }; } // While the view is observed, its built Collection stays subscribed, as // the Collection would be if it served the observer itself. ga() { if (this.collection && this.listenerCount > 0) { this.f8 ??= this.collection.subscribeChanges(() => { }); } } // Turns the group's changes into this query's, tracking which rows have // passed its residual conjuncts for this subscription. gd(callback) { const passes = this.ge; const visible = new Map(this.entries()); return (changes) => { const out = []; for (const change of changes) { const { key, value } = change; const previous = visible.get(key); const next = change.type !== `delete` && passes(value); if (next) visible.set(key, value); else visible.delete(key); if (previous && next) { out.push({ type: `update`, key, value, previousValue: previous }); } else if (previous) { out.push({ type: `delete`, key, value: previous }); } else if (next) { out.push({ type: `insert`, key, value }); } } if (out.length > 0) callback(out); }; } on(...args) { const [event, listener] = args; if (event !== `status:change`) return this.source.on(...args); const listeners = this.partition.statusListeners; listeners.add(listener); return () => listeners.delete(listener); } preload() { return this.source.preload(); } cleanup() { return this.collection?.cleanup() ?? Promise.resolve(); } /** This view as the Collection it stands in for. */ get publicCollection() { return this.proxy ??= new Proxy( this, forwardToCollection ); } materialize() { if (!this.collection) { this.collection = createLiveQueryCollection({ query: this.query, startSync: true, gcTime: this.gcTime }); this.ga(); } return this.collection; } } const forwardToCollection = { get(view, property) { const collection = view.materialize(); const value = Reflect.get(collection, property, collection); return typeof value === `function` ? value.bind(collection) : value; }, has(view, property) { return Reflect.has(view.materialize(), property); }, // So `instanceof` and query sources accept it as the Collection it is. getPrototypeOf(view) { return Reflect.getPrototypeOf(view.materialize()); } }; function getPooledQueryIdentity(query) { const ir = query._getQuery(); if (ir.from.type !== `collectionRef`) return void 0; const shape = poolableShape(ir); if (!shape || shape.residual.length > 0) return void 0; return JSON.stringify([ir.from.collection.id, shape.shapeKey, shape.groupKey]); } function createPooledLiveQuery(query, { gcTime = 5e3 } = {}) { const ir = query._getQuery(); const shape = poolableShape(ir); if (!shape || ir.from.type !== `collectionRef`) return void 0; const source = ir.from.collection; if (source.config.syncMode === `on-demand` || getPersistedReadinessSource(source.config)) { return void 0; } let partitions = partitionsBySource.get(source); if (!partitions) { partitions = /* @__PURE__ */ new Map(); partitionsBySource.set(source, partitions); } const { shapeKey } = shape; let partition = partitions.get(shapeKey); if (!partition) { const owner = partitions; const created = new Partition( source, shape.paths, shape.orderBy && rowComparator(shape.orderBy, ir.from.alias, source), { add: () => { if (!owner.has(shapeKey)) owner.set(shapeKey, created); }, remove: () => { if (owner.get(shapeKey) === created) owner.delete(shapeKey); } } ); partition = created; partitions.set(shapeKey, partition); } return new PooledLiveQuery( source, query, partition, shape.groupKey, gcTime, shape.residual.length > 0 ? rowPredicate(shape.residual, ir.from.alias) : void 0 ); } export { createPooledLiveQuery, getPooledQueryIdentity }; //# sourceMappingURL=pooled-live-query.js.map