UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

786 lines (785 loc) 23 kB
"use strict"; 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