A work-in-progress chat bot for Streamplace with chat overlay functionality
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Reconnecting frontend websocket

+68 -36
+67 -36
islands/ChatOverlay.tsx
··· 28 28 const [connected, setConnected] = useState(false); 29 29 const [isClient, setIsClient] = useState(false); 30 30 const wsRef = useRef<WebSocket | null>(null); 31 + const retryDelayRef = useRef<number>(1000); 32 + const retryTimeoutRef = useRef<number | null>(null); 33 + const isMountedRef = useRef<boolean>(true); 31 34 32 35 // Prune old pending deletes (older than 60 seconds) 33 36 const prunePendingDeletes = useCallback( ··· 41 44 // This effect runs only on the client after hydration 42 45 useEffect(() => { 43 46 setIsClient(true); 47 + return () => { 48 + isMountedRef.current = false; 49 + }; 44 50 }, []); 45 51 46 52 // Handle message expiration ··· 105 111 const wsUrl = 106 112 `${protocol}//${globalThis.location.host}/api/websocket/${streamerHandle}`; 107 113 108 - console.log(`Attempting to connect to: ${wsUrl}`); 114 + function connect() { 115 + if (!isMountedRef.current) return; 109 116 110 - const ws = new WebSocket(wsUrl); 111 - wsRef.current = ws; 117 + console.log(`Attempting to connect to: ${wsUrl}`); 118 + const ws = new WebSocket(wsUrl); 119 + wsRef.current = ws; 112 120 113 - ws.onopen = () => { 114 - console.log(`🟢 WebSocket CONNECTED to: ${wsUrl}`); 115 - setConnected(true); 116 - }; 121 + ws.onopen = () => { 122 + console.log(`🟢 WebSocket CONNECTED to: ${wsUrl}`); 123 + retryDelayRef.current = 1000; // reset backoff on successful connection 124 + setConnected(true); 125 + }; 117 126 118 - ws.onmessage = (event) => { 119 - try { 120 - const data = JSON.parse(event.data); 121 - console.log("Received message:", data); 127 + ws.onmessage = (event) => { 128 + try { 129 + const data = JSON.parse(event.data); 130 + console.log("Received message:", data); 122 131 123 - if (data.type === "chat_message") { 124 - const message: EnrichedChatMessage = { 125 - ...data.data, 126 - timestamp: new Date(data.data.timestamp), 127 - }; 132 + if (data.type === "chat_message") { 133 + const message: EnrichedChatMessage = { 134 + ...data.data, 135 + timestamp: new Date(data.data.timestamp), 136 + }; 128 137 129 - handleAddMessage(message); 130 - } else if (data.type === "subscribed") { 131 - console.log("Successfully subscribed to:", data.streamer); 138 + handleAddMessage(message); 139 + } else if (data.type === "subscribed") { 140 + console.log( 141 + "Successfully subscribed to:", 142 + data.streamer, 143 + ); 144 + } 145 + } catch (error) { 146 + console.error("Error parsing message:", error); 132 147 } 133 - } catch (error) { 134 - console.error("Error parsing message:", error); 135 - } 136 - }; 148 + }; 149 + 150 + ws.onclose = (event) => { 151 + console.log( 152 + "Disconnected from WebSocket:", 153 + event.code, 154 + event.reason, 155 + ); 156 + setConnected(false); 157 + 158 + if (!isMountedRef.current) return; 159 + 160 + console.log(`Reconnecting in ${retryDelayRef.current}ms...`); 161 + retryTimeoutRef.current = setTimeout(() => { 162 + retryDelayRef.current = Math.min( 163 + retryDelayRef.current * 2, 164 + 30_000, 165 + ); 166 + connect(); 167 + }, retryDelayRef.current); 168 + }; 137 169 138 - ws.onclose = (event) => { 139 - console.log( 140 - "Disconnected from WebSocket:", 141 - event.code, 142 - event.reason, 143 - ); 144 - setConnected(false); 145 - }; 170 + ws.onerror = (error) => { 171 + // Don't reconnect here — onclose always fires after onerror 172 + console.error("WebSocket error:", error); 173 + }; 174 + } 146 175 147 - ws.onerror = (error) => { 148 - console.error("WebSocket error:", error); 149 - }; 176 + connect(); 150 177 151 178 return () => { 152 - if (ws.readyState === WebSocket.OPEN) { 153 - ws.close(); 179 + isMountedRef.current = false; 180 + if (retryTimeoutRef.current !== null) { 181 + clearTimeout(retryTimeoutRef.current); 182 + } 183 + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { 184 + wsRef.current.close(); 154 185 } 155 186 }; 156 187 }, [isClient, streamerHandle, handleAddMessage]);
+1
utils/websocket.ts
··· 133 133 } 134 134 135 135 removeClient(client: WebSocket) { 136 + if (!this.clients.has(client)) return; 136 137 const clientStreamers = this.clients.get(client); 137 138 if (clientStreamers) { 138 139 for (const streamer of clientStreamers) {