"use client"; import { useEffect, useRef, useState } from "react"; import { createBrowserClient } from "@supabase/ssr"; import { realtimeRefresh } from "@/lib/realtime-refresh"; export function useAppealRealtime({ enabled, ownerId, refresh, onError, }: { enabled: boolean; ownerId?: string; refresh: () => Promise; onError: (error: unknown) => void; }) { const [status, setStatus] = useState<"connecting" | "live" | "offline">( "connecting", ); const handlers = useRef({ refresh, onError }); useEffect(() => { handlers.current = { refresh, onError }; }, [refresh, onError]); useEffect(() => { if (!enabled) return; const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY; if (!url || !key) { setStatus("offline"); return; } let active = true; let connected = false; let debounce: ReturnType | undefined; const updates = realtimeRefresh( () => handlers.current.refresh(), (error) => handlers.current.onError(error), ); const invalidate = () => { if (!active || debounce !== undefined) return; debounce = setTimeout(() => { debounce = undefined; void updates.request(); }, 200); }; const client = createBrowserClient(url, key); const options = { schema: "public", table: "account_appeals", ...(ownerId ? { filter: `owner_id=eq.${ownerId}` } : {}), }; const channel = client .channel(`account-appeals:${ownerId ?? "staff"}:${crypto.randomUUID()}`, { config: { postgres_changes_options: { wait: true } }, }) .on("system", {}, (event) => { if ( active && event.extension === "postgres_changes" && event.status === "error" ) { connected = false; setStatus("offline"); console.warn( "Appeal live updates unavailable:", String(event.message ?? "subscription error") .replace(/eyJ[A-Za-z0-9_.-]+/g, "[redacted]") .slice(0, 240), ); } }) // DELETE events cannot be checked against a surviving row's RLS policy. .on("postgres_changes", { ...options, event: "INSERT" }, invalidate) .on("postgres_changes", { ...options, event: "UPDATE" }, invalidate); if (ownerId) channel.on( "postgres_changes", { event: "UPDATE", schema: "public", table: "profiles", filter: `id=eq.${ownerId}`, }, invalidate, ); channel.subscribe((state, error) => { if (!active) return; connected = state === "SUBSCRIBED"; if (error) console.warn( "Appeal live updates unavailable:", error.message .replace(/eyJ[A-Za-z0-9_.-]+/g, "[redacted]") .slice(0, 240), ); setStatus( connected ? "live" : state === "CLOSED" || state === "CHANNEL_ERROR" || state === "TIMED_OUT" ? "offline" : "connecting", ); if (connected) invalidate(); // Close the initial-load/reconnection gap. }); const visible = () => { if (document.visibilityState === "visible") invalidate(); }; document.addEventListener("visibilitychange", visible); window.addEventListener("focus", invalidate); window.addEventListener("online", invalidate); const fallback = setInterval(() => { if (!connected && document.visibilityState === "visible") invalidate(); }, 30000); return () => { active = false; updates.close(); if (debounce !== undefined) clearTimeout(debounce); clearInterval(fallback); document.removeEventListener("visibilitychange", visible); window.removeEventListener("focus", invalidate); window.removeEventListener("online", invalidate); void client.removeChannel(channel); }; }, [enabled, ownerId]); return status; }