Avoid terminal full-detail fallback overload
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user