Files
tech-tracker-next/src/lib/hooks/useSharedStatusSubscription.ts

132 lines
3.5 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { createClient } from '@/utils/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
export type ConnectionStatus =
| 'connecting'
| 'connected'
| 'disconnected'
| 'updating';
// Singleton state
let sharedChannel: RealtimeChannel | null = null;
let sharedConnectionStatus: ConnectionStatus = 'disconnected';
const subscribers = new Set<(status: ConnectionStatus) => void>();
const statusUpdateCallbacks = new Set<() => void>();
//const subscribers: Set<(status: ConnectionStatus) => void> = new Set();
//const statusUpdateCallbacks: Set<() => void> = new Set();
let reconnectAttempts = 0;
let reconnectTimeout: NodeJS.Timeout | undefined;
const supabase = createClient();
const notifySubscribers = (status: ConnectionStatus) => {
sharedConnectionStatus = status;
subscribers.forEach(callback => callback(status));
};
const notifyStatusUpdate = () => {
statusUpdateCallbacks.forEach(callback => callback());
};
const cleanup = () => {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = undefined;
}
if (sharedChannel) {
supabase.removeChannel(sharedChannel).catch((error) => {
console.error('Error removing shared channel:', error);
});
sharedChannel = null;
}
};
const connect = () => {
if (sharedChannel) return; // Already connected or connecting
cleanup();
notifySubscribers('connecting');
const channel = supabase
.channel('shared_status_updates', {
config: { broadcast: {self: true }}
})
.on('broadcast', { event: 'status_updated' }, () => {
notifyStatusUpdate();
})
.subscribe((status) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (status === 'SUBSCRIBED') {
notifySubscribers('connected');
reconnectAttempts = 0;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') {
notifySubscribers('disconnected');
if (reconnectAttempts < 5) {
reconnectAttempts++;
const delay = 2000 * reconnectAttempts;
reconnectTimeout = setTimeout(() => {
if (subscribers.size > 0) { // Only reconnect if there are active subscribers
connect();
}
}, delay);
}
}
});
sharedChannel = channel;
};
const disconnect = () => {
cleanup();
notifySubscribers('disconnected');
};
export const useSharedStatusSubscription = (onStatusUpdate?: () => void) => {
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>(sharedConnectionStatus);
useEffect(() => {
// Subscribe to status changes
subscribers.add(setConnectionStatus);
// Subscribe to status updates
if (onStatusUpdate) {
statusUpdateCallbacks.add(onStatusUpdate);
}
// Connect if this is the first subscriber
if (subscribers.size === 1) {
const timeout = setTimeout(connect, 1000);
return () => clearTimeout(timeout);
}
return () => {
// Cleanup subscriptions
subscribers.delete(setConnectionStatus);
if (onStatusUpdate) {
statusUpdateCallbacks.delete(onStatusUpdate);
}
// Disconnect if no more subscribers
if (subscribers.size === 0) {
disconnect();
}
};
}, [onStatusUpdate]);
const reconnect = useCallback(() => {
reconnectAttempts = 0;
connect();
}, []);
return {
connectionStatus,
connect: reconnect,
disconnect,
};
};