Skip to content

Commit c90b4d8

Browse files
samwillistomkuehl
andauthored
Ensure liveQueryCollections are not ready until all it's source collections are ready (#390)
Co-authored-by: Tom Kühl <[email protected]>
1 parent 69a6d2d commit c90b4d8

File tree

10 files changed

+493
-50
lines changed

10 files changed

+493
-50
lines changed

.changeset/clever-ducks-strive.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@tanstack/svelte-db": patch
3+
"@tanstack/react-db": patch
4+
"@tanstack/solid-db": patch
5+
"@tanstack/vue-db": patch
6+
"@tanstack/db": patch
7+
---
8+
9+
Ensure that the ready status is correctly returned from a live query

packages/db/src/collection.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,12 @@ export class CollectionImpl<
985985
for (const listener of this.changeListeners) {
986986
listener([])
987987
}
988+
// Emit to key-specific listeners
989+
for (const [_key, keyListeners] of this.changeKeyListeners) {
990+
for (const listener of keyListeners) {
991+
listener([])
992+
}
993+
}
988994
}
989995

990996
/**

packages/db/src/query/live-query-collection.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ export function liveQueryCollectionOptions<
165165
const collections = extractCollectionsFromQuery(query)
166166

167167
const allCollectionsReady = () => {
168+
return Object.values(collections).every((collection) =>
169+
collection.isReady()
170+
)
171+
}
172+
173+
const allCollectionsReadyOrInitialCommit = () => {
168174
return Object.values(collections).every(
169175
(collection) =>
170176
collection.status === `ready` || collection.status === `initialCommit`
@@ -309,7 +315,10 @@ export function liveQueryCollectionOptions<
309315

310316
const maybeRunGraph = () => {
311317
// We only run the graph if all the collections are ready
312-
if (allCollectionsReady() && subscribedToAllCollections) {
318+
if (
319+
allCollectionsReadyOrInitialCommit() &&
320+
subscribedToAllCollections
321+
) {
313322
graph.run()
314323
// On the initial run, we may need to do an empty commit to ensure that
315324
// the collection is initialized
@@ -318,7 +327,9 @@ export function liveQueryCollectionOptions<
318327
commit()
319328
}
320329
// Mark the collection as ready after the first successful run
321-
markReady()
330+
if (allCollectionsReady()) {
331+
markReady()
332+
}
322333
}
323334
}
324335

packages/db/tests/query/live-query-collection.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,23 @@ describe(`createLiveQueryCollection`, () => {
170170
expect(liveQuery.status).toBe(`ready`)
171171
expect(liveQuery.size).toBe(0)
172172
})
173+
174+
it(`shouldn't call markReady when source collection sync doesn't call markReady`, () => {
175+
const collection = createCollection<{ id: string }>({
176+
sync: {
177+
sync({ begin, commit }) {
178+
begin()
179+
commit()
180+
},
181+
},
182+
getKey: (item) => item.id,
183+
startSync: true,
184+
})
185+
186+
const liveQuery = createLiveQueryCollection({
187+
query: (q) => q.from({ collection }),
188+
startSync: true,
189+
})
190+
expect(liveQuery.isReady()).toBe(false)
191+
})
173192
})

packages/react-db/tests/useLiveQuery.test.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,15 +1077,17 @@ describe(`Query Collections`, () => {
10771077
it(`should update isLoading when collection status changes`, async () => {
10781078
let beginFn: (() => void) | undefined
10791079
let commitFn: (() => void) | undefined
1080+
let markReadyFn: (() => void) | undefined
10801081

10811082
const collection = createCollection<Person>({
10821083
id: `status-change-has-loaded-test`,
10831084
getKey: (person: Person) => person.id,
10841085
startSync: false,
10851086
sync: {
1086-
sync: ({ begin, commit }) => {
1087+
sync: ({ begin, commit, markReady }) => {
10871088
beginFn = begin
10881089
commitFn = commit
1090+
markReadyFn = markReady
10891091
// Don't sync immediately
10901092
},
10911093
},
@@ -1116,9 +1118,10 @@ describe(`Query Collections`, () => {
11161118

11171119
// Trigger the first commit to make collection ready
11181120
act(() => {
1119-
if (beginFn && commitFn) {
1121+
if (beginFn && commitFn && markReadyFn) {
11201122
beginFn()
11211123
commitFn()
1124+
markReadyFn()
11221125
}
11231126
})
11241127

@@ -1202,8 +1205,10 @@ describe(`Query Collections`, () => {
12021205
it(`should handle isLoading with complex queries including joins`, async () => {
12031206
let personBeginFn: (() => void) | undefined
12041207
let personCommitFn: (() => void) | undefined
1208+
let personMarkReadyFn: (() => void) | undefined
12051209
let issueBeginFn: (() => void) | undefined
12061210
let issueCommitFn: (() => void) | undefined
1211+
let issueMarkReadyFn: (() => void) | undefined
12071212

12081213
const personCollection = createCollection<Person>({
12091214
id: `join-has-loaded-persons`,
@@ -1212,10 +1217,8 @@ describe(`Query Collections`, () => {
12121217
sync: {
12131218
sync: ({ begin, commit, markReady }) => {
12141219
personBeginFn = begin
1215-
personCommitFn = () => {
1216-
commit()
1217-
markReady()
1218-
}
1220+
personCommitFn = commit
1221+
personMarkReadyFn = markReady
12191222
// Don't sync immediately
12201223
},
12211224
},
@@ -1231,10 +1234,8 @@ describe(`Query Collections`, () => {
12311234
sync: {
12321235
sync: ({ begin, commit, markReady }) => {
12331236
issueBeginFn = begin
1234-
issueCommitFn = () => {
1235-
commit()
1236-
markReady()
1237-
}
1237+
issueCommitFn = commit
1238+
issueMarkReadyFn = markReady
12381239
// Don't sync immediately
12391240
},
12401241
},
@@ -1269,13 +1270,15 @@ describe(`Query Collections`, () => {
12691270

12701271
// Trigger the first commit for both collections to make them ready
12711272
act(() => {
1272-
if (personBeginFn && personCommitFn) {
1273+
if (personBeginFn && personCommitFn && personMarkReadyFn) {
12731274
personBeginFn()
12741275
personCommitFn()
1276+
personMarkReadyFn()
12751277
}
1276-
if (issueBeginFn && issueCommitFn) {
1278+
if (issueBeginFn && issueCommitFn && issueMarkReadyFn) {
12771279
issueBeginFn()
12781280
issueCommitFn()
1281+
issueMarkReadyFn()
12791282
}
12801283
})
12811284

packages/solid-db/tests/useLiveQuery.test.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,15 +1053,17 @@ describe(`Query Collections`, () => {
10531053
it(`should update isLoading when collection status changes`, async () => {
10541054
let beginFn: (() => void) | undefined
10551055
let commitFn: (() => void) | undefined
1056+
let markReadyFn: (() => void) | undefined
10561057

10571058
const collection = createCollection<Person>({
10581059
id: `status-change-has-loaded-test`,
10591060
getKey: (person: Person) => person.id,
10601061
startSync: false,
10611062
sync: {
1062-
sync: ({ begin, commit }) => {
1063+
sync: ({ begin, commit, markReady }) => {
10631064
beginFn = begin
10641065
commitFn = commit
1066+
markReadyFn = markReady
10651067
// Don't sync immediately
10661068
},
10671069
},
@@ -1089,9 +1091,10 @@ describe(`Query Collections`, () => {
10891091
collection.preload()
10901092

10911093
// Trigger the first commit to make collection ready
1092-
if (beginFn && commitFn) {
1094+
if (beginFn && commitFn && markReadyFn) {
10931095
beginFn()
10941096
commitFn()
1097+
markReadyFn()
10951098
}
10961099

10971100
// Insert data
@@ -1172,8 +1175,10 @@ describe(`Query Collections`, () => {
11721175
it(`should handle isLoading with complex queries including joins`, async () => {
11731176
let personBeginFn: (() => void) | undefined
11741177
let personCommitFn: (() => void) | undefined
1178+
let personMarkReadyFn: (() => void) | undefined
11751179
let issueBeginFn: (() => void) | undefined
11761180
let issueCommitFn: (() => void) | undefined
1181+
let issueMarkReadyFn: (() => void) | undefined
11771182

11781183
const personCollection = createCollection<Person>({
11791184
id: `join-has-loaded-persons`,
@@ -1182,10 +1187,8 @@ describe(`Query Collections`, () => {
11821187
sync: {
11831188
sync: ({ begin, commit, markReady }) => {
11841189
personBeginFn = begin
1185-
personCommitFn = () => {
1186-
commit()
1187-
markReady()
1188-
}
1190+
personCommitFn = commit
1191+
personMarkReadyFn = markReady
11891192
// Don't sync immediately
11901193
},
11911194
},
@@ -1201,10 +1204,8 @@ describe(`Query Collections`, () => {
12011204
sync: {
12021205
sync: ({ begin, commit, markReady }) => {
12031206
issueBeginFn = begin
1204-
issueCommitFn = () => {
1205-
commit()
1206-
markReady()
1207-
}
1207+
issueCommitFn = commit
1208+
issueMarkReadyFn = markReady
12081209
// Don't sync immediately
12091210
},
12101211
},
@@ -1236,13 +1237,15 @@ describe(`Query Collections`, () => {
12361237
issueCollection.preload()
12371238

12381239
// Trigger the first commit for both collections to make them ready
1239-
if (personBeginFn && personCommitFn) {
1240+
if (personBeginFn && personCommitFn && personMarkReadyFn) {
12401241
personBeginFn()
12411242
personCommitFn()
1243+
personMarkReadyFn()
12421244
}
1243-
if (issueBeginFn && issueCommitFn) {
1245+
if (issueBeginFn && issueCommitFn && issueMarkReadyFn) {
12441246
issueBeginFn()
12451247
issueCommitFn()
1248+
issueMarkReadyFn()
12461249
}
12471250

12481251
// Insert data into both collections

packages/svelte-db/src/useLiveQuery.svelte.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { untrack } from "svelte"
1+
import { flushSync, untrack } from "svelte"
22
import { createLiveQueryCollection } from "@tanstack/db"
33
import { SvelteMap } from "svelte/reactivity"
44
import type {
@@ -246,7 +246,10 @@ export function useLiveQuery(
246246

247247
if (isCollection) {
248248
// It's already a collection, ensure sync is started for Svelte helpers
249-
unwrappedParam.startSyncImmediate()
249+
// Only start sync if the collection is in idle state
250+
if (unwrappedParam.status === `idle`) {
251+
unwrappedParam.startSyncImmediate()
252+
}
250253
return unwrappedParam
251254
}
252255

@@ -312,6 +315,15 @@ export function useLiveQuery(
312315
// Initialize data array in correct order
313316
syncDataFromCollection(currentCollection)
314317

318+
// Listen for the first ready event to catch status transitions
319+
// that might not trigger change events (fixes async status transition bug)
320+
currentCollection.onFirstReady(() => {
321+
// Use flushSync to ensure Svelte reactivity updates properly
322+
flushSync(() => {
323+
status = currentCollection.status
324+
})
325+
})
326+
315327
// Subscribe to collection changes with granular updates
316328
currentUnsubscribe = currentCollection.subscribeChanges(
317329
(changes: Array<ChangeMessage<any>>) => {

0 commit comments

Comments
 (0)