/** * SSE 订阅层 —— apps/web 唯一一条服务端推送连接(编码规范 §9.2)。 * * 「SSE 事件到达 → **按信封目标**失效对应 TanStack Query 缓存,由订阅层统一做; * 组件禁止自行轮询或手写缓存。」 * * 推送不带业务载荷:它只说「哪个目标动了」,画面照旧由服务端投影重建 —— 前端零过程状态。 * 断线由 `EventSource` 自行重连;重连后整体失效一次,补上断线期间错过的变更。 */ import type { QueryClient } from '@tanstack/react-query'; type Touch = { kind: 'task' | 'workflow' | 'staff' | 'system'; id: string; reason: string }; export function connectStream(queryClient: QueryClient): () => void { const source = new EventSource('/api/stream'); source.addEventListener('touch', (event) => { const touch = JSON.parse((event as MessageEvent).data) as Touch; switch (touch.kind) { case 'task': // 工作台三处 + 治理台任务面都以任务为轴,一起失效 void queryClient.invalidateQueries({ queryKey: ['task', touch.id] }); void queryClient.invalidateQueries({ queryKey: ['tasks'] }); void queryClient.invalidateQueries({ queryKey: ['gov', 'tasks'] }); void queryClient.invalidateQueries({ queryKey: ['gov', 'tasklog', touch.id] }); break; case 'workflow': void queryClient.invalidateQueries({ queryKey: ['gov', 'workflow', touch.id] }); void queryClient.invalidateQueries({ queryKey: ['gov', 'tasks'] }); void queryClient.invalidateQueries({ queryKey: ['gov', 'approvals'] }); break; case 'staff': void queryClient.invalidateQueries({ queryKey: ['messages'] }); void queryClient.invalidateQueries({ queryKey: ['gov', 'staff'] }); break; case 'system': void queryClient.invalidateQueries({ queryKey: ['gov'] }); break; } }); // 重连即补课:断线期间的推送收不到,回来先整体失效一次 source.addEventListener('open', () => { void queryClient.invalidateQueries(); }); return () => source.close(); }