From ace43b39a9efa13ee55f30b90356c502bdd59fe9 Mon Sep 17 00:00:00 2001 From: Nezumi-2711 Date: Mon, 21 Sep 2026 10:11:50 +0700 Subject: [PATCH] fix: cache query status --- src/client/queries/status.ts | 3 ++- src/worker/routes/status.ts | 21 ++++++++++++++++++--- test/status.spec.ts | 12 ++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/client/queries/status.ts b/src/client/queries/status.ts index 3b7d16d..8dafcbd 100644 --- a/src/client/queries/status.ts +++ b/src/client/queries/status.ts @@ -12,7 +12,8 @@ export function useStatusQuery() { queryKey: statusKeys.all, queryFn: ({ signal }) => getStatus(signal), refetchInterval: 60_000, - refetchIntervalInBackground: false, + refetchIntervalInBackground: true, + refetchOnWindowFocus: true, }); } diff --git a/src/worker/routes/status.ts b/src/worker/routes/status.ts index 8fa4397..2506934 100644 --- a/src/worker/routes/status.ts +++ b/src/worker/routes/status.ts @@ -9,6 +9,7 @@ import { resolveFavicon } from './monitors'; const DAY_MS = 24 * 60 * 60 * 1000; const FAVICON_CACHE_SECONDS = 86_400; +const STATUS_CACHE_TIME_HEADER = 'X-Upwatch-Status-Cache-Time'; type ServiceStatus = 'up' | 'degraded' | 'down' | 'unknown' | 'maintenance'; type OverallStatus = 'operational' | 'degraded' | 'down'; type DailyAggregate = { monitorId: number; day: Date; totalChecks: number; upChecks: number }; @@ -134,20 +135,34 @@ function statusCacheKey(context: Context<{ Bindings: Env }>): Request { * STATUS_CACHE_SECONDS env var to 0 to disable. */ async function cachedStatusResponse(context: Context<{ Bindings: Env }>): Promise { - if (resolveStatusCacheSeconds(context.env) <= 0) return undefined; + const maxAgeSeconds = resolveStatusCacheSeconds(context.env); + if (maxAgeSeconds <= 0) return undefined; const cache = edgeCache(); if (!cache) return undefined; try { - return await cache.match(statusCacheKey(context)); + const cached = await cache.match(statusCacheKey(context)); + if (!cached) return undefined; + if (!isStatusCacheFresh(cached, maxAgeSeconds)) return undefined; + return cached; } catch { return undefined; } } +export function isStatusCacheFresh(response: Response, maxAgeSeconds: number, now = Date.now()): boolean { + const cachedAt = Number(response.headers.get(STATUS_CACHE_TIME_HEADER)); + return Number.isFinite(cachedAt) && now - cachedAt < maxAgeSeconds * 1_000; +} + function jsonWithEdgeCache(context: Context<{ Bindings: Env }>, body: unknown): Response { const seconds = resolveStatusCacheSeconds(context.env); if (seconds <= 0) return Response.json(body); - const response = Response.json(body, { headers: { 'Cache-Control': `public, max-age=${seconds}` } }); + const response = Response.json(body, { + headers: { + 'Cache-Control': `public, max-age=${seconds}`, + [STATUS_CACHE_TIME_HEADER]: String(Date.now()), + }, + }); const cache = edgeCache(); if (cache) { try { diff --git a/test/status.spec.ts b/test/status.spec.ts index cfa3262..1dfe346 100644 --- a/test/status.spec.ts +++ b/test/status.spec.ts @@ -2,6 +2,7 @@ import { applyD1Migrations, type D1Migration } from 'cloudflare:test'; import { env, exports as worker } from 'cloudflare:workers'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { DEGRADED_MESSAGE } from '../src/worker/ai/fallback-message'; +import { isStatusCacheFresh } from '../src/worker/routes/status'; const DAY_MS = 24 * 60 * 60 * 1000; @@ -143,6 +144,17 @@ describe('public status API', () => { } }); + it('only serves status cache entries within the configured TTL', () => { + const now = 1_000_000; + const fresh = new Response(null, { headers: { 'X-Upwatch-Status-Cache-Time': String(now - 59_999) } }); + const expired = new Response(null, { headers: { 'X-Upwatch-Status-Cache-Time': String(now - 60_000) } }); + const legacy = new Response(); + + expect(isStatusCacheFresh(fresh, 60, now)).toBe(true); + expect(isStatusCacheFresh(expired, 60, now)).toBe(false); + expect(isStatusCacheFresh(legacy, 60, now)).toBe(false); + }); + it('keeps a service operational while failures are unconfirmed', async () => { await insertMonitor({ name: 'Flaky API',