"use client"; import React, { useEffect, useRef } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { WEBSOCKET_URL } from "./apiClient"; import { useAuth } from "./AuthProvider"; import { z } from "zod"; import { invalidateTranscriptLists, TRANSCRIPT_SEARCH_URL } from "./apiHooks"; const UserEvent = z.object({ event: z.string(), }); type UserEvent = z.TypeOf; class UserEventsStore { private socket: WebSocket | null = null; private listeners: Set<(event: MessageEvent) => void> = new Set(); private closeTimeoutId: number | null = null; private isConnecting = false; ensureConnection(url: string, subprotocols?: string[]) { if (typeof window === "undefined") return; if (this.closeTimeoutId !== null) { clearTimeout(this.closeTimeoutId); this.closeTimeoutId = null; } if (this.isConnecting) return; if ( this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) ) { return; } this.isConnecting = true; const ws = new WebSocket(url, subprotocols || []); this.socket = ws; ws.onmessage = (event: MessageEvent) => { this.listeners.forEach((listener) => { try { listener(event); } catch (err) { console.error("UserEvents listener error", err); } }); }; ws.onopen = () => { if (this.socket === ws) this.isConnecting = false; }; ws.onclose = () => { if (this.socket === ws) { this.socket = null; this.isConnecting = false; } }; ws.onerror = () => { if (this.socket === ws) this.isConnecting = false; }; } subscribe(listener: (event: MessageEvent) => void): () => void { this.listeners.add(listener); if (this.closeTimeoutId !== null) { clearTimeout(this.closeTimeoutId); this.closeTimeoutId = null; } return () => { this.listeners.delete(listener); if (this.listeners.size === 0) { this.closeTimeoutId = window.setTimeout(() => { if (this.socket) { try { this.socket.close(); } catch (err) { console.warn("Error closing user events socket", err); } } this.socket = null; this.closeTimeoutId = null; }, 1000); } }; } } const sharedStore = new UserEventsStore(); export function UserEventsProvider({ children, }: { children: React.ReactNode; }) { const auth = useAuth(); const queryClient = useQueryClient(); const tokenRef = useRef(null); const detachRef = useRef<(() => void) | null>(null); useEffect(() => { // Only tear down when the user is truly unauthenticated if (auth.status === "unauthenticated") { if (detachRef.current) { try { detachRef.current(); } catch (err) { console.warn("Error detaching UserEvents listener", err); } detachRef.current = null; } tokenRef.current = null; return; } // During loading/refreshing, keep the existing connection intact if (auth.status !== "authenticated") { return; } // Authenticated: pin the initial token for the lifetime of this WS connection if (!tokenRef.current && auth.accessToken) { tokenRef.current = auth.accessToken; } const pinnedToken = tokenRef.current; const url = `${WEBSOCKET_URL}/v1/events`; // Ensure a single shared connection sharedStore.ensureConnection( url, pinnedToken ? ["bearer", pinnedToken] : undefined, ); // Subscribe once; avoid re-subscribing during transient status changes if (!detachRef.current) { const onMessage = (event: MessageEvent) => { try { const msg = UserEvent.parse(JSON.parse(event.data)); const eventName = msg.event; const invalidateList = () => invalidateTranscriptLists(queryClient); switch (eventName) { case "TRANSCRIPT_CREATED": case "TRANSCRIPT_DELETED": case "TRANSCRIPT_STATUS": case "TRANSCRIPT_FINAL_TITLE": case "TRANSCRIPT_DURATION": invalidateList().then(() => {}); break; default: // Ignore other content events for list updates break; } } catch (err) { console.warn("Invalid user event message", event.data); } }; const unsubscribe = sharedStore.subscribe(onMessage); detachRef.current = unsubscribe; } }, [auth.status, queryClient]); // On unmount, detach the listener and clear the pinned token useEffect(() => { return () => { if (detachRef.current) { try { detachRef.current(); } catch (err) { console.warn("Error detaching UserEvents listener on unmount", err); } detachRef.current = null; } tokenRef.current = null; }; }, []); return <>{children}; }