收敛终端图表详情刷新入口
This commit is contained in:
@@ -554,6 +554,84 @@ function readCachedHourlyForInitialRow(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HourlyDetailFetchOptions = {
|
||||||
|
ignoreCache?: boolean;
|
||||||
|
bypassLocalCache?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HourlyDetailFetchRequest = {
|
||||||
|
source: ChartDetailSource;
|
||||||
|
fetchOptions?: HourlyDetailFetchOptions;
|
||||||
|
applyOptions?: { updateLiveTemp?: boolean };
|
||||||
|
showUserError?: boolean;
|
||||||
|
isCancelled?: () => boolean;
|
||||||
|
onEmpty?: () => void;
|
||||||
|
onError?: () => void;
|
||||||
|
onSettled?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useHourlyDetailFetcher({
|
||||||
|
city,
|
||||||
|
targetResolution,
|
||||||
|
markDetailRequest,
|
||||||
|
markDetailDegraded,
|
||||||
|
applySuccessfulHourlyDetail,
|
||||||
|
}: {
|
||||||
|
city: string;
|
||||||
|
targetResolution: string;
|
||||||
|
markDetailRequest: (source: ChartDetailSource) => void;
|
||||||
|
markDetailDegraded: (options?: { showUserError?: boolean }) => void;
|
||||||
|
applySuccessfulHourlyDetail: (data: HourlyForecast, options?: { updateLiveTemp?: boolean }) => void;
|
||||||
|
}) {
|
||||||
|
return useCallback(
|
||||||
|
async ({
|
||||||
|
source,
|
||||||
|
fetchOptions = {},
|
||||||
|
applyOptions,
|
||||||
|
showUserError = false,
|
||||||
|
isCancelled,
|
||||||
|
onEmpty,
|
||||||
|
onError,
|
||||||
|
onSettled,
|
||||||
|
}: HourlyDetailFetchRequest) => {
|
||||||
|
const cancelled = () => Boolean(isCancelled?.());
|
||||||
|
if (!city || cancelled()) return null;
|
||||||
|
|
||||||
|
markDetailRequest(source);
|
||||||
|
try {
|
||||||
|
const data = await fetchHourlyForecastForCity(city, {
|
||||||
|
...fetchOptions,
|
||||||
|
resolution: targetResolution,
|
||||||
|
});
|
||||||
|
if (cancelled()) return null;
|
||||||
|
if (!data) {
|
||||||
|
if (onEmpty) {
|
||||||
|
onEmpty();
|
||||||
|
} else {
|
||||||
|
markDetailDegraded({ showUserError });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
applySuccessfulHourlyDetail(data, applyOptions);
|
||||||
|
return data;
|
||||||
|
} catch {
|
||||||
|
if (!cancelled()) {
|
||||||
|
if (onError) {
|
||||||
|
onError();
|
||||||
|
} else {
|
||||||
|
markDetailDegraded({ showUserError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (!cancelled()) onSettled?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[city, targetResolution, markDetailRequest, markDetailDegraded, applySuccessfulHourlyDetail],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Main component ─────────────────────────────────────────────────────
|
// ── Main component ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function LiveTemperatureThresholdChart({
|
export function LiveTemperatureThresholdChart({
|
||||||
@@ -763,6 +841,14 @@ export function LiveTemperatureThresholdChart({
|
|||||||
}));
|
}));
|
||||||
}, [city, targetResolution, getLatestRowSnapshot]);
|
}, [city, targetResolution, getLatestRowSnapshot]);
|
||||||
|
|
||||||
|
const runHourlyDetailFetch = useHourlyDetailFetcher({
|
||||||
|
city,
|
||||||
|
targetResolution,
|
||||||
|
markDetailRequest,
|
||||||
|
markDetailDegraded,
|
||||||
|
applySuccessfulHourlyDetail,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!city || !currentRowObservationSignature) return;
|
if (!city || !currentRowObservationSignature) return;
|
||||||
if (lastRowObservationSignatureRef.current === currentRowObservationSignature) return;
|
if (lastRowObservationSignatureRef.current === currentRowObservationSignature) return;
|
||||||
@@ -843,7 +929,6 @@ export function LiveTemperatureThresholdChart({
|
|||||||
setShowingStaleDetail(false);
|
setShowingStaleDetail(false);
|
||||||
}
|
}
|
||||||
setIsHourlyLoading(true);
|
setIsHourlyLoading(true);
|
||||||
markDetailRequest("network");
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let retryScheduled = false;
|
let retryScheduled = false;
|
||||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -853,45 +938,26 @@ export function LiveTemperatureThresholdChart({
|
|||||||
markDetailDegraded();
|
markDetailDegraded();
|
||||||
retryTimer = setTimeout(() => {
|
retryTimer = setTimeout(() => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
markDetailRequest("network");
|
void runHourlyDetailFetch({
|
||||||
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })
|
source: "network",
|
||||||
.then((data) => {
|
fetchOptions: { bypassLocalCache: true },
|
||||||
if (cancelled) return;
|
showUserError: true,
|
||||||
if (!data) {
|
isCancelled: () => cancelled,
|
||||||
markDetailDegraded({ showUserError: true });
|
onSettled: () => setIsHourlyLoading(false),
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
applySuccessfulHourlyDetail(data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
markDetailDegraded({ showUserError: true });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setIsHourlyLoading(false);
|
|
||||||
});
|
|
||||||
}, TRANSIENT_DETAIL_RETRY_DELAY_MS);
|
}, TRANSIENT_DETAIL_RETRY_DELAY_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
fetchHourlyForecastForCity(city, { resolution: targetResolution })
|
void runHourlyDetailFetch({
|
||||||
.then((data) => {
|
source: "network",
|
||||||
if (cancelled) return;
|
showUserError: true,
|
||||||
if (!data) {
|
isCancelled: () => cancelled,
|
||||||
scheduleTransientDetailRetry();
|
onEmpty: scheduleTransientDetailRetry,
|
||||||
return;
|
onSettled: () => {
|
||||||
}
|
if (!retryScheduled) setIsHourlyLoading(false);
|
||||||
applySuccessfulHourlyDetail(data);
|
},
|
||||||
})
|
});
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
markDetailDegraded({ showUserError: true });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled && !retryScheduled) setIsHourlyLoading(false);
|
|
||||||
});
|
|
||||||
}, DETAIL_LOAD_BATCH_DELAY_MS);
|
}, DETAIL_LOAD_BATCH_DELAY_MS);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -911,8 +977,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
detailRetryNonce,
|
detailRetryNonce,
|
||||||
getLatestRowSnapshot,
|
getLatestRowSnapshot,
|
||||||
markDetailDegraded,
|
markDetailDegraded,
|
||||||
markDetailRequest,
|
runHourlyDetailFetch,
|
||||||
applySuccessfulHourlyDetail,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -950,54 +1015,34 @@ export function LiveTemperatureThresholdChart({
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const refreshProbabilityOverlayAfterPatch = () => {
|
const refreshProbabilityOverlayAfterPatch = () => {
|
||||||
markDetailRequest("force_refresh");
|
void runHourlyDetailFetch({
|
||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
source: "force_refresh",
|
||||||
.then((data) => {
|
fetchOptions: { ignoreCache: true },
|
||||||
if (cancelled) return;
|
isCancelled: () => cancelled,
|
||||||
if (!data) {
|
});
|
||||||
markDetailDegraded();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applySuccessfulHourlyDetail(data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
markDetailDegraded();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshProbabilityOverlayAfterPatch();
|
refreshProbabilityOverlayAfterPatch();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [latestPatch, city, targetResolution, compact, isActive, isMaximized, getLatestRowSnapshot, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]);
|
}, [latestPatch, city, targetResolution, compact, isActive, isMaximized, getLatestRowSnapshot, runHourlyDetailFetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resyncVersion || !city) return;
|
if (!resyncVersion || !city) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
markDetailRequest("force_refresh");
|
void runHourlyDetailFetch({
|
||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
source: "force_refresh",
|
||||||
.then((data) => {
|
fetchOptions: { ignoreCache: true },
|
||||||
if (cancelled) return;
|
isCancelled: () => cancelled,
|
||||||
if (!data) {
|
onSettled: () => {
|
||||||
markDetailDegraded();
|
setIsHourlyLoading(false);
|
||||||
return;
|
},
|
||||||
}
|
});
|
||||||
applySuccessfulHourlyDetail(data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
markDetailDegraded();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setIsHourlyLoading(false);
|
|
||||||
});
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [resyncVersion, city, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]);
|
}, [resyncVersion, city, runHourlyDetailFetch]);
|
||||||
|
|
||||||
// ── SSE fallback: visible charts refresh cached detail at observation cadence if patches stop. ──
|
// ── SSE fallback: visible charts refresh cached detail at observation cadence if patches stop. ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1007,25 +1052,14 @@ export function LiveTemperatureThresholdChart({
|
|||||||
const refreshCachedDetail = () => {
|
const refreshCachedDetail = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
lastPatchAtRef.current = now;
|
lastPatchAtRef.current = now;
|
||||||
markDetailRequest("network");
|
|
||||||
|
|
||||||
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })
|
void runHourlyDetailFetch({
|
||||||
.then((data) => {
|
source: "network",
|
||||||
if (cancelled) return;
|
fetchOptions: { bypassLocalCache: true },
|
||||||
if (!data) {
|
applyOptions: { updateLiveTemp: true },
|
||||||
markDetailDegraded();
|
isCancelled: () => cancelled,
|
||||||
return;
|
onSettled: () => setIsHourlyLoading(false),
|
||||||
}
|
});
|
||||||
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
markDetailDegraded();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setIsHourlyLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkFallback = () => {
|
const checkFallback = () => {
|
||||||
@@ -1040,7 +1074,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
};
|
};
|
||||||
}, [city, compact, isActive, isMaximized, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]);
|
}, [city, compact, isActive, isMaximized, targetResolution, runHourlyDetailFetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activationRefreshKey) return;
|
if (!activationRefreshKey) return;
|
||||||
@@ -1055,25 +1089,14 @@ export function LiveTemperatureThresholdChart({
|
|||||||
|
|
||||||
lastForegroundRefreshAtRef.current = now;
|
lastForegroundRefreshAtRef.current = now;
|
||||||
lastPatchAtRef.current = now;
|
lastPatchAtRef.current = now;
|
||||||
markDetailRequest("network");
|
|
||||||
|
|
||||||
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })
|
void runHourlyDetailFetch({
|
||||||
.then((data) => {
|
source: "network",
|
||||||
if (cancelled) return;
|
fetchOptions: { bypassLocalCache: true },
|
||||||
if (!data) {
|
applyOptions: { updateLiveTemp: true },
|
||||||
markDetailDegraded();
|
isCancelled: () => cancelled,
|
||||||
return;
|
onSettled: () => setIsHourlyLoading(false),
|
||||||
}
|
});
|
||||||
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
markDetailDegraded();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setIsHourlyLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshActivatedCachedDetail();
|
refreshActivatedCachedDetail();
|
||||||
@@ -1087,9 +1110,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
isActive,
|
isActive,
|
||||||
isMaximized,
|
isMaximized,
|
||||||
targetResolution,
|
targetResolution,
|
||||||
markDetailDegraded,
|
runHourlyDetailFetch,
|
||||||
markDetailRequest,
|
|
||||||
applySuccessfulHourlyDetail,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1110,22 +1131,13 @@ export function LiveTemperatureThresholdChart({
|
|||||||
|
|
||||||
lastForegroundRefreshAtRef.current = now;
|
lastForegroundRefreshAtRef.current = now;
|
||||||
lastPatchAtRef.current = now;
|
lastPatchAtRef.current = now;
|
||||||
markDetailRequest("network");
|
|
||||||
|
|
||||||
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })
|
void runHourlyDetailFetch({
|
||||||
.then((data) => {
|
source: "network",
|
||||||
if (cancelled) return;
|
fetchOptions: { bypassLocalCache: true },
|
||||||
if (!data) {
|
applyOptions: { updateLiveTemp: true },
|
||||||
markDetailDegraded();
|
isCancelled: () => cancelled,
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
markDetailDegraded();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
@@ -1140,7 +1152,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
window.removeEventListener("focus", refreshForegroundFullDetail);
|
window.removeEventListener("focus", refreshForegroundFullDetail);
|
||||||
};
|
};
|
||||||
}, [city, compact, isActive, isMaximized, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]);
|
}, [city, compact, isActive, isMaximized, targetResolution, runHourlyDetailFetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!city || !currentCityLocalDate) return;
|
if (!city || !currentCityLocalDate) return;
|
||||||
@@ -1150,27 +1162,20 @@ export function LiveTemperatureThresholdChart({
|
|||||||
|
|
||||||
localDayRolloverFetchDateRef.current = currentCityLocalDate;
|
localDayRolloverFetchDateRef.current = currentCityLocalDate;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
markDetailRequest("force_refresh");
|
void runHourlyDetailFetch({
|
||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
source: "force_refresh",
|
||||||
.then((data) => {
|
fetchOptions: { ignoreCache: true },
|
||||||
if (cancelled) return;
|
isCancelled: () => cancelled,
|
||||||
if (!data) {
|
onError: () => {
|
||||||
markDetailDegraded();
|
localDayRolloverFetchDateRef.current = "";
|
||||||
return;
|
markDetailDegraded();
|
||||||
}
|
},
|
||||||
applySuccessfulHourlyDetail(data);
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
localDayRolloverFetchDateRef.current = "";
|
|
||||||
markDetailDegraded();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [city, currentCityLocalDate, hourly?.localDate, row?.local_date, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]);
|
}, [city, currentCityLocalDate, hourly?.localDate, row?.local_date, targetResolution, markDetailDegraded, runHourlyDetailFetch]);
|
||||||
|
|
||||||
const chartHourly = useMemo<HourlyForecast>(() => {
|
const chartHourly = useMemo<HourlyForecast>(() => {
|
||||||
if (!hourly) return hourly;
|
if (!hourly) return hourly;
|
||||||
|
|||||||
@@ -88,11 +88,19 @@ export async function runTests() {
|
|||||||
assert(
|
assert(
|
||||||
chartSource.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation") &&
|
chartSource.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation") &&
|
||||||
chartSource.includes("refreshCachedDetail") &&
|
chartSource.includes("refreshCachedDetail") &&
|
||||||
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") &&
|
chartSource.includes("runHourlyDetailFetch") &&
|
||||||
|
chartSource.includes("fetchOptions: { bypassLocalCache: true }") &&
|
||||||
chartLogicSource.includes("options.bypassLocalCache") &&
|
chartLogicSource.includes("options.bypassLocalCache") &&
|
||||||
chartLogicSource.includes("const forceRefresh = Boolean(options.ignoreCache)"),
|
chartLogicSource.includes("const forceRefresh = Boolean(options.ignoreCache)"),
|
||||||
"visible charts should bypass the five-minute browser detail cache every observation cadence without force-refreshing backend sources",
|
"visible charts should bypass the five-minute browser detail cache every observation cadence without force-refreshing backend sources",
|
||||||
);
|
);
|
||||||
|
const componentHourlyFetchCalls = chartSource.match(/fetchHourlyForecastForCity\(city,/g) || [];
|
||||||
|
assert(
|
||||||
|
chartSource.includes("function useHourlyDetailFetcher") &&
|
||||||
|
componentHourlyFetchCalls.length === 1 &&
|
||||||
|
!/fetchHourlyForecastForCity\(city,[\s\S]*?\)\s*\.then\(/.test(chartSource),
|
||||||
|
"temperature chart should centralize full-detail fetch lifecycle in useHourlyDetailFetcher instead of duplicating then/catch branches across effects",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("preloadTemperatureChartCanvas"),
|
chartSource.includes("preloadTemperatureChartCanvas"),
|
||||||
"terminal chart canvas should expose a preload hook for first-paint optimization",
|
"terminal chart canvas should expose a preload hook for first-paint optimization",
|
||||||
@@ -120,11 +128,12 @@ export async function runTests() {
|
|||||||
assert(
|
assert(
|
||||||
chartSource.includes("activationRefreshKey") &&
|
chartSource.includes("activationRefreshKey") &&
|
||||||
chartSource.includes("refreshActivatedCachedDetail") &&
|
chartSource.includes("refreshActivatedCachedDetail") &&
|
||||||
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })"),
|
chartSource.includes("fetchOptions: { bypassLocalCache: true }") &&
|
||||||
|
chartSource.includes("applyOptions: { updateLiveTemp: true }"),
|
||||||
"switching back to the terminal tab should refresh visible chart detail through cached backend data without forcing external sources",
|
"switching back to the terminal tab should refresh visible chart detail through cached backend data without forcing external sources",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })") &&
|
chartSource.includes("fetchOptions: { ignoreCache: true }") &&
|
||||||
chartSource.includes("const mergedHourly = mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, latestRow)") &&
|
chartSource.includes("const mergedHourly = mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, latestRow)") &&
|
||||||
chartSource.includes("return mergedHourly;"),
|
chartSource.includes("return mergedHourly;"),
|
||||||
"visible chart fallback must refresh full city detail at the current chart resolution while preserving newer live observations",
|
"visible chart fallback must refresh full city detail at the current chart resolution while preserving newer live observations",
|
||||||
@@ -325,7 +334,7 @@ export async function runTests() {
|
|||||||
assert(
|
assert(
|
||||||
chartSource.includes("TRANSIENT_DETAIL_RETRY_DELAY_MS") &&
|
chartSource.includes("TRANSIENT_DETAIL_RETRY_DELAY_MS") &&
|
||||||
chartSource.includes("scheduleTransientDetailRetry") &&
|
chartSource.includes("scheduleTransientDetailRetry") &&
|
||||||
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") &&
|
chartSource.includes("fetchOptions: { bypassLocalCache: true }") &&
|
||||||
chartSource.includes("!retryScheduled"),
|
chartSource.includes("!retryScheduled"),
|
||||||
"cold partial detail-batch misses should stay in loading state and retry cached detail once before showing unavailable",
|
"cold partial detail-batch misses should stay in loading state and retry cached detail once before showing unavailable",
|
||||||
);
|
);
|
||||||
@@ -351,7 +360,7 @@ export async function runTests() {
|
|||||||
"live row and SSE patch merges must persist their hourly snapshots so returning to terminal restores runway history immediately",
|
"live row and SSE patch merges must persist their hourly snapshots so returning to terminal restores runway history immediately",
|
||||||
);
|
);
|
||||||
const coldDetailFetchBlock =
|
const coldDetailFetchBlock =
|
||||||
/useEffect\(\(\) => \{\s*if \(!city\) \{[\s\S]*?fetchHourlyForecastForCity\(city, \{ resolution: targetResolution \}\)[\s\S]*?\n \}, \[([\s\S]*?)\]\);/.exec(chartSource)?.[1] || "";
|
/useEffect\(\(\) => \{\s*if \(!city\) \{[\s\S]*?scheduleTransientDetailRetry[\s\S]*?runHourlyDetailFetch\(\{[\s\S]*?\n \}, \[([\s\S]*?)\]\);/.exec(chartSource)?.[1] || "";
|
||||||
assert(
|
assert(
|
||||||
coldDetailFetchBlock.length > 0 &&
|
coldDetailFetchBlock.length > 0 &&
|
||||||
!/^\s*row\s*,?\s*$/m.test(coldDetailFetchBlock),
|
!/^\s*row\s*,?\s*$/m.test(coldDetailFetchBlock),
|
||||||
@@ -362,8 +371,8 @@ export async function runTests() {
|
|||||||
.match(/setHourly\(data\);/g) || [];
|
.match(/setHourly\(data\);/g) || [];
|
||||||
assert(
|
assert(
|
||||||
rawSuccessfulSetHourlyCalls.length === 0 &&
|
rawSuccessfulSetHourlyCalls.length === 0 &&
|
||||||
(chartSource.match(/applySuccessfulHourlyDetail\(data/g) || []).length >= 5,
|
(chartSource.match(/applySuccessfulHourlyDetail\(data/g) || []).length === 1,
|
||||||
"all successful city detail fetch branches should use the shared success handler",
|
"all successful city detail fetch branches should flow through the shared fetcher and success handler once",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("const showDetailErrorBadge = !compact || isActive || isMaximized") &&
|
chartSource.includes("const showDetailErrorBadge = !compact || isActive || isMaximized") &&
|
||||||
|
|||||||
@@ -231,14 +231,17 @@ export function runTests() {
|
|||||||
);
|
);
|
||||||
const fallbackRefreshBlock = chart.match(/const refreshCachedDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
|
const fallbackRefreshBlock = chart.match(/const refreshCachedDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
|
||||||
assert(
|
assert(
|
||||||
fallbackRefreshBlock.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") &&
|
fallbackRefreshBlock.includes("runHourlyDetailFetch") &&
|
||||||
|
fallbackRefreshBlock.includes("fetchOptions: { bypassLocalCache: true }") &&
|
||||||
!fallbackRefreshBlock.includes("ignoreCache: true") &&
|
!fallbackRefreshBlock.includes("ignoreCache: true") &&
|
||||||
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
||||||
"no-patch fallback refresh should revalidate through cached backend detail without force-refreshing sources or showing the loading overlay",
|
"no-patch fallback refresh should revalidate through cached backend detail without force-refreshing sources or showing the loading overlay",
|
||||||
);
|
);
|
||||||
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, targetResolution, applySuccessfulHourlyDetail\]\);/)?.[0] || "";
|
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, runHourlyDetailFetch\]\);/)?.[0] || "";
|
||||||
assert(
|
assert(
|
||||||
!resyncBlock.includes("setIsHourlyLoading(true)"),
|
resyncBlock.includes("runHourlyDetailFetch") &&
|
||||||
|
resyncBlock.includes("fetchOptions: { ignoreCache: true }") &&
|
||||||
|
!resyncBlock.includes("setIsHourlyLoading(true)"),
|
||||||
"SSE replay resync should refresh full detail in the background without showing the loading overlay",
|
"SSE replay resync should refresh full detail in the background without showing the loading overlay",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -250,7 +253,7 @@ export function runTests() {
|
|||||||
const foregroundRefreshBlock = chart.match(/const refreshForegroundFullDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
|
const foregroundRefreshBlock = chart.match(/const refreshForegroundFullDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
|
||||||
assert(
|
assert(
|
||||||
foregroundRefreshBlock.includes("bypassLocalCache: true") &&
|
foregroundRefreshBlock.includes("bypassLocalCache: true") &&
|
||||||
foregroundRefreshBlock.includes("fetchHourlyForecastForCity") &&
|
foregroundRefreshBlock.includes("runHourlyDetailFetch") &&
|
||||||
foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") &&
|
foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") &&
|
||||||
!foregroundRefreshBlock.includes("ignoreCache: true") &&
|
!foregroundRefreshBlock.includes("ignoreCache: true") &&
|
||||||
!foregroundRefreshBlock.includes("setIsHourlyLoading(true)"),
|
!foregroundRefreshBlock.includes("setIsHourlyLoading(true)"),
|
||||||
@@ -291,11 +294,11 @@ export function runTests() {
|
|||||||
"temperature chart must trigger a throttled background probability refresh after live observation patches",
|
"temperature chart must trigger a throttled background probability refresh after live observation patches",
|
||||||
);
|
);
|
||||||
const patchEffectBlock = chart.match(
|
const patchEffectBlock = chart.match(
|
||||||
/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?refreshProbabilityOverlayAfterPatch\(\);[\s\S]*?\}, \[[^\]]*latestPatch[^\]]*applySuccessfulHourlyDetail[^\]]*\]\);/,
|
/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?refreshProbabilityOverlayAfterPatch\(\);[\s\S]*?\}, \[[^\]]*latestPatch[^\]]*runHourlyDetailFetch[^\]]*\]\);/,
|
||||||
)?.[0] || "";
|
)?.[0] || "";
|
||||||
assert(
|
assert(
|
||||||
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
|
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
|
||||||
patchEffectBlock.includes("ignoreCache: true") &&
|
patchEffectBlock.includes("fetchOptions: { ignoreCache: true }") &&
|
||||||
!patchEffectBlock.includes("setIsHourlyLoading(true)"),
|
!patchEffectBlock.includes("setIsHourlyLoading(true)"),
|
||||||
"live patch probability refresh must recompute legacy Gaussian in the background without showing a loading overlay",
|
"live patch probability refresh must recompute legacy Gaussian in the background without showing a loading overlay",
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user