/** * 事件流(SSE,用 fetch 读,API2):事件只是「有变化」的提示,以资源的 GET 结果为准。 * 进度事件直接写进解析任务和项目列表的缓存,让进度条即时动;其余事件让相关查询失效、重新请求。 * 断线由 api-client 自动重连;页面上的 5 秒轮询兜底。 */ import { subscribeEvents, type BdEvent } from "@bid-doctor/api-client"; import type { QueryClient } from "@tanstack/vue-query"; import { apiOptions } from "./api"; import { qk } from "./keys"; import { TERMINAL_JOB, type CheckRun, type ParseJob, type ProjectListItem } from "./queries"; type ProjectPage = { items: ProjectListItem[]; next_cursor: string | null }; function onEvent(qc: QueryClient, e: BdEvent) { const job = e.job; const jobId = job?.id; if ((e.type === "job.progress" || e.type === "job.finished") && job && jobId && job.kind === "parse_job") { // 事件可能比 GET 结果旧(重连补发、乱序):已结束的任务不再被进度事件改回「进行中」,进度只增不减 qc.setQueryData(qk.parseJob(jobId), (old) => !old || TERMINAL_JOB.has(old.status) || (job.progress ?? 0) < old.progress ? old : { ...old, status: job.status ?? old.status, stage: (job.stage as ParseJob["stage"]) ?? old.stage, progress: job.progress ?? old.progress, }, ); qc.setQueryData(qk.projects, (old) => old && { ...old, items: old.items.map((p) => p.active_job && p.active_job.id === jobId && (job.progress ?? 0) >= (p.active_job.progress ?? 0) ? { ...p, active_job: { ...p.active_job, progress: job.progress ?? p.active_job.progress } } : p, ), }, ); if (e.type === "job.finished") { // 结束后要拿完整结果(error、result),并刷新项目、额度、清单 void qc.invalidateQueries({ queryKey: qk.parseJob(jobId) }); void qc.invalidateQueries({ queryKey: qk.notifications }); void qc.invalidateQueries({ queryKey: qk.projects }); void qc.invalidateQueries({ queryKey: qk.quota }); if (e.project_id) { void qc.invalidateQueries({ queryKey: qk.project(e.project_id) }); void qc.invalidateQueries({ queryKey: qk.checklist(e.project_id) }); void qc.invalidateQueries({ queryKey: qk.checklistItems(e.project_id) }); } } return; } if ((e.type === "job.progress" || e.type === "job.finished") && job && jobId && job.kind === "check_run") { qc.setQueryData(qk.checkRun(jobId), (old) => !old || TERMINAL_JOB.has(old.status) || (job.progress ?? 0) < old.progress ? old : { ...old, status: job.status ?? old.status, stage: (job.stage as CheckRun["stage"]) ?? old.stage, progress: job.progress ?? old.progress }, ); if (e.type === "job.finished") { void qc.invalidateQueries({ queryKey: qk.checkRun(jobId) }); void qc.invalidateQueries({ queryKey: qk.notifications }); void qc.invalidateQueries({ queryKey: qk.findings(jobId) }); void qc.invalidateQueries({ queryKey: qk.projects }); void qc.invalidateQueries({ queryKey: qk.quota }); if (e.project_id) { void qc.invalidateQueries({ queryKey: qk.project(e.project_id) }); void qc.invalidateQueries({ queryKey: qk.checkPreview(e.project_id) }); void qc.invalidateQueries({ queryKey: qk.checkRuns(e.project_id) }); } } return; } switch (e.type) { case "quota.changed": void qc.invalidateQueries({ queryKey: qk.quota }); void qc.invalidateQueries({ queryKey: qk.notifications }); break; case "project.changed": void qc.invalidateQueries({ queryKey: qk.projects }); if (e.project_id) void qc.invalidateQueries({ queryKey: qk.project(e.project_id) }); break; case "resync": void qc.invalidateQueries(); break; default: // notification.created、report.ready:骨架阶段还没有对应界面 break; } } /** 登录后的页面框架挂载时开始订阅,返回取消函数。 */ export function startEventStream(qc: QueryClient): () => void { return subscribeEvents(apiOptions, (e) => onEvent(qc, e)); }