fix(web): SSE 连接添加指数退避重连策略
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:
@@ -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();
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user