Avoid terminal full-detail fallback overload

This commit is contained in:
2569718930@qq.com
2026-06-01 12:06:46 +08:00
parent fee402145e
commit fb6c354317
10 changed files with 269 additions and 89 deletions
@@ -142,15 +142,15 @@ export async function runTests() {
);
assert(
flushCityDetailBatchBlock.includes("if (!payload)") &&
flushCityDetailBatchBlock.includes("resolveCityDetailBatchWithSingleFallback") &&
flushCityDetailBatchBlock.includes("resolveBatchWaiters(waiters, null)"),
"single-city detail fallback should be reserved for whole-batch failures rather than successful batch misses",
flushCityDetailBatchBlock.includes("resolveAllBatchWaitersAsNull") &&
!flushCityDetailBatchBlock.includes("resolveCityDetailBatchWithSingleFallback"),
"whole-batch failures should stop at the chart batch layer instead of fanning out into single-city full-detail requests",
);
const fetchHourlyBlock = chartLogicSource.match(/async function fetchHourlyForecastForCity[\s\S]*?\r?\n}\r?\n\r?\nfunction fetchCityDetailWithTimeout/)?.[0] || "";
const fetchHourlyBlock = chartLogicSource.match(/async function fetchHourlyForecastForCity[\s\S]*?\r?\n}\r?\n\r?\nfunction shouldPollLiveChart/)?.[0] || "";
assert(
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",
"first-paint and background city detail refreshes should both enter the batch queue without falling back to single requests",
);
assert(
dashboardSource.includes("ONLINE_USERS_REFRESH_MS = 5 * 60_000") &&
@@ -259,7 +259,7 @@ export function runTests() {
);
assert(
chartLogic.includes("HOURLY_DETAIL_REQUEST_TIMEOUT_MS = 12_000") &&
chartLogic.includes("fetchCityDetailWithTimeout") &&
chartLogic.includes("fetchCityDetailBatchWithTimeout") &&
chartLogic.includes("signal: controller.signal") &&
chartLogic.includes("controller.abort()"),
"city detail chart fetches must have a frontend timeout so panels cannot stay on 加载图表 forever",
@@ -1033,16 +1033,6 @@ function primeCityDetailCache(
return data;
}
async function fetchSingleHourlyForecastForCity(
city: string,
resolution: string,
): Promise<HourlyForecast> {
const res = await fetchCityDetailWithTimeout(city, resolution);
if (!res || !res.ok) return null;
const json = await res.json() as CityDetail;
return primeCityDetailCache(city, resolution, json);
}
function queueCityDetailBatch(city: string, resolution: string): Promise<HourlyForecast> {
return new Promise<HourlyForecast>((resolve, reject) => {
const queue = _cityDetailBatchQueues.get(resolution) || {
@@ -1073,13 +1063,6 @@ function resolveBatchWaiters(
(waiters || []).forEach((waiter) => waiter.resolve(value));
}
function rejectBatchWaiters(
waiters: CityDetailBatchWaiter[] | undefined,
reason: unknown,
) {
(waiters || []).forEach((waiter) => waiter.reject(reason));
}
function resolveCityDetailFromBatch(
details: Record<string, CityDetail | null | undefined> | undefined,
city: string,
@@ -1119,7 +1102,7 @@ async function flushCityDetailBatch(resolution: string) {
try {
const payload = await fetchCityDetailBatchWithTimeout(cities, resolution);
if (!payload) {
await resolveCityDetailBatchWithSingleFallback(cities, queue, resolution);
resolveAllBatchWaitersAsNull(cities, queue);
return;
}
@@ -1145,29 +1128,17 @@ async function flushCityDetailBatch(resolution: string) {
}),
);
} catch (error) {
await resolveCityDetailBatchWithSingleFallback(cities, queue, resolution, error);
resolveAllBatchWaitersAsNull(cities, queue);
}
}
async function resolveCityDetailBatchWithSingleFallback(
function resolveAllBatchWaitersAsNull(
cities: string[],
queue: CityDetailBatchQueue,
resolution: string,
reason?: unknown,
) {
await Promise.all(
cities.map(async (city) => {
const waiters = queue.waiters.get(city);
try {
resolveBatchWaiters(
waiters,
await runQueuedHourlyDetailRequest(() => fetchSingleHourlyForecastForCity(city, resolution)),
);
} catch (fallbackError) {
rejectBatchWaiters(waiters, fallbackError || reason);
}
}),
);
cities.forEach((city) => {
resolveBatchWaiters(queue.waiters.get(city), null);
});
}
function fetchCityDetailBatchWithTimeout(cities: string[], resolution: string) {
@@ -1227,17 +1198,6 @@ async function fetchHourlyForecastForCity(
return request;
}
function fetchCityDetailWithTimeout(city: string, resolution: string) {
const controller = new AbortController();
const timeoutId = globalThis.setTimeout(() => controller.abort(), HOURLY_DETAIL_REQUEST_TIMEOUT_MS);
return fetch(`/api/city/${encodeURIComponent(city)}/detail?depth=full&force_refresh=false&resolution=${resolution}`, {
headers: { Accept: "application/json" },
signal: controller.signal,
})
.catch(() => null)
.finally(() => globalThis.clearTimeout(timeoutId));
}
function shouldPollLiveChart({
city,
compact,