Реал-тайм через SSE (push-to-invalidate): живые приглашения/партии, авто-обновление без перезагрузки

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 22:52:41 +03:00
co-authored by Claude Opus 4.8
parent 16452f7bbe
commit 2500ca575b
15 changed files with 357 additions and 9 deletions
+56
View File
@@ -0,0 +1,56 @@
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { qk } from "../api/queryKeys";
interface ServerEvent {
type: "match" | "group" | "invitations";
match_id?: number;
group_id?: number;
}
/**
* Подписка на SSE-поток `/api/events` (push-to-invalidate): по событию с сервера точечно
* инвалидируем нужные запросы, и TanStack Query сам их перезапрашивает. EventSource
* переподключается автоматически. Открываем одно соединение, только когда авторизованы.
*/
export function useServerEvents(enabled: boolean) {
const qc = useQueryClient();
useEffect(() => {
if (!enabled) return;
const base = import.meta.env.VITE_API_BASE_URL || "";
const es = new EventSource(`${base}/api/events`, { withCredentials: true });
es.onmessage = (e) => {
let ev: ServerEvent;
try {
ev = JSON.parse(e.data) as ServerEvent;
} catch {
return;
}
if (ev.type === "invitations") {
qc.invalidateQueries({ queryKey: qk.invitations });
} else if (ev.type === "match") {
if (ev.match_id != null) qc.invalidateQueries({ queryKey: qk.match(ev.match_id) });
if (ev.group_id != null) {
qc.invalidateQueries({ queryKey: qk.groupMatches(ev.group_id) });
qc.invalidateQueries({ queryKey: qk.groupStats(ev.group_id) });
}
qc.invalidateQueries({ queryKey: qk.home });
qc.invalidateQueries({ queryKey: qk.leaderboard });
} else if (ev.type === "group") {
if (ev.group_id != null) {
qc.invalidateQueries({ queryKey: qk.group(ev.group_id) });
qc.invalidateQueries({ queryKey: qk.groupMembers(ev.group_id) });
qc.invalidateQueries({ queryKey: qk.groupStats(ev.group_id) });
}
qc.invalidateQueries({ queryKey: qk.me });
qc.invalidateQueries({ queryKey: qk.home });
qc.invalidateQueries({ queryKey: qk.invitations });
}
};
// onerror не логируем: EventSource переподключается сам.
return () => es.close();
}, [enabled, qc]);
}