{"version":3,"file":"suspense-stream.cjs","names":["useStream"],"sources":["../src/suspense-stream.ts"],"sourcesContent":["/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */\n\n\"use client\";\n\n/**\n * Slim v1 port of `useSuspenseStream`.\n *\n * Rebuilt on top of the v2-native {@link useStream} hook. The legacy\n * implementation (pre-v1) prefetched `threads.getHistory(threadId)`\n * into an external `SuspenseCache` because the legacy hook had no\n * hydration affordance. v1 drops the `history` surface entirely and\n * exposes {@link StreamController.hydrationPromise} directly, so the\n * suspense integration reduces to:\n *\n *   1. Suspend (throw the hydration promise) while the first hydrate\n *      call for the current thread is in flight.\n *   2. Throw non-streaming errors to the nearest Error Boundary.\n *   3. Rename `isLoading` → `isStreaming` so the caller can model a\n *      rendered-but-streaming state distinct from the suspended\n *      initial-load state.\n *\n * Dropped from the pre-v1 surface (replaced by the built-in hydrate\n * lifecycle or no longer applicable once `history` is gone):\n *\n * - `SuspenseCache`, `createSuspenseCache`, `invalidateSuspenseCache`\n * - the `suspenseCache` option\n * - the `fetchStateHistory: { limit }` prefetch knob\n */\n\nimport type { Interrupt } from \"@langchain/langgraph-sdk\";\nimport type {\n  AssembledToolCall,\n  SubagentDiscoverySnapshot,\n  SubgraphDiscoverySnapshot,\n  SubmissionQueueEntry,\n  SubmissionQueueSnapshot,\n  InferStateType,\n} from \"@langchain/langgraph-sdk/stream\";\nimport {\n  useStream,\n  type UseStreamOptions,\n  type UseStreamReturn,\n} from \"./use-stream.js\";\n\n/**\n * Return shape of {@link useSuspenseStream}. Identical to the\n * {@link UseStreamReturn} surface except:\n *\n * - `isLoading` / `isThreadLoading` / `hydrationPromise` are removed\n *   (Suspense and Error Boundaries handle those phases).\n * - `isStreaming: boolean` is added so callers can show a streaming\n *   indicator distinct from the suspended initial-load state.\n */\nexport type UseSuspenseStreamReturn<\n  T = Record<string, unknown>,\n  InterruptType = unknown,\n  ConfigurableType extends object = Record<string, unknown>,\n> = Omit<\n  UseStreamReturn<T, InterruptType, ConfigurableType>,\n  \"isLoading\" | \"isThreadLoading\" | \"hydrationPromise\"\n> & {\n  /**\n   * Whether the stream is currently receiving data from the server.\n   * Unlike the suspended initial-load state, the component stays\n   * mounted while `isStreaming` is `true`.\n   */\n  isStreaming: boolean;\n};\n\n/**\n * Suspense-compatible variant of {@link useStream}.\n *\n * Suspends the component while the initial thread hydration is in\n * flight and throws non-streaming errors to the nearest Error\n * Boundary. During active streaming the component stays rendered and\n * {@link UseSuspenseStreamReturn.isStreaming} indicates whether\n * tokens are arriving.\n *\n * @example\n * ```tsx\n * <ErrorBoundary fallback={<ErrorDisplay />}>\n *   <Suspense fallback={<Spinner />}>\n *     <Chat />\n *   </Suspense>\n * </ErrorBoundary>\n *\n * function Chat() {\n *   const { messages, submit, isStreaming } = useSuspenseStream({\n *     assistantId: \"agent\",\n *     apiUrl: \"http://localhost:2024\",\n *     threadId,\n *   });\n *   return <MessageList messages={messages} streaming={isStreaming} />;\n * }\n * ```\n */\n/**\n * Module-level cache of in-flight / settled hydration attempts keyed\n * on a stable `(apiUrl, assistantId, threadId)` tuple. Required\n * because React Suspense discards the suspended fiber while the\n * thrown promise is unresolved, so the `StreamController` (and its\n * `hydrationPromise`) created in one render is thrown away before\n * the next render runs. Without this external store we'd spawn a\n * fresh controller on every retry and never converge.\n *\n * The cache stores the settled outcome (success/failure) so\n * re-renders after commit short-circuit without waiting. Entries are\n * keyed loosely — two mounts of the same `(apiUrl, assistantId,\n * threadId)` tuple share a single hydration.\n */\ninterface SuspenseEntry {\n  promise: Promise<void>;\n  settled: boolean;\n  error?: Error;\n}\nconst suspenseEntries = new Map<string, SuspenseEntry>();\n\nfunction suspenseKey(options: {\n  apiUrl?: string;\n  assistantId?: string;\n  threadId?: string | null;\n}): string | null {\n  if (options.threadId == null) return null;\n  return `${options.apiUrl ?? \"_\"}::${options.assistantId ?? \"_\"}::${options.threadId}`;\n}\n\nexport function useSuspenseStream<T = Record<string, unknown>>(\n  options: UseStreamOptions<InferStateType<T>>\n): UseSuspenseStreamReturn<T> {\n  const asBag = options as {\n    apiUrl?: string;\n    assistantId?: string;\n    threadId?: string | null;\n  };\n  const key = suspenseKey(asBag);\n\n  const stream = useStream<T>(options as Parameters<typeof useStream<T>>[0]);\n\n  // First render for this `(apiUrl, assistantId, threadId)`: install\n  // an entry that tracks the current controller's hydration. The\n  // same promise is thrown on every retry, so React Suspense sees a\n  // stable dependency even when the fiber — and its controller — is\n  // discarded and rebuilt between retries.\n  if (key != null && !suspenseEntries.has(key)) {\n    const entry: SuspenseEntry = {\n      promise: stream.hydrationPromise.then(\n        () => {\n          entry.settled = true;\n        },\n        (error) => {\n          entry.settled = true;\n          entry.error =\n            // eslint-disable-next-line no-instanceof/no-instanceof\n            error instanceof Error ? error : new Error(String(error));\n          throw entry.error;\n        }\n      ),\n      settled: false,\n    };\n    suspenseEntries.set(key, entry);\n  }\n\n  const entry = key != null ? suspenseEntries.get(key) : undefined;\n\n  // Suspend until the first hydrate settles. The promise is stable\n  // across Suspense retries because it's anchored in the module-\n  // level cache, not in the per-render controller.\n  if (entry && !entry.settled) {\n    // eslint-disable-next-line @typescript-eslint/no-throw-literal\n    throw entry.promise;\n  }\n\n  // Propagate hydrate failures to the nearest Error Boundary once.\n  if (entry?.error != null) {\n    throw entry.error;\n  }\n\n  // Hydration errors surface via `stream.error` after the promise\n  // rejects; hand them to the nearest Error Boundary when no run is\n  // currently active (streaming errors must stay in-hook so the UI\n  // can recover without losing partial content).\n  if (stream.error != null && !stream.isLoading) {\n    // eslint-disable-next-line no-instanceof/no-instanceof\n    throw stream.error instanceof Error\n      ? stream.error\n      : new Error(String(stream.error));\n  }\n\n  // Build the return object with explicit getters so lazy access\n  // still reflects the latest snapshot even if the caller destructures\n  // late in a render.\n  return {\n    get values() {\n      return stream.values as UseSuspenseStreamReturn<T>[\"values\"];\n    },\n    get messages() {\n      return stream.messages;\n    },\n    get toolCalls(): AssembledToolCall[] {\n      return stream.toolCalls;\n    },\n    get interrupt(): Interrupt | undefined {\n      return stream.interrupt as Interrupt | undefined;\n    },\n    get interrupts(): Interrupt[] {\n      return stream.interrupts as Interrupt[];\n    },\n    get subagents() {\n      return stream.subagents as ReadonlyMap<\n        string,\n        SubagentDiscoverySnapshot\n      > as UseSuspenseStreamReturn<T>[\"subagents\"];\n    },\n    get subgraphs(): ReadonlyMap<string, SubgraphDiscoverySnapshot> {\n      return stream.subgraphs;\n    },\n    get subgraphsByNode(): ReadonlyMap<\n      string,\n      readonly SubgraphDiscoverySnapshot[]\n    > {\n      return stream.subgraphsByNode;\n    },\n    submit: stream.submit as UseSuspenseStreamReturn<T>[\"submit\"],\n    stop: stream.stop,\n    disconnect: stream.disconnect,\n    respond: stream.respond as UseSuspenseStreamReturn<T>[\"respond\"],\n    getThread: stream.getThread,\n    get client() {\n      return stream.client;\n    },\n    get assistantId() {\n      return stream.assistantId;\n    },\n    get threadId() {\n      return stream.threadId;\n    },\n    get error() {\n      return stream.error;\n    },\n    get isStreaming() {\n      return stream.isLoading;\n    },\n  } as UseSuspenseStreamReturn<T>;\n}\n\n// Re-export the transitional companion types so existing call sites\n// keep resolving without reaching into `./use-stream.js` directly.\nexport type { SubmissionQueueEntry, SubmissionQueueSnapshot };\n"],"mappings":";;;AAmHA,MAAM,kCAAkB,IAAI,KAA4B;AAExD,SAAS,YAAY,SAIH;AAChB,KAAI,QAAQ,YAAY,KAAM,QAAO;AACrC,QAAO,GAAG,QAAQ,UAAU,IAAI,IAAI,QAAQ,eAAe,IAAI,IAAI,QAAQ;;AAG7E,SAAgB,kBACd,SAC4B;CAM5B,MAAM,MAAM,YALE,QAKgB;CAE9B,MAAM,SAASA,mBAAAA,UAAa,QAA8C;AAO1E,KAAI,OAAO,QAAQ,CAAC,gBAAgB,IAAI,IAAI,EAAE;EAC5C,MAAM,QAAuB;GAC3B,SAAS,OAAO,iBAAiB,WACzB;AACJ,UAAM,UAAU;OAEjB,UAAU;AACT,UAAM,UAAU;AAChB,UAAM,QAEJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;AAC3D,UAAM,MAAM;KAEf;GACD,SAAS;GACV;AACD,kBAAgB,IAAI,KAAK,MAAM;;CAGjC,MAAM,QAAQ,OAAO,OAAO,gBAAgB,IAAI,IAAI,GAAG,KAAA;AAKvD,KAAI,SAAS,CAAC,MAAM,QAElB,OAAM,MAAM;AAId,KAAI,OAAO,SAAS,KAClB,OAAM,MAAM;AAOd,KAAI,OAAO,SAAS,QAAQ,CAAC,OAAO,UAElC,OAAM,OAAO,iBAAiB,QAC1B,OAAO,QACP,IAAI,MAAM,OAAO,OAAO,MAAM,CAAC;AAMrC,QAAO;EACL,IAAI,SAAS;AACX,UAAO,OAAO;;EAEhB,IAAI,WAAW;AACb,UAAO,OAAO;;EAEhB,IAAI,YAAiC;AACnC,UAAO,OAAO;;EAEhB,IAAI,YAAmC;AACrC,UAAO,OAAO;;EAEhB,IAAI,aAA0B;AAC5B,UAAO,OAAO;;EAEhB,IAAI,YAAY;AACd,UAAO,OAAO;;EAKhB,IAAI,YAA4D;AAC9D,UAAO,OAAO;;EAEhB,IAAI,kBAGF;AACA,UAAO,OAAO;;EAEhB,QAAQ,OAAO;EACf,MAAM,OAAO;EACb,YAAY,OAAO;EACnB,SAAS,OAAO;EAChB,WAAW,OAAO;EAClB,IAAI,SAAS;AACX,UAAO,OAAO;;EAEhB,IAAI,cAAc;AAChB,UAAO,OAAO;;EAEhB,IAAI,WAAW;AACb,UAAO,OAAO;;EAEhB,IAAI,QAAQ;AACV,UAAO,OAAO;;EAEhB,IAAI,cAAc;AAChB,UAAO,OAAO;;EAEjB"}