@tanstack/db 0.5.15 → 0.5.17
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 +15 -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/collection/subscription.cjs +4 -1
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- 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/types.d.cts +18 -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 +15 -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/collection/subscription.js +5 -2
- package/dist/esm/collection/subscription.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/types.d.ts +18 -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 +22 -2
- package/src/collection/index.ts +9 -6
- package/src/collection/subscription.ts +12 -2
- package/src/errors.ts +13 -0
- package/src/query/builder/index.ts +27 -0
- package/src/query/index.ts +2 -0
- package/src/types.ts +22 -5
- package/src/utils.ts +20 -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,13 +98,29 @@ 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)
|
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
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ensureIndexForExpression } from '../indexes/auto-index.js'
|
|
2
2
|
import { and, eq, gte, lt } from '../query/builder/functions.js'
|
|
3
|
-
import { Value } from '../query/ir.js'
|
|
3
|
+
import { PropRef, Value } from '../query/ir.js'
|
|
4
4
|
import { EventEmitter } from '../event-emitter.js'
|
|
5
|
+
import { compileExpression } from '../query/compiler/evaluators.js'
|
|
5
6
|
import { buildCursor } from '../utils/cursor.js'
|
|
6
7
|
import {
|
|
7
8
|
createFilterFunctionFromExpression,
|
|
@@ -494,6 +495,13 @@ export class CollectionSubscription
|
|
|
494
495
|
const valuesNeeded = () => Math.max(limit - changes.length, 0)
|
|
495
496
|
const collectionExhausted = () => keys.length === 0
|
|
496
497
|
|
|
498
|
+
// Create a value extractor for the orderBy field to properly track the biggest indexed value
|
|
499
|
+
const orderByExpression = orderBy[0]!.expression
|
|
500
|
+
const valueExtractor =
|
|
501
|
+
orderByExpression.type === `ref`
|
|
502
|
+
? compileExpression(new PropRef(orderByExpression.path), true)
|
|
503
|
+
: null
|
|
504
|
+
|
|
497
505
|
while (valuesNeeded() > 0 && !collectionExhausted()) {
|
|
498
506
|
const insertedKeys = new Set<string | number>() // Track keys we add to `changes` in this iteration
|
|
499
507
|
|
|
@@ -504,7 +512,9 @@ export class CollectionSubscription
|
|
|
504
512
|
key,
|
|
505
513
|
value,
|
|
506
514
|
})
|
|
507
|
-
|
|
515
|
+
// Extract the indexed value (e.g., salary) from the row, not the full row
|
|
516
|
+
// This is needed for index.take() to work correctly with the BTree comparator
|
|
517
|
+
biggestObservedValue = valueExtractor ? valueExtractor(value) : value
|
|
508
518
|
insertedKeys.add(key) // Track this key
|
|
509
519
|
}
|
|
510
520
|
|
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
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,33 @@ 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>
|
|
783
801
|
}
|
|
784
802
|
|
|
785
|
-
export interface SubscribeChangesSnapshotOptions
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
> {
|
|
803
|
+
export interface SubscribeChangesSnapshotOptions<
|
|
804
|
+
T extends object = Record<string, unknown>,
|
|
805
|
+
> extends Omit<SubscribeChangesOptions<T>, `includeInitialState`> {
|
|
789
806
|
orderBy?: OrderBy
|
|
790
807
|
limit?: number
|
|
791
808
|
}
|
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`) {
|