@tanstack/db 0.5.16 → 0.5.18
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/changes.cjs +18 -1
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/changes.d.cts +1 -1
- package/dist/cjs/collection/index.cjs +8 -5
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +9 -6
- package/dist/cjs/errors.cjs +13 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs +12 -0
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +2 -1
- package/dist/cjs/query/index.d.cts +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +10 -1
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +41 -32
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/types.d.cts +24 -2
- package/dist/cjs/utils.cjs +9 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/collection/changes.d.ts +1 -1
- package/dist/esm/collection/changes.js +18 -1
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/index.d.ts +9 -6
- package/dist/esm/collection/index.js +8 -5
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +13 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/query/builder/index.d.ts +2 -1
- package/dist/esm/query/builder/index.js +13 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/index.d.ts +1 -1
- package/dist/esm/query/live/collection-config-builder.js +10 -1
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.js +41 -32
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/types.d.ts +24 -2
- package/dist/esm/utils.js +9 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/collection/changes.ts +29 -2
- package/src/collection/index.ts +9 -6
- package/src/errors.ts +13 -0
- package/src/query/builder/index.ts +27 -0
- package/src/query/index.ts +2 -0
- package/src/query/live/collection-config-builder.ts +23 -2
- package/src/query/live/collection-subscriber.ts +60 -41
- package/src/types.ts +28 -5
- package/src/utils.ts +20 -0
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\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\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\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\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\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\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\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":"AA4BO,SAAS,WAAW,GAAQ,GAAiB;AAClD,SAAO,mBAAmB,GAAG,GAAG,oBAAI,KAAK;AAC3C;AAKA,SAAS,mBACP,GACA,GACA,SACS;AAET,MAAI,MAAM,EAAG,QAAO;AAGpB,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO;AAGnC,MAAI,OAAO,MAAM,OAAO,EAAG,QAAO;AAGlC,MAAI,aAAa,MAAM;AACrB,QAAI,EAAE,aAAa,MAAO,QAAO;AACjC,WAAO,EAAE,cAAc,EAAE,QAAA;AAAA,EAC3B;
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"description": "A reactive client store for building super fast apps on sync",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"src"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@standard-schema/spec": "^1.
|
|
41
|
-
"@tanstack/pacer-lite": "^0.
|
|
40
|
+
"@standard-schema/spec": "^1.1.0",
|
|
41
|
+
"@tanstack/pacer-lite": "^0.2.0",
|
|
42
42
|
"@tanstack/db-ivm": "0.1.14"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@tanstack/config": "^0.22.2",
|
|
49
49
|
"@vitest/coverage-istanbul": "^3.2.4",
|
|
50
|
-
"arktype": "^2.1.
|
|
50
|
+
"arktype": "^2.1.29",
|
|
51
51
|
"mitt": "^3.0.1",
|
|
52
52
|
"superjson": "^2.2.6",
|
|
53
53
|
"temporal-polyfill": "^0.3.0"
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { NegativeActiveSubscribersError } from '../errors'
|
|
2
|
+
import {
|
|
3
|
+
createSingleRowRefProxy,
|
|
4
|
+
toExpression,
|
|
5
|
+
} from '../query/builder/ref-proxy.js'
|
|
2
6
|
import { CollectionSubscription } from './subscription.js'
|
|
3
7
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
4
8
|
import type { ChangeMessage, SubscribeChangesOptions } from '../types'
|
|
@@ -94,19 +98,42 @@ export class CollectionChangesManager<
|
|
|
94
98
|
*/
|
|
95
99
|
public subscribeChanges(
|
|
96
100
|
callback: (changes: Array<ChangeMessage<TOutput>>) => void,
|
|
97
|
-
options: SubscribeChangesOptions = {},
|
|
101
|
+
options: SubscribeChangesOptions<TOutput> = {},
|
|
98
102
|
): CollectionSubscription {
|
|
99
103
|
// Start sync and track subscriber
|
|
100
104
|
this.addSubscriber()
|
|
101
105
|
|
|
106
|
+
// Compile where callback to whereExpression if provided
|
|
107
|
+
if (options.where && options.whereExpression) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Cannot specify both 'where' and 'whereExpression' options. Use one or the other.`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { where, ...opts } = options
|
|
114
|
+
let whereExpression = opts.whereExpression
|
|
115
|
+
if (where) {
|
|
116
|
+
const proxy = createSingleRowRefProxy<TOutput>()
|
|
117
|
+
const result = where(proxy)
|
|
118
|
+
whereExpression = toExpression(result)
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
const subscription = new CollectionSubscription(this.collection, callback, {
|
|
103
|
-
...
|
|
122
|
+
...opts,
|
|
123
|
+
whereExpression,
|
|
104
124
|
onUnsubscribe: () => {
|
|
105
125
|
this.removeSubscriber()
|
|
106
126
|
this.changeSubscriptions.delete(subscription)
|
|
107
127
|
},
|
|
108
128
|
})
|
|
109
129
|
|
|
130
|
+
// Register status listener BEFORE requesting snapshot to avoid race condition.
|
|
131
|
+
// This ensures the listener catches all status transitions, even if the
|
|
132
|
+
// loadSubset promise resolves synchronously or very quickly.
|
|
133
|
+
if (options.onStatusChange) {
|
|
134
|
+
subscription.on(`status:change`, options.onStatusChange)
|
|
135
|
+
}
|
|
136
|
+
|
|
110
137
|
if (options.includeInitialState) {
|
|
111
138
|
subscription.requestSnapshot({ trackLoadSubsetPromise: false })
|
|
112
139
|
} else if (options.includeInitialState === false) {
|
package/src/collection/index.ts
CHANGED
|
@@ -849,26 +849,29 @@ export class CollectionImpl<
|
|
|
849
849
|
* }, { includeInitialState: true })
|
|
850
850
|
*
|
|
851
851
|
* @example
|
|
852
|
-
* // Subscribe only to changes matching a condition
|
|
852
|
+
* // Subscribe only to changes matching a condition using where callback
|
|
853
|
+
* import { eq } from "@tanstack/db"
|
|
854
|
+
*
|
|
853
855
|
* const subscription = collection.subscribeChanges((changes) => {
|
|
854
856
|
* updateUI(changes)
|
|
855
857
|
* }, {
|
|
856
858
|
* includeInitialState: true,
|
|
857
|
-
* where: (row) => row.status
|
|
859
|
+
* where: (row) => eq(row.status, "active")
|
|
858
860
|
* })
|
|
859
861
|
*
|
|
860
862
|
* @example
|
|
861
|
-
* //
|
|
863
|
+
* // Using multiple conditions with and()
|
|
864
|
+
* import { and, eq, gt } from "@tanstack/db"
|
|
865
|
+
*
|
|
862
866
|
* const subscription = collection.subscribeChanges((changes) => {
|
|
863
867
|
* updateUI(changes)
|
|
864
868
|
* }, {
|
|
865
|
-
*
|
|
866
|
-
* whereExpression: eq(row.status, 'active')
|
|
869
|
+
* where: (row) => and(eq(row.status, "active"), gt(row.priority, 5))
|
|
867
870
|
* })
|
|
868
871
|
*/
|
|
869
872
|
public subscribeChanges(
|
|
870
873
|
callback: (changes: Array<ChangeMessage<TOutput>>) => void,
|
|
871
|
-
options: SubscribeChangesOptions = {},
|
|
874
|
+
options: SubscribeChangesOptions<TOutput> = {},
|
|
872
875
|
): CollectionSubscription {
|
|
873
876
|
return this._changes.subscribeChanges(callback, options)
|
|
874
877
|
}
|
package/src/errors.ts
CHANGED
|
@@ -390,6 +390,19 @@ export class QueryMustHaveFromClauseError extends QueryBuilderError {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
export class InvalidWhereExpressionError extends QueryBuilderError {
|
|
394
|
+
constructor(valueType: string) {
|
|
395
|
+
super(
|
|
396
|
+
`Invalid where() expression: Expected a query expression, but received a ${valueType}. ` +
|
|
397
|
+
`This usually happens when using JavaScript's comparison operators (===, !==, <, >, etc.) directly. ` +
|
|
398
|
+
`Instead, use the query builder functions:\n\n` +
|
|
399
|
+
` ❌ .where(({ user }) => user.id === 'abc')\n` +
|
|
400
|
+
` ✅ .where(({ user }) => eq(user.id, 'abc'))\n\n` +
|
|
401
|
+
`Available comparison functions: eq, gt, gte, lt, lte, and, or, not, like, ilike, isNull, isUndefined`,
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
393
406
|
// Query Compilation Errors
|
|
394
407
|
export class QueryCompilationError extends TanStackDBError {
|
|
395
408
|
constructor(message: string) {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
InvalidSourceError,
|
|
13
13
|
InvalidSourceTypeError,
|
|
14
|
+
InvalidWhereExpressionError,
|
|
14
15
|
JoinConditionMustBeEqualityError,
|
|
15
16
|
OnlyOneSourceAllowedError,
|
|
16
17
|
QueryMustHaveFromClauseError,
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
import type {
|
|
30
31
|
CompareOptions,
|
|
31
32
|
Context,
|
|
33
|
+
GetResult,
|
|
32
34
|
GroupByCallback,
|
|
33
35
|
JoinOnCallback,
|
|
34
36
|
MergeContextForJoinCallback,
|
|
@@ -361,6 +363,13 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
|
|
|
361
363
|
const refProxy = createRefProxy(aliases) as RefsForContext<TContext>
|
|
362
364
|
const expression = callback(refProxy)
|
|
363
365
|
|
|
366
|
+
// Validate that the callback returned a valid expression
|
|
367
|
+
// This catches common mistakes like using JavaScript comparison operators (===, !==, etc.)
|
|
368
|
+
// which return boolean primitives instead of expression objects
|
|
369
|
+
if (!isExpressionLike(expression)) {
|
|
370
|
+
throw new InvalidWhereExpressionError(getValueTypeName(expression))
|
|
371
|
+
}
|
|
372
|
+
|
|
364
373
|
const existingWhere = this.query.where || []
|
|
365
374
|
|
|
366
375
|
return new BaseQueryBuilder({
|
|
@@ -402,6 +411,13 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
|
|
|
402
411
|
const refProxy = createRefProxy(aliases) as RefsForContext<TContext>
|
|
403
412
|
const expression = callback(refProxy)
|
|
404
413
|
|
|
414
|
+
// Validate that the callback returned a valid expression
|
|
415
|
+
// This catches common mistakes like using JavaScript comparison operators (===, !==, etc.)
|
|
416
|
+
// which return boolean primitives instead of expression objects
|
|
417
|
+
if (!isExpressionLike(expression)) {
|
|
418
|
+
throw new InvalidWhereExpressionError(getValueTypeName(expression))
|
|
419
|
+
}
|
|
420
|
+
|
|
405
421
|
const existingHaving = this.query.having || []
|
|
406
422
|
|
|
407
423
|
return new BaseQueryBuilder({
|
|
@@ -789,6 +805,14 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
|
|
|
789
805
|
}
|
|
790
806
|
}
|
|
791
807
|
|
|
808
|
+
// Helper to get a descriptive type name for error messages
|
|
809
|
+
function getValueTypeName(value: unknown): string {
|
|
810
|
+
if (value === null) return `null`
|
|
811
|
+
if (value === undefined) return `undefined`
|
|
812
|
+
if (typeof value === `object`) return `object`
|
|
813
|
+
return typeof value
|
|
814
|
+
}
|
|
815
|
+
|
|
792
816
|
// Helper to ensure we have a BasicExpression/Aggregate for a value
|
|
793
817
|
function toExpr(value: any): BasicExpression | Aggregate {
|
|
794
818
|
if (value === undefined) return toExpression(null)
|
|
@@ -864,6 +888,9 @@ export type ExtractContext<T> =
|
|
|
864
888
|
? TContext
|
|
865
889
|
: never
|
|
866
890
|
|
|
891
|
+
// Helper type to extract the result type from a QueryBuilder (similar to Zod's z.infer)
|
|
892
|
+
export type QueryResult<T> = GetResult<ExtractContext<T>>
|
|
893
|
+
|
|
867
894
|
// Export the types from types.ts for convenience
|
|
868
895
|
export type {
|
|
869
896
|
Context,
|
package/src/query/index.ts
CHANGED
|
@@ -566,6 +566,21 @@ export class CollectionConfigBuilder<
|
|
|
566
566
|
},
|
|
567
567
|
)
|
|
568
568
|
|
|
569
|
+
// Listen for loadingSubset changes on the live query collection BEFORE subscribing.
|
|
570
|
+
// This ensures we don't miss the event if subset loading completes synchronously.
|
|
571
|
+
// When isLoadingSubset becomes false, we may need to mark the collection as ready
|
|
572
|
+
// (if all source collections are already ready but we were waiting for subset load to complete)
|
|
573
|
+
const loadingSubsetUnsubscribe = config.collection.on(
|
|
574
|
+
`loadingSubset:change`,
|
|
575
|
+
(event) => {
|
|
576
|
+
if (!event.isLoadingSubset) {
|
|
577
|
+
// Subset loading finished, check if we can now mark ready
|
|
578
|
+
this.updateLiveQueryStatus(config)
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
syncState.unsubscribeCallbacks.add(loadingSubsetUnsubscribe)
|
|
583
|
+
|
|
569
584
|
const loadSubsetDataCallbacks = this.subscribeToAllCollections(
|
|
570
585
|
config,
|
|
571
586
|
fullSyncState,
|
|
@@ -793,8 +808,14 @@ export class CollectionConfigBuilder<
|
|
|
793
808
|
return
|
|
794
809
|
}
|
|
795
810
|
|
|
796
|
-
// Mark ready when all source collections are ready
|
|
797
|
-
|
|
811
|
+
// Mark ready when all source collections are ready AND
|
|
812
|
+
// the live query collection is not loading subset data.
|
|
813
|
+
// This prevents marking the live query ready before its data is loaded
|
|
814
|
+
// (fixes issue where useLiveQuery returns isReady=true with empty data)
|
|
815
|
+
if (
|
|
816
|
+
this.allCollectionsReady() &&
|
|
817
|
+
!this.liveQueryCollection?.isLoadingSubset
|
|
818
|
+
) {
|
|
798
819
|
markReady()
|
|
799
820
|
}
|
|
800
821
|
}
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
} from '../compiler/expressions.js'
|
|
6
6
|
import type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'
|
|
7
7
|
import type { Collection } from '../../collection/index.js'
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ChangeMessage,
|
|
10
|
+
SubscriptionStatusChangeEvent,
|
|
11
|
+
} from '../../types.js'
|
|
9
12
|
import type { Context, GetResult } from '../builder/types.js'
|
|
10
13
|
import type { BasicExpression } from '../ir.js'
|
|
11
14
|
import type { OrderByOptimizationInfo } from '../compiler/order-by.js'
|
|
@@ -53,26 +56,10 @@ export class CollectionSubscriber<
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {
|
|
56
|
-
let subscription: CollectionSubscription
|
|
57
59
|
const orderByInfo = this.getOrderByInfo()
|
|
58
|
-
if (orderByInfo) {
|
|
59
|
-
subscription = this.subscribeToOrderedChanges(
|
|
60
|
-
whereExpression,
|
|
61
|
-
orderByInfo,
|
|
62
|
-
)
|
|
63
|
-
} else {
|
|
64
|
-
// If the source alias is lazy then we should not include the initial state
|
|
65
|
-
const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(
|
|
66
|
-
this.alias,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
subscription = this.subscribeToMatchingChanges(
|
|
70
|
-
whereExpression,
|
|
71
|
-
includeInitialState,
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
60
|
|
|
75
|
-
|
|
61
|
+
// Track load promises using subscription from the event (avoids circular dependency)
|
|
62
|
+
const trackLoadPromise = (subscription: CollectionSubscription) => {
|
|
76
63
|
// Guard against duplicate transitions
|
|
77
64
|
if (!this.subscriptionLoadingPromises.has(subscription)) {
|
|
78
65
|
let resolve: () => void
|
|
@@ -89,16 +76,12 @@ export class CollectionSubscriber<
|
|
|
89
76
|
}
|
|
90
77
|
}
|
|
91
78
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Subscribe to subscription status changes to propagate loading state
|
|
99
|
-
const statusUnsubscribe = subscription.on(`status:change`, (event) => {
|
|
79
|
+
// Status change handler - passed to subscribeChanges so it's registered
|
|
80
|
+
// BEFORE any snapshot is requested, preventing race conditions
|
|
81
|
+
const onStatusChange = (event: SubscriptionStatusChangeEvent) => {
|
|
82
|
+
const subscription = event.subscription as CollectionSubscription
|
|
100
83
|
if (event.status === `loadingSubset`) {
|
|
101
|
-
trackLoadPromise()
|
|
84
|
+
trackLoadPromise(subscription)
|
|
102
85
|
} else {
|
|
103
86
|
// status is 'ready'
|
|
104
87
|
const deferred = this.subscriptionLoadingPromises.get(subscription)
|
|
@@ -108,7 +91,34 @@ export class CollectionSubscriber<
|
|
|
108
91
|
deferred.resolve()
|
|
109
92
|
}
|
|
110
93
|
}
|
|
111
|
-
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Create subscription with onStatusChange - listener is registered before any async work
|
|
97
|
+
let subscription: CollectionSubscription
|
|
98
|
+
if (orderByInfo) {
|
|
99
|
+
subscription = this.subscribeToOrderedChanges(
|
|
100
|
+
whereExpression,
|
|
101
|
+
orderByInfo,
|
|
102
|
+
onStatusChange,
|
|
103
|
+
)
|
|
104
|
+
} else {
|
|
105
|
+
// If the source alias is lazy then we should not include the initial state
|
|
106
|
+
const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(
|
|
107
|
+
this.alias,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
subscription = this.subscribeToMatchingChanges(
|
|
111
|
+
whereExpression,
|
|
112
|
+
includeInitialState,
|
|
113
|
+
onStatusChange,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check current status after subscribing - if status is 'loadingSubset', track it.
|
|
118
|
+
// The onStatusChange listener will catch the transition to 'ready'.
|
|
119
|
+
if (subscription.status === `loadingSubset`) {
|
|
120
|
+
trackLoadPromise(subscription)
|
|
121
|
+
}
|
|
112
122
|
|
|
113
123
|
const unsubscribe = () => {
|
|
114
124
|
// If subscription has a pending promise, resolve it before unsubscribing
|
|
@@ -119,7 +129,6 @@ export class CollectionSubscriber<
|
|
|
119
129
|
deferred.resolve()
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
statusUnsubscribe()
|
|
123
132
|
subscription.unsubscribe()
|
|
124
133
|
}
|
|
125
134
|
// currentSyncState is always defined when subscribe() is called
|
|
@@ -179,22 +188,22 @@ export class CollectionSubscriber<
|
|
|
179
188
|
|
|
180
189
|
private subscribeToMatchingChanges(
|
|
181
190
|
whereExpression: BasicExpression<boolean> | undefined,
|
|
182
|
-
includeInitialState: boolean
|
|
183
|
-
|
|
191
|
+
includeInitialState: boolean,
|
|
192
|
+
onStatusChange: (event: SubscriptionStatusChangeEvent) => void,
|
|
193
|
+
): CollectionSubscription {
|
|
184
194
|
const sendChanges = (
|
|
185
195
|
changes: Array<ChangeMessage<any, string | number>>,
|
|
186
196
|
) => {
|
|
187
197
|
this.sendChangesToPipeline(changes)
|
|
188
198
|
}
|
|
189
199
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// If we pass `false`, changes.ts would call markAllStateAsSeen() which
|
|
194
|
-
// disables filtering - but internal subscriptions still need filtering.
|
|
200
|
+
// Create subscription with onStatusChange - listener is registered before snapshot
|
|
201
|
+
// Note: For non-ordered queries (no limit/offset), we use trackLoadSubsetPromise: false
|
|
202
|
+
// which is the default behavior in subscribeChanges
|
|
195
203
|
const subscription = this.collection.subscribeChanges(sendChanges, {
|
|
196
204
|
...(includeInitialState && { includeInitialState }),
|
|
197
205
|
whereExpression,
|
|
206
|
+
onStatusChange,
|
|
198
207
|
})
|
|
199
208
|
|
|
200
209
|
return subscription
|
|
@@ -203,22 +212,31 @@ export class CollectionSubscriber<
|
|
|
203
212
|
private subscribeToOrderedChanges(
|
|
204
213
|
whereExpression: BasicExpression<boolean> | undefined,
|
|
205
214
|
orderByInfo: OrderByOptimizationInfo,
|
|
206
|
-
|
|
215
|
+
onStatusChange: (event: SubscriptionStatusChangeEvent) => void,
|
|
216
|
+
): CollectionSubscription {
|
|
207
217
|
const { orderBy, offset, limit, index } = orderByInfo
|
|
208
218
|
|
|
219
|
+
// Use a holder to forward-reference subscription in the callback
|
|
220
|
+
const subscriptionHolder: { current?: CollectionSubscription } = {}
|
|
221
|
+
|
|
209
222
|
const sendChangesInRange = (
|
|
210
223
|
changes: Iterable<ChangeMessage<any, string | number>>,
|
|
211
224
|
) => {
|
|
212
225
|
// Split live updates into a delete of the old value and an insert of the new value
|
|
213
226
|
const splittedChanges = splitUpdates(changes)
|
|
214
|
-
this.sendChangesToPipelineWithTracking(
|
|
227
|
+
this.sendChangesToPipelineWithTracking(
|
|
228
|
+
splittedChanges,
|
|
229
|
+
subscriptionHolder.current!,
|
|
230
|
+
)
|
|
215
231
|
}
|
|
216
232
|
|
|
217
|
-
// Subscribe to changes
|
|
218
|
-
// values
|
|
233
|
+
// Subscribe to changes with onStatusChange - listener is registered before any snapshot
|
|
234
|
+
// values bigger than what we've sent don't need to be sent because they can't affect the topK
|
|
219
235
|
const subscription = this.collection.subscribeChanges(sendChangesInRange, {
|
|
220
236
|
whereExpression,
|
|
237
|
+
onStatusChange,
|
|
221
238
|
})
|
|
239
|
+
subscriptionHolder.current = subscription
|
|
222
240
|
|
|
223
241
|
// Listen for truncate events to reset cursor tracking state and sentToD2Keys
|
|
224
242
|
// This ensures that after a must-refetch/truncate, we don't use stale cursor data
|
|
@@ -236,6 +254,7 @@ export class CollectionSubscriber<
|
|
|
236
254
|
// Normalize the orderBy clauses such that the references are relative to the collection
|
|
237
255
|
const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
|
|
238
256
|
|
|
257
|
+
// Trigger the snapshot request - onStatusChange listener is already registered
|
|
239
258
|
if (index) {
|
|
240
259
|
// We have an index on the first orderBy column - use lazy loading optimization
|
|
241
260
|
// This works for both single-column and multi-column orderBy:
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
|
4
4
|
import type { Transaction } from './transactions'
|
|
5
5
|
import type { BasicExpression, OrderBy } from './query/ir.js'
|
|
6
6
|
import type { EventEmitter } from './event-emitter.js'
|
|
7
|
+
import type { SingleRowRefProxy } from './query/builder/ref-proxy.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Interface for a collection-like object that provides the necessary methods
|
|
@@ -775,17 +776,39 @@ export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>
|
|
|
775
776
|
/**
|
|
776
777
|
* Options for subscribing to collection changes
|
|
777
778
|
*/
|
|
778
|
-
export interface SubscribeChangesOptions
|
|
779
|
+
export interface SubscribeChangesOptions<
|
|
780
|
+
T extends object = Record<string, unknown>,
|
|
781
|
+
> {
|
|
779
782
|
/** Whether to include the current state as initial changes */
|
|
780
783
|
includeInitialState?: boolean
|
|
784
|
+
/**
|
|
785
|
+
* Callback function for filtering changes using a row proxy.
|
|
786
|
+
* The callback receives a proxy object that records property access,
|
|
787
|
+
* allowing you to use query builder functions like `eq`, `gt`, etc.
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* ```ts
|
|
791
|
+
* import { eq } from "@tanstack/db"
|
|
792
|
+
*
|
|
793
|
+
* collection.subscribeChanges(callback, {
|
|
794
|
+
* where: (row) => eq(row.status, "active")
|
|
795
|
+
* })
|
|
796
|
+
* ```
|
|
797
|
+
*/
|
|
798
|
+
where?: (row: SingleRowRefProxy<T>) => any
|
|
781
799
|
/** Pre-compiled expression for filtering changes */
|
|
782
800
|
whereExpression?: BasicExpression<boolean>
|
|
801
|
+
/**
|
|
802
|
+
* Listener for subscription status changes.
|
|
803
|
+
* Registered BEFORE any snapshot is requested, ensuring no status transitions are missed.
|
|
804
|
+
* @internal
|
|
805
|
+
*/
|
|
806
|
+
onStatusChange?: (event: SubscriptionStatusChangeEvent) => void
|
|
783
807
|
}
|
|
784
808
|
|
|
785
|
-
export interface SubscribeChangesSnapshotOptions
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
> {
|
|
809
|
+
export interface SubscribeChangesSnapshotOptions<
|
|
810
|
+
T extends object = Record<string, unknown>,
|
|
811
|
+
> extends Omit<SubscribeChangesOptions<T>, `includeInitialState`> {
|
|
789
812
|
orderBy?: OrderBy
|
|
790
813
|
limit?: number
|
|
791
814
|
}
|
package/src/utils.ts
CHANGED
|
@@ -52,12 +52,16 @@ function deepEqualsInternal(
|
|
|
52
52
|
if (!(b instanceof Date)) return false
|
|
53
53
|
return a.getTime() === b.getTime()
|
|
54
54
|
}
|
|
55
|
+
// Symmetric check: if b is Date but a is not, they're not equal
|
|
56
|
+
if (b instanceof Date) return false
|
|
55
57
|
|
|
56
58
|
// Handle RegExp objects
|
|
57
59
|
if (a instanceof RegExp) {
|
|
58
60
|
if (!(b instanceof RegExp)) return false
|
|
59
61
|
return a.source === b.source && a.flags === b.flags
|
|
60
62
|
}
|
|
63
|
+
// Symmetric check: if b is RegExp but a is not, they're not equal
|
|
64
|
+
if (b instanceof RegExp) return false
|
|
61
65
|
|
|
62
66
|
// Handle Map objects - only if both are Maps
|
|
63
67
|
if (a instanceof Map) {
|
|
@@ -78,6 +82,8 @@ function deepEqualsInternal(
|
|
|
78
82
|
visited.delete(a)
|
|
79
83
|
return result
|
|
80
84
|
}
|
|
85
|
+
// Symmetric check: if b is Map but a is not, they're not equal
|
|
86
|
+
if (b instanceof Map) return false
|
|
81
87
|
|
|
82
88
|
// Handle Set objects - only if both are Sets
|
|
83
89
|
if (a instanceof Set) {
|
|
@@ -106,6 +112,8 @@ function deepEqualsInternal(
|
|
|
106
112
|
visited.delete(a)
|
|
107
113
|
return result
|
|
108
114
|
}
|
|
115
|
+
// Symmetric check: if b is Set but a is not, they're not equal
|
|
116
|
+
if (b instanceof Set) return false
|
|
109
117
|
|
|
110
118
|
// Handle TypedArrays
|
|
111
119
|
if (
|
|
@@ -124,6 +132,14 @@ function deepEqualsInternal(
|
|
|
124
132
|
|
|
125
133
|
return true
|
|
126
134
|
}
|
|
135
|
+
// Symmetric check: if b is TypedArray but a is not, they're not equal
|
|
136
|
+
if (
|
|
137
|
+
ArrayBuffer.isView(b) &&
|
|
138
|
+
!(b instanceof DataView) &&
|
|
139
|
+
!ArrayBuffer.isView(a)
|
|
140
|
+
) {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
127
143
|
|
|
128
144
|
// Handle Temporal objects
|
|
129
145
|
// Check if both are Temporal objects of the same type
|
|
@@ -142,6 +158,8 @@ function deepEqualsInternal(
|
|
|
142
158
|
// Fallback to toString comparison for other types
|
|
143
159
|
return a.toString() === b.toString()
|
|
144
160
|
}
|
|
161
|
+
// Symmetric check: if b is Temporal but a is not, they're not equal
|
|
162
|
+
if (isTemporal(b)) return false
|
|
145
163
|
|
|
146
164
|
// Handle arrays
|
|
147
165
|
if (Array.isArray(a)) {
|
|
@@ -159,6 +177,8 @@ function deepEqualsInternal(
|
|
|
159
177
|
visited.delete(a)
|
|
160
178
|
return result
|
|
161
179
|
}
|
|
180
|
+
// Symmetric check: if b is array but a is not, they're not equal
|
|
181
|
+
if (Array.isArray(b)) return false
|
|
162
182
|
|
|
163
183
|
// Handle objects
|
|
164
184
|
if (typeof a === `object`) {
|