117 lines
3.3 KiB
TypeScript
117 lines
3.3 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { Logger } from "@/lib/logger";
|
|
import { useAuth } from "@/contexts/auth-context";
|
|
import { tournamentQueries } from "@/features/tournaments/queries";
|
|
import { reactionKeys, reactionQueries } from "@/features/reactions/queries";
|
|
|
|
const logger = new Logger('ServerEvents');
|
|
|
|
type SSEEvent = {
|
|
type: string;
|
|
[key: string]: any;
|
|
};
|
|
|
|
type EventHandler = (event: SSEEvent, queryClient: ReturnType<typeof useQueryClient>, currentSessionId?: string) => void;
|
|
|
|
const eventHandlers: Record<string, EventHandler> = {
|
|
"connected": () => {
|
|
logger.info("New Connection");
|
|
},
|
|
"ping": () => {},
|
|
"heartbeat": () => {},
|
|
"match": (event, queryClient) => {
|
|
queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId))
|
|
queryClient.invalidateQueries(tournamentQueries.current())
|
|
},
|
|
"reaction": (event, queryClient) => {
|
|
queryClient.invalidateQueries(reactionQueries.match(event.matchId));
|
|
queryClient.setQueryData(reactionKeys.match(event.matchId), () => event.reactions);
|
|
}
|
|
};
|
|
|
|
export function useServerEvents() {
|
|
const queryClient = useQueryClient();
|
|
const { user } = useAuth();
|
|
const retryCountRef = useRef(0);
|
|
const shouldConnectRef = useRef(true);
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
if (!user?.id) return;
|
|
|
|
shouldConnectRef.current = true;
|
|
retryCountRef.current = 0;
|
|
|
|
const connectEventSource = () => {
|
|
if (!shouldConnectRef.current) return;
|
|
|
|
|
|
const eventSource = new EventSource(`/api/events/$`);
|
|
|
|
eventSource.onopen = () => {
|
|
retryCountRef.current = 0;
|
|
};
|
|
|
|
eventSource.onmessage = (event) => {
|
|
try {
|
|
const data: SSEEvent = JSON.parse(event.data);
|
|
logger.info("Event received", data);
|
|
|
|
const handler = eventHandlers[data.type];
|
|
if (handler) {
|
|
handler(data, queryClient, user?.id);
|
|
} else {
|
|
logger.warn(`Unhandled SSE event type: ${data.type}`);
|
|
}
|
|
} catch (error) {
|
|
logger.error("Error parsing SSE message", error);
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = (error) => {
|
|
logger.error("SSE connection error", error);
|
|
eventSource.close();
|
|
|
|
if (shouldConnectRef.current && retryCountRef.current < 10) {
|
|
retryCountRef.current += 1;
|
|
const delay = Math.min(
|
|
1000 * Math.pow(1.5, retryCountRef.current - 1),
|
|
15000
|
|
);
|
|
|
|
logger.info(
|
|
`SSE reconnection attempt ${retryCountRef.current}/10 in ${delay}ms`
|
|
);
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
if (shouldConnectRef.current) {
|
|
connectEventSource();
|
|
}
|
|
}, delay);
|
|
} else if (retryCountRef.current >= 10) {
|
|
logger.error("SSE max reconnection attempts reached");
|
|
}
|
|
};
|
|
|
|
return eventSource;
|
|
};
|
|
|
|
const eventSource = connectEventSource();
|
|
|
|
return () => {
|
|
logger.info("Closing SSE connection");
|
|
shouldConnectRef.current = false;
|
|
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
timeoutRef.current = null;
|
|
}
|
|
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
}
|
|
};
|
|
}, [user?.id]);
|
|
} |