@tanstack/db
Version:
A reactive client store for building super fast apps on sync
411 lines (410 loc) • 11.9 kB
JavaScript
import { isRefProxy, toExpression } from "./builder/ref-proxy.js";
import { getQueryIR } from "./builder/index.js";
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 JSON.stringify(canonicalizeQueryIR(query));
}
function getStableQueryBuilderHash(query) {
return getStableQueryIRHash(getQueryIR(query));
}
function getStableValueHash(value, path = `value`) {
return JSON.stringify(canonicalizeRuntimeValue(value, path, /* @__PURE__ */ new WeakSet()));
}
function canonicalizeQueryIR(query) {
return canonicalizeQuery(query, `query`, /* @__PURE__ */ new WeakSet());
}
function canonicalizeQuery(query, path, seen) {
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)
};
if (query.select) {
result.select = canonicalizeSelect(query.select, `${path}.select`, seen);
}
if (query.join) {
result.join = query.join.map(
(join, index) => canonicalizeJoin(join, `${path}.join[${index}]`, seen)
);
}
if (query.where) {
result.where = query.where.map(
(where, index) => canonicalizeWhere(where, `${path}.where[${index}]`, seen)
);
}
if (query.groupBy) {
result.groupBy = query.groupBy.map(
(expression, index) => canonicalizeExpression(expression, `${path}.groupBy[${index}]`, seen)
);
}
if (query.having) {
result.having = query.having.map(
(having, index) => canonicalizeWhere(having, `${path}.having[${index}]`, seen)
);
}
if (query.orderBy) {
result.orderBy = query.orderBy.map(
(orderBy, index) => canonicalizeOrderBy(orderBy, `${path}.orderBy[${index}]`, seen)
);
}
if (query.limit !== void 0) {
result.limit = canonicalizeRuntimeValue(query.limit, `${path}.limit`, seen);
}
if (query.offset !== void 0) {
result.offset = canonicalizeRuntimeValue(
query.offset,
`${path}.offset`,
seen
);
}
if (query.distinct) {
result.distinct = true;
}
if (query.singleResult) {
result.singleResult = true;
}
return result;
}
function canonicalizeJoin(join, path, seen) {
return {
type: join.type,
from: canonicalizeSource(join.from, `${path}.from`, seen),
left: canonicalizeExpression(join.left, `${path}.left`, seen),
right: canonicalizeExpression(join.right, `${path}.right`, seen)
};
}
function canonicalizeSource(source, path, seen) {
if (source.type === `collectionRef`) {
return {
type: `collectionRef`,
alias: source.alias,
collectionId: canonicalizeRuntimeValue(
source.collection.id,
`${path}.collection.id`,
seen
)
};
}
if (source.type === `unionFrom`) {
return {
type: `unionFrom`,
sources: source.sources.map(
(unionSource, index) => canonicalizeSource(unionSource, `${path}.sources[${index}]`, seen)
)
};
}
if (source.type === `unionAll`) {
return {
type: `unionAll`,
queries: source.queries.map(
(query, index) => canonicalizeQuery(query, `${path}.queries[${index}]`, seen)
)
};
}
return {
type: `queryRef`,
alias: source.alias,
query: canonicalizeQuery(source.query, `${path}.query`, seen)
};
}
function canonicalizeSelect(select, path, seen) {
return {
type: `select`,
fields: Object.keys(select).sort().map((key) => [
key,
canonicalizeSelectValue(select[key], `${path}.${key}`, seen)
])
};
}
function canonicalizeSelectValue(value, path, seen) {
if (isRefProxy(value)) {
return canonicalizeExpression(toExpression(value), path, seen);
}
if (isExpression(value)) {
return canonicalizeExpression(value, path, seen);
}
if (isPlainObject(value)) {
return canonicalizeSelect(value, path, seen);
}
return canonicalizeRuntimeValue(value, path, seen);
}
function canonicalizeWhere(where, path, seen) {
if (isWhereObject(where)) {
const result = {
type: `where`,
expression: canonicalizeExpression(
where.expression,
`${path}.expression`,
seen
)
};
if (where.residual === true) {
result.residual = true;
}
return result;
}
return canonicalizeExpression(where, path, seen);
}
function canonicalizeOrderBy(orderBy, path, seen) {
return {
expression: canonicalizeExpression(
orderBy.expression,
`${path}.expression`,
seen
),
compareOptions: canonicalizeRuntimeValue(
orderBy.compareOptions,
`${path}.compareOptions`,
seen
)
};
}
function canonicalizeExpression(expression, path, seen) {
if (expression.type === `ref`) {
return {
type: `ref`,
path: expression.path.map(
(segment, index) => canonicalizeRuntimeValue(segment, `${path}.path[${index}]`, seen)
)
};
}
if (expression.type === `val`) {
return {
type: `val`,
value: canonicalizeRuntimeValue(expression.value, `${path}.value`, seen)
};
}
if (expression.type === `func`) {
return {
type: `func`,
name: expression.name,
args: expression.args.map(
(arg, index) => canonicalizeExpression(arg, `${path}.args[${index}]`, seen)
)
};
}
if (expression.type === `agg`) {
return {
type: `agg`,
name: expression.name,
args: expression.args.map(
(arg, index) => canonicalizeExpression(arg, `${path}.args[${index}]`, seen)
)
};
}
if (expression.type === `conditionalSelect`) {
const result2 = {
type: `conditionalSelect`,
branches: expression.branches.map((branch, index) => ({
condition: canonicalizeExpression(
branch.condition,
`${path}.branches[${index}].condition`,
seen
),
value: canonicalizeSelectValue(
branch.value,
`${path}.branches[${index}].value`,
seen
)
}))
};
if (expression.defaultValue !== void 0) {
result2.defaultValue = canonicalizeSelectValue(
expression.defaultValue,
`${path}.defaultValue`,
seen
);
}
return result2;
}
const result = {
type: `includesSubquery`,
query: canonicalizeQuery(expression.query, `${path}.query`, seen),
correlationField: canonicalizeExpression(
expression.correlationField,
`${path}.correlationField`,
seen
),
childCorrelationField: canonicalizeExpression(
expression.childCorrelationField,
`${path}.childCorrelationField`,
seen
),
fieldName: expression.fieldName,
materialization: expression.materialization
};
if (expression.parentFilters) {
result.parentFilters = expression.parentFilters.map(
(where, index) => canonicalizeWhere(where, `${path}.parentFilters[${index}]`, seen)
);
}
if (expression.parentProjection) {
result.parentProjection = expression.parentProjection.map(
(projection, index) => canonicalizeExpression(
projection,
`${path}.parentProjection[${index}]`,
seen
)
);
}
if (expression.scalarField !== void 0) {
result.scalarField = expression.scalarField;
}
return result;
}
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 (isRefProxy(value)) {
return canonicalizeExpression(toExpression(value), path, seen);
}
if (Array.isArray(value)) {
return withCircularGuard(value, path, seen, () => [
`array`,
value.map(
(item, index) => canonicalizeRuntimeValue(item, `${path}[${index}]`, 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], index) => [
canonicalizeRuntimeValue(key, `${path}.key[${index}]`, seen),
canonicalizeRuntimeValue(entryValue, `${path}.value[${index}]`, seen)
]
);
entries.sort(compareStableIdentityValues);
return [`Map`, entries];
});
}
if (value instanceof Set) {
return withCircularGuard(value, path, seen, () => {
const entries = Array.from(
value,
(entry, index) => canonicalizeRuntimeValue(entry, `${path}[${index}]`, seen)
);
entries.sort(compareStableIdentityValues);
return [`Set`, entries];
});
}
if (isPlainObject(value)) {
return canonicalizeObject(value, path, seen);
}
throw new UnhashableQueryIRError(path, `non-plain object value`);
}
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;
}
export {
UnhashableQueryIRError,
canonicalizeQueryIR,
getStableQueryBuilderHash,
getStableQueryIRHash,
getStableValueHash
};
//# sourceMappingURL=ir-stable-identity.js.map