"use client"; import { useEffect, useRef, useCallback, useState } from "react"; import { createClient } from "@/lib/supabase/client"; import type { Message, Conversation } from "@/types"; import type { RealtimeChannel } from "@supabase/supabase-js"; interface RealtimeEvent { eventType: "INSERT" | "UPDATE" | "DELETE"; new: T; old: Partial; } interface UseRealtimeOptions { channelName: string; onMessageEvent?: (event: RealtimeEvent) => void; onConversationEvent?: (event: RealtimeEvent) => void; enabled?: boolean; } export function useRealtime({ channelName, onMessageEvent, onConversationEvent, enabled = true, }: UseRealtimeOptions) { const channelRef = useRef(null); const [isConnected, setIsConnected] = useState(false); // Store latest callbacks in refs to avoid re-subscribing when the // parent re-renders with fresh closures. Assigned inside an effect // so the mutation doesn't happen during render (React 19's refs // rule) — subscribers only read `.current` inside async Realtime // callbacks, which always run after the render that updates it. const onMessageRef = useRef(onMessageEvent); const onConversationRef = useRef(onConversationEvent); useEffect(() => { onMessageRef.current = onMessageEvent; onConversationRef.current = onConversationEvent; }); useEffect(() => { if (!enabled) return; const supabase = createClient(); const channel = supabase .channel(channelName) .on( "postgres_changes", { event: "*", schema: "public", table: "messages" }, (payload) => { onMessageRef.current?.({ eventType: payload.eventType as RealtimeEvent["eventType"], new: payload.new as Message, old: payload.old as Partial, }); } ) .on( "postgres_changes", { event: "*", schema: "public", table: "conversations" }, (payload) => { onConversationRef.current?.({ eventType: payload.eventType as RealtimeEvent["eventType"], new: payload.new as Conversation, old: payload.old as Partial, }); } ) .subscribe((status) => { setIsConnected(status === "SUBSCRIBED"); }); channelRef.current = channel; return () => { supabase.removeChannel(channel); channelRef.current = null; setIsConnected(false); }; }, [channelName, enabled]); const unsubscribe = useCallback(() => { if (channelRef.current) { const supabase = createClient(); supabase.removeChannel(channelRef.current); channelRef.current = null; setIsConnected(false); } }, []); return { isConnected, unsubscribe }; }