diff --git a/frontend/README.md b/frontend/README.md index 1b8fe348..9b77260d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -192,7 +192,7 @@ Ops: - 支付相关路由:`no-store` - 当 detail 缓存只返回单模型或单日 forecast 时,前端会自动强刷完整 detail,并在补齐前显示同步提示 / 占位卡 - 今日日内分析打开时如果正在切换城市、日期或 detail 深度,弹窗会阻断旧内容点击并显示刷新锁 -- 终端图表订阅 `/api/events?cities=...&since_revision=...&replay_limit=500`,接收 `city_observation_patch.v1`;无 patch 超过 2 分钟时,可见图表才触发 60 秒兜底刷新 +- 终端图表订阅 `/api/events?cities=...&since_revision=...&replay_limit=按可见城市数动态限制`,接收 `city_observation_patch.v1`;无 patch 超过 2 分钟时,可见图表才触发 60 秒兜底刷新 - 前端只消费 HTTP snapshot + SSE patch,不直接感知 Redis;Redis Stream / SQLite event log 都由后端统一封装 ## Vercel 节流建议 diff --git a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts index bc670069..dabf6fc5 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts @@ -94,6 +94,13 @@ export function runTests() { assert(hook.includes("subscribedCities"), "frontend patch hook must track the visible city subscription set"); assert(hook.includes("since_revision"), "frontend patch hook must reconnect with since_revision"); assert(hook.includes("resync_required"), "frontend patch hook must react to server resync_required events"); + assert( + hook.includes("SSE_REPLAY_EVENTS_PER_CITY") && + hook.includes("resolveSseReplayLimit") && + hook.includes('params.set("replay_limit", String(resolveSseReplayLimit(cities.length)))') && + !hook.includes('params.set("replay_limit", "500")'), + "frontend patch hook should size SSE replay_limit by visible city count instead of always asking for 500 events", + ); assert(hook.includes("lastRevision"), "frontend patch hook must track the global last processed revision"); assert(hook.includes("Map<"), "frontend patch hook must keep latest patches in a Map"); assert(hook.includes("useLatestPatch"), "frontend patch hook must export useLatestPatch(city)"); diff --git a/frontend/hooks/use-sse-patches.ts b/frontend/hooks/use-sse-patches.ts index 4fa9946f..948425b5 100644 --- a/frontend/hooks/use-sse-patches.ts +++ b/frontend/hooks/use-sse-patches.ts @@ -5,6 +5,9 @@ import { resolveBackendApiUrl } from "@/lib/backend-api"; const V1_EVENT_TYPE = "city_observation_patch.v1"; const SSE_SUBSCRIPTION_RECONNECT_DELAY_MS = 150; +const SSE_REPLAY_BASE_LIMIT = 60; +const SSE_REPLAY_EVENTS_PER_CITY = 24; +const SSE_REPLAY_MAX_LIMIT = 240; export type CityPatch = { type?: string; @@ -91,12 +94,17 @@ function buildSseUrl(baseUrl: string) { if (lastRevision > 0) { params.set("since_revision", String(lastRevision)); } - params.set("replay_limit", "500"); + params.set("replay_limit", String(resolveSseReplayLimit(cities.length))); const query = params.toString(); return query ? `${baseUrl}?${query}` : baseUrl; } +function resolveSseReplayLimit(cityCount: number) { + const requested = Math.max(1, cityCount) * SSE_REPLAY_EVENTS_PER_CITY; + return Math.max(SSE_REPLAY_BASE_LIMIT, Math.min(SSE_REPLAY_MAX_LIMIT, requested)); +} + function currentConnectionKey() { return `${useFallbackUrl ? "fallback" : "direct"}:${subscribedCityList().join("|")}:${lastRevision}`; }