Debounce terminal SSE subscription reconnects

This commit is contained in:
2569718930@qq.com
2026-05-31 20:15:39 +08:00
parent 78ea0326a5
commit ada5f274d3
2 changed files with 37 additions and 2 deletions
@@ -99,6 +99,18 @@ export function runTests() {
assert(hook.includes("useLatestPatch"), "frontend patch hook must export useLatestPatch(city)");
assert(hook.includes("revision"), "frontend patch hook must track revisions and skip stale patches");
assert(hook.includes("setTimeout"), "frontend patch hook must implement explicit reconnect backoff");
assert(
hook.includes("SSE_SUBSCRIPTION_RECONNECT_DELAY_MS") &&
hook.includes("scheduleSubscriptionReconnect") &&
hook.includes("clearSubscriptionReconnectTimer"),
"frontend patch hook must debounce visible-city subscription changes before reopening SSE",
);
const subscriptionBlock = hook.match(/function registerCitySubscription[\s\S]*?\n}\r?\n\r?\nfunction normalizeLegacyPatch/)?.[0] || "";
assert(
subscriptionBlock.includes("scheduleSubscriptionReconnect()") &&
!subscriptionBlock.includes("ensureSsePatchConnection();"),
"city subscription mount/unmount should schedule one coalesced SSE reconnect instead of reconnecting per chart",
);
const bffEventsRoute = readFrontendFile("app", "api", "events", "route.ts");
assert(bffEventsRoute.includes("searchParams"), "Next.js SSE proxy must forward query parameters to FastAPI");
+25 -2
View File
@@ -4,6 +4,7 @@ import { useEffect, useSyncExternalStore } from "react";
import { resolveBackendApiUrl } from "@/lib/backend-api";
const V1_EVENT_TYPE = "city_observation_patch.v1";
const SSE_SUBSCRIPTION_RECONNECT_DELAY_MS = 150;
export type CityPatch = {
type?: string;
@@ -38,6 +39,7 @@ const subscribedCities = new Map<string, number>();
let eventSource: EventSource | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let subscriptionReconnectTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectAttempt = 0;
let patchVersion = 0;
let resyncVersion = 0;
@@ -74,6 +76,12 @@ function clearReconnectTimer() {
reconnectTimer = null;
}
function clearSubscriptionReconnectTimer() {
if (!subscriptionReconnectTimer) return;
clearTimeout(subscriptionReconnectTimer);
subscriptionReconnectTimer = null;
}
function buildSseUrl(baseUrl: string) {
const params = new URLSearchParams();
const cities = subscribedCityList();
@@ -113,6 +121,7 @@ function closeEventSource() {
function reconnectNow() {
if (typeof window === "undefined") return;
clearReconnectTimer();
clearSubscriptionReconnectTimer();
closeEventSource();
connectSsePatches();
}
@@ -159,6 +168,7 @@ function connectSsePatches() {
function ensureSsePatchConnection() {
if (subscribedCities.size === 0) {
clearSubscriptionReconnectTimer();
closeEventSource();
clearReconnectTimer();
return;
@@ -167,6 +177,19 @@ function ensureSsePatchConnection() {
reconnectNow();
}
function scheduleSubscriptionReconnect() {
if (typeof window === "undefined") return;
if (subscribedCities.size === 0) {
ensureSsePatchConnection();
return;
}
clearSubscriptionReconnectTimer();
subscriptionReconnectTimer = setTimeout(() => {
subscriptionReconnectTimer = null;
ensureSsePatchConnection();
}, SSE_SUBSCRIPTION_RECONNECT_DELAY_MS);
}
function registerCitySubscription(city: string) {
const cityKey = normalizeCityKey(city);
if (!cityKey) return () => {};
@@ -174,7 +197,7 @@ function registerCitySubscription(city: string) {
const previousCount = subscribedCities.get(cityKey) ?? 0;
subscribedCities.set(cityKey, previousCount + 1);
if (previousCount === 0) {
ensureSsePatchConnection();
scheduleSubscriptionReconnect();
}
return () => {
@@ -184,7 +207,7 @@ function registerCitySubscription(city: string) {
} else {
subscribedCities.set(cityKey, nextCount);
}
ensureSsePatchConnection();
scheduleSubscriptionReconnect();
};
}