@tanstack/react-db
Version:
React integration for @tanstack/db
1 lines • 33.6 kB
Source Map (JSON)
{"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;;;;;;"}