@tanstack/db 0.5.33 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +3 -2
- package/dist/cjs/collection/changes.cjs +13 -4
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/changes.d.cts +10 -1
- package/dist/cjs/collection/cleanup-queue.cjs +89 -0
- package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
- package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
- package/dist/cjs/collection/events.cjs +14 -0
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +39 -1
- package/dist/cjs/collection/index.cjs +66 -28
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +49 -36
- package/dist/cjs/collection/indexes.cjs +211 -62
- package/dist/cjs/collection/indexes.cjs.map +1 -1
- package/dist/cjs/collection/indexes.d.cts +27 -17
- package/dist/cjs/collection/lifecycle.cjs +5 -22
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +0 -1
- package/dist/cjs/collection/mutations.cjs +18 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/collection/mutations.d.cts +1 -0
- package/dist/cjs/collection/state.cjs +381 -53
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +65 -1
- package/dist/cjs/collection/subscription.cjs +6 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +4 -0
- package/dist/cjs/collection/sync.cjs +108 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +2 -0
- package/dist/cjs/collection/transaction-metadata.cjs +5 -0
- package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
- package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +22 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +11 -3
- package/dist/cjs/indexes/auto-index.cjs +13 -6
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.cjs +0 -3
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -6
- package/dist/cjs/indexes/basic-index.cjs +361 -0
- package/dist/cjs/indexes/basic-index.cjs.map +1 -0
- package/dist/cjs/indexes/basic-index.d.cts +102 -0
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/indexes/index-options.d.cts +8 -9
- package/dist/cjs/indexes/index-registry.cjs +89 -0
- package/dist/cjs/indexes/index-registry.cjs.map +1 -0
- package/dist/cjs/indexes/index-registry.d.cts +61 -0
- package/dist/cjs/local-only.cjs +5 -0
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +27 -11
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +25 -3
- package/dist/cjs/query/builder/index.cjs +200 -39
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +4 -3
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
- package/dist/cjs/query/builder/types.d.cts +84 -19
- package/dist/cjs/query/compiler/evaluators.cjs +51 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +100 -28
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.d.cts +4 -2
- package/dist/cjs/query/compiler/index.cjs +283 -11
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +30 -2
- package/dist/cjs/query/compiler/order-by.cjs +29 -10
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/compiler/select.cjs +8 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/index.d.cts +2 -1
- package/dist/cjs/query/ir.cjs +18 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +21 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +501 -5
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
- package/dist/cjs/query/live/types.d.cts +3 -3
- package/dist/cjs/query/live/utils.cjs +43 -3
- package/dist/cjs/query/live/utils.cjs.map +1 -1
- package/dist/cjs/query/live/utils.d.cts +1 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +9 -6
- package/dist/cjs/query/query-once.cjs.map +1 -1
- package/dist/cjs/query/query-once.d.cts +7 -5
- package/dist/cjs/query/subset-dedupe.cjs +9 -3
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
- package/dist/cjs/types.d.cts +42 -8
- package/dist/cjs/utils/array-utils.cjs +27 -0
- package/dist/cjs/utils/array-utils.cjs.map +1 -0
- package/dist/cjs/utils/array-utils.d.cts +16 -0
- package/dist/cjs/utils/comparison.cjs +11 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.cjs +4 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils.cjs +7 -9
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +6 -1
- package/dist/cjs/virtual-props.cjs +33 -0
- package/dist/cjs/virtual-props.cjs.map +1 -0
- package/dist/cjs/virtual-props.d.cts +196 -0
- package/dist/esm/collection/change-events.d.ts +3 -2
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/changes.d.ts +10 -1
- package/dist/esm/collection/changes.js +13 -4
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/cleanup-queue.d.ts +30 -0
- package/dist/esm/collection/cleanup-queue.js +89 -0
- package/dist/esm/collection/cleanup-queue.js.map +1 -0
- package/dist/esm/collection/events.d.ts +39 -1
- package/dist/esm/collection/events.js +14 -0
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +49 -36
- package/dist/esm/collection/index.js +67 -29
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/indexes.d.ts +27 -17
- package/dist/esm/collection/indexes.js +211 -62
- package/dist/esm/collection/indexes.js.map +1 -1
- package/dist/esm/collection/lifecycle.d.ts +0 -1
- package/dist/esm/collection/lifecycle.js +5 -22
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/mutations.d.ts +1 -0
- package/dist/esm/collection/mutations.js +18 -0
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/collection/state.d.ts +65 -1
- package/dist/esm/collection/state.js +381 -53
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +4 -0
- package/dist/esm/collection/subscription.js +6 -0
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +2 -0
- package/dist/esm/collection/sync.js +108 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/collection/transaction-metadata.d.ts +1 -0
- package/dist/esm/collection/transaction-metadata.js +5 -0
- package/dist/esm/collection/transaction-metadata.js.map +1 -0
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +11 -3
- package/dist/esm/index.js +25 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +13 -6
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -6
- package/dist/esm/indexes/base-index.js +1 -4
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/basic-index.d.ts +102 -0
- package/dist/esm/indexes/basic-index.js +361 -0
- package/dist/esm/indexes/basic-index.js.map +1 -0
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +8 -9
- package/dist/esm/indexes/index-registry.d.ts +61 -0
- package/dist/esm/indexes/index-registry.js +89 -0
- package/dist/esm/indexes/index-registry.js.map +1 -0
- package/dist/esm/local-only.js +5 -0
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +25 -3
- package/dist/esm/query/builder/functions.js +27 -11
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +4 -3
- package/dist/esm/query/builder/index.js +201 -40
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +84 -19
- package/dist/esm/query/compiler/evaluators.js +51 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.d.ts +4 -2
- package/dist/esm/query/compiler/group-by.js +101 -29
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +30 -2
- package/dist/esm/query/compiler/index.js +285 -13
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js +30 -11
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js +8 -0
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/index.d.ts +2 -1
- package/dist/esm/query/ir.d.ts +21 -1
- package/dist/esm/query/ir.js +18 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
- package/dist/esm/query/live/collection-config-builder.js +503 -7
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/types.d.ts +3 -3
- package/dist/esm/query/live/utils.d.ts +1 -0
- package/dist/esm/query/live/utils.js +43 -3
- package/dist/esm/query/live/utils.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +9 -6
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/query-once.d.ts +7 -5
- package/dist/esm/query/query-once.js.map +1 -1
- package/dist/esm/query/subset-dedupe.js +9 -3
- package/dist/esm/query/subset-dedupe.js.map +1 -1
- package/dist/esm/types.d.ts +42 -8
- package/dist/esm/utils/array-utils.d.ts +16 -0
- package/dist/esm/utils/array-utils.js +27 -0
- package/dist/esm/utils/array-utils.js.map +1 -0
- package/dist/esm/utils/comparison.js +11 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.js +4 -0
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/dist/esm/utils.d.ts +6 -1
- package/dist/esm/utils.js +7 -9
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/virtual-props.d.ts +196 -0
- package/dist/esm/virtual-props.js +33 -0
- package/dist/esm/virtual-props.js.map +1 -0
- package/package.json +2 -2
- package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
- package/src/collection/change-events.ts +13 -9
- package/src/collection/changes.ts +30 -7
- package/src/collection/cleanup-queue.ts +105 -0
- package/src/collection/events.ts +65 -0
- package/src/collection/index.ts +110 -45
- package/src/collection/indexes.ts +283 -76
- package/src/collection/lifecycle.ts +5 -26
- package/src/collection/mutations.ts +21 -0
- package/src/collection/state.ts +545 -71
- package/src/collection/subscription.ts +7 -0
- package/src/collection/sync.ts +137 -0
- package/src/collection/transaction-metadata.ts +1 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +46 -3
- package/src/indexes/auto-index.ts +18 -8
- package/src/indexes/base-index.ts +2 -10
- package/src/indexes/basic-index.ts +507 -0
- package/src/indexes/btree-index.ts +1 -1
- package/src/indexes/index-options.ts +17 -37
- package/src/indexes/index-registry.ts +174 -0
- package/src/local-only.ts +7 -0
- package/src/query/builder/functions.ts +84 -7
- package/src/query/builder/index.ts +329 -9
- package/src/query/builder/ref-proxy.ts +22 -4
- package/src/query/builder/types.ts +257 -62
- package/src/query/compiler/evaluators.ts +57 -0
- package/src/query/compiler/group-by.ts +156 -35
- package/src/query/compiler/index.ts +445 -15
- package/src/query/compiler/order-by.ts +51 -12
- package/src/query/compiler/select.ts +9 -0
- package/src/query/index.ts +7 -0
- package/src/query/ir.ts +23 -2
- package/src/query/live/collection-config-builder.ts +809 -9
- package/src/query/live/types.ts +10 -4
- package/src/query/live/utils.ts +64 -3
- package/src/query/live-query-collection.ts +43 -18
- package/src/query/query-once.ts +31 -12
- package/src/query/subset-dedupe.ts +11 -7
- package/src/types.ts +49 -9
- package/src/utils/array-utils.ts +49 -0
- package/src/utils/comparison.ts +14 -0
- package/src/utils/index-optimization.ts +4 -0
- package/src/utils.ts +12 -9
- package/src/virtual-props.ts +282 -0
- package/dist/cjs/indexes/lazy-index.cjs +0 -190
- package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
- package/dist/cjs/indexes/lazy-index.d.cts +0 -96
- package/dist/esm/indexes/lazy-index.d.ts +0 -96
- package/dist/esm/indexes/lazy-index.js +0 -190
- package/dist/esm/indexes/lazy-index.js.map +0 -1
- package/src/indexes/lazy-index.ts +0 -251
package/dist/esm/utils.js
CHANGED
|
@@ -61,8 +61,8 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
if (isTemporal(a) && isTemporal(b)) {
|
|
64
|
-
const aTag =
|
|
65
|
-
const bTag =
|
|
64
|
+
const aTag = a[Symbol.toStringTag];
|
|
65
|
+
const bTag = b[Symbol.toStringTag];
|
|
66
66
|
if (aTag !== bTag) return false;
|
|
67
67
|
if (typeof a.equals === `function`) {
|
|
68
68
|
return a.equals(b);
|
|
@@ -102,7 +102,7 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
102
102
|
}
|
|
103
103
|
return false;
|
|
104
104
|
}
|
|
105
|
-
const temporalTypes = [
|
|
105
|
+
const temporalTypes = /* @__PURE__ */ new Set([
|
|
106
106
|
`Temporal.Duration`,
|
|
107
107
|
`Temporal.Instant`,
|
|
108
108
|
`Temporal.PlainDate`,
|
|
@@ -111,13 +111,11 @@ const temporalTypes = [
|
|
|
111
111
|
`Temporal.PlainTime`,
|
|
112
112
|
`Temporal.PlainYearMonth`,
|
|
113
113
|
`Temporal.ZonedDateTime`
|
|
114
|
-
];
|
|
115
|
-
function getStringTag(a) {
|
|
116
|
-
return a[Symbol.toStringTag];
|
|
117
|
-
}
|
|
114
|
+
]);
|
|
118
115
|
function isTemporal(a) {
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
if (a == null || typeof a !== `object`) return false;
|
|
117
|
+
const tag = a[Symbol.toStringTag];
|
|
118
|
+
return typeof tag === `string` && temporalTypes.has(tag);
|
|
121
119
|
}
|
|
122
120
|
const DEFAULT_COMPARE_OPTIONS = {
|
|
123
121
|
direction: `asc`,
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Generic utility functions\n */\n\nimport type { CompareOptions } from './query/builder/types'\n\ninterface TypedArray {\n length: number\n [index: number]: number\n}\n\n/**\n * Deep equality function that compares two values recursively\n * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns True if the values are deeply equal, false otherwise\n *\n * @example\n * ```typescript\n * deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)\n * deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true\n * deepEquals({ a: 1 }, { a: 2 }) // false\n * deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true\n * deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true\n * ```\n */\nexport function deepEquals(a: any, b: any): boolean {\n return deepEqualsInternal(a, b, new Map())\n}\n\n/**\n * Internal implementation with cycle detection to prevent infinite recursion\n */\nfunction deepEqualsInternal(\n a: any,\n b: any,\n visited: Map<object, object>,\n): boolean {\n // Handle strict equality (primitives, same reference)\n if (a === b) return true\n\n // Handle null/undefined\n if (a == null || b == null) return false\n\n // Handle different types\n if (typeof a !== typeof b) return false\n\n // Handle Date objects\n if (a instanceof Date) {\n if (!(b instanceof Date)) return false\n return a.getTime() === b.getTime()\n }\n // Symmetric check: if b is Date but a is not, they're not equal\n if (b instanceof Date) return false\n\n // Handle RegExp objects\n if (a instanceof RegExp) {\n if (!(b instanceof RegExp)) return false\n return a.source === b.source && a.flags === b.flags\n }\n // Symmetric check: if b is RegExp but a is not, they're not equal\n if (b instanceof RegExp) return false\n\n // Handle Map objects - only if both are Maps\n if (a instanceof Map) {\n if (!(b instanceof Map)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const entries = Array.from(a.entries())\n const result = entries.every(([key, val]) => {\n return b.has(key) && deepEqualsInternal(val, b.get(key), visited)\n })\n\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is Map but a is not, they're not equal\n if (b instanceof Map) return false\n\n // Handle Set objects - only if both are Sets\n if (a instanceof Set) {\n if (!(b instanceof Set)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Convert to arrays for comparison\n const aValues = Array.from(a)\n const bValues = Array.from(b)\n\n // Simple comparison for primitive values\n if (aValues.every((val) => typeof val !== `object`)) {\n visited.delete(a)\n return aValues.every((val) => b.has(val))\n }\n\n // For objects in sets, we need to do a more complex comparison\n // This is a simplified approach and may not work for all cases\n const result = aValues.length === bValues.length\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is Set but a is not, they're not equal\n if (b instanceof Set) return false\n\n // Handle TypedArrays\n if (\n ArrayBuffer.isView(a) &&\n ArrayBuffer.isView(b) &&\n !(a instanceof DataView) &&\n !(b instanceof DataView)\n ) {\n const typedA = a as unknown as TypedArray\n const typedB = b as unknown as TypedArray\n if (typedA.length !== typedB.length) return false\n\n for (let i = 0; i < typedA.length; i++) {\n if (typedA[i] !== typedB[i]) return false\n }\n\n return true\n }\n // Symmetric check: if b is TypedArray but a is not, they're not equal\n if (\n ArrayBuffer.isView(b) &&\n !(b instanceof DataView) &&\n !ArrayBuffer.isView(a)\n ) {\n return false\n }\n\n // Handle Temporal objects\n // Check if both are Temporal objects of the same type\n if (isTemporal(a) && isTemporal(b)) {\n const aTag = getStringTag(a)\n const bTag = getStringTag(b)\n\n // If they're different Temporal types, they're not equal\n if (aTag !== bTag) return false\n\n // Use Temporal's built-in equals method if available\n if (typeof a.equals === `function`) {\n return a.equals(b)\n }\n\n // Fallback to toString comparison for other types\n return a.toString() === b.toString()\n }\n // Symmetric check: if b is Temporal but a is not, they're not equal\n if (isTemporal(b)) return false\n\n // Handle arrays\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const result = a.every((item, index) =>\n deepEqualsInternal(item, b[index], visited),\n )\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is array but a is not, they're not equal\n if (Array.isArray(b)) return false\n\n // Handle objects\n if (typeof a === `object`) {\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Get all keys from both objects\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n\n // Check if they have the same number of keys\n if (keysA.length !== keysB.length) {\n visited.delete(a)\n return false\n }\n\n // Check if all keys exist in both objects and their values are equal\n const result = keysA.every(\n (key) => key in b && deepEqualsInternal(a[key], b[key], visited),\n )\n\n visited.delete(a)\n return result\n }\n\n // For primitives that aren't strictly equal\n return false\n}\n\nconst temporalTypes = [\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n]\n\nfunction getStringTag(a: any): any {\n return a[Symbol.toStringTag]\n}\n\n/** Checks if the value is a Temporal object by checking for the Temporal brand */\nexport function isTemporal(a: any): boolean {\n const tag = getStringTag(a)\n return typeof tag === `string` && temporalTypes.includes(tag)\n}\n\nexport const DEFAULT_COMPARE_OPTIONS: CompareOptions = {\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Generic utility functions\n */\n\nimport type { CompareOptions } from './query/builder/types'\n\ninterface TypedArray {\n length: number\n [index: number]: number\n}\n\n/**\n * Deep equality function that compares two values recursively\n * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns True if the values are deeply equal, false otherwise\n *\n * @example\n * ```typescript\n * deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)\n * deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true\n * deepEquals({ a: 1 }, { a: 2 }) // false\n * deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true\n * deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true\n * ```\n */\nexport function deepEquals(a: any, b: any): boolean {\n return deepEqualsInternal(a, b, new Map())\n}\n\n/**\n * Internal implementation with cycle detection to prevent infinite recursion\n */\nfunction deepEqualsInternal(\n a: any,\n b: any,\n visited: Map<object, object>,\n): boolean {\n // Handle strict equality (primitives, same reference)\n if (a === b) return true\n\n // Handle null/undefined\n if (a == null || b == null) return false\n\n // Handle different types\n if (typeof a !== typeof b) return false\n\n // Handle Date objects\n if (a instanceof Date) {\n if (!(b instanceof Date)) return false\n return a.getTime() === b.getTime()\n }\n // Symmetric check: if b is Date but a is not, they're not equal\n if (b instanceof Date) return false\n\n // Handle RegExp objects\n if (a instanceof RegExp) {\n if (!(b instanceof RegExp)) return false\n return a.source === b.source && a.flags === b.flags\n }\n // Symmetric check: if b is RegExp but a is not, they're not equal\n if (b instanceof RegExp) return false\n\n // Handle Map objects - only if both are Maps\n if (a instanceof Map) {\n if (!(b instanceof Map)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const entries = Array.from(a.entries())\n const result = entries.every(([key, val]) => {\n return b.has(key) && deepEqualsInternal(val, b.get(key), visited)\n })\n\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is Map but a is not, they're not equal\n if (b instanceof Map) return false\n\n // Handle Set objects - only if both are Sets\n if (a instanceof Set) {\n if (!(b instanceof Set)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Convert to arrays for comparison\n const aValues = Array.from(a)\n const bValues = Array.from(b)\n\n // Simple comparison for primitive values\n if (aValues.every((val) => typeof val !== `object`)) {\n visited.delete(a)\n return aValues.every((val) => b.has(val))\n }\n\n // For objects in sets, we need to do a more complex comparison\n // This is a simplified approach and may not work for all cases\n const result = aValues.length === bValues.length\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is Set but a is not, they're not equal\n if (b instanceof Set) return false\n\n // Handle TypedArrays\n if (\n ArrayBuffer.isView(a) &&\n ArrayBuffer.isView(b) &&\n !(a instanceof DataView) &&\n !(b instanceof DataView)\n ) {\n const typedA = a as unknown as TypedArray\n const typedB = b as unknown as TypedArray\n if (typedA.length !== typedB.length) return false\n\n for (let i = 0; i < typedA.length; i++) {\n if (typedA[i] !== typedB[i]) return false\n }\n\n return true\n }\n // Symmetric check: if b is TypedArray but a is not, they're not equal\n if (\n ArrayBuffer.isView(b) &&\n !(b instanceof DataView) &&\n !ArrayBuffer.isView(a)\n ) {\n return false\n }\n\n // Handle Temporal objects\n // Check if both are Temporal objects of the same type\n if (isTemporal(a) && isTemporal(b)) {\n const aTag = a[Symbol.toStringTag]\n const bTag = b[Symbol.toStringTag]\n\n // If they're different Temporal types, they're not equal\n if (aTag !== bTag) return false\n\n // Use Temporal's built-in equals method if available\n if (typeof a.equals === `function`) {\n return a.equals(b)\n }\n\n // Fallback to toString comparison for other types\n return a.toString() === b.toString()\n }\n // Symmetric check: if b is Temporal but a is not, they're not equal\n if (isTemporal(b)) return false\n\n // Handle arrays\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const result = a.every((item, index) =>\n deepEqualsInternal(item, b[index], visited),\n )\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is array but a is not, they're not equal\n if (Array.isArray(b)) return false\n\n // Handle objects\n if (typeof a === `object`) {\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Get all keys from both objects\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n\n // Check if they have the same number of keys\n if (keysA.length !== keysB.length) {\n visited.delete(a)\n return false\n }\n\n // Check if all keys exist in both objects and their values are equal\n const result = keysA.every(\n (key) => key in b && deepEqualsInternal(a[key], b[key], visited),\n )\n\n visited.delete(a)\n return result\n }\n\n // For primitives that aren't strictly equal\n return false\n}\n\nconst temporalTypes = new Set([\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n])\n\nexport interface TemporalLike {\n [Symbol.toStringTag]: string\n toString: () => string\n equals?: (other: unknown) => boolean\n}\n\n/** Checks if the value is a Temporal object by checking for the Temporal brand */\nexport function isTemporal(a: unknown): a is TemporalLike {\n if (a == null || typeof a !== `object`) return false\n const tag = (a as Record<symbol, unknown>)[Symbol.toStringTag]\n return typeof tag === `string` && temporalTypes.has(tag)\n}\n\nexport const DEFAULT_COMPARE_OPTIONS: CompareOptions = {\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual Properties for TanStack DB
|
|
3
|
+
*
|
|
4
|
+
* Virtual properties are computed, read-only properties that provide metadata about rows
|
|
5
|
+
* (sync status, source, selection state) without being part of the persisted data model.
|
|
6
|
+
*
|
|
7
|
+
* Virtual properties are prefixed with `$` to distinguish them from user data fields.
|
|
8
|
+
* User schemas should not include `$`-prefixed fields as they are reserved.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Origin of the last confirmed change to a row, from the current client's perspective.
|
|
12
|
+
*
|
|
13
|
+
* - `'local'`: The change originated from this client (e.g., a mutation made here)
|
|
14
|
+
* - `'remote'`: The change was received via sync from another client/server
|
|
15
|
+
*
|
|
16
|
+
* Note: This reflects the client's perspective, not the original creator.
|
|
17
|
+
* User A creates order → $origin = 'local' on User A's client
|
|
18
|
+
* Order syncs to server
|
|
19
|
+
* User B receives order → $origin = 'remote' on User B's client
|
|
20
|
+
*/
|
|
21
|
+
export type VirtualOrigin = 'local' | 'remote';
|
|
22
|
+
/**
|
|
23
|
+
* Virtual properties available on every row in TanStack DB collections.
|
|
24
|
+
*
|
|
25
|
+
* These properties are:
|
|
26
|
+
* - Computed (not stored in the data model)
|
|
27
|
+
* - Read-only (cannot be mutated directly)
|
|
28
|
+
* - Available in queries (WHERE, ORDER BY, SELECT)
|
|
29
|
+
* - Included when spreading rows (`...user`)
|
|
30
|
+
*
|
|
31
|
+
* @template TKey - The type of the row's key (string or number)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // Accessing virtual properties on a row
|
|
36
|
+
* const user = collection.get('user-1')
|
|
37
|
+
* if (user.$synced) {
|
|
38
|
+
* console.log('Confirmed by backend')
|
|
39
|
+
* }
|
|
40
|
+
* if (user.$origin === 'local') {
|
|
41
|
+
* console.log('Created/modified locally')
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Using virtual properties in queries
|
|
48
|
+
* const confirmedOrders = createLiveQueryCollection({
|
|
49
|
+
* query: (q) => q
|
|
50
|
+
* .from({ order: orders })
|
|
51
|
+
* .where(({ order }) => eq(order.$synced, true))
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export interface VirtualRowProps<TKey extends string | number = string | number> {
|
|
56
|
+
/**
|
|
57
|
+
* Whether this row reflects confirmed state from the backend.
|
|
58
|
+
*
|
|
59
|
+
* - `true`: Row is confirmed by the backend (no pending optimistic mutations)
|
|
60
|
+
* - `false`: Row has pending optimistic mutations that haven't been confirmed
|
|
61
|
+
*
|
|
62
|
+
* For local-only collections (no sync), this is always `true`.
|
|
63
|
+
* For live query collections, this is passed through from the source collection.
|
|
64
|
+
*/
|
|
65
|
+
readonly $synced: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Origin of the last confirmed change to this row, from the current client's perspective.
|
|
68
|
+
*
|
|
69
|
+
* - `'local'`: The change originated from this client
|
|
70
|
+
* - `'remote'`: The change was received via sync
|
|
71
|
+
*
|
|
72
|
+
* For local-only collections, this is always `'local'`.
|
|
73
|
+
* For live query collections, this is passed through from the source collection.
|
|
74
|
+
*/
|
|
75
|
+
readonly $origin: VirtualOrigin;
|
|
76
|
+
/**
|
|
77
|
+
* The row's key (primary identifier).
|
|
78
|
+
*
|
|
79
|
+
* This is the same value returned by `collection.config.getKey(row)`.
|
|
80
|
+
* Useful when you need the key in projections or computations.
|
|
81
|
+
*/
|
|
82
|
+
readonly $key: TKey;
|
|
83
|
+
/**
|
|
84
|
+
* The ID of the source collection this row originated from.
|
|
85
|
+
*
|
|
86
|
+
* In joins, this can help identify which collection each row came from.
|
|
87
|
+
* For live query collections, this is the ID of the upstream collection.
|
|
88
|
+
*/
|
|
89
|
+
readonly $collectionId: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Adds virtual properties to a row type.
|
|
93
|
+
*
|
|
94
|
+
* @template T - The base row type
|
|
95
|
+
* @template TKey - The type of the row's key
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* type User = { id: string; name: string }
|
|
100
|
+
* type UserWithVirtual = WithVirtualProps<User, string>
|
|
101
|
+
* // { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export type WithVirtualProps<T extends object, TKey extends string | number = string | number> = T & VirtualRowProps<TKey>;
|
|
105
|
+
/**
|
|
106
|
+
* Extracts the base type from a type that may have virtual properties.
|
|
107
|
+
* Useful when you need to work with the raw data without virtual properties.
|
|
108
|
+
*
|
|
109
|
+
* @template T - The type that may include virtual properties
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* type UserWithVirtual = { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote' }
|
|
114
|
+
* type User = WithoutVirtualProps<UserWithVirtual>
|
|
115
|
+
* // { id: string; name: string }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export type WithoutVirtualProps<T> = Omit<T, keyof VirtualRowProps>;
|
|
119
|
+
/**
|
|
120
|
+
* Checks if a value has virtual properties attached.
|
|
121
|
+
*
|
|
122
|
+
* @param value - The value to check
|
|
123
|
+
* @returns true if the value has virtual properties
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* if (hasVirtualProps(row)) {
|
|
128
|
+
* console.log('Synced:', row.$synced)
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export declare function hasVirtualProps(value: unknown): value is VirtualRowProps<string | number>;
|
|
133
|
+
/**
|
|
134
|
+
* Creates virtual properties for a row in a source collection.
|
|
135
|
+
*
|
|
136
|
+
* This is the internal function used by collections to add virtual properties
|
|
137
|
+
* to rows when emitting change messages.
|
|
138
|
+
*
|
|
139
|
+
* @param key - The row's key
|
|
140
|
+
* @param collectionId - The collection's ID
|
|
141
|
+
* @param isSynced - Whether the row is synced (not optimistic)
|
|
142
|
+
* @param origin - Whether the change was local or remote
|
|
143
|
+
* @returns Virtual properties object to merge with the row
|
|
144
|
+
*
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export declare function createVirtualProps<TKey extends string | number>(key: TKey, collectionId: string, isSynced: boolean, origin: VirtualOrigin): VirtualRowProps<TKey>;
|
|
148
|
+
/**
|
|
149
|
+
* Enriches a row with virtual properties using the "add-if-missing" pattern.
|
|
150
|
+
*
|
|
151
|
+
* If the row already has virtual properties (from an upstream collection),
|
|
152
|
+
* they are preserved. If not, new virtual properties are computed and added.
|
|
153
|
+
*
|
|
154
|
+
* This is the key function that enables pass-through semantics for nested
|
|
155
|
+
* live query collections.
|
|
156
|
+
*
|
|
157
|
+
* @param row - The row to enrich
|
|
158
|
+
* @param key - The row's key
|
|
159
|
+
* @param collectionId - The collection's ID
|
|
160
|
+
* @param computeSynced - Function to compute $synced if missing
|
|
161
|
+
* @param computeOrigin - Function to compute $origin if missing
|
|
162
|
+
* @returns The row with virtual properties (possibly the same object if already present)
|
|
163
|
+
*
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
export declare function enrichRowWithVirtualProps<T extends object, TKey extends string | number>(row: T, key: TKey, collectionId: string, computeSynced: () => boolean, computeOrigin: () => VirtualOrigin): WithVirtualProps<T, TKey>;
|
|
167
|
+
/**
|
|
168
|
+
* Computes aggregate virtual properties for a group of rows.
|
|
169
|
+
*
|
|
170
|
+
* For aggregates:
|
|
171
|
+
* - `$synced`: true if ALL rows in the group are synced; false if ANY row is optimistic
|
|
172
|
+
* - `$origin`: 'local' if ANY row in the group is local; otherwise 'remote'
|
|
173
|
+
*
|
|
174
|
+
* @param rows - The rows in the group
|
|
175
|
+
* @param groupKey - The group key
|
|
176
|
+
* @param collectionId - The collection ID
|
|
177
|
+
* @returns Virtual properties for the aggregate row
|
|
178
|
+
*
|
|
179
|
+
* @internal
|
|
180
|
+
*/
|
|
181
|
+
export declare function computeAggregateVirtualProps<TKey extends string | number>(rows: Array<Partial<VirtualRowProps<string | number>>>, groupKey: TKey, collectionId: string): VirtualRowProps<TKey>;
|
|
182
|
+
/**
|
|
183
|
+
* List of virtual property names for iteration and checking.
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
export declare const VIRTUAL_PROP_NAMES: readonly ["$synced", "$origin", "$key", "$collectionId"];
|
|
187
|
+
/**
|
|
188
|
+
* Checks if a property name is a virtual property.
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
export declare function isVirtualPropName(name: string): boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Checks whether a property path references a virtual property.
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
export declare function hasVirtualPropPath(path: Array<string>): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function hasVirtualProps(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && VIRTUAL_PROP_NAMES.every((name) => name in value);
|
|
3
|
+
}
|
|
4
|
+
function enrichRowWithVirtualProps(row, key, collectionId, computeSynced, computeOrigin) {
|
|
5
|
+
const existingRow = row;
|
|
6
|
+
return {
|
|
7
|
+
...row,
|
|
8
|
+
$synced: existingRow.$synced ?? computeSynced(),
|
|
9
|
+
$origin: existingRow.$origin ?? computeOrigin(),
|
|
10
|
+
$key: existingRow.$key ?? key,
|
|
11
|
+
$collectionId: existingRow.$collectionId ?? collectionId
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const VIRTUAL_PROP_NAMES = [
|
|
15
|
+
"$synced",
|
|
16
|
+
"$origin",
|
|
17
|
+
"$key",
|
|
18
|
+
"$collectionId"
|
|
19
|
+
];
|
|
20
|
+
function isVirtualPropName(name) {
|
|
21
|
+
return VIRTUAL_PROP_NAMES.includes(name);
|
|
22
|
+
}
|
|
23
|
+
function hasVirtualPropPath(path) {
|
|
24
|
+
return path.some((segment) => isVirtualPropName(segment));
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
VIRTUAL_PROP_NAMES,
|
|
28
|
+
enrichRowWithVirtualProps,
|
|
29
|
+
hasVirtualPropPath,
|
|
30
|
+
hasVirtualProps,
|
|
31
|
+
isVirtualPropName
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=virtual-props.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtual-props.js","sources":["../../src/virtual-props.ts"],"sourcesContent":["/**\n * Virtual Properties for TanStack DB\n *\n * Virtual properties are computed, read-only properties that provide metadata about rows\n * (sync status, source, selection state) without being part of the persisted data model.\n *\n * Virtual properties are prefixed with `$` to distinguish them from user data fields.\n * User schemas should not include `$`-prefixed fields as they are reserved.\n */\n\n/**\n * Origin of the last confirmed change to a row, from the current client's perspective.\n *\n * - `'local'`: The change originated from this client (e.g., a mutation made here)\n * - `'remote'`: The change was received via sync from another client/server\n *\n * Note: This reflects the client's perspective, not the original creator.\n * User A creates order → $origin = 'local' on User A's client\n * Order syncs to server\n * User B receives order → $origin = 'remote' on User B's client\n */\nexport type VirtualOrigin = 'local' | 'remote'\n\n/**\n * Virtual properties available on every row in TanStack DB collections.\n *\n * These properties are:\n * - Computed (not stored in the data model)\n * - Read-only (cannot be mutated directly)\n * - Available in queries (WHERE, ORDER BY, SELECT)\n * - Included when spreading rows (`...user`)\n *\n * @template TKey - The type of the row's key (string or number)\n *\n * @example\n * ```typescript\n * // Accessing virtual properties on a row\n * const user = collection.get('user-1')\n * if (user.$synced) {\n * console.log('Confirmed by backend')\n * }\n * if (user.$origin === 'local') {\n * console.log('Created/modified locally')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Using virtual properties in queries\n * const confirmedOrders = createLiveQueryCollection({\n * query: (q) => q\n * .from({ order: orders })\n * .where(({ order }) => eq(order.$synced, true))\n * })\n * ```\n */\nexport interface VirtualRowProps<\n TKey extends string | number = string | number,\n> {\n /**\n * Whether this row reflects confirmed state from the backend.\n *\n * - `true`: Row is confirmed by the backend (no pending optimistic mutations)\n * - `false`: Row has pending optimistic mutations that haven't been confirmed\n *\n * For local-only collections (no sync), this is always `true`.\n * For live query collections, this is passed through from the source collection.\n */\n readonly $synced: boolean\n\n /**\n * Origin of the last confirmed change to this row, from the current client's perspective.\n *\n * - `'local'`: The change originated from this client\n * - `'remote'`: The change was received via sync\n *\n * For local-only collections, this is always `'local'`.\n * For live query collections, this is passed through from the source collection.\n */\n readonly $origin: VirtualOrigin\n\n /**\n * The row's key (primary identifier).\n *\n * This is the same value returned by `collection.config.getKey(row)`.\n * Useful when you need the key in projections or computations.\n */\n readonly $key: TKey\n\n /**\n * The ID of the source collection this row originated from.\n *\n * In joins, this can help identify which collection each row came from.\n * For live query collections, this is the ID of the upstream collection.\n */\n readonly $collectionId: string\n}\n\n/**\n * Adds virtual properties to a row type.\n *\n * @template T - The base row type\n * @template TKey - The type of the row's key\n *\n * @example\n * ```typescript\n * type User = { id: string; name: string }\n * type UserWithVirtual = WithVirtualProps<User, string>\n * // { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string }\n * ```\n */\nexport type WithVirtualProps<\n T extends object,\n TKey extends string | number = string | number,\n> = T & VirtualRowProps<TKey>\n\n/**\n * Extracts the base type from a type that may have virtual properties.\n * Useful when you need to work with the raw data without virtual properties.\n *\n * @template T - The type that may include virtual properties\n *\n * @example\n * ```typescript\n * type UserWithVirtual = { id: string; name: string; $synced: boolean; $origin: 'local' | 'remote' }\n * type User = WithoutVirtualProps<UserWithVirtual>\n * // { id: string; name: string }\n * ```\n */\nexport type WithoutVirtualProps<T> = Omit<T, keyof VirtualRowProps>\n\n/**\n * Checks if a value has virtual properties attached.\n *\n * @param value - The value to check\n * @returns true if the value has virtual properties\n *\n * @example\n * ```typescript\n * if (hasVirtualProps(row)) {\n * console.log('Synced:', row.$synced)\n * }\n * ```\n */\nexport function hasVirtualProps(\n value: unknown,\n): value is VirtualRowProps<string | number> {\n return (\n typeof value === 'object' &&\n value !== null &&\n VIRTUAL_PROP_NAMES.every((name) => name in value)\n )\n}\n\n/**\n * Creates virtual properties for a row in a source collection.\n *\n * This is the internal function used by collections to add virtual properties\n * to rows when emitting change messages.\n *\n * @param key - The row's key\n * @param collectionId - The collection's ID\n * @param isSynced - Whether the row is synced (not optimistic)\n * @param origin - Whether the change was local or remote\n * @returns Virtual properties object to merge with the row\n *\n * @internal\n */\nexport function createVirtualProps<TKey extends string | number>(\n key: TKey,\n collectionId: string,\n isSynced: boolean,\n origin: VirtualOrigin,\n): VirtualRowProps<TKey> {\n return {\n $synced: isSynced,\n $origin: origin,\n $key: key,\n $collectionId: collectionId,\n }\n}\n\n/**\n * Enriches a row with virtual properties using the \"add-if-missing\" pattern.\n *\n * If the row already has virtual properties (from an upstream collection),\n * they are preserved. If not, new virtual properties are computed and added.\n *\n * This is the key function that enables pass-through semantics for nested\n * live query collections.\n *\n * @param row - The row to enrich\n * @param key - The row's key\n * @param collectionId - The collection's ID\n * @param computeSynced - Function to compute $synced if missing\n * @param computeOrigin - Function to compute $origin if missing\n * @returns The row with virtual properties (possibly the same object if already present)\n *\n * @internal\n */\nexport function enrichRowWithVirtualProps<\n T extends object,\n TKey extends string | number,\n>(\n row: T,\n key: TKey,\n collectionId: string,\n computeSynced: () => boolean,\n computeOrigin: () => VirtualOrigin,\n): WithVirtualProps<T, TKey> {\n // Use nullish coalescing to preserve existing virtual properties (pass-through)\n // This is the \"add-if-missing\" pattern described in the RFC\n const existingRow = row as Partial<VirtualRowProps<TKey>>\n\n return {\n ...row,\n $synced: existingRow.$synced ?? computeSynced(),\n $origin: existingRow.$origin ?? computeOrigin(),\n $key: existingRow.$key ?? key,\n $collectionId: existingRow.$collectionId ?? collectionId,\n } as WithVirtualProps<T, TKey>\n}\n\n/**\n * Computes aggregate virtual properties for a group of rows.\n *\n * For aggregates:\n * - `$synced`: true if ALL rows in the group are synced; false if ANY row is optimistic\n * - `$origin`: 'local' if ANY row in the group is local; otherwise 'remote'\n *\n * @param rows - The rows in the group\n * @param groupKey - The group key\n * @param collectionId - The collection ID\n * @returns Virtual properties for the aggregate row\n *\n * @internal\n */\nexport function computeAggregateVirtualProps<TKey extends string | number>(\n rows: Array<Partial<VirtualRowProps<string | number>>>,\n groupKey: TKey,\n collectionId: string,\n): VirtualRowProps<TKey> {\n // $synced = true only if ALL rows are synced (false if ANY is optimistic)\n const allSynced = rows.every((row) => row.$synced ?? true)\n\n // $origin = 'local' if ANY row is local (consistent with \"local influence\" semantics)\n const hasLocal = rows.some((row) => row.$origin === 'local')\n\n return {\n $synced: allSynced,\n $origin: hasLocal ? 'local' : 'remote',\n $key: groupKey,\n $collectionId: collectionId,\n }\n}\n\n/**\n * List of virtual property names for iteration and checking.\n * @internal\n */\nexport const VIRTUAL_PROP_NAMES = [\n '$synced',\n '$origin',\n '$key',\n '$collectionId',\n] as const\n\n/**\n * Checks if a property name is a virtual property.\n * @internal\n */\nexport function isVirtualPropName(name: string): boolean {\n return VIRTUAL_PROP_NAMES.includes(name as any)\n}\n\n/**\n * Checks whether a property path references a virtual property.\n * @internal\n */\nexport function hasVirtualPropPath(path: Array<string>): boolean {\n return path.some((segment) => isVirtualPropName(segment))\n}\n"],"names":[],"mappings":"AAgJO,SAAS,gBACd,OAC2C;AAC3C,SACE,OAAO,UAAU,YACjB,UAAU,QACV,mBAAmB,MAAM,CAAC,SAAS,QAAQ,KAAK;AAEpD;AAgDO,SAAS,0BAId,KACA,KACA,cACA,eACA,eAC2B;AAG3B,QAAM,cAAc;AAEpB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,YAAY,WAAW,cAAA;AAAA,IAChC,SAAS,YAAY,WAAW,cAAA;AAAA,IAChC,MAAM,YAAY,QAAQ;AAAA,IAC1B,eAAe,YAAY,iBAAiB;AAAA,EAAA;AAEhD;AAuCO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,SAAS,kBAAkB,MAAuB;AACvD,SAAO,mBAAmB,SAAS,IAAW;AAChD;AAMO,SAAS,mBAAmB,MAA8B;AAC/D,SAAO,KAAK,KAAK,CAAC,YAAY,kBAAkB,OAAO,CAAC;AAC1D;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A reactive client store for building super fast apps on sync",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@standard-schema/spec": "^1.1.0",
|
|
42
42
|
"@tanstack/pacer-lite": "^0.2.1",
|
|
43
|
-
"@tanstack/db-ivm": "0.1.
|
|
43
|
+
"@tanstack/db-ivm": "0.1.18"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"typescript": ">=4.7"
|
|
@@ -78,7 +78,7 @@ onInsert: async ({ transaction }) => {
|
|
|
78
78
|
|
|
79
79
|
## Utility Methods (`collection.utils`)
|
|
80
80
|
|
|
81
|
-
- `awaitTxId(txid, timeout?)` -- wait for txid in Electric stream; default timeout
|
|
81
|
+
- `awaitTxId(txid, timeout?)` -- wait for txid in Electric stream; default timeout 5s
|
|
82
82
|
- `awaitMatch(matchFn, timeout?)` -- wait for message matching predicate; default timeout 3000ms
|
|
83
83
|
|
|
84
84
|
### Helper Exports
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import type { CollectionImpl } from './index.js'
|
|
23
23
|
import type { SingleRowRefProxy } from '../query/builder/ref-proxy'
|
|
24
24
|
import type { BasicExpression, OrderBy } from '../query/ir.js'
|
|
25
|
+
import type { WithVirtualProps } from '../virtual-props.js'
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Returns the current state of the collection as an array of changes
|
|
@@ -58,14 +59,14 @@ export function currentStateAsChanges<
|
|
|
58
59
|
T extends object,
|
|
59
60
|
TKey extends string | number,
|
|
60
61
|
>(
|
|
61
|
-
collection: CollectionLike<T, TKey>,
|
|
62
|
+
collection: CollectionLike<WithVirtualProps<T, TKey>, TKey>,
|
|
62
63
|
options: CurrentStateAsChangesOptions = {},
|
|
63
|
-
): Array<ChangeMessage<T>> | void {
|
|
64
|
+
): Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> | void {
|
|
64
65
|
// Helper function to collect filtered results
|
|
65
66
|
const collectFilteredResults = (
|
|
66
|
-
filterFn?: (value: T) => boolean,
|
|
67
|
-
): Array<ChangeMessage<T>> => {
|
|
68
|
-
const result: Array<ChangeMessage<T>> = []
|
|
67
|
+
filterFn?: (value: WithVirtualProps<T, TKey>) => boolean,
|
|
68
|
+
): Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> => {
|
|
69
|
+
const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
|
|
69
70
|
for (const [key, value] of collection.entries()) {
|
|
70
71
|
// If no filter function is provided, include all items
|
|
71
72
|
if (filterFn?.(value) ?? true) {
|
|
@@ -106,7 +107,7 @@ export function currentStateAsChanges<
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Convert keys to change messages
|
|
109
|
-
const result: Array<ChangeMessage<T>> = []
|
|
110
|
+
const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
|
|
110
111
|
for (const key of orderedKeys) {
|
|
111
112
|
const value = collection.get(key)
|
|
112
113
|
if (value !== undefined) {
|
|
@@ -138,7 +139,7 @@ export function currentStateAsChanges<
|
|
|
138
139
|
|
|
139
140
|
if (optimizationResult.canOptimize) {
|
|
140
141
|
// Use index optimization
|
|
141
|
-
const result: Array<ChangeMessage<T>> = []
|
|
142
|
+
const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
|
|
142
143
|
for (const key of optimizationResult.matchingKeys) {
|
|
143
144
|
const value = collection.get(key)
|
|
144
145
|
if (value !== undefined) {
|
|
@@ -241,9 +242,12 @@ export function createFilterFunctionFromExpression<T extends object>(
|
|
|
241
242
|
* @param options - The subscription options containing the where clause
|
|
242
243
|
* @returns A filtered callback function
|
|
243
244
|
*/
|
|
244
|
-
export function createFilteredCallback<
|
|
245
|
+
export function createFilteredCallback<
|
|
246
|
+
T extends object,
|
|
247
|
+
TKey extends string | number = string | number,
|
|
248
|
+
>(
|
|
245
249
|
originalCallback: (changes: Array<ChangeMessage<T>>) => void,
|
|
246
|
-
options: SubscribeChangesOptions,
|
|
250
|
+
options: SubscribeChangesOptions<T, TKey>,
|
|
247
251
|
): (changes: Array<ChangeMessage<T>>) => void {
|
|
248
252
|
const filterFn = createFilterFunctionFromExpression(options.whereExpression!)
|
|
249
253
|
|
|
@@ -10,6 +10,8 @@ import type { CollectionLifecycleManager } from './lifecycle.js'
|
|
|
10
10
|
import type { CollectionSyncManager } from './sync.js'
|
|
11
11
|
import type { CollectionEventsManager } from './events.js'
|
|
12
12
|
import type { CollectionImpl } from './index.js'
|
|
13
|
+
import type { CollectionStateManager } from './state.js'
|
|
14
|
+
import type { WithVirtualProps } from '../virtual-props.js'
|
|
13
15
|
|
|
14
16
|
export class CollectionChangesManager<
|
|
15
17
|
TOutput extends object = Record<string, unknown>,
|
|
@@ -21,6 +23,7 @@ export class CollectionChangesManager<
|
|
|
21
23
|
private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
|
|
22
24
|
private events!: CollectionEventsManager
|
|
23
25
|
private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
26
|
+
private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
|
|
24
27
|
|
|
25
28
|
public activeSubscribersCount = 0
|
|
26
29
|
public changeSubscriptions = new Set<CollectionSubscription>()
|
|
@@ -37,11 +40,13 @@ export class CollectionChangesManager<
|
|
|
37
40
|
sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
|
|
38
41
|
events: CollectionEventsManager
|
|
39
42
|
collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
43
|
+
state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
|
|
40
44
|
}) {
|
|
41
45
|
this.lifecycle = deps.lifecycle
|
|
42
46
|
this.sync = deps.sync
|
|
43
47
|
this.events = deps.events
|
|
44
48
|
this.collection = deps.collection
|
|
49
|
+
this.state = deps.state
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -55,6 +60,16 @@ export class CollectionChangesManager<
|
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Enriches a change message with virtual properties ($synced, $origin, $key, $collectionId).
|
|
65
|
+
* Uses the "add-if-missing" pattern to preserve virtual properties from upstream collections.
|
|
66
|
+
*/
|
|
67
|
+
private enrichChangeWithVirtualProps(
|
|
68
|
+
change: ChangeMessage<TOutput, TKey>,
|
|
69
|
+
): ChangeMessage<WithVirtualProps<TOutput, TKey>, TKey> {
|
|
70
|
+
return this.state.enrichChangeMessage(change)
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
/**
|
|
59
74
|
* Emit events either immediately or batch them for later emission
|
|
60
75
|
*/
|
|
@@ -70,26 +85,32 @@ export class CollectionChangesManager<
|
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
// Either we're not batching, or we're forcing emission (user action or ending batch cycle)
|
|
73
|
-
let
|
|
88
|
+
let rawEvents = changes
|
|
74
89
|
|
|
75
90
|
if (forceEmit) {
|
|
76
91
|
// Force emit is used to end a batch (e.g. after a sync commit). Combine any
|
|
77
92
|
// buffered optimistic events with the final changes so subscribers see the
|
|
78
93
|
// whole picture, even if the sync diff is empty.
|
|
79
94
|
if (this.batchedEvents.length > 0) {
|
|
80
|
-
|
|
95
|
+
rawEvents = [...this.batchedEvents, ...changes]
|
|
81
96
|
}
|
|
82
97
|
this.batchedEvents = []
|
|
83
98
|
this.shouldBatchEvents = false
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
if (
|
|
101
|
+
if (rawEvents.length === 0) {
|
|
87
102
|
return
|
|
88
103
|
}
|
|
89
104
|
|
|
105
|
+
// Enrich all change messages with virtual properties
|
|
106
|
+
// This uses the "add-if-missing" pattern to preserve pass-through semantics
|
|
107
|
+
const enrichedEvents: Array<
|
|
108
|
+
ChangeMessage<WithVirtualProps<TOutput, TKey>, TKey>
|
|
109
|
+
> = rawEvents.map((change) => this.enrichChangeWithVirtualProps(change))
|
|
110
|
+
|
|
90
111
|
// Emit to all listeners
|
|
91
112
|
for (const subscription of this.changeSubscriptions) {
|
|
92
|
-
subscription.emitEvents(
|
|
113
|
+
subscription.emitEvents(enrichedEvents)
|
|
93
114
|
}
|
|
94
115
|
}
|
|
95
116
|
|
|
@@ -97,8 +118,10 @@ export class CollectionChangesManager<
|
|
|
97
118
|
* Subscribe to changes in the collection
|
|
98
119
|
*/
|
|
99
120
|
public subscribeChanges(
|
|
100
|
-
callback: (
|
|
101
|
-
|
|
121
|
+
callback: (
|
|
122
|
+
changes: Array<ChangeMessage<WithVirtualProps<TOutput, TKey>>>,
|
|
123
|
+
) => void,
|
|
124
|
+
options: SubscribeChangesOptions<TOutput, TKey> = {},
|
|
102
125
|
): CollectionSubscription {
|
|
103
126
|
// Start sync and track subscriber
|
|
104
127
|
this.addSubscriber()
|
|
@@ -113,7 +136,7 @@ export class CollectionChangesManager<
|
|
|
113
136
|
const { where, ...opts } = options
|
|
114
137
|
let whereExpression = opts.whereExpression
|
|
115
138
|
if (where) {
|
|
116
|
-
const proxy = createSingleRowRefProxy<TOutput
|
|
139
|
+
const proxy = createSingleRowRefProxy<WithVirtualProps<TOutput, TKey>>()
|
|
117
140
|
const result = where(proxy)
|
|
118
141
|
whereExpression = toExpression(result)
|
|
119
142
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
type CleanupTask = {
|
|
2
|
+
executeAt: number
|
|
3
|
+
callback: () => void
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Batches many GC registrations behind a single shared timeout.
|
|
8
|
+
*/
|
|
9
|
+
export class CleanupQueue {
|
|
10
|
+
private static instance: CleanupQueue | null = null
|
|
11
|
+
|
|
12
|
+
private tasks: Map<unknown, CleanupTask> = new Map()
|
|
13
|
+
|
|
14
|
+
private timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
15
|
+
private microtaskScheduled = false
|
|
16
|
+
|
|
17
|
+
private constructor() {}
|
|
18
|
+
|
|
19
|
+
public static getInstance(): CleanupQueue {
|
|
20
|
+
if (!CleanupQueue.instance) {
|
|
21
|
+
CleanupQueue.instance = new CleanupQueue()
|
|
22
|
+
}
|
|
23
|
+
return CleanupQueue.instance
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Queues a cleanup task and defers timeout selection to a microtask so
|
|
28
|
+
* multiple synchronous registrations can share one root timer.
|
|
29
|
+
*/
|
|
30
|
+
public schedule(key: unknown, gcTime: number, callback: () => void): void {
|
|
31
|
+
const executeAt = Date.now() + gcTime
|
|
32
|
+
this.tasks.set(key, { executeAt, callback })
|
|
33
|
+
|
|
34
|
+
if (!this.microtaskScheduled) {
|
|
35
|
+
this.microtaskScheduled = true
|
|
36
|
+
Promise.resolve().then(() => {
|
|
37
|
+
this.microtaskScheduled = false
|
|
38
|
+
this.updateTimeout()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public cancel(key: unknown): void {
|
|
44
|
+
this.tasks.delete(key)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Keeps only one active timeout: whichever task is due next.
|
|
49
|
+
*/
|
|
50
|
+
private updateTimeout(): void {
|
|
51
|
+
if (this.timeoutId !== null) {
|
|
52
|
+
clearTimeout(this.timeoutId)
|
|
53
|
+
this.timeoutId = null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.tasks.size === 0) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let earliestTime = Infinity
|
|
61
|
+
for (const task of this.tasks.values()) {
|
|
62
|
+
if (task.executeAt < earliestTime) {
|
|
63
|
+
earliestTime = task.executeAt
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const delay = Math.max(0, earliestTime - Date.now())
|
|
68
|
+
this.timeoutId = setTimeout(() => this.process(), delay)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Runs every task whose deadline has passed, then schedules the next wakeup
|
|
73
|
+
* if there is still pending work.
|
|
74
|
+
*/
|
|
75
|
+
private process(): void {
|
|
76
|
+
this.timeoutId = null
|
|
77
|
+
const now = Date.now()
|
|
78
|
+
for (const [key, task] of this.tasks.entries()) {
|
|
79
|
+
if (now >= task.executeAt) {
|
|
80
|
+
this.tasks.delete(key)
|
|
81
|
+
try {
|
|
82
|
+
task.callback()
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error in CleanupQueue task:', error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.tasks.size > 0) {
|
|
90
|
+
this.updateTimeout()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resets the singleton instance for tests.
|
|
96
|
+
*/
|
|
97
|
+
public static resetInstance(): void {
|
|
98
|
+
if (CleanupQueue.instance) {
|
|
99
|
+
if (CleanupQueue.instance.timeoutId !== null) {
|
|
100
|
+
clearTimeout(CleanupQueue.instance.timeoutId)
|
|
101
|
+
}
|
|
102
|
+
CleanupQueue.instance = null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|