@tanstack/db
Version:
A reactive client store for building super fast apps on sync
786 lines (785 loc) • 23 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const comparison = require("../utils/comparison.cjs");
const refProxy = require("./builder/ref-proxy.cjs");
const index = require("./builder/index.cjs");
const runtimeReferenceIdentity = require("./runtime-reference-identity.cjs");
class UnhashableQueryIRError extends Error {
constructor(path, reason) {
super(`Query IR is not stably hashable at ${path}: ${reason}`);
this.path = path;
this.reason = reason;
this.name = `UnhashableQueryIRError`;
}
}
function getStableQueryIRHash(query) {
return getQueryIdentity(query);
}
function getStableQueryBuilderHash(query) {
return getStableQueryIRHash(index.getQueryIR(query));
}
function getStableValueHash(value, path = `value`) {
return JSON.stringify(canonicalizeRuntimeValue(value, path, /* @__PURE__ */ new WeakSet()));
}
function getQueryIdentity(query) {
return JSON.stringify(canonicalizeQueryIR(query));
}
function getStableExpressionHash(expression) {
return JSON.stringify(
canonicalizeExpression(
expression,
`expression`,
/* @__PURE__ */ new WeakSet(),
`exact-output`
)
);
}
function getLoadSubsetDemandKey(options) {
if (options.where === void 0 && !options.orderBy?.length && options.limit === void 0 && (options.offset === void 0 || options.offset === 0) && options.cursor === void 0) {
return void 0;
}
const seen = /* @__PURE__ */ new WeakSet();
const result = {
type: `loadSubsetDemand`,
query: canonicalizeLoadSubsetQuery(options, `loadSubset`, seen)
};
if (options.limit !== void 0) {
result.limit = canonicalizeRuntimeValue(
options.limit,
`loadSubset.limit`,
seen
);
}
if (options.offset !== void 0 && options.offset !== 0) {
result.offset = canonicalizeRuntimeValue(
options.offset,
`loadSubset.offset`,
seen
);
}
if (options.cursor !== void 0) {
const cursor = {
whereFrom: canonicalizeExpression(
options.cursor.whereFrom,
`loadSubset.cursor.whereFrom`,
seen,
`exact-output`
),
whereCurrent: canonicalizeExpression(
options.cursor.whereCurrent,
`loadSubset.cursor.whereCurrent`,
seen,
`exact-output`
)
};
if (options.cursor.lastKey !== void 0) {
cursor.lastKey = canonicalizeRuntimeValue(
options.cursor.lastKey,
`loadSubset.cursor.lastKey`,
seen
);
}
result.cursor = cursor;
}
return JSON.stringify(result);
}
function canonicalizeQueryIR(query) {
return canonicalizeQuery(query, `query`, /* @__PURE__ */ new WeakSet());
}
function createAliasScope(query, parent) {
const bindings = /* @__PURE__ */ new Map();
const bindSource = (source) => {
if (source.type === `unionFrom`) {
source.sources.forEach(bindSource);
return;
}
if (source.type === `unionAll`) return;
if (!bindings.has(source.alias)) {
bindings.set(source.alias, bindings.size);
}
};
bindSource(query.from);
query.join?.forEach(({ from }) => bindSource(from));
return {
bindings,
hasUnqualifiedOutput: query.from.type === `unionAll`,
parent
};
}
function resolveAliasBinding(scope, alias) {
let current = scope;
let parentDistance = 0;
while (current) {
const binding = current.bindings.get(alias);
if (binding !== void 0) return [parentDistance, binding];
if (current.hasUnqualifiedOutput) return void 0;
current = current.parent;
parentDistance++;
}
return void 0;
}
function canonicalizeQuery(query, path, seen, parentScope) {
return canonicalizeQueryInScope(
query,
path,
seen,
createAliasScope(query, parentScope)
);
}
function canonicalizeQueryInScope(query, path, seen, scope) {
if (query.fnSelect) {
throw new UnhashableQueryIRError(`${path}.fnSelect`, `function select`);
}
if (query.fnWhere?.length) {
throw new UnhashableQueryIRError(`${path}.fnWhere`, `function where`);
}
if (query.fnHaving?.length) {
throw new UnhashableQueryIRError(`${path}.fnHaving`, `function having`);
}
const result = {
type: `query`,
from: canonicalizeSource(query.from, `${path}.from`, seen, scope)
};
if (query.select) {
result.select = canonicalizeSelect(
query.select,
`${path}.select`,
seen,
scope
);
}
if (!query.select && (query.from.type === `unionFrom` || query.join !== void 0 || query.groupBy !== void 0)) {
result.implicitOutput = {
type: `namespaced`,
aliases: Array.from(scope.bindings.keys())
};
}
if (query.join) {
result.join = query.join.map(
(join, index2) => canonicalizeJoin(join, `${path}.join[${index2}]`, seen, scope)
);
}
if (query.where) {
result.where = canonicalizeImplicitConjunction(
query.where,
`${path}.where`,
seen,
scope
);
}
if (query.groupBy) {
result.groupBy = query.groupBy.map(
(expression, index2) => canonicalizeExpression(
expression,
`${path}.groupBy[${index2}]`,
seen,
`exact-output`,
scope
)
);
}
if (query.having) {
result.having = canonicalizeImplicitConjunction(
query.having,
`${path}.having`,
seen,
scope
);
}
if (query.orderBy) {
result.orderBy = query.orderBy.map(
(orderBy, index2) => canonicalizeOrderBy(
orderBy,
`${path}.orderBy[${index2}]`,
seen,
`ordering-operand`,
scope
)
);
}
if (query.limit !== void 0) {
result.limit = canonicalizeRuntimeValue(query.limit, `${path}.limit`, seen);
}
if (query.offset !== void 0 && query.offset !== 0) {
result.offset = canonicalizeRuntimeValue(
query.offset,
`${path}.offset`,
seen
);
}
if (query.distinct) {
result.distinct = true;
}
if (query.singleResult) {
result.singleResult = true;
}
return result;
}
function canonicalizeImplicitConjunction(clauses, path, seen, scope) {
const canonical = clauses.map(
(clause, index2) => canonicalizeWhere(clause, `${path}[${index2}]`, seen, scope)
);
canonical.sort(compareStableIdentityValues);
return canonical.filter(
(clause, index2) => index2 === 0 || compareStableIdentityValues(clause, canonical[index2 - 1]) !== 0
);
}
function canonicalizeLoadSubsetQuery(options, path, seen) {
const result = {
type: `loadSubsetQuery`
};
if (options.where !== void 0) {
result.where = canonicalizeExpression(
options.where,
`${path}.where`,
seen,
`exact-output`
);
}
if (options.orderBy?.length) {
result.orderBy = options.orderBy.map(
(orderBy, index2) => canonicalizeOrderBy(
orderBy,
`${path}.orderBy[${index2}]`,
seen,
`ordering-operand`
)
);
}
return result;
}
function canonicalizeJoin(join, path, seen, scope) {
return {
type: join.type,
from: canonicalizeSource(join.from, `${path}.from`, seen, scope),
left: canonicalizeExpression(
join.left,
`${path}.left`,
seen,
`equality-operand`,
scope
),
right: canonicalizeExpression(
join.right,
`${path}.right`,
seen,
`equality-operand`,
scope
)
};
}
function canonicalizeSource(source, path, seen, scope) {
if (source.type === `collectionRef`) {
return {
type: `collectionRef`,
collectionId: canonicalizeRuntimeValue(
source.collection.id,
`${path}.collection.id`,
seen
)
};
}
if (source.type === `unionFrom`) {
return {
type: `unionFrom`,
sources: source.sources.map(
(unionSource, index2) => canonicalizeSource(
unionSource,
`${path}.sources[${index2}]`,
seen,
scope
)
)
};
}
if (source.type === `unionAll`) {
return {
type: `unionAll`,
queries: source.queries.map(
(query, index2) => (
// Branches are peers that may capture the union query's outer scope;
// they are not children of the union result row itself.
canonicalizeQuery(
query,
`${path}.queries[${index2}]`,
seen,
scope.parent
)
)
)
};
}
return {
type: `queryRef`,
query: canonicalizeQuery(source.query, `${path}.query`, seen, scope)
};
}
function canonicalizeSelect(select, path, seen, scope) {
return {
type: `select`,
fields: Object.keys(select).sort().map((key) => [
key,
canonicalizeSelectValue(select[key], `${path}.${key}`, seen, scope)
])
};
}
function canonicalizeSelectValue(value, path, seen, scope) {
if (refProxy.isRefProxy(value)) {
return canonicalizeExpression(
refProxy.toExpression(value),
path,
seen,
`exact-output`,
scope
);
}
if (isExpression(value)) {
return canonicalizeExpression(value, path, seen, `exact-output`, scope);
}
if (isPlainObject(value)) {
return canonicalizeSelect(value, path, seen, scope);
}
return canonicalizeExactOutputRuntimeValue(value, path, seen);
}
function canonicalizeWhere(where, path, seen, scope) {
if (isWhereObject(where)) {
const result = {
type: `where`,
expression: canonicalizeExpression(
where.expression,
`${path}.expression`,
seen,
`exact-output`,
scope
)
};
if (where.residual === true) {
result.residual = true;
}
return result;
}
return canonicalizeExpression(where, path, seen, `exact-output`, scope);
}
function canonicalizeOrderBy(orderBy, path, seen, valueContext = `exact-output`, scope) {
return {
expression: canonicalizeExpression(
orderBy.expression,
`${path}.expression`,
seen,
valueContext,
scope
),
compareOptions: canonicalizeRuntimeValue(
orderBy.compareOptions,
`${path}.compareOptions`,
seen
)
};
}
function canonicalizeExpression(expression, path, seen, valueContext = `exact-output`, scope) {
if (expression.type === `ref`) {
const binding = resolveAliasBinding(scope, expression.path[0] ?? ``);
return {
type: `ref`,
path: binding === void 0 ? expression.path.map(
(segment, index2) => canonicalizeRuntimeValue(segment, `${path}.path[${index2}]`, seen)
) : [
[`binding`, ...binding],
...expression.path.slice(1).map(
(segment, index2) => canonicalizeRuntimeValue(
segment,
`${path}.path[${index2 + 1}]`,
seen
)
)
]
};
}
if (expression.type === `val`) {
return {
type: `val`,
value: valueContext === `equality-operand` ? canonicalizeEqualityRuntimeValue(
expression.value,
`${path}.value`,
seen,
scope
) : valueContext === `ordering-operand` ? canonicalizeOrderingRuntimeValue(
expression.value,
`${path}.value`,
seen
) : canonicalizeExactOutputRuntimeValue(
expression.value,
`${path}.value`,
seen
)
};
}
if (expression.type === `func`) {
if (expression.name === `in` && expression.args.length === 2 && expression.args[1]?.type === `val` && Array.isArray(expression.args[1].value)) {
const candidates = expression.args[1].value.map(
(value, index2) => canonicalizeEqualityRuntimeValue(
value,
`${path}.args[1].value[${index2}]`,
seen,
scope
)
);
return canonicalizeFunction(expression.name, [
canonicalizeExpression(
expression.args[0],
`${path}.args[0]`,
seen,
`equality-operand`,
scope
),
{
type: `val`,
// IN tests membership. Candidate order and duplicates do not change
// its result, but each candidate keeps its own equality semantics.
value: [`set`, sortUniqueStableIdentityValues(candidates)]
}
]);
}
const operandContext = expression.name === `eq` ? `equality-operand` : expression.name === `gt` || expression.name === `gte` || expression.name === `lt` || expression.name === `lte` ? `ordering-operand` : `exact-output`;
const args = expression.args.map(
(arg, index2) => canonicalizeExpression(
arg,
`${path}.args[${index2}]`,
seen,
operandContext,
scope
)
);
return canonicalizeFunction(expression.name, args);
}
if (expression.type === `agg`) {
return {
type: `agg`,
name: expression.name,
args: expression.args.map(
(arg, index2) => canonicalizeExpression(
arg,
`${path}.args[${index2}]`,
seen,
`exact-output`,
scope
)
)
};
}
if (expression.type === `conditionalSelect`) {
const result2 = {
type: `conditionalSelect`,
branches: expression.branches.map((branch, index2) => ({
condition: canonicalizeExpression(
branch.condition,
`${path}.branches[${index2}].condition`,
seen,
`exact-output`,
scope
),
value: canonicalizeSelectValue(
branch.value,
`${path}.branches[${index2}].value`,
seen,
scope
)
}))
};
if (expression.defaultValue !== void 0) {
result2.defaultValue = canonicalizeSelectValue(
expression.defaultValue,
`${path}.defaultValue`,
seen,
scope
);
}
return result2;
}
const childScope = createAliasScope(expression.query, scope);
const result = {
type: `includesSubquery`,
query: canonicalizeQueryInScope(
expression.query,
`${path}.query`,
seen,
childScope
),
correlationField: canonicalizeExpression(
expression.correlationField,
`${path}.correlationField`,
seen,
`equality-operand`,
scope
),
childCorrelationField: canonicalizeExpression(
expression.childCorrelationField,
`${path}.childCorrelationField`,
seen,
`equality-operand`,
childScope
),
fieldName: expression.fieldName,
materialization: expression.materialization
};
if (expression.parentFilters) {
result.parentFilters = expression.parentFilters.map(
(where, index2) => canonicalizeWhere(where, `${path}.parentFilters[${index2}]`, seen, scope)
);
}
if (expression.parentProjection) {
result.parentProjection = expression.parentProjection.map(
(projection, index2) => canonicalizeExpression(
projection,
`${path}.parentProjection[${index2}]`,
seen,
`exact-output`,
scope
)
);
}
if (expression.scalarField !== void 0) {
result.scalarField = expression.scalarField;
}
return result;
}
function canonicalizeFunction(name, args) {
if ((name === `and` || name === `or`) && args.length > 0) {
const flattened = args.flatMap(
(arg) => isCanonicalFunction(arg, name) ? arg.args : [arg]
);
const unique = sortUniqueStableIdentityValues(flattened);
return { type: `func`, name, args: unique };
}
if (name === `eq` && args.length === 2) {
args.sort(compareStableIdentityValues);
return { type: `func`, name, args };
}
if ((name === `gt` || name === `gte` || name === `lt` || name === `lte`) && args.length === 2 && compareStableIdentityValues(args[0], args[1]) > 0) {
return {
type: `func`,
name: invertComparison(name),
args: [args[1], args[0]]
};
}
return { type: `func`, name, args };
}
function sortUniqueStableIdentityValues(values) {
const keyedValues = values.map((value) => ({
key: JSON.stringify(value),
value
}));
keyedValues.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
return keyedValues.filter(
(entry, index2) => index2 === 0 || entry.key !== keyedValues[index2 - 1].key
).map((entry) => entry.value);
}
function isCanonicalFunction(value, name) {
return value !== null && typeof value === `object` && !Array.isArray(value) && value.type === `func` && value.name === name && Array.isArray(value.args);
}
function invertComparison(name) {
switch (name) {
case `gt`:
return `lt`;
case `gte`:
return `lte`;
case `lt`:
return `gt`;
case `lte`:
return `gte`;
}
}
function canonicalizeRuntimeValue(value, path, seen) {
if (value === null) return [`null`];
if (typeof value === `string`) {
return [`string`, value];
}
if (typeof value === `boolean`) {
return [`boolean`, value];
}
if (typeof value === `number`) {
if (Number.isNaN(value)) {
return [`number`, `NaN`];
}
if (value === Infinity) {
return [`number`, `Infinity`];
}
if (value === -Infinity) {
return [`number`, `-Infinity`];
}
if (Object.is(value, -0)) {
return [`number`, `-0`];
}
return [`number`, value];
}
if (typeof value === `undefined`) {
return [`undefined`];
}
if (typeof value === `bigint`) {
return [`bigint`, value.toString()];
}
if (typeof value === `function`) {
throw new UnhashableQueryIRError(path, `function value`);
}
if (typeof value === `symbol`) {
throw new UnhashableQueryIRError(path, `symbol value`);
}
if (refProxy.isRefProxy(value)) {
return canonicalizeExpression(refProxy.toExpression(value), path, seen);
}
if (Array.isArray(value)) {
return withCircularGuard(value, path, seen, () => [
`array`,
value.map(
(item, index2) => canonicalizeRuntimeValue(item, `${path}[${index2}]`, seen)
)
]);
}
if (value instanceof Date) {
const timestamp = value.getTime();
if (Number.isNaN(timestamp)) {
throw new UnhashableQueryIRError(path, `invalid Date`);
}
return [`Date`, value.toISOString()];
}
if (value instanceof ArrayBuffer) {
return [`binary`, `ArrayBuffer`, Array.from(new Uint8Array(value))];
}
if (ArrayBuffer.isView(value)) {
return [
`binary`,
value.constructor.name,
Array.from(
new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
)
];
}
if (value instanceof Map) {
return withCircularGuard(value, path, seen, () => {
const entries = Array.from(
value.entries(),
([key, entryValue], index2) => [
canonicalizeRuntimeValue(key, `${path}.key[${index2}]`, seen),
canonicalizeRuntimeValue(entryValue, `${path}.value[${index2}]`, seen)
]
);
entries.sort(compareStableIdentityValues);
return [`Map`, entries];
});
}
if (value instanceof Set) {
return withCircularGuard(value, path, seen, () => {
const entries = Array.from(
value,
(entry, index2) => canonicalizeRuntimeValue(entry, `${path}[${index2}]`, seen)
);
entries.sort(compareStableIdentityValues);
return [`Set`, entries];
});
}
if (isPlainObject(value)) {
return canonicalizeObject(value, path, seen);
}
throw new UnhashableQueryIRError(path, `non-plain object value`);
}
function canonicalizeExactOutputRuntimeValue(value, path, seen) {
if (typeof value === `object` && value !== null) {
return runtimeReferenceIdentity.getRuntimeReferenceIdentity(value);
}
return canonicalizeRuntimeValue(value, path, seen);
}
function canonicalizeEqualityRuntimeValue(value, path, seen, scope) {
if (refProxy.isRefProxy(value)) {
return canonicalizeExpression(
refProxy.toExpression(value),
path,
seen,
`equality-operand`,
scope
);
}
if (typeof value === `number` && Object.is(value, -0)) {
return canonicalizeRuntimeValue(0, path, seen);
}
const isUint8Array = typeof Buffer !== `undefined` && value instanceof Buffer || value instanceof Uint8Array;
if (isUint8Array) {
return [`binary`, `Uint8Array`, Array.from(value)];
}
const normalized = comparison.normalizeValue(value);
if (normalized !== value) {
return canonicalizeRuntimeValue(normalized, path, seen);
}
if (typeof value === `object` && value !== null) {
return runtimeReferenceIdentity.getRuntimeReferenceIdentity(value);
}
return canonicalizeRuntimeValue(value, path, seen);
}
function canonicalizeOrderingRuntimeValue(value, path, seen) {
if (typeof value === `number` && Object.is(value, -0)) {
return canonicalizeRuntimeValue(0, path, seen);
}
if (value instanceof Date && Number.isNaN(value.getTime())) {
return canonicalizeRuntimeValue(Number.NaN, path, seen);
}
const normalized = comparison.normalizeValue(value);
if (normalized !== value && !(value instanceof Uint8Array)) {
return canonicalizeRuntimeValue(normalized, path, seen);
}
try {
return canonicalizeRuntimeValue(value, path, seen);
} catch (error) {
if (error instanceof UnhashableQueryIRError && typeof value === `object` && value !== null) {
return runtimeReferenceIdentity.getRuntimeReferenceIdentity(value);
}
throw error;
}
}
function compareStableIdentityValues(left, right) {
const serializedLeft = JSON.stringify(left);
const serializedRight = JSON.stringify(right);
return serializedLeft < serializedRight ? -1 : serializedLeft > serializedRight ? 1 : 0;
}
function canonicalizeObject(value, path, seen) {
return withCircularGuard(value, path, seen, () => [
`object`,
Object.keys(value).sort().map((key) => [
key,
canonicalizeRuntimeValue(value[key], `${path}.${key}`, seen)
])
]);
}
function withCircularGuard(value, path, seen, callback) {
if (seen.has(value)) {
throw new UnhashableQueryIRError(path, `circular value`);
}
seen.add(value);
try {
return callback();
} finally {
seen.delete(value);
}
}
function isWhereObject(where) {
return `expression` in where;
}
function isExpression(value) {
if (value === null || typeof value !== `object`) {
return false;
}
const expressionType = value.type;
return expressionType === `agg` || expressionType === `conditionalSelect` || expressionType === `func` || expressionType === `ref` || expressionType === `val` || expressionType === `includesSubquery`;
}
function isPlainObject(value) {
if (value === null || typeof value !== `object`) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
exports.UnhashableQueryIRError = UnhashableQueryIRError;
exports.canonicalizeQueryIR = canonicalizeQueryIR;
exports.getLoadSubsetDemandKey = getLoadSubsetDemandKey;
exports.getQueryIdentity = getQueryIdentity;
exports.getStableExpressionHash = getStableExpressionHash;
exports.getStableQueryBuilderHash = getStableQueryBuilderHash;
exports.getStableQueryIRHash = getStableQueryIRHash;
exports.getStableValueHash = getStableValueHash;
//# sourceMappingURL=ir-stable-identity.cjs.map