@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.
- package/dist/cjs/live-query-internals.cjs.map +1 -1
- package/dist/cjs/live-query-internals.d.cts +3 -1
- package/dist/cjs/source-id-bindings.cjs +9 -42
- package/dist/cjs/source-id-bindings.cjs.map +1 -1
- package/dist/cjs/source-id-bindings.d.cts +6 -17
- package/dist/cjs/useLiveInfiniteQuery.cjs +93 -173
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +3 -14
- package/dist/cjs/useLiveQuery.cjs +187 -224
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +37 -136
- package/dist/cjs/useLiveSuspenseQuery.cjs +71 -64
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
- package/dist/cjs/useLiveSuspenseQuery.d.cts +10 -9
- package/dist/cjs/usePacedMutations.cjs.map +1 -1
- package/dist/cjs/usePacedMutations.d.cts +1 -1
- package/dist/esm/live-query-internals.d.ts +3 -1
- package/dist/esm/live-query-internals.js.map +1 -1
- package/dist/esm/source-id-bindings.d.ts +6 -17
- package/dist/esm/source-id-bindings.js +9 -40
- package/dist/esm/source-id-bindings.js.map +1 -1
- package/dist/esm/useLiveInfiniteQuery.d.ts +3 -14
- package/dist/esm/useLiveInfiniteQuery.js +95 -175
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.d.ts +37 -136
- package/dist/esm/useLiveQuery.js +189 -226
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.d.ts +10 -9
- package/dist/esm/useLiveSuspenseQuery.js +71 -64
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
- package/dist/esm/usePacedMutations.d.ts +1 -1
- package/dist/esm/usePacedMutations.js.map +1 -1
- package/package.json +3 -3
- package/skills/react-db/SKILL.md +24 -15
- package/src/live-query-internals.ts +3 -0
- package/src/source-id-bindings.ts +16 -62
- package/src/useLiveInfiniteQuery.ts +138 -296
- package/src/useLiveQuery.ts +329 -495
- package/src/useLiveSuspenseQuery.ts +122 -132
- 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 {
|
|
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
|
|
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
|
-
|
|
37
|
-
export declare function
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 (
|
|
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,
|
|
217
|
+
* const { data, isLoading, isLoadingError } = useLiveQuery(sharedCollection)
|
|
300
218
|
*
|
|
301
219
|
* if (isLoading) return <div>Loading...</div>
|
|
302
|
-
* if (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|