fix: optimize pricing for deploying

This commit is contained in:
2026-09-21 10:56:08 +07:00
parent ace43b39a9
commit 23cffe9d17
4 changed files with 31 additions and 10 deletions
+3 -3
View File
@@ -40,13 +40,13 @@ export type PublicStatus = {
}; };
export function getStatus(signal?: AbortSignal) { export function getStatus(signal?: AbortSignal) {
return getJson<PublicStatus>('/api/status', { signal }); return getJson<PublicStatus>('/api/status', { signal, cache: 'no-store' });
} }
export function getIncidentHistory(signal?: AbortSignal) { export function getIncidentHistory(signal?: AbortSignal) {
return getJson<{ incidents: PublicIncident[] }>('/api/status/incidents', { signal }); return getJson<{ incidents: PublicIncident[] }>('/api/status/incidents', { signal, cache: 'no-store' });
} }
export function getPublicIncident(id: number, signal?: AbortSignal) { export function getPublicIncident(id: number, signal?: AbortSignal) {
return getJson<{ incident: PublicIncident }>(`/api/status/incidents/${id}`, { signal }); return getJson<{ incident: PublicIncident }>(`/api/status/incidents/${id}`, { signal, cache: 'no-store' });
} }
+3 -2
View File
@@ -12,7 +12,7 @@ export function useStatusQuery() {
queryKey: statusKeys.all, queryKey: statusKeys.all,
queryFn: ({ signal }) => getStatus(signal), queryFn: ({ signal }) => getStatus(signal),
refetchInterval: 60_000, refetchInterval: 60_000,
refetchIntervalInBackground: true, refetchIntervalInBackground: false,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}); });
} }
@@ -21,8 +21,9 @@ export function useIncidentHistoryQuery() {
return useQuery({ return useQuery({
queryKey: statusKeys.history, queryKey: statusKeys.history,
queryFn: ({ signal }) => getIncidentHistory(signal), queryFn: ({ signal }) => getIncidentHistory(signal),
refetchInterval: 120_000, refetchInterval: 300_000,
refetchIntervalInBackground: false, refetchIntervalInBackground: false,
refetchOnWindowFocus: true,
}); });
} }
+21 -5
View File
@@ -10,6 +10,11 @@ import { resolveFavicon } from './monitors';
const DAY_MS = 24 * 60 * 60 * 1000; const DAY_MS = 24 * 60 * 60 * 1000;
const FAVICON_CACHE_SECONDS = 86_400; const FAVICON_CACHE_SECONDS = 86_400;
const STATUS_CACHE_TIME_HEADER = 'X-Upwatch-Status-Cache-Time'; const STATUS_CACHE_TIME_HEADER = 'X-Upwatch-Status-Cache-Time';
const STATUS_NO_STORE_HEADERS = {
'Cache-Control': 'no-store',
'CDN-Cache-Control': 'no-store',
'Cloudflare-CDN-Cache-Control': 'no-store',
};
type ServiceStatus = 'up' | 'degraded' | 'down' | 'unknown' | 'maintenance'; type ServiceStatus = 'up' | 'degraded' | 'down' | 'unknown' | 'maintenance';
type OverallStatus = 'operational' | 'degraded' | 'down'; type OverallStatus = 'operational' | 'degraded' | 'down';
type DailyAggregate = { monitorId: number; day: Date; totalChecks: number; upChecks: number }; type DailyAggregate = { monitorId: number; day: Date; totalChecks: number; upChecks: number };
@@ -143,7 +148,7 @@ async function cachedStatusResponse(context: Context<{ Bindings: Env }>): Promis
const cached = await cache.match(statusCacheKey(context)); const cached = await cache.match(statusCacheKey(context));
if (!cached) return undefined; if (!cached) return undefined;
if (!isStatusCacheFresh(cached, maxAgeSeconds)) return undefined; if (!isStatusCacheFresh(cached, maxAgeSeconds)) return undefined;
return cached; return statusResponse(cached);
} catch { } catch {
return undefined; return undefined;
} }
@@ -154,10 +159,21 @@ export function isStatusCacheFresh(response: Response, maxAgeSeconds: number, no
return Number.isFinite(cachedAt) && now - cachedAt < maxAgeSeconds * 1_000; return Number.isFinite(cachedAt) && now - cachedAt < maxAgeSeconds * 1_000;
} }
function statusResponse(response: Response): Response {
const headers = new Headers(response.headers);
for (const [name, value] of Object.entries(STATUS_NO_STORE_HEADERS)) headers.set(name, value);
headers.delete(STATUS_CACHE_TIME_HEADER);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
function jsonWithEdgeCache(context: Context<{ Bindings: Env }>, body: unknown): Response { function jsonWithEdgeCache(context: Context<{ Bindings: Env }>, body: unknown): Response {
const seconds = resolveStatusCacheSeconds(context.env); const seconds = resolveStatusCacheSeconds(context.env);
if (seconds <= 0) return Response.json(body); if (seconds <= 0) return Response.json(body, { headers: STATUS_NO_STORE_HEADERS });
const response = Response.json(body, { const cachedResponse = Response.json(body, {
headers: { headers: {
'Cache-Control': `public, max-age=${seconds}`, 'Cache-Control': `public, max-age=${seconds}`,
[STATUS_CACHE_TIME_HEADER]: String(Date.now()), [STATUS_CACHE_TIME_HEADER]: String(Date.now()),
@@ -166,12 +182,12 @@ function jsonWithEdgeCache(context: Context<{ Bindings: Env }>, body: unknown):
const cache = edgeCache(); const cache = edgeCache();
if (cache) { if (cache) {
try { try {
context.executionCtx.waitUntil(cache.put(statusCacheKey(context), response.clone()).catch(() => undefined)); context.executionCtx.waitUntil(cache.put(statusCacheKey(context), cachedResponse).catch(() => undefined));
} catch { } catch {
// No ExecutionContext available (e.g. unit tests): serve without populating the edge cache. // No ExecutionContext available (e.g. unit tests): serve without populating the edge cache.
} }
} }
return response; return Response.json(body, { headers: STATUS_NO_STORE_HEADERS });
} }
const statusRoutes = new Hono<{ Bindings: Env }>(); const statusRoutes = new Hono<{ Bindings: Env }>();
+4
View File
@@ -119,6 +119,10 @@ describe('public status API', () => {
const body = await response.json<PublicStatusResponse>(); const body = await response.json<PublicStatusResponse>();
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get('Cache-Control')).toBe('no-store');
expect(response.headers.get('CDN-Cache-Control')).toBe('no-store');
expect(response.headers.get('Cloudflare-CDN-Cache-Control')).toBe('no-store');
expect(response.headers.get('X-Upwatch-Status-Cache-Time')).toBeNull();
expect(body.overall).toBe('operational'); expect(body.overall).toBe('operational');
expect(body.updatedAt).toEqual(expect.any(Number)); expect(body.updatedAt).toEqual(expect.any(Number));
expect(body.services).toHaveLength(1); expect(body.services).toHaveLength(1);