chore: directory migration - gateway→server, web→client/electron
This commit is contained in:
109
client/electron/src/api/sse.ts
Normal file
109
client/electron/src/api/sse.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { API_BASE, withToken } from './config';
|
||||
|
||||
export type SSEEventHandler = (eventType: string, data: Record<string, unknown>) => void;
|
||||
export type SSEStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
|
||||
|
||||
const EVENTS = [
|
||||
'new_mail',
|
||||
'permission_decision',
|
||||
'session_update',
|
||||
'session_archived',
|
||||
'agent_online'
|
||||
] as const;
|
||||
|
||||
let es: EventSource | null = null;
|
||||
let handlers: SSEEventHandler[] = [];
|
||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let backoff = 1000;
|
||||
let _status: SSEStatus = 'disconnected';
|
||||
let statusHandlers: Array<(s: SSEStatus) => void> = [];
|
||||
|
||||
/** 监听 SSE 连接状态变化 */
|
||||
export function onSSEStatus(handler: (s: SSEStatus) => void): () => void {
|
||||
statusHandlers.push(handler);
|
||||
return () => {
|
||||
statusHandlers = statusHandlers.filter(h => h !== handler);
|
||||
};
|
||||
}
|
||||
|
||||
/** 当前 SSE 连接状态 */
|
||||
export function getSSEStatus(): SSEStatus {
|
||||
return _status;
|
||||
}
|
||||
|
||||
function setStatus(s: SSEStatus) {
|
||||
if (_status === s) return;
|
||||
_status = s;
|
||||
statusHandlers.forEach(h => h(s));
|
||||
}
|
||||
|
||||
export function connectSSE(onEvent: SSEEventHandler): () => void {
|
||||
handlers.push(onEvent);
|
||||
if (!es) open();
|
||||
|
||||
return () => {
|
||||
handlers = handlers.filter(h => h !== onEvent);
|
||||
if (handlers.length === 0) close();
|
||||
};
|
||||
}
|
||||
|
||||
function open() {
|
||||
close(false);
|
||||
setStatus('connecting');
|
||||
// EventSource 无法设置请求头:Cookie 模式靠同源 Cookie,
|
||||
// 密钥模式只能把令牌放进 query(服务端仅此端点与附件下载接受 ?access_token=)。
|
||||
es = new EventSource(withToken(`${API_BASE}/events/stream`), { withCredentials: true });
|
||||
|
||||
// EventSource 会自动重连,但它的 readyState 在网络断开时
|
||||
// 不一定及时反映状态。用 onopen 判断实际连上了。
|
||||
es.onopen = () => {
|
||||
backoff = 1000;
|
||||
setStatus('connected');
|
||||
};
|
||||
|
||||
es.addEventListener('connected', () => {
|
||||
backoff = 1000;
|
||||
setStatus('connected');
|
||||
});
|
||||
|
||||
for (const name of EVENTS) {
|
||||
es.addEventListener(name, (e: MessageEvent) => {
|
||||
let data: Record<string, unknown> = {};
|
||||
try {
|
||||
data = JSON.parse(e.data);
|
||||
} catch {
|
||||
/* 忽略非 JSON 负载 */
|
||||
}
|
||||
handlers.forEach(h => h(name, data));
|
||||
});
|
||||
}
|
||||
|
||||
es.onerror = () => {
|
||||
close(false);
|
||||
if (handlers.length === 0) return;
|
||||
if (retryTimer) return;
|
||||
setStatus('reconnecting');
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = null;
|
||||
backoff = Math.min(backoff * 2, 15000);
|
||||
open();
|
||||
}, backoff);
|
||||
};
|
||||
}
|
||||
|
||||
function close(clearHandlers = true) {
|
||||
if (retryTimer) {
|
||||
clearTimeout(retryTimer);
|
||||
retryTimer = null;
|
||||
}
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
}
|
||||
if (clearHandlers) handlers = [];
|
||||
setStatus('disconnected');
|
||||
}
|
||||
|
||||
export function disconnectSSE() {
|
||||
close();
|
||||
}
|
||||
Reference in New Issue
Block a user