forked from Team-StackUp/stackup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseEventStream.ts
More file actions
101 lines (87 loc) · 2.86 KB
/
Copy pathuseEventStream.ts
File metadata and controls
101 lines (87 loc) · 2.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { useEffect, useLayoutEffect, useRef } from 'react'
import { env } from '@/shared/config/env'
type EventHandler = (data: unknown) => void
type EventHandlers = Record<string, EventHandler>
type UseEventStreamOptions = {
/** SSE_BASE_URL 에 붙일 경로. null 이면 연결하지 않음. */
path: string | null
/** 매 (재)연결 시점에 stream token 을 새로 발급받는다. */
getToken: () => Promise<string | null>
/** SSE `event:` 이름 → 핸들러. data 는 JSON 파싱되어 전달됨(파싱 실패 시 raw 문자열). */
handlers: EventHandlers
enabled?: boolean
}
const BASE_BACKOFF_MS = 1_000
const MAX_BACKOFF_MS = 30_000
// EventSource 는 커스텀 헤더를 못 싣는다 → 인증은 ?access_token= 쿼리(stream token)로 전달. 호스트는 RealTime 서버(REALTIME_BASE_URL).
export function useEventStream({
path,
getToken,
handlers,
enabled = true,
}: UseEventStreamOptions): void {
const handlersRef = useRef(handlers)
const getTokenRef = useRef(getToken)
// 렌더 중 ref 쓰기는 금지 → effect 에서 최신 값으로 동기화.
useLayoutEffect(() => {
handlersRef.current = handlers
getTokenRef.current = getToken
})
useEffect(() => {
if (!enabled || !path) return
let source: EventSource | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let attempt = 0
let cancelled = false
const scheduleReconnect = () => {
const delay = Math.min(BASE_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS)
attempt += 1
reconnectTimer = setTimeout(() => {
if (!cancelled) void connect()
}, delay)
}
const connect = async () => {
if (cancelled) return
let token: string | null = null
try {
token = await getTokenRef.current()
} catch {
scheduleReconnect()
return
}
if (cancelled) return
const query = token ? `?access_token=${encodeURIComponent(token)}` : ''
const es = new EventSource(`${env.REALTIME_BASE_URL}${path}${query}`, {
withCredentials: true,
})
source = es
es.onopen = () => {
attempt = 0
}
es.onerror = () => {
es.close()
if (source === es) source = null
if (!cancelled) scheduleReconnect()
}
for (const name of Object.keys(handlersRef.current)) {
es.addEventListener(name, (event) => {
const raw = (event as MessageEvent<string>).data
let parsed: unknown = raw
try {
parsed = JSON.parse(raw)
} catch {
// keep-alive 등 비 JSON payload — raw 전달
}
handlersRef.current[name]?.(parsed)
})
}
}
void connect()
return () => {
cancelled = true
if (reconnectTimer) clearTimeout(reconnectTimer)
source?.close()
source = null
}
}, [path, enabled])
}