@tanstack/db 0.5.17 → 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 +3 -0
- package/dist/cjs/collection/changes.cjs.map +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 +6 -0
- package/dist/esm/collection/changes.js +3 -0
- package/dist/esm/collection/changes.js.map +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 +6 -0
- package/package.json +1 -1
- package/src/collection/changes.ts +7 -0
- package/src/query/live/collection-config-builder.ts +23 -2
- package/src/query/live/collection-subscriber.ts +60 -41
- package/src/types.ts +6 -0
|
@@ -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
|
@@ -798,6 +798,12 @@ export interface SubscribeChangesOptions<
|
|
|
798
798
|
where?: (row: SingleRowRefProxy<T>) => any
|
|
799
799
|
/** Pre-compiled expression for filtering changes */
|
|
800
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
|
|
801
807
|
}
|
|
802
808
|
|
|
803
809
|
export interface SubscribeChangesSnapshotOptions<
|