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 PROBABILITY_REFRESH_AFTER_PATCH_MS = 60_000;
|
||||||
const FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS = 90_000;
|
const FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS = 90_000;
|
||||||
const DETAIL_LOAD_BATCH_DELAY_MS = 0;
|
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(
|
const TemperatureChartCanvas = dynamic(
|
||||||
() =>
|
() =>
|
||||||
@@ -115,12 +118,46 @@ function shouldFetchCityDetailForChart({
|
|||||||
city,
|
city,
|
||||||
documentHidden,
|
documentHidden,
|
||||||
isChartVisible,
|
isChartVisible,
|
||||||
|
compact = false,
|
||||||
|
isActive = false,
|
||||||
|
isMaximized = false,
|
||||||
|
slotIndex = 0,
|
||||||
|
detailLoadReady = true,
|
||||||
}: {
|
}: {
|
||||||
city: string;
|
city: string;
|
||||||
documentHidden: boolean;
|
documentHidden: boolean;
|
||||||
isChartVisible: 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 ─────────────────────────────────────────────────────
|
// ── Main component ─────────────────────────────────────────────────────
|
||||||
@@ -136,6 +173,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
isMaximized = false,
|
isMaximized = false,
|
||||||
disableClose = false,
|
disableClose = false,
|
||||||
isActive = !compact,
|
isActive = !compact,
|
||||||
|
slotIndex = 0,
|
||||||
}: {
|
}: {
|
||||||
isEn: boolean;
|
isEn: boolean;
|
||||||
row: ScanOpportunityRow | null;
|
row: ScanOpportunityRow | null;
|
||||||
@@ -179,6 +217,11 @@ export function LiveTemperatureThresholdChart({
|
|||||||
const [targetResolution, setTargetResolution] = useState<string>(() =>
|
const [targetResolution, setTargetResolution] = useState<string>(() =>
|
||||||
prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m",
|
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(() =>
|
const [currentCityLocalDate, setCurrentCityLocalDate] = useState(() =>
|
||||||
formatCityLocalDate(row?.tz_offset_seconds),
|
formatCityLocalDate(row?.tz_offset_seconds),
|
||||||
);
|
);
|
||||||
@@ -191,7 +234,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
setTargetResolution(prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m");
|
setTargetResolution(prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m");
|
||||||
setHourly(seedHourlyForecastFromRow(row));
|
setHourly(seedHourlyForecastFromRow(row));
|
||||||
setLiveTemp(null);
|
setLiveTemp(null);
|
||||||
setIsHourlyLoading(Boolean(city));
|
setIsHourlyLoading(Boolean(city) && detailLoadDelayMs === 0);
|
||||||
setDetailError(null);
|
setDetailError(null);
|
||||||
setDetailRetryNonce(0);
|
setDetailRetryNonce(0);
|
||||||
setShowingStaleDetail(false);
|
setShowingStaleDetail(false);
|
||||||
@@ -202,7 +245,22 @@ export function LiveTemperatureThresholdChart({
|
|||||||
lastForegroundRefreshAtRef.current = 0;
|
lastForegroundRefreshAtRef.current = 0;
|
||||||
localDayRolloverFetchDateRef.current = "";
|
localDayRolloverFetchDateRef.current = "";
|
||||||
setCurrentCityLocalDate(formatCityLocalDate(row?.tz_offset_seconds));
|
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(() => {
|
useEffect(() => {
|
||||||
const node = chartVisibilityRef.current;
|
const node = chartVisibilityRef.current;
|
||||||
@@ -235,17 +293,6 @@ export function LiveTemperatureThresholdChart({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!shouldFetchCityDetailForChart({
|
|
||||||
city,
|
|
||||||
documentHidden:
|
|
||||||
typeof document !== "undefined" && document.visibilityState === "hidden",
|
|
||||||
isChartVisible,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheKey = `${city}:${targetResolution}`;
|
const cacheKey = `${city}:${targetResolution}`;
|
||||||
let cached = _hourlyCache.get(cacheKey);
|
let cached = _hourlyCache.get(cacheKey);
|
||||||
if (!cached || Date.now() - Number(cached.ts || 0) >= HOURLY_CACHE_TTL_MS) {
|
if (!cached || Date.now() - Number(cached.ts || 0) >= HOURLY_CACHE_TTL_MS) {
|
||||||
@@ -270,6 +317,23 @@ export function LiveTemperatureThresholdChart({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!shouldFetchCityDetailForChart({
|
||||||
|
city,
|
||||||
|
documentHidden:
|
||||||
|
typeof document !== "undefined" && document.visibilityState === "hidden",
|
||||||
|
isChartVisible,
|
||||||
|
compact,
|
||||||
|
isActive,
|
||||||
|
isMaximized,
|
||||||
|
slotIndex,
|
||||||
|
detailLoadReady,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
setIsHourlyLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!cached && !hasLoadedHourlyDetailRef.current) {
|
if (!cached && !hasLoadedHourlyDetailRef.current) {
|
||||||
setHourly(seedHourlyForecastFromRow(row));
|
setHourly(seedHourlyForecastFromRow(row));
|
||||||
setShowingStaleDetail(false);
|
setShowingStaleDetail(false);
|
||||||
@@ -302,7 +366,19 @@ export function LiveTemperatureThresholdChart({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [city, row, targetResolution, isChartVisible, detailRetryNonce, isEn]);
|
}, [
|
||||||
|
city,
|
||||||
|
row,
|
||||||
|
targetResolution,
|
||||||
|
isChartVisible,
|
||||||
|
compact,
|
||||||
|
isActive,
|
||||||
|
isMaximized,
|
||||||
|
slotIndex,
|
||||||
|
detailLoadReady,
|
||||||
|
detailRetryNonce,
|
||||||
|
isEn,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!latestPatch || latestPatch.revision <= lastAppliedPatchRevisionRef.current) return;
|
if (!latestPatch || latestPatch.revision <= lastAppliedPatchRevisionRef.current) return;
|
||||||
@@ -897,6 +973,7 @@ export const __getLiveObservationLabelsForTest = getLiveObservationLabels;
|
|||||||
export const __getObservationDisplayMetricsForTest = getObservationDisplayMetrics;
|
export const __getObservationDisplayMetricsForTest = getObservationDisplayMetrics;
|
||||||
export const __getPeakGlowStateForTest = getPeakGlowState;
|
export const __getPeakGlowStateForTest = getPeakGlowState;
|
||||||
export const __getWundergroundDailyHighForTest = getWundergroundDailyHigh;
|
export const __getWundergroundDailyHighForTest = getWundergroundDailyHigh;
|
||||||
|
export const __getInitialDetailLoadDelayMsForTest = getInitialDetailLoadDelayMs;
|
||||||
export const __shouldFetchCityDetailForChartForTest = shouldFetchCityDetailForChart;
|
export const __shouldFetchCityDetailForChartForTest = shouldFetchCityDetailForChart;
|
||||||
export const __shouldPollLiveChartForTest = shouldPollLiveChart;
|
export const __shouldPollLiveChartForTest = shouldPollLiveChart;
|
||||||
export const __mergePatchIntoHourlyForTest = mergePatchIntoHourly;
|
export const __mergePatchIntoHourlyForTest = mergePatchIntoHourly;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@/lib/refresh-policy";
|
} from "@/lib/refresh-policy";
|
||||||
import { scanTerminalQueryPolicy } from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
import { scanTerminalQueryPolicy } from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
||||||
import {
|
import {
|
||||||
|
__getInitialDetailLoadDelayMsForTest,
|
||||||
__shouldFetchCityDetailForChartForTest,
|
__shouldFetchCityDetailForChartForTest,
|
||||||
__shouldPollLiveChartForTest,
|
__shouldPollLiveChartForTest,
|
||||||
} from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
|
} from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
|
||||||
@@ -164,6 +165,46 @@ export async function runTests() {
|
|||||||
__shouldFetchCityDetailForChartForTest({ city: "paris", documentHidden: true, isChartVisible: true }) === false,
|
__shouldFetchCityDetailForChartForTest({ city: "paris", documentHidden: true, isChartVisible: true }) === false,
|
||||||
"hidden browser tabs should not prefetch city detail",
|
"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(
|
const normalizedBatchDetail = __resolveCityDetailFromBatchForTest(
|
||||||
{
|
{
|
||||||
"hong kong": {
|
"hong kong": {
|
||||||
|
|||||||
Reference in New Issue
Block a user