Initial Commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user