Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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;
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user