Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
168f698
fix(db): show loading status during initial loadSubset for on-demand …
claude Dec 3, 2025
060cf1e
chore: add changeset for loading status fix
claude Dec 3, 2025
6c6968c
test(electric-db-collection): wait for loadSubset in live query test
claude Dec 3, 2025
3c50df6
fix(db): track loadSubset for on-demand sources with trackLoadSubsetP…
claude Dec 3, 2025
9b1ab9c
Merge origin/main into claude/fix-livequery-loading-status-01TNsEPERm…
claude Dec 16, 2025
97adb37
ci: apply automated fixes
autofix-ci[bot] Dec 16, 2025
13b586a
Merge remote-tracking branch 'origin/main' into claude/fix-livequery-…
claude Dec 16, 2025
b136dde
Merge branch 'claude/fix-livequery-loading-status-01TNsEPERmfs3ub3e9i…
claude Dec 16, 2025
a2f6206
Address reviewer feedback: lift unsubscribe to class level and add cl…
claude Dec 16, 2025
dd72367
ci: apply automated fixes
autofix-ci[bot] Dec 16, 2025
0e0ad9c
chore: fix query-db-collection version mismatch in examples
claude Dec 16, 2025
c6c2ca4
Merge branch 'claude/fix-livequery-loading-status-01TNsEPERmfs3ub3e9i…
claude Dec 16, 2025
1bdc7d0
chore: update lockfile for query-db-collection version bump
claude Dec 16, 2025
45c186e
test: add test for joined on-demand collections loading state
claude Dec 16, 2025
9c2d1e1
ci: apply automated fixes
autofix-ci[bot] Dec 16, 2025
99f9cef
Merge branch 'main' into claude/fix-livequery-loading-status-01TNsEPE…
KyleAMathews Dec 17, 2025
e27f341
Merge branch 'main' into claude/fix-livequery-loading-status-01TNsEPE…
KyleAMathews Dec 17, 2025
d2f0782
fix(query-db-collection): remove preload calls on on-demand collectio…
claude Dec 17, 2025
e33b921
Merge branch 'claude/fix-livequery-loading-status-01TNsEPERmfs3ub3e9i…
claude Dec 17, 2025
f85ffa7
fix(query-db-collection): remove source collection cleanup from after…
claude Dec 17, 2025
b72430b
fix(query-db-collection): cancel pending queries in e2e afterEach
claude Dec 17, 2025
957d826
fix(query-db-collection): use targeted query cleanup in e2e afterEach
KyleAMathews Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fix-livequery-loading-status-ondemand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/db': patch
---

fix(db): show loading status during initial loadSubset for on-demand sync

Fixed an issue where live queries using on-demand sync mode would immediately show `isLoading: false` and `status: 'ready'` even while the initial data was still being fetched. Now the live query correctly shows `isLoading: true` and `status: 'loading'` until the first `loadSubset` completes.

This ensures that UI components can properly display loading indicators while waiting for the initial data to arrive from on-demand sync sources. Subsequent `loadSubset` calls (e.g., from pagination or windowing) do not affect the ready status.
59 changes: 54 additions & 5 deletions packages/db/src/query/live/collection-config-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ export class CollectionConfigBuilder<
// Error state tracking
private isInErrorState = false

// Track whether we've already marked the live query as ready
// Used to ensure we only wait for the first loadSubset, not subsequent ones
private hasMarkedReady = false

// Track whether we've set up the loadingSubset listener
// Prevents duplicate listeners when updateLiveQueryStatus is called multiple times
private hasSetupLoadingListener = false

// Unsubscribe function for loadingSubset listener
// Registered when waiting for initial load to complete, unregistered when sync stops
private unsubscribeFromLoadingListener?: () => void

// Reference to the live query collection for error state transitions
public liveQueryCollection?: Collection<TResult, any, any>

Expand Down Expand Up @@ -611,6 +623,14 @@ export class CollectionConfigBuilder<
// The scheduler's listener Set would otherwise keep a strong reference to this builder
this.unsubscribeFromSchedulerClears?.()
this.unsubscribeFromSchedulerClears = undefined

// Unregister from loadingSubset listener to prevent memory leaks
this.unsubscribeFromLoadingListener?.()
this.unsubscribeFromLoadingListener = undefined

// Reset ready state tracking for potential restart
this.hasMarkedReady = false
this.hasSetupLoadingListener = false
}
}

Expand Down Expand Up @@ -788,15 +808,44 @@ export class CollectionConfigBuilder<
private updateLiveQueryStatus(config: SyncMethods<TResult>) {
const { markReady } = config

// Don't update status if already in error
if (this.isInErrorState) {
// Don't update status if already in error or already marked ready
if (this.isInErrorState || this.hasMarkedReady) {
return
}

// Mark ready when all source collections are ready
if (this.allCollectionsReady()) {
markReady()
// Check if all source collections are ready
if (!this.allCollectionsReady()) {
return
}

// If the live query is currently loading a subset (e.g., initial on-demand load),
// wait for it to complete before marking ready. This ensures that for on-demand
// sync mode, the live query isn't marked ready until the first data is loaded.
// We only wait for the FIRST loadSubset - subsequent loads (pagination/windowing)
// should not affect the ready status.
if (this.liveQueryCollection?.isLoadingSubset) {
// Set up a one-time listener if we haven't already
if (!this.hasSetupLoadingListener) {
this.hasSetupLoadingListener = true
this.unsubscribeFromLoadingListener = this.liveQueryCollection.on(
`loadingSubset:change`,
(event) => {
if (!event.isLoadingSubset) {
// Clean up the listener
this.unsubscribeFromLoadingListener?.()
this.unsubscribeFromLoadingListener = undefined
// Re-check and mark ready now that loading is complete
this.updateLiveQueryStatus(config)
}
},
)
}
return
}

// Mark ready when all source collections are ready and no initial loading is in progress
this.hasMarkedReady = true
markReady()
}

/**
Expand Down
30 changes: 24 additions & 6 deletions packages/db/src/query/live/collection-subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ export class CollectionSubscriber<
this.alias,
)

subscription = this.subscribeToMatchingChanges(
const result = this.subscribeToMatchingChanges(
whereExpression,
includeInitialState,
)
subscription = result.subscription
}

const trackLoadPromise = () => {
Expand All @@ -84,8 +85,10 @@ export class CollectionSubscriber<
}
}

// It can be that we are not yet subscribed when the first `loadSubset` call happens (i.e. the initial query).
// So we also check the status here and if it's `loadingSubset` then we track the load promise
// For on-demand sources with initial state, we need to track the initial loadSubset.
// The subscription status will be 'loadingSubset' if there's a pending load.
// For on-demand sources, requestSnapshot was called with trackLoadSubsetPromise: true,
// so the status should be 'loadingSubset' unless data arrived synchronously.
if (subscription.status === `loadingSubset`) {
trackLoadPromise()
}
Expand Down Expand Up @@ -155,19 +158,34 @@ export class CollectionSubscriber<
private subscribeToMatchingChanges(
whereExpression: BasicExpression<boolean> | undefined,
includeInitialState: boolean = false,
) {
): { subscription: CollectionSubscription } {
const sendChanges = (
changes: Array<ChangeMessage<any, string | number>>,
) => {
this.sendChangesToPipeline(changes)
}

// Track whether this is an on-demand source with initial state requested.
// For on-demand sync mode, we need to track the initial loadSubset promise
// so that the live query collection shows isLoading=true until data arrives.
const isOnDemandWithInitialState =
this.collection.config.syncMode === `on-demand` && includeInitialState

// For on-demand sources, we create the subscription WITHOUT includeInitialState
// and then manually call requestSnapshot with trackLoadSubsetPromise: true.
// This is because the default includeInitialState path calls requestSnapshot
// with trackLoadSubsetPromise: false, which doesn't track loading state.
const subscription = this.collection.subscribeChanges(sendChanges, {
includeInitialState,
includeInitialState: !isOnDemandWithInitialState && includeInitialState,
whereExpression,
})

return subscription
// For on-demand sources with initial state, manually request snapshot with tracking
if (isOnDemandWithInitialState) {
subscription.requestSnapshot({ trackLoadSubsetPromise: true })
}

return { subscription }
}

private subscribeToOrderedChanges(
Expand Down
Loading
Loading