Skip to content

Commit 6250a92

Browse files
samwillistomkuehl
andauthored
Fix: Live query collections stuck in initialCommit status when source collections are preloaded after creation (#395)
Co-authored-by: Tom Kühl <[email protected]>
1 parent c90b4d8 commit 6250a92

File tree

3 files changed

+105
-2
lines changed

3 files changed

+105
-2
lines changed

.changeset/fast-crabs-change.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/db": patch
3+
---
4+
5+
Ensure LiveQueryCollections are properly transitioning to ready state when source collections are preloaded after creation of the live query collection

packages/db/src/collection.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,8 @@ export class CollectionImpl<
393393
this.onFirstReadyCallbacks = []
394394
callbacks.forEach((callback) => callback())
395395

396-
// If the collection is empty when it becomes ready, emit an empty change event
397396
// to notify subscribers (like LiveQueryCollection) that the collection is ready
398-
if (this.size === 0 && this.changeListeners.size > 0) {
397+
if (this.changeListeners.size > 0) {
399398
this.emitEmptyReadyEvent()
400399
}
401400
}

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createCollection } from "../../src/collection.js"
33
import { createLiveQueryCollection, eq } from "../../src/query/index.js"
44
import { Query } from "../../src/query/builder/index.js"
55
import { mockSyncCollectionOptions } from "../utls.js"
6+
import type { ChangeMessage } from "../../src/types.js"
67

78
// Sample user type for tests
89
type User = {
@@ -189,4 +190,102 @@ describe(`createLiveQueryCollection`, () => {
189190
})
190191
expect(liveQuery.isReady()).toBe(false)
191192
})
193+
194+
it(`should update after source collection is loaded even when not preloaded before rendering`, async () => {
195+
// Create a source collection that doesn't start sync immediately
196+
let beginCallback: (() => void) | undefined
197+
let writeCallback:
198+
| ((message: Omit<ChangeMessage<User, string | number>, `key`>) => void)
199+
| undefined
200+
let markReadyCallback: (() => void) | undefined
201+
let commitCallback: (() => void) | undefined
202+
203+
const sourceCollection = createCollection<User>({
204+
id: `delayed-source-collection`,
205+
getKey: (user) => user.id,
206+
startSync: false, // Don't start sync immediately
207+
sync: {
208+
sync: ({ begin, commit, write, markReady }) => {
209+
beginCallback = begin
210+
commitCallback = commit
211+
markReadyCallback = markReady
212+
writeCallback = write
213+
return () => {} // cleanup function
214+
},
215+
},
216+
onInsert: ({ transaction }) => {
217+
const newItem = transaction.mutations[0].modified
218+
// We need to call begin, write, and commit to properly sync the data
219+
beginCallback!()
220+
writeCallback!({
221+
type: `insert`,
222+
value: newItem,
223+
})
224+
commitCallback!()
225+
return Promise.resolve()
226+
},
227+
onUpdate: () => Promise.resolve(),
228+
onDelete: () => Promise.resolve(),
229+
})
230+
231+
// Create a live query collection BEFORE the source collection is preloaded
232+
// This simulates the scenario where the live query is created during rendering
233+
// but the source collection hasn't been preloaded yet
234+
const liveQuery = createLiveQueryCollection((q) =>
235+
q
236+
.from({ user: sourceCollection })
237+
.where(({ user }) => eq(user.active, true))
238+
)
239+
240+
// Initially, the live query should be in idle state (default startSync: false)
241+
expect(liveQuery.status).toBe(`idle`)
242+
expect(liveQuery.size).toBe(0)
243+
244+
// Now preload the source collection (simulating what happens after rendering)
245+
sourceCollection.preload()
246+
247+
// Store the promise so we can wait for it later
248+
const preloadPromise = liveQuery.preload()
249+
250+
// Trigger the initial data load first
251+
if (beginCallback && writeCallback && commitCallback && markReadyCallback) {
252+
beginCallback()
253+
// Write initial data
254+
writeCallback({
255+
type: `insert`,
256+
value: { id: 1, name: `Alice`, active: true },
257+
})
258+
writeCallback({
259+
type: `insert`,
260+
value: { id: 2, name: `Bob`, active: false },
261+
})
262+
writeCallback({
263+
type: `insert`,
264+
value: { id: 3, name: `Charlie`, active: true },
265+
})
266+
commitCallback()
267+
markReadyCallback()
268+
}
269+
270+
// Wait for the preload to complete
271+
await preloadPromise
272+
273+
// The live query should be ready and have the initial data
274+
expect(liveQuery.size).toBe(2) // Alice and Charlie are active
275+
expect(liveQuery.get(1)).toEqual({ id: 1, name: `Alice`, active: true })
276+
expect(liveQuery.get(3)).toEqual({ id: 3, name: `Charlie`, active: true })
277+
expect(liveQuery.get(2)).toBeUndefined() // Bob is not active
278+
// This test should fail because the live query is stuck in 'initialCommit' status
279+
expect(liveQuery.status).toBe(`ready`) // This should be 'ready' but is currently 'initialCommit'
280+
281+
// Now add some new data to the source collection (this should work as per the original report)
282+
sourceCollection.insert({ id: 4, name: `David`, active: true })
283+
284+
// Wait for the mutation to propagate
285+
await new Promise((resolve) => setTimeout(resolve, 10))
286+
287+
// The live query should update to include the new data
288+
expect(liveQuery.size).toBe(3) // Alice, Charlie, and David are active
289+
expect(liveQuery.get(4)).toEqual({ id: 4, name: `David`, active: true })
290+
})
192291
})

0 commit comments

Comments
 (0)