Two bugs caused the domainRealtime channel to fail with CHANNEL_ERROR and
reconnect-loop every 3s (which also dragged down the notifRealtime channel via
the shared websocket close):
1. useDomainSubmissionRealtime.ts filtered domain_submissions on a column that
doesn't exist (`submitter_id`) — the actual column is `user_id`. Postgres
raised on the publication-side filter registration → CHANNEL_ERROR.
2. rebreak.user_custom_domains was never added to the supabase_realtime
publication — the channel also subscribes to that table. New migration
20260511_fix_realtime_user_custom_domains adds it.
(Diagnosis via backyard agent against the self-hosted Supabase on the Hetzner box.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import { useEffect } from "react";
|
|
import { supabase } from "../lib/supabase";
|
|
import type { RealtimeChannel } from "@supabase/supabase-js";
|
|
|
|
/**
|
|
* Realtime-Subscription für die Blocker-Page.
|
|
* Lauscht auf:
|
|
* - UPDATE auf rebreak.domain_submissions → ruft `onChange()` (refetch)
|
|
* - INSERT auf rebreak.notifications mit type=domain_accepted für eigene recipient_id → refetch
|
|
*
|
|
* Pendant zum Nuxt-Code in apps/rebreak/app/pages/app/blocker/index.vue.
|
|
*/
|
|
export function useDomainSubmissionRealtime(
|
|
onChange: () => void,
|
|
enabled: boolean = true,
|
|
) {
|
|
useEffect(() => {
|
|
if (!enabled) return;
|
|
let channel: RealtimeChannel | null = null;
|
|
let cancelled = false;
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
async function subscribe() {
|
|
const { data } = await supabase.auth.getSession();
|
|
const session = data.session;
|
|
if (!session?.access_token) return;
|
|
if (cancelled) return;
|
|
|
|
supabase.realtime.setAuth(session.access_token);
|
|
const myId = session.user.id;
|
|
|
|
channel = supabase
|
|
.channel(`blocker:domains:${myId}:${Date.now()}`)
|
|
.on(
|
|
"postgres_changes",
|
|
{
|
|
event: "*",
|
|
schema: "rebreak",
|
|
table: "domain_submissions",
|
|
filter: `user_id=eq.${myId}`,
|
|
},
|
|
() => onChange(),
|
|
)
|
|
.on(
|
|
"postgres_changes",
|
|
{
|
|
event: "*",
|
|
schema: "rebreak",
|
|
table: "user_custom_domains",
|
|
filter: `user_id=eq.${myId}`,
|
|
},
|
|
() => onChange(),
|
|
)
|
|
.on(
|
|
"postgres_changes",
|
|
{
|
|
event: "INSERT",
|
|
schema: "rebreak",
|
|
table: "notifications",
|
|
filter: `recipient_id=eq.${myId}`,
|
|
},
|
|
(payload: any) => {
|
|
const t = payload.new?.type;
|
|
if (t === "domain_accepted" || t === "domain_rejected") {
|
|
onChange();
|
|
}
|
|
},
|
|
)
|
|
.subscribe((status, err) => {
|
|
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
console.warn("[domainRealtime] error:", status, err ?? "");
|
|
cleanup();
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
reconnectTimer = setTimeout(() => {
|
|
if (!cancelled) subscribe();
|
|
}, 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
function cleanup() {
|
|
if (channel) {
|
|
supabase.removeChannel(channel);
|
|
channel = null;
|
|
}
|
|
}
|
|
|
|
subscribe();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
cleanup();
|
|
};
|
|
}, [enabled, onChange]);
|
|
}
|