'use client'; import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { io } from 'socket.io-client'; import { API_URL, getAccessToken, refreshAccessToken } from '@/lib/api/client'; import { useSession } from '@/lib/session'; // Server topics mapped to the dashboard queries that show that data const TOPIC_QUERIES: Record = { tabs: [['tabs'], ['reminder-candidates']], commands: [['commands']], 'planned-tabs': [['planned-tabs'], ['reminder-candidates']], device: [['device']], reminders: [['reminder-candidates'], ['reminder-history']], users: [['users']], stock: [['stock'], ['stock-history']], }; const RealtimeContext = createContext({ connected: false }); export function RealtimeProvider({ children }: { children: ReactNode }) { const queryClient = useQueryClient(); const { venueId } = useSession(); const [connected, setConnected] = useState(false); useEffect(() => { const socket = io(`${new URL(API_URL).origin}/realtime`, { // Read on every (re)connect so a refreshed token is used. The venue is only used for a superadmin viewing // another venue; everyone else follows their own auth: (cb) => cb({ token: getAccessToken(), venueId }), transports: ['websocket', 'polling'], }); let hasConnected = false; let refreshing = false; let rejections = 0; socket.on('connect', () => { setConnected(true); rejections = 0; // Changes may have been missed while disconnected if (hasConnected) queryClient.invalidateQueries(); hasConnected = true; }); socket.on('disconnect', () => setConnected(false)); socket.on('invalidate', ({ topics }: { topics: string[] }) => { for (const topic of topics) { for (const queryKey of TOPIC_QUERIES[topic] ?? []) { queryClient.invalidateQueries({ queryKey }); } } }); // The access token expired: refresh it and reconnect (rejected handshakes are not retried // automatically). If refreshing fails, the next API call sends the user to the login page. socket.on('connect_error', async (error) => { if (error.message !== 'unauthorized' || refreshing || ++rejections > 3) return; refreshing = true; const token = await refreshAccessToken(); refreshing = false; if (token) socket.connect(); }); return () => { socket.disconnect(); }; }, [queryClient, venueId]); return {children}; } export function useRealtime() { return useContext(RealtimeContext); } /** Polling interval to use only while live updates are unavailable. */ export function usePollingFallback(intervalMs: number): number | false { const { connected } = useRealtime(); return connected ? false : intervalMs; }