@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.
Files changed (53) hide show
  1. package/dist/cjs/collection/changes.cjs +18 -1
  2. package/dist/cjs/collection/changes.cjs.map +1 -1
  3. package/dist/cjs/collection/changes.d.cts +1 -1
  4. package/dist/cjs/collection/index.cjs +8 -5
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +9 -6
  7. package/dist/cjs/errors.cjs +13 -0
  8. package/dist/cjs/errors.cjs.map +1 -1
  9. package/dist/cjs/errors.d.cts +3 -0
  10. package/dist/cjs/index.cjs +1 -0
  11. package/dist/cjs/index.cjs.map +1 -1
  12. package/dist/cjs/query/builder/index.cjs +12 -0
  13. package/dist/cjs/query/builder/index.cjs.map +1 -1
  14. package/dist/cjs/query/builder/index.d.cts +2 -1
  15. package/dist/cjs/query/index.d.cts +1 -1
  16. package/dist/cjs/query/live/collection-config-builder.cjs +10 -1
  17. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  18. package/dist/cjs/query/live/collection-subscriber.cjs +41 -32
  19. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  20. package/dist/cjs/types.d.cts +24 -2
  21. package/dist/cjs/utils.cjs +9 -0
  22. package/dist/cjs/utils.cjs.map +1 -1
  23. package/dist/esm/collection/changes.d.ts +1 -1
  24. package/dist/esm/collection/changes.js +18 -1
  25. package/dist/esm/collection/changes.js.map +1 -1
  26. package/dist/esm/collection/index.d.ts +9 -6
  27. package/dist/esm/collection/index.js +8 -5
  28. package/dist/esm/collection/index.js.map +1 -1
  29. package/dist/esm/errors.d.ts +3 -0
  30. package/dist/esm/errors.js +13 -0
  31. package/dist/esm/errors.js.map +1 -1
  32. package/dist/esm/index.js +2 -1
  33. package/dist/esm/query/builder/index.d.ts +2 -1
  34. package/dist/esm/query/builder/index.js +13 -1
  35. package/dist/esm/query/builder/index.js.map +1 -1
  36. package/dist/esm/query/index.d.ts +1 -1
  37. package/dist/esm/query/live/collection-config-builder.js +10 -1
  38. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  39. package/dist/esm/query/live/collection-subscriber.js +41 -32
  40. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  41. package/dist/esm/types.d.ts +24 -2
  42. package/dist/esm/utils.js +9 -0
  43. package/dist/esm/utils.js.map +1 -1
  44. package/package.json +4 -4
  45. package/src/collection/changes.ts +29 -2
  46. package/src/collection/index.ts +9 -6
  47. package/src/errors.ts +13 -0
  48. package/src/query/builder/index.ts +27 -0
  49. package/src/query/index.ts +2 -0
  50. package/src/query/live/collection-config-builder.ts +23 -2
  51. package/src/query/live/collection-subscriber.ts +60 -41
  52. package/src/types.ts +28 -5
  53. package/src/utils.ts +20 -0
@@ -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":"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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 = 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.16",
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.0.0",
41
- "@tanstack/pacer-lite": "^0.1.1",
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.28",
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
- ...options,
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) {
@@ -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 === 'active'
859
+ * where: (row) => eq(row.status, "active")
858
860
  * })
859
861
  *
860
862
  * @example
861
- * // Subscribe using a pre-compiled expression
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
- * includeInitialState: true,
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,
@@ -10,6 +10,8 @@ export {
10
10
  type Source,
11
11
  type GetResult,
12
12
  type InferResultType,
13
+ type ExtractContext,
14
+ type QueryResult,
13
15
  } from './builder/index.js'
14
16
 
15
17
  // Expression functions exports
@@ -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
- if (this.allCollectionsReady()) {
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 { ChangeMessage } from '../../types.js'
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
- const trackLoadPromise = () => {
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
- // It can be that we are not yet subscribed when the first `loadSubset` call happens (i.e. the initial query).
93
- // So we also check the status here and if it's `loadingSubset` then we track the load promise
94
- if (subscription.status === `loadingSubset`) {
95
- trackLoadPromise()
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 = false,
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
- // Only pass includeInitialState when true. When it's false, we leave it
191
- // undefined so that user subscriptions with explicit `includeInitialState: false`
192
- // can be distinguished from internal lazy-loading subscriptions.
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(splittedChanges, subscription)
227
+ this.sendChangesToPipelineWithTracking(
228
+ splittedChanges,
229
+ subscriptionHolder.current!,
230
+ )
215
231
  }
216
232
 
217
- // Subscribe to changes and only send changes that are smaller than the biggest value we've sent so far
218
- // values that are bigger don't need to be sent because they can't affect the topK
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 extends Omit<
786
- SubscribeChangesOptions,
787
- `includeInitialState`
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`) {