* #708 apply strictier linting rules and fix simple eslint bugs * #708 fix eslint errors relate to promise * #708 fix eslint import/no-extraneous-dependencies * #708 fix eslint errors of react-hook * #708 enable eslint check for typescript --------- Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
+176
-176
@@ -1,71 +1,71 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import type { WebSocketWithCleanup } from "./connection";
|
||||
import { closeWebSocketConnection } from "./connection/lifecycle/closeWebSocketConnection";
|
||||
import { establishWebSocketConnection } from "./connection/lifecycle/establishWebSocketConnection";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { AppDispatch } from '@/app/store'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { useSelectedCalendars } from '@/utils/storage/useSelectedCalendars'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import type { WebSocketWithCleanup } from './connection'
|
||||
import { closeWebSocketConnection } from './connection/lifecycle/closeWebSocketConnection'
|
||||
import { establishWebSocketConnection } from './connection/lifecycle/establishWebSocketConnection'
|
||||
import {
|
||||
setupWebSocketPing,
|
||||
type PingCleanup,
|
||||
} from "./connection/lifecycle/pingWebSocket";
|
||||
import { useWebSocketReconnect } from "./connection/lifecycle/useWebSocketReconnect";
|
||||
type PingCleanup
|
||||
} from './connection/lifecycle/pingWebSocket'
|
||||
import { useWebSocketReconnect } from './connection/lifecycle/useWebSocketReconnect'
|
||||
import {
|
||||
registerWebSocketState,
|
||||
setWebSocketConnecting,
|
||||
} from "./connection/webSocketState";
|
||||
import { updateCalendars } from "./messaging/updateCalendars";
|
||||
import { syncCalendarRegistrations } from "./operations";
|
||||
import { WebSocketStatusSnackbar } from "./WebSocketStatusSnackbar";
|
||||
setWebSocketConnecting
|
||||
} from './connection/webSocketState'
|
||||
import { updateCalendars } from './messaging/updateCalendars'
|
||||
import { syncCalendarRegistrations } from './operations'
|
||||
import { WebSocketStatusSnackbar } from './WebSocketStatusSnackbar'
|
||||
|
||||
export function WebSocketGate() {
|
||||
const socketRef = useRef<WebSocketWithCleanup | null>(null);
|
||||
const previousCalendarListRef = useRef<string[]>([]);
|
||||
const previousTempCalendarListRef = useRef<string[]>([]);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
const isConnectingRef = useRef(false);
|
||||
const pingCleanupRef = useRef<PingCleanup | null>(null);
|
||||
const socketRef = useRef<WebSocketWithCleanup | null>(null)
|
||||
const previousCalendarListRef = useRef<string[]>([])
|
||||
const previousTempCalendarListRef = useRef<string[]>([])
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const reconnectAttemptsRef = useRef(0)
|
||||
const isConnectingRef = useRef(false)
|
||||
const pingCleanupRef = useRef<PingCleanup | null>(null)
|
||||
|
||||
const connectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const didConnectTimeoutRef = useRef(false);
|
||||
const CONNECT_TIMEOUT_MS = 10_000;
|
||||
const connectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const didConnectTimeoutRef = useRef(false)
|
||||
const CONNECT_TIMEOUT_MS = 10_000
|
||||
|
||||
const hadSocketBeforeRef = useRef(false);
|
||||
const justReconnectedRef = useRef(false);
|
||||
const [websocketStatus, setWebSocketStatus] = useState("");
|
||||
const hadSocketBeforeRef = useRef(false)
|
||||
const justReconnectedRef = useRef(false)
|
||||
const [websocketStatus, setWebSocketStatus] = useState('')
|
||||
const [websocketStatusSerity, setWebSocketStatusSerity] = useState<
|
||||
"success" | "info" | "warning" | "error" | undefined
|
||||
>();
|
||||
'success' | 'info' | 'warning' | 'error' | undefined
|
||||
>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const isAuthenticated = useAppSelector((state) =>
|
||||
const dispatch = useAppDispatch()
|
||||
const isAuthenticated = useAppSelector(state =>
|
||||
Boolean(state.user.userData && state.user.tokens)
|
||||
);
|
||||
const isAuthenticatedRef = useRef(isAuthenticated);
|
||||
)
|
||||
const isAuthenticatedRef = useRef(isAuthenticated)
|
||||
|
||||
const [isSocketOpen, setIsSocketOpen] = useState(false);
|
||||
const isPending = useAppSelector((state) => state.calendars.pending);
|
||||
const [shouldConnect, setShouldConnect] = useState(false);
|
||||
const [isSocketOpen, setIsSocketOpen] = useState(false)
|
||||
const isPending = useAppSelector(state => state.calendars.pending)
|
||||
const [shouldConnect, setShouldConnect] = useState(false)
|
||||
|
||||
const calendarList = useSelectedCalendars();
|
||||
const calendarList = useSelectedCalendars()
|
||||
const tempCalendarList = Object.keys(
|
||||
useAppSelector((state) => state?.calendars?.templist) ?? {}
|
||||
);
|
||||
useAppSelector(state => state?.calendars?.templist) ?? {}
|
||||
)
|
||||
|
||||
const calendarsToRefreshRef = useRef<
|
||||
Map<string, { calendar: Calendar; type?: "temp" }>
|
||||
>(new Map());
|
||||
const calendarsToHideRef = useRef<Set<string>>(new Set());
|
||||
const shouldRefreshCalendarListRef = useRef<boolean>(false);
|
||||
Map<string, { calendar: Calendar; type?: 'temp' }>
|
||||
>(new Map())
|
||||
const calendarsToHideRef = useRef<Set<string>>(new Set())
|
||||
const shouldRefreshCalendarListRef = useRef<boolean>(false)
|
||||
const debouncedUpdateFnRef = useRef<
|
||||
((dispatch: AppDispatch) => void) | undefined
|
||||
>();
|
||||
const currentDebouncePeriodRef = useRef<number | undefined>();
|
||||
>()
|
||||
const currentDebouncePeriodRef = useRef<number | undefined>()
|
||||
|
||||
const onMessage = useCallback(
|
||||
(message: unknown) => {
|
||||
@@ -74,133 +74,133 @@ export function WebSocketGate() {
|
||||
calendarsToHide: calendarsToHideRef.current,
|
||||
shouldRefreshCalendarListRef,
|
||||
debouncedUpdateFn: debouncedUpdateFnRef.current,
|
||||
currentDebouncePeriod: currentDebouncePeriodRef.current,
|
||||
};
|
||||
updateCalendars(message, dispatch, accumulators);
|
||||
currentDebouncePeriod: currentDebouncePeriodRef.current
|
||||
}
|
||||
updateCalendars(message, dispatch, accumulators)
|
||||
// Persist any mutations back to refs
|
||||
debouncedUpdateFnRef.current = accumulators.debouncedUpdateFn;
|
||||
currentDebouncePeriodRef.current = accumulators.currentDebouncePeriod;
|
||||
debouncedUpdateFnRef.current = accumulators.debouncedUpdateFn
|
||||
currentDebouncePeriodRef.current = accumulators.currentDebouncePeriod
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
)
|
||||
|
||||
const { scheduleReconnect, clearReconnectTimeout } = useWebSocketReconnect(
|
||||
reconnectTimeoutRef,
|
||||
isAuthenticatedRef,
|
||||
reconnectAttemptsRef,
|
||||
setShouldConnect
|
||||
);
|
||||
)
|
||||
|
||||
const onClose = useCallback(
|
||||
(event: CloseEvent) => {
|
||||
// Socket already cleaned up by internal handler before this callback fires
|
||||
socketRef.current = null;
|
||||
setIsSocketOpen(false);
|
||||
socketRef.current = null
|
||||
setIsSocketOpen(false)
|
||||
|
||||
// Only attempt reconnection if it wasn't a normal closure
|
||||
// Code 1000 = normal closure, 1001 = going away (e.g., page unload)
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(
|
||||
`WebSocket closed unexpectedly (code: ${event.code}, reason: ${event.reason || "none"}). ` +
|
||||
`WebSocket closed unexpectedly (code: ${event.code}, reason: ${event.reason || 'none'}). ` +
|
||||
`Attempting to reconnect...`
|
||||
);
|
||||
setWebSocketStatus(t("websocket.closedUnexpectedly"));
|
||||
setWebSocketStatusSerity("warning");
|
||||
scheduleReconnect();
|
||||
)
|
||||
setWebSocketStatus(t('websocket.closedUnexpectedly'))
|
||||
setWebSocketStatusSerity('warning')
|
||||
scheduleReconnect()
|
||||
} else {
|
||||
reconnectAttemptsRef.current = 0;
|
||||
clearReconnectTimeout();
|
||||
reconnectAttemptsRef.current = 0
|
||||
clearReconnectTimeout()
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[scheduleReconnect, clearReconnectTimeout]
|
||||
);
|
||||
)
|
||||
|
||||
const onError = useCallback((error: Event) => {
|
||||
console.error("WebSocket error:", error);
|
||||
console.error('WebSocket error:', error)
|
||||
const errorMessage =
|
||||
(error as ErrorEvent)?.message ?? error.type ?? "unknown";
|
||||
setWebSocketStatus(t("websocket.error", { error: errorMessage }));
|
||||
setWebSocketStatusSerity("error");
|
||||
(error as ErrorEvent)?.message ?? error.type ?? 'unknown'
|
||||
setWebSocketStatus(t('websocket.error', { error: errorMessage }))
|
||||
setWebSocketStatusSerity('error')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const callBacks = useMemo(
|
||||
() => ({
|
||||
onMessage,
|
||||
onClose,
|
||||
onError,
|
||||
onError
|
||||
}),
|
||||
[onMessage, onClose, onError]
|
||||
);
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
isAuthenticatedRef.current = isAuthenticated;
|
||||
}, [isAuthenticated]);
|
||||
isAuthenticatedRef.current = isAuthenticated
|
||||
}, [isAuthenticated])
|
||||
|
||||
// Reset reconnection state on successful connection and mark for calendar re-sync
|
||||
useEffect(() => {
|
||||
if (isSocketOpen) {
|
||||
if (connectTimeoutRef.current) {
|
||||
clearTimeout(connectTimeoutRef.current);
|
||||
connectTimeoutRef.current = null;
|
||||
clearTimeout(connectTimeoutRef.current)
|
||||
connectTimeoutRef.current = null
|
||||
}
|
||||
|
||||
// Reset timeout marker on successful connection
|
||||
didConnectTimeoutRef.current = false;
|
||||
didConnectTimeoutRef.current = false
|
||||
|
||||
if (hadSocketBeforeRef.current) {
|
||||
justReconnectedRef.current = true;
|
||||
setWebSocketStatus(t("websocket.reconnected"));
|
||||
setWebSocketStatusSerity("success");
|
||||
justReconnectedRef.current = true
|
||||
setWebSocketStatus(t('websocket.reconnected'))
|
||||
setWebSocketStatusSerity('success')
|
||||
}
|
||||
|
||||
hadSocketBeforeRef.current = true;
|
||||
reconnectAttemptsRef.current = 0;
|
||||
hadSocketBeforeRef.current = true
|
||||
reconnectAttemptsRef.current = 0
|
||||
|
||||
clearReconnectTimeout();
|
||||
clearReconnectTimeout()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSocketOpen, clearReconnectTimeout]);
|
||||
}, [isSocketOpen, clearReconnectTimeout])
|
||||
|
||||
// Manage WebSocket connection
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
const abortController = new AbortController()
|
||||
|
||||
const cleanup = () => {
|
||||
if (connectTimeoutRef.current) {
|
||||
clearTimeout(connectTimeoutRef.current);
|
||||
connectTimeoutRef.current = null;
|
||||
clearTimeout(connectTimeoutRef.current)
|
||||
connectTimeoutRef.current = null
|
||||
}
|
||||
closeWebSocketConnection(socketRef, setIsSocketOpen);
|
||||
clearReconnectTimeout();
|
||||
};
|
||||
closeWebSocketConnection(socketRef, setIsSocketOpen)
|
||||
clearReconnectTimeout()
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
cleanup();
|
||||
reconnectAttemptsRef.current = 0;
|
||||
cleanup()
|
||||
reconnectAttemptsRef.current = 0
|
||||
|
||||
hadSocketBeforeRef.current = false;
|
||||
return;
|
||||
hadSocketBeforeRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
const connect = async () => {
|
||||
if (isConnectingRef.current || isSocketOpen) return;
|
||||
isConnectingRef.current = true;
|
||||
setWebSocketConnecting(true);
|
||||
didConnectTimeoutRef.current = false;
|
||||
if (isConnectingRef.current || isSocketOpen) return
|
||||
isConnectingRef.current = true
|
||||
setWebSocketConnecting(true)
|
||||
didConnectTimeoutRef.current = false
|
||||
connectTimeoutRef.current = setTimeout(() => {
|
||||
console.warn("WebSocket connection attempt timed out");
|
||||
console.warn('WebSocket connection attempt timed out')
|
||||
|
||||
didConnectTimeoutRef.current = true;
|
||||
abortController.abort();
|
||||
connectTimeoutRef.current = null;
|
||||
isConnectingRef.current = false;
|
||||
setWebSocketConnecting(false);
|
||||
cleanup();
|
||||
didConnectTimeoutRef.current = true
|
||||
abortController.abort()
|
||||
connectTimeoutRef.current = null
|
||||
isConnectingRef.current = false
|
||||
setWebSocketConnecting(false)
|
||||
cleanup()
|
||||
|
||||
scheduleReconnect();
|
||||
}, CONNECT_TIMEOUT_MS);
|
||||
scheduleReconnect()
|
||||
}, CONNECT_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
await establishWebSocketConnection(
|
||||
@@ -208,50 +208,50 @@ export function WebSocketGate() {
|
||||
socketRef,
|
||||
setIsSocketOpen,
|
||||
abortController.signal
|
||||
);
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn("WebSocket establishment failed:", err);
|
||||
console.warn('WebSocket establishment failed:', err)
|
||||
|
||||
if (connectTimeoutRef.current) {
|
||||
clearTimeout(connectTimeoutRef.current);
|
||||
connectTimeoutRef.current = null;
|
||||
clearTimeout(connectTimeoutRef.current)
|
||||
connectTimeoutRef.current = null
|
||||
}
|
||||
|
||||
// Only schedule reconnect if the timeout handler hasn't already done so
|
||||
if (!didConnectTimeoutRef.current) {
|
||||
scheduleReconnect();
|
||||
scheduleReconnect()
|
||||
}
|
||||
} finally {
|
||||
isConnectingRef.current = false;
|
||||
setWebSocketConnecting(false);
|
||||
isConnectingRef.current = false
|
||||
setWebSocketConnecting(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
cleanup();
|
||||
};
|
||||
abortController.abort()
|
||||
cleanup()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isAuthenticated,
|
||||
callBacks,
|
||||
clearReconnectTimeout,
|
||||
shouldConnect,
|
||||
scheduleReconnect,
|
||||
]);
|
||||
scheduleReconnect
|
||||
])
|
||||
|
||||
// Register using a diff with previous calendars
|
||||
useEffect(() => {
|
||||
if (isPending) return;
|
||||
if (isPending) return
|
||||
|
||||
// If we just reconnected, force a re-sync
|
||||
if (justReconnectedRef.current && isSocketOpen) {
|
||||
console.info("Re-syncing calendars after reconnection");
|
||||
previousCalendarListRef.current = [];
|
||||
previousTempCalendarListRef.current = [];
|
||||
justReconnectedRef.current = false;
|
||||
console.info('Re-syncing calendars after reconnection')
|
||||
previousCalendarListRef.current = []
|
||||
previousTempCalendarListRef.current = []
|
||||
justReconnectedRef.current = false
|
||||
}
|
||||
|
||||
syncCalendarRegistrations(
|
||||
@@ -259,8 +259,8 @@ export function WebSocketGate() {
|
||||
socketRef,
|
||||
calendarList,
|
||||
previousCalendarListRef
|
||||
);
|
||||
}, [isSocketOpen, calendarList, isPending]);
|
||||
)
|
||||
}, [isSocketOpen, calendarList, isPending])
|
||||
|
||||
useEffect(() => {
|
||||
syncCalendarRegistrations(
|
||||
@@ -268,103 +268,103 @@ export function WebSocketGate() {
|
||||
socketRef,
|
||||
tempCalendarList,
|
||||
previousTempCalendarListRef
|
||||
);
|
||||
}, [isSocketOpen, tempCalendarList]);
|
||||
)
|
||||
}, [isSocketOpen, tempCalendarList])
|
||||
|
||||
// Handle browser online/offline events
|
||||
useEffect(() => {
|
||||
const handleOnline = () => {
|
||||
setWebSocketStatus(t("websocket.browserOnline"));
|
||||
setWebSocketStatusSerity("success");
|
||||
setWebSocketStatus(t('websocket.browserOnline'))
|
||||
setWebSocketStatusSerity('success')
|
||||
if (!isSocketOpen && isAuthenticatedRef.current) {
|
||||
reconnectAttemptsRef.current = 0;
|
||||
clearReconnectTimeout();
|
||||
setShouldConnect((prev) => !prev);
|
||||
reconnectAttemptsRef.current = 0
|
||||
clearReconnectTimeout()
|
||||
setShouldConnect(prev => !prev)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleOffline = () => {
|
||||
setWebSocketStatus(t("websocket.browserOffline"));
|
||||
setWebSocketStatusSerity("warning");
|
||||
cleanupConnection();
|
||||
};
|
||||
setWebSocketStatus(t('websocket.browserOffline'))
|
||||
setWebSocketStatusSerity('warning')
|
||||
cleanupConnection()
|
||||
}
|
||||
|
||||
const cleanupConnection = () => {
|
||||
closeWebSocketConnection(socketRef, setIsSocketOpen);
|
||||
clearReconnectTimeout();
|
||||
closeWebSocketConnection(socketRef, setIsSocketOpen)
|
||||
clearReconnectTimeout()
|
||||
if (connectTimeoutRef.current) {
|
||||
clearTimeout(connectTimeoutRef.current);
|
||||
connectTimeoutRef.current = null;
|
||||
clearTimeout(connectTimeoutRef.current)
|
||||
connectTimeoutRef.current = null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("online", handleOnline);
|
||||
window.addEventListener("offline", handleOffline);
|
||||
window.addEventListener('online', handleOnline)
|
||||
window.addEventListener('offline', handleOffline)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", handleOnline);
|
||||
window.removeEventListener("offline", handleOffline);
|
||||
};
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSocketOpen, isAuthenticated, clearReconnectTimeout]);
|
||||
}, [isSocketOpen, isAuthenticated, clearReconnectTimeout])
|
||||
|
||||
useEffect(() => {
|
||||
// Only set up ping if socket is open
|
||||
if (!isSocketOpen || !socketRef.current) {
|
||||
// Clean up existing ping if socket closed
|
||||
if (pingCleanupRef.current) {
|
||||
pingCleanupRef.current.stop();
|
||||
pingCleanupRef.current = null;
|
||||
pingCleanupRef.current.stop()
|
||||
pingCleanupRef.current = null
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Set up ping monitoring
|
||||
const pingCleanup = setupWebSocketPing(socketRef.current, {
|
||||
onConnectionDead: () => {
|
||||
console.warn("WebSocket connection appears dead (no pong received)");
|
||||
setWebSocketStatus(t("websocket.browserOffline"));
|
||||
setWebSocketStatusSerity("warning");
|
||||
console.warn('WebSocket connection appears dead (no pong received)')
|
||||
setWebSocketStatus(t('websocket.browserOffline'))
|
||||
setWebSocketStatusSerity('warning')
|
||||
|
||||
// Trigger reconnection
|
||||
if (socketRef.current) {
|
||||
socketRef.current.close();
|
||||
socketRef.current.close()
|
||||
}
|
||||
},
|
||||
onPingFail: () => {
|
||||
console.warn("Failed to send ping");
|
||||
},
|
||||
});
|
||||
console.warn('Failed to send ping')
|
||||
}
|
||||
})
|
||||
|
||||
pingCleanupRef.current = pingCleanup;
|
||||
pingCleanupRef.current = pingCleanup
|
||||
|
||||
return () => {
|
||||
if (pingCleanupRef.current) {
|
||||
pingCleanupRef.current.stop();
|
||||
pingCleanupRef.current = null;
|
||||
pingCleanupRef.current.stop()
|
||||
pingCleanupRef.current = null
|
||||
}
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSocketOpen]);
|
||||
}, [isSocketOpen])
|
||||
|
||||
const triggerReconnect = useCallback(() => {
|
||||
reconnectAttemptsRef.current = 0;
|
||||
clearReconnectTimeout();
|
||||
setShouldConnect((prev) => !prev);
|
||||
}, [clearReconnectTimeout]);
|
||||
reconnectAttemptsRef.current = 0
|
||||
clearReconnectTimeout()
|
||||
setShouldConnect(prev => !prev)
|
||||
}, [clearReconnectTimeout])
|
||||
|
||||
useEffect(() => {
|
||||
registerWebSocketState(socketRef, triggerReconnect);
|
||||
}, [triggerReconnect]);
|
||||
registerWebSocketState(socketRef, triggerReconnect)
|
||||
}, [triggerReconnect])
|
||||
|
||||
return websocketStatus ? (
|
||||
<WebSocketStatusSnackbar
|
||||
message={websocketStatus}
|
||||
severity={websocketStatusSerity}
|
||||
onClose={() => {
|
||||
setWebSocketStatus("");
|
||||
setWebSocketStatusSerity(undefined);
|
||||
setWebSocketStatus('')
|
||||
setWebSocketStatusSerity(undefined)
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { Alert, Button, Snackbar } from "@linagora/twake-mui";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { Alert, Button, Snackbar } from '@linagora/twake-mui'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
|
||||
export function WebSocketStatusSnackbar({
|
||||
message,
|
||||
severity,
|
||||
onClose,
|
||||
onClose
|
||||
}: {
|
||||
message: string;
|
||||
severity: "success" | "info" | "warning" | "error" | undefined;
|
||||
onClose: () => void;
|
||||
message: string
|
||||
severity: 'success' | 'info' | 'warning' | 'error' | undefined
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
return (
|
||||
<Snackbar
|
||||
open={!!message}
|
||||
onClose={onClose}
|
||||
autoHideDuration={
|
||||
severity === "warning" || severity === "error" ? null : 6000
|
||||
severity === 'warning' || severity === 'error' ? null : 6000
|
||||
}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
severity={severity}
|
||||
onClose={onClose}
|
||||
sx={{ width: "100%" }}
|
||||
sx={{ width: '100%' }}
|
||||
action={
|
||||
<Button color="inherit" size="small" onClick={onClose}>
|
||||
{t("common.ok")}
|
||||
{t('common.ok')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { api } from "@/utils/apiUtils";
|
||||
import { WebSocketTicket } from "./types";
|
||||
import { api } from '@/utils/apiUtils'
|
||||
import { WebSocketTicket } from './types'
|
||||
|
||||
export async function fetchWebSocketTicket(): Promise<WebSocketTicket> {
|
||||
const response = await api.post("ws/ticket");
|
||||
const response = await api.post('ws/ticket')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch WebSocket ticket");
|
||||
throw new Error('Failed to fetch WebSocket ticket')
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return response.json()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface WebSocketTicket {
|
||||
clientAddress: string;
|
||||
value: string;
|
||||
generatedOn: string;
|
||||
validUntil: string;
|
||||
username: string;
|
||||
clientAddress: string
|
||||
value: string
|
||||
generatedOn: string
|
||||
validUntil: string
|
||||
username: string
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fetchWebSocketTicket } from "../api/fetchWebSocketTicket";
|
||||
import { WS_INBOUND_EVENTS } from "../protocols";
|
||||
import { WebSocketCallbacks, WebSocketWithCleanup } from "./types";
|
||||
import { fetchWebSocketTicket } from '../api/fetchWebSocketTicket'
|
||||
import { WS_INBOUND_EVENTS } from '../protocols'
|
||||
import { WebSocketCallbacks, WebSocketWithCleanup } from './types'
|
||||
|
||||
export async function createWebSocketConnection(
|
||||
callbacks: WebSocketCallbacks
|
||||
@@ -9,97 +9,97 @@ export async function createWebSocketConnection(
|
||||
window.WEBSOCKET_URL ??
|
||||
window.CALENDAR_BASE_URL?.replace(
|
||||
/^http(s)?:/,
|
||||
(_: string, s: string | undefined) => (s ? "wss:" : "ws:")
|
||||
(_: string, s: string | undefined) => (s ? 'wss:' : 'ws:')
|
||||
) ??
|
||||
"";
|
||||
''
|
||||
|
||||
if (!wsBaseUrl) {
|
||||
throw new Error("WEBSOCKET_URL is not defined");
|
||||
throw new Error('WEBSOCKET_URL is not defined')
|
||||
}
|
||||
|
||||
const ticket = await fetchWebSocketTicket();
|
||||
const ticket = await fetchWebSocketTicket()
|
||||
|
||||
const socket = new WebSocket(
|
||||
`${wsBaseUrl}/ws?ticket=${encodeURIComponent(ticket.value)}`
|
||||
);
|
||||
)
|
||||
|
||||
const CONNECTION_TIMEOUT_MS = 10_000;
|
||||
const CONNECTION_TIMEOUT_MS = 10_000
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
socket.removeEventListener(
|
||||
WS_INBOUND_EVENTS.CONNECTION_OPENED,
|
||||
openHandler
|
||||
);
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler);
|
||||
socket.close();
|
||||
reject(new Error("WebSocket connection timed out"));
|
||||
}, CONNECTION_TIMEOUT_MS);
|
||||
)
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler)
|
||||
socket.close()
|
||||
reject(new Error('WebSocket connection timed out'))
|
||||
}, CONNECTION_TIMEOUT_MS)
|
||||
|
||||
const openHandler = () => {
|
||||
console.info("WebSocket connection opened");
|
||||
clearTimeout(timeoutId);
|
||||
console.info('WebSocket connection opened')
|
||||
clearTimeout(timeoutId)
|
||||
socket.removeEventListener(
|
||||
WS_INBOUND_EVENTS.CONNECTION_OPENED,
|
||||
openHandler
|
||||
);
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler);
|
||||
resolve();
|
||||
};
|
||||
)
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler)
|
||||
resolve()
|
||||
}
|
||||
|
||||
const errorHandler = (error: Event) => {
|
||||
console.error("WebSocket connection failed:", error);
|
||||
clearTimeout(timeoutId);
|
||||
console.error('WebSocket connection failed:', error)
|
||||
clearTimeout(timeoutId)
|
||||
socket.removeEventListener(
|
||||
WS_INBOUND_EVENTS.CONNECTION_OPENED,
|
||||
openHandler
|
||||
);
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler);
|
||||
reject(new Error("WebSocket connection failed"));
|
||||
};
|
||||
)
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler)
|
||||
reject(new Error('WebSocket connection failed'))
|
||||
}
|
||||
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.CONNECTION_OPENED, openHandler);
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler);
|
||||
});
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.CONNECTION_OPENED, openHandler)
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler)
|
||||
})
|
||||
|
||||
// Store references to event handlers so they can be cleaned up later
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
callbacks.onMessage(message);
|
||||
const message = JSON.parse(event.data)
|
||||
callbacks.onMessage(message)
|
||||
} catch (error) {
|
||||
console.error("Failed to parse WebSocket message:", error);
|
||||
console.error('Failed to parse WebSocket message:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const closeHandler = (event: CloseEvent) => {
|
||||
console.info("WebSocket closed:", event.code, event.reason);
|
||||
cleanup();
|
||||
callbacks.onClose?.(event);
|
||||
};
|
||||
console.info('WebSocket closed:', event.code, event.reason)
|
||||
cleanup()
|
||||
callbacks.onClose?.(event)
|
||||
}
|
||||
|
||||
const errorHandler = (error: Event) => {
|
||||
console.error("WebSocket error:", error);
|
||||
callbacks.onError?.(error);
|
||||
};
|
||||
console.error('WebSocket error:', error)
|
||||
callbacks.onError?.(error)
|
||||
}
|
||||
|
||||
// Cleanup function to remove all event listeners
|
||||
const cleanup = () => {
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.MESSAGE, messageHandler);
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.MESSAGE, messageHandler)
|
||||
socket.removeEventListener(
|
||||
WS_INBOUND_EVENTS.CONNECTION_CLOSED,
|
||||
closeHandler
|
||||
);
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler);
|
||||
};
|
||||
)
|
||||
socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler)
|
||||
}
|
||||
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.MESSAGE, messageHandler);
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeHandler);
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler);
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.MESSAGE, messageHandler)
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeHandler)
|
||||
socket.addEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler)
|
||||
|
||||
// Attach cleanup method to socket
|
||||
const socketWithCleanup = socket as WebSocketWithCleanup;
|
||||
socketWithCleanup.cleanup = cleanup;
|
||||
const socketWithCleanup = socket as WebSocketWithCleanup
|
||||
socketWithCleanup.cleanup = cleanup
|
||||
|
||||
return socketWithCleanup;
|
||||
return socketWithCleanup
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { createWebSocketConnection } from "./createConnection";
|
||||
export type { WebSocketWithCleanup, WebSocketCallbacks } from "./types";
|
||||
export { createWebSocketConnection } from './createConnection'
|
||||
export type { WebSocketWithCleanup, WebSocketCallbacks } from './types'
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
import { type MutableRefObject } from "react";
|
||||
import type { WebSocketWithCleanup } from "../types";
|
||||
import { getWebSocketState } from "../webSocketState";
|
||||
import { type MutableRefObject } from 'react'
|
||||
import type { WebSocketWithCleanup } from '../types'
|
||||
import { getWebSocketState } from '../webSocketState'
|
||||
|
||||
const TIMEOUT_MS = window.WS_PING_TIMEOUT_PERIOD_MS ?? 10_000;
|
||||
let inFlightCheck: Promise<void> | null = null;
|
||||
const TIMEOUT_MS = window.WS_PING_TIMEOUT_PERIOD_MS ?? 10_000
|
||||
let inFlightCheck: Promise<void> | null = null
|
||||
|
||||
function waitForSocketOpen(
|
||||
socketRef: MutableRefObject<WebSocketWithCleanup | null>
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = Date.now() + TIMEOUT_MS;
|
||||
const deadline = Date.now() + TIMEOUT_MS
|
||||
|
||||
const poll = () => {
|
||||
if (socketRef.current?.readyState === WebSocket.OPEN) return resolve();
|
||||
if (socketRef.current?.readyState === WebSocket.OPEN) return resolve()
|
||||
if (Date.now() >= deadline)
|
||||
return reject(new Error("[WS] Timed out waiting for reconnection"));
|
||||
setTimeout(poll, 100);
|
||||
};
|
||||
return reject(new Error('[WS] Timed out waiting for reconnection'))
|
||||
setTimeout(poll, 100)
|
||||
}
|
||||
|
||||
poll();
|
||||
});
|
||||
poll()
|
||||
})
|
||||
}
|
||||
|
||||
export function assertWebSocketAlive(): Promise<void> {
|
||||
if (inFlightCheck) return inFlightCheck;
|
||||
const { socketRef, triggerReconnect, isConnecting } = getWebSocketState();
|
||||
if (inFlightCheck) return inFlightCheck
|
||||
const { socketRef, triggerReconnect, isConnecting } = getWebSocketState()
|
||||
|
||||
// Not registered yet or mid-bootstrap — don't interfere, don't block
|
||||
if (!socketRef || isConnecting || !triggerReconnect) return Promise.resolve();
|
||||
if (!socketRef || isConnecting || !triggerReconnect) return Promise.resolve()
|
||||
|
||||
const socket = socketRef.current;
|
||||
const socket = socketRef.current
|
||||
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
triggerReconnect();
|
||||
return waitForSocketOpen(socketRef);
|
||||
triggerReconnect()
|
||||
return waitForSocketOpen(socketRef)
|
||||
}
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
console.warn("[WS] Pong not received — triggering reconnect");
|
||||
triggerReconnect();
|
||||
waitForSocketOpen(socketRef).then(resolve, reject);
|
||||
}, TIMEOUT_MS);
|
||||
cleanup()
|
||||
console.warn('[WS] Pong not received — triggering reconnect')
|
||||
triggerReconnect()
|
||||
waitForSocketOpen(socketRef).then(resolve, reject).catch(reject)
|
||||
}, TIMEOUT_MS)
|
||||
|
||||
const handlePong = (event: MessageEvent) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg) {
|
||||
cleanup();
|
||||
resolve();
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
} catch {
|
||||
// not parseable, keep waiting
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
socket.removeEventListener("message", handlePong);
|
||||
};
|
||||
clearTimeout(timeout)
|
||||
socket.removeEventListener('message', handlePong)
|
||||
}
|
||||
|
||||
socket.addEventListener("message", handlePong);
|
||||
socket.send(JSON.stringify({ type: "ping" }));
|
||||
socket.addEventListener('message', handlePong)
|
||||
socket.send(JSON.stringify({ type: 'ping' }))
|
||||
}).finally(() => {
|
||||
inFlightCheck = null;
|
||||
});
|
||||
inFlightCheck = promise;
|
||||
return promise;
|
||||
inFlightCheck = null
|
||||
})
|
||||
inFlightCheck = promise
|
||||
return promise
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { WebSocketWithCleanup } from "../types";
|
||||
import { WebSocketWithCleanup } from '../types'
|
||||
|
||||
export function closeWebSocketConnection(
|
||||
socketRef: React.MutableRefObject<WebSocketWithCleanup | null>,
|
||||
setIsSocketOpen: (value: boolean) => void
|
||||
) {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.cleanup();
|
||||
socketRef.current.close();
|
||||
socketRef.current = null;
|
||||
setIsSocketOpen(false);
|
||||
socketRef.current.cleanup()
|
||||
socketRef.current.close()
|
||||
socketRef.current = null
|
||||
setIsSocketOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createWebSocketConnection } from "../createConnection";
|
||||
import { WebSocketCallbacks, WebSocketWithCleanup } from "../types";
|
||||
import { createWebSocketConnection } from '../createConnection'
|
||||
import { WebSocketCallbacks, WebSocketWithCleanup } from '../types'
|
||||
|
||||
export async function establishWebSocketConnection(
|
||||
callbacks: WebSocketCallbacks,
|
||||
@@ -8,21 +8,21 @@ export async function establishWebSocketConnection(
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
try {
|
||||
const socket = await createWebSocketConnection(callbacks);
|
||||
const socket = await createWebSocketConnection(callbacks)
|
||||
|
||||
if (signal?.aborted) {
|
||||
socket.cleanup();
|
||||
socket.close();
|
||||
return;
|
||||
socket.cleanup()
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
|
||||
socketRef.current = socket;
|
||||
socketRef.current = socket
|
||||
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
setIsSocketOpen(true);
|
||||
setIsSocketOpen(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create WebSocket connection:", error);
|
||||
setIsSocketOpen(false);
|
||||
console.error('Failed to create WebSocket connection:', error)
|
||||
setIsSocketOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { WebSocketWithCleanup } from "../types";
|
||||
import { WebSocketWithCleanup } from '../types'
|
||||
|
||||
export interface PingConfig {
|
||||
/** Interval between ping attempts in milliseconds */
|
||||
pingInterval?: number;
|
||||
pingInterval?: number
|
||||
/** Timeout for pong response in milliseconds */
|
||||
pongTimeout?: number;
|
||||
pongTimeout?: number
|
||||
/** Callback when connection is deemed dead */
|
||||
onConnectionDead?: () => void;
|
||||
onConnectionDead?: () => void
|
||||
/** Callback when ping fails */
|
||||
onPingFail?: () => void;
|
||||
onPingFail?: () => void
|
||||
/** Callback when pong is received successfully */
|
||||
onPongReceived?: () => void;
|
||||
onPongReceived?: () => void
|
||||
}
|
||||
|
||||
export interface PingCleanup {
|
||||
stop: () => void;
|
||||
stop: () => void
|
||||
/** Force send a ping immediately */
|
||||
sendPing: () => void;
|
||||
sendPing: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_PING_INTERVAL = window.WS_PING_PERIOD_MS ?? 30000;
|
||||
const DEFAULT_PONG_TIMEOUT = window.WS_PING_TIMEOUT_PERIOD_MS ?? 35000;
|
||||
const DEFAULT_PING_INTERVAL = window.WS_PING_PERIOD_MS ?? 30000
|
||||
const DEFAULT_PONG_TIMEOUT = window.WS_PING_TIMEOUT_PERIOD_MS ?? 35000
|
||||
|
||||
/**
|
||||
* Sets up a ping/pong mechanism to monitor WebSocket connection health
|
||||
@@ -51,130 +51,130 @@ export function setupWebSocketPing(
|
||||
pongTimeout = DEFAULT_PONG_TIMEOUT,
|
||||
onConnectionDead,
|
||||
onPingFail,
|
||||
onPongReceived,
|
||||
} = config;
|
||||
onPongReceived
|
||||
} = config
|
||||
|
||||
let pingIntervalId: NodeJS.Timeout | null = null;
|
||||
let pongTimeoutId: NodeJS.Timeout | null = null;
|
||||
let isWaitingForPong = false;
|
||||
let isStopped = false;
|
||||
let pingIntervalId: NodeJS.Timeout | null = null
|
||||
let pongTimeoutId: NodeJS.Timeout | null = null
|
||||
let isWaitingForPong = false
|
||||
let isStopped = false
|
||||
|
||||
const cleanup = () => {
|
||||
if (pingIntervalId) {
|
||||
clearInterval(pingIntervalId);
|
||||
pingIntervalId = null;
|
||||
clearInterval(pingIntervalId)
|
||||
pingIntervalId = null
|
||||
}
|
||||
if (pongTimeoutId) {
|
||||
clearTimeout(pongTimeoutId);
|
||||
pongTimeoutId = null;
|
||||
clearTimeout(pongTimeoutId)
|
||||
pongTimeoutId = null
|
||||
}
|
||||
isWaitingForPong = false;
|
||||
};
|
||||
isWaitingForPong = false
|
||||
}
|
||||
|
||||
const sendPing = () => {
|
||||
if (isStopped || !socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// If we're still waiting for a previous pong, connection might be dead
|
||||
if (isWaitingForPong) {
|
||||
console.warn(
|
||||
"Pong not received for previous ping. Connection may be dead."
|
||||
);
|
||||
onPingFail?.();
|
||||
onConnectionDead?.();
|
||||
isStopped = true;
|
||||
cleanup();
|
||||
return;
|
||||
'Pong not received for previous ping. Connection may be dead.'
|
||||
)
|
||||
onPingFail?.()
|
||||
onConnectionDead?.()
|
||||
isStopped = true
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
socket.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
||||
socket.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }))
|
||||
|
||||
isWaitingForPong = true;
|
||||
isWaitingForPong = true
|
||||
|
||||
// Set timeout for pong response
|
||||
pongTimeoutId = setTimeout(() => {
|
||||
if (isWaitingForPong) {
|
||||
console.warn("Pong timeout exceeded. Connection may be dead.");
|
||||
onPingFail?.();
|
||||
onConnectionDead?.();
|
||||
cleanup();
|
||||
console.warn('Pong timeout exceeded. Connection may be dead.')
|
||||
onPingFail?.()
|
||||
onConnectionDead?.()
|
||||
cleanup()
|
||||
}
|
||||
}, pongTimeout);
|
||||
}, pongTimeout)
|
||||
} catch (error) {
|
||||
console.error("Failed to send ping:", error);
|
||||
onPingFail?.();
|
||||
cleanup();
|
||||
console.error('Failed to send ping:', error)
|
||||
onPingFail?.()
|
||||
cleanup()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handlePong = () => {
|
||||
if (pongTimeoutId) {
|
||||
clearTimeout(pongTimeoutId);
|
||||
pongTimeoutId = null;
|
||||
clearTimeout(pongTimeoutId)
|
||||
pongTimeoutId = null
|
||||
}
|
||||
isWaitingForPong = false;
|
||||
onPongReceived?.();
|
||||
};
|
||||
isWaitingForPong = false
|
||||
onPongReceived?.()
|
||||
}
|
||||
|
||||
// Start ping interval
|
||||
const startPinging = () => {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
console.warn("Cannot start pinging: socket is not open");
|
||||
return;
|
||||
console.warn('Cannot start pinging: socket is not open')
|
||||
return
|
||||
}
|
||||
|
||||
// Send first ping immediately
|
||||
sendPing();
|
||||
sendPing()
|
||||
|
||||
// Set up recurring pings
|
||||
pingIntervalId = setInterval(() => {
|
||||
sendPing();
|
||||
}, pingInterval);
|
||||
};
|
||||
sendPing()
|
||||
}, pingInterval)
|
||||
}
|
||||
|
||||
// Set up message listener for pong responses
|
||||
const originalOnMessage = socket?.onmessage;
|
||||
const originalOnMessage = socket?.onmessage
|
||||
if (socket) {
|
||||
socket.onmessage = (event) => {
|
||||
let isPong = false;
|
||||
if (typeof event.data === "string") {
|
||||
socket.onmessage = event => {
|
||||
let isPong = false
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const payload = JSON.parse(event.data);
|
||||
const payload = JSON.parse(event.data)
|
||||
// Check if it's an empty object {}
|
||||
isPong =
|
||||
typeof payload === "object" &&
|
||||
typeof payload === 'object' &&
|
||||
payload !== null &&
|
||||
Object.keys(payload).length === 0;
|
||||
Object.keys(payload).length === 0
|
||||
} catch {
|
||||
// Non-JSON payload
|
||||
}
|
||||
}
|
||||
if (isPong) {
|
||||
handlePong();
|
||||
handlePong()
|
||||
}
|
||||
// Call original handler
|
||||
originalOnMessage?.call(socket, event);
|
||||
};
|
||||
originalOnMessage?.call(socket, event)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the ping mechanism
|
||||
startPinging();
|
||||
startPinging()
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
isStopped = true;
|
||||
cleanup();
|
||||
isStopped = true
|
||||
cleanup()
|
||||
// Restore original onmessage handler
|
||||
if (socket) {
|
||||
socket.onmessage = originalOnMessage ?? null;
|
||||
socket.onmessage = originalOnMessage ?? null
|
||||
}
|
||||
},
|
||||
sendPing: () => {
|
||||
if (!isStopped) {
|
||||
sendPing();
|
||||
sendPing()
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getRetryDelay, RetryBackoffConfig } from "@/utils/getRetryDelay";
|
||||
import { Dispatch, MutableRefObject, SetStateAction, useCallback } from "react";
|
||||
import { getRetryDelay, RetryBackoffConfig } from '@/utils/getRetryDelay'
|
||||
import { Dispatch, MutableRefObject, SetStateAction, useCallback } from 'react'
|
||||
|
||||
export const RECONNECT_CONFIG: RetryBackoffConfig = {
|
||||
initialDelay: 1000, // 1 second
|
||||
maxDelay: 30000, // 30 seconds
|
||||
};
|
||||
export const MAX_RECONNECT_ATTEMPTS = 10;
|
||||
maxDelay: 30000 // 30 seconds
|
||||
}
|
||||
export const MAX_RECONNECT_ATTEMPTS = 10
|
||||
|
||||
export function useWebSocketReconnect(
|
||||
reconnectTimeoutRef: MutableRefObject<NodeJS.Timeout | null>,
|
||||
@@ -15,44 +15,44 @@ export function useWebSocketReconnect(
|
||||
) {
|
||||
const clearReconnectTimeout = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const scheduleReconnect = useCallback(() => {
|
||||
if (!isAuthenticatedRef.current) return;
|
||||
if (!isAuthenticatedRef.current) return
|
||||
|
||||
if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) {
|
||||
console.error(
|
||||
`Max WebSocket reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
clearReconnectTimeout();
|
||||
clearReconnectTimeout()
|
||||
|
||||
const delay = getRetryDelay(reconnectAttemptsRef.current, RECONNECT_CONFIG);
|
||||
reconnectAttemptsRef.current += 1;
|
||||
const delay = getRetryDelay(reconnectAttemptsRef.current, RECONNECT_CONFIG)
|
||||
reconnectAttemptsRef.current += 1
|
||||
|
||||
console.info(
|
||||
`Scheduling WebSocket reconnection in ${Math.round(delay)}ms ` +
|
||||
`(attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
|
||||
);
|
||||
)
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
if (!isAuthenticatedRef.current) {
|
||||
reconnectTimeoutRef.current = null;
|
||||
return;
|
||||
reconnectTimeoutRef.current = null
|
||||
return
|
||||
}
|
||||
console.info(
|
||||
`Attempting WebSocket reconnection (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
|
||||
);
|
||||
setShouldConnect((prev) => !prev);
|
||||
clearReconnectTimeout();
|
||||
}, delay);
|
||||
)
|
||||
setShouldConnect(prev => !prev)
|
||||
clearReconnectTimeout()
|
||||
}, delay)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [clearReconnectTimeout]);
|
||||
return { scheduleReconnect, clearReconnectTimeout };
|
||||
}, [clearReconnectTimeout])
|
||||
return { scheduleReconnect, clearReconnectTimeout }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export interface WebSocketWithCleanup extends WebSocket {
|
||||
cleanup: () => void;
|
||||
cleanup: () => void
|
||||
}
|
||||
|
||||
export interface WebSocketCallbacks {
|
||||
onMessage: (data: unknown) => void;
|
||||
onClose?: (event: CloseEvent) => void;
|
||||
onError?: (error: Event) => void;
|
||||
onMessage: (data: unknown) => void
|
||||
onClose?: (event: CloseEvent) => void
|
||||
onError?: (error: Event) => void
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { type MutableRefObject } from "react";
|
||||
import type { WebSocketWithCleanup } from "./types";
|
||||
import { type MutableRefObject } from 'react'
|
||||
import type { WebSocketWithCleanup } from './types'
|
||||
|
||||
interface WebSocketState {
|
||||
socketRef: MutableRefObject<WebSocketWithCleanup | null> | null;
|
||||
triggerReconnect: (() => void) | null;
|
||||
isConnecting: boolean;
|
||||
socketRef: MutableRefObject<WebSocketWithCleanup | null> | null
|
||||
triggerReconnect: (() => void) | null
|
||||
isConnecting: boolean
|
||||
}
|
||||
|
||||
const state: WebSocketState = {
|
||||
socketRef: null,
|
||||
triggerReconnect: null,
|
||||
isConnecting: false,
|
||||
};
|
||||
isConnecting: false
|
||||
}
|
||||
|
||||
export function registerWebSocketState(
|
||||
socketRef: React.MutableRefObject<WebSocketWithCleanup | null>,
|
||||
triggerReconnect: () => void
|
||||
) {
|
||||
state.socketRef = socketRef;
|
||||
state.triggerReconnect = triggerReconnect;
|
||||
state.socketRef = socketRef
|
||||
state.triggerReconnect = triggerReconnect
|
||||
}
|
||||
|
||||
export function setWebSocketConnecting(value: boolean) {
|
||||
state.isConnecting = value;
|
||||
state.isConnecting = value
|
||||
}
|
||||
|
||||
export function getWebSocketState(): Readonly<WebSocketState> {
|
||||
return state;
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { WebSocketGate } from "./WebSocketGate";
|
||||
export type { WebSocketWithCleanup, WebSocketCallbacks } from "./connection";
|
||||
export { WebSocketGate } from './WebSocketGate'
|
||||
export type { WebSocketWithCleanup, WebSocketCallbacks } from './connection'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { parseMessage } from "./parseMessage";
|
||||
export { updateCalendars } from "./updateCalendars";
|
||||
export { parseCalendarPath } from "./parseCalendarPath";
|
||||
export { parseMessage } from './parseMessage'
|
||||
export { updateCalendars } from './updateCalendars'
|
||||
export { parseCalendarPath } from './parseCalendarPath'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const CALENDAR_PATH_REGEX = /^\/calendars\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;
|
||||
const CALENDAR_PATH_REGEX = /^\/calendars\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/
|
||||
|
||||
export function parseCalendarPath(key: string) {
|
||||
if (!CALENDAR_PATH_REGEX.test(key)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const [, , userId, calendarId] = key.split("/");
|
||||
const [, , userId, calendarId] = key.split('/')
|
||||
|
||||
return `${userId}/${calendarId}`;
|
||||
return `${userId}/${calendarId}`
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import { WS_INBOUND_EVENTS } from "../protocols";
|
||||
import { WS_INBOUND_EVENTS } from '../protocols'
|
||||
|
||||
export function parseMessage(message: unknown) {
|
||||
const calendarsToRefresh = new Set<string>();
|
||||
const calendarsToHide = new Set<string>();
|
||||
let shouldRefreshCalendarList = false;
|
||||
if (typeof message !== "object" || message === null) {
|
||||
return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList };
|
||||
const calendarsToRefresh = new Set<string>()
|
||||
const calendarsToHide = new Set<string>()
|
||||
let shouldRefreshCalendarList = false
|
||||
if (typeof message !== 'object' || message === null) {
|
||||
return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList }
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(message)) {
|
||||
switch (key) {
|
||||
case WS_INBOUND_EVENTS.CLIENT_REGISTERED:
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((cal: string) => calendarsToRefresh.add(cal));
|
||||
value.forEach((cal: string) => calendarsToRefresh.add(cal))
|
||||
}
|
||||
break;
|
||||
break
|
||||
case WS_INBOUND_EVENTS.CLIENT_UNREGISTERED:
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((cal: string) => calendarsToHide.add(cal));
|
||||
value.forEach((cal: string) => calendarsToHide.add(cal))
|
||||
}
|
||||
break;
|
||||
break
|
||||
case WS_INBOUND_EVENTS.CALENDAR_CLIENT_REGISTERED:
|
||||
break;
|
||||
break
|
||||
case WS_INBOUND_EVENTS.CALENDAR_LIST:
|
||||
Object.keys(value).forEach((key) => {
|
||||
if (key === "subscribed") {
|
||||
Object.keys(value).forEach(key => {
|
||||
if (key === 'subscribed') {
|
||||
if (Array.isArray(value[key])) {
|
||||
value[key].forEach((cal: string) => calendarsToRefresh.add(cal));
|
||||
value[key].forEach((cal: string) => calendarsToRefresh.add(cal))
|
||||
}
|
||||
} else if (key === "deleted") {
|
||||
shouldRefreshCalendarList = true;
|
||||
} else if (key === 'deleted') {
|
||||
shouldRefreshCalendarList = true
|
||||
} else {
|
||||
shouldRefreshCalendarList = true;
|
||||
shouldRefreshCalendarList = true
|
||||
}
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
default: {
|
||||
calendarsToRefresh.add(key);
|
||||
calendarsToRefresh.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList };
|
||||
return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { AppDispatch } from '@/app/store'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
|
||||
export interface UpdateCalendarsAccumulators {
|
||||
calendarsToRefresh: Map<string, { calendar: Calendar; type?: "temp" }>;
|
||||
calendarsToHide: Set<string>;
|
||||
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>;
|
||||
debouncedUpdateFn?: (dispatch: AppDispatch) => void;
|
||||
currentDebouncePeriod?: number;
|
||||
calendarsToRefresh: Map<string, { calendar: Calendar; type?: 'temp' }>
|
||||
calendarsToHide: Set<string>
|
||||
shouldRefreshCalendarListRef: React.MutableRefObject<boolean>
|
||||
debouncedUpdateFn?: (dispatch: AppDispatch) => void
|
||||
currentDebouncePeriod?: number
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { AppDispatch } from "@/app/store";
|
||||
import { store } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import type { AppDispatch } from '@/app/store'
|
||||
import { store } from '@/app/store'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import {
|
||||
getCalendarsListAsync,
|
||||
refreshCalendarWithSyncToken,
|
||||
} from "@/features/Calendars/services";
|
||||
import { findCalendarById, getDisplayedCalendarRange } from "@/utils";
|
||||
import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars";
|
||||
import { debounce } from "lodash";
|
||||
import { parseCalendarPath } from "./parseCalendarPath";
|
||||
import { parseMessage } from "./parseMessage";
|
||||
import { UpdateCalendarsAccumulators } from "./type/UpdateCalendarsAccumulators";
|
||||
refreshCalendarWithSyncToken
|
||||
} from '@/features/Calendars/services'
|
||||
import { findCalendarById, getDisplayedCalendarRange } from '@/utils'
|
||||
import { setSelectedCalendars } from '@/utils/storage/setSelectedCalendars'
|
||||
import { debounce } from 'lodash'
|
||||
import { parseCalendarPath } from './parseCalendarPath'
|
||||
import { parseMessage } from './parseMessage'
|
||||
import { UpdateCalendarsAccumulators } from './type/UpdateCalendarsAccumulators'
|
||||
|
||||
const DEFAULT_DEBOUNCE_MS = 0;
|
||||
const DEFAULT_DEBOUNCE_MS = 0
|
||||
|
||||
function createDebouncedUpdate(
|
||||
debouncePeriodMs: number,
|
||||
getCalendarsToRefresh: () => Map<
|
||||
string,
|
||||
{ calendar: Calendar; type?: "temp" }
|
||||
{ calendar: Calendar; type?: 'temp' }
|
||||
>,
|
||||
getCalendarsToHide: () => Set<string>,
|
||||
getShouldRefreshCalendarList: () => boolean,
|
||||
@@ -26,32 +26,32 @@ function createDebouncedUpdate(
|
||||
) {
|
||||
return debounce(
|
||||
(dispatch: AppDispatch) => {
|
||||
const currentRange = getDisplayedCalendarRange();
|
||||
const currentRange = getDisplayedCalendarRange()
|
||||
|
||||
// Snapshot state
|
||||
const calendarsToProcess = new Map(getCalendarsToRefresh());
|
||||
const calendarsToHideSnapshot = new Set(getCalendarsToHide());
|
||||
const shouldRefresh = getShouldRefreshCalendarList();
|
||||
const calendarsToProcess = new Map(getCalendarsToRefresh())
|
||||
const calendarsToHideSnapshot = new Set(getCalendarsToHide())
|
||||
const shouldRefresh = getShouldRefreshCalendarList()
|
||||
|
||||
// Clear accumulators
|
||||
getCalendarsToRefresh().clear();
|
||||
getCalendarsToHide().clear();
|
||||
resetShouldRefreshCalendarList();
|
||||
getCalendarsToRefresh().clear()
|
||||
getCalendarsToHide().clear()
|
||||
resetShouldRefreshCalendarList()
|
||||
|
||||
try {
|
||||
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess);
|
||||
processCalendarsToHide(calendarsToHideSnapshot);
|
||||
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess)
|
||||
processCalendarsToHide(calendarsToHideSnapshot)
|
||||
|
||||
if (shouldRefresh) {
|
||||
dispatch(getCalendarsListAsync());
|
||||
dispatch(getCalendarsListAsync())
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error processing accumulated calendar updates:", error);
|
||||
console.warn('Error processing accumulated calendar updates:', error)
|
||||
}
|
||||
},
|
||||
debouncePeriodMs,
|
||||
{ leading: true, trailing: true }
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function updateCalendars(
|
||||
@@ -59,22 +59,22 @@ export function updateCalendars(
|
||||
dispatch: AppDispatch,
|
||||
accumulators: UpdateCalendarsAccumulators
|
||||
) {
|
||||
const state = store.getState();
|
||||
const state = store.getState()
|
||||
const { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList } =
|
||||
parseMessage(message);
|
||||
parseMessage(message)
|
||||
|
||||
// Accumulate
|
||||
accumulateCalendarsToRefresh(
|
||||
state,
|
||||
calendarsToRefresh,
|
||||
accumulators.calendarsToRefresh
|
||||
);
|
||||
accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide);
|
||||
)
|
||||
accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide)
|
||||
|
||||
accumulators.shouldRefreshCalendarListRef.current ||=
|
||||
shouldRefreshCalendarList;
|
||||
shouldRefreshCalendarList
|
||||
|
||||
const debouncePeriod = window.WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS;
|
||||
const debouncePeriod = window.WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS
|
||||
|
||||
if (debouncePeriod > 0) {
|
||||
if (
|
||||
@@ -87,32 +87,32 @@ export function updateCalendars(
|
||||
() => accumulators.calendarsToHide,
|
||||
() => accumulators.shouldRefreshCalendarListRef.current,
|
||||
() => {
|
||||
accumulators.shouldRefreshCalendarListRef.current = false;
|
||||
accumulators.shouldRefreshCalendarListRef.current = false
|
||||
}
|
||||
);
|
||||
accumulators.currentDebouncePeriod = debouncePeriod;
|
||||
)
|
||||
accumulators.currentDebouncePeriod = debouncePeriod
|
||||
}
|
||||
accumulators.debouncedUpdateFn(dispatch);
|
||||
return;
|
||||
accumulators.debouncedUpdateFn(dispatch)
|
||||
return
|
||||
}
|
||||
|
||||
// Immediate processing if debounce disabled
|
||||
const currentRange = getDisplayedCalendarRange();
|
||||
const calendarsToProcess = new Map(accumulators.calendarsToRefresh);
|
||||
const calendarsToHideSnapshot = new Set(accumulators.calendarsToHide);
|
||||
const currentRange = getDisplayedCalendarRange()
|
||||
const calendarsToProcess = new Map(accumulators.calendarsToRefresh)
|
||||
const calendarsToHideSnapshot = new Set(accumulators.calendarsToHide)
|
||||
|
||||
accumulators.calendarsToRefresh.clear();
|
||||
accumulators.calendarsToHide.clear();
|
||||
accumulators.shouldRefreshCalendarListRef.current = false;
|
||||
accumulators.calendarsToRefresh.clear()
|
||||
accumulators.calendarsToHide.clear()
|
||||
accumulators.shouldRefreshCalendarListRef.current = false
|
||||
|
||||
try {
|
||||
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess);
|
||||
processCalendarsToHide(calendarsToHideSnapshot);
|
||||
processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess)
|
||||
processCalendarsToHide(calendarsToHideSnapshot)
|
||||
if (shouldRefreshCalendarList) {
|
||||
dispatch(getCalendarsListAsync());
|
||||
dispatch(getCalendarsListAsync())
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error processing calendar updates:", error);
|
||||
console.warn('Error processing calendar updates:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,67 +120,67 @@ export function updateCalendars(
|
||||
function accumulateCalendarsToRefresh(
|
||||
state: ReturnType<typeof store.getState>,
|
||||
calendarPaths: Set<string>,
|
||||
calendarsToRefreshMap: Map<string, { calendar: Calendar; type?: "temp" }>
|
||||
calendarsToRefreshMap: Map<string, { calendar: Calendar; type?: 'temp' }>
|
||||
) {
|
||||
calendarPaths.forEach((calendarPath) => {
|
||||
const calendarId = parseCalendarPath(calendarPath);
|
||||
calendarPaths.forEach(calendarPath => {
|
||||
const calendarId = parseCalendarPath(calendarPath)
|
||||
if (!calendarId) {
|
||||
console.warn("Invalid calendar path received:", calendarPath);
|
||||
return;
|
||||
console.warn('Invalid calendar path received:', calendarPath)
|
||||
return
|
||||
}
|
||||
const calendar = findCalendarById(state, calendarId);
|
||||
const calendar = findCalendarById(state, calendarId)
|
||||
if (!calendar) {
|
||||
console.warn("Calendar not found for id:", calendarId);
|
||||
return;
|
||||
console.warn('Calendar not found for id:', calendarId)
|
||||
return
|
||||
}
|
||||
calendarsToRefreshMap.set(calendarId, calendar);
|
||||
});
|
||||
calendarsToRefreshMap.set(calendarId, calendar)
|
||||
})
|
||||
}
|
||||
|
||||
function accumulateCalendarsToHide(
|
||||
calendarPaths: Set<string>,
|
||||
calendarsToHideSet: Set<string>
|
||||
) {
|
||||
calendarPaths.forEach((calendarPath) => {
|
||||
const calendarId = parseCalendarPath(calendarPath);
|
||||
calendarPaths.forEach(calendarPath => {
|
||||
const calendarId = parseCalendarPath(calendarPath)
|
||||
if (calendarId) {
|
||||
calendarsToHideSet.add(calendarId);
|
||||
calendarsToHideSet.add(calendarId)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function processCalendarsToRefresh(
|
||||
dispatch: AppDispatch,
|
||||
currentRange: { start: Date; end: Date },
|
||||
calendarsMap: Map<string, { calendar: Calendar; type?: "temp" }>
|
||||
calendarsMap: Map<string, { calendar: Calendar; type?: 'temp' }>
|
||||
) {
|
||||
calendarsMap.forEach((calendar) => {
|
||||
calendarsMap.forEach(calendar => {
|
||||
dispatch(
|
||||
refreshCalendarWithSyncToken({
|
||||
calendar: calendar.calendar,
|
||||
calType: calendar.type,
|
||||
calendarRange: currentRange,
|
||||
calendarRange: currentRange
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function processCalendarsToHide(calendarsToHideSnapshot: Set<string>) {
|
||||
if (calendarsToHideSnapshot.size === 0) return;
|
||||
if (calendarsToHideSnapshot.size === 0) return
|
||||
|
||||
let currentSelectedCalendars: string[];
|
||||
let currentSelectedCalendars: string[]
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem("selectedCalendars") ?? "[]";
|
||||
currentSelectedCalendars = JSON.parse(stored);
|
||||
const stored = localStorage.getItem('selectedCalendars') ?? '[]'
|
||||
currentSelectedCalendars = JSON.parse(stored)
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse selectedCalendars from localStorage:", error);
|
||||
return;
|
||||
console.warn('Failed to parse selectedCalendars from localStorage:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const updatedSelectedCalendars = currentSelectedCalendars.filter(
|
||||
(id) => !calendarsToHideSnapshot.has(id)
|
||||
);
|
||||
id => !calendarsToHideSnapshot.has(id)
|
||||
)
|
||||
|
||||
setSelectedCalendars(updatedSelectedCalendars);
|
||||
setSelectedCalendars(updatedSelectedCalendars)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { registerToCalendars } from "./registerToCalendars";
|
||||
export { unregisterToCalendars } from "./unregisterToCalendars";
|
||||
export { syncCalendarRegistrations } from "./syncCalendarRegistrations";
|
||||
export { registerToCalendars } from './registerToCalendars'
|
||||
export { unregisterToCalendars } from './unregisterToCalendars'
|
||||
export { syncCalendarRegistrations } from './syncCalendarRegistrations'
|
||||
|
||||
@@ -3,14 +3,14 @@ export function registerToCalendars(
|
||||
calendarURIList: string[]
|
||||
): void {
|
||||
if (socket.readyState !== WebSocket.OPEN) {
|
||||
throw new Error("Cannot register: WebSocket is not open");
|
||||
throw new Error('Cannot register: WebSocket is not open')
|
||||
}
|
||||
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
register: calendarURIList,
|
||||
register: calendarURIList
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
console.info("Registered to calendars", calendarURIList);
|
||||
console.info('Registered to calendars', calendarURIList)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebSocketWithCleanup } from "../connection";
|
||||
import { registerToCalendars } from "./registerToCalendars";
|
||||
import { unregisterToCalendars } from "./unregisterToCalendars";
|
||||
import { WebSocketWithCleanup } from '../connection'
|
||||
import { registerToCalendars } from './registerToCalendars'
|
||||
import { unregisterToCalendars } from './unregisterToCalendars'
|
||||
|
||||
export function syncCalendarRegistrations(
|
||||
isSocketOpen: boolean,
|
||||
@@ -13,36 +13,34 @@ export function syncCalendarRegistrations(
|
||||
!socketRef.current ||
|
||||
socketRef.current.readyState !== WebSocket.OPEN
|
||||
) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const currentPaths = calendarList.map((cal) => `/calendars/${cal}`);
|
||||
const currentPaths = calendarList.map(cal => `/calendars/${cal}`)
|
||||
const previousPaths = previousCalendarListRef.current.map(
|
||||
(cal) => `/calendars/${cal}`
|
||||
);
|
||||
cal => `/calendars/${cal}`
|
||||
)
|
||||
|
||||
const toRegister = currentPaths.filter(
|
||||
(path) => !previousPaths.includes(path)
|
||||
);
|
||||
const toRegister = currentPaths.filter(path => !previousPaths.includes(path))
|
||||
const toUnregister = previousPaths.filter(
|
||||
(path) => !currentPaths.includes(path)
|
||||
);
|
||||
path => !currentPaths.includes(path)
|
||||
)
|
||||
|
||||
try {
|
||||
if (toRegister.length > 0) {
|
||||
registerToCalendars(socketRef.current, toRegister);
|
||||
registerToCalendars(socketRef.current, toRegister)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to register calendar:", error);
|
||||
return;
|
||||
console.error('Failed to register calendar:', error)
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (toUnregister.length > 0) {
|
||||
unregisterToCalendars(socketRef.current, toUnregister);
|
||||
unregisterToCalendars(socketRef.current, toUnregister)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to unregister calendar:", error);
|
||||
return;
|
||||
console.error('Failed to unregister calendar:', error)
|
||||
return
|
||||
}
|
||||
previousCalendarListRef.current = calendarList;
|
||||
previousCalendarListRef.current = calendarList
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ export function unregisterToCalendars(
|
||||
calendarURIList: string[]
|
||||
): void {
|
||||
if (socket.readyState !== WebSocket.OPEN) {
|
||||
throw new Error("Cannot unregister: WebSocket is not open");
|
||||
throw new Error('Cannot unregister: WebSocket is not open')
|
||||
}
|
||||
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
unregister: calendarURIList,
|
||||
unregister: calendarURIList
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
console.info("Unregistered to calendars", calendarURIList);
|
||||
console.info('Unregistered to calendars', calendarURIList)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export const WS_INBOUND_EVENTS = {
|
||||
CONNECTION_OPENED: "open",
|
||||
MESSAGE: "message",
|
||||
ERROR: "error",
|
||||
CONNECTION_CLOSED: "close",
|
||||
CLIENT_REGISTERED: "registered",
|
||||
CLIENT_UNREGISTERED: "unregistered",
|
||||
CALENDAR_CLIENT_REGISTERED: "calendarListRegistered",
|
||||
CALENDAR_LIST: "calendarList",
|
||||
} as const;
|
||||
CONNECTION_OPENED: 'open',
|
||||
MESSAGE: 'message',
|
||||
ERROR: 'error',
|
||||
CONNECTION_CLOSED: 'close',
|
||||
CLIENT_REGISTERED: 'registered',
|
||||
CLIENT_UNREGISTERED: 'unregistered',
|
||||
CALENDAR_CLIENT_REGISTERED: 'calendarListRegistered',
|
||||
CALENDAR_LIST: 'calendarList'
|
||||
} as const
|
||||
|
||||
Reference in New Issue
Block a user