Defer secondary chart detail loads
This commit is contained in:
@@ -58,6 +58,9 @@ const PEAK_GLOW_BADGE_CLASS = {
|
||||
const PROBABILITY_REFRESH_AFTER_PATCH_MS = 60_000;
|
||||
const FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS = 90_000;
|
||||
const DETAIL_LOAD_BATCH_DELAY_MS = 0;
|
||||
const INITIAL_DETAIL_LOAD_SLOTS = 3;
|
||||
const DEFERRED_DETAIL_LOAD_DELAY_MS = 1_200;
|
||||
const DEFERRED_DETAIL_LOAD_STEP_MS = 450;
|
||||
|
||||
const TemperatureChartCanvas = dynamic(
|
||||
() =>
|
||||
@@ -115,12 +118,46 @@ function shouldFetchCityDetailForChart({
|
||||
city,
|
||||
documentHidden,
|
||||
isChartVisible,
|
||||
compact = false,
|
||||
isActive = false,
|
||||
isMaximized = false,
|
||||
slotIndex = 0,
|
||||
detailLoadReady = true,
|
||||
}: {
|
||||
city: string;
|
||||
documentHidden: boolean;
|
||||
isChartVisible: boolean;
|
||||
compact?: boolean;
|
||||
isActive?: boolean;
|
||||
isMaximized?: boolean;
|
||||
slotIndex?: number;
|
||||
detailLoadReady?: boolean;
|
||||
}) {
|
||||
return Boolean(city) && isChartVisible && !documentHidden;
|
||||
if (!city || !isChartVisible || documentHidden) return false;
|
||||
if (!compact || isActive || isMaximized) return true;
|
||||
if (normalizeSlotIndex(slotIndex) < INITIAL_DETAIL_LOAD_SLOTS) return true;
|
||||
return detailLoadReady;
|
||||
}
|
||||
|
||||
function normalizeSlotIndex(slotIndex: number | null | undefined) {
|
||||
return Number.isFinite(slotIndex) && Number(slotIndex) >= 0 ? Math.floor(Number(slotIndex)) : 0;
|
||||
}
|
||||
|
||||
function getInitialDetailLoadDelayMs({
|
||||
compact,
|
||||
isActive,
|
||||
isMaximized,
|
||||
slotIndex,
|
||||
}: {
|
||||
compact?: boolean;
|
||||
isActive?: boolean;
|
||||
isMaximized?: boolean;
|
||||
slotIndex?: number;
|
||||
}) {
|
||||
if (!compact || isActive || isMaximized) return 0;
|
||||
const normalizedIndex = normalizeSlotIndex(slotIndex);
|
||||
if (normalizedIndex < INITIAL_DETAIL_LOAD_SLOTS) return 0;
|
||||
return DEFERRED_DETAIL_LOAD_DELAY_MS + (normalizedIndex - INITIAL_DETAIL_LOAD_SLOTS) * DEFERRED_DETAIL_LOAD_STEP_MS;
|
||||
}
|
||||
|
||||
// ── Main component ─────────────────────────────────────────────────────
|
||||
@@ -136,6 +173,7 @@ export function LiveTemperatureThresholdChart({
|
||||
isMaximized = false,
|
||||
disableClose = false,
|
||||
isActive = !compact,
|
||||
slotIndex = 0,
|
||||
}: {
|
||||
isEn: boolean;
|
||||
row: ScanOpportunityRow | null;
|
||||
@@ -179,6 +217,11 @@ export function LiveTemperatureThresholdChart({
|
||||
const [targetResolution, setTargetResolution] = useState<string>(() =>
|
||||
prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m",
|
||||
);
|
||||
const detailLoadDelayMs = useMemo(
|
||||
() => getInitialDetailLoadDelayMs({ compact, isActive, isMaximized, slotIndex }),
|
||||
[compact, isActive, isMaximized, slotIndex],
|
||||
);
|
||||
const [detailLoadReady, setDetailLoadReady] = useState(() => detailLoadDelayMs === 0);
|
||||
const [currentCityLocalDate, setCurrentCityLocalDate] = useState(() =>
|
||||
formatCityLocalDate(row?.tz_offset_seconds),
|
||||
);
|
||||
@@ -191,7 +234,7 @@ export function LiveTemperatureThresholdChart({
|
||||
setTargetResolution(prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m");
|
||||
setHourly(seedHourlyForecastFromRow(row));
|
||||
setLiveTemp(null);
|
||||
setIsHourlyLoading(Boolean(city));
|
||||
setIsHourlyLoading(Boolean(city) && detailLoadDelayMs === 0);
|
||||
setDetailError(null);
|
||||
setDetailRetryNonce(0);
|
||||
setShowingStaleDetail(false);
|
||||
@@ -202,7 +245,22 @@ export function LiveTemperatureThresholdChart({
|
||||
lastForegroundRefreshAtRef.current = 0;
|
||||
localDayRolloverFetchDateRef.current = "";
|
||||
setCurrentCityLocalDate(formatCityLocalDate(row?.tz_offset_seconds));
|
||||
}, [city]);
|
||||
}, [city, detailLoadDelayMs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!city) {
|
||||
setDetailLoadReady(false);
|
||||
return;
|
||||
}
|
||||
if (detailLoadDelayMs <= 0) {
|
||||
setDetailLoadReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setDetailLoadReady(false);
|
||||
const id = setTimeout(() => setDetailLoadReady(true), detailLoadDelayMs);
|
||||
return () => clearTimeout(id);
|
||||
}, [city, detailLoadDelayMs]);
|
||||
|
||||
useEffect(() => {
|
||||
const node = chartVisibilityRef.current;
|
||||
@@ -235,17 +293,6 @@ export function LiveTemperatureThresholdChart({
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!shouldFetchCityDetailForChart({
|
||||
city,
|
||||
documentHidden:
|
||||
typeof document !== "undefined" && document.visibilityState === "hidden",
|
||||
isChartVisible,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheKey = `${city}:${targetResolution}`;
|
||||
let cached = _hourlyCache.get(cacheKey);
|
||||
if (!cached || Date.now() - Number(cached.ts || 0) >= HOURLY_CACHE_TTL_MS) {
|
||||
@@ -270,6 +317,23 @@ export function LiveTemperatureThresholdChart({
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!shouldFetchCityDetailForChart({
|
||||
city,
|
||||
documentHidden:
|
||||
typeof document !== "undefined" && document.visibilityState === "hidden",
|
||||
isChartVisible,
|
||||
compact,
|
||||
isActive,
|
||||
isMaximized,
|
||||
slotIndex,
|
||||
detailLoadReady,
|
||||
})
|
||||
) {
|
||||
setIsHourlyLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cached && !hasLoadedHourlyDetailRef.current) {
|
||||
setHourly(seedHourlyForecastFromRow(row));
|
||||
setShowingStaleDetail(false);
|
||||
@@ -302,7 +366,19 @@ export function LiveTemperatureThresholdChart({
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [city, row, targetResolution, isChartVisible, detailRetryNonce, isEn]);
|
||||
}, [
|
||||
city,
|
||||
row,
|
||||
targetResolution,
|
||||
isChartVisible,
|
||||
compact,
|
||||
isActive,
|
||||
isMaximized,
|
||||
slotIndex,
|
||||
detailLoadReady,
|
||||
detailRetryNonce,
|
||||
isEn,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestPatch || latestPatch.revision <= lastAppliedPatchRevisionRef.current) return;
|
||||
@@ -897,6 +973,7 @@ export const __getLiveObservationLabelsForTest = getLiveObservationLabels;
|
||||
export const __getObservationDisplayMetricsForTest = getObservationDisplayMetrics;
|
||||
export const __getPeakGlowStateForTest = getPeakGlowState;
|
||||
export const __getWundergroundDailyHighForTest = getWundergroundDailyHigh;
|
||||
export const __getInitialDetailLoadDelayMsForTest = getInitialDetailLoadDelayMs;
|
||||
export const __shouldFetchCityDetailForChartForTest = shouldFetchCityDetailForChart;
|
||||
export const __shouldPollLiveChartForTest = shouldPollLiveChart;
|
||||
export const __mergePatchIntoHourlyForTest = mergePatchIntoHourly;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@/lib/refresh-policy";
|
||||
import { scanTerminalQueryPolicy } from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
||||
import {
|
||||
__getInitialDetailLoadDelayMsForTest,
|
||||
__shouldFetchCityDetailForChartForTest,
|
||||
__shouldPollLiveChartForTest,
|
||||
} from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
|
||||
@@ -164,6 +165,46 @@ export async function runTests() {
|
||||
__shouldFetchCityDetailForChartForTest({ city: "paris", documentHidden: true, isChartVisible: true }) === false,
|
||||
"hidden browser tabs should not prefetch city detail",
|
||||
);
|
||||
assert(
|
||||
__getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: false, slotIndex: 0 }) === 0 &&
|
||||
__getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: false, slotIndex: 2 }) === 0,
|
||||
"first visible grid charts should fetch full detail immediately",
|
||||
);
|
||||
assert(
|
||||
__getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: false, slotIndex: 3 }) > 0,
|
||||
"later non-active grid charts should defer full-detail loading after first paint",
|
||||
);
|
||||
assert(
|
||||
__getInitialDetailLoadDelayMsForTest({ compact: true, isActive: true, isMaximized: false, slotIndex: 8 }) === 0 &&
|
||||
__getInitialDetailLoadDelayMsForTest({ compact: true, isActive: false, isMaximized: true, slotIndex: 8 }) === 0,
|
||||
"active or maximized charts should bypass deferred detail loading",
|
||||
);
|
||||
assert(
|
||||
__shouldFetchCityDetailForChartForTest({
|
||||
city: "paris",
|
||||
documentHidden: false,
|
||||
isChartVisible: true,
|
||||
compact: true,
|
||||
isActive: false,
|
||||
isMaximized: false,
|
||||
slotIndex: 5,
|
||||
detailLoadReady: false,
|
||||
}) === false,
|
||||
"deferred grid charts should not enter the detail request queue before their delay is ready",
|
||||
);
|
||||
assert(
|
||||
__shouldFetchCityDetailForChartForTest({
|
||||
city: "paris",
|
||||
documentHidden: false,
|
||||
isChartVisible: true,
|
||||
compact: true,
|
||||
isActive: false,
|
||||
isMaximized: false,
|
||||
slotIndex: 5,
|
||||
detailLoadReady: true,
|
||||
}) === true,
|
||||
"deferred grid charts should fetch detail after their delay is ready",
|
||||
);
|
||||
const normalizedBatchDetail = __resolveCityDetailFromBatchForTest(
|
||||
{
|
||||
"hong kong": {
|
||||
|
||||
Reference in New Issue
Block a user