@tanstack/db
Version:
A reactive client store for building super fast apps on sync
294 lines (293 loc) • 9.15 kB
JavaScript
import { compileSingleRowExpression, toBooleanPredicate } from "../query/compiler/evaluators.js";
import { optimizeExpressionWithIndexes, findIndexForField } from "../utils/index-optimization.js";
import { ensureIndexForField } from "../indexes/auto-index.js";
import { getPropRefPropertyPath } from "../query/ir.js";
import { isVirtualPropName } from "../virtual-props.js";
import { makeComparator } from "../utils/comparison.js";
import { buildCompareOptions } from "../query/compiler/order-by.js";
function currentStateAsChanges(collection, options = {}, scanStoredRows) {
const collectFilteredResults = (filterFn) => {
const result = [];
const prefilter = scanStoredRows && options.where && compileEqualityPrefilter(options.where);
if (filterFn && scanStoredRows && prefilter) {
for (const [key, value] of scanStoredRows(prefilter)) {
if (filterFn(value)) result.push({ type: `insert`, key, value });
}
return result;
}
for (const [key, value] of collection.entries()) {
if (filterFn?.(value) ?? true) {
result.push({
type: `insert`,
key,
value
});
}
}
return result;
};
if (options.limit !== void 0 && !options.orderBy) {
throw new Error(`limit cannot be used without orderBy`);
}
if (options.orderBy) {
const whereFilter = options.where ? createFilterFunctionFromExpression(options.where) : void 0;
const orderedKeys = getOrderedKeys(
collection,
options.orderBy,
options.limit,
whereFilter,
options.optimizedOnly
);
if (orderedKeys === void 0) {
return;
}
const result = [];
for (const key of orderedKeys) {
const value = collection.get(key);
if (value !== void 0) {
result.push({
type: `insert`,
key,
value
});
}
}
return result;
}
if (!options.where) {
return collectFilteredResults();
}
try {
const expression = options.where;
const optimizationResult = optimizeExpressionWithIndexes(
expression,
collection
);
if (optimizationResult.canOptimize) {
const filterFn = optimizationResult.isExact ? void 0 : createFilterFunctionFromExpression(expression);
const result = [];
for (const key of optimizationResult.matchingKeys) {
const value = collection.get(key);
if (value !== void 0 && (filterFn?.(value) ?? true)) {
result.push({
type: `insert`,
key,
value
});
}
}
return result;
} else {
if (options.optimizedOnly) {
return;
}
const filterFn = createFilterFunctionFromExpression(expression);
return collectFilteredResults(filterFn);
}
} catch (error) {
console.warn(
`${collection.id ? `[${collection.id}] ` : ``}Error processing where clause, falling back to full scan:`,
error
);
const filterFn = createFilterFunctionFromExpression(options.where);
if (options.optimizedOnly) {
return;
}
return collectFilteredResults(filterFn);
}
}
function createFilterFunctionFromExpression(expression) {
const evaluator = compileSingleRowExpression(expression);
return (item) => {
try {
const result = evaluator(item);
return toBooleanPredicate(result);
} catch {
return false;
}
};
}
const UNREADABLE_ROUTE_VALUE = /* @__PURE__ */ Symbol(
`unreadable route value`
);
function findEqualityRoute(expression, { storedRows = false } = {}) {
const conjuncts = [];
const collect = (node) => {
if (node.type === `func` && node.name === `and`) node.args.forEach(collect);
else conjuncts.push(node);
};
collect(expression);
let best;
for (const conjunct of conjuncts) {
if (conjunct.type !== `func` || conjunct.name !== `eq`) continue;
const [left, right] = conjunct.args;
const ref = left?.type === `ref` ? left : right?.type === `ref` ? right : void 0;
const literal = left?.type === `val` ? left : right?.type === `val` ? right : void 0;
if (!ref || !literal) continue;
const expected = literal.value;
if (typeof expected !== `string` && typeof expected !== `boolean`) continue;
const path = getPropRefPropertyPath(ref);
if (storedRows && (path.length === 0 || isVirtualPropName(path[0]))) {
continue;
}
if (best === void 0 || typeof best.expected === `boolean`) {
best = { path, pathKey: JSON.stringify(path), expected };
}
if (typeof expected === `string`) break;
}
return best;
}
function readRouteValue(row, path) {
try {
let value = row;
for (const segment of path) {
if (value === null || value === void 0) return void 0;
value = value[segment];
}
return value;
} catch {
return UNREADABLE_ROUTE_VALUE;
}
}
function compileEqualityPrefilter(expression) {
const route = findEqualityRoute(expression, { storedRows: true });
if (route === void 0) return void 0;
const { path, expected } = route;
if (path.length === 1) {
const field = path[0];
return (row) => {
try {
return row[field] === expected;
} catch {
return true;
}
};
}
return (row) => {
const value = readRouteValue(row, path);
return value === UNREADABLE_ROUTE_VALUE || value === expected;
};
}
function createFilteredCallback(originalCallback, options) {
const filterFn = createFilterFunctionFromExpression(options.whereExpression);
return (changes) => {
const filteredChanges = [];
for (const change of changes) {
if (change.type === `insert`) {
if (filterFn(change.value)) {
filteredChanges.push(change);
}
} else if (change.type === `update`) {
const newValueMatches = filterFn(change.value);
const oldValueMatches = change.previousValue ? filterFn(change.previousValue) : false;
if (newValueMatches && oldValueMatches) {
filteredChanges.push(change);
} else if (newValueMatches && !oldValueMatches) {
filteredChanges.push({
...change,
type: `insert`
});
} else if (!newValueMatches && oldValueMatches) {
filteredChanges.push({
...change,
type: `delete`,
value: change.previousValue
// Use the previous value for the delete
});
}
} else {
if (filterFn(change.value)) {
filteredChanges.push(change);
}
}
}
if (filteredChanges.length > 0 || changes.length === 0) {
originalCallback(filteredChanges);
return true;
}
return false;
};
}
function getOrderedKeys(collection, orderBy, limit, whereFilter, optimizedOnly) {
if (orderBy.length === 1) {
const clause = orderBy[0];
const orderByExpression = clause.expression;
if (orderByExpression.type === `ref`) {
const propRef = orderByExpression;
const fieldPath = propRef.path;
const compareOpts = buildCompareOptions(clause, collection);
ensureIndexForField(
fieldPath[0],
fieldPath,
collection,
compareOpts
);
const index = findIndexForField(collection, fieldPath, compareOpts);
if (index && index.supports(`gt`)) {
const filterFn = (key) => {
const value = collection.get(key);
if (value === void 0) {
return false;
}
return whereFilter?.(value) ?? true;
};
return index.takeFromStart(limit ?? index.keyCount, filterFn);
}
}
}
if (optimizedOnly) {
return;
}
const allItems = [];
for (const [key, value] of collection.entries()) {
if (whereFilter?.(value) ?? true) {
allItems.push({ key, value });
}
}
const clauses = orderBy.map((clause) => ({
expression: clause.expression,
compare: makeComparator(buildCompareOptions(clause, collection))
}));
const compare = (a, b) => {
for (const clause of clauses) {
const aValue = extractValueFromItem(a.value, clause.expression);
const bValue = extractValueFromItem(b.value, clause.expression);
const result = clause.compare(aValue, bValue);
if (result !== 0) {
return result;
}
}
return 0;
};
allItems.sort(compare);
const sortedKeys = allItems.map((item) => item.key);
if (limit !== void 0) {
return sortedKeys.slice(0, limit);
}
return sortedKeys;
}
function extractValueFromItem(item, expression) {
if (expression.type === `ref`) {
const propRef = expression;
let value = item;
for (const pathPart of propRef.path) {
value = value?.[pathPart];
}
return value;
} else if (expression.type === `val`) {
return expression.value;
} else {
const evaluator = compileSingleRowExpression(expression);
return evaluator(item);
}
}
export {
UNREADABLE_ROUTE_VALUE,
compileEqualityPrefilter,
createFilterFunctionFromExpression,
createFilteredCallback,
currentStateAsChanges,
findEqualityRoute,
readRouteValue
};
//# sourceMappingURL=change-events.js.map