@tanstack/db
Version:
A reactive client store for building super fast apps on sync
497 lines (496 loc) • 16.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const SortedMap = require("../SortedMap.cjs");
const cleanupQueue = require("../collection/cleanup-queue.cjs");
const lifecycle = require("../collection/lifecycle.cjs");
const comparison = require("../utils/comparison.cjs");
const virtualProps = require("../virtual-props.cjs");
const persistedReadiness = require("../persisted-readiness.cjs");
const ir = require("./ir.cjs");
const equalityConjunct = require("./equality-conjunct.cjs");
const evaluators = require("./compiler/evaluators.cjs");
const orderBy = require("./compiler/order-by.cjs");
const liveQueryCollection = require("./live-query-collection.cjs");
const partitionsBySource = /* @__PURE__ */ new WeakMap();
const EMPTY_GROUP = {
key: ``,
rows: new SortedMap.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 = equalityConjunct.equalityKey(equalityConjunct.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(equalityConjunct.readPath(a, path), equalityConjunct.readPath(b, path))
);
}
group(key) {
let group = this.groups.get(key);
if (!group) {
const revision = ++this.gj;
group = {
key,
rows: new SortedMap.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(lifecycle.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.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.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 && !virtualProps.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.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) => evaluators.compileExpression(expression));
const namespaced = {};
return (row) => {
namespaced[alias] = row;
return conjuncts.every(
(conjunct) => evaluators.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(
ir.getWhereExpression(where),
query.from.alias,
conjuncts,
residual
)) {
return void 0;
}
}
if (conjuncts.length === 0) return void 0;
const orderBy2 = query.orderBy?.length ? query.orderBy : void 0;
const orderKey = orderBy2 ? orderByKey(orderBy2, 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: orderBy2 };
}
function orderByKey(orderBy$1, alias, source) {
let key = ``;
for (const clause of orderBy$1) {
const { expression } = clause;
if (expression.type !== `ref` || !readsOnlyRow(expression, alias) || expression.path.length < 2) {
return void 0;
}
const options = orderBy.buildCompareOptions(clause, source);
if (options.stringSort === `custom`) return void 0;
key += JSON.stringify([expression.path.slice(1), options]);
}
return key;
}
function rowComparator(orderBy$1, alias, source) {
const terms = orderBy$1.map((clause) => ({
read: evaluators.compileExpression(clause.expression),
compare: comparison.makeComparator(orderBy.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 = liveQueryCollection.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 ir2 = query._getQuery();
if (ir2.from.type !== `collectionRef`) return void 0;
const shape = poolableShape(ir2);
if (!shape || shape.residual.length > 0) return void 0;
return JSON.stringify([ir2.from.collection.id, shape.shapeKey, shape.groupKey]);
}
function createPooledLiveQuery(query, { gcTime = 5e3 } = {}) {
const ir2 = query._getQuery();
const shape = poolableShape(ir2);
if (!shape || ir2.from.type !== `collectionRef`) return void 0;
const source = ir2.from.collection;
if (source.config.syncMode === `on-demand` || persistedReadiness.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, ir2.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, ir2.from.alias) : void 0
);
}
exports.createPooledLiveQuery = createPooledLiveQuery;
exports.getPooledQueryIdentity = getPooledQueryIdentity;
//# sourceMappingURL=pooled-live-query.cjs.map