import type { QueryClient, QueryKey } from '@tanstack/react-query'; /** Minimum surface of socket.io's client we use here. Kept loose so the * helper can be unit-tested with a stub object without dragging the full * socket.io dependency into the test runtime. */ export interface SocketLike { on(event: string, handler: (...args: unknown[]) => void): unknown; off(event: string, handler: (...args: unknown[]) => void): unknown; } export type EventMap = Record; /** * Pure subscription logic for `useRealtimeInvalidation`. Registers one * handler per event key. Each handler reads the latest eventMap from the * supplied getter so callers can pass a fresh object literal on every render * without re-subscribing. * * Returns a cleanup function that removes the registered handlers. * * Lives in its own JSX-free file so it can be unit-tested under vitest's * node environment without dragging the React provider into the bundle. */ export function subscribeRealtimeInvalidations( socket: SocketLike, eventKeys: string[], queryClient: Pick, getEventMap: () => EventMap, ): () => void { const handlers: Array<{ event: string; handler: (...args: unknown[]) => void }> = []; for (const event of eventKeys) { const handler = () => { // Read the LATEST map at fire-time - not at subscription time - so // callers passing inline `{ 'client:created': [...] }` literals don't // bind a stale snapshot if they re-render. const queryKeys = getEventMap()[event]; if (!queryKeys) return; for (const key of queryKeys) { queryClient.invalidateQueries({ queryKey: key }); } }; socket.on(event, handler); handlers.push({ event, handler }); } return () => { for (const { event, handler } of handlers) { socket.off(event, handler); } }; }