Skip to content

Commit f179e03

Browse files
authored
perf(cache): CF edge caching + eliminate request storms (~25M/week saved) (koala73#2829)
* perf(cache): add CF edge caching, eliminate request storms Three changes to reduce origin request volume: 1. Gateway cache tiers now include `public, s-maxage` so Cloudflare can cache API responses at edge (previously browser-only). Bumped 27 slow-seeded endpoints to appropriate tiers (static->daily for 6h+ seeds, slow->static for 2h seeds). 2. Population exposure: moved computation client-side. The server handler is pure math on 20 hardcoded countries, no reason for network calls. Eliminates ~17.7M requests/week (20 calls per page load -> 0). 3. Consumer prices: wrapped fetchAllMarketsOverview in a circuit breaker so the combined 8-market result is cached as a unit. Returning visitors within 30min hit localStorage instead of firing 8 separate API calls. * test: update shipping-rates tier assertion (static -> daily) * test: update cache tier assertions for three-tier caching design * fix(security): force slow-browser tier for premium endpoints Premium endpoints (PREMIUM_RPC_PATHS + ENDPOINT_ENTITLEMENTS) must not get public s-maxage headers. CF would cache authenticated responses and serve them without re-running auth/entitlement checks. Force these to slow-browser tier (browser-only max-age, no public/s-maxage). * fix(security): add list-market-implications to PREMIUM_RPC_PATHS PRO-only panel endpoint was missing from premium paths, allowing CF edge caching to serve authenticated responses to unauthenticated users. * chore: disable deduct-situation panel and endpoint Panel set to enabled:false in panels.ts, server handler returns early with provider:'disabled'. Code preserved for re-enabling later. * fix(security): suppress CDN-Cache-Control for premium endpoints too P1: slow-browser tier still had CDN-Cache-Control with public s-maxage, letting Vercel CDN cache premium responses for same-origin requests. Now CDN caching is fully disabled for premium endpoints. P2: revert server-side deduct-situation disable. Keep backend intact so the published API and correlation engine enrichment still work. Only the panel is disabled (enabled:false in panels.ts).
1 parent 3c10106 commit f179e03

8 files changed

Lines changed: 133 additions & 119 deletions

File tree

server/gateway.ts

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,16 @@ export const serverOptions: ServerOptions = { onError: mapErrorToResponse };
2828

2929
type CacheTier = 'fast' | 'medium' | 'slow' | 'slow-browser' | 'static' | 'daily' | 'no-store';
3030

31-
// Browser-only cache: no `public` or `s-maxage` so Cloudflare (which ignores
32-
// Vary: Origin) does NOT cache these responses. CF sits in front of api.worldmonitor.app
33-
// and would otherwise pin ACAO: worldmonitor.app on the cached response, breaking CORS
34-
// for preview deployments. Vercel CDN caching is handled separately by CDN-Cache-Control.
31+
// Three-tier caching: browser (max-age) → CF edge (s-maxage) → Vercel CDN (CDN-Cache-Control).
32+
// CF ignores Vary: Origin so it may pin a single ACAO value, but this is acceptable
33+
// since production traffic is same-origin and preview deployments hit Vercel CDN directly.
3534
const TIER_HEADERS: Record<CacheTier, string> = {
36-
fast: 'max-age=60, stale-while-revalidate=60, stale-if-error=600',
37-
medium: 'max-age=120, stale-while-revalidate=120, stale-if-error=900',
38-
slow: 'max-age=300, stale-while-revalidate=300, stale-if-error=3600',
35+
fast: 'public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=600',
36+
medium: 'public, max-age=120, s-maxage=600, stale-while-revalidate=120, stale-if-error=900',
37+
slow: 'public, max-age=300, s-maxage=1800, stale-while-revalidate=300, stale-if-error=3600',
3938
'slow-browser': 'max-age=300, stale-while-revalidate=60, stale-if-error=1800',
40-
static: 'max-age=600, stale-while-revalidate=600, stale-if-error=14400',
41-
daily: 'max-age=3600, stale-while-revalidate=7200, stale-if-error=172800',
39+
static: 'public, max-age=600, s-maxage=3600, stale-while-revalidate=600, stale-if-error=14400',
40+
daily: 'public, max-age=3600, s-maxage=14400, stale-while-revalidate=7200, stale-if-error=172800',
4241
'no-store': 'no-store',
4342
};
4443

@@ -80,7 +79,7 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
8079
'/api/infrastructure/v1/list-internet-traffic-anomalies': 'slow',
8180

8281
'/api/unrest/v1/list-unrest-events': 'slow',
83-
'/api/cyber/v1/list-cyber-threats': 'slow',
82+
'/api/cyber/v1/list-cyber-threats': 'static',
8483
'/api/conflict/v1/list-acled-events': 'slow',
8584
'/api/military/v1/get-theater-posture': 'slow',
8685
'/api/infrastructure/v1/get-temporal-baseline': 'slow',
@@ -99,7 +98,7 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
9998
'/api/natural/v1/list-natural-events': 'slow',
10099
'/api/wildfire/v1/list-fire-detections': 'static',
101100
'/api/maritime/v1/list-navigational-warnings': 'static',
102-
'/api/supply-chain/v1/get-shipping-rates': 'static',
101+
'/api/supply-chain/v1/get-shipping-rates': 'daily',
103102
'/api/economic/v1/get-fred-series': 'static',
104103
'/api/economic/v1/get-bls-series': 'daily',
105104
'/api/economic/v1/get-energy-prices': 'static',
@@ -108,41 +107,41 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
108107
'/api/giving/v1/get-giving-summary': 'static',
109108
'/api/intelligence/v1/get-country-intel-brief': 'static',
110109
'/api/intelligence/v1/get-gdelt-topic-timeline': 'medium',
111-
'/api/climate/v1/list-climate-anomalies': 'static',
112-
'/api/climate/v1/list-climate-disasters': 'static',
113-
'/api/climate/v1/get-co2-monitoring': 'static',
114-
'/api/climate/v1/get-ocean-ice-data': 'static',
110+
'/api/climate/v1/list-climate-anomalies': 'daily',
111+
'/api/climate/v1/list-climate-disasters': 'daily',
112+
'/api/climate/v1/get-co2-monitoring': 'daily',
113+
'/api/climate/v1/get-ocean-ice-data': 'daily',
115114
'/api/climate/v1/list-air-quality-data': 'fast',
116115
'/api/climate/v1/list-climate-news': 'slow',
117-
'/api/sanctions/v1/list-sanctions-pressure': 'static',
116+
'/api/sanctions/v1/list-sanctions-pressure': 'daily',
118117
'/api/sanctions/v1/lookup-sanction-entity': 'no-store',
119118
'/api/radiation/v1/list-radiation-observations': 'slow',
120119
'/api/thermal/v1/list-thermal-escalations': 'slow',
121-
'/api/research/v1/list-tech-events': 'static',
122-
'/api/military/v1/get-usni-fleet-report': 'static',
120+
'/api/research/v1/list-tech-events': 'daily',
121+
'/api/military/v1/get-usni-fleet-report': 'daily',
123122
'/api/military/v1/list-defense-patents': 'daily',
124-
'/api/conflict/v1/list-ucdp-events': 'static',
125-
'/api/conflict/v1/get-humanitarian-summary': 'static',
123+
'/api/conflict/v1/list-ucdp-events': 'daily',
124+
'/api/conflict/v1/get-humanitarian-summary': 'daily',
126125
'/api/conflict/v1/list-iran-events': 'slow',
127-
'/api/displacement/v1/get-displacement-summary': 'static',
128-
'/api/displacement/v1/get-population-exposure': 'static',
129-
'/api/economic/v1/get-bis-policy-rates': 'static',
130-
'/api/economic/v1/get-bis-exchange-rates': 'static',
131-
'/api/economic/v1/get-bis-credit': 'static',
132-
'/api/trade/v1/get-tariff-trends': 'static',
133-
'/api/trade/v1/get-trade-flows': 'static',
134-
'/api/trade/v1/get-trade-barriers': 'static',
135-
'/api/trade/v1/get-trade-restrictions': 'static',
136-
'/api/trade/v1/get-customs-revenue': 'static',
137-
'/api/trade/v1/list-comtrade-flows': 'static',
138-
'/api/economic/v1/list-world-bank-indicators': 'static',
139-
'/api/economic/v1/get-energy-capacity': 'static',
140-
'/api/economic/v1/list-grocery-basket-prices': 'static',
141-
'/api/economic/v1/list-bigmac-prices': 'static',
142-
'/api/economic/v1/list-fuel-prices': 'static',
143-
'/api/economic/v1/get-fao-food-price-index': 'static',
144-
'/api/economic/v1/get-crude-inventories': 'static',
145-
'/api/economic/v1/get-nat-gas-storage': 'static',
126+
'/api/displacement/v1/get-displacement-summary': 'daily',
127+
'/api/displacement/v1/get-population-exposure': 'daily',
128+
'/api/economic/v1/get-bis-policy-rates': 'daily',
129+
'/api/economic/v1/get-bis-exchange-rates': 'daily',
130+
'/api/economic/v1/get-bis-credit': 'daily',
131+
'/api/trade/v1/get-tariff-trends': 'daily',
132+
'/api/trade/v1/get-trade-flows': 'daily',
133+
'/api/trade/v1/get-trade-barriers': 'daily',
134+
'/api/trade/v1/get-trade-restrictions': 'daily',
135+
'/api/trade/v1/get-customs-revenue': 'daily',
136+
'/api/trade/v1/list-comtrade-flows': 'daily',
137+
'/api/economic/v1/list-world-bank-indicators': 'daily',
138+
'/api/economic/v1/get-energy-capacity': 'daily',
139+
'/api/economic/v1/list-grocery-basket-prices': 'daily',
140+
'/api/economic/v1/list-bigmac-prices': 'daily',
141+
'/api/economic/v1/list-fuel-prices': 'daily',
142+
'/api/economic/v1/get-fao-food-price-index': 'daily',
143+
'/api/economic/v1/get-crude-inventories': 'daily',
144+
'/api/economic/v1/get-nat-gas-storage': 'daily',
146145
'/api/economic/v1/get-eu-yield-curve': 'daily',
147146
'/api/supply-chain/v1/get-critical-minerals': 'daily',
148147
'/api/military/v1/get-aircraft-details': 'static',
@@ -160,7 +159,7 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
160159
'/api/infrastructure/v1/get-cable-health': 'slow',
161160
'/api/positive-events/v1/list-positive-geo-events': 'slow',
162161

163-
'/api/military/v1/list-military-bases': 'static',
162+
'/api/military/v1/list-military-bases': 'daily',
164163
'/api/economic/v1/get-macro-signals': 'medium',
165164
'/api/economic/v1/get-national-debt': 'daily',
166165
'/api/prediction/v1/list-prediction-markets': 'medium',
@@ -171,7 +170,7 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
171170
'/api/news/v1/list-feed-digest': 'slow',
172171
'/api/intelligence/v1/get-country-facts': 'daily',
173172
'/api/intelligence/v1/list-security-advisories': 'slow',
174-
'/api/intelligence/v1/list-satellites': 'slow',
173+
'/api/intelligence/v1/list-satellites': 'static',
175174
'/api/intelligence/v1/list-gps-interference': 'slow',
176175
'/api/intelligence/v1/list-cross-source-signals': 'medium',
177176
'/api/intelligence/v1/list-oref-alerts': 'fast',
@@ -424,15 +423,17 @@ export function createDomainGateway(
424423
} else {
425424
const rpcName = pathname.split('/').pop() ?? '';
426425
const envOverride = process.env[`CACHE_TIER_OVERRIDE_${rpcName.replace(/-/g, '_').toUpperCase()}`] as CacheTier | undefined;
427-
const tier = (envOverride && envOverride in TIER_HEADERS ? envOverride : null) ?? RPC_CACHE_TIER[pathname] ?? 'medium';
426+
const isPremium = PREMIUM_RPC_PATHS.has(pathname) || getRequiredTier(pathname) !== null;
427+
const tier = isPremium ? 'slow-browser' as CacheTier
428+
: (envOverride && envOverride in TIER_HEADERS ? envOverride : null) ?? RPC_CACHE_TIER[pathname] ?? 'medium';
428429
mergedHeaders.set('Cache-Control', TIER_HEADERS[tier]);
429430
// Only allow Vercel CDN caching for trusted origins (worldmonitor.app, Vercel previews,
430431
// Tauri). No-origin server-side requests (external scrapers) must always reach the edge
431432
// function so the auth check in validateApiKey() can run. Without this guard, a cached
432433
// 200 from a trusted-origin browser request could be served to a no-origin scraper,
433434
// bypassing auth entirely.
434435
const reqOrigin = request.headers.get('origin') || '';
435-
const cdnCache = isAllowedOrigin(reqOrigin) ? TIER_CDN_CACHE[tier] : null;
436+
const cdnCache = !isPremium && isAllowedOrigin(reqOrigin) ? TIER_CDN_CACHE[tier] : null;
436437
if (cdnCache) mergedHeaders.set('CDN-Cache-Control', cdnCache);
437438
mergedHeaders.set('X-Cache-Tier', tier);
438439

src/config/panels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ const FULL_PANELS: Record<string, PanelConfig> = {
9898
'national-debt': { name: 'Global Debt Clock', enabled: true, priority: 2 },
9999
'cross-source-signals': { name: 'Cross-Source Signals', enabled: true, priority: 2 },
100100
'market-implications': { name: 'AI Market Implications', enabled: true, priority: 1, premium: 'locked' as const },
101-
'deduction': { name: 'Deduct Situation', enabled: true, priority: 1, premium: 'locked' as const },
101+
'deduction': { name: 'Deduct Situation', enabled: false, priority: 1, premium: 'locked' as const },
102102
'geo-hubs': { name: 'Geopolitical Hubs', enabled: false, priority: 2 },
103103
'tech-hubs': { name: 'Hot Tech Hubs', enabled: false, priority: 2 },
104104
};

src/services/consumer-prices/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,17 @@ export async function fetchConsumerPriceFreshness(
257257
}
258258
}
259259

260+
const allMarketsBreaker = createCircuitBreaker<GetConsumerPriceOverviewResponse[]>({
261+
name: 'All Markets Overview',
262+
cacheTtlMs: 30 * 60 * 1000,
263+
persistCache: true,
264+
});
265+
260266
export async function fetchAllMarketsOverview(): Promise<GetConsumerPriceOverviewResponse[]> {
261-
return Promise.all(
262-
SINGLE_MARKETS.map((m) => fetchConsumerPriceOverview(m.code, `essentials-${m.code}`)),
267+
return allMarketsBreaker.execute(
268+
() => Promise.all(
269+
SINGLE_MARKETS.map((m) => fetchConsumerPriceOverview(m.code, `essentials-${m.code}`)),
270+
),
271+
[],
263272
);
264273
}

src/services/population-exposure.ts

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,58 @@
1-
import { createCircuitBreaker } from '@/utils';
2-
import { getRpcBaseUrl } from '@/services/rpc-client';
3-
import type { CountryPopulation, PopulationExposure } from '@/types';
4-
import { DisplacementServiceClient } from '@/generated/client/worldmonitor/displacement/v1/service_client';
5-
import type { GetPopulationExposureResponse } from '@/generated/client/worldmonitor/displacement/v1/service_client';
1+
import type { PopulationExposure } from '@/types';
62

7-
const client = new DisplacementServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) });
3+
const PRIORITY_COUNTRIES: Record<string, { name: string; pop: number; area: number }> = {
4+
UKR: { name: 'Ukraine', pop: 37000000, area: 603550 },
5+
RUS: { name: 'Russia', pop: 144100000, area: 17098242 },
6+
ISR: { name: 'Israel', pop: 9800000, area: 22072 },
7+
PSE: { name: 'Palestine', pop: 5400000, area: 6020 },
8+
SYR: { name: 'Syria', pop: 22100000, area: 185180 },
9+
IRN: { name: 'Iran', pop: 88600000, area: 1648195 },
10+
TWN: { name: 'Taiwan', pop: 23600000, area: 36193 },
11+
ETH: { name: 'Ethiopia', pop: 126500000, area: 1104300 },
12+
SDN: { name: 'Sudan', pop: 48100000, area: 1861484 },
13+
SSD: { name: 'South Sudan', pop: 11400000, area: 619745 },
14+
SOM: { name: 'Somalia', pop: 18100000, area: 637657 },
15+
YEM: { name: 'Yemen', pop: 34400000, area: 527968 },
16+
AFG: { name: 'Afghanistan', pop: 42200000, area: 652230 },
17+
PAK: { name: 'Pakistan', pop: 240500000, area: 881913 },
18+
IND: { name: 'India', pop: 1428600000, area: 3287263 },
19+
MMR: { name: 'Myanmar', pop: 54200000, area: 676578 },
20+
COD: { name: 'DR Congo', pop: 102300000, area: 2344858 },
21+
NGA: { name: 'Nigeria', pop: 223800000, area: 923768 },
22+
MLI: { name: 'Mali', pop: 22600000, area: 1240192 },
23+
BFA: { name: 'Burkina Faso', pop: 22700000, area: 274200 },
24+
};
825

9-
const countriesBreaker = createCircuitBreaker<GetPopulationExposureResponse>({ name: 'WorldPop Countries', cacheTtlMs: 30 * 60 * 1000, persistCache: true });
26+
const EXPOSURE_CENTROIDS: Record<string, [number, number]> = {
27+
UKR: [48.4, 31.2], RUS: [61.5, 105.3], ISR: [31.0, 34.8], PSE: [31.9, 35.2],
28+
SYR: [35.0, 38.0], IRN: [32.4, 53.7], TWN: [23.7, 121.0], ETH: [9.1, 40.5],
29+
SDN: [15.5, 32.5], SSD: [6.9, 31.3], SOM: [5.2, 46.2], YEM: [15.6, 48.5],
30+
AFG: [33.9, 67.7], PAK: [30.4, 69.3], IND: [20.6, 79.0], MMR: [19.8, 96.7],
31+
COD: [-4.0, 21.8], NGA: [9.1, 7.5], MLI: [17.6, -4.0], BFA: [12.3, -1.6],
32+
};
1033

11-
const exposureBreaker = createCircuitBreaker<ExposureResponse | null>({
12-
name: 'PopExposure',
13-
cacheTtlMs: 6 * 60 * 60 * 1000,
14-
persistCache: true,
15-
maxCacheEntries: 256,
16-
});
34+
function computeExposure(lat: number, lon: number, radiusKm: number) {
35+
let bestMatch: string | null = null;
36+
let bestDist = Infinity;
1737

18-
export async function fetchCountryPopulations(): Promise<CountryPopulation[]> {
19-
const result = await countriesBreaker.execute(async () => {
20-
return client.getPopulationExposure({ mode: 'countries', lat: 0, lon: 0, radius: 0 });
21-
}, { success: false, countries: [] });
22-
23-
return result.countries;
24-
}
38+
for (const [code, [cLat, cLon]] of Object.entries(EXPOSURE_CENTROIDS)) {
39+
const dist = Math.sqrt((lat - cLat) ** 2 + (lon - cLon) ** 2);
40+
if (dist < bestDist) {
41+
bestDist = dist;
42+
bestMatch = code;
43+
}
44+
}
2545

26-
interface ExposureResponse {
27-
exposedPopulation: number;
28-
exposureRadiusKm: number;
29-
nearestCountry: string;
30-
densityPerKm2: number;
31-
}
46+
const info = bestMatch ? PRIORITY_COUNTRIES[bestMatch]! : { pop: 50_000_000, area: 500_000 };
47+
const density = info.pop / info.area;
48+
const areaKm2 = Math.PI * radiusKm * radiusKm;
3249

33-
export async function fetchExposure(lat: number, lon: number, radiusKm: number): Promise<ExposureResponse | null> {
34-
const cacheKey = `${lat.toFixed(1)},${lon.toFixed(1)},${radiusKm}`;
35-
return exposureBreaker.execute(
36-
async () => {
37-
const result = await client.getPopulationExposure({ mode: 'exposure', lat, lon, radius: radiusKm });
38-
return result.exposure ?? null;
39-
},
40-
null,
41-
{ cacheKey },
42-
);
50+
return {
51+
exposedPopulation: Math.round(density * areaKm2),
52+
exposureRadiusKm: radiusKm,
53+
nearestCountry: bestMatch || '',
54+
densityPerKm2: Math.round(density),
55+
};
4356
}
4457

4558
interface EventForExposure {
@@ -70,37 +83,24 @@ function getRadiusForEventType(type: string): number {
7083
}
7184
}
7285

73-
export async function enrichEventsWithExposure(
86+
export function enrichEventsWithExposure(
7487
events: EventForExposure[],
75-
): Promise<PopulationExposure[]> {
76-
const MAX_CONCURRENT = 10;
77-
const results: PopulationExposure[] = [];
78-
79-
for (let i = 0; i < events.length; i += MAX_CONCURRENT) {
80-
const batch = events.slice(i, i + MAX_CONCURRENT);
81-
const batchResults = await Promise.allSettled(
82-
batch.map(async (event) => {
83-
const radius = getRadiusForEventType(event.type);
84-
const exposure = await fetchExposure(event.lat, event.lon, radius);
85-
if (!exposure) return null;
86-
return {
87-
eventId: event.id,
88-
eventName: event.name,
89-
eventType: event.type,
90-
lat: event.lat,
91-
lon: event.lon,
92-
exposedPopulation: exposure.exposedPopulation,
93-
exposureRadiusKm: radius,
94-
} as PopulationExposure;
95-
})
96-
);
97-
98-
for (const r of batchResults) {
99-
if (r.status === 'fulfilled' && r.value) results.push(r.value);
100-
}
101-
}
102-
103-
return results.sort((a, b) => b.exposedPopulation - a.exposedPopulation);
88+
): PopulationExposure[] {
89+
return events
90+
.map((event) => {
91+
const radius = getRadiusForEventType(event.type);
92+
const exposure = computeExposure(event.lat, event.lon, radius);
93+
return {
94+
eventId: event.id,
95+
eventName: event.name,
96+
eventType: event.type,
97+
lat: event.lat,
98+
lon: event.lon,
99+
exposedPopulation: exposure.exposedPopulation,
100+
exposureRadiusKm: radius,
101+
} as PopulationExposure;
102+
})
103+
.sort((a, b) => b.exposedPopulation - a.exposedPopulation);
104104
}
105105

106106
export function formatPopulation(n: number): string {

src/shared/premium-paths.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const PREMIUM_RPC_PATHS = new Set<string>([
1010
'/api/market/v1/backtest-stock',
1111
'/api/market/v1/list-stored-stock-backtests',
1212
'/api/intelligence/v1/deduct-situation',
13+
'/api/intelligence/v1/list-market-implications',
1314
'/api/resilience/v1/get-resilience-score',
1415
'/api/resilience/v1/get-resilience-ranking',
1516
]);

tests/gateway-cdn-origin-policy.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,6 @@ describe('gateway CDN origin policy', () => {
109109
assert.equal(withKey.status, 200);
110110
assert.equal(withKey.headers.get('Access-Control-Allow-Origin'), 'https://worldmonitor.app');
111111
assert.equal(withKey.headers.get('Vary'), 'Origin');
112-
assert.match(withKey.headers.get('CDN-Cache-Control') ?? '', /s-maxage=/);
112+
assert.equal(withKey.headers.get('CDN-Cache-Control'), null, 'premium endpoints must NOT have CDN caching');
113113
});
114114
});

tests/route-cache-tier.test.mjs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,13 @@ describe('RPC_CACHE_TIER route parity', () => {
8585
);
8686
});
8787

88-
it('slow-browser tier includes max-age, slow tier does not', () => {
88+
it('slow tier includes public s-maxage for CF edge caching, slow-browser does not', () => {
8989
const gatewaySrc = readFileSync(join(root, 'server', 'gateway.ts'), 'utf-8');
90-
assert.match(gatewaySrc, /slow-browser.*max-age/s, 'slow-browser tier must include max-age');
91-
const slowLine = gatewaySrc.match(/^\s+slow: 'public.*'/m)?.[0] ?? '';
92-
assert.ok(!slowLine.includes('max-age'), 'slow tier must NOT include max-age');
90+
const slowLine = gatewaySrc.match(/^\s+slow: '.*'/m)?.[0] ?? '';
91+
assert.ok(slowLine.includes('public'), 'slow tier must include public for CF caching');
92+
assert.ok(slowLine.includes('s-maxage'), 'slow tier must include s-maxage for CF edge TTL');
93+
const slowBrowserLine = gatewaySrc.match(/^\s+'slow-browser': '.*'/m)?.[0] ?? '';
94+
assert.ok(!slowBrowserLine.includes('public'), 'slow-browser tier must NOT include public');
95+
assert.ok(!slowBrowserLine.includes('s-maxage'), 'slow-browser tier must NOT include s-maxage');
9396
});
9497
});

tests/supply-chain-v2.test.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,8 @@ describe('Gateway daily cache tier', () => {
342342
assert.match(src, /\/api\/supply-chain\/v1\/get-chokepoint-status':\s*'medium'/);
343343
});
344344

345-
it('shipping rates route still uses static tier', () => {
346-
assert.match(src, /\/api\/supply-chain\/v1\/get-shipping-rates':\s*'static'/);
345+
it('shipping rates route uses daily tier (24h seed interval)', () => {
346+
assert.match(src, /\/api\/supply-chain\/v1\/get-shipping-rates':\s*'daily'/);
347347
});
348348
});
349349

0 commit comments

Comments
 (0)