'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { createClient } from '@/utils/supabase'; import type { RealtimeChannel } from '@supabase/supabase-js'; export type ConnectionStatus = | 'connecting' | 'connected' | 'disconnected' | 'updating'; type UseStatusSubscriptionOptions = { enabled?: boolean; onStatusUpdate?: () => void; maxReconnectAttempts?: number; reconnectDelay?: number; } export const useStatusSubscription = ({ enabled = true, onStatusUpdate, maxReconnectAttempts = 5, reconnectDelay = 2000, }: UseStatusSubscriptionOptions = {}) => { const [connectionStatus, setConnectionStatus] = useState('disconnected'); const channelRef = useRef(null); const supabaseRef = useRef(createClient()); const reconnectAttemptsRef = useRef(0); const reconnectTimeoutRef = useRef(undefined); const isComponentMountedRef = useRef(true); const visibilityTimeoutRef = useRef(undefined); const cleanup = useCallback(() => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = undefined; } if (visibilityTimeoutRef.current) { clearTimeout(visibilityTimeoutRef.current); visibilityTimeoutRef.current = undefined; } if (channelRef.current) { supabaseRef.current.removeChannel(channelRef.current).catch((error) => { console.error('❌ cleanup: Error removing channel:', error); }); channelRef.current = null; } }, []); const connect = useCallback(() => { if (!enabled || !isComponentMountedRef.current) return; cleanup(); setConnectionStatus('connecting'); const channel = supabaseRef.current .channel('status_updates', { config: { broadcast: {self: true }} }); channel .on('broadcast', { event: 'status_updated' }, (payload) => { onStatusUpdate?.(); }) .subscribe((status) => { if (!isComponentMountedRef.current) return; // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (status === 'SUBSCRIBED') { setConnectionStatus('connected'); reconnectAttemptsRef.current = 0; // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison } else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') { setConnectionStatus('disconnected'); if (reconnectAttemptsRef.current < maxReconnectAttempts) { reconnectAttemptsRef.current++; const delay = reconnectDelay * reconnectAttemptsRef.current; reconnectTimeoutRef.current = setTimeout(() => { if (isComponentMountedRef.current) connect(); }, delay); } else { console.warn('⚠️ connect: Max reconnection attempts reached'); setConnectionStatus('disconnected'); } } }); channelRef.current = channel; }, [enabled, onStatusUpdate, maxReconnectAttempts, reconnectDelay, cleanup]); const disconnect = useCallback(() => { cleanup(); setConnectionStatus('disconnected'); }, [cleanup]); const reconnect = useCallback(() => { reconnectAttemptsRef.current = 0; connect(); }, [connect]); // Handle visibility change for better reconnection useEffect(() => { const handleVisibilityChange = () => { if (!enabled) return; if (document.visibilityState === 'visible') { visibilityTimeoutRef.current = setTimeout(() => { if (connectionStatus === 'disconnected' && isComponentMountedRef.current) { reconnect(); } }, 1000); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [enabled, connectionStatus, reconnect]); // Initial connection - SIMPLIFIED to avoid dependency issues useEffect(() => { if (!enabled) { disconnect(); return; } const initialTimeout = setTimeout(() => { if (isComponentMountedRef.current) connect(); }, 1000); return () => { clearTimeout(initialTimeout); }; }, [enabled]); // Cleanup on unmount useEffect(() => { return () => { cleanup(); }; }, [cleanup]); return { connectionStatus, connect: reconnect, disconnect, }; };