82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
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
|
|
* ? <button onClick={stop}>중단</button>
|
|
* : <button onClick={() => onSubmit(input)}>보내기</button>
|
|
* ```
|
|
*/
|
|
export interface StreamSessionResult<T> {
|
|
ok: boolean
|
|
/** 사용자가 stop()을 눌러 중단된 경우 true. ok=false일 때만 의미 있음. */
|
|
aborted: boolean
|
|
/** ok=true면 fn의 반환값, ok=false면 throw된 에러 */
|
|
value?: T
|
|
error?: unknown
|
|
}
|
|
|
|
export function useStreamSession() {
|
|
const ctrlRef = useRef<AbortController | null>(null)
|
|
const [isRunning, setIsRunning] = useState(false)
|
|
|
|
// 언마운트 시 진행 중인 스트림 abort (메모리/네트워크 누수 방지)
|
|
useEffect(() => {
|
|
return () => {
|
|
ctrlRef.current?.abort()
|
|
ctrlRef.current = null
|
|
}
|
|
}, [])
|
|
|
|
const run = useCallback(
|
|
async <T>(fn: (signal: AbortSignal) => Promise<T>): Promise<StreamSessionResult<T>> => {
|
|
// 이전 진행 중인 스트림이 있으면 자동 취소 (사용자가 새 쿼리 보낸 경우)
|
|
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 }
|
|
}
|