fix chart refresh and bot command surface
This commit is contained in:
@@ -923,7 +923,7 @@ export function LiveTemperatureThresholdChart({
|
||||
lastPatchAtRef.current = now;
|
||||
markDetailRequest("network");
|
||||
|
||||
fetchHourlyForecastForCity(city, { resolution: targetResolution })
|
||||
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
if (!data) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
__resetHourlyDetailRequestQueueForTest,
|
||||
__runQueuedHourlyDetailRequestForTest,
|
||||
clearCityDetailCache,
|
||||
fetchHourlyForecastForCity,
|
||||
readCityDetailBatchDiagnostics,
|
||||
} from "@/components/dashboard/scan-terminal/temperature-chart-logic";
|
||||
|
||||
@@ -64,6 +65,13 @@ export async function runTests() {
|
||||
!querySource.includes("window.setInterval"),
|
||||
"scan list should subscribe to SSE patch state instead of running a 5-minute interval",
|
||||
);
|
||||
assert(
|
||||
querySource.includes("handleForegroundScanRefresh") &&
|
||||
querySource.includes('document.visibilityState !== "visible"') &&
|
||||
querySource.includes("SCAN_CACHE_TTL_MS") &&
|
||||
querySource.includes("fetchScanTerminal({ forceRefresh: false, showLoading: false })"),
|
||||
"scan list should silently revalidate stale rows when a long-hidden browser tab returns to the foreground",
|
||||
);
|
||||
assert(
|
||||
chartLogicSource.includes("DASHBOARD_REFRESH_POLICY_MS.metar") &&
|
||||
!chartSource.includes("window.setInterval"),
|
||||
@@ -79,8 +87,10 @@ export async function runTests() {
|
||||
assert(
|
||||
chartSource.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation") &&
|
||||
chartSource.includes("refreshCachedDetail") &&
|
||||
chartSource.includes("fetchHourlyForecastForCity(city, { resolution: targetResolution })"),
|
||||
"visible charts should do a lightweight cached detail refresh every observation cadence when no SSE patch arrives",
|
||||
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") &&
|
||||
chartLogicSource.includes("options.bypassLocalCache") &&
|
||||
chartLogicSource.includes("const forceRefresh = Boolean(options.ignoreCache)"),
|
||||
"visible charts should bypass the five-minute browser detail cache every observation cadence without force-refreshing backend sources",
|
||||
);
|
||||
assert(
|
||||
chartSource.includes("preloadTemperatureChartCanvas"),
|
||||
@@ -442,6 +452,7 @@ export async function runTests() {
|
||||
|
||||
const originalWindow = (globalThis as any).window;
|
||||
const originalSessionStorage = (globalThis as any).sessionStorage;
|
||||
const originalFetch = (globalThis as any).fetch;
|
||||
const store = new Map<string, string>();
|
||||
(globalThis as any).window = {};
|
||||
(globalThis as any).sessionStorage = {
|
||||
@@ -462,6 +473,50 @@ export async function runTests() {
|
||||
},
|
||||
};
|
||||
try {
|
||||
clearCityDetailCache();
|
||||
const revalidationUrls: string[] = [];
|
||||
let revalidationFetches = 0;
|
||||
(globalThis as any).fetch = async (url: string) => {
|
||||
revalidationFetches += 1;
|
||||
revalidationUrls.push(String(url));
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
cities: ["fallback-revalidate"],
|
||||
details: {
|
||||
"fallback-revalidate": {
|
||||
city: "fallback-revalidate",
|
||||
hourly: {
|
||||
times: ["00:00"],
|
||||
temps: [revalidationFetches],
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: {},
|
||||
missing: [],
|
||||
partial: false,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const firstDetail = await fetchHourlyForecastForCity("fallback-revalidate", { resolution: "10m" });
|
||||
const cachedDetail = await fetchHourlyForecastForCity("fallback-revalidate", { resolution: "10m" });
|
||||
const revalidatedDetail = await fetchHourlyForecastForCity("fallback-revalidate", {
|
||||
bypassLocalCache: true,
|
||||
resolution: "10m",
|
||||
});
|
||||
assert(
|
||||
firstDetail?.temps[0] === 1 &&
|
||||
cachedDetail?.temps[0] === 1 &&
|
||||
revalidatedDetail?.temps[0] === 2 &&
|
||||
revalidationFetches === 2,
|
||||
"bypassLocalCache should revalidate cached chart detail through the network",
|
||||
);
|
||||
assert(
|
||||
revalidationUrls.every((url) => url.includes("force_refresh=false")),
|
||||
"bypassLocalCache must not force-refresh backend observation sources",
|
||||
);
|
||||
|
||||
clearCityDetailCache();
|
||||
const cacheKey = "paris:10m";
|
||||
store.set(
|
||||
@@ -490,6 +545,7 @@ export async function runTests() {
|
||||
clearCityDetailCache();
|
||||
(globalThis as any).window = originalWindow;
|
||||
(globalThis as any).sessionStorage = originalSessionStorage;
|
||||
(globalThis as any).fetch = originalFetch;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,10 +231,10 @@ export function runTests() {
|
||||
);
|
||||
const fallbackRefreshBlock = chart.match(/const refreshCachedDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
|
||||
assert(
|
||||
fallbackRefreshBlock.includes("fetchHourlyForecastForCity(city, { resolution: targetResolution })") &&
|
||||
fallbackRefreshBlock.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") &&
|
||||
!fallbackRefreshBlock.includes("ignoreCache: true") &&
|
||||
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
|
||||
"no-patch fallback refresh should update the chart through cached batch detail without force-refreshing or showing the loading overlay",
|
||||
"no-patch fallback refresh should revalidate through cached backend detail without force-refreshing sources or showing the loading overlay",
|
||||
);
|
||||
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, targetResolution, applySuccessfulHourlyDetail\]\);/)?.[0] || "";
|
||||
assert(
|
||||
|
||||
@@ -1321,6 +1321,7 @@ function mergeRowObservationIntoHourly(
|
||||
}
|
||||
|
||||
type HourlyForecastFetchOptions = {
|
||||
bypassLocalCache?: boolean;
|
||||
ignoreCache?: boolean;
|
||||
resolution?: string;
|
||||
};
|
||||
@@ -1561,13 +1562,14 @@ async function fetchHourlyForecastForCity(
|
||||
const resParam = options.resolution || "10m";
|
||||
const cacheKey = `${city}:${resParam}`;
|
||||
const forceRefresh = Boolean(options.ignoreCache);
|
||||
const bypassLocalCache = forceRefresh || Boolean(options.bypassLocalCache);
|
||||
|
||||
if (!forceRefresh) {
|
||||
if (!bypassLocalCache) {
|
||||
const cached = readHourlyCacheEntry(cacheKey);
|
||||
if (cached) {
|
||||
return cached.data;
|
||||
}
|
||||
} else {
|
||||
} else if (forceRefresh) {
|
||||
const recentlyRefreshed = readHourlyCacheEntry(cacheKey, {
|
||||
maxAgeMs: HOURLY_FORCE_REFRESH_DEDUP_MS,
|
||||
});
|
||||
@@ -1576,7 +1578,11 @@ async function fetchHourlyForecastForCity(
|
||||
}
|
||||
}
|
||||
|
||||
const requestKey = options.ignoreCache ? `${city}:${resParam}:live` : `${city}:${resParam}`;
|
||||
const requestKey = forceRefresh
|
||||
? `${city}:${resParam}:live`
|
||||
: bypassLocalCache
|
||||
? `${city}:${resParam}:revalidate`
|
||||
: `${city}:${resParam}`;
|
||||
const pending = _hourlyRequestCache.get(requestKey);
|
||||
if (pending) return pending;
|
||||
|
||||
|
||||
@@ -103,6 +103,8 @@ export function useScanTerminalQuery({
|
||||
} = useRemoteDataQuery<ScanTerminalResponse>();
|
||||
|
||||
const lastForcedScanRefreshAtRef = useRef(0);
|
||||
const lastForegroundScanRefreshAtRef = useRef(0);
|
||||
const lastScanSuccessAtRef = useRef(0);
|
||||
const patchVersion = useSsePatchVersion();
|
||||
const [cachedRows, setCachedRows] = useState<ScanTerminalResponse | null>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -140,7 +142,11 @@ export function useScanTerminalQuery({
|
||||
tradingRegion,
|
||||
}),
|
||||
showLoading,
|
||||
onSuccess: (data) => { writeScanCache(data, tradingRegion || ""); setCachedRows(data); },
|
||||
onSuccess: (data) => {
|
||||
lastScanSuccessAtRef.current = Date.now();
|
||||
writeScanCache(data, tradingRegion || "");
|
||||
setCachedRows(data);
|
||||
},
|
||||
});
|
||||
},
|
||||
[isPro, proAccessLoading, run, timezoneOffsetSeconds, tradingRegion],
|
||||
@@ -172,6 +178,34 @@ export function useScanTerminalQuery({
|
||||
void fetchScanTerminal({ forceRefresh: true, showLoading: true });
|
||||
}, [fetchScanTerminal, terminalData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || typeof document === "undefined") return;
|
||||
if (proAccessLoading || !isPro) return;
|
||||
|
||||
const handleForegroundScanRefresh = () => {
|
||||
if (document.visibilityState !== "visible") return;
|
||||
if (scanRemote.status === "loading") return;
|
||||
const now = Date.now();
|
||||
if (now - lastForegroundScanRefreshAtRef.current < 30_000) return;
|
||||
if (
|
||||
lastScanSuccessAtRef.current > 0 &&
|
||||
now - lastScanSuccessAtRef.current < SCAN_CACHE_TTL_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastForegroundScanRefreshAtRef.current = now;
|
||||
void fetchScanTerminal({ forceRefresh: false, showLoading: false });
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleForegroundScanRefresh);
|
||||
window.addEventListener("focus", handleForegroundScanRefresh);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleForegroundScanRefresh);
|
||||
window.removeEventListener("focus", handleForegroundScanRefresh);
|
||||
};
|
||||
}, [fetchScanTerminal, isPro, proAccessLoading, scanRemote.status]);
|
||||
|
||||
// Preload adjacent regions in idle time for instant tab switches
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !tradingRegion || !isPro) return;
|
||||
|
||||
Reference in New Issue
Block a user