@tanstack/react-db 0.5.11 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cjs/live-query-internals.cjs.map +1 -1
  2. package/dist/cjs/live-query-internals.d.cts +3 -1
  3. package/dist/cjs/source-id-bindings.cjs +9 -42
  4. package/dist/cjs/source-id-bindings.cjs.map +1 -1
  5. package/dist/cjs/source-id-bindings.d.cts +6 -17
  6. package/dist/cjs/useLiveInfiniteQuery.cjs +93 -173
  7. package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
  8. package/dist/cjs/useLiveInfiniteQuery.d.cts +3 -14
  9. package/dist/cjs/useLiveQuery.cjs +187 -224
  10. package/dist/cjs/useLiveQuery.cjs.map +1 -1
  11. package/dist/cjs/useLiveQuery.d.cts +37 -136
  12. package/dist/cjs/useLiveSuspenseQuery.cjs +71 -64
  13. package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
  14. package/dist/cjs/useLiveSuspenseQuery.d.cts +10 -9
  15. package/dist/cjs/usePacedMutations.cjs.map +1 -1
  16. package/dist/cjs/usePacedMutations.d.cts +1 -1
  17. package/dist/esm/live-query-internals.d.ts +3 -1
  18. package/dist/esm/live-query-internals.js.map +1 -1
  19. package/dist/esm/source-id-bindings.d.ts +6 -17
  20. package/dist/esm/source-id-bindings.js +9 -40
  21. package/dist/esm/source-id-bindings.js.map +1 -1
  22. package/dist/esm/useLiveInfiniteQuery.d.ts +3 -14
  23. package/dist/esm/useLiveInfiniteQuery.js +95 -175
  24. package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
  25. package/dist/esm/useLiveQuery.d.ts +37 -136
  26. package/dist/esm/useLiveQuery.js +189 -226
  27. package/dist/esm/useLiveQuery.js.map +1 -1
  28. package/dist/esm/useLiveSuspenseQuery.d.ts +10 -9
  29. package/dist/esm/useLiveSuspenseQuery.js +71 -64
  30. package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
  31. package/dist/esm/usePacedMutations.d.ts +1 -1
  32. package/dist/esm/usePacedMutations.js.map +1 -1
  33. package/package.json +3 -3
  34. package/skills/react-db/SKILL.md +24 -15
  35. package/src/live-query-internals.ts +3 -0
  36. package/src/source-id-bindings.ts +16 -62
  37. package/src/useLiveInfiniteQuery.ts +138 -296
  38. package/src/useLiveQuery.ts +329 -495
  39. package/src/useLiveSuspenseQuery.ts +122 -132
  40. package/src/usePacedMutations.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef, useSyncExternalStore } from 'react'\nimport {\n UnhashableQueryIRError,\n createLiveQueryObserver,\n deepEquals,\n getPreparedLiveQueryIdentity,\n getStableValueHash,\n isCollection,\n prepareLiveQueryValue,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { setLiveQueryResultInfo } from './live-query-internals'\nimport { shouldWarnInDevelopment } from './development'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n getPreparedSources,\n getSourceObjectToken,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport type {\n Collection,\n CollectionImpl,\n CollectionStatus,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n// Suspense renders can be abandoned before React subscribes. Keep their\n// collection briefly so a nearby retry can commit without starting over.\nconst DEFAULT_SUSPENSE_GC_TIME_MS = 5000\nconst DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16\nconst DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10\nconst DERIVED_IDENTITY_TOTAL_WARN_MS = 50\nconst warnedDepsCallsites = new Set<string>()\nconst warnedDerivedIdentityCallsites = new Set<string>()\nconst warnedUnhashableIdentityCallsites = new Set<string>()\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\ntype SuspenseCollection = Collection<object, string | number, {}>\ntype SuspenseCollectionEntry = {\n collection: SuspenseCollection\n removeStatusListeners: () => void\n}\n\nconst unscopedSuspenseCollections = new Map<string, SuspenseCollectionEntry>()\nconst suspenseCollectionsByClient = new WeakMap<\n DbClient,\n Map<string, SuspenseCollectionEntry>\n>()\nfunction getSourceQualifiedSuspenseKey(\n preparedValue: unknown,\n queryHash: string,\n client: DbClient | undefined,\n): string {\n const sourceIds = getPreparedSources(\n preparedValue,\n client,\n `useLiveSuspenseQuery`,\n ).map(getSourceObjectToken)\n if (sourceIds.length === 0) return queryHash\n return `${sourceIds.join(`,`)}:${queryHash}`\n}\n\nfunction getSuspenseCollections(\n client: DbClient | undefined,\n): Map<string, SuspenseCollectionEntry> {\n if (!client) return unscopedSuspenseCollections\n let collections = suspenseCollectionsByClient.get(client)\n if (!collections) {\n collections = new Map()\n suspenseCollectionsByClient.set(client, collections)\n }\n return collections\n}\n\nfunction releaseSuspenseCollection(\n collections: Map<string, SuspenseCollectionEntry>,\n queryHash: string,\n collection: SuspenseCollection,\n): void {\n const entry = collections.get(queryHash)\n if (entry?.collection !== collection) return\n collections.delete(queryHash)\n entry.removeStatusListeners()\n}\n\nexport type DerivedIdentityProfiler = {\n renderCount: number\n totalMs: number\n maxMs: number\n warned: boolean\n}\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\nexport type LiveQueryKey = ReadonlyArray<unknown>\ntype UseLiveQueryConfigOptions<TContext extends Context> = Omit<\n LiveQueryCollectionConfig<TContext>,\n `query`\n> & {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n}\n\ntype ConfiguredQueryBuilder<TContext extends Context> = Extract<\n LiveQueryCollectionConfig<TContext>[`query`],\n QueryBuilder<TContext>\n>\n\nexport type UseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> &\n Pick<LiveQueryCollectionConfig<TContext>, `query`>\n\nexport type ConditionalUseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> & {\n query:\n | ConfiguredQueryBuilder<TContext>\n | ((\n q: InitialQueryBuilder,\n ) => ConfiguredQueryBuilder<TContext> | undefined | null)\n }\n\nexport function warnDeprecatedDepsArray(\n hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedDepsCallsites.has(callsite)) {\n return\n }\n warnedDepsCallsites.add(callsite)\n const replacement =\n hookName === `useLiveQuery`\n ? `useLiveQuery({ query })`\n : `useLiveInfiniteQuery(query, { queryKey })`\n console.warn(\n `[${hookName}] The dependency-array form is deprecated and will be removed in 1.0. Use ${replacement} instead. Provide queryKey only for functional/opaque queries or to avoid deriving identity from structured query IR on render.`,\n )\n}\n\nfunction getCurrentTime(): number {\n return typeof performance !== `undefined` &&\n typeof performance.now === `function`\n ? performance.now()\n : Date.now()\n}\n\nfunction getWarningCallsite(stackIndex: number): string {\n const stack = new Error().stack ?? `unknown`\n const lines = stack.split(`\\n`)\n const userFrame = lines\n .slice(1)\n .find(\n (line) =>\n !line.includes(`useLiveQuery.ts`) &&\n !line.includes(`useLiveSuspenseQuery.ts`) &&\n !line.includes(`useLiveInfiniteQuery.ts`),\n )\n return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack\n}\n\nfunction warnDerivedIdentityHotPath(\n profiler: DerivedIdentityProfiler,\n durationMs: number,\n): void {\n if (\n profiler.warned ||\n !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n return\n }\n\n const isSlowSingleRender =\n durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS\n const isHotRenderPath =\n profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&\n profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS\n\n if (!isSlowSingleRender && !isHotRenderPath) {\n return\n }\n\n const callsite = getWarningCallsite(5)\n if (warnedDerivedIdentityCallsites.has(callsite)) {\n profiler.warned = true\n return\n }\n\n warnedDerivedIdentityCallsites.add(callsite)\n profiler.warned = true\n\n const reason = isSlowSingleRender\n ? `one render took ${durationMs.toFixed(1)}ms`\n : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`\n\n console.warn(\n `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +\n `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,\n )\n}\n\nfunction getExplicitQueryKey(value: unknown): LiveQueryKey | undefined {\n return value &&\n typeof value === `object` &&\n Array.isArray((value as { queryKey?: unknown }).queryKey)\n ? (value as { queryKey: LiveQueryKey }).queryKey\n : undefined\n}\n\nfunction getExplicitDbClient(value: unknown): DbClient | undefined {\n return value &&\n typeof value === `object` &&\n `client` in value &&\n (value as { client?: unknown }).client !== undefined\n ? (value as { client: DbClient }).client\n : undefined\n}\n\nexport function prepareQueryValue(\n value: unknown,\n dbClient: DbClient | undefined,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): unknown {\n return prepareLiveQueryValue(value, dbClient, deferredCollections)\n}\n\ntype DerivedQueryPreparation =\n | {\n status: `hashable`\n value: unknown\n identityDeps: Array<unknown>\n }\n | {\n status: `unhashable`\n value: unknown\n error: UnhashableQueryIRError\n }\n\nexport function prepareDerivedQuery(\n value: unknown,\n dbClient: DbClient | undefined,\n profiler: DerivedIdentityProfiler,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): DerivedQueryPreparation {\n const shouldProfile = shouldWarnInDevelopment(\n `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,\n )\n const start = shouldProfile ? getCurrentTime() : 0\n const preparedValue = prepareQueryValue(value, dbClient, deferredCollections)\n\n try {\n const identity = getPreparedLiveQueryIdentity(preparedValue)\n return {\n status: `hashable`,\n value: preparedValue,\n identityDeps: [`derived`, identity],\n }\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n return { status: `unhashable`, value: preparedValue, error }\n }\n\n throw error\n } finally {\n if (shouldProfile) {\n const durationMs = getCurrentTime() - start\n profiler.renderCount += 1\n profiler.totalMs += durationMs\n profiler.maxMs = Math.max(profiler.maxMs, durationMs)\n warnDerivedIdentityHotPath(profiler, durationMs)\n }\n }\n}\n\nexport function warnUnhashableDerivedIdentity(\n error: UnhashableQueryIRError,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedUnhashableIdentityCallsites.has(callsite)) {\n return\n }\n warnedUnhashableIdentityCallsites.add(callsite)\n\n console.warn(\n `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` +\n `It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` +\n `Unhashable queries without queryKey will throw in 1.0.`,\n )\n}\n\n/**\n * Create a live query using a query function.\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Prefer config object syntax\n * const { data, isLoading } = useLiveQuery({\n * query: (q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * })\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * })\n *\n * @example\n * // Structured captured values are included in derived query identity\n * const { data, state } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // Return undefined or null to disable a query\n * const { data, isEnabled } = useLiveQuery({\n * query: (q) => {\n * if (!userId) return undefined\n * return q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.userId, userId))\n * },\n * })\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery({\n * query: (q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * })\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todoCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({\n * query: queryBuilder,\n * })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 7: Accept config object with a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 8: Accept config object with legacy deps\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 9: Accept config object with legacy deps and a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n deps: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 10: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 10: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps?: Array<unknown>,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps)\n}\n\n/** @internal Shared implementation for the Suspense wrapper. */\nexport function useLiveQueryForSuspense(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps, true)\n}\n\n// What one hook instance keeps across renders. It lives in a single ref\n// slot, so a render neither looks up nor allocates more.\nfunction createHookInstance(dbClient: DbClient | undefined) {\n return {\n collection: null as Collection<object, string | number, {}> | null,\n deps: null as Array<unknown> | null,\n config: null as unknown,\n client: dbClient,\n legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,\n derivedIdentityProfiler: {\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n } as DerivedIdentityProfiler,\n deferredCollections: new Set<\n CollectionImpl<any, string | number, any, any, any>\n >(),\n sourceIds: createSourceIdBindings(),\n observer: null as LiveQueryObserver<object, string | number> | null,\n queryHash: undefined as string | undefined,\n suspenseKey: undefined as string | undefined,\n identityError: undefined as UnhashableQueryIRError | undefined,\n subscribe: null as ((onStoreChange: () => void) => () => void) | null,\n }\n}\n\nfunction useLiveQueryImpl(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n forSuspense = false,\n) {\n const contextDbClient = useOptionalDbClient()\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n const dbClient = inputIsCollection\n ? contextDbClient\n : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)\n const resolvedDeps = deps ?? []\n\n const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)\n const instance = (instanceRef.current ??= createHookInstance(dbClient))\n const resumeDeferredCollections = () =>\n resumeSyncStarts(instance.deferredCollections)\n\n const queryKey = !inputIsCollection\n ? getExplicitQueryKey(configOrQueryOrCollection)\n : undefined\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown>\n let streamIdentity: unknown = undefined\n let identityError: UnhashableQueryIRError | undefined\n\n if (queryKey) {\n identityDeps = queryKey\n streamIdentity = [`queryKey`, queryKey]\n } else if (deps !== undefined) {\n identityDeps = resolvedDeps\n // Deps decide reuse. Only hydration and Suspense read the query hash; the\n // development warning about unhashable queries still derives it.\n if (\n dbClient ||\n forSuspense ||\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n )\n try {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n streamIdentity = [\n `deps`,\n resolvedDeps,\n getPreparedLiveQueryIdentity(preparedQueryValue),\n ]\n } catch (error) {\n if (!(error instanceof UnhashableQueryIRError)) throw error\n warnUnhashableDerivedIdentity(error)\n identityError = error\n }\n } else if (inputIsCollection) {\n identityDeps = []\n streamIdentity = [`collection`, configOrQueryOrCollection.id]\n } else {\n const preparation = prepareDerivedQuery(\n configOrQueryOrCollection,\n dbClient,\n instance.derivedIdentityProfiler,\n instance.deferredCollections,\n )\n preparedQueryValue = preparation.value\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n streamIdentity = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = instance.legacyUnhashableIdentity\n identityError = preparation.error\n }\n }\n\n let queryHash: string | undefined\n if (streamIdentity !== undefined) {\n try {\n queryHash = getStableValueHash(streamIdentity, `queryKey`)\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n if (queryKey !== undefined) throw error\n identityError = error\n } else {\n throw error\n }\n }\n }\n\n if (deps !== undefined) {\n warnDeprecatedDepsArray()\n }\n\n if (\n queryKey === undefined &&\n deps === undefined &&\n preparedQueryValue !== unpreparedQueryValue\n ) {\n claimSourceIds(\n instance.sourceIds,\n preparedQueryValue,\n dbClient,\n `useLiveQuery`,\n resumeDeferredCollections,\n )\n }\n\n const canReuseSuspenseKey =\n forSuspense &&\n !inputIsCollection &&\n queryHash !== undefined &&\n instance.collection !== null &&\n instance.client === dbClient &&\n instance.queryHash === queryHash &&\n instance.suspenseKey !== undefined\n\n if (\n forSuspense &&\n !inputIsCollection &&\n queryHash &&\n !canReuseSuspenseKey &&\n preparedQueryValue === unpreparedQueryValue\n ) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n\n const suspenseKey =\n queryHash && forSuspense && !inputIsCollection\n ? canReuseSuspenseKey\n ? instance.suspenseKey\n : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient)\n : queryHash\n\n const suspenseCollections =\n forSuspense && !inputIsCollection && suspenseKey\n ? getSuspenseCollections(dbClient)\n : undefined\n const suspenseEntry = suspenseKey\n ? suspenseCollections?.get(suspenseKey)\n : undefined\n const suspenseCollection = suspenseEntry?.collection\n\n const identityChanged =\n instance.deps === null ||\n (deps !== undefined\n ? instance.deps.length !== identityDeps.length ||\n instance.deps.some((dep, index) => dep !== identityDeps[index])\n : !deepEquals(instance.deps, identityDeps))\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !instance.collection ||\n (inputIsCollection && instance.config !== configOrQueryOrCollection) ||\n (!inputIsCollection && (instance.client !== dbClient || identityChanged))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (\n syncMode === `on-demand` &&\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n instance.collection = configOrQueryOrCollection\n instance.config = configOrQueryOrCollection\n } else {\n if (suspenseCollection) {\n instance.collection = suspenseCollection\n } else {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n if (!dbClient) {\n getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`)\n }\n instance.collection = resolveLiveQueryValue(preparedQueryValue, {\n gcTime: forSuspense\n ? DEFAULT_SUSPENSE_GC_TIME_MS\n : DEFAULT_GC_TIME_MS,\n // Hydration and Suspense key the live-query Collection by identity.\n pool: !forSuspense && !dbClient,\n // Without a DbClient, Suspense starts it through a preload below.\n startSync: !forSuspense || dbClient !== undefined,\n }) as SuspenseCollection | null\n if (suspenseCollections && suspenseKey && instance.collection) {\n const collection = instance.collection\n const removeCleanupListener = collection.on(`status:cleaned-up`, () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n )\n const removeErrorListener = collection.on(`status:error`, () => {\n // Keep the failed collection through React's immediate retry so\n // the hook can throw its actual load error to an ErrorBoundary.\n // Retire it afterward so a later boundary reset starts fresh.\n setTimeout(\n () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n 0,\n )\n })\n const entry: SuspenseCollectionEntry = {\n collection,\n removeStatusListeners: () => {\n removeCleanupListener()\n removeErrorListener()\n },\n }\n suspenseCollections.set(suspenseKey, entry)\n }\n }\n instance.config = configOrQueryOrCollection\n instance.deps = [...identityDeps]\n }\n instance.client = dbClient\n instance.queryHash = queryHash\n instance.suspenseKey = suspenseKey\n instance.identityError = identityError\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n instance.observer = createLiveQueryObserver(instance.collection, {\n mode: `wholesale`,\n client: dbClient,\n queryHash: instance.queryHash,\n onPreload: resumeDeferredCollections,\n })\n }\n const observer = instance.observer!\n // A Suspense render asks for its data. Preloading starts the sync run, so a\n // source that already holds the rows makes it ready before the snapshot is\n // read instead of suspending once. useLiveSuspenseQuery surfaces failures.\n // A DbClient defers source starts past render and owns its streamed query.\n if (\n forSuspense &&\n !dbClient &&\n (instance.collection?.status === `idle` ||\n instance.collection?.status === `cleaned-up`)\n ) {\n observer.preloadForInitialRender().catch(() => {})\n }\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n if (!instance.subscribe || needsNewCollection) {\n instance.subscribe = (onStoreChange: () => void) => {\n const unsubscribe = observer.subscribe(onStoreChange)\n resumeDeferredCollections()\n return unsubscribe\n }\n }\n\n const returned = useSyncExternalStore(\n instance.subscribe,\n () => observer.getSnapshot(),\n () => observer.getServerSnapshot(),\n )\n // Only useLiveSuspenseQuery reads this, and it costs a define per render.\n if (forSuspense) {\n setLiveQueryResultInfo(returned, {\n client: dbClient,\n queryHash: instance.queryHash,\n identityError: instance.identityError,\n observer,\n })\n }\n return returned as any\n}\n"],"names":["getPreparedSources","shouldWarnInDevelopment","claimSourceIds","useSyncExternalStore","setLiveQueryResultInfo"],"mappings":";;;;;;;;;AAwCA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AAIA;AAKE;AAAkBA;AAChB;AACA;AACA;AAEF;AACA;AACF;AAEA;AAGE;AACA;AACA;AACE;AACA;AAAmD;AAErD;AACF;AAEA;AAKE;AACA;AACA;AACA;AACF;AA2CO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AACA;AAIA;AAAQ;AAC8F;AAExG;AAEA;AACE;AAIF;AAEA;AACE;AACA;AAA0B;AAC1B;AAEG;AAI2C;AAE9C;AACF;AAEA;AAIE;AAIE;AAAA;AAGF;AAEA;AAIA;AACE;AAAA;AAGF;AACA;AACE;AACA;AAAA;AAGF;AACA;AAEA;AAIA;AAAQ;AAC2I;AAGrJ;AAEA;AACE;AAKF;AAEA;AACE;AAMF;AAEO;AAKL;AACF;AAcO;AAML;AAAsBC;AACpB;AAEF;AACA;AAEA;AACE;AACA;AAAO;AACG;AACD;AAC2B;AAAA;AAGpC;AACE;AAAqD;AAGvD;AAAM;AAEN;AACE;AACA;AACA;AACA;AACA;AAA+C;AACjD;AAEJ;AAEO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AAEA;AAAQ;AAC4F;AAItG;AAkYO;AAIL;AACF;AAGO;AAIL;AACF;AAIA;AACE;AAAO;AACO;AACN;AACE;AACA;AACsC;AACrB;AACV;AACJ;AACF;AACC;AAAA;AAEe;AAGd;AACD;AACC;AACE;AACE;AACJ;AAEf;AAEA;AAKE;AAEA;AACA;AAGA;AAEA;AACA;AACA;AAGA;AAGA;AAEA;AACA;AACA;AAEA;AACE;AACA;AAAsC;AAEtC;AAGA;AAKE;AACE;AAAqB;AACnB;AACA;AACS;AAEX;AAAiB;AACf;AACA;AAC+C;AAAA;AAGjD;AACA;AACA;AAAgB;AAClB;AAEF;AACA;AAA4D;AAE5D;AAAoB;AAClB;AACA;AACS;AACA;AAEX;AACA;AACE;AACA;AAA6B;AAE7B;AACA;AACA;AAA4B;AAC9B;AAGF;AACA;AACE;AACE;AAAyD;AAEzD;AACE;AACA;AAAgB;AAEhB;AAAM;AACR;AACF;AAGF;AACE;AAAA;AAGF;AAKEC;AAAAA;AACW;AACT;AACA;AACA;AACA;AAAA;AAIJ;AASA;AAOE;AAAqB;AACnB;AACA;AACS;AAAA;AAIb;AAOA;AAIA;AAGA;AAEA;AAQA;AAKA;AACE;AAIE;AAGA;AAIE;AAAQ;;AACN;AAAA;;AAAA;AAAA;AAAA;AAQJ;AACA;AACA;AAAkB;AAElB;AACE;AAAsB;AAEtB;AACE;AAAqB;AACnB;AACA;AACS;AAAA;AAGb;AACEF;AAA+D;AAEjE;AAAgE;AAG1D;AAAA;AAEmB;AAAA;AAEiB;AAE1C;AACE;AACA;AAAyC;AAAG;AAC1C;AACE;AACA;AACA;AAAA;AAGJ;AAIE;AAAA;AAEI;AACE;AACA;AACA;AAAA;AAEJ;AAAA;AAGJ;AAAuC;AACrC;AAEE;AACA;AAAA;AACF;AAEF;AAA0C;AAC5C;AAEF;AACA;AAAgC;AAElC;AACA;AACA;AACA;AAAyB;AAS3B;AAOE;AAAiE;AACzD;AACE;AACY;AACT;AACZ;AAEH;AAKA;AAME;AAA+C;AAAE;AAKnD;AACE;AACE;AACA;AACA;AAAO;AACT;AAGF;AAAiBG;AACN;AACM;AACA;AAGjB;AACEC;AAAiC;AACvB;AACY;AACI;AACxB;AACD;AAEH;AACF;;;;;;;"}
1
+ {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef, useSyncExternalStore } from 'react'\nimport {\n UnhashableQueryIRError,\n createLiveQueryObserver,\n createLiveQuerySourceIdClaims,\n deepEquals,\n getLiveQueryKey,\n getSourceObjectToken,\n getStableValueHash,\n identifyLiveQuery,\n isCollection,\n prepareLiveQueryValue,\n rejectLiveQueryInput,\n releaseDeferredOnReclaim,\n resolveLiveQueryValue,\n resumeDeferredLiveQueryCollections,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { setLiveQueryResultInfo } from './live-query-internals'\nimport { shouldWarnInDevelopment } from './development'\nimport {\n getPreparedSources,\n getUnboundDescriptorReporter,\n} from './source-id-bindings'\nimport type {\n Collection,\n CollectionImpl,\n CollectionStatus,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryIdentityScope,\n LiveQueryKey,\n LiveQueryObserver,\n LiveQuerySourceIdClaims,\n LiveQueryStatusFlags,\n NonSingleResult,\n PooledLiveQueries,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n// Suspense renders can be abandoned before React subscribes. Keep their\n// collection briefly so a nearby retry can commit without starting over.\nconst DEFAULT_SUSPENSE_GC_TIME_MS = 5000\nconst DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16\nconst DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10\nconst DERIVED_IDENTITY_TOTAL_WARN_MS = 50\nconst warnedDerivedIdentityCallsites = new Set<string>()\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\ntype SuspenseCollection = Collection<object, string | number, {}>\ntype SuspenseCollectionEntry = {\n collection: SuspenseCollection\n removeStatusListeners: () => void\n}\n\nconst unscopedSuspenseCollections = new Map<string, SuspenseCollectionEntry>()\nconst suspenseCollectionsByClient = new WeakMap<\n DbClient,\n Map<string, SuspenseCollectionEntry>\n>()\nfunction getSourceQualifiedSuspenseKey(\n preparedValue: unknown,\n queryHash: string,\n client: DbClient | undefined,\n): string {\n const sourceIds = getPreparedSources(\n preparedValue,\n client,\n `useLiveSuspenseQuery`,\n ).map(getSourceObjectToken)\n if (sourceIds.length === 0) return queryHash\n return `${sourceIds.join(`,`)}:${queryHash}`\n}\n\nfunction getSuspenseCollections(\n client: DbClient | undefined,\n): Map<string, SuspenseCollectionEntry> {\n if (!client) return unscopedSuspenseCollections\n let collections = suspenseCollectionsByClient.get(client)\n if (!collections) {\n collections = new Map()\n suspenseCollectionsByClient.set(client, collections)\n }\n return collections\n}\n\nfunction releaseSuspenseCollection(\n collections: Map<string, SuspenseCollectionEntry>,\n queryHash: string,\n collection: SuspenseCollection,\n): void {\n const entry = collections.get(queryHash)\n if (entry?.collection !== collection) return\n collections.delete(queryHash)\n entry.removeStatusListeners()\n}\n\nexport type DerivedIdentityProfiler = {\n renderCount: number\n totalMs: number\n maxMs: number\n warned: boolean\n}\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\nexport type { LiveQueryKey }\ntype UseLiveQueryConfigOptions<TContext extends Context> = Omit<\n LiveQueryCollectionConfig<TContext>,\n `query`\n> & {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n /**\n * Pass `pooledLiveQueries` to share one partition among equality-filtered\n * queries of one shape. Suspense and DbClient queries never pool.\n */\n pool?: PooledLiveQueries\n}\n\ntype ConfiguredQueryBuilder<TContext extends Context> = Extract<\n LiveQueryCollectionConfig<TContext>[`query`],\n QueryBuilder<TContext>\n>\n\nexport type UseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> &\n Pick<LiveQueryCollectionConfig<TContext>, `query`>\n\nexport type ConditionalUseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> & {\n query:\n | ConfiguredQueryBuilder<TContext>\n | ((\n q: InitialQueryBuilder,\n ) => ConfiguredQueryBuilder<TContext> | undefined | null)\n }\n\nfunction getCurrentTime(): number {\n return typeof performance !== `undefined` &&\n typeof performance.now === `function`\n ? performance.now()\n : Date.now()\n}\n\nfunction getWarningCallsite(stackIndex: number): string {\n const stack = new Error().stack ?? `unknown`\n const lines = stack.split(`\\n`)\n const userFrame = lines\n .slice(1)\n .find(\n (line) =>\n !line.includes(`useLiveQuery.ts`) &&\n !line.includes(`useLiveSuspenseQuery.ts`) &&\n !line.includes(`useLiveInfiniteQuery.ts`),\n )\n return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack\n}\n\nfunction warnDerivedIdentityHotPath(\n profiler: DerivedIdentityProfiler,\n durationMs: number,\n): void {\n if (\n profiler.warned ||\n !(\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) &&\n process.env.NODE_ENV !== `production`\n )\n ) {\n return\n }\n\n const isSlowSingleRender =\n durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS\n const isHotRenderPath =\n profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&\n profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS\n\n if (!isSlowSingleRender && !isHotRenderPath) {\n return\n }\n\n const callsite = getWarningCallsite(5)\n if (warnedDerivedIdentityCallsites.has(callsite)) {\n profiler.warned = true\n return\n }\n\n warnedDerivedIdentityCallsites.add(callsite)\n profiler.warned = true\n\n const reason = isSlowSingleRender\n ? `one render took ${durationMs.toFixed(1)}ms`\n : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`\n\n console.warn(\n `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +\n `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,\n )\n}\n\nfunction getExplicitDbClient(value: unknown): DbClient | undefined {\n return value &&\n typeof value === `object` &&\n `client` in value &&\n (value as { client?: unknown }).client !== undefined\n ? (value as { client: DbClient }).client\n : undefined\n}\n\nexport function prepareQueryValue(\n value: unknown,\n dbClient: DbClient | undefined,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): unknown {\n return prepareLiveQueryValue(value, dbClient, deferredCollections)\n}\n\ntype DerivedQueryPreparation = {\n value: unknown\n identityDeps: Array<unknown>\n}\n\n/** How a hook checks a prepared query whose identity it compares. */\nexport function getIdentityScope(\n claims: LiveQuerySourceIdClaims,\n dbClient: DbClient | undefined,\n hookName: string,\n): LiveQueryIdentityScope {\n return {\n claims,\n client: dbClient,\n bindingName: hookName,\n onUnboundDescriptor: getUnboundDescriptorReporter(dbClient, hookName),\n }\n}\n\n/**\n * Prepare a query without `queryKey`, derive its identity, and claim its\n * source Collection IDs. A query whose IR has no stable identity throws\n * `UnhashableQueryIRError`. The hook's render boundary releases the sync\n * starts a rejected render deferred.\n */\nexport function prepareDerivedQuery(\n value: unknown,\n profiler: DerivedIdentityProfiler,\n scope: LiveQueryIdentityScope,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): DerivedQueryPreparation {\n const shouldProfile =\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) &&\n process.env.NODE_ENV !== `production`\n const start = shouldProfile ? getCurrentTime() : 0\n const preparedValue = prepareQueryValue(\n value,\n scope.client,\n deferredCollections,\n )\n\n try {\n return {\n value: preparedValue,\n identityDeps: [`derived`, identifyLiveQuery(preparedValue, scope)],\n }\n } finally {\n if (shouldProfile) {\n const durationMs = getCurrentTime() - start\n profiler.renderCount += 1\n profiler.totalMs += durationMs\n profiler.maxMs = Math.max(profiler.maxMs, durationMs)\n warnDerivedIdentityHotPath(profiler, durationMs)\n }\n }\n}\n\n/**\n * Create a live query using a query function.\n * @param queryFn - Query function that defines what data to fetch\n * @returns Object with reactive data, state, and status information\n * @example\n * // Prefer config object syntax\n * const { data, isLoading } = useLiveQuery({\n * query: (q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * })\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * })\n *\n * @example\n * // Structured captured values are included in derived query identity\n * const { data, state } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // Return undefined or null to disable a query\n * const { data, isEnabled } = useLiveQuery({\n * query: (q) => {\n * if (!userId) return undefined\n * return q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.userId, userId))\n * },\n * })\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery({\n * query: (q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * })\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isLoadingError, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todoCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isLoadingError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isEnabled: true // Always true if always returns QueryBuilder\n} & LiveQueryStatusFlags\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isEnabled: boolean\n} & LiveQueryStatusFlags\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isEnabled: boolean\n} & LiveQueryStatusFlags\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isEnabled: boolean\n} & LiveQueryStatusFlags\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n): {\n state:\n Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isEnabled: boolean\n} & LiveQueryStatusFlags\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({\n * query: queryBuilder,\n * })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isLoadingError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isLoadingError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isEnabled: true // Always true when query always returns a builder\n} & LiveQueryStatusFlags\n\n// Overload 7: Accept config object with a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isEnabled: boolean\n} & LiveQueryStatusFlags\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isLoadingError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isLoadingError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 10: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isEnabled: true // Always true for pre-created live query collections\n} & LiveQueryStatusFlags\n\n// Overload 10: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isEnabled: true // Always true for pre-created live query collections\n} & LiveQueryStatusFlags\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(configOrQueryOrCollection: any) {\n return useLiveQueryImpl(configOrQueryOrCollection)\n}\n\n/** @internal Shared implementation for the Suspense wrapper. */\nexport function useLiveQueryForSuspense(configOrQueryOrCollection: any) {\n return useLiveQueryImpl(configOrQueryOrCollection, true)\n}\n\n// What one hook instance keeps across renders. It lives in a single ref\n// slot, so a render neither looks up nor allocates more.\nfunction createHookInstance(dbClient: DbClient | undefined) {\n return {\n collection: null as Collection<object, string | number, {}> | null,\n identityDeps: null as Array<unknown> | null,\n config: null as unknown,\n client: dbClient,\n derivedIdentityProfiler: {\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n } as DerivedIdentityProfiler,\n deferredCollections: new Set<\n CollectionImpl<any, string | number, any, any, any>\n >(),\n sourceIds: createLiveQuerySourceIdClaims(),\n cancelReclaimRelease: undefined as (() => void) | undefined,\n observer: null as LiveQueryObserver<object, string | number> | null,\n queryHash: undefined as string | undefined,\n suspenseKey: undefined as string | undefined,\n identityError: undefined as UnhashableQueryIRError | undefined,\n subscribe: null as ((onStoreChange: () => void) => () => void) | null,\n }\n}\n\nfunction useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) {\n const contextDbClient = useOptionalDbClient()\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n const dbClient = inputIsCollection\n ? contextDbClient\n : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)\n\n const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)\n const instance = (instanceRef.current ??= createHookInstance(dbClient))\n // A commit releases the starts itself, so it drops the reclaim listener.\n const resumeDeferredCollections = () => {\n instance.cancelReclaimRelease?.()\n instance.cancelReclaimRelease = undefined\n resumeDeferredLiveQueryCollections(instance.deferredCollections)\n }\n\n // The render boundary: a render that throws anywhere after preparation\n // never commits, so it releases the sync starts it deferred here.\n try {\n const queryKey = !inputIsCollection\n ? getLiveQueryKey(configOrQueryOrCollection)\n : undefined\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown>\n let streamIdentity: unknown = undefined\n let identityError: UnhashableQueryIRError | undefined\n\n if (queryKey) {\n identityDeps = queryKey\n streamIdentity = [`queryKey`, queryKey]\n } else if (inputIsCollection) {\n identityDeps = []\n streamIdentity = [`collection`, configOrQueryOrCollection.id]\n } else {\n const preparation = prepareDerivedQuery(\n configOrQueryOrCollection,\n instance.derivedIdentityProfiler,\n getIdentityScope(instance.sourceIds, dbClient, `useLiveQuery`),\n instance.deferredCollections,\n )\n preparedQueryValue = preparation.value\n identityDeps = preparation.identityDeps\n streamIdentity = preparation.identityDeps\n }\n\n let queryHash: string | undefined\n if (streamIdentity !== undefined) {\n try {\n queryHash = getStableValueHash(streamIdentity, `queryKey`)\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n if (queryKey !== undefined) throw error\n identityError = error\n } else {\n throw error\n }\n }\n }\n\n const canReuseSuspenseKey =\n forSuspense &&\n !inputIsCollection &&\n queryHash !== undefined &&\n instance.collection !== null &&\n instance.client === dbClient &&\n instance.queryHash === queryHash &&\n instance.suspenseKey !== undefined\n\n if (\n forSuspense &&\n !inputIsCollection &&\n queryHash &&\n !canReuseSuspenseKey &&\n preparedQueryValue === unpreparedQueryValue\n ) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n\n const suspenseKey =\n queryHash && forSuspense && !inputIsCollection\n ? canReuseSuspenseKey\n ? instance.suspenseKey\n : getSourceQualifiedSuspenseKey(\n preparedQueryValue,\n queryHash,\n dbClient,\n )\n : queryHash\n\n const suspenseCollections =\n forSuspense && !inputIsCollection && suspenseKey\n ? getSuspenseCollections(dbClient)\n : undefined\n const suspenseEntry = suspenseKey\n ? suspenseCollections?.get(suspenseKey)\n : undefined\n const suspenseCollection = suspenseEntry?.collection\n\n const identityChanged =\n instance.identityDeps === null ||\n !deepEquals(instance.identityDeps, identityDeps)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !instance.collection ||\n (inputIsCollection && instance.config !== configOrQueryOrCollection) ||\n (!inputIsCollection && (instance.client !== dbClient || identityChanged))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (\n syncMode === `on-demand` &&\n shouldWarnInDevelopment(\n `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,\n ) &&\n process.env.NODE_ENV !== `production`\n ) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n instance.collection = configOrQueryOrCollection\n instance.config = configOrQueryOrCollection\n } else {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n\n if (suspenseCollection) {\n instance.collection = suspenseCollection\n } else {\n // A keyed query claims nothing, so it reports an unbound descriptor\n // here; the other forms reported it when they claimed.\n if (!dbClient && queryKey !== undefined) {\n getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`)\n }\n instance.collection = resolveLiveQueryValue(preparedQueryValue, {\n gcTime: forSuspense\n ? DEFAULT_SUSPENSE_GC_TIME_MS\n : DEFAULT_GC_TIME_MS,\n // Hydration and Suspense key the live-query Collection by identity,\n // so they never pool, even when the config passes a pool.\n pool: forSuspense || dbClient ? false : undefined,\n // Without a DbClient, Suspense starts it through a preload below.\n startSync: !forSuspense || dbClient !== undefined,\n }) as SuspenseCollection | null\n // A render that never commits releases its deferred sync starts\n // when garbage collection reclaims the live query it built. A\n // Collection the callback returned is not this render's to reclaim.\n if (instance.collection && !isCollection(preparedQueryValue)) {\n instance.cancelReclaimRelease?.()\n instance.cancelReclaimRelease = releaseDeferredOnReclaim(\n instance.collection,\n instance.deferredCollections,\n )\n }\n if (suspenseCollections && suspenseKey && instance.collection) {\n const collection = instance.collection\n const removeCleanupListener = collection.on(\n `status:cleaned-up`,\n () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n )\n // Keep a failed or crashed collection through React's immediate\n // retry so the hook can throw its actual error to an ErrorBoundary.\n // Retire it afterward so a later boundary reset starts fresh.\n const retireAfterRetry = () =>\n setTimeout(\n () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n 0,\n )\n const removeErrorListener = collection.on(\n `status:error`,\n retireAfterRetry,\n )\n const removeCrashListener = collection.on(\n `status:crashed`,\n retireAfterRetry,\n )\n const entry: SuspenseCollectionEntry = {\n collection,\n removeStatusListeners: () => {\n removeCleanupListener()\n removeErrorListener()\n removeCrashListener()\n },\n }\n suspenseCollections.set(suspenseKey, entry)\n }\n }\n instance.config = configOrQueryOrCollection\n instance.identityDeps = [...identityDeps]\n }\n instance.client = dbClient\n instance.queryHash = queryHash\n instance.suspenseKey = suspenseKey\n instance.identityError = identityError\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n instance.observer = createLiveQueryObserver(instance.collection, {\n mode: `wholesale`,\n client: dbClient,\n queryHash: instance.queryHash,\n onPreload: resumeDeferredCollections,\n suspendUntilReady: forSuspense,\n })\n }\n const observer = instance.observer!\n // A Suspense render asks for its data. Preloading starts the sync run, so a\n // source that already holds the rows makes it ready before the snapshot is\n // read instead of suspending once. useLiveSuspenseQuery surfaces failures.\n // A DbClient defers source starts past render and owns its streamed query.\n if (\n forSuspense &&\n !dbClient &&\n (instance.collection?.status === `idle` ||\n instance.collection?.status === `cleaned-up`)\n ) {\n observer.preload().catch(() => {})\n }\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n if (!instance.subscribe || needsNewCollection) {\n instance.subscribe = (onStoreChange: () => void) => {\n const unsubscribe = observer.subscribe(onStoreChange)\n resumeDeferredCollections()\n return unsubscribe\n }\n }\n\n const returned = useSyncExternalStore(\n instance.subscribe,\n () => observer.getSnapshot(),\n () => observer.getServerSnapshot(forSuspense),\n )\n // Only useLiveSuspenseQuery reads this, and it costs a define per render.\n if (forSuspense) {\n setLiveQueryResultInfo(returned, {\n client: dbClient,\n queryHash: instance.queryHash,\n identityError: instance.identityError,\n observer,\n deferredCollections: instance.deferredCollections,\n })\n }\n return returned as any\n } catch (error) {\n return rejectLiveQueryInput(instance.deferredCollections, error)\n }\n}\n"],"names":["getPreparedSources","resumeDeferredLiveQueryCollections","shouldWarnInDevelopment","releaseDeferredOnReclaim","useSyncExternalStore","setLiveQueryResultInfo"],"mappings":";;;;;;;;;AA+CA;AAGA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AAIA;AAKE;AAAkBA;AAChB;AACA;AACA;AAEF;AACA;AACF;AAEA;AAGE;AACA;AACA;AACE;AACA;AAAmD;AAErD;AACF;AAEA;AAKE;AACA;AACA;AACA;AACF;AAgDA;AACE;AAIF;AAEA;AACE;AACA;AAA0B;AAC1B;AAEG;AAI2C;AAE9C;AACF;AAEA;AAIE;AAOE;AAAA;AAGF;AAEA;AAIA;AACE;AAAA;AAGF;AACA;AACE;AACA;AAAA;AAGF;AACA;AAEA;AAIA;AAAQ;AAC2I;AAGrJ;AAEA;AACE;AAMF;AAEO;AAKL;AACF;AAQO;AAKL;AAAO;AACL;AACQ;AACK;AACuD;AAExE;AAQO;AAML;AAGA;AACA;AAAsB;AACpB;AACM;AACN;AAGF;AACE;AAAO;AACE;AAC0D;AAAA;AAGnE;AACE;AACA;AACA;AACA;AACA;AAA+C;AACjD;AAEJ;AA2QO;AACL;AACF;AAGO;AACL;AACF;AAIA;AACE;AAAO;AACO;AACE;AACN;AACA;AACiB;AACV;AACJ;AACF;AACC;AAAA;AAEe;AAGd;AACW;AACZ;AACC;AACE;AACE;AACJ;AAEf;AAEA;AACE;AAEA;AACA;AAIA;AACA;AAEA;AACE;AACA;AACAC;AAA+D;AAKjE;AACE;AAGA;AAEA;AACA;AACA;AAEA;AACE;AACA;AAAsC;AAEtC;AACA;AAA4D;AAE5D;AAAoB;AAClB;AACS;AACoD;AACpD;AAEX;AACA;AACA;AAA6B;AAG/B;AACA;AACE;AACE;AAAyD;AAEzD;AACE;AACA;AAAgB;AAEhB;AAAM;AACR;AACF;AAGF;AASA;AAOE;AAAqB;AACnB;AACA;AACS;AAAA;AAIb;AAIQ;AACE;AACA;AACA;AAIV;AAIA;AAGA;AAEA;AAKA;AAKA;AACE;AAIE;AAGA;AAEEC;AACE;AAIF;AAAQ;;AACN;AAAA;;AAAA;AAAA;AAAA;AAQJ;AACA;AACA;AAAkB;AAElB;AACE;AAAqB;AACnB;AACA;AACS;AAAA;AAIb;AACE;AAAsB;AAItB;AACEF;AAA+D;AAEjE;AAAgE;AAG1D;AAAA;AAAA;AAGoC;AAAA;AAEA;AAK1C;AACE;AACA;AAAgCG;AACrB;AACA;AAAA;AAGb;AACE;AACA;AAAyC;AACvC;AAEE;AACE;AACA;AACA;AAAA;AAMN;AACE;AAEI;AACE;AACA;AACA;AAAA;AAEJ;AAEJ;AAAuC;AACrC;AACA;AAEF;AAAuC;AACrC;AACA;AAEF;AAAuC;AACrC;AAEE;AACA;AACA;AAAA;AACF;AAEF;AAA0C;AAC5C;AAEF;AACA;AAAwC;AAE1C;AACA;AACA;AACA;AAAyB;AAS3B;AAOE;AAAiE;AACzD;AACE;AACY;AACT;AACQ;AACpB;AAEH;AAKA;AAME;AAA+B;AAAE;AAKnC;AACE;AACE;AACA;AACA;AAAO;AACT;AAGF;AAAiBC;AACN;AACM;AAC6B;AAG9C;AACEC;AAAiC;AACvB;AACY;AACI;AACxB;AAC8B;AAC/B;AAEH;AAAO;AAEP;AAA+D;AAEnE;;;;;;"}
@@ -1,4 +1,4 @@
1
- import { UnhashableQueryIRError, Collection, CollectionImpl, CollectionStatus, Context, DbClient, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryPersistedStatus, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
1
+ import { Collection, CollectionImpl, CollectionStatus, Context, DbClient, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryIdentityScope, LiveQueryKey, LiveQuerySourceIdClaims, LiveQueryStatusFlags, NonSingleResult, PooledLiveQueries, QueryBuilder, SingleResult } from '@tanstack/db';
2
2
  export type DerivedIdentityProfiler = {
3
3
  renderCount: number;
4
4
  totalMs: number;
@@ -6,7 +6,7 @@ export type DerivedIdentityProfiler = {
6
6
  warned: boolean;
7
7
  };
8
8
  export type UseLiveQueryStatus = CollectionStatus | `disabled`;
9
- export type LiveQueryKey = ReadonlyArray<unknown>;
9
+ export type { LiveQueryKey };
10
10
  type UseLiveQueryConfigOptions<TContext extends Context> = Omit<LiveQueryCollectionConfig<TContext>, `query`> & {
11
11
  /**
12
12
  * Explicit identity for queries that contain opaque functional variants or
@@ -16,29 +16,34 @@ type UseLiveQueryConfigOptions<TContext extends Context> = Omit<LiveQueryCollect
16
16
  queryKey?: LiveQueryKey;
17
17
  /** Override the nearest DbProvider for this query. */
18
18
  client?: DbClient;
19
+ /**
20
+ * Pass `pooledLiveQueries` to share one partition among equality-filtered
21
+ * queries of one shape. Suspense and DbClient queries never pool.
22
+ */
23
+ pool?: PooledLiveQueries;
19
24
  };
20
25
  type ConfiguredQueryBuilder<TContext extends Context> = Extract<LiveQueryCollectionConfig<TContext>[`query`], QueryBuilder<TContext>>;
21
26
  export type UseLiveQueryConfig<TContext extends Context> = UseLiveQueryConfigOptions<TContext> & Pick<LiveQueryCollectionConfig<TContext>, `query`>;
22
27
  export type ConditionalUseLiveQueryConfig<TContext extends Context> = UseLiveQueryConfigOptions<TContext> & {
23
28
  query: ConfiguredQueryBuilder<TContext> | ((q: InitialQueryBuilder) => ConfiguredQueryBuilder<TContext> | undefined | null);
24
29
  };
25
- export declare function warnDeprecatedDepsArray(hookName?: `useLiveQuery` | `useLiveInfiniteQuery`): void;
26
30
  export declare function prepareQueryValue(value: unknown, dbClient: DbClient | undefined, deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>): unknown;
27
31
  type DerivedQueryPreparation = {
28
- status: `hashable`;
29
32
  value: unknown;
30
33
  identityDeps: Array<unknown>;
31
- } | {
32
- status: `unhashable`;
33
- value: unknown;
34
- error: UnhashableQueryIRError;
35
34
  };
36
- export declare function prepareDerivedQuery(value: unknown, dbClient: DbClient | undefined, profiler: DerivedIdentityProfiler, deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>): DerivedQueryPreparation;
37
- export declare function warnUnhashableDerivedIdentity(error: UnhashableQueryIRError): void;
35
+ /** How a hook checks a prepared query whose identity it compares. */
36
+ export declare function getIdentityScope(claims: LiveQuerySourceIdClaims, dbClient: DbClient | undefined, hookName: string): LiveQueryIdentityScope;
37
+ /**
38
+ * Prepare a query without `queryKey`, derive its identity, and claim its
39
+ * source Collection IDs. A query whose IR has no stable identity throws
40
+ * `UnhashableQueryIRError`. The hook's render boundary releases the sync
41
+ * starts a rejected render deferred.
42
+ */
43
+ export declare function prepareDerivedQuery(value: unknown, profiler: DerivedIdentityProfiler, scope: LiveQueryIdentityScope, deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>): DerivedQueryPreparation;
38
44
  /**
39
45
  * Create a live query using a query function.
40
46
  * @param queryFn - Query function that defines what data to fetch
41
- * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
42
47
  * @returns Object with reactive data, state, and status information
43
48
  * @example
44
49
  * // Prefer config object syntax
@@ -91,12 +96,12 @@ export declare function warnUnhashableDerivedIdentity(error: UnhashableQueryIREr
91
96
  *
92
97
  * @example
93
98
  * // Handle loading and error states
94
- * const { data, isLoading, isError, status } = useLiveQuery({
99
+ * const { data, isLoading, isLoadingError, status } = useLiveQuery({
95
100
  * query: (q) => q.from({ todos: todoCollection })
96
101
  * })
97
102
  *
98
103
  * if (isLoading) return <div>Loading...</div>
99
- * if (isError) return <div>Error: {status}</div>
104
+ * if (isLoadingError) return <div>Error: {status}</div>
100
105
  *
101
106
  * return (
102
107
  * <ul>
@@ -104,85 +109,44 @@ export declare function warnUnhashableDerivedIdentity(error: UnhashableQueryIREr
104
109
  * </ul>
105
110
  * )
106
111
  */
107
- export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<unknown>): {
112
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>): {
108
113
  state: Map<string | number, GetResult<TContext>>;
109
114
  data: InferResultType<TContext>;
110
115
  collection: Collection<GetResult<TContext>, string | number, {}>;
111
116
  status: CollectionStatus;
112
- isLoading: boolean;
113
- isReady: boolean;
114
- persistedStatus: LiveQueryPersistedStatus;
115
- isPersistedReady: boolean;
116
- persistedError: unknown | undefined;
117
- isIdle: boolean;
118
- isError: boolean;
119
- isCleanedUp: boolean;
120
117
  isEnabled: true;
121
- };
122
- export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<unknown>): {
118
+ } & LiveQueryStatusFlags;
119
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): {
123
120
  state: Map<string | number, GetResult<TContext>> | undefined;
124
121
  data: InferResultType<TContext> | undefined;
125
122
  collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
126
123
  status: UseLiveQueryStatus;
127
- isLoading: boolean;
128
- isReady: boolean;
129
- persistedStatus: LiveQueryPersistedStatus;
130
- isPersistedReady: boolean;
131
- persistedError: unknown | undefined;
132
- isIdle: boolean;
133
- isError: boolean;
134
- isCleanedUp: boolean;
135
124
  isEnabled: boolean;
136
- };
137
- export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => LiveQueryCollectionConfig<TContext> | undefined | null, deps?: Array<unknown>): {
125
+ } & LiveQueryStatusFlags;
126
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => LiveQueryCollectionConfig<TContext> | undefined | null): {
138
127
  state: Map<string | number, GetResult<TContext>> | undefined;
139
128
  data: InferResultType<TContext> | undefined;
140
129
  collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
141
130
  status: UseLiveQueryStatus;
142
- isLoading: boolean;
143
- isReady: boolean;
144
- persistedStatus: LiveQueryPersistedStatus;
145
- isPersistedReady: boolean;
146
- persistedError: unknown | undefined;
147
- isIdle: boolean;
148
- isError: boolean;
149
- isCleanedUp: boolean;
150
131
  isEnabled: boolean;
151
- };
152
- export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
132
+ } & LiveQueryStatusFlags;
133
+ export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => Collection<TResult, TKey, TUtils> | undefined | null): {
153
134
  state: Map<TKey, TResult> | undefined;
154
135
  data: Array<TResult> | undefined;
155
136
  collection: Collection<TResult, TKey, TUtils> | undefined;
156
137
  status: UseLiveQueryStatus;
157
- isLoading: boolean;
158
- isReady: boolean;
159
- persistedStatus: LiveQueryPersistedStatus;
160
- isPersistedReady: boolean;
161
- persistedError: unknown | undefined;
162
- isIdle: boolean;
163
- isError: boolean;
164
- isCleanedUp: boolean;
165
138
  isEnabled: boolean;
166
- };
167
- export declare function useLiveQuery<TContext extends Context, TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | LiveQueryCollectionConfig<TContext> | Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
139
+ } & LiveQueryStatusFlags;
140
+ export declare function useLiveQuery<TContext extends Context, TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | LiveQueryCollectionConfig<TContext> | Collection<TResult, TKey, TUtils> | undefined | null): {
168
141
  state: Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined;
169
142
  data: InferResultType<TContext> | Array<TResult> | undefined;
170
143
  collection: Collection<GetResult<TContext>, string | number, {}> | Collection<TResult, TKey, TUtils> | undefined;
171
144
  status: UseLiveQueryStatus;
172
- isLoading: boolean;
173
- isReady: boolean;
174
- persistedStatus: LiveQueryPersistedStatus;
175
- isPersistedReady: boolean;
176
- persistedError: unknown | undefined;
177
- isIdle: boolean;
178
- isError: boolean;
179
- isCleanedUp: boolean;
180
145
  isEnabled: boolean;
181
- };
146
+ } & LiveQueryStatusFlags;
182
147
  /**
183
148
  * Create a live query using configuration object
184
149
  * @param config - Configuration object with query and options
185
- * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
186
150
  * @returns Object with reactive data, state, and status information
187
151
  * @example
188
152
  * // Basic config object usage
@@ -204,12 +168,12 @@ export declare function useLiveQuery<TContext extends Context, TResult extends o
204
168
  *
205
169
  * @example
206
170
  * // Handle all states uniformly
207
- * const { data, isLoading, isReady, isError } = useLiveQuery({
171
+ * const { data, isLoading, isReady, isLoadingError } = useLiveQuery({
208
172
  * query: (q) => q.from({ items: itemCollection })
209
173
  * })
210
174
  *
211
175
  * if (isLoading) return <div>Loading...</div>
212
- * if (isError) return <div>Something went wrong</div>
176
+ * if (isLoadingError) return <div>Something went wrong</div>
213
177
  * if (!isReady) return <div>Preparing...</div>
214
178
  *
215
179
  * return <div>{data.length} items loaded</div>
@@ -219,61 +183,15 @@ export declare function useLiveQuery<TContext extends Context>(config: UseLiveQu
219
183
  data: InferResultType<TContext>;
220
184
  collection: Collection<GetResult<TContext>, string | number, {}>;
221
185
  status: CollectionStatus;
222
- isLoading: boolean;
223
- isReady: boolean;
224
- persistedStatus: LiveQueryPersistedStatus;
225
- isPersistedReady: boolean;
226
- persistedError: unknown | undefined;
227
- isIdle: boolean;
228
- isError: boolean;
229
- isCleanedUp: boolean;
230
186
  isEnabled: true;
231
- };
187
+ } & LiveQueryStatusFlags;
232
188
  export declare function useLiveQuery<TContext extends Context>(config: ConditionalUseLiveQueryConfig<TContext>): {
233
189
  state: Map<string | number, GetResult<TContext>> | undefined;
234
190
  data: InferResultType<TContext> | undefined;
235
191
  collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
236
192
  status: UseLiveQueryStatus;
237
- isLoading: boolean;
238
- isReady: boolean;
239
- persistedStatus: LiveQueryPersistedStatus;
240
- isPersistedReady: boolean;
241
- persistedError: unknown | undefined;
242
- isIdle: boolean;
243
- isError: boolean;
244
- isCleanedUp: boolean;
245
- isEnabled: boolean;
246
- };
247
- export declare function useLiveQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>, deps?: Array<unknown>): {
248
- state: Map<string | number, GetResult<TContext>>;
249
- data: InferResultType<TContext>;
250
- collection: Collection<GetResult<TContext>, string | number, {}>;
251
- status: CollectionStatus;
252
- isLoading: boolean;
253
- isReady: boolean;
254
- persistedStatus: LiveQueryPersistedStatus;
255
- isPersistedReady: boolean;
256
- persistedError: unknown | undefined;
257
- isIdle: boolean;
258
- isError: boolean;
259
- isCleanedUp: boolean;
260
- isEnabled: true;
261
- };
262
- export declare function useLiveQuery<TContext extends Context>(config: ConditionalUseLiveQueryConfig<TContext>, deps: Array<unknown>): {
263
- state: Map<string | number, GetResult<TContext>> | undefined;
264
- data: InferResultType<TContext> | undefined;
265
- collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
266
- status: UseLiveQueryStatus;
267
- isLoading: boolean;
268
- isReady: boolean;
269
- persistedStatus: LiveQueryPersistedStatus;
270
- isPersistedReady: boolean;
271
- persistedError: unknown | undefined;
272
- isIdle: boolean;
273
- isError: boolean;
274
- isCleanedUp: boolean;
275
193
  isEnabled: boolean;
276
- };
194
+ } & LiveQueryStatusFlags;
277
195
  /**
278
196
  * Subscribe to an existing live query collection
279
197
  * @param liveQueryCollection - Pre-created live query collection to subscribe to
@@ -296,10 +214,10 @@ export declare function useLiveQuery<TContext extends Context>(config: Condition
296
214
  *
297
215
  * @example
298
216
  * // Handle states consistently
299
- * const { data, isLoading, isError } = useLiveQuery(sharedCollection)
217
+ * const { data, isLoading, isLoadingError } = useLiveQuery(sharedCollection)
300
218
  *
301
219
  * if (isLoading) return <div>Loading...</div>
302
- * if (isError) return <div>Error loading data</div>
220
+ * if (isLoadingError) return <div>Error loading data</div>
303
221
  *
304
222
  * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>
305
223
  */
@@ -308,31 +226,14 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
308
226
  data: Array<TResult>;
309
227
  collection: Collection<TResult, TKey, TUtils>;
310
228
  status: CollectionStatus;
311
- isLoading: boolean;
312
- isReady: boolean;
313
- persistedStatus: LiveQueryPersistedStatus;
314
- isPersistedReady: boolean;
315
- persistedError: unknown | undefined;
316
- isIdle: boolean;
317
- isError: boolean;
318
- isCleanedUp: boolean;
319
229
  isEnabled: true;
320
- };
230
+ } & LiveQueryStatusFlags;
321
231
  export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult): {
322
232
  state: Map<TKey, TResult>;
323
233
  data: TResult | undefined;
324
234
  collection: Collection<TResult, TKey, TUtils> & SingleResult;
325
235
  status: CollectionStatus;
326
- isLoading: boolean;
327
- isReady: boolean;
328
- persistedStatus: LiveQueryPersistedStatus;
329
- isPersistedReady: boolean;
330
- persistedError: unknown | undefined;
331
- isIdle: boolean;
332
- isError: boolean;
333
- isCleanedUp: boolean;
334
236
  isEnabled: true;
335
- };
237
+ } & LiveQueryStatusFlags;
336
238
  /** @internal Shared implementation for the Suspense wrapper. */
337
- export declare function useLiveQueryForSuspense(configOrQueryOrCollection: any, deps: Array<unknown> | undefined): any;
338
- export {};
239
+ export declare function useLiveQueryForSuspense(configOrQueryOrCollection: any): any;
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const react = require("react");
5
+ const db = require("@tanstack/db");
5
6
  const useLiveQuery = require("./useLiveQuery.cjs");
6
7
  const liveQueryInternals = require("./live-query-internals.cjs");
7
8
  const initialRenderErrors = /* @__PURE__ */ new WeakMap();
@@ -24,88 +25,94 @@ function rememberInitialRenderError(collection, client, error, clientQuery) {
24
25
  if (client) entry.byClient.set(client, { error, clientQuery });
25
26
  else entry.unscoped = { error };
26
27
  }
27
- function useLiveSuspenseQuery(configOrQueryOrCollection, deps) {
28
+ function useLiveSuspenseQuery(configOrQueryOrCollection) {
28
29
  const promiseRef = react.useRef(null);
29
30
  const collectionRef = react.useRef(null);
30
31
  const hasBeenReadyRef = react.useRef(false);
31
- const result = deps === void 0 ? useLiveQuery.useLiveQueryForSuspense(configOrQueryOrCollection, void 0) : useLiveQuery.useLiveQueryForSuspense(configOrQueryOrCollection, deps);
32
+ const result = useLiveQuery.useLiveQueryForSuspense(configOrQueryOrCollection);
32
33
  if (!result.isEnabled) {
33
34
  throw new Error(
34
35
  `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). The Suspense pattern requires data to always be defined (T, not T | undefined). Solutions: 1) Use conditional rendering - don't render the component until the condition is met. 2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`
35
36
  );
36
37
  }
37
38
  const queryInfo = liveQueryInternals.getLiveQueryResultInfo(result);
38
- if (collectionRef.current !== result.collection) {
39
- promiseRef.current = null;
40
- collectionRef.current = result.collection;
41
- hasBeenReadyRef.current = false;
42
- }
43
- const collectionStatus = result.collection.status;
44
- if (result.isReady || queryInfo.observer.isInitialRenderReady()) {
45
- hasBeenReadyRef.current = true;
46
- promiseRef.current = null;
47
- clearInitialRenderError(result.collection, queryInfo.client);
48
- }
49
- const observerError = queryInfo.observer.getError();
50
- const clientQuery = queryInfo.client && queryInfo.queryHash ? queryInfo.client._getLiveQuery(queryInfo.queryHash) : void 0;
51
- if (!hasBeenReadyRef.current && (result.persistedStatus === `unavailable` || result.persistedStatus === `error`) && (observerError !== void 0 || clientQuery?.status === `error`)) {
52
- promiseRef.current = null;
53
- throw observerError === void 0 ? clientQuery?.error : observerError;
54
- }
55
- const errorEntry = initialRenderErrors.get(result.collection);
56
- const initialRenderError = queryInfo.client ? errorEntry?.byClient.get(queryInfo.client) : errorEntry?.unscoped;
57
- if (initialRenderError?.clientQuery !== void 0 && initialRenderError.clientQuery !== clientQuery) {
58
- clearInitialRenderError(result.collection, queryInfo.client);
59
- } else if (initialRenderError && !hasBeenReadyRef.current) {
60
- promiseRef.current = null;
61
- throw initialRenderError.error;
62
- }
63
- if (collectionStatus === `error` && !hasBeenReadyRef.current && (result.persistedStatus === `unavailable` || result.persistedStatus === `error`)) {
64
- promiseRef.current = null;
65
- throw new Error(`Collection "${result.collection.id}" failed to load`);
66
- }
67
- if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle || result.isError || collectionStatus === `error` && result.persistedStatus !== `unavailable`)) {
39
+ try {
40
+ if (collectionRef.current !== result.collection) {
41
+ promiseRef.current = null;
42
+ collectionRef.current = result.collection;
43
+ hasBeenReadyRef.current = false;
44
+ }
45
+ const collectionStatus = result.collection.status;
46
+ if (result.isReady) {
47
+ hasBeenReadyRef.current = true;
48
+ promiseRef.current = null;
49
+ clearInitialRenderError(result.collection, queryInfo.client);
50
+ }
51
+ const observerError = queryInfo.observer.getError();
52
+ const clientQuery = queryInfo.client && queryInfo.queryHash ? queryInfo.client._getLiveQuery(queryInfo.queryHash) : void 0;
53
+ if (!hasBeenReadyRef.current && (observerError !== void 0 || clientQuery?.status === `error`)) {
54
+ promiseRef.current = null;
55
+ throw observerError === void 0 ? clientQuery?.error : observerError;
56
+ }
68
57
  if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) {
69
58
  const reason = queryInfo.identityError ? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}` : `the query has no stable identity`;
70
59
  throw new Error(
71
60
  `Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`
72
61
  );
73
62
  }
74
- if (!promiseRef.current) {
75
- const collection = result.collection;
76
- const client = queryInfo.client;
77
- const queryHash = queryInfo.queryHash;
78
- let active = true;
79
- const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {
80
- active = false;
81
- });
82
- let preload;
83
- try {
84
- preload = queryInfo.observer.preloadForInitialRender();
85
- } catch (error) {
86
- stopWatchingCleanup();
87
- throw error;
63
+ const errorEntry = initialRenderErrors.get(result.collection);
64
+ const initialRenderError = queryInfo.client ? errorEntry?.byClient.get(queryInfo.client) : errorEntry?.unscoped;
65
+ if (initialRenderError?.clientQuery !== void 0 && initialRenderError.clientQuery !== clientQuery) {
66
+ clearInitialRenderError(result.collection, queryInfo.client);
67
+ } else if (initialRenderError && !hasBeenReadyRef.current) {
68
+ promiseRef.current = null;
69
+ throw db.toBoundaryError(initialRenderError.error);
70
+ }
71
+ if (collectionStatus === `crashed` || collectionStatus === `error` && !hasBeenReadyRef.current) {
72
+ promiseRef.current = null;
73
+ throw db.toBoundaryError(result.collection.health.error?.cause);
74
+ }
75
+ if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle || result.isLoadingError || result.isCrashed)) {
76
+ if (!promiseRef.current) {
77
+ const collection = result.collection;
78
+ const client = queryInfo.client;
79
+ const queryHash = queryInfo.queryHash;
80
+ let active = true;
81
+ const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {
82
+ active = false;
83
+ });
84
+ let preload;
85
+ try {
86
+ preload = queryInfo.observer.preload();
87
+ } catch (error) {
88
+ stopWatchingCleanup();
89
+ throw error;
90
+ }
91
+ const preloadClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
92
+ promiseRef.current = preload.catch((error) => {
93
+ const latestClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
94
+ if (active && latestClientQuery === preloadClientQuery)
95
+ rememberInitialRenderError(
96
+ collection,
97
+ client,
98
+ error,
99
+ preloadClientQuery
100
+ );
101
+ throw error;
102
+ }).finally(stopWatchingCleanup);
88
103
  }
89
- const preloadClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
90
- promiseRef.current = preload.catch((error) => {
91
- const latestClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
92
- if (active && latestClientQuery === preloadClientQuery)
93
- rememberInitialRenderError(
94
- collection,
95
- client,
96
- error,
97
- preloadClientQuery
98
- );
99
- throw error;
100
- }).finally(stopWatchingCleanup);
104
+ throw promiseRef.current;
101
105
  }
102
- throw promiseRef.current;
106
+ return {
107
+ state: result.state,
108
+ data: result.data,
109
+ collection: result.collection,
110
+ health: result.health
111
+ };
112
+ } catch (error) {
113
+ if (error instanceof Promise) throw error;
114
+ return db.rejectLiveQueryInput(queryInfo.deferredCollections, error);
103
115
  }
104
- return {
105
- state: result.state,
106
- data: result.data,
107
- collection: result.collection
108
- };
109
116
  }
110
117
  exports.useLiveSuspenseQuery = useLiveSuspenseQuery;
111
118
  //# sourceMappingURL=useLiveSuspenseQuery.cjs.map