拆分图表实时观测和模型缓存
This commit is contained in:
@@ -38,7 +38,6 @@ import {
|
|||||||
readCityDetailBatchDiagnostics,
|
readCityDetailBatchDiagnostics,
|
||||||
readHourlyDetailSnapshot,
|
readHourlyDetailSnapshot,
|
||||||
readHourlyDetailSnapshotAgeMs,
|
readHourlyDetailSnapshotAgeMs,
|
||||||
rememberHourlyDetailSnapshot,
|
|
||||||
selectCompactSecondaryTemp,
|
selectCompactSecondaryTemp,
|
||||||
selectDisplayRunwayTemp,
|
selectDisplayRunwayTemp,
|
||||||
selectInitialHourlyForRowChange,
|
selectInitialHourlyForRowChange,
|
||||||
@@ -65,7 +64,6 @@ const PEAK_GLOW_BADGE_CLASS = {
|
|||||||
cooling: "border-slate-200 bg-slate-100 text-slate-500",
|
cooling: "border-slate-200 bg-slate-100 text-slate-500",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const PROBABILITY_REFRESH_AFTER_PATCH_MS = DASHBOARD_REFRESH_POLICY_MS.metar;
|
|
||||||
const FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS = 90_000;
|
const FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS = 90_000;
|
||||||
const LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback;
|
const LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback;
|
||||||
const DETAIL_LOAD_BATCH_DELAY_MS = 0;
|
const DETAIL_LOAD_BATCH_DELAY_MS = 0;
|
||||||
@@ -684,7 +682,6 @@ export function LiveTemperatureThresholdChart({
|
|||||||
const chartVisibilityRef = useRef<HTMLDivElement | null>(null);
|
const chartVisibilityRef = useRef<HTMLDivElement | null>(null);
|
||||||
const lastPatchAtRef = useRef<number>(Date.now());
|
const lastPatchAtRef = useRef<number>(Date.now());
|
||||||
const lastAppliedPatchRevisionRef = useRef<number>(0);
|
const lastAppliedPatchRevisionRef = useRef<number>(0);
|
||||||
const lastProbabilityRefreshAtRef = useRef<number>(0);
|
|
||||||
const lastForegroundRefreshAtRef = useRef<number>(0);
|
const lastForegroundRefreshAtRef = useRef<number>(0);
|
||||||
const lastRowObservationSignatureRef = useRef<string>("");
|
const lastRowObservationSignatureRef = useRef<string>("");
|
||||||
const localDayRolloverFetchDateRef = useRef<string>("");
|
const localDayRolloverFetchDateRef = useRef<string>("");
|
||||||
@@ -763,7 +760,6 @@ export function LiveTemperatureThresholdChart({
|
|||||||
hasLoadedHourlyDetailRef.current = false;
|
hasLoadedHourlyDetailRef.current = false;
|
||||||
lastPatchAtRef.current = now;
|
lastPatchAtRef.current = now;
|
||||||
lastAppliedPatchRevisionRef.current = 0;
|
lastAppliedPatchRevisionRef.current = 0;
|
||||||
lastProbabilityRefreshAtRef.current = 0;
|
|
||||||
lastForegroundRefreshAtRef.current = 0;
|
lastForegroundRefreshAtRef.current = 0;
|
||||||
lastRowObservationSignatureRef.current = "";
|
lastRowObservationSignatureRef.current = "";
|
||||||
localDayRolloverFetchDateRef.current = "";
|
localDayRolloverFetchDateRef.current = "";
|
||||||
@@ -811,12 +807,8 @@ export function LiveTemperatureThresholdChart({
|
|||||||
}, [row?.tz_offset_seconds]);
|
}, [row?.tz_offset_seconds]);
|
||||||
|
|
||||||
const commitHourlySnapshot = useCallback((buildNext: (previous: HourlyForecast) => HourlyForecast) => {
|
const commitHourlySnapshot = useCallback((buildNext: (previous: HourlyForecast) => HourlyForecast) => {
|
||||||
setHourly((prev) => {
|
setHourly((prev) => buildNext(prev));
|
||||||
const next = buildNext(prev);
|
}, []);
|
||||||
rememberHourlyDetailSnapshot(city, targetResolution, next);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, [city, targetResolution]);
|
|
||||||
|
|
||||||
const applySuccessfulHourlyDetail = useCallback((data: HourlyForecast, options?: { updateLiveTemp?: boolean }) => {
|
const applySuccessfulHourlyDetail = useCallback((data: HourlyForecast, options?: { updateLiveTemp?: boolean }) => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@@ -1024,46 +1016,19 @@ export function LiveTemperatureThresholdChart({
|
|||||||
sseSourceToCollectorLatencySec: latestPatch.delivery?.source_to_collector_latency_sec ?? null,
|
sseSourceToCollectorLatencySec: latestPatch.delivery?.source_to_collector_latency_sec ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const hasObservationChange =
|
}, [latestPatch, getLatestRowSnapshot, commitHourlySnapshot]);
|
||||||
tempValue !== null ||
|
|
||||||
Array.isArray(latestPatch.changes.runway_points) ||
|
|
||||||
Boolean(latestPatch.changes.amos);
|
|
||||||
if (!hasObservationChange || !shouldPollLiveChart({ city, compact, isActive, isMaximized })) return;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastProbabilityRefreshAtRef.current < PROBABILITY_REFRESH_AFTER_PATCH_MS) return;
|
|
||||||
lastProbabilityRefreshAtRef.current = now;
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
const refreshProbabilityOverlayAfterPatch = () => {
|
|
||||||
void runHourlyDetailFetch({
|
|
||||||
source: "force_refresh",
|
|
||||||
fetchOptions: { ignoreCache: true },
|
|
||||||
isCancelled: () => cancelled,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
refreshProbabilityOverlayAfterPatch();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [latestPatch, city, targetResolution, compact, isActive, isMaximized, getLatestRowSnapshot, commitHourlySnapshot, runHourlyDetailFetch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resyncVersion || !city) return;
|
if (!resyncVersion || !city) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void runHourlyDetailFetch({
|
void fetchLiveObservationForCity(city).then((payload) => {
|
||||||
source: "force_refresh",
|
if (cancelled || !payload) return;
|
||||||
fetchOptions: { ignoreCache: true },
|
applyLiveObservationPayload(payload);
|
||||||
isCancelled: () => cancelled,
|
|
||||||
onSettled: () => {
|
|
||||||
setIsHourlyLoading(false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [resyncVersion, city, runHourlyDetailFetch]);
|
}, [resyncVersion, city, applyLiveObservationPayload]);
|
||||||
|
|
||||||
// ── SSE fallback: visible charts merge no-store observations if patches stop. ──
|
// ── SSE fallback: visible charts merge no-store observations if patches stop. ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
+13
-13
@@ -82,9 +82,9 @@ export async function runTests() {
|
|||||||
assert(
|
assert(
|
||||||
chartSource.includes("useLatestPatch") &&
|
chartSource.includes("useLatestPatch") &&
|
||||||
chartSource.includes("latestPatch") &&
|
chartSource.includes("latestPatch") &&
|
||||||
chartSource.includes("DASHBOARD_REFRESH_POLICY_MS.metar") &&
|
!chartSource.includes("DASHBOARD_REFRESH_POLICY_MS.metar") &&
|
||||||
!chartSource.includes("2 * 60_000"),
|
!chartSource.includes("2 * 60_000"),
|
||||||
"selected city chart should consume SSE patches and keep METAR cadence for heavy probability refreshes instead of a 2-minute forced refresh",
|
"selected city chart should consume SSE patches without coupling live observations to the model/detail refresh cadence",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback") &&
|
chartSource.includes("LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback") &&
|
||||||
@@ -136,15 +136,15 @@ export async function runTests() {
|
|||||||
"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("fetchOptions: { ignoreCache: true }") &&
|
chartSource.includes("fetchLiveObservationForCity") &&
|
||||||
chartSource.includes("const mergedHourly = mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, latestRow)") &&
|
chartSource.includes("mergeObservationPayloadIntoHourly") &&
|
||||||
chartSource.includes("return mergedHourly;"),
|
!chartSource.includes("rememberHourlyDetailSnapshot"),
|
||||||
"visible chart fallback must refresh full city detail at the current chart resolution while preserving newer live observations",
|
"visible chart fallback must use the no-store observation endpoint without writing live overlays into full-detail cache",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("PROBABILITY_REFRESH_AFTER_PATCH_MS = DASHBOARD_REFRESH_POLICY_MS.metar") &&
|
!chartSource.includes("PROBABILITY_REFRESH_AFTER_PATCH_MS") &&
|
||||||
!chartSource.includes("PROBABILITY_REFRESH_AFTER_PATCH_MS = 60_000"),
|
!chartSource.includes("refreshProbabilityOverlayAfterPatch"),
|
||||||
"live observation patches should update the plotted line immediately and throttle heavy probability/detail refreshes to METAR cadence",
|
"live observation patches must not force-refresh model, DEB, probability, or detail-batch data",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartLogicSource.includes("HOURLY_FORCE_REFRESH_DEDUP_MS") &&
|
chartLogicSource.includes("HOURLY_FORCE_REFRESH_DEDUP_MS") &&
|
||||||
@@ -370,8 +370,8 @@ export async function runTests() {
|
|||||||
);
|
);
|
||||||
const rememberSnapshotCalls = chartSource.match(/rememberHourlyDetailSnapshot\(/g) || [];
|
const rememberSnapshotCalls = chartSource.match(/rememberHourlyDetailSnapshot\(/g) || [];
|
||||||
assert(
|
assert(
|
||||||
rememberSnapshotCalls.length === 1,
|
rememberSnapshotCalls.length === 0,
|
||||||
"temperature chart should persist hourly snapshots through one local commit helper instead of writing cache snapshots from multiple effects",
|
"temperature chart component must not persist live-merged snapshots into the full-detail cache",
|
||||||
);
|
);
|
||||||
const markDetailDegradedBlock =
|
const markDetailDegradedBlock =
|
||||||
/const markDetailDegraded = useCallback\([\s\S]*?\n \}, \[[^\]]*\]\);/.exec(chartSource)?.[0] || "";
|
/const markDetailDegraded = useCallback\([\s\S]*?\n \}, \[[^\]]*\]\);/.exec(chartSource)?.[0] || "";
|
||||||
@@ -388,13 +388,13 @@ export async function runTests() {
|
|||||||
successfulHourlyDetailBlock.includes("getLatestRowSnapshot") &&
|
successfulHourlyDetailBlock.includes("getLatestRowSnapshot") &&
|
||||||
successfulHourlyDetailBlock.includes("commitHourlySnapshot") &&
|
successfulHourlyDetailBlock.includes("commitHourlySnapshot") &&
|
||||||
!/\[row\]/.test(successfulHourlyDetailBlock),
|
!/\[row\]/.test(successfulHourlyDetailBlock),
|
||||||
"successful city detail refreshes must read the latest row, persist the merged snapshot, and avoid depending on the row object",
|
"successful city detail refreshes must read the latest row, merge into chart state, and avoid depending on the row object",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("commitHourlySnapshot") &&
|
chartSource.includes("commitHourlySnapshot") &&
|
||||||
chartSource.includes("mergeRowObservationIntoHourly") &&
|
chartSource.includes("mergeRowObservationIntoHourly") &&
|
||||||
chartSource.includes("mergePatchIntoHourly"),
|
chartSource.includes("mergePatchIntoHourly"),
|
||||||
"live row and SSE patch merges must persist their hourly snapshots through the shared commit helper so returning to terminal restores runway history immediately",
|
"live row and SSE patch merges must flow through the shared state helper without touching the full-detail cache",
|
||||||
);
|
);
|
||||||
const coldDetailFetchBlock =
|
const coldDetailFetchBlock =
|
||||||
/useEffect\(\(\) => \{\s*if \(!city\) \{[\s\S]*?scheduleTransientDetailRetry[\s\S]*?runHourlyDetailFetch\(\{[\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] || "";
|
||||||
|
|||||||
+17
-20
@@ -189,12 +189,12 @@ export function runTests() {
|
|||||||
assert(!chart.includes("function mergePatchIntoHourly"), "LiveTemperatureThresholdChart.tsx must not define SSE patch merge logic inline");
|
assert(!chart.includes("function mergePatchIntoHourly"), "LiveTemperatureThresholdChart.tsx must not define SSE patch merge logic inline");
|
||||||
assert(chart.includes("useLatestPatch"), "temperature chart must consume useLatestPatch(city)");
|
assert(chart.includes("useLatestPatch"), "temperature chart must consume useLatestPatch(city)");
|
||||||
assert(chart.includes("latestPatch"), "temperature chart must react to incoming SSE patches");
|
assert(chart.includes("latestPatch"), "temperature chart must react to incoming SSE patches");
|
||||||
assert(chart.includes("useSseResyncVersion"), "temperature chart must resync full detail when SSE replay is incomplete");
|
assert(chart.includes("useSseResyncVersion"), "temperature chart must resync live observations when SSE replay is incomplete");
|
||||||
assert(chartLogic.includes("runway_points"), "temperature chart must merge v1 runway_points into runway history");
|
assert(chartLogic.includes("runway_points"), "temperature chart must merge v1 runway_points into runway history");
|
||||||
assert(
|
assert(
|
||||||
chart.includes("DASHBOARD_REFRESH_POLICY_MS.metar") &&
|
!chart.includes("DASHBOARD_REFRESH_POLICY_MS.metar") &&
|
||||||
!chart.includes("2 * 60_000"),
|
!chart.includes("2 * 60_000"),
|
||||||
"temperature chart must keep METAR cadence for heavy patch-triggered probability refreshes",
|
"temperature chart must not attach model/detail refresh cadence to live observation patches",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chart.includes("LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback"),
|
chart.includes("LIVE_OBSERVATION_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.liveObservationFallback"),
|
||||||
@@ -239,12 +239,18 @@ export function runTests() {
|
|||||||
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
||||||
"no-patch fallback refresh should merge no-store observation data without refreshing cached detail-batch or showing the loading overlay",
|
"no-patch fallback refresh should merge no-store observation data without refreshing cached detail-batch or showing the loading overlay",
|
||||||
);
|
);
|
||||||
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, runHourlyDetailFetch\]\);/)?.[0] || "";
|
|
||||||
assert(
|
assert(
|
||||||
resyncBlock.includes("runHourlyDetailFetch") &&
|
!chart.includes("rememberHourlyDetailSnapshot"),
|
||||||
resyncBlock.includes("fetchOptions: { ignoreCache: true }") &&
|
"temperature chart must not write row/SSE/observation overlays back into the full-detail cache",
|
||||||
|
);
|
||||||
|
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, applyLiveObservationPayload\]\);/)?.[0] || "";
|
||||||
|
assert(
|
||||||
|
resyncBlock.includes("fetchLiveObservationForCity") &&
|
||||||
|
resyncBlock.includes("applyLiveObservationPayload") &&
|
||||||
|
!resyncBlock.includes("runHourlyDetailFetch") &&
|
||||||
|
!resyncBlock.includes("ignoreCache: true") &&
|
||||||
!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 no-store observations without touching cached detail-batch or showing the loading overlay",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chart.includes("visibilitychange") &&
|
chart.includes("visibilitychange") &&
|
||||||
@@ -290,19 +296,10 @@ export function runTests() {
|
|||||||
"runway charts must request 1-minute detail resolution so historical runway lines match live SSE patch cadence",
|
"runway charts must request 1-minute detail resolution so historical runway lines match live SSE patch cadence",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chart.includes("PROBABILITY_REFRESH_AFTER_PATCH_MS") &&
|
!chart.includes("PROBABILITY_REFRESH_AFTER_PATCH_MS") &&
|
||||||
chart.includes("lastProbabilityRefreshAtRef") &&
|
!chart.includes("lastProbabilityRefreshAtRef") &&
|
||||||
chart.includes("refreshProbabilityOverlayAfterPatch"),
|
!chart.includes("refreshProbabilityOverlayAfterPatch"),
|
||||||
"temperature chart must trigger a throttled background probability refresh after live observation patches",
|
"live observation patches must not trigger model/probability/detail refreshes; cached detail-batch stays on its own cadence",
|
||||||
);
|
|
||||||
const patchEffectBlock = chart.match(
|
|
||||||
/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?refreshProbabilityOverlayAfterPatch\(\);[\s\S]*?\}, \[[^\]]*latestPatch[^\]]*runHourlyDetailFetch[^\]]*\]\);/,
|
|
||||||
)?.[0] || "";
|
|
||||||
assert(
|
|
||||||
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
|
|
||||||
patchEffectBlock.includes("fetchOptions: { ignoreCache: true }") &&
|
|
||||||
!patchEffectBlock.includes("setIsHourlyLoading(true)"),
|
|
||||||
"live patch probability refresh must recompute legacy Gaussian in the background without showing a loading overlay",
|
|
||||||
);
|
);
|
||||||
assert(!chartCanvas.includes("ResponsiveContainer"), "temperature chart canvas must not mount Recharts through ResponsiveContainer at 0x0");
|
assert(!chartCanvas.includes("ResponsiveContainer"), "temperature chart canvas must not mount Recharts through ResponsiveContainer at 0x0");
|
||||||
assert(chartCanvas.includes("ResizeObserver"), "temperature chart canvas must measure its host with ResizeObserver");
|
assert(chartCanvas.includes("ResizeObserver"), "temperature chart canvas must measure its host with ResizeObserver");
|
||||||
|
|||||||
Reference in New Issue
Block a user