Skip to content

Commit d85c1e3

Browse files
committed
rides-v1: edge-cache the rollup endpoint (closed-window: 24h immutable)
Cold queries against `/api/rides-v1[/cells]` are ~3-4s in prod because each fans out to ~14 year-shards on R2 + decode + filter + stitch. Without an explicit cache, every request re-pays that cost (Workers don't auto-cache their own responses). Mirroring the `/api/totals` pattern (`index.ts:1232-1280`): - Check `caches.default` by URL on entry; HIT returns immediately. - After computing, set `Cache-Control`: 24h immutable when the query window ends ≥5min in the past (shards are frozen), 60s otherwise (covers cron lag + cascade-compactor write latency before the current month closes). - Stash in cache via `ctx.waitUntil` so the response doesn't block on the put. Both endpoints share the cache wrapper since they have identical request shape (just different output structure).
1 parent 678456e commit d85c1e3

1 file changed

Lines changed: 32 additions & 8 deletions

File tree

gbfs/api/src/index.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,19 +1166,43 @@ export default {
11661166
// selected via `?anchor=start|end` (default `start`); FE composes
11671167
// stacked start+end charts via two parallel requests. See
11681168
// `specs/rides-pyramid-v1.md` + `rides_v1.ts`.
1169-
if (url.pathname === '/api/rides-v1') {
1169+
if (url.pathname === '/api/rides-v1' || url.pathname === '/api/rides-v1/cells') {
1170+
// Edge cache: rides-v1 cold queries are O(seconds) since they
1171+
// fan out to ~14 year-shards on R2 + decode + filter + stitch.
1172+
// Mirroring the /api/totals pattern (`index.ts:1232-1280`):
1173+
// past-only windows are immutable → 24h cache; queries that
1174+
// touch the current month get 60s (cron lag + cascade write
1175+
// latency slack).
1176+
const cache = caches.default;
1177+
const cacheKey = new Request(url.toString(), { method: 'GET' });
1178+
const hit = await cache.match(cacheKey);
1179+
if (hit) {
1180+
const headers = new Headers(hit.headers);
1181+
headers.set('X-Cache', 'HIT');
1182+
return new Response(hit.body, { status: hit.status, headers });
1183+
}
1184+
let resp: Response;
11701185
try {
1171-
return await serveRidesV1(env.R2, request, env.CORS_ORIGIN ?? '*');
1186+
resp = url.pathname === '/api/rides-v1'
1187+
? await serveRidesV1(env.R2, request, env.CORS_ORIGIN ?? '*')
1188+
: await serveRidesV1Cells(env.R2, request, env.CORS_ORIGIN ?? '*');
11721189
} catch (err: any) {
11731190
return errorResponse(err.message ?? 'rides-v1 error', 500, env);
11741191
}
1175-
}
1176-
if (url.pathname === '/api/rides-v1/cells') {
1177-
try {
1178-
return await serveRidesV1Cells(env.R2, request, env.CORS_ORIGIN ?? '*');
1179-
} catch (err: any) {
1180-
return errorResponse(err.message ?? 'rides-v1/cells error', 500, env);
1192+
if (resp.ok) {
1193+
// `to` exclusive — past-only iff `to` ≤ now − 5min (cron slack).
1194+
const toRaw = url.searchParams.get('to');
1195+
const toMs = toRaw ? Date.parse(toRaw) : NaN;
1196+
const isPast = Number.isFinite(toMs) && toMs <= Date.now() - 300_000;
1197+
const cacheControl = isPast
1198+
? 'public, max-age=86400, immutable'
1199+
: 'public, max-age=60';
1200+
const headers = new Headers(resp.headers);
1201+
headers.set('Cache-Control', cacheControl);
1202+
resp = new Response(resp.body, { status: resp.status, headers });
1203+
ctx.waitUntil(cache.put(cacheKey, resp.clone()));
11811204
}
1205+
return resp;
11821206
}
11831207

11841208
// /api/query — unified multi-scale time-series (trips + availability).

0 commit comments

Comments
 (0)