Reduce terminal detail refresh traffic

This commit is contained in:
2569718930@qq.com
2026-05-31 21:01:11 +08:00
parent 8d552a9279
commit 071d749ef0
4 changed files with 44 additions and 7 deletions
@@ -20,6 +20,7 @@ export function runTests() {
const cached = buildCityDetailProxyCachePolicy("false", 15); const cached = buildCityDetailProxyCachePolicy("false", 15);
assert.equal(cached.fetchMode, "revalidate"); assert.equal(cached.fetchMode, "revalidate");
assert.equal(cached.revalidateSeconds, 15); assert.equal(cached.revalidateSeconds, 15);
assert.match(cached.responseCacheControl, /max-age=15/);
assert.match(cached.responseCacheControl, /s-maxage=15/); assert.match(cached.responseCacheControl, /s-maxage=15/);
const scanForced = buildForceRefreshProxyCachePolicy("true", 10); const scanForced = buildForceRefreshProxyCachePolicy("true", 10);
@@ -76,6 +76,12 @@ export async function runTests() {
chartSource.includes("setHourly(data)"), chartSource.includes("setHourly(data)"),
"visible chart fallback must refresh the full city detail payload at the current chart resolution when SSE patches stop", "visible chart fallback must refresh the full city detail payload at the current chart resolution when SSE patches stop",
); );
assert(
chartLogicSource.includes("HOURLY_FORCE_REFRESH_DEDUP_MS") &&
chartLogicSource.includes("maxAgeMs: HOURLY_FORCE_REFRESH_DEDUP_MS") &&
chartLogicSource.includes("recentlyRefreshed.data"),
"forced chart detail refreshes should reuse a very recent full-detail payload instead of refetching repeatedly",
);
assert( assert(
__shouldPollLiveChartForTest({ city: "shanghai", compact: true, isActive: false, isMaximized: false }) === true, __shouldPollLiveChartForTest({ city: "shanghai", compact: true, isActive: false, isMaximized: false }) === true,
"compact grid slots are visible charts and should run the no-patch fallback guard", "compact grid slots are visible charts and should run the no-patch fallback guard",
@@ -353,6 +353,7 @@ type LocalDayBounds = { start: number; end: number };
const MAX_OBS_POINTS = 1440; const MAX_OBS_POINTS = 1440;
const HOURLY_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.metar; const HOURLY_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.metar;
const HOURLY_FORCE_REFRESH_DEDUP_MS = 60_000;
const _hourlyCache = new Map<string, { ts: number; data: HourlyForecast }>(); const _hourlyCache = new Map<string, { ts: number; data: HourlyForecast }>();
const _hourlyRequestCache = new Map<string, Promise<HourlyForecast>>(); const _hourlyRequestCache = new Map<string, Promise<HourlyForecast>>();
const MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS = 3; const MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS = 3;
@@ -366,13 +367,16 @@ const SESSION_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.metar;
type HourlyCacheEntry = { ts: number; data: HourlyForecast }; type HourlyCacheEntry = { ts: number; data: HourlyForecast };
function isFreshHourlyCacheEntry(entry: HourlyCacheEntry | null | undefined) { function isFreshHourlyCacheEntry(
return Boolean(entry && Date.now() - Number(entry.ts || 0) < SESSION_CACHE_TTL_MS); entry: HourlyCacheEntry | null | undefined,
maxAgeMs = SESSION_CACHE_TTL_MS,
) {
return Boolean(entry && Date.now() - Number(entry.ts || 0) < maxAgeMs);
} }
function readSessionCache( function readSessionCache(
city: string, city: string,
options: { allowStale?: boolean } = {}, options: { allowStale?: boolean; maxAgeMs?: number } = {},
): HourlyCacheEntry | null { ): HourlyCacheEntry | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
try { try {
@@ -382,7 +386,7 @@ function readSessionCache(
if ( if (
item && item &&
item.ts && item.ts &&
(options.allowStale || Date.now() - item.ts < SESSION_CACHE_TTL_MS) (options.allowStale || Date.now() - item.ts < (options.maxAgeMs ?? SESSION_CACHE_TTL_MS))
) { ) {
return item; return item;
} }
@@ -392,10 +396,10 @@ function readSessionCache(
function readHourlyCacheEntry( function readHourlyCacheEntry(
cacheKey: string, cacheKey: string,
options: { allowStale?: boolean } = {}, options: { allowStale?: boolean; maxAgeMs?: number } = {},
): HourlyCacheEntry | null { ): HourlyCacheEntry | null {
const cached = _hourlyCache.get(cacheKey); const cached = _hourlyCache.get(cacheKey);
if (cached && (options.allowStale || isFreshHourlyCacheEntry(cached))) { if (cached && (options.allowStale || isFreshHourlyCacheEntry(cached, options.maxAgeMs))) {
return cached; return cached;
} }
@@ -1193,6 +1197,13 @@ async function fetchHourlyForecastForCity(
if (cached) { if (cached) {
return cached.data; return cached.data;
} }
} else {
const recentlyRefreshed = readHourlyCacheEntry(cacheKey, {
maxAgeMs: HOURLY_FORCE_REFRESH_DEDUP_MS,
});
if (recentlyRefreshed) {
return recentlyRefreshed.data;
}
} }
const requestKey = options.ignoreCache ? `${city}:${resParam}:live` : `${city}:${resParam}`; const requestKey = options.ignoreCache ? `${city}:${resParam}:live` : `${city}:${resParam}`;
@@ -2459,6 +2470,7 @@ export {
MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS, MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
HOURLY_DETAIL_REQUEST_TIMEOUT_MS, HOURLY_DETAIL_REQUEST_TIMEOUT_MS,
HOURLY_CACHE_TTL_MS, HOURLY_CACHE_TTL_MS,
HOURLY_FORCE_REFRESH_DEDUP_MS,
_hourlyCache, _hourlyCache,
__readHourlyCacheEntryForTest, __readHourlyCacheEntryForTest,
resolveCityDetailFromBatch as __resolveCityDetailFromBatchForTest, resolveCityDetailFromBatch as __resolveCityDetailFromBatchForTest,
+19 -1
View File
@@ -28,4 +28,22 @@ export function buildForceRefreshProxyCachePolicy(
}; };
} }
export const buildCityDetailProxyCachePolicy = buildForceRefreshProxyCachePolicy; export function buildCityDetailProxyCachePolicy(
forceRefresh: string | null | undefined,
revalidateSeconds = 15,
): ProxyCachePolicy {
if (isForceRefreshValue(forceRefresh)) {
return {
fetchMode: "no-store",
responseCacheControl: "no-store, max-age=0",
};
}
return {
fetchMode: "revalidate",
responseCacheControl: `public, max-age=${revalidateSeconds}, s-maxage=${revalidateSeconds}, stale-while-revalidate=${Math.max(
revalidateSeconds * 3,
30,
)}`,
revalidateSeconds,
};
}