chore: ticket section implement sse

This commit is contained in:
2026-06-19 18:06:11 +05:30
parent a567a52e26
commit fffd8e3e12
25 changed files with 711 additions and 307 deletions

View File

@@ -0,0 +1,142 @@
'use client';
import { useCallback, useEffect, useRef } from 'react';
import {
sseService,
type SseConnection,
type SseEventMap,
} from '@/services/sse';
const DEFAULT_RETRY_DELAY_MS = 2500;
const EMPTY_RECONNECT_EVENTS: ReadonlyArray<PropertyKey> = [];
interface SseToken {
sse_token: string;
}
interface UseSseWithTokenOptions<TEvents extends Record<string, unknown>> {
enabled?: boolean;
getToken: () => Promise<SseToken>;
getPath: (token: string) => string;
events: SseEventMap<TEvents>;
retryDelayMs?: number;
onConnectionError?: () => void;
reconnectOnEvents?: ReadonlyArray<keyof TEvents>;
shouldStop?: (
eventName: keyof TEvents,
event: TEvents[keyof TEvents],
) => boolean;
}
export function useSseWithToken<TEvents extends Record<string, unknown>>({
enabled = true,
getToken,
getPath,
events,
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
onConnectionError,
reconnectOnEvents = EMPTY_RECONNECT_EVENTS as ReadonlyArray<keyof TEvents>,
shouldStop,
}: UseSseWithTokenOptions<TEvents>) {
const connectionRef = useRef<SseConnection | null>(null);
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const stoppedRef = useRef(false);
const clearRetryTimer = useCallback(() => {
if (!retryTimerRef.current) return;
clearTimeout(retryTimerRef.current);
retryTimerRef.current = null;
}, []);
const closeConnection = useCallback(() => {
connectionRef.current?.close();
connectionRef.current = null;
}, []);
const stop = useCallback(() => {
stoppedRef.current = true;
clearRetryTimer();
closeConnection();
}, [clearRetryTimer, closeConnection]);
useEffect(() => {
if (!enabled) return;
stoppedRef.current = false;
const scheduleReconnect = (notify = true) => {
if (stoppedRef.current || retryTimerRef.current) return;
closeConnection();
if (notify) onConnectionError?.();
retryTimerRef.current = setTimeout(() => {
retryTimerRef.current = null;
connect();
}, retryDelayMs);
};
const connect = () => {
if (stoppedRef.current) return;
getToken()
.then(({ sse_token }) => {
if (stoppedRef.current) return;
const wrappedEvents = Object.fromEntries(
Object.entries(events).map(([eventName, handler]) => [
eventName,
(event: unknown, messageEvent: MessageEvent) => {
(
handler as (
event: unknown,
messageEvent: MessageEvent,
) => void
)(event, messageEvent);
if (
shouldStop?.(
eventName as keyof TEvents,
event as TEvents[keyof TEvents],
)
) {
stop();
return;
}
if (reconnectOnEvents.includes(eventName as keyof TEvents)) {
scheduleReconnect(false);
}
},
]),
) as SseEventMap<TEvents>;
const connection = sseService.connect(
getPath(sse_token),
wrappedEvents,
);
connection.source.onerror = () => scheduleReconnect();
connectionRef.current = connection;
})
.catch(() => scheduleReconnect());
};
connect();
return stop;
}, [
closeConnection,
enabled,
events,
getPath,
getToken,
onConnectionError,
reconnectOnEvents,
retryDelayMs,
shouldStop,
stop,
]);
return { stop };
}