{"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport {\n  getLiveQueryStatusFlags,\n  getSourceObjectToken,\n  getStableValueHash,\n  rejectLiveQueryInput,\n  toBoundaryError,\n} from '@tanstack/db'\nimport { useLiveQueryImpl } from './useLiveQuery'\nimport { getPreparedSources } from './source-id-bindings'\nimport type { UseLiveQueryConfig } from './useLiveQuery'\nimport type {\n  Collection,\n  Context,\n  DbClient,\n  GetResult,\n  HealthNode,\n  InferResultType,\n  InitialQueryBuilder,\n  LiveQueryObserver,\n  LiveQuerySnapshot,\n  NonSingleResult,\n  QueryBuilder,\n  SingleResult,\n} from '@tanstack/db'\n\ntype SuspenseCollection = Collection<object, string | number, {}>\ntype SuspenseCollectionEntry = {\n  collection: SuspenseCollection\n  windowOwner: object\n  owner?: SuspenseBinding\n  observer?: LiveQueryObserver<object, string | number>\n  release: () => void\n}\n\n// Retry resources and first-load errors use the same scope without a DbClient.\nconst unscopedClient = {}\nconst suspenseCollectionsByClient = new WeakMap<\n  object,\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  const {\n    query: _query,\n    pool: _pool,\n    ...options\n  } = (preparedValue as { query?: unknown } | null | undefined)?.query\n    ? (preparedValue as Record<string, unknown>)\n    : {}\n  // Local output options qualify pending retry adoption; query identity binds\n  // mounted options. Object/function declarations compare by exact runtime reference.\n  options.autoIndex ??= `off`\n  for (const key in options) {\n    const value = options[key]\n    if (value === undefined) delete options[key]\n    else if (\n      value &&\n      (typeof value === `object` || typeof value === `function`)\n    )\n      options[key] = getSourceObjectToken(value)\n  }\n  return getStableValueHash([sourceIds, options, queryHash])\n}\n\n/** Uncommitted retries may adopt work; successful outputs have one owner. */\nexport class SuspenseBinding {\n  private entry?: SuspenseCollectionEntry\n  private seed?: LiveQuerySnapshot<object, string | number>\n  collection?: Collection<any, any, any>\n  promise?: Promise<void>\n  ready = false\n\n  get needsOwnCollection(): boolean {\n    return this.entry !== undefined && this.entry.owner !== this\n  }\n\n  resolve(\n    input: unknown,\n    hash: string,\n    client: DbClient | undefined,\n    compile: (owned: boolean, windowOwner: object) => SuspenseCollection | null,\n  ): SuspenseCollection | null {\n    const scope = client ?? unscopedClient\n    let cache = suspenseCollectionsByClient.get(scope)\n    if (!cache) {\n      cache = new Map()\n      suspenseCollectionsByClient.set(scope, cache)\n    }\n    const owned = this.entry?.owner === this\n    const key = owned\n      ? undefined\n      : getSourceQualifiedSuspenseKey(input, hash, client)\n    const cached = key === undefined ? undefined : cache.get(key)\n    if (cached) return (this.entry = cached).collection\n\n    // A new attempt gets a fresh resolver token. Adoption retains that token;\n    // another attempt must not overwrite the adopted Collection's plan.\n    const windowOwner = owned ? this.entry!.windowOwner : {}\n    const collection = compile(owned, windowOwner)\n    if (owned && collection === this.entry!.collection) return collection\n    this.seed = undefined\n    if (collection && this.entry?.collection !== collection) {\n      const entryKey = key ?? getSourceQualifiedSuspenseKey(input, hash, client)\n      const release = () => {\n        if (cache.get(entryKey) === entry) cache.delete(entryKey)\n        removeStatusListener()\n      }\n      const entry = { collection, windowOwner, release }\n      this.entry = entry\n      cache.set(entryKey, entry)\n      const removeStatusListener = collection.on(\n        `status:change`,\n        ({ status }) => {\n          if (status === `cleaned-up`) release()\n          // Preserve failures through React's immediate retry.\n          else if (status === `error` || status === `crashed`)\n            setTimeout(release, 0)\n        },\n      )\n    }\n    return collection\n  }\n\n  // The first successful candidate may keep the preload. Other candidates get\n  // private outputs seeded from its completed snapshot. Keep the retry entry\n  // available until commit, since React can discard a successful candidate.\n  forkReady(\n    observer: LiveQueryObserver<object, string | number>,\n    compile: (owned: boolean, windowOwner: object) => SuspenseCollection | null,\n  ): SuspenseCollection | undefined {\n    if (!this.entry || this.entry.owner === this) return\n    const resource = (this.entry.observer ??= observer)\n    const snapshot =\n      typeof window === `undefined`\n        ? resource.getServerSnapshot(true)\n        : resource.getSnapshot()\n    if (!snapshot.isReady) return\n    if (!this.entry.owner) {\n      this.entry.owner = this\n      return\n    }\n    const windowOwner = {}\n    const collection = compile(false, windowOwner)\n    if (!collection) return\n    this.seed = { ...snapshot, collection }\n    this.entry = {\n      collection,\n      windowOwner,\n      owner: this,\n      release: this.entry.release,\n    }\n    return collection\n  }\n\n  read(snapshot: LiveQuerySnapshot<object, string | number>) {\n    if (snapshot.isReady || snapshot.isLoadingError || snapshot.isCrashed) {\n      // A copied ready view is provisional until this output's first load.\n      // Its failure must end both the seed and the hook's ready latch.\n      if (this.seed && snapshot.isLoadingError) this.ready = false\n      this.seed = undefined\n    }\n    // Copy only rows from the peer. This output's observer owns its health,\n    // including source notifications and query-local request failures.\n    if (this.seed && snapshot.health !== this.seed.health) {\n      this.seed = {\n        ...this.seed,\n        ...getLiveQueryStatusFlags(`ready`, snapshot.health),\n      }\n    }\n    return this.seed ?? snapshot\n  }\n\n  claim(): (() => void) | undefined {\n    // Capture this render's entry: React can replay an older commit's effect\n    // after a replacement suspends. That effect must release only its entry.\n    return this.entry?.release\n  }\n}\n\n// React can discard a render that suspends, including its refs. Keep the\n// initial-render failure across retries, scoped by client because streamed\n// preloads of the same collection can fail independently.\nconst initialRenderErrors = new WeakMap<\n  Collection<any, any, any>,\n  WeakMap<object, { error: unknown; clientQuery?: object }>\n>()\n\nfunction clearInitialRenderError(\n  collection: Collection<any, any, any>,\n  client: DbClient | undefined,\n) {\n  initialRenderErrors.get(collection)?.delete(client ?? unscopedClient)\n}\n\nfunction rememberInitialRenderError(\n  collection: Collection<any, any, any>,\n  client: DbClient | undefined,\n  error: unknown,\n  clientQuery?: object,\n) {\n  if (collection.status === `cleaned-up`) return\n  let entry = initialRenderErrors.get(collection)\n  if (!entry) {\n    entry = new WeakMap()\n    collection.once(`status:cleaned-up`, () => {\n      initialRenderErrors.delete(collection)\n    })\n    initialRenderErrors.set(collection, entry)\n  }\n  entry.set(client ?? unscopedClient, { error, clientQuery })\n}\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\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 the first load fails or the Collection crashes (caught by\n * an Error boundary): the cause itself when it is an Error, otherwise an Error\n * with the problem's message (`health.error.message`) whose `cause` is the\n * value the sync adapter reported\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): {\n  state: Map<string | number, GetResult<TContext>>\n  data: InferResultType<TContext>\n  collection: Collection<GetResult<TContext>, string | number, {}>\n  health: HealthNode | undefined\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  health: HealthNode | undefined\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  health: HealthNode | undefined\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  health: HealthNode | undefined\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(configOrQueryOrCollection: any) {\n  const bindingRef = useRef<SuspenseBinding | null>(null)\n  const binding = (bindingRef.current ??= new SuspenseBinding())\n\n  // Use useLiveQuery to handle collection management and reactivity\n  const { result, client, queryHash, observer, deferredCollections } =\n    useLiveQueryImpl(configOrQueryOrCollection, binding)\n\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  // An enabled observer snapshot always has its Collection.\n  const collection = result.collection!\n\n  // The render boundary: an error thrown before this render commits releases\n  // the sync starts it deferred. A thrown promise suspends; its preload owns\n  // those starts.\n  try {\n    // Reset promise and ready state when query identity changes\n    if (binding.collection !== collection) {\n      binding.promise = undefined\n      binding.collection = collection\n      binding.ready = false\n    }\n\n    // SUSPENSE LOGIC: Throw promise or error based on collection status\n\n    const collectionStatus = collection.status\n\n    // Track when we reach ready state\n    if (result.isReady) {\n      binding.ready = true\n      binding.promise = undefined\n      clearInitialRenderError(collection, client)\n    }\n\n    const observerError = observer.getError()\n    // A client request can reject with undefined, which getError() cannot\n    // distinguish from no error. The request status preserves that distinction.\n    const clientQuery =\n      client && queryHash ? client._getLiveQuery(queryHash) : undefined\n    if (\n      !binding.ready &&\n      (observerError !== undefined || clientQuery?.status === `error`)\n    ) {\n      throw observerError === undefined ? clientQuery?.error : observerError\n    }\n\n    const initialRenderError = initialRenderErrors\n      .get(collection)\n      ?.get(client ?? unscopedClient)\n    // A replacement client query must not inherit the prior request's error.\n    if (\n      initialRenderError?.clientQuery !== undefined &&\n      initialRenderError.clientQuery !== clientQuery\n    ) {\n      clearInitialRenderError(collection, client)\n    } else if (initialRenderError && !binding.ready) {\n      throw toBoundaryError(initialRenderError.error)\n    }\n\n    // A failed first load throws only before the first ready; later sync\n    // problems surface in `health` with the data kept (as in TanStack Query).\n    // A crash throws at any time: the Collection can no longer serve reads.\n    if (\n      collectionStatus === `crashed` ||\n      (collectionStatus === `error` && !binding.ready)\n    ) {\n      throw toBoundaryError(collection.health.error?.cause)\n    }\n\n    if (\n      !binding.ready &&\n      (result.isLoading ||\n        result.isIdle ||\n        result.isLoadingError ||\n        result.isCrashed)\n    ) {\n      // Create or reuse promise for current collection\n      if (!binding.promise) {\n        let active = true\n        const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {\n          active = false\n        })\n        let preload: Promise<void>\n        try {\n          preload = observer.preload()\n        } catch (error) {\n          stopWatchingCleanup()\n          throw error\n        }\n        const preloadClientQuery =\n          client && queryHash ? client._getLiveQuery(queryHash) : undefined\n        binding.promise = preload\n          .catch((error: unknown) => {\n            const latestClientQuery =\n              client && queryHash ? client._getLiveQuery(queryHash) : undefined\n            if (active && latestClientQuery === preloadClientQuery)\n              rememberInitialRenderError(\n                collection,\n                client,\n                error,\n                preloadClientQuery,\n              )\n            throw error\n          })\n          .finally(stopWatchingCleanup)\n      }\n      // React Suspense catches this promise and retries after preload settles.\n      throw binding.promise\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,\n      health: result.health,\n    }\n  } catch (error) {\n    if (error instanceof Promise) throw error\n    binding.promise = undefined\n    return rejectLiveQueryInput(deferredCollections, error)\n  }\n}\n"],"names":["getPreparedSources"],"mappings":";;;;;;;AAsCA;AACA;AAIA;AAKE;AAAkBA;AAChB;AACA;AACA;AAEF;AAAM;AACG;AACD;AACH;AAML;AACA;AACE;AACA;AAA2C;AAKzC;AAAyC;AAE7C;AACF;AAGO;AAAsB;AAK3B;AAAQ;AAAA;AAGN;AAAwD;AAC1D;AAQE;AACA;AACA;AACE;AACA;AAA4C;AAE9C;AACA;AAGA;AACA;AAIA;AACA;AACA;AACA;AACA;AACE;AACA;AACE;AACA;AAAA;AAEF;AACA;AACA;AACA;AAAwC;AACtC;AAEE;AAA6B;AAG3B;AAAqB;AACzB;AAAA;AAGJ;AAAO;AACT;AAAA;AAAA;AAAA;AASE;AACA;AACA;AAIA;AACA;AACE;AACA;AAAA;AAEF;AACA;AACA;AACA;AACA;AAAa;AACX;AACA;AACO;AACa;AAEtB;AAAO;AACT;AAGE;AAGE;AACA;AAAY;AAId;AACE;AAAY;AACF;AAC2C;AAAA;AAGvD;AAAoB;AACtB;AAKE;AAAmB;AAEvB;AAKA;AAKA;AAIE;AACF;AAEA;AAME;AACA;AACA;AACE;AACA;AACE;AAAqC;AAEvC;AAAyC;AAE3C;AACF;AA+IO;AACL;AACA;AAGA;AAGA;AAEE;AAAU;AACR;AAAA;AASJ;AAKA;AAEE;AACE;AACA;AACA;AAAgB;AAKlB;AAGA;AACE;AACA;AACA;AAA0C;AAG5C;AAGA;AAEA;AAIE;AAAyD;AAG3D;AAIA;AAIE;AAA0C;AAE1C;AAA8C;AAMhD;AAIE;AAAoD;AAGtD;AAQE;AACE;AACA;AACE;AAAS;AAEX;AACA;AACE;AAAmB;AAEnB;AACA;AAAM;AAER;AAEA;AAEI;AAEA;AACE;AAAA;AACE;AACA;AACA;AACA;AAEJ;AAAM;AAEoB;AAGhC;AAAc;AAKhB;AAAO;AACS;AACD;AACb;AACe;AAAA;AAGjB;AACA;AACA;AAAsD;AAE1D;;;"}