@langchain/react 0.3.4 → 1.0.0
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/README.md +48 -523
- package/dist/context.cjs +12 -30
- package/dist/context.cjs.map +1 -1
- package/dist/context.d.cts +22 -39
- package/dist/context.d.cts.map +1 -1
- package/dist/context.d.ts +22 -39
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -29
- package/dist/context.js.map +1 -1
- package/dist/index.cjs +29 -30
- package/dist/index.d.cts +10 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/selectors.cjs +178 -0
- package/dist/selectors.cjs.map +1 -0
- package/dist/selectors.d.cts +183 -0
- package/dist/selectors.d.cts.map +1 -0
- package/dist/selectors.d.ts +183 -0
- package/dist/selectors.d.ts.map +1 -0
- package/dist/selectors.js +168 -0
- package/dist/selectors.js.map +1 -0
- package/dist/suspense-stream.cjs +34 -159
- package/dist/suspense-stream.cjs.map +1 -1
- package/dist/suspense-stream.d.cts +15 -71
- package/dist/suspense-stream.d.cts.map +1 -1
- package/dist/suspense-stream.d.ts +15 -71
- package/dist/suspense-stream.d.ts.map +1 -1
- package/dist/suspense-stream.js +35 -158
- package/dist/suspense-stream.js.map +1 -1
- package/dist/use-audio-player.cjs +679 -0
- package/dist/use-audio-player.cjs.map +1 -0
- package/dist/use-audio-player.d.cts +161 -0
- package/dist/use-audio-player.d.cts.map +1 -0
- package/dist/use-audio-player.d.ts +161 -0
- package/dist/use-audio-player.d.ts.map +1 -0
- package/dist/use-audio-player.js +679 -0
- package/dist/use-audio-player.js.map +1 -0
- package/dist/use-media-url.cjs +49 -0
- package/dist/use-media-url.cjs.map +1 -0
- package/dist/use-media-url.d.cts +28 -0
- package/dist/use-media-url.d.cts.map +1 -0
- package/dist/use-media-url.d.ts +28 -0
- package/dist/use-media-url.d.ts.map +1 -0
- package/dist/use-media-url.js +49 -0
- package/dist/use-media-url.js.map +1 -0
- package/dist/use-projection.cjs +41 -0
- package/dist/use-projection.cjs.map +1 -0
- package/dist/use-projection.d.cts +27 -0
- package/dist/use-projection.d.cts.map +1 -0
- package/dist/use-projection.d.ts +27 -0
- package/dist/use-projection.d.ts.map +1 -0
- package/dist/use-projection.js +41 -0
- package/dist/use-projection.js.map +1 -0
- package/dist/use-stream.cjs +185 -0
- package/dist/use-stream.cjs.map +1 -0
- package/dist/use-stream.d.cts +184 -0
- package/dist/use-stream.d.cts.map +1 -0
- package/dist/use-stream.d.ts +184 -0
- package/dist/use-stream.d.ts.map +1 -0
- package/dist/use-stream.js +183 -0
- package/dist/use-stream.js.map +1 -0
- package/dist/use-video-player.cjs +218 -0
- package/dist/use-video-player.cjs.map +1 -0
- package/dist/use-video-player.d.cts +65 -0
- package/dist/use-video-player.d.cts.map +1 -0
- package/dist/use-video-player.d.ts +65 -0
- package/dist/use-video-player.d.ts.map +1 -0
- package/dist/use-video-player.js +218 -0
- package/dist/use-video-player.js.map +1 -0
- package/package.json +9 -8
- package/dist/stream.cjs +0 -18
- package/dist/stream.cjs.map +0 -1
- package/dist/stream.custom.cjs +0 -209
- package/dist/stream.custom.cjs.map +0 -1
- package/dist/stream.custom.d.cts +0 -3
- package/dist/stream.custom.d.ts +0 -3
- package/dist/stream.custom.js +0 -209
- package/dist/stream.custom.js.map +0 -1
- package/dist/stream.d.cts +0 -174
- package/dist/stream.d.cts.map +0 -1
- package/dist/stream.d.ts +0 -174
- package/dist/stream.d.ts.map +0 -1
- package/dist/stream.js +0 -18
- package/dist/stream.js.map +0 -1
- package/dist/stream.lgp.cjs +0 -671
- package/dist/stream.lgp.cjs.map +0 -1
- package/dist/stream.lgp.js +0 -671
- package/dist/stream.lgp.js.map +0 -1
- package/dist/thread.cjs +0 -18
- package/dist/thread.cjs.map +0 -1
- package/dist/thread.js +0 -18
- package/dist/thread.js.map +0 -1
- package/dist/types.d.cts +0 -109
- package/dist/types.d.cts.map +0 -1
- package/dist/types.d.ts +0 -109
- package/dist/types.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suspense-stream.cjs","names":["Client","useStreamLGP"],"sources":["../src/suspense-stream.tsx"],"sourcesContent":["/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */\n\n\"use client\";\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { ThreadState, BagTemplate } from \"@langchain/langgraph-sdk\";\nimport { Client, getClientConfigHash } from \"@langchain/langgraph-sdk/client\";\nimport type {\n UseStreamThread,\n ResolveStreamOptions,\n ResolveStreamInterface,\n InferBag,\n} from \"@langchain/langgraph-sdk/ui\";\nimport { useStreamLGP } from \"./stream.lgp.js\";\nimport type { WithClassMessages } from \"./stream.js\";\n\n// ---------------------------------------------------------------------------\n// Suspense cache\n// ---------------------------------------------------------------------------\n\nexport type SuspenseCacheEntry<T> =\n | { status: \"pending\"; promise: Promise<void> }\n | { status: \"resolved\"; data: T }\n | { status: \"rejected\"; error: unknown };\n\nexport type SuspenseCache = Map<string, SuspenseCacheEntry<unknown>>;\n\nconst defaultSuspenseCache: SuspenseCache = new Map();\n\nexport function createSuspenseCache(): SuspenseCache {\n return new Map();\n}\n\nfunction getCacheKey(\n client: Client,\n threadId: string,\n limit: boolean | number\n): string {\n return `suspense:${getClientConfigHash(client)}:${threadId}:${limit}`;\n}\n\nfunction fetchThreadHistory<StateType extends Record<string, unknown>>(\n client: Client,\n threadId: string,\n options?: { limit?: boolean | number }\n): Promise<ThreadState<StateType>[]> {\n if (options?.limit === false) {\n return client.threads.getState<StateType>(threadId).then((state) => {\n if (state.checkpoint == null) return [];\n return [state];\n });\n }\n\n const limit = typeof options?.limit === \"number\" ? options.limit : 10;\n return client.threads.getHistory<StateType>(threadId, { limit });\n}\n\nfunction getOrCreateCacheEntry<StateType extends Record<string, unknown>>(\n cache: SuspenseCache,\n client: Client,\n threadId: string,\n limit: boolean | number\n): SuspenseCacheEntry<ThreadState<StateType>[]> {\n const key = getCacheKey(client, threadId, limit);\n let entry = cache.get(key) as\n | SuspenseCacheEntry<ThreadState<StateType>[]>\n | undefined;\n\n if (!entry) {\n // Start fetch. The promise always resolves (never rejects) so React\n // Suspense correctly waits for it and then retries the render.\n const promise = fetchThreadHistory<StateType>(client, threadId, { limit })\n .then((data) => {\n cache.set(key, { status: \"resolved\", data });\n })\n .catch((error: unknown) => {\n cache.set(key, { status: \"rejected\", error });\n });\n\n entry = { status: \"pending\", promise };\n cache.set(key, entry);\n }\n\n return entry;\n}\n\n/**\n * Clear the internal Suspense cache used by {@link useSuspenseStream}.\n *\n * Call this from an Error Boundary's `onReset` callback so that a retry\n * triggers a fresh thread-history fetch rather than re-throwing the\n * cached error.\n *\n * @example\n * ```tsx\n * <ErrorBoundary\n * onReset={() => invalidateSuspenseCache()}\n * fallbackRender={({ resetErrorBoundary }) => (\n * <button onClick={resetErrorBoundary}>Retry</button>\n * )}\n * >\n * <Suspense fallback={<Spinner />}>\n * <Chat />\n * </Suspense>\n * </ErrorBoundary>\n * ```\n */\nexport function invalidateSuspenseCache(\n cache: SuspenseCache = defaultSuspenseCache\n): void {\n cache.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Return-type helper\n// ---------------------------------------------------------------------------\n\ntype WithSuspense<T> = Omit<T, \"isLoading\" | \"error\" | \"isThreadLoading\"> & {\n isStreaming: boolean;\n};\n\ntype UseSuspenseStreamOptions<\n T = Record<string, unknown>,\n Bag extends BagTemplate = BagTemplate,\n> = ResolveStreamOptions<T, InferBag<T, Bag>> & {\n /**\n * Optional cache store used by Suspense history prefetching.\n * Provide a custom cache in tests to avoid cross-test cache sharing.\n */\n suspenseCache?: SuspenseCache;\n};\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense-compatible variant of {@link useStream} for LangGraph Platform.\n *\n * `useSuspenseStream` suspends the component while the initial thread\n * history is being fetched and throws errors to the nearest React Error\n * Boundary. During active streaming the component stays rendered and\n * `isStreaming` indicates whether 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 * });\n * return <MessageList messages={messages} />;\n * }\n * ```\n *\n * @template T - Either a ReactAgent / DeepAgent type or a state record type.\n * @template Bag - Type configuration bag (ConfigurableType, InterruptType, …).\n */\nexport function useSuspenseStream<\n T = Record<string, unknown>,\n Bag extends BagTemplate = BagTemplate,\n>(\n options: UseSuspenseStreamOptions<T, InferBag<T, Bag>>\n): WithClassMessages<WithSuspense<ResolveStreamInterface<T, InferBag<T, Bag>>>>;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useSuspenseStream(options: any): any {\n type StateType = Record<string, unknown>;\n const cache: SuspenseCache = options.suspenseCache ?? defaultSuspenseCache;\n\n // ---- client (needed before useStreamLGP for cache key derivation) ----\n const client = useMemo(\n () =>\n options.client ??\n new Client({\n apiUrl: options.apiUrl,\n apiKey: options.apiKey,\n callerOptions: options.callerOptions,\n defaultHeaders: options.defaultHeaders,\n }),\n [\n options.client,\n options.apiKey,\n options.apiUrl,\n options.callerOptions,\n options.defaultHeaders,\n ]\n );\n\n const { threadId } = options;\n\n const historyLimit: boolean | number =\n typeof options.fetchStateHistory === \"object\" &&\n options.fetchStateHistory != null\n ? (options.fetchStateHistory.limit ?? false)\n : (options.fetchStateHistory ?? false);\n\n // Only manage history via the suspense cache when the caller hasn't\n // supplied an external `thread` and there's a threadId to load.\n const needsHistoryFetch = threadId != null && options.thread == null;\n\n // ---- suspense cache lookup (synchronous, may create fetch) ----\n let cacheEntry: SuspenseCacheEntry<ThreadState<StateType>[]> | undefined;\n\n if (needsHistoryFetch) {\n cacheEntry = getOrCreateCacheEntry<StateType>(\n cache,\n client,\n threadId,\n historyLimit\n );\n }\n\n const cachedData =\n cacheEntry?.status === \"resolved\" ? cacheEntry.data : undefined;\n\n // ---- mutable ref so `mutate` always writes the freshest data ----\n const cachedDataRef = useRef(cachedData);\n if (cachedData != null) {\n cachedDataRef.current = cachedData;\n }\n\n // Re-render trigger after external mutate calls.\n const [, setMutateVersion] = useState(0);\n\n const mutate = useCallback(\n async (\n mutateId?: string\n ): Promise<ThreadState<StateType>[] | null | undefined> => {\n const fetchId = mutateId ?? threadId;\n if (!fetchId) return undefined;\n try {\n const data = await fetchThreadHistory<StateType>(client, fetchId, {\n limit: historyLimit,\n });\n const key = getCacheKey(client, fetchId, historyLimit);\n cache.set(key, { status: \"resolved\", data });\n cachedDataRef.current = data;\n setMutateVersion((v) => v + 1);\n return data;\n } catch {\n return undefined;\n }\n },\n [cache, client, threadId, historyLimit]\n );\n\n // ---- build thread override for useStreamLGP ----\n const thread: UseStreamThread<StateType> | undefined = useMemo(() => {\n if (!needsHistoryFetch) return options.thread;\n return {\n data: cachedDataRef.current,\n error: undefined,\n isLoading: false,\n mutate,\n };\n // `cachedData` is included so the memo recomputes when the cache\n // transitions from pending → resolved across suspend/retry cycles.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [needsHistoryFetch, options.thread, cachedData, mutate]);\n\n // ---- delegate to useStreamLGP (must always run – Rules of Hooks) ----\n const stream = useStreamLGP({\n ...options,\n client,\n thread,\n });\n\n // ---- post-hook: suspend or throw ----\n\n // Suspend while thread history is loading, but only when the stream\n // itself is idle. If an active stream is running (e.g. the thread was\n // just created during submit), suspending would discard the stream\n // state, so we skip it.\n if (needsHistoryFetch && cacheEntry && !stream.isLoading) {\n if (cacheEntry.status === \"pending\") {\n // eslint-disable-next-line @typescript-eslint/no-throw-literal\n throw cacheEntry.promise;\n }\n if (cacheEntry.status === \"rejected\") {\n // Clear cache so a subsequent retry (ErrorBoundary reset) starts\n // a fresh fetch instead of re-throwing the stale error.\n const key = getCacheKey(client, threadId!, historyLimit);\n cache.delete(key);\n // eslint-disable-next-line no-instanceof/no-instanceof\n throw cacheEntry.error instanceof Error\n ? cacheEntry.error\n : new Error(String(cacheEntry.error));\n }\n }\n\n // Throw non-streaming errors to the nearest Error Boundary.\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 return object explicitly to avoid triggering throwing getters\n // (e.g. `history` throws when `fetchStateHistory` is not set).\n return {\n get values() {\n return stream.values;\n },\n get messages() {\n return stream.messages;\n },\n get toolCalls() {\n return stream.toolCalls;\n },\n get toolProgress() {\n return stream.toolProgress;\n },\n getToolCalls: stream.getToolCalls.bind(stream),\n get interrupt() {\n return stream.interrupt;\n },\n get interrupts() {\n return stream.interrupts;\n },\n get subagents() {\n return stream.subagents;\n },\n get activeSubagents() {\n return stream.activeSubagents;\n },\n getSubagent: stream.getSubagent.bind(stream),\n getSubagentsByType: stream.getSubagentsByType.bind(stream),\n getSubagentsByMessage: stream.getSubagentsByMessage.bind(stream),\n getMessagesMetadata: stream.getMessagesMetadata.bind(stream),\n get history() {\n return stream.history;\n },\n get experimental_branchTree() {\n return stream.experimental_branchTree;\n },\n stop: stream.stop,\n submit: stream.submit,\n switchThread: stream.switchThread,\n joinStream: stream.joinStream,\n get branch() {\n return stream.branch;\n },\n setBranch: stream.setBranch,\n get client() {\n return stream.client;\n },\n get assistantId() {\n return stream.assistantId;\n },\n get queue() {\n return stream.queue;\n },\n get isStreaming() {\n return stream.isLoading;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"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 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,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"}
|
|
@@ -1,81 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { InferBag, ResolveStreamInterface, ResolveStreamOptions } from "@langchain/langgraph-sdk/ui";
|
|
1
|
+
import { UseStreamOptions as UseStreamOptions$1, UseStreamReturn } from "./use-stream.cjs";
|
|
2
|
+
import { InferStateType } from "@langchain/langgraph-sdk/stream";
|
|
4
3
|
|
|
5
4
|
//#region src/suspense-stream.d.ts
|
|
6
|
-
type SuspenseCacheEntry<T> = {
|
|
7
|
-
status: "pending";
|
|
8
|
-
promise: Promise<void>;
|
|
9
|
-
} | {
|
|
10
|
-
status: "resolved";
|
|
11
|
-
data: T;
|
|
12
|
-
} | {
|
|
13
|
-
status: "rejected";
|
|
14
|
-
error: unknown;
|
|
15
|
-
};
|
|
16
|
-
type SuspenseCache = Map<string, SuspenseCacheEntry<unknown>>;
|
|
17
|
-
declare function createSuspenseCache(): SuspenseCache;
|
|
18
5
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Call this from an Error Boundary's `onReset` callback so that a retry
|
|
22
|
-
* triggers a fresh thread-history fetch rather than re-throwing the
|
|
23
|
-
* cached error.
|
|
6
|
+
* Return shape of {@link useSuspenseStream}. Identical to the
|
|
7
|
+
* {@link UseStreamReturn} surface except:
|
|
24
8
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* fallbackRender={({ resetErrorBoundary }) => (
|
|
30
|
-
* <button onClick={resetErrorBoundary}>Retry</button>
|
|
31
|
-
* )}
|
|
32
|
-
* >
|
|
33
|
-
* <Suspense fallback={<Spinner />}>
|
|
34
|
-
* <Chat />
|
|
35
|
-
* </Suspense>
|
|
36
|
-
* </ErrorBoundary>
|
|
37
|
-
* ```
|
|
9
|
+
* - `isLoading` / `isThreadLoading` / `hydrationPromise` are removed
|
|
10
|
+
* (Suspense and Error Boundaries handle those phases).
|
|
11
|
+
* - `isStreaming: boolean` is added so callers can show a streaming
|
|
12
|
+
* indicator distinct from the suspended initial-load state.
|
|
38
13
|
*/
|
|
39
|
-
|
|
40
|
-
type WithSuspense<T> = Omit<T, "isLoading" | "error" | "isThreadLoading"> & {
|
|
41
|
-
isStreaming: boolean;
|
|
42
|
-
};
|
|
43
|
-
type UseSuspenseStreamOptions<T = Record<string, unknown>, Bag extends BagTemplate = BagTemplate> = ResolveStreamOptions<T, InferBag<T, Bag>> & {
|
|
14
|
+
type UseSuspenseStreamReturn<T = Record<string, unknown>, InterruptType = unknown, ConfigurableType extends object = Record<string, unknown>> = Omit<UseStreamReturn<T, InterruptType, ConfigurableType>, "isLoading" | "isThreadLoading" | "hydrationPromise"> & {
|
|
44
15
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
16
|
+
* Whether the stream is currently receiving data from the server.
|
|
17
|
+
* Unlike the suspended initial-load state, the component stays
|
|
18
|
+
* mounted while `isStreaming` is `true`.
|
|
47
19
|
*/
|
|
48
|
-
|
|
20
|
+
isStreaming: boolean;
|
|
49
21
|
};
|
|
50
|
-
|
|
51
|
-
* A Suspense-compatible variant of {@link useStream} for LangGraph Platform.
|
|
52
|
-
*
|
|
53
|
-
* `useSuspenseStream` suspends the component while the initial thread
|
|
54
|
-
* history is being fetched and throws errors to the nearest React Error
|
|
55
|
-
* Boundary. During active streaming the component stays rendered and
|
|
56
|
-
* `isStreaming` indicates whether tokens are arriving.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```tsx
|
|
60
|
-
* <ErrorBoundary fallback={<ErrorDisplay />}>
|
|
61
|
-
* <Suspense fallback={<Spinner />}>
|
|
62
|
-
* <Chat />
|
|
63
|
-
* </Suspense>
|
|
64
|
-
* </ErrorBoundary>
|
|
65
|
-
*
|
|
66
|
-
* function Chat() {
|
|
67
|
-
* const { messages, submit, isStreaming } = useSuspenseStream({
|
|
68
|
-
* assistantId: "agent",
|
|
69
|
-
* apiUrl: "http://localhost:2024",
|
|
70
|
-
* });
|
|
71
|
-
* return <MessageList messages={messages} />;
|
|
72
|
-
* }
|
|
73
|
-
* ```
|
|
74
|
-
*
|
|
75
|
-
* @template T - Either a ReactAgent / DeepAgent type or a state record type.
|
|
76
|
-
* @template Bag - Type configuration bag (ConfigurableType, InterruptType, …).
|
|
77
|
-
*/
|
|
78
|
-
declare function useSuspenseStream<T = Record<string, unknown>, Bag extends BagTemplate = BagTemplate>(options: UseSuspenseStreamOptions<T, InferBag<T, Bag>>): WithClassMessages$1<WithSuspense<ResolveStreamInterface<T, InferBag<T, Bag>>>>;
|
|
22
|
+
declare function useSuspenseStream<T = Record<string, unknown>>(options: UseStreamOptions$1<InferStateType<T>>): UseSuspenseStreamReturn<T>;
|
|
79
23
|
//#endregion
|
|
80
|
-
export {
|
|
24
|
+
export { UseSuspenseStreamReturn, useSuspenseStream };
|
|
81
25
|
//# sourceMappingURL=suspense-stream.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suspense-stream.d.cts","names":[],"sources":["../src/suspense-stream.
|
|
1
|
+
{"version":3,"file":"suspense-stream.d.cts","names":[],"sources":["../src/suspense-stream.ts"],"mappings":";;;;;;AAqDA;;;;;;;KAAY,uBAAA,KACN,MAAA,8EAE8B,MAAA,qBAChC,IAAA,CACF,eAAA,CAAgB,CAAA,EAAG,aAAA,EAAe,gBAAA;EAAlC;;;;;EAQA,WAAA;AAAA;AAAA,iBA4Dc,iBAAA,KAAsB,MAAA,kBAAA,CACpC,OAAA,EAAS,kBAAA,CAAiB,cAAA,CAAe,CAAA,KACxC,uBAAA,CAAwB,CAAA"}
|
|
@@ -1,81 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { BagTemplate } from "@langchain/langgraph-sdk";
|
|
1
|
+
import { UseStreamOptions as UseStreamOptions$1, UseStreamReturn } from "./use-stream.js";
|
|
2
|
+
import { InferStateType } from "@langchain/langgraph-sdk/stream";
|
|
4
3
|
|
|
5
4
|
//#region src/suspense-stream.d.ts
|
|
6
|
-
type SuspenseCacheEntry<T> = {
|
|
7
|
-
status: "pending";
|
|
8
|
-
promise: Promise<void>;
|
|
9
|
-
} | {
|
|
10
|
-
status: "resolved";
|
|
11
|
-
data: T;
|
|
12
|
-
} | {
|
|
13
|
-
status: "rejected";
|
|
14
|
-
error: unknown;
|
|
15
|
-
};
|
|
16
|
-
type SuspenseCache = Map<string, SuspenseCacheEntry<unknown>>;
|
|
17
|
-
declare function createSuspenseCache(): SuspenseCache;
|
|
18
5
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Call this from an Error Boundary's `onReset` callback so that a retry
|
|
22
|
-
* triggers a fresh thread-history fetch rather than re-throwing the
|
|
23
|
-
* cached error.
|
|
6
|
+
* Return shape of {@link useSuspenseStream}. Identical to the
|
|
7
|
+
* {@link UseStreamReturn} surface except:
|
|
24
8
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* fallbackRender={({ resetErrorBoundary }) => (
|
|
30
|
-
* <button onClick={resetErrorBoundary}>Retry</button>
|
|
31
|
-
* )}
|
|
32
|
-
* >
|
|
33
|
-
* <Suspense fallback={<Spinner />}>
|
|
34
|
-
* <Chat />
|
|
35
|
-
* </Suspense>
|
|
36
|
-
* </ErrorBoundary>
|
|
37
|
-
* ```
|
|
9
|
+
* - `isLoading` / `isThreadLoading` / `hydrationPromise` are removed
|
|
10
|
+
* (Suspense and Error Boundaries handle those phases).
|
|
11
|
+
* - `isStreaming: boolean` is added so callers can show a streaming
|
|
12
|
+
* indicator distinct from the suspended initial-load state.
|
|
38
13
|
*/
|
|
39
|
-
|
|
40
|
-
type WithSuspense<T> = Omit<T, "isLoading" | "error" | "isThreadLoading"> & {
|
|
41
|
-
isStreaming: boolean;
|
|
42
|
-
};
|
|
43
|
-
type UseSuspenseStreamOptions<T = Record<string, unknown>, Bag extends BagTemplate = BagTemplate> = ResolveStreamOptions<T, InferBag<T, Bag>> & {
|
|
14
|
+
type UseSuspenseStreamReturn<T = Record<string, unknown>, InterruptType = unknown, ConfigurableType extends object = Record<string, unknown>> = Omit<UseStreamReturn<T, InterruptType, ConfigurableType>, "isLoading" | "isThreadLoading" | "hydrationPromise"> & {
|
|
44
15
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
16
|
+
* Whether the stream is currently receiving data from the server.
|
|
17
|
+
* Unlike the suspended initial-load state, the component stays
|
|
18
|
+
* mounted while `isStreaming` is `true`.
|
|
47
19
|
*/
|
|
48
|
-
|
|
20
|
+
isStreaming: boolean;
|
|
49
21
|
};
|
|
50
|
-
|
|
51
|
-
* A Suspense-compatible variant of {@link useStream} for LangGraph Platform.
|
|
52
|
-
*
|
|
53
|
-
* `useSuspenseStream` suspends the component while the initial thread
|
|
54
|
-
* history is being fetched and throws errors to the nearest React Error
|
|
55
|
-
* Boundary. During active streaming the component stays rendered and
|
|
56
|
-
* `isStreaming` indicates whether tokens are arriving.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```tsx
|
|
60
|
-
* <ErrorBoundary fallback={<ErrorDisplay />}>
|
|
61
|
-
* <Suspense fallback={<Spinner />}>
|
|
62
|
-
* <Chat />
|
|
63
|
-
* </Suspense>
|
|
64
|
-
* </ErrorBoundary>
|
|
65
|
-
*
|
|
66
|
-
* function Chat() {
|
|
67
|
-
* const { messages, submit, isStreaming } = useSuspenseStream({
|
|
68
|
-
* assistantId: "agent",
|
|
69
|
-
* apiUrl: "http://localhost:2024",
|
|
70
|
-
* });
|
|
71
|
-
* return <MessageList messages={messages} />;
|
|
72
|
-
* }
|
|
73
|
-
* ```
|
|
74
|
-
*
|
|
75
|
-
* @template T - Either a ReactAgent / DeepAgent type or a state record type.
|
|
76
|
-
* @template Bag - Type configuration bag (ConfigurableType, InterruptType, …).
|
|
77
|
-
*/
|
|
78
|
-
declare function useSuspenseStream<T = Record<string, unknown>, Bag extends BagTemplate = BagTemplate>(options: UseSuspenseStreamOptions<T, InferBag<T, Bag>>): WithClassMessages$1<WithSuspense<ResolveStreamInterface<T, InferBag<T, Bag>>>>;
|
|
22
|
+
declare function useSuspenseStream<T = Record<string, unknown>>(options: UseStreamOptions$1<InferStateType<T>>): UseSuspenseStreamReturn<T>;
|
|
79
23
|
//#endregion
|
|
80
|
-
export {
|
|
24
|
+
export { UseSuspenseStreamReturn, useSuspenseStream };
|
|
81
25
|
//# sourceMappingURL=suspense-stream.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suspense-stream.d.ts","names":[],"sources":["../src/suspense-stream.
|
|
1
|
+
{"version":3,"file":"suspense-stream.d.ts","names":[],"sources":["../src/suspense-stream.ts"],"mappings":";;;;;;AAqDA;;;;;;;KAAY,uBAAA,KACN,MAAA,8EAE8B,MAAA,qBAChC,IAAA,CACF,eAAA,CAAgB,CAAA,EAAG,aAAA,EAAe,gBAAA;EAAlC;;;;;EAQA,WAAA;AAAA;AAAA,iBA4Dc,iBAAA,KAAsB,MAAA,kBAAA,CACpC,OAAA,EAAS,kBAAA,CAAiB,cAAA,CAAe,CAAA,KACxC,uBAAA,CAAwB,CAAA"}
|
package/dist/suspense-stream.js
CHANGED
|
@@ -1,141 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return /* @__PURE__ */ new Map();
|
|
9
|
-
}
|
|
10
|
-
function getCacheKey(client, threadId, limit) {
|
|
11
|
-
return `suspense:${getClientConfigHash(client)}:${threadId}:${limit}`;
|
|
12
|
-
}
|
|
13
|
-
function fetchThreadHistory(client, threadId, options) {
|
|
14
|
-
if (options?.limit === false) return client.threads.getState(threadId).then((state) => {
|
|
15
|
-
if (state.checkpoint == null) return [];
|
|
16
|
-
return [state];
|
|
17
|
-
});
|
|
18
|
-
const limit = typeof options?.limit === "number" ? options.limit : 10;
|
|
19
|
-
return client.threads.getHistory(threadId, { limit });
|
|
20
|
-
}
|
|
21
|
-
function getOrCreateCacheEntry(cache, client, threadId, limit) {
|
|
22
|
-
const key = getCacheKey(client, threadId, limit);
|
|
23
|
-
let entry = cache.get(key);
|
|
24
|
-
if (!entry) {
|
|
25
|
-
entry = {
|
|
26
|
-
status: "pending",
|
|
27
|
-
promise: fetchThreadHistory(client, threadId, { limit }).then((data) => {
|
|
28
|
-
cache.set(key, {
|
|
29
|
-
status: "resolved",
|
|
30
|
-
data
|
|
31
|
-
});
|
|
32
|
-
}).catch((error) => {
|
|
33
|
-
cache.set(key, {
|
|
34
|
-
status: "rejected",
|
|
35
|
-
error
|
|
36
|
-
});
|
|
37
|
-
})
|
|
38
|
-
};
|
|
39
|
-
cache.set(key, entry);
|
|
40
|
-
}
|
|
41
|
-
return entry;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Clear the internal Suspense cache used by {@link useSuspenseStream}.
|
|
45
|
-
*
|
|
46
|
-
* Call this from an Error Boundary's `onReset` callback so that a retry
|
|
47
|
-
* triggers a fresh thread-history fetch rather than re-throwing the
|
|
48
|
-
* cached error.
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```tsx
|
|
52
|
-
* <ErrorBoundary
|
|
53
|
-
* onReset={() => invalidateSuspenseCache()}
|
|
54
|
-
* fallbackRender={({ resetErrorBoundary }) => (
|
|
55
|
-
* <button onClick={resetErrorBoundary}>Retry</button>
|
|
56
|
-
* )}
|
|
57
|
-
* >
|
|
58
|
-
* <Suspense fallback={<Spinner />}>
|
|
59
|
-
* <Chat />
|
|
60
|
-
* </Suspense>
|
|
61
|
-
* </ErrorBoundary>
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
function invalidateSuspenseCache(cache = defaultSuspenseCache) {
|
|
65
|
-
cache.clear();
|
|
2
|
+
import { useStream } from "./use-stream.js";
|
|
3
|
+
//#region src/suspense-stream.ts
|
|
4
|
+
const suspenseEntries = /* @__PURE__ */ new Map();
|
|
5
|
+
function suspenseKey(options) {
|
|
6
|
+
if (options.threadId == null) return null;
|
|
7
|
+
return `${options.apiUrl ?? "_"}::${options.assistantId ?? "_"}::${options.threadId}`;
|
|
66
8
|
}
|
|
67
9
|
function useSuspenseStream(options) {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
]);
|
|
81
|
-
const { threadId } = options;
|
|
82
|
-
const historyLimit = typeof options.fetchStateHistory === "object" && options.fetchStateHistory != null ? options.fetchStateHistory.limit ?? false : options.fetchStateHistory ?? false;
|
|
83
|
-
const needsHistoryFetch = threadId != null && options.thread == null;
|
|
84
|
-
let cacheEntry;
|
|
85
|
-
if (needsHistoryFetch) cacheEntry = getOrCreateCacheEntry(cache, client, threadId, historyLimit);
|
|
86
|
-
const cachedData = cacheEntry?.status === "resolved" ? cacheEntry.data : void 0;
|
|
87
|
-
const cachedDataRef = useRef(cachedData);
|
|
88
|
-
if (cachedData != null) cachedDataRef.current = cachedData;
|
|
89
|
-
const [, setMutateVersion] = useState(0);
|
|
90
|
-
const mutate = useCallback(async (mutateId) => {
|
|
91
|
-
const fetchId = mutateId ?? threadId;
|
|
92
|
-
if (!fetchId) return void 0;
|
|
93
|
-
try {
|
|
94
|
-
const data = await fetchThreadHistory(client, fetchId, { limit: historyLimit });
|
|
95
|
-
const key = getCacheKey(client, fetchId, historyLimit);
|
|
96
|
-
cache.set(key, {
|
|
97
|
-
status: "resolved",
|
|
98
|
-
data
|
|
99
|
-
});
|
|
100
|
-
cachedDataRef.current = data;
|
|
101
|
-
setMutateVersion((v) => v + 1);
|
|
102
|
-
return data;
|
|
103
|
-
} catch {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
}, [
|
|
107
|
-
cache,
|
|
108
|
-
client,
|
|
109
|
-
threadId,
|
|
110
|
-
historyLimit
|
|
111
|
-
]);
|
|
112
|
-
const thread = useMemo(() => {
|
|
113
|
-
if (!needsHistoryFetch) return options.thread;
|
|
114
|
-
return {
|
|
115
|
-
data: cachedDataRef.current,
|
|
116
|
-
error: void 0,
|
|
117
|
-
isLoading: false,
|
|
118
|
-
mutate
|
|
10
|
+
const key = suspenseKey(options);
|
|
11
|
+
const stream = useStream(options);
|
|
12
|
+
if (key != null && !suspenseEntries.has(key)) {
|
|
13
|
+
const entry = {
|
|
14
|
+
promise: stream.hydrationPromise.then(() => {
|
|
15
|
+
entry.settled = true;
|
|
16
|
+
}, (error) => {
|
|
17
|
+
entry.settled = true;
|
|
18
|
+
entry.error = error instanceof Error ? error : new Error(String(error));
|
|
19
|
+
throw entry.error;
|
|
20
|
+
}),
|
|
21
|
+
settled: false
|
|
119
22
|
};
|
|
120
|
-
|
|
121
|
-
needsHistoryFetch,
|
|
122
|
-
options.thread,
|
|
123
|
-
cachedData,
|
|
124
|
-
mutate
|
|
125
|
-
]);
|
|
126
|
-
const stream = useStreamLGP({
|
|
127
|
-
...options,
|
|
128
|
-
client,
|
|
129
|
-
thread
|
|
130
|
-
});
|
|
131
|
-
if (needsHistoryFetch && cacheEntry && !stream.isLoading) {
|
|
132
|
-
if (cacheEntry.status === "pending") throw cacheEntry.promise;
|
|
133
|
-
if (cacheEntry.status === "rejected") {
|
|
134
|
-
const key = getCacheKey(client, threadId, historyLimit);
|
|
135
|
-
cache.delete(key);
|
|
136
|
-
throw cacheEntry.error instanceof Error ? cacheEntry.error : new Error(String(cacheEntry.error));
|
|
137
|
-
}
|
|
23
|
+
suspenseEntries.set(key, entry);
|
|
138
24
|
}
|
|
25
|
+
const entry = key != null ? suspenseEntries.get(key) : void 0;
|
|
26
|
+
if (entry && !entry.settled) throw entry.promise;
|
|
27
|
+
if (entry?.error != null) throw entry.error;
|
|
139
28
|
if (stream.error != null && !stream.isLoading) throw stream.error instanceof Error ? stream.error : new Error(String(stream.error));
|
|
140
29
|
return {
|
|
141
30
|
get values() {
|
|
@@ -147,10 +36,6 @@ function useSuspenseStream(options) {
|
|
|
147
36
|
get toolCalls() {
|
|
148
37
|
return stream.toolCalls;
|
|
149
38
|
},
|
|
150
|
-
get toolProgress() {
|
|
151
|
-
return stream.toolProgress;
|
|
152
|
-
},
|
|
153
|
-
getToolCalls: stream.getToolCalls.bind(stream),
|
|
154
39
|
get interrupt() {
|
|
155
40
|
return stream.interrupt;
|
|
156
41
|
},
|
|
@@ -160,35 +45,27 @@ function useSuspenseStream(options) {
|
|
|
160
45
|
get subagents() {
|
|
161
46
|
return stream.subagents;
|
|
162
47
|
},
|
|
163
|
-
get
|
|
164
|
-
return stream.
|
|
48
|
+
get subgraphs() {
|
|
49
|
+
return stream.subgraphs;
|
|
165
50
|
},
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
getSubagentsByMessage: stream.getSubagentsByMessage.bind(stream),
|
|
169
|
-
getMessagesMetadata: stream.getMessagesMetadata.bind(stream),
|
|
170
|
-
get history() {
|
|
171
|
-
return stream.history;
|
|
51
|
+
get subgraphsByNode() {
|
|
52
|
+
return stream.subgraphsByNode;
|
|
172
53
|
},
|
|
173
|
-
get experimental_branchTree() {
|
|
174
|
-
return stream.experimental_branchTree;
|
|
175
|
-
},
|
|
176
|
-
stop: stream.stop,
|
|
177
54
|
submit: stream.submit,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return stream.branch;
|
|
182
|
-
},
|
|
183
|
-
setBranch: stream.setBranch,
|
|
55
|
+
stop: stream.stop,
|
|
56
|
+
respond: stream.respond,
|
|
57
|
+
getThread: stream.getThread,
|
|
184
58
|
get client() {
|
|
185
59
|
return stream.client;
|
|
186
60
|
},
|
|
187
61
|
get assistantId() {
|
|
188
62
|
return stream.assistantId;
|
|
189
63
|
},
|
|
190
|
-
get
|
|
191
|
-
return stream.
|
|
64
|
+
get threadId() {
|
|
65
|
+
return stream.threadId;
|
|
66
|
+
},
|
|
67
|
+
get error() {
|
|
68
|
+
return stream.error;
|
|
192
69
|
},
|
|
193
70
|
get isStreaming() {
|
|
194
71
|
return stream.isLoading;
|
|
@@ -196,6 +73,6 @@ function useSuspenseStream(options) {
|
|
|
196
73
|
};
|
|
197
74
|
}
|
|
198
75
|
//#endregion
|
|
199
|
-
export {
|
|
76
|
+
export { useSuspenseStream };
|
|
200
77
|
|
|
201
78
|
//# sourceMappingURL=suspense-stream.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suspense-stream.js","names":[],"sources":["../src/suspense-stream.tsx"],"sourcesContent":["/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */\n\n\"use client\";\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { ThreadState, BagTemplate } from \"@langchain/langgraph-sdk\";\nimport { Client, getClientConfigHash } from \"@langchain/langgraph-sdk/client\";\nimport type {\n UseStreamThread,\n ResolveStreamOptions,\n ResolveStreamInterface,\n InferBag,\n} from \"@langchain/langgraph-sdk/ui\";\nimport { useStreamLGP } from \"./stream.lgp.js\";\nimport type { WithClassMessages } from \"./stream.js\";\n\n// ---------------------------------------------------------------------------\n// Suspense cache\n// ---------------------------------------------------------------------------\n\nexport type SuspenseCacheEntry<T> =\n | { status: \"pending\"; promise: Promise<void> }\n | { status: \"resolved\"; data: T }\n | { status: \"rejected\"; error: unknown };\n\nexport type SuspenseCache = Map<string, SuspenseCacheEntry<unknown>>;\n\nconst defaultSuspenseCache: SuspenseCache = new Map();\n\nexport function createSuspenseCache(): SuspenseCache {\n return new Map();\n}\n\nfunction getCacheKey(\n client: Client,\n threadId: string,\n limit: boolean | number\n): string {\n return `suspense:${getClientConfigHash(client)}:${threadId}:${limit}`;\n}\n\nfunction fetchThreadHistory<StateType extends Record<string, unknown>>(\n client: Client,\n threadId: string,\n options?: { limit?: boolean | number }\n): Promise<ThreadState<StateType>[]> {\n if (options?.limit === false) {\n return client.threads.getState<StateType>(threadId).then((state) => {\n if (state.checkpoint == null) return [];\n return [state];\n });\n }\n\n const limit = typeof options?.limit === \"number\" ? options.limit : 10;\n return client.threads.getHistory<StateType>(threadId, { limit });\n}\n\nfunction getOrCreateCacheEntry<StateType extends Record<string, unknown>>(\n cache: SuspenseCache,\n client: Client,\n threadId: string,\n limit: boolean | number\n): SuspenseCacheEntry<ThreadState<StateType>[]> {\n const key = getCacheKey(client, threadId, limit);\n let entry = cache.get(key) as\n | SuspenseCacheEntry<ThreadState<StateType>[]>\n | undefined;\n\n if (!entry) {\n // Start fetch. The promise always resolves (never rejects) so React\n // Suspense correctly waits for it and then retries the render.\n const promise = fetchThreadHistory<StateType>(client, threadId, { limit })\n .then((data) => {\n cache.set(key, { status: \"resolved\", data });\n })\n .catch((error: unknown) => {\n cache.set(key, { status: \"rejected\", error });\n });\n\n entry = { status: \"pending\", promise };\n cache.set(key, entry);\n }\n\n return entry;\n}\n\n/**\n * Clear the internal Suspense cache used by {@link useSuspenseStream}.\n *\n * Call this from an Error Boundary's `onReset` callback so that a retry\n * triggers a fresh thread-history fetch rather than re-throwing the\n * cached error.\n *\n * @example\n * ```tsx\n * <ErrorBoundary\n * onReset={() => invalidateSuspenseCache()}\n * fallbackRender={({ resetErrorBoundary }) => (\n * <button onClick={resetErrorBoundary}>Retry</button>\n * )}\n * >\n * <Suspense fallback={<Spinner />}>\n * <Chat />\n * </Suspense>\n * </ErrorBoundary>\n * ```\n */\nexport function invalidateSuspenseCache(\n cache: SuspenseCache = defaultSuspenseCache\n): void {\n cache.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Return-type helper\n// ---------------------------------------------------------------------------\n\ntype WithSuspense<T> = Omit<T, \"isLoading\" | \"error\" | \"isThreadLoading\"> & {\n isStreaming: boolean;\n};\n\ntype UseSuspenseStreamOptions<\n T = Record<string, unknown>,\n Bag extends BagTemplate = BagTemplate,\n> = ResolveStreamOptions<T, InferBag<T, Bag>> & {\n /**\n * Optional cache store used by Suspense history prefetching.\n * Provide a custom cache in tests to avoid cross-test cache sharing.\n */\n suspenseCache?: SuspenseCache;\n};\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense-compatible variant of {@link useStream} for LangGraph Platform.\n *\n * `useSuspenseStream` suspends the component while the initial thread\n * history is being fetched and throws errors to the nearest React Error\n * Boundary. During active streaming the component stays rendered and\n * `isStreaming` indicates whether 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 * });\n * return <MessageList messages={messages} />;\n * }\n * ```\n *\n * @template T - Either a ReactAgent / DeepAgent type or a state record type.\n * @template Bag - Type configuration bag (ConfigurableType, InterruptType, …).\n */\nexport function useSuspenseStream<\n T = Record<string, unknown>,\n Bag extends BagTemplate = BagTemplate,\n>(\n options: UseSuspenseStreamOptions<T, InferBag<T, Bag>>\n): WithClassMessages<WithSuspense<ResolveStreamInterface<T, InferBag<T, Bag>>>>;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useSuspenseStream(options: any): any {\n type StateType = Record<string, unknown>;\n const cache: SuspenseCache = options.suspenseCache ?? defaultSuspenseCache;\n\n // ---- client (needed before useStreamLGP for cache key derivation) ----\n const client = useMemo(\n () =>\n options.client ??\n new Client({\n apiUrl: options.apiUrl,\n apiKey: options.apiKey,\n callerOptions: options.callerOptions,\n defaultHeaders: options.defaultHeaders,\n }),\n [\n options.client,\n options.apiKey,\n options.apiUrl,\n options.callerOptions,\n options.defaultHeaders,\n ]\n );\n\n const { threadId } = options;\n\n const historyLimit: boolean | number =\n typeof options.fetchStateHistory === \"object\" &&\n options.fetchStateHistory != null\n ? (options.fetchStateHistory.limit ?? false)\n : (options.fetchStateHistory ?? false);\n\n // Only manage history via the suspense cache when the caller hasn't\n // supplied an external `thread` and there's a threadId to load.\n const needsHistoryFetch = threadId != null && options.thread == null;\n\n // ---- suspense cache lookup (synchronous, may create fetch) ----\n let cacheEntry: SuspenseCacheEntry<ThreadState<StateType>[]> | undefined;\n\n if (needsHistoryFetch) {\n cacheEntry = getOrCreateCacheEntry<StateType>(\n cache,\n client,\n threadId,\n historyLimit\n );\n }\n\n const cachedData =\n cacheEntry?.status === \"resolved\" ? cacheEntry.data : undefined;\n\n // ---- mutable ref so `mutate` always writes the freshest data ----\n const cachedDataRef = useRef(cachedData);\n if (cachedData != null) {\n cachedDataRef.current = cachedData;\n }\n\n // Re-render trigger after external mutate calls.\n const [, setMutateVersion] = useState(0);\n\n const mutate = useCallback(\n async (\n mutateId?: string\n ): Promise<ThreadState<StateType>[] | null | undefined> => {\n const fetchId = mutateId ?? threadId;\n if (!fetchId) return undefined;\n try {\n const data = await fetchThreadHistory<StateType>(client, fetchId, {\n limit: historyLimit,\n });\n const key = getCacheKey(client, fetchId, historyLimit);\n cache.set(key, { status: \"resolved\", data });\n cachedDataRef.current = data;\n setMutateVersion((v) => v + 1);\n return data;\n } catch {\n return undefined;\n }\n },\n [cache, client, threadId, historyLimit]\n );\n\n // ---- build thread override for useStreamLGP ----\n const thread: UseStreamThread<StateType> | undefined = useMemo(() => {\n if (!needsHistoryFetch) return options.thread;\n return {\n data: cachedDataRef.current,\n error: undefined,\n isLoading: false,\n mutate,\n };\n // `cachedData` is included so the memo recomputes when the cache\n // transitions from pending → resolved across suspend/retry cycles.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [needsHistoryFetch, options.thread, cachedData, mutate]);\n\n // ---- delegate to useStreamLGP (must always run – Rules of Hooks) ----\n const stream = useStreamLGP({\n ...options,\n client,\n thread,\n });\n\n // ---- post-hook: suspend or throw ----\n\n // Suspend while thread history is loading, but only when the stream\n // itself is idle. If an active stream is running (e.g. the thread was\n // just created during submit), suspending would discard the stream\n // state, so we skip it.\n if (needsHistoryFetch && cacheEntry && !stream.isLoading) {\n if (cacheEntry.status === \"pending\") {\n // eslint-disable-next-line @typescript-eslint/no-throw-literal\n throw cacheEntry.promise;\n }\n if (cacheEntry.status === \"rejected\") {\n // Clear cache so a subsequent retry (ErrorBoundary reset) starts\n // a fresh fetch instead of re-throwing the stale error.\n const key = getCacheKey(client, threadId!, historyLimit);\n cache.delete(key);\n // eslint-disable-next-line no-instanceof/no-instanceof\n throw cacheEntry.error instanceof Error\n ? cacheEntry.error\n : new Error(String(cacheEntry.error));\n }\n }\n\n // Throw non-streaming errors to the nearest Error Boundary.\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 return object explicitly to avoid triggering throwing getters\n // (e.g. `history` throws when `fetchStateHistory` is not set).\n return {\n get values() {\n return stream.values;\n },\n get messages() {\n return stream.messages;\n },\n get toolCalls() {\n return stream.toolCalls;\n },\n get toolProgress() {\n return stream.toolProgress;\n },\n getToolCalls: stream.getToolCalls.bind(stream),\n get interrupt() {\n return stream.interrupt;\n },\n get interrupts() {\n return stream.interrupts;\n },\n get subagents() {\n return stream.subagents;\n },\n get activeSubagents() {\n return stream.activeSubagents;\n },\n getSubagent: stream.getSubagent.bind(stream),\n getSubagentsByType: stream.getSubagentsByType.bind(stream),\n getSubagentsByMessage: stream.getSubagentsByMessage.bind(stream),\n getMessagesMetadata: stream.getMessagesMetadata.bind(stream),\n get history() {\n return stream.history;\n },\n get experimental_branchTree() {\n return stream.experimental_branchTree;\n },\n stop: stream.stop,\n submit: stream.submit,\n switchThread: stream.switchThread,\n joinStream: stream.joinStream,\n get branch() {\n return stream.branch;\n },\n setBranch: stream.setBranch,\n get client() {\n return stream.client;\n },\n get assistantId() {\n return stream.assistantId;\n },\n get queue() {\n return stream.queue;\n },\n get isStreaming() {\n return stream.isLoading;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"suspense-stream.js","names":[],"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 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,SAAS,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,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"}
|