Clear chart stale retry state
This commit is contained in:
@@ -519,6 +519,18 @@ export function LiveTemperatureThresholdChart({
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [row?.tz_offset_seconds]);
|
}, [row?.tz_offset_seconds]);
|
||||||
|
|
||||||
|
const applySuccessfulHourlyDetail = useCallback((data: HourlyForecast, options?: { updateLiveTemp?: boolean }) => {
|
||||||
|
if (!data) return;
|
||||||
|
hasLoadedHourlyDetailRef.current = true;
|
||||||
|
if (options?.updateLiveTemp) {
|
||||||
|
const temp = getLiveTempFromHourly(data);
|
||||||
|
if (temp !== null) setLiveTemp(temp);
|
||||||
|
}
|
||||||
|
setHourly(data);
|
||||||
|
setDetailError(null);
|
||||||
|
setShowingStaleDetail(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!city) {
|
if (!city) {
|
||||||
setIsHourlyLoading(false);
|
setIsHourlyLoading(false);
|
||||||
@@ -581,10 +593,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
setDetailError(isEn ? "Data temporarily unavailable." : "数据暂不可用");
|
setDetailError(isEn ? "Data temporarily unavailable." : "数据暂不可用");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hasLoadedHourlyDetailRef.current = true;
|
applySuccessfulHourlyDetail(data);
|
||||||
setHourly(data);
|
|
||||||
setDetailError(null);
|
|
||||||
setShowingStaleDetail(false);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) setDetailError(isEn ? "Data temporarily unavailable." : "数据暂不可用");
|
if (!cancelled) setDetailError(isEn ? "Data temporarily unavailable." : "数据暂不可用");
|
||||||
@@ -610,6 +619,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
detailLoadReady,
|
detailLoadReady,
|
||||||
detailRetryNonce,
|
detailRetryNonce,
|
||||||
isEn,
|
isEn,
|
||||||
|
applySuccessfulHourlyDetail,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -635,8 +645,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled || !data) return;
|
if (cancelled || !data) return;
|
||||||
hasLoadedHourlyDetailRef.current = true;
|
applySuccessfulHourlyDetail(data);
|
||||||
setHourly(data);
|
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -645,7 +654,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [latestPatch, row, city, targetResolution, compact, isActive, isMaximized]);
|
}, [latestPatch, row, city, targetResolution, compact, isActive, isMaximized, applySuccessfulHourlyDetail]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resyncVersion || !city) return;
|
if (!resyncVersion || !city) return;
|
||||||
@@ -653,8 +662,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled || !data) return;
|
if (cancelled || !data) return;
|
||||||
hasLoadedHourlyDetailRef.current = true;
|
applySuccessfulHourlyDetail(data);
|
||||||
setHourly(data);
|
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -663,7 +671,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [resyncVersion, city, targetResolution]);
|
}, [resyncVersion, city, targetResolution, applySuccessfulHourlyDetail]);
|
||||||
|
|
||||||
// ── SSE fallback: only full-fetch if a visible chart has seen no patch for one METAR cadence ──
|
// ── SSE fallback: only full-fetch if a visible chart has seen no patch for one METAR cadence ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -676,10 +684,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled || !data) return;
|
if (cancelled || !data) return;
|
||||||
hasLoadedHourlyDetailRef.current = true;
|
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
|
||||||
const temp = getLiveTempFromHourly(data);
|
|
||||||
if (temp !== null) setLiveTemp(temp);
|
|
||||||
setHourly(data);
|
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -699,7 +704,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
};
|
};
|
||||||
}, [city, compact, isActive, isMaximized, targetResolution]);
|
}, [city, compact, isActive, isMaximized, targetResolution, applySuccessfulHourlyDetail]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldPollLiveChart({ city, compact, isActive, isMaximized })) return;
|
if (!shouldPollLiveChart({ city, compact, isActive, isMaximized })) return;
|
||||||
@@ -723,10 +728,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled || !data) return;
|
if (cancelled || !data) return;
|
||||||
hasLoadedHourlyDetailRef.current = true;
|
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
|
||||||
const temp = getLiveTempFromHourly(data);
|
|
||||||
if (temp !== null) setLiveTemp(temp);
|
|
||||||
setHourly(data);
|
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -743,7 +745,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]);
|
}, [city, compact, isActive, isMaximized, targetResolution, applySuccessfulHourlyDetail]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!city || !currentCityLocalDate) return;
|
if (!city || !currentCityLocalDate) return;
|
||||||
@@ -756,8 +758,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled || !data) return;
|
if (cancelled || !data) return;
|
||||||
hasLoadedHourlyDetailRef.current = true;
|
applySuccessfulHourlyDetail(data);
|
||||||
setHourly(data);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) localDayRolloverFetchDateRef.current = "";
|
if (!cancelled) localDayRolloverFetchDateRef.current = "";
|
||||||
@@ -766,7 +767,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [city, currentCityLocalDate, hourly?.localDate, row?.local_date, targetResolution]);
|
}, [city, currentCityLocalDate, hourly?.localDate, row?.local_date, targetResolution, applySuccessfulHourlyDetail]);
|
||||||
|
|
||||||
const chartHourly = useMemo<HourlyForecast>(() => {
|
const chartHourly = useMemo<HourlyForecast>(() => {
|
||||||
if (!hourly) return hourly;
|
if (!hourly) return hourly;
|
||||||
|
|||||||
@@ -191,6 +191,22 @@ export async function runTests() {
|
|||||||
chartCanvasSourceIncludes(chartSource, "handleRetryDetail"),
|
chartCanvasSourceIncludes(chartSource, "handleRetryDetail"),
|
||||||
"city detail charts should show stale cache first and expose a retryable unavailable state",
|
"city detail charts should show stale cache first and expose a retryable unavailable state",
|
||||||
);
|
);
|
||||||
|
const successfulHourlyDetailBlock =
|
||||||
|
/const applySuccessfulHourlyDetail = useCallback\([\s\S]*?\n \}, \[\]\);/.exec(chartSource)?.[0] || "";
|
||||||
|
assert(
|
||||||
|
successfulHourlyDetailBlock.includes("setDetailError(null)") &&
|
||||||
|
successfulHourlyDetailBlock.includes("setShowingStaleDetail(false)") &&
|
||||||
|
successfulHourlyDetailBlock.includes("setHourly(data)"),
|
||||||
|
"successful city detail refreshes must clear stale-cache retry state when fresh detail arrives",
|
||||||
|
);
|
||||||
|
const rawSuccessfulSetHourlyCalls = chartSource
|
||||||
|
.replace(successfulHourlyDetailBlock, "")
|
||||||
|
.match(/setHourly\(data\);/g) || [];
|
||||||
|
assert(
|
||||||
|
rawSuccessfulSetHourlyCalls.length === 0 &&
|
||||||
|
(chartSource.match(/applySuccessfulHourlyDetail\(data/g) || []).length >= 5,
|
||||||
|
"all successful city detail fetch branches should use the shared success handler",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("const showDetailErrorBadge = !compact || isActive || isMaximized") &&
|
chartSource.includes("const showDetailErrorBadge = !compact || isActive || isMaximized") &&
|
||||||
chartSource.includes("showDetailErrorBadge={showDetailErrorBadge}") &&
|
chartSource.includes("showDetailErrorBadge={showDetailErrorBadge}") &&
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export function runTests() {
|
|||||||
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
||||||
"no-patch fallback refresh should update the chart in the background without showing the loading overlay",
|
"no-patch fallback refresh should update the chart in the background without showing the loading overlay",
|
||||||
);
|
);
|
||||||
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, targetResolution\]\);/)?.[0] || "";
|
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, targetResolution, applySuccessfulHourlyDetail\]\);/)?.[0] || "";
|
||||||
assert(
|
assert(
|
||||||
!resyncBlock.includes("setIsHourlyLoading(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",
|
||||||
@@ -240,7 +240,7 @@ export function runTests() {
|
|||||||
chart.includes("refreshProbabilityOverlayAfterPatch"),
|
chart.includes("refreshProbabilityOverlayAfterPatch"),
|
||||||
"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(/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?\}, \[latestPatch, row, city, targetResolution, compact, isActive, isMaximized\]\);/)?.[0] || "";
|
const patchEffectBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?\}, \[latestPatch, row, city, targetResolution, compact, isActive, isMaximized, applySuccessfulHourlyDetail\]\);/)?.[0] || "";
|
||||||
assert(
|
assert(
|
||||||
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
|
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
|
||||||
patchEffectBlock.includes("ignoreCache: true") &&
|
patchEffectBlock.includes("ignoreCache: true") &&
|
||||||
|
|||||||
Reference in New Issue
Block a user