diff --git a/.changeset/fast-crabs-change.md b/.changeset/fast-crabs-change.md new file mode 100644 index 00000000..0df420b4 --- /dev/null +++ b/.changeset/fast-crabs-change.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db": patch +--- + +Ensure LiveQueryCollections are properly transitioning to ready state when source collections are preloaded after creation of the live query collection diff --git a/packages/db/src/collection.ts b/packages/db/src/collection.ts index 18cb2c0b..c145cd94 100644 --- a/packages/db/src/collection.ts +++ b/packages/db/src/collection.ts @@ -393,9 +393,8 @@ export class CollectionImpl< this.onFirstReadyCallbacks = [] callbacks.forEach((callback) => callback()) - // If the collection is empty when it becomes ready, emit an empty change event // to notify subscribers (like LiveQueryCollection) that the collection is ready - if (this.size === 0 && this.changeListeners.size > 0) { + if (this.changeListeners.size > 0) { this.emitEmptyReadyEvent() } } diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 15bd930d..2d812a61 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -3,6 +3,7 @@ import { createCollection } from "../../src/collection.js" import { createLiveQueryCollection, eq } from "../../src/query/index.js" import { Query } from "../../src/query/builder/index.js" import { mockSyncCollectionOptions } from "../utls.js" +import type { ChangeMessage } from "../../src/types.js" // Sample user type for tests type User = { @@ -189,4 +190,102 @@ describe(`createLiveQueryCollection`, () => { }) expect(liveQuery.isReady()).toBe(false) }) + + it(`should update after source collection is loaded even when not preloaded before rendering`, async () => { + // Create a source collection that doesn't start sync immediately + let beginCallback: (() => void) | undefined + let writeCallback: + | ((message: Omit, `key`>) => void) + | undefined + let markReadyCallback: (() => void) | undefined + let commitCallback: (() => void) | undefined + + const sourceCollection = createCollection({ + id: `delayed-source-collection`, + getKey: (user) => user.id, + startSync: false, // Don't start sync immediately + sync: { + sync: ({ begin, commit, write, markReady }) => { + beginCallback = begin + commitCallback = commit + markReadyCallback = markReady + writeCallback = write + return () => {} // cleanup function + }, + }, + onInsert: ({ transaction }) => { + const newItem = transaction.mutations[0].modified + // We need to call begin, write, and commit to properly sync the data + beginCallback!() + writeCallback!({ + type: `insert`, + value: newItem, + }) + commitCallback!() + return Promise.resolve() + }, + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + // Create a live query collection BEFORE the source collection is preloaded + // This simulates the scenario where the live query is created during rendering + // but the source collection hasn't been preloaded yet + const liveQuery = createLiveQueryCollection((q) => + q + .from({ user: sourceCollection }) + .where(({ user }) => eq(user.active, true)) + ) + + // Initially, the live query should be in idle state (default startSync: false) + expect(liveQuery.status).toBe(`idle`) + expect(liveQuery.size).toBe(0) + + // Now preload the source collection (simulating what happens after rendering) + sourceCollection.preload() + + // Store the promise so we can wait for it later + const preloadPromise = liveQuery.preload() + + // Trigger the initial data load first + if (beginCallback && writeCallback && commitCallback && markReadyCallback) { + beginCallback() + // Write initial data + writeCallback({ + type: `insert`, + value: { id: 1, name: `Alice`, active: true }, + }) + writeCallback({ + type: `insert`, + value: { id: 2, name: `Bob`, active: false }, + }) + writeCallback({ + type: `insert`, + value: { id: 3, name: `Charlie`, active: true }, + }) + commitCallback() + markReadyCallback() + } + + // Wait for the preload to complete + await preloadPromise + + // The live query should be ready and have the initial data + expect(liveQuery.size).toBe(2) // Alice and Charlie are active + expect(liveQuery.get(1)).toEqual({ id: 1, name: `Alice`, active: true }) + expect(liveQuery.get(3)).toEqual({ id: 3, name: `Charlie`, active: true }) + expect(liveQuery.get(2)).toBeUndefined() // Bob is not active + // This test should fail because the live query is stuck in 'initialCommit' status + expect(liveQuery.status).toBe(`ready`) // This should be 'ready' but is currently 'initialCommit' + + // Now add some new data to the source collection (this should work as per the original report) + sourceCollection.insert({ id: 4, name: `David`, active: true }) + + // Wait for the mutation to propagate + await new Promise((resolve) => setTimeout(resolve, 10)) + + // The live query should update to include the new data + expect(liveQuery.size).toBe(3) // Alice, Charlie, and David are active + expect(liveQuery.get(4)).toEqual({ id: 4, name: `David`, active: true }) + }) })