fix(web): SSE 连接添加指数退避重连策略
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

useAlertSSE hook 和 message store 的 connectSSE 均改为手动重连:
1s→2s→4s→8s→16s→30s(cap),最大重试 10 次,随机 jitter 0.5-1.0x。
替代浏览器原生 EventSource 固定 ~3s 重连,避免服务端压力。
This commit is contained in:
iven
2026-04-30 22:30:47 +08:00
parent 0dcaf7915f
commit 8f9895be98
2 changed files with 124 additions and 47 deletions

View File

@@ -1,6 +1,10 @@
import { create } from 'zustand';
import { getUnreadCount, listMessages, markRead, type MessageInfo } from '../api/messages';
const INITIAL_DELAY_MS = 1000;
const MAX_DELAY_MS = 30_000;
const MAX_RETRIES = 10;
interface MessageState {
unreadCount: number;
recentMessages: MessageInfo[];
@@ -71,32 +75,81 @@ export const useMessageStore = create<MessageState>((set, get) => ({
},
connectSSE: () => {
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api/v1';
const token = localStorage.getItem('access_token');
if (!token) return () => {};
let es: EventSource | null = null;
let retryCount = 0;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let disposed = false;
const url = `${baseUrl}/messages/stream?token=${encodeURIComponent(token)}`;
const es = new EventSource(url);
es.addEventListener('message', () => {
get().fetchUnreadCount();
get().fetchRecentMessages();
});
es.addEventListener('alert', () => {
get().fetchUnreadCount();
});
es.addEventListener('vital_update', () => {
// 体征数据更新事件 — 预留:未来可触发趋势图刷新
});
es.onerror = () => {
// SSE 连接断开时 EventSource 会自动重连
const clearReconnectTimer = () => {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};
const connect = () => {
if (es) {
es.close();
es = null;
}
clearReconnectTimer();
const token = localStorage.getItem('access_token');
if (!token || disposed) return;
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api/v1';
const url = `${baseUrl}/messages/stream?token=${encodeURIComponent(token)}`;
es = new EventSource(url);
es.onopen = () => {
retryCount = 0;
};
es.addEventListener('message', () => {
get().fetchUnreadCount();
get().fetchRecentMessages();
});
es.addEventListener('alert', () => {
get().fetchUnreadCount();
});
es.addEventListener('vital_update', () => {
// 体征数据更新事件 — 预留:未来可触发趋势图刷新
});
es.onerror = () => {
es?.close();
es = null;
retryCount += 1;
if (retryCount > MAX_RETRIES || disposed) return;
const delay = Math.min(
INITIAL_DELAY_MS * Math.pow(2, retryCount - 1),
MAX_DELAY_MS,
);
const jitter = delay * (0.5 + Math.random() * 0.5);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
if (disposed) return;
const tokenNow = localStorage.getItem('access_token');
if (!tokenNow) return;
connect();
}, jitter);
};
};
connect();
return () => {
es.close();
disposed = true;
if (es) {
es.close();
es = null;
}
clearReconnectTimer();
};
},
}));