import { useCallback, useEffect, useRef, useState } from "react"
import { isAbortError } from "./abort"
/**
* 컴포넌트 로컬 스트림 세션 hook.
*
* AbortController 생성·관리·중단·정리까지 한 묶음으로. 컴포넌트가 언마운트되면 진행 중인 스트림도 자동 abort.
*
* zustand 같은 글로벌 store 패턴을 쓰는 경우 직접 controller를 들고 다니는 게 더 자연스러움 — 이 훅은
* 컴포넌트 단독으로 스트림 시작/중단 할 때 쓰기 편함. (자세한 store 패턴은 README 참고)
*
* 사용 예:
* ```tsx
* const { run, stop, isRunning } = useStreamSession()
*
* const onSubmit = (q: string) => {
* void run(async (signal) => {
* await streamLLM({
* path: "/chat/stream",
* body: { messages: [{ role: "user", content: q }] },
* signal,
* handlers: { onToken: (d) => setText((t) => t + d), onDone: () => {} },
* })
* })
* }
*
* return isRunning
* ?
* :
* ```
*/
export interface StreamSessionResult {
ok: boolean
/** 사용자가 stop()을 눌러 중단된 경우 true. ok=false일 때만 의미 있음. */
aborted: boolean
/** ok=true면 fn의 반환값, ok=false면 throw된 에러 */
value?: T
error?: unknown
}
export function useStreamSession() {
const ctrlRef = useRef(null)
const [isRunning, setIsRunning] = useState(false)
// 언마운트 시 진행 중인 스트림 abort (메모리/네트워크 누수 방지)
useEffect(() => {
return () => {
ctrlRef.current?.abort()
ctrlRef.current = null
}
}, [])
const run = useCallback(
async (fn: (signal: AbortSignal) => Promise): Promise> => {
// 이전 진행 중인 스트림이 있으면 자동 취소 (사용자가 새 쿼리 보낸 경우)
ctrlRef.current?.abort()
const ctrl = new AbortController()
ctrlRef.current = ctrl
setIsRunning(true)
try {
const value = await fn(ctrl.signal)
return { ok: true, aborted: false, value }
} catch (error) {
const aborted = ctrl.signal.aborted || isAbortError(error)
return { ok: false, aborted, error }
} finally {
if (ctrlRef.current === ctrl) {
ctrlRef.current = null
setIsRunning(false)
}
}
},
[]
)
const stop = useCallback(() => {
ctrlRef.current?.abort()
}, [])
return { run, stop, isRunning }
}