@tanstack/db
Version:
A reactive client store for building super fast apps on sync
294 lines (293 loc) • 9.47 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const evaluators = require("../query/compiler/evaluators.cjs");
const indexOptimization = require("../utils/index-optimization.cjs");
const autoIndex = require("../indexes/auto-index.cjs");
const ir = require("../query/ir.cjs");
const virtualProps = require("../virtual-props.cjs");
const comparison = require("../utils/comparison.cjs");
const orderBy = require("../query/compiler/order-by.cjs");
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 = indexOptimization.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 = evaluators.compileSingleRowExpression(expression);
return (item) => {
try {
const result = evaluator(item);
return evaluators.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 = ir.getPropRefPropertyPath(ref);
if (storedRows && (path.length === 0 || virtualProps.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$1, limit, whereFilter, optimizedOnly) {
if (orderBy$1.length === 1) {
const clause = orderBy$1[0];
const orderByExpression = clause.expression;
if (orderByExpression.type === `ref`) {
const propRef = orderByExpression;
const fieldPath = propRef.path;
const compareOpts = orderBy.buildCompareOptions(clause, collection);
autoIndex.ensureIndexForField(
fieldPath[0],
fieldPath,
collection,
compareOpts
);
const index = indexOptimization.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$1.map((clause) => ({
expression: clause.expression,
compare: comparison.makeComparator(orderBy.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 = evaluators.compileSingleRowExpression(expression);
return evaluator(item);
}
}
exports.UNREADABLE_ROUTE_VALUE = UNREADABLE_ROUTE_VALUE;
exports.compileEqualityPrefilter = compileEqualityPrefilter;
exports.createFilterFunctionFromExpression = createFilterFunctionFromExpression;
exports.createFilteredCallback = createFilteredCallback;
exports.currentStateAsChanges = currentStateAsChanges;
exports.findEqualityRoute = findEqualityRoute;
exports.readRouteValue = readRouteValue;
//# sourceMappingURL=change-events.cjs.map