Files
hms/apps/web/src/stores/message.ts
iven 8f9895be98
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
fix(web): SSE 连接添加指数退避重连策略
useAlertSSE hook 和 message store 的 connectSSE 均改为手动重连:
1s→2s→4s→8s→16s→30s(cap),最大重试 10 次,随机 jitter 0.5-1.0x。
替代浏览器原生 EventSource 固定 ~3s 重连,避免服务端压力。
2026-04-30 22:30:47 +08:00

156 lines
4.0 KiB
TypeScript

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[];
fetchUnreadCount: () => Promise<void>;
fetchRecentMessages: () => Promise<void>;
markAsRead: (id: string) => Promise<void>;
connectSSE: () => () => void;
}
// 请求去重:记录正在进行的请求,防止并发重复调用
let unreadCountPromise: Promise<void> | null = null;
let recentMessagesPromise: Promise<void> | null = null;
export const useMessageStore = create<MessageState>((set, get) => ({
unreadCount: 0,
recentMessages: [],
fetchUnreadCount: async () => {
// 如果已有进行中的请求,复用该 Promise
if (unreadCountPromise) {
await unreadCountPromise;
return;
}
unreadCountPromise = (async () => {
try {
const result = await getUnreadCount();
set({ unreadCount: result.count });
} catch {
// 静默失败,不影响用户体验
} finally {
unreadCountPromise = null;
}
})();
await unreadCountPromise;
},
fetchRecentMessages: async () => {
if (recentMessagesPromise) {
await recentMessagesPromise;
return;
}
recentMessagesPromise = (async () => {
try {
const result = await listMessages({ page: 1, page_size: 5 });
set({ recentMessages: result.data });
} catch {
// 静默失败
} finally {
recentMessagesPromise = null;
}
})();
await recentMessagesPromise;
},
markAsRead: async (id: string) => {
const prev = { unreadCount: get().unreadCount, recentMessages: get().recentMessages };
set((state) => ({
unreadCount: Math.max(0, state.unreadCount - 1),
recentMessages: state.recentMessages.map((m) =>
m.id === id ? { ...m, is_read: true } : m,
),
}));
try {
await markRead(id);
} catch {
set({ unreadCount: prev.unreadCount, recentMessages: prev.recentMessages });
}
},
connectSSE: () => {
let es: EventSource | null = null;
let retryCount = 0;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let disposed = false;
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 () => {
disposed = true;
if (es) {
es.close();
es = null;
}
clearReconnectTimer();
};
},
}));