@@ -2,12 +2,18 @@ import { useAppDispatch, useAppSelector } from "@/app/hooks";
22import { AppDispatch } from "@/app/store" ;
33import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars" ;
44import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
5+ import { useI18n } from "twake-i18n" ;
56import type { WebSocketWithCleanup } from "./connection" ;
67import { closeWebSocketConnection } from "./connection/lifecycle/closeWebSocketConnection" ;
78import { establishWebSocketConnection } from "./connection/lifecycle/establishWebSocketConnection" ;
89import { useWebSocketReconnect } from "./connection/lifecycle/useWebSocketReconnect" ;
910import { updateCalendars } from "./messaging/updateCalendars" ;
1011import { syncCalendarRegistrations } from "./operations" ;
12+ import { WebSocketStatusSnackbar } from "./WebSocketStatusSnackbar" ;
13+ import {
14+ setupWebSocketPing ,
15+ type PingCleanup ,
16+ } from "./connection/lifecycle/pingWebSocket" ;
1117
1218export function WebSocketGate ( ) {
1319 const socketRef = useRef < WebSocketWithCleanup | null > ( null ) ;
@@ -16,12 +22,19 @@ export function WebSocketGate() {
1622 const reconnectTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
1723 const reconnectAttemptsRef = useRef ( 0 ) ;
1824 const isConnectingRef = useRef ( false ) ;
25+ const pingCleanupRef = useRef < PingCleanup | null > ( null ) ;
1926
2027 const connectTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
2128 const CONNECT_TIMEOUT_MS = 10_000 ;
2229
2330 const hadSocketBeforeRef = useRef ( false ) ;
2431 const justReconnectedRef = useRef ( false ) ;
32+ const [ websocketStatus , setWebSocketStatus ] = useState ( "" ) ;
33+ const [ websocketStatusSerity , setWebSocketStatusSerity ] = useState <
34+ "success" | "info" | "warning" | "error" | undefined
35+ > ( ) ;
36+
37+ const { t } = useI18n ( ) ;
2538
2639 const dispatch = useAppDispatch ( ) ;
2740 const isAuthenticated = useAppSelector ( ( state ) =>
@@ -81,6 +94,8 @@ export function WebSocketGate() {
8194 `WebSocket closed unexpectedly (code: ${ event . code } , reason: ${ event . reason || "none" } ). ` +
8295 `Attempting to reconnect...`
8396 ) ;
97+ setWebSocketStatus ( t ( "websocket.closedUnexpectedly" ) ) ;
98+ setWebSocketStatusSerity ( "warning" ) ;
8499 scheduleReconnect ( ) ;
85100 } else {
86101 reconnectAttemptsRef . current = 0 ;
@@ -92,6 +107,10 @@ export function WebSocketGate() {
92107
93108 const onError = useCallback ( ( error : Event ) => {
94109 console . error ( "WebSocket error:" , error ) ;
110+ const errorMessage =
111+ ( error as ErrorEvent ) ?. message ?? error . type ?? "unknown" ;
112+ setWebSocketStatus ( t ( "websocket.error" , { error : errorMessage } ) ) ;
113+ setWebSocketStatusSerity ( "error" ) ;
95114 } , [ ] ) ;
96115
97116 const callBacks = useMemo (
@@ -110,15 +129,15 @@ export function WebSocketGate() {
110129 // Reset reconnection state on successful connection and mark for calendar re-sync
111130 useEffect ( ( ) => {
112131 if ( isSocketOpen ) {
113- console . log ( "WebSocket connected successfully" ) ;
114-
115132 if ( connectTimeoutRef . current ) {
116133 clearTimeout ( connectTimeoutRef . current ) ;
117134 connectTimeoutRef . current = null ;
118135 }
119136
120137 if ( hadSocketBeforeRef . current ) {
121138 justReconnectedRef . current = true ;
139+ setWebSocketStatus ( t ( "websocket.reconnected" ) ) ;
140+ setWebSocketStatusSerity ( "success" ) ;
122141 }
123142
124143 hadSocketBeforeRef . current = true ;
@@ -227,7 +246,8 @@ export function WebSocketGate() {
227246 // Handle browser online/offline events
228247 useEffect ( ( ) => {
229248 const handleOnline = ( ) => {
230- console . log ( "Browser is online, attempting WebSocket reconnection" ) ;
249+ setWebSocketStatus ( t ( "websocket.browserOnline" ) ) ;
250+ setWebSocketStatusSerity ( "success" ) ;
231251 if ( ! isSocketOpen && isAuthenticatedRef . current ) {
232252 reconnectAttemptsRef . current = 0 ;
233253 clearReconnectTimeout ( ) ;
@@ -236,9 +256,8 @@ export function WebSocketGate() {
236256 } ;
237257
238258 const handleOffline = ( ) => {
239- console . log (
240- "Browser is offline, pausing WebSocket reconnection attempts"
241- ) ;
259+ setWebSocketStatus ( t ( "websocket.browserOffline" ) ) ;
260+ setWebSocketStatusSerity ( "warning" ) ;
242261 cleanupConnection ( ) ;
243262 } ;
244263
@@ -260,5 +279,52 @@ export function WebSocketGate() {
260279 } ;
261280 } , [ isSocketOpen , isAuthenticated , clearReconnectTimeout ] ) ;
262281
263- return null ;
282+ useEffect ( ( ) => {
283+ // Only set up ping if socket is open
284+ if ( ! isSocketOpen || ! socketRef . current ) {
285+ // Clean up existing ping if socket closed
286+ if ( pingCleanupRef . current ) {
287+ pingCleanupRef . current . stop ( ) ;
288+ pingCleanupRef . current = null ;
289+ }
290+ return ;
291+ }
292+
293+ // Set up ping monitoring
294+ const pingCleanup = setupWebSocketPing ( socketRef . current , {
295+ onConnectionDead : ( ) => {
296+ console . warn ( "WebSocket connection appears dead (no pong received)" ) ;
297+ setWebSocketStatus ( t ( "websocket.browserOffline" ) ) ;
298+ setWebSocketStatusSerity ( "warning" ) ;
299+
300+ // Trigger reconnection
301+ if ( socketRef . current ) {
302+ socketRef . current . close ( ) ;
303+ }
304+ } ,
305+ onPingFail : ( ) => {
306+ console . warn ( "Failed to send ping" ) ;
307+ } ,
308+ } ) ;
309+
310+ pingCleanupRef . current = pingCleanup ;
311+
312+ return ( ) => {
313+ if ( pingCleanupRef . current ) {
314+ pingCleanupRef . current . stop ( ) ;
315+ pingCleanupRef . current = null ;
316+ }
317+ } ;
318+ } , [ isSocketOpen ] ) ;
319+
320+ return websocketStatus ? (
321+ < WebSocketStatusSnackbar
322+ message = { websocketStatus }
323+ severity = { websocketStatusSerity }
324+ onClose = { ( ) => {
325+ setWebSocketStatus ( "" ) ;
326+ setWebSocketStatusSerity ( undefined ) ;
327+ } }
328+ />
329+ ) : null ;
264330}
0 commit comments