[#552] added check for ws status before write REST calls (#569)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-02-23 11:21:54 +01:00
committed by GitHub
parent f40165d2e2
commit 5e9e52c8dd
4 changed files with 130 additions and 4 deletions
+8
View File
@@ -1,12 +1,16 @@
import { Auth } from "@/features/User/oidcAuth";
import { assertWebSocketAlive } from "@/websocket/connection/lifecycle/assertWebSocketAlive";
import ky from "ky";
import { getRetryDelay } from "./getRetryDelay";
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
const RETRY_CONFIG = {
maxRetries: 10,
initialDelay: 1000,
maxDelay: 120000,
};
export const api = ky.extend({
prefixUrl: window.CALENDAR_BASE_URL,
retry: {
@@ -28,6 +32,10 @@ export const api = ky.extend({
if (access_token) {
request.headers.set("Authorization", `Bearer ${access_token}`);
}
if (MUTATING_METHODS.has(request.method)) {
await assertWebSocketAlive();
}
return request;
},
],
+21 -4
View File
@@ -7,14 +7,18 @@ import { useI18n } from "twake-i18n";
import type { WebSocketWithCleanup } from "./connection";
import { closeWebSocketConnection } from "./connection/lifecycle/closeWebSocketConnection";
import { establishWebSocketConnection } from "./connection/lifecycle/establishWebSocketConnection";
import { useWebSocketReconnect } from "./connection/lifecycle/useWebSocketReconnect";
import { updateCalendars } from "./messaging/updateCalendars";
import { syncCalendarRegistrations } from "./operations";
import { WebSocketStatusSnackbar } from "./WebSocketStatusSnackbar";
import {
setupWebSocketPing,
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";
export function WebSocketGate() {
const socketRef = useRef<WebSocketWithCleanup | null>(null);
@@ -183,6 +187,7 @@ export function WebSocketGate() {
const connect = async () => {
if (isConnectingRef.current || isSocketOpen) return;
isConnectingRef.current = true;
setWebSocketConnecting(true);
didConnectTimeoutRef.current = false;
connectTimeoutRef.current = setTimeout(() => {
console.warn("WebSocket connection attempt timed out");
@@ -191,6 +196,7 @@ export function WebSocketGate() {
abortController.abort();
connectTimeoutRef.current = null;
isConnectingRef.current = false;
setWebSocketConnecting(false);
cleanup();
scheduleReconnect();
@@ -217,6 +223,7 @@ export function WebSocketGate() {
}
} finally {
isConnectingRef.current = false;
setWebSocketConnecting(false);
}
};
@@ -340,6 +347,16 @@ export function WebSocketGate() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSocketOpen]);
const triggerReconnect = useCallback(() => {
reconnectAttemptsRef.current = 0;
clearReconnectTimeout();
setShouldConnect((prev) => !prev);
}, [clearReconnectTimeout]);
useEffect(() => {
registerWebSocketState(socketRef, triggerReconnect);
}, [triggerReconnect]);
return websocketStatus ? (
<WebSocketStatusSnackbar
message={websocketStatus}
@@ -0,0 +1,71 @@
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;
function waitForSocketOpen(
socketRef: MutableRefObject<WebSocketWithCleanup | null>
): Promise<void> {
return new Promise((resolve, reject) => {
const deadline = Date.now() + TIMEOUT_MS;
const poll = () => {
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);
};
poll();
});
}
export function assertWebSocketAlive(): Promise<void> {
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();
const socket = socketRef.current;
if (!socket || socket.readyState !== WebSocket.OPEN) {
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);
const handlePong = (event: MessageEvent) => {
try {
const msg = JSON.parse(event.data);
if (msg) {
cleanup();
resolve();
}
} catch {
// not parseable, keep waiting
}
};
const cleanup = () => {
clearTimeout(timeout);
socket.removeEventListener("message", handlePong);
};
socket.addEventListener("message", handlePong);
socket.send(JSON.stringify({ type: "ping" }));
}).finally(() => {
inFlightCheck = null;
});
inFlightCheck = promise;
return promise;
}
@@ -0,0 +1,30 @@
import { type MutableRefObject } from "react";
import type { WebSocketWithCleanup } from "./types";
interface WebSocketState {
socketRef: MutableRefObject<WebSocketWithCleanup | null> | null;
triggerReconnect: (() => void) | null;
isConnecting: boolean;
}
const state: WebSocketState = {
socketRef: null,
triggerReconnect: null,
isConnecting: false,
};
export function registerWebSocketState(
socketRef: React.MutableRefObject<WebSocketWithCleanup | null>,
triggerReconnect: () => void
) {
state.socketRef = socketRef;
state.triggerReconnect = triggerReconnect;
}
export function setWebSocketConnecting(value: boolean) {
state.isConnecting = value;
}
export function getWebSocketState(): Readonly<WebSocketState> {
return state;
}