Stabilize chart detail refresh against live rows
This commit is contained in:
@@ -599,6 +599,9 @@ export function LiveTemperatureThresholdChart({
|
|||||||
const [chartFreshness, setChartFreshness] = useState<ChartFreshnessState>(() =>
|
const [chartFreshness, setChartFreshness] = useState<ChartFreshnessState>(() =>
|
||||||
createChartFreshnessState(),
|
createChartFreshnessState(),
|
||||||
);
|
);
|
||||||
|
const latestRowRef = useRef<ScanOpportunityRow | null>(row);
|
||||||
|
latestRowRef.current = row;
|
||||||
|
const getLatestRowSnapshot = useCallback(() => latestRowRef.current, []);
|
||||||
const hasLoadedHourlyDetailRef = useRef(false);
|
const hasLoadedHourlyDetailRef = useRef(false);
|
||||||
const hourlyCityRef = useRef<string>("");
|
const hourlyCityRef = useRef<string>("");
|
||||||
const chartVisibilityRef = useRef<HTMLDivElement | null>(null);
|
const chartVisibilityRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -732,14 +735,15 @@ export function LiveTemperatureThresholdChart({
|
|||||||
const applySuccessfulHourlyDetail = useCallback((data: HourlyForecast, options?: { updateLiveTemp?: boolean }) => {
|
const applySuccessfulHourlyDetail = useCallback((data: HourlyForecast, options?: { updateLiveTemp?: boolean }) => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const loadedAtMs = Date.now();
|
const loadedAtMs = Date.now();
|
||||||
const rowSeed = seedHourlyForecastFromRow(row);
|
const latestRow = getLatestRowSnapshot();
|
||||||
const dataWithCurrentRow = mergeHourlyWithLiveObservations(data, rowSeed, row);
|
const rowSeed = seedHourlyForecastFromRow(latestRow);
|
||||||
|
const dataWithCurrentRow = mergeHourlyWithLiveObservations(data, rowSeed, latestRow);
|
||||||
hasLoadedHourlyDetailRef.current = true;
|
hasLoadedHourlyDetailRef.current = true;
|
||||||
if (options?.updateLiveTemp) {
|
if (options?.updateLiveTemp) {
|
||||||
const temp = getLiveTempFromHourly(dataWithCurrentRow);
|
const temp = getLiveTempFromHourly(dataWithCurrentRow);
|
||||||
if (temp !== null) setLiveTemp(temp);
|
if (temp !== null) setLiveTemp(temp);
|
||||||
}
|
}
|
||||||
setHourly((prev) => mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, row));
|
setHourly((prev) => mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, latestRow));
|
||||||
setDetailError(null);
|
setDetailError(null);
|
||||||
setShowingStaleDetail(false);
|
setShowingStaleDetail(false);
|
||||||
setChartFreshness((prev) => ({
|
setChartFreshness((prev) => ({
|
||||||
@@ -752,7 +756,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
? "network"
|
? "network"
|
||||||
: prev.detailSource,
|
: prev.detailSource,
|
||||||
}));
|
}));
|
||||||
}, [row]);
|
}, [getLatestRowSnapshot]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!city || !currentRowObservationSignature) return;
|
if (!city || !currentRowObservationSignature) return;
|
||||||
@@ -826,7 +830,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!cached && !hasLoadedHourlyDetailRef.current) {
|
if (!cached && !hasLoadedHourlyDetailRef.current) {
|
||||||
setHourly(seedHourlyForecastFromRow(row));
|
setHourly(seedHourlyForecastFromRow(getLatestRowSnapshot()));
|
||||||
setShowingStaleDetail(false);
|
setShowingStaleDetail(false);
|
||||||
}
|
}
|
||||||
setIsHourlyLoading(true);
|
setIsHourlyLoading(true);
|
||||||
@@ -888,7 +892,6 @@ export function LiveTemperatureThresholdChart({
|
|||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
city,
|
city,
|
||||||
row,
|
|
||||||
targetResolution,
|
targetResolution,
|
||||||
isChartVisible,
|
isChartVisible,
|
||||||
compact,
|
compact,
|
||||||
@@ -897,6 +900,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
slotIndex,
|
slotIndex,
|
||||||
detailLoadReady,
|
detailLoadReady,
|
||||||
detailRetryNonce,
|
detailRetryNonce,
|
||||||
|
getLatestRowSnapshot,
|
||||||
markDetailDegraded,
|
markDetailDegraded,
|
||||||
markDetailRequest,
|
markDetailRequest,
|
||||||
applySuccessfulHourlyDetail,
|
applySuccessfulHourlyDetail,
|
||||||
@@ -909,7 +913,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
lastPatchAtRef.current = patchAppliedAtMs;
|
lastPatchAtRef.current = patchAppliedAtMs;
|
||||||
const tempValue = validNumber(latestPatch.changes.temp);
|
const tempValue = validNumber(latestPatch.changes.temp);
|
||||||
if (tempValue !== null) setLiveTemp(tempValue);
|
if (tempValue !== null) setLiveTemp(tempValue);
|
||||||
setHourly((prev) => mergePatchIntoHourly(prev ?? seedHourlyForecastFromRow(row), latestPatch));
|
setHourly((prev) => mergePatchIntoHourly(prev ?? seedHourlyForecastFromRow(getLatestRowSnapshot()), latestPatch));
|
||||||
setChartFreshness((prev) => ({
|
setChartFreshness((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
ssePatchAppliedAtMs: patchAppliedAtMs,
|
ssePatchAppliedAtMs: patchAppliedAtMs,
|
||||||
@@ -954,7 +958,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [latestPatch, row, city, targetResolution, compact, isActive, isMaximized, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]);
|
}, [latestPatch, city, targetResolution, compact, isActive, isMaximized, getLatestRowSnapshot, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resyncVersion || !city) return;
|
if (!resyncVersion || !city) return;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export async function runTests() {
|
|||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })") &&
|
chartSource.includes("fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })") &&
|
||||||
chartSource.includes("setHourly((prev) => mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, row))"),
|
chartSource.includes("setHourly((prev) => mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, latestRow))"),
|
||||||
"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",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -244,7 +244,7 @@ export async function runTests() {
|
|||||||
assert(
|
assert(
|
||||||
chartLogicSource.includes("_hourlyRequestCache") &&
|
chartLogicSource.includes("_hourlyRequestCache") &&
|
||||||
chartLogicSource.includes("seedHourlyForecastFromRow") &&
|
chartLogicSource.includes("seedHourlyForecastFromRow") &&
|
||||||
chartSource.includes("setHourly(seedHourlyForecastFromRow(row))"),
|
chartSource.includes("setHourly(seedHourlyForecastFromRow(getLatestRowSnapshot()))"),
|
||||||
"terminal charts should render from row data immediately and dedupe concurrent city detail requests",
|
"terminal charts should render from row data immediately and dedupe concurrent city detail requests",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -328,13 +328,26 @@ export async function runTests() {
|
|||||||
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",
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
chartSource.includes("latestRowRef") &&
|
||||||
|
chartSource.includes("latestRowRef.current = row"),
|
||||||
|
"temperature chart should keep the latest row in a ref so live row changes do not restart detail fetches",
|
||||||
|
);
|
||||||
const successfulHourlyDetailBlock =
|
const successfulHourlyDetailBlock =
|
||||||
/const applySuccessfulHourlyDetail = useCallback\([\s\S]*?\n \}, \[row\]\);/.exec(chartSource)?.[0] || "";
|
/const applySuccessfulHourlyDetail = useCallback\([\s\S]*?\n \}, \[[^\]]*\]\);/.exec(chartSource)?.[0] || "";
|
||||||
assert(
|
assert(
|
||||||
successfulHourlyDetailBlock.includes("setDetailError(null)") &&
|
successfulHourlyDetailBlock.includes("setDetailError(null)") &&
|
||||||
successfulHourlyDetailBlock.includes("setShowingStaleDetail(false)") &&
|
successfulHourlyDetailBlock.includes("setShowingStaleDetail(false)") &&
|
||||||
successfulHourlyDetailBlock.includes("mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, row)"),
|
successfulHourlyDetailBlock.includes("getLatestRowSnapshot") &&
|
||||||
"successful city detail refreshes must clear stale-cache retry state when fresh detail arrives",
|
!/\[row\]/.test(successfulHourlyDetailBlock),
|
||||||
|
"successful city detail refreshes must read the latest row without depending on the row object",
|
||||||
|
);
|
||||||
|
const coldDetailFetchBlock =
|
||||||
|
/useEffect\(\(\) => \{\s*if \(!city\) \{[\s\S]*?fetchHourlyForecastForCity\(city, \{ resolution: targetResolution \}\)[\s\S]*?\n \}, \[([\s\S]*?)\]\);/.exec(chartSource)?.[1] || "";
|
||||||
|
assert(
|
||||||
|
coldDetailFetchBlock.length > 0 &&
|
||||||
|
!/^\s*row\s*,?\s*$/m.test(coldDetailFetchBlock),
|
||||||
|
"cold detail fetch effect must not restart just because the live scan row object changed",
|
||||||
);
|
);
|
||||||
const rawSuccessfulSetHourlyCalls = chartSource
|
const rawSuccessfulSetHourlyCalls = chartSource
|
||||||
.replace(successfulHourlyDetailBlock, "")
|
.replace(successfulHourlyDetailBlock, "")
|
||||||
|
|||||||
Reference in New Issue
Block a user