Optimize terminal batching and cache guards

This commit is contained in:
2569718930@qq.com
2026-05-31 18:28:50 +08:00
parent a4253c224a
commit 668f4d9bd3
10 changed files with 473 additions and 79 deletions
@@ -77,6 +77,8 @@ const TrainingDashboard = dynamic(
},
);
const ONLINE_USERS_REFRESH_MS = 5 * 60_000;
function createEmptyAccess(loading = true): ProAccessState {
return {
loading,
@@ -425,14 +427,22 @@ function PolyWeatherTerminal({
useEffect(() => {
const fetchOnline = () => {
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
fetch("/api/ops/online-users", { headers: { Accept: "application/json" } })
.then((r) => (r.ok ? r.json() : null))
.then((d) => { if (d?.online != null) setOnlineCount(d.online); })
.catch(() => {});
};
fetchOnline();
const id = setInterval(fetchOnline, 60_000);
return () => clearInterval(id);
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") fetchOnline();
};
document.addEventListener("visibilitychange", handleVisibilityChange);
const id = setInterval(fetchOnline, ONLINE_USERS_REFRESH_MS);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
clearInterval(id);
};
}, []);
const [gridCols, setGridCols] = useState<number>(() => {
@@ -101,6 +101,10 @@ function formatCityLocalDate(tzOffsetSeconds: number | null | undefined) {
return `${y}-${m}-${d}`;
}
function getLiveTempFromHourly(data: HourlyForecast) {
return validNumber(data?.airportCurrent?.temp) ?? validNumber(data?.airportPrimary?.temp) ?? null;
}
function getWundergroundDailyHigh(hourly: HourlyForecast) {
return validNumber(hourly?.wundergroundCurrent?.max_so_far) ?? null;
}
@@ -365,6 +369,8 @@ export function LiveTemperatureThresholdChart({
.then((data) => {
if (cancelled || !data) return;
hasLoadedHourlyDetailRef.current = true;
const temp = getLiveTempFromHourly(data);
if (temp !== null) setLiveTemp(temp);
setHourly(data);
})
.catch(() => {})
@@ -377,15 +383,6 @@ export function LiveTemperatureThresholdChart({
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
if (Date.now() - lastPatchAtRef.current < 2 * 60_000) return;
fetch(`/api/city/${encodeURIComponent(city)}/summary`)
.then((res) => (res.ok ? res.json() : null))
.then((payload) => {
if (cancelled || !payload) return;
const temp = validNumber(payload?.current?.temp);
if (temp !== null) setLiveTemp(temp);
})
.catch(() => {});
refreshFullDetail();
};
@@ -403,19 +400,12 @@ export function LiveTemperatureThresholdChart({
const refreshForegroundFullDetail = () => {
lastPatchAtRef.current = Date.now();
fetch(`/api/city/${encodeURIComponent(city)}/summary`)
.then((res) => (res.ok ? res.json() : null))
.then((payload) => {
if (cancelled || !payload) return;
const temp = validNumber(payload?.current?.temp);
if (temp !== null) setLiveTemp(temp);
})
.catch(() => {});
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
.then((data) => {
if (cancelled || !data) return;
hasLoadedHourlyDetailRef.current = true;
const temp = getLiveTempFromHourly(data);
if (temp !== null) setLiveTemp(temp);
setHourly(data);
})
.catch(() => {});
@@ -49,6 +49,10 @@ export async function runTests() {
path.join(projectRoot, "components", "dashboard", "scan-terminal", "temperature-chart-logic.ts"),
"utf8",
);
const dashboardSource = fs.readFileSync(
path.join(projectRoot, "components", "dashboard", "ScanTerminalDashboard.tsx"),
"utf8",
);
assert(
querySource.includes("useSsePatchVersion") &&
@@ -91,10 +95,16 @@ export async function runTests() {
chartLogicSource.includes("primeCityDetailCache"),
"visible terminal chart detail fetches should be coalesced into one batch request and prime the shared chart cache",
);
const fetchHourlyBlock = chartLogicSource.match(/async function fetchHourlyForecastForCity[\s\S]*?\n}\n\nfunction fetchCityDetailWithTimeout/)?.[0] || "";
assert(
chartLogicSource.includes("options.ignoreCache\n ? runQueuedHourlyDetailRequest") &&
chartLogicSource.includes(": queueCityDetailBatch(city, resParam)"),
"normal first-paint city detail requests should enter the batch queue before single-request concurrency limiting",
fetchHourlyBlock.includes("queueCityDetailBatch(city, resParam)") &&
!fetchHourlyBlock.includes("runQueuedHourlyDetailRequest"),
"first-paint and background city detail refreshes should both enter the batch queue before falling back to single requests",
);
assert(
dashboardSource.includes("ONLINE_USERS_REFRESH_MS = 5 * 60_000") &&
dashboardSource.includes('document.visibilityState === "hidden"'),
"terminal online-user presence should refresh slowly and pause while the browser tab is hidden",
);
assert(
chartSource.includes("IntersectionObserver") &&
@@ -182,6 +182,10 @@ export function runTests() {
!foregroundRefreshBlock.includes("setIsHourlyLoading(true)"),
"foreground resume refresh should update full detail immediately in the background without showing the loading overlay",
);
assert(
!chart.includes("/api/city/${encodeURIComponent(city)}/summary"),
"visible chart fallback and foreground refresh should not issue a separate summary request after requesting full detail",
);
assert(chart.includes("viewMode"), "temperature chart must expose a view mode for DEB-peak auto view versus full-day view");
assert(chart.includes('useState<"auto" | "full">("full")'), "temperature chart must default every city panel to the all-day view");
assert(
@@ -1098,7 +1098,10 @@ async function flushCityDetailBatch(resolution: string) {
return;
}
try {
resolveBatchWaiters(waiters, await fetchSingleHourlyForecastForCity(city, resolution));
resolveBatchWaiters(
waiters,
await runQueuedHourlyDetailRequest(() => fetchSingleHourlyForecastForCity(city, resolution)),
);
} catch (error) {
rejectBatchWaiters(waiters, error);
}
@@ -1109,7 +1112,10 @@ async function flushCityDetailBatch(resolution: string) {
cities.map(async (city) => {
const waiters = queue.waiters.get(city);
try {
resolveBatchWaiters(waiters, await fetchSingleHourlyForecastForCity(city, resolution));
resolveBatchWaiters(
waiters,
await runQueuedHourlyDetailRequest(() => fetchSingleHourlyForecastForCity(city, resolution)),
);
} catch (fallbackError) {
rejectBatchWaiters(waiters, fallbackError || error);
}
@@ -1158,11 +1164,7 @@ async function fetchHourlyForecastForCity(
const pending = _hourlyRequestCache.get(requestKey);
if (pending) return pending;
const request = (
options.ignoreCache
? runQueuedHourlyDetailRequest(() => fetchSingleHourlyForecastForCity(city, resParam))
: queueCityDetailBatch(city, resParam)
)
const request = queueCityDetailBatch(city, resParam)
.finally(() => {
_hourlyRequestCache.delete(requestKey);
});