Files
MineGeo/components/useAppealRealtime.ts

129 lines
4.0 KiB
TypeScript

"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<void>;
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<typeof setTimeout> | 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;
}