132 lines
3.5 KiB
TypeScript
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,
|
|
};
|
|
};
|