148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
'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<ConnectionStatus>('disconnected');
|
|
const channelRef = useRef<RealtimeChannel | null>(null);
|
|
const supabaseRef = useRef(createClient());
|
|
const reconnectAttemptsRef = useRef(0);
|
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
const isComponentMountedRef = useRef(true);
|
|
const visibilityTimeoutRef = useRef<NodeJS.Timeout | undefined>(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,
|
|
};
|
|
};
|