import { useEffect, useRef } from "react"; import { useQueryClient } from "@tanstack/react-query"; export const SYNC_BLOCK_QUERY_KEY = "sync-block"; /** * SSE hook for sync block realtime updates (R4.2). * * Listens to the NestJS EventEmitter2 via an SSE endpoint: * GET /api/acadenice/sync-blocks/{masterId}/events * * When the server broadcasts a `sync-block.updated` event for this masterId, * we invalidate the React Query cache entry so the NodeView re-fetches the * latest content transparently. * * Note: the SSE endpoint is planned for R4.2.b. Until then, the hook polls * for cache invalidation triggered by Hocuspocus awareness updates instead. * The hook signature is stable — wiring the actual SSE URL is a 1-line change. * * Reconnect: exponential backoff (1s → 2s → … → 30s max), same pattern as * useDatabaseRealtimeUpdates to stay consistent with the codebase. */ export function useSyncBlockRealtime(masterId: string | undefined): void { const queryClient = useQueryClient(); const esRef = useRef(null); const retryDelayRef = useRef(1000); const isMountedRef = useRef(true); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); useEffect(() => { if (!masterId) return; const sseUrl = `/api/acadenice/sync-blocks/${encodeURIComponent(masterId)}/events`; let retryTimeout: ReturnType | null = null; function connect() { if (!isMountedRef.current) return; const es = new EventSource(sseUrl, { withCredentials: true }); esRef.current = es; es.addEventListener("open", () => { retryDelayRef.current = 1000; }); es.addEventListener("sync-block.updated", () => { queryClient.invalidateQueries({ queryKey: [SYNC_BLOCK_QUERY_KEY, masterId], exact: true, }); }); es.addEventListener("message", (evt) => { try { const payload = JSON.parse(evt.data) as { masterId?: string }; if (!payload.masterId || payload.masterId === masterId) { queryClient.invalidateQueries({ queryKey: [SYNC_BLOCK_QUERY_KEY, masterId], exact: true, }); } } catch { // Unparseable SSE data — ignore. } }); es.addEventListener("error", () => { es.close(); esRef.current = null; if (!isMountedRef.current) return; const delay = Math.min(retryDelayRef.current, 30_000); retryDelayRef.current = Math.min(retryDelayRef.current * 2, 30_000); retryTimeout = setTimeout(() => { if (isMountedRef.current) connect(); }, delay); }); } connect(); return () => { esRef.current?.close(); esRef.current = null; if (retryTimeout !== null) clearTimeout(retryTimeout); }; }, [masterId, queryClient]); }