{"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport { useLiveQuery } from './useLiveQuery'\nimport { getLiveQueryResultInfo } from './live-query-internals'\nimport type { UseLiveQueryConfig } from './useLiveQuery'\nimport type {\n  Collection,\n  Context,\n  GetResult,\n  InferResultType,\n  InitialQueryBuilder,\n  LiveQueryCollectionConfig,\n  NonSingleResult,\n  QueryBuilder,\n  SingleResult,\n} from '@tanstack/db'\n\n/**\n * Create a live query with React Suspense support\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 and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n *   const { data } = useLiveSuspenseQuery({\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 *   return (\n *     <ul>\n *       {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n *     </ul>\n *   )\n * }\n *\n * function App() {\n *   return (\n *     <Suspense fallback={<div>Loading...</div>}>\n *       <TodoList />\n *     </Suspense>\n *   )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n *   (q) => q.from({ todos: todosCollection })\n *          .where(({ todos }) => eq(todos.id, 1))\n *          .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // Structured captured values are included in derived query identity and trigger re-suspension\n * const { data } = useLiveSuspenseQuery({\n *   query: (q) => q.from({ todos: todosCollection })\n *          .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // With Error boundary\n * function App() {\n *   return (\n *     <ErrorBoundary fallback={<div>Error loading data</div>}>\n *       <Suspense fallback={<div>Loading...</div>}>\n *         <TodoList />\n *       </Suspense>\n *     </ErrorBoundary>\n *   )\n * }\n *\n * @remarks\n * **Important:** This hook does NOT support disabled queries (returning undefined/null).\n * Following TanStack Query's useSuspenseQuery design, the query callback must always\n * return a valid query, collection, or config object.\n *\n * ❌ **This will cause a type error:**\n * ```ts\n * useLiveSuspenseQuery(\n *   (q) => userId ? q.from({ users }) : undefined  // ❌ Error!\n * )\n * ```\n *\n * ✅ **Use conditional rendering instead:**\n * ```ts\n * function Profile({ userId }: { userId: string }) {\n *   const { data } = useLiveSuspenseQuery({\n *     query: (q) => q.from({ users }).where(({ users }) => eq(users.id, userId)),\n *   })\n *   return <div>{data.name}</div>\n * }\n *\n * // In parent component:\n * {userId ? <Profile userId={userId} /> : <div>No user</div>}\n * ```\n *\n * ✅ **For optional inputs, conditionally render a component with complete query inputs:**\n * ```ts\n * {userId ? <Profile userId={userId} /> : <div>No user</div>}\n * ```\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<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}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<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}\n\n// Overload 3: Accept legacy config object\nexport function useLiveSuspenseQuery<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}\n\n// Overload 4: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\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}\n\n// Overload 5: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\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}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n  configOrQueryOrCollection: any,\n  deps?: Array<unknown>,\n) {\n  const promiseRef = useRef<Promise<void> | null>(null)\n  const collectionRef = useRef<Collection<any, any, any> | null>(null)\n  const hasBeenReadyRef = useRef(false)\n\n  // Use useLiveQuery to handle collection management and reactivity\n  const result =\n    deps === undefined\n      ? useLiveQuery(configOrQueryOrCollection)\n      : useLiveQuery(configOrQueryOrCollection, deps)\n  const queryInfo = getLiveQueryResultInfo(result)\n\n  // Reset promise and ready state when query identity changes\n  if (collectionRef.current !== result.collection) {\n    promiseRef.current = null\n    collectionRef.current = result.collection\n    hasBeenReadyRef.current = false\n  }\n\n  // SUSPENSE LOGIC: Throw promise or error based on collection status\n  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n  if (!result.isEnabled) {\n    // Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior\n    throw new Error(\n      `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). ` +\n        `The Suspense pattern requires data to always be defined (T, not T | undefined). ` +\n        `Solutions: ` +\n        `1) Use conditional rendering - don't render the component until the condition is met. ` +\n        `2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`,\n    )\n  }\n\n  const collectionStatus = result.collection.status\n\n  // Track when we reach ready state\n  if (result.isReady) {\n    hasBeenReadyRef.current = true\n    promiseRef.current = null\n  }\n\n  const observerError = queryInfo.observer.getError()\n  if (observerError !== undefined && !hasBeenReadyRef.current) {\n    promiseRef.current = null\n    throw observerError\n  }\n\n  // Only throw errors during initial load (before first ready)\n  // After success, errors surface as stale data (matches TanStack Query behavior)\n  if (collectionStatus === `error` && !hasBeenReadyRef.current) {\n    promiseRef.current = null\n    // TODO: Once collections hold a reference to their last error object (#671),\n    // we should rethrow that actual error instead of creating a generic message\n    throw new Error(`Collection \"${result.collection.id}\" failed to load`)\n  }\n\n  if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle)) {\n    if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) {\n      const reason = queryInfo.identityError\n        ? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}`\n        : `the query has no stable identity`\n      throw new Error(\n        `Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`,\n      )\n    }\n\n    // Create or reuse promise for current collection\n    if (!promiseRef.current) {\n      promiseRef.current = queryInfo.observer.preload()\n    }\n    // THROW PROMISE - React Suspense catches this (React 18+ required)\n    // Note: We don't check React version here. In React <18, this will be caught\n    // by an Error Boundary, which provides a reasonable failure mode.\n    throw promiseRef.current\n  }\n\n  // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n  // If error after success, return last known good state (stale data)\n  return {\n    state: result.state,\n    data: result.data,\n    collection: result.collection,\n  }\n}\n"],"names":[],"mappings":";;;;;;AAoKO;AAIL;AACA;AACA;AAGA;AAIA;AAGA;AACE;AACA;AACA;AAA0B;AAK5B;AAEE;AAAU;AACR;AAAA;AAQJ;AAGA;AACE;AACA;AAAqB;AAGvB;AACA;AACE;AACA;AAAM;AAKR;AACE;AAGA;AAAqE;AAGvE;AACE;AACE;AAGA;AAAU;AACkD;AAAA;AAK9D;AACE;AAAwC;AAK1C;AAAiB;AAKnB;AAAO;AACS;AACD;AACM;AAEvB;;"}