diff --git a/packages/db/src/collection.ts b/packages/db/src/collection.ts index 6409ceaa4..7b0f16667 100644 --- a/packages/db/src/collection.ts +++ b/packages/db/src/collection.ts @@ -237,7 +237,8 @@ export class CollectionImpl< Array > = { idle: [`loading`, `error`, `cleaned-up`], - loading: [`ready`, `error`, `cleaned-up`], + loading: [`initialCommit`, `error`, `cleaned-up`], + initialCommit: [`ready`, `error`, `cleaned-up`], ready: [`cleaned-up`, `error`], error: [`cleaned-up`, `idle`], "cleaned-up": [`loading`, `error`], @@ -382,14 +383,18 @@ export class CollectionImpl< pendingTransaction.committed = true - // Update status to ready - // We do this before committing as we want the events from the changes to - // be from a "ready" state. + // Update status to initialCommit when transitioning from loading + // This indicates we're in the process of committing the first transaction if (this._status === `loading`) { - this.setStatus(`ready`) + this.setStatus(`initialCommit`) } this.commitPendingTransactions() + + // Transition from initialCommit to ready after the first commit is complete + if (this._status === `initialCommit`) { + this.setStatus(`ready`) + } }, }) diff --git a/packages/db/src/query/live-query-collection.ts b/packages/db/src/query/live-query-collection.ts index fa31cde7c..fe284c164 100644 --- a/packages/db/src/query/live-query-collection.ts +++ b/packages/db/src/query/live-query-collection.ts @@ -162,7 +162,8 @@ export function liveQueryCollectionOptions< const allCollectionsReady = () => { return Object.values(collections).every( - (collection) => collection.status === `ready` + (collection) => + collection.status === `ready` || collection.status === `initialCommit` ) } diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index bdef947f8..f87be4321 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -244,6 +244,8 @@ export type CollectionStatus = | `idle` /** Sync has started but hasn't received the first commit yet */ | `loading` + /** Collection is in the process of committing its first transaction */ + | `initialCommit` /** Collection has received at least one commit and is ready for use */ | `ready` /** An error occurred during sync initialization */ diff --git a/packages/db/tests/collection-errors.test.ts b/packages/db/tests/collection-errors.test.ts index 888a5f323..4c877114c 100644 --- a/packages/db/tests/collection-errors.test.ts +++ b/packages/db/tests/collection-errors.test.ts @@ -360,7 +360,7 @@ describe(`Collection Error Handling`, () => { // Valid transitions from loading expect(() => - collectionImpl.validateStatusTransition(`loading`, `ready`) + collectionImpl.validateStatusTransition(`loading`, `initialCommit`) ).not.toThrow() expect(() => collectionImpl.validateStatusTransition(`loading`, `error`) @@ -369,6 +369,17 @@ describe(`Collection Error Handling`, () => { collectionImpl.validateStatusTransition(`loading`, `cleaned-up`) ).not.toThrow() + // Valid transitions from initialCommit + expect(() => + collectionImpl.validateStatusTransition(`initialCommit`, `ready`) + ).not.toThrow() + expect(() => + collectionImpl.validateStatusTransition(`initialCommit`, `error`) + ).not.toThrow() + expect(() => + collectionImpl.validateStatusTransition(`initialCommit`, `cleaned-up`) + ).not.toThrow() + // Valid transitions from ready expect(() => collectionImpl.validateStatusTransition(`ready`, `cleaned-up`) @@ -397,6 +408,12 @@ describe(`Collection Error Handling`, () => { expect(() => collectionImpl.validateStatusTransition(`idle`, `idle`) ).not.toThrow() + expect(() => + collectionImpl.validateStatusTransition( + `initialCommit`, + `initialCommit` + ) + ).not.toThrow() expect(() => collectionImpl.validateStatusTransition(`ready`, `ready`) ).not.toThrow() diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index b079beaf5..cdbb989a0 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -2,6 +2,7 @@ import { useRef, useSyncExternalStore } from "react" import { createLiveQueryCollection } from "@tanstack/db" import type { Collection, + CollectionStatus, Context, GetResult, InitialQueryBuilder, @@ -17,6 +18,12 @@ export function useLiveQuery( state: Map> data: Array> collection: Collection, string | number, {}> + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } // Overload 2: Accept config object @@ -27,6 +34,12 @@ export function useLiveQuery( state: Map> data: Array> collection: Collection, string | number, {}> + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } // Overload 3: Accept pre-created live query collection @@ -40,6 +53,12 @@ export function useLiveQuery< state: Map data: Array collection: Collection + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } // Implementation - use function overloads to infer the actual collection type @@ -171,5 +190,13 @@ export function useLiveQuery( state: snapshot.state, data: snapshot.data, collection: snapshot.collection, + status: snapshot.collection.status, + isLoading: + snapshot.collection.status === `loading` || + snapshot.collection.status === `initialCommit`, + isReady: snapshot.collection.status === `ready`, + isIdle: snapshot.collection.status === `idle`, + isError: snapshot.collection.status === `error`, + isCleanedUp: snapshot.collection.status === `cleaned-up`, } } diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index 04e75b2c7..0cda09d3a 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -966,4 +966,425 @@ describe(`Query Collections`, () => { age: 35, }) }) + + describe(`isLoaded property`, () => { + it(`should be true initially and false after collection is ready`, async () => { + let beginFn: (() => void) | undefined + let commitFn: (() => void) | undefined + + // Create a collection that doesn't start sync immediately + const collection = createCollection({ + id: `has-loaded-test`, + getKey: (person: Person) => person.id, + startSync: false, // Don't start sync immediately + sync: { + sync: ({ begin, commit }) => { + beginFn = begin + commitFn = commit + // Don't call begin/commit immediately + }, + }, + onInsert: async () => {}, + onUpdate: async () => {}, + onDelete: async () => {}, + }) + + const { result } = renderHook(() => { + return useLiveQuery((q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })) + ) + }) + + // Initially isLoading should be true + expect(result.current.isLoading).toBe(true) + + // Start sync manually + act(() => { + collection.preload() + }) + + // Trigger the first commit to make collection ready + act(() => { + if (beginFn && commitFn) { + beginFn() + commitFn() + } + }) + + // Insert data + act(() => { + collection.insert({ + id: `1`, + name: `John Doe`, + age: 35, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + }) + + // Wait for collection to become ready + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) + // Note: Data may not appear immediately due to live query evaluation timing + // The main test is that isLoading transitions from true to false + }) + + it(`should be false for pre-created collections that are already syncing`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `pre-created-has-loaded-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + // Create a live query collection that's already syncing + const liveQueryCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })), + startSync: true, + }) + + // Wait a bit for the collection to start syncing + await new Promise((resolve) => setTimeout(resolve, 10)) + + const { result } = renderHook(() => { + return useLiveQuery(liveQueryCollection) + }) + + // For pre-created collections that are already syncing, isLoading should be true + expect(result.current.isLoading).toBe(false) + expect(result.current.state.size).toBe(1) + }) + + it(`should update isLoading when collection status changes`, async () => { + let beginFn: (() => void) | undefined + let commitFn: (() => void) | undefined + + const collection = createCollection({ + id: `status-change-has-loaded-test`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + beginFn = begin + commitFn = commit + // Don't sync immediately + }, + }, + onInsert: async () => {}, + onUpdate: async () => {}, + onDelete: async () => {}, + }) + + const { result } = renderHook(() => { + return useLiveQuery((q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })) + ) + }) + + // Initially should be true + expect(result.current.isLoading).toBe(true) + + // Start sync manually + act(() => { + collection.preload() + }) + + // Trigger the first commit to make collection ready + act(() => { + if (beginFn && commitFn) { + beginFn() + commitFn() + } + }) + + // Insert data + act(() => { + collection.insert({ + id: `1`, + name: `John Doe`, + age: 35, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + }) + + expect(result.current.isLoading).toBe(false) + expect(result.current.isReady).toBe(true) + + // Wait for collection to become ready + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) + expect(result.current.status).toBe(`ready`) + }) + + it(`should maintain isReady state during live updates`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `live-updates-has-loaded-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const { result } = renderHook(() => { + return useLiveQuery((q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })) + ) + }) + + // Wait for initial load + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) + + const initialIsReady = result.current.isReady + + // Perform live updates + act(() => { + collection.utils.begin() + collection.utils.write({ + type: `insert`, + value: { + id: `4`, + name: `Kyle Doe`, + age: 40, + email: `kyle.doe@example.com`, + isActive: true, + team: `team1`, + }, + }) + collection.utils.commit() + }) + + // Wait for update to process + await waitFor(() => { + expect(result.current.state.size).toBe(2) + }) + + // isReady should remain true during live updates + expect(result.current.isReady).toBe(true) + expect(result.current.isReady).toBe(initialIsReady) + }) + + it(`should handle isLoading with complex queries including joins`, async () => { + let personBeginFn: (() => void) | undefined + let personCommitFn: (() => void) | undefined + let issueBeginFn: (() => void) | undefined + let issueCommitFn: (() => void) | undefined + + const personCollection = createCollection({ + id: `join-has-loaded-persons`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + personBeginFn = begin + personCommitFn = commit + // Don't sync immediately + }, + }, + onInsert: async () => {}, + onUpdate: async () => {}, + onDelete: async () => {}, + }) + + const issueCollection = createCollection({ + id: `join-has-loaded-issues`, + getKey: (issue: Issue) => issue.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + issueBeginFn = begin + issueCommitFn = commit + // Don't sync immediately + }, + }, + onInsert: async () => {}, + onUpdate: async () => {}, + onDelete: async () => {}, + }) + + const { result } = renderHook(() => { + return useLiveQuery((q) => + q + .from({ issues: issueCollection }) + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id) + ) + .select(({ issues, persons }) => ({ + id: issues.id, + title: issues.title, + name: persons.name, + })) + ) + }) + + // Initially should be true + expect(result.current.isLoading).toBe(true) + + // Start sync for both collections + act(() => { + personCollection.preload() + issueCollection.preload() + }) + + // Trigger the first commit for both collections to make them ready + act(() => { + if (personBeginFn && personCommitFn) { + personBeginFn() + personCommitFn() + } + if (issueBeginFn && issueCommitFn) { + issueBeginFn() + issueCommitFn() + } + }) + + // Insert data into both collections + act(() => { + personCollection.insert({ + id: `1`, + name: `John Doe`, + age: 30, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + issueCollection.insert({ + id: `1`, + title: `Issue 1`, + description: `Issue 1 description`, + userId: `1`, + }) + }) + + // Wait for both collections to sync + await waitFor(() => { + expect(result.current.isReady).toBe(true) + }) + // Note: Joined data may not appear immediately due to live query evaluation timing + // The main test is that isLoading transitions from false to true + }) + + it(`should handle isLoading with parameterized queries`, async () => { + let beginFn: (() => void) | undefined + let commitFn: (() => void) | undefined + + const collection = createCollection({ + id: `params-has-loaded-test`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + beginFn = begin + commitFn = commit + // Don't sync immediately + }, + }, + onInsert: async () => {}, + onUpdate: async () => {}, + onDelete: async () => {}, + }) + + const { result, rerender } = renderHook( + ({ minAge }: { minAge: number }) => { + return useLiveQuery( + (q) => + q + .from({ collection }) + .where(({ collection: c }) => gt(c.age, minAge)) + .select(({ collection: c }) => ({ + id: c.id, + name: c.name, + })), + [minAge] + ) + }, + { initialProps: { minAge: 30 } } + ) + + // Initially should be false + expect(result.current.isLoading).toBe(true) + + // Start sync manually + act(() => { + collection.preload() + }) + + // Trigger the first commit to make collection ready + act(() => { + if (beginFn && commitFn) { + beginFn() + commitFn() + } + }) + + // Insert data + act(() => { + collection.insert({ + id: `1`, + name: `John Doe`, + age: 35, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + collection.insert({ + id: `2`, + name: `Jane Doe`, + age: 25, + email: `jane.doe@example.com`, + isActive: true, + team: `team2`, + }) + }) + + // Wait for initial load + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) + + // Change parameters + act(() => { + rerender({ minAge: 25 }) + }) + + // isReady should remain true even when parameters change + await waitFor(() => { + expect(result.current.isReady).toBe(true) + }) + // Note: Data size may not change immediately due to live query evaluation timing + // The main test is that isReady remains true when parameters change + }) + }) }) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index ff02f8500..5a073c1cc 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -3,6 +3,7 @@ import { getCurrentInstance, onUnmounted, reactive, + ref, toValue, watchEffect, } from "vue" @@ -10,6 +11,7 @@ import { createLiveQueryCollection } from "@tanstack/db" import type { ChangeMessage, Collection, + CollectionStatus, Context, GetResult, InitialQueryBuilder, @@ -22,6 +24,12 @@ export interface UseLiveQueryReturn { state: ComputedRef> data: ComputedRef> collection: ComputedRef> + status: ComputedRef + isLoading: ComputedRef + isReady: ComputedRef + isIdle: ComputedRef + isError: ComputedRef + isCleanedUp: ComputedRef } export interface UseLiveQueryReturnWithCollection< @@ -32,6 +40,12 @@ export interface UseLiveQueryReturnWithCollection< state: ComputedRef> data: ComputedRef> collection: ComputedRef> + status: ComputedRef + isLoading: ComputedRef + isReady: ComputedRef + isIdle: ComputedRef + isError: ComputedRef + isCleanedUp: ComputedRef } // Overload 1: Accept just the query function @@ -114,6 +128,9 @@ export function useLiveQuery( // Computed wrapper for the data to match expected return type const data = computed(() => internalData) + // Track collection status reactively + const status = ref(collection.value.status) + // Helper to sync data array from collection in correct order const syncDataFromCollection = ( currentCollection: Collection @@ -129,6 +146,9 @@ export function useLiveQuery( watchEffect((onInvalidate) => { const currentCollection = collection.value + // Update status ref whenever the effect runs + status.value = currentCollection.status + // Clean up previous subscription if (currentUnsubscribe) { currentUnsubscribe() @@ -161,6 +181,8 @@ export function useLiveQuery( // Update the data array to maintain sorted order syncDataFromCollection(currentCollection) + // Update status ref on every change + status.value = currentCollection.status } ) @@ -192,5 +214,13 @@ export function useLiveQuery( state: computed(() => state), data, collection: computed(() => collection.value), + status: computed(() => status.value), + isLoading: computed( + () => status.value === `loading` || status.value === `initialCommit` + ), + isReady: computed(() => status.value === `ready`), + isIdle: computed(() => status.value === `idle`), + isError: computed(() => status.value === `error`), + isCleanedUp: computed(() => status.value === `cleaned-up`), } } diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 4e06ea1fe..874729d8d 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -82,6 +82,22 @@ async function waitForVueUpdate() { await new Promise((resolve) => setTimeout(resolve, 50)) } +// Helper function to poll for a condition until it passes or times out +async function waitFor(fn: () => void, timeout = 2000, interval = 20) { + const start = Date.now() + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + try { + fn() + return + } catch (err) { + if (Date.now() - start > timeout) throw err + await new Promise((resolve) => setTimeout(resolve, interval)) + } + } +} + describe(`Query Collections`, () => { it(`should work with basic collection and select`, async () => { const collection = createCollection( @@ -820,6 +836,348 @@ describe(`Query Collections`, () => { expect(state.value.get(`3`)).toBeUndefined() }) + describe(`isReady property`, () => { + it(`should be false initially and true after collection is ready`, async () => { + let beginFn: (() => void) | undefined + let commitFn: (() => void) | undefined + + // Create a collection that doesn't start sync immediately + const collection = createCollection({ + id: `is-ready-test`, + getKey: (person: Person) => person.id, + startSync: false, // Don't start sync immediately + sync: { + sync: ({ begin, commit }) => { + beginFn = begin + commitFn = commit + // Don't call begin/commit immediately + }, + }, + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + const { isReady } = useLiveQuery((q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })) + ) + + // Initially isReady should be false (collection is in idle state) + expect(isReady.value).toBe(false) + + // Start sync manually + collection.preload() + + // Trigger the first commit to make collection ready + if (beginFn && commitFn) { + beginFn() + commitFn() + } + + // Insert data + collection.insert({ + id: `1`, + name: `John Doe`, + age: 35, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + + await waitFor(() => expect(isReady.value).toBe(true)) + }) + + it(`should be true for pre-created collections that are already syncing`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `pre-created-is-ready-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + // Create a live query collection that's already syncing + const liveQueryCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })), + startSync: true, + }) + + await waitForVueUpdate() + const { isReady } = useLiveQuery(liveQueryCollection) + expect(isReady.value).toBe(true) + }) + + it(`should be false for pre-created collections that are not syncing`, () => { + const collection = createCollection({ + id: `not-syncing-is-ready-test`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: () => { + // Don't sync immediately + }, + }, + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + // Create a live query collection that's NOT syncing + const liveQueryCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })), + startSync: false, // Not syncing + }) + + const { isReady } = useLiveQuery(liveQueryCollection) + expect(isReady.value).toBe(false) + }) + + it(`should update isReady when collection status changes`, async () => { + let beginFn: (() => void) | undefined + let commitFn: (() => void) | undefined + + const collection = createCollection({ + id: `status-change-is-ready-test`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + beginFn = begin + commitFn = commit + // Don't sync immediately + }, + }, + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + const { isReady } = useLiveQuery((q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })) + ) + + expect(isReady.value).toBe(false) + collection.preload() + if (beginFn && commitFn) { + beginFn() + commitFn() + } + collection.insert({ + id: `1`, + name: `John Doe`, + age: 35, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + await waitFor(() => expect(isReady.value).toBe(true)) + }) + + it(`should maintain isReady state during live updates`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `live-updates-is-ready-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const { isReady } = useLiveQuery((q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })) + ) + + await waitForVueUpdate() + const initialIsReady = isReady.value + collection.utils.begin() + collection.utils.write({ + type: `insert`, + value: { + id: `4`, + name: `Kyle Doe`, + age: 40, + email: `kyle.doe@example.com`, + isActive: true, + team: `team1`, + }, + }) + collection.utils.commit() + await waitForVueUpdate() + expect(isReady.value).toBe(true) + expect(isReady.value).toBe(initialIsReady) + }) + + it(`should handle isReady with complex queries including joins`, async () => { + let personBeginFn: (() => void) | undefined + let personCommitFn: (() => void) | undefined + let issueBeginFn: (() => void) | undefined + let issueCommitFn: (() => void) | undefined + + const personCollection = createCollection({ + id: `join-is-ready-persons`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + personBeginFn = begin + personCommitFn = commit + // Don't sync immediately + }, + }, + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + const issueCollection = createCollection({ + id: `join-is-ready-issues`, + getKey: (issue: Issue) => issue.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + issueBeginFn = begin + issueCommitFn = commit + // Don't sync immediately + }, + }, + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + const { isReady } = useLiveQuery((q) => + q + .from({ issues: issueCollection }) + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id) + ) + .select(({ issues, persons }) => ({ + id: issues.id, + title: issues.title, + name: persons.name, + })) + ) + + expect(isReady.value).toBe(false) + personCollection.preload() + issueCollection.preload() + if (personBeginFn && personCommitFn) { + personBeginFn() + personCommitFn() + } + if (issueBeginFn && issueCommitFn) { + issueBeginFn() + issueCommitFn() + } + personCollection.insert({ + id: `1`, + name: `John Doe`, + age: 30, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + issueCollection.insert({ + id: `1`, + title: `Issue 1`, + description: `Issue 1 description`, + userId: `1`, + }) + await waitFor(() => expect(isReady.value).toBe(true)) + }) + + it(`should handle isReady with parameterized queries`, async () => { + let beginFn: (() => void) | undefined + let commitFn: (() => void) | undefined + + const collection = createCollection({ + id: `params-is-ready-test`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: ({ begin, commit }) => { + beginFn = begin + commitFn = commit + // Don't sync immediately + }, + }, + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + const minAge = ref(30) + const { isReady } = useLiveQuery( + (q) => + q + .from({ collection }) + .where(({ collection: c }) => gt(c.age, minAge.value)) + .select(({ collection: c }) => ({ + id: c.id, + name: c.name, + })), + [minAge] + ) + + expect(isReady.value).toBe(false) + collection.preload() + if (beginFn && commitFn) { + beginFn() + commitFn() + } + collection.insert({ + id: `1`, + name: `John Doe`, + age: 35, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + collection.insert({ + id: `2`, + name: `Jane Doe`, + age: 25, + email: `jane.doe@example.com`, + isActive: true, + team: `team2`, + }) + await waitFor(() => expect(isReady.value).toBe(true)) + minAge.value = 25 + await waitFor(() => expect(isReady.value).toBe(true)) + }) + }) + it(`should accept config object with pre-built QueryBuilder instance`, async () => { const collection = createCollection( mockSyncCollectionOptions({ diff --git a/packages/vue-db/vite.config.ts b/packages/vue-db/vite.config.ts index 7e4a41ac2..bf10fb50d 100644 --- a/packages/vue-db/vite.config.ts +++ b/packages/vue-db/vite.config.ts @@ -8,7 +8,6 @@ const config = defineConfig({ test: { name: packageJson.name, dir: `./tests`, - watch: false, environment: `jsdom`, coverage: { enabled: true, provider: `istanbul`, include: [`src/**/*`] }, typecheck: { enabled: true },