From 2f8e19509f105949484b14a54632a6511876529e Mon Sep 17 00:00:00 2001 From: olaven Date: Mon, 29 Sep 2025 21:52:10 +0200 Subject: [PATCH] Add caching to feed endpoint In order to reduce load on Deno KV as the costs are relatively high. --- lib/utils.test.ts | 40 +++++++++++++++++++++++++++++++++- lib/utils.ts | 6 +++++ routes/api/feeds/[seriesId].ts | 26 ++++++++++++++++++---- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/lib/utils.test.ts b/lib/utils.test.ts index 903ee45..aa08f83 100644 --- a/lib/utils.test.ts +++ b/lib/utils.test.ts @@ -1,5 +1,5 @@ import { assertEquals, assertNotEquals } from "$std/assert/mod.ts"; -import { responseJSON, responseXML } from "./utils.ts"; +import { responseJSON, responseXML, withExpiry } from "./utils.ts"; Deno.test("JSON response is stringified", async () => { const body = { message: "Hello, World!" }; @@ -14,3 +14,41 @@ Deno.test("XML resopnse is _not_ stringified", async () => { assertEquals(responseBody, body); assertNotEquals(responseBody, JSON.stringify(body)); }); + +Deno.test( + "Response with cache control returns a response with the correct header", + () => { + const response = responseJSON({ message: "Hello, World!" }, 200); + const cachedResponse = withExpiry(response, 3600); + assertEquals(cachedResponse.headers.get("Cache-Control"), "max-age=3600"); + }, +); + +Deno.test( + "Response with cache control returns the same body as the original", + async () => { + const body = { message: "Hello, World!" }; + const response = responseJSON(body, 200); + const cachedResponse = withExpiry(response, 3600); + assertEquals(await cachedResponse.text(), await response.text()); + }, +); + +Deno.test("Response with cache control does not modify the original", () => { + const response = responseJSON({ message: "Hello, World!" }, 200); + withExpiry(response, 3600); + assertEquals(response.headers.get("Cache-Control"), null); +}); + +Deno.test( + "Response with cache control returns the correct number of seconds", + () => { + const response = responseJSON({ message: "Hello, World!" }, 200); + const ttlInSeconds = 1234; + const cachedResponse = withExpiry(response, ttlInSeconds); + assertEquals( + cachedResponse.headers.get("Cache-Control"), + `max-age=${ttlInSeconds}`, + ); + }, +); diff --git a/lib/utils.ts b/lib/utils.ts index 55e6d6a..a31a213 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -24,6 +24,12 @@ export function responseXML(body: string, status: Status) { return response(body, status, "xml"); } +export const withExpiry = (response: Response, ttlInSeconds: number) => { + const clonedResponse = response.clone(); + clonedResponse.headers.set("Cache-Control", `max-age=${ttlInSeconds}`); + return clonedResponse; +}; + function response(body: string, status: number, type: "json" | "xml") { return new Response(body, { status, diff --git a/routes/api/feeds/[seriesId].ts b/routes/api/feeds/[seriesId].ts index d480386..358822c 100644 --- a/routes/api/feeds/[seriesId].ts +++ b/routes/api/feeds/[seriesId].ts @@ -1,18 +1,36 @@ import { FreshContext, STATUS_CODE } from "$fresh/server.ts"; import { caching } from "../../../lib/caching.ts"; import { rss } from "../../../lib/rss.ts"; -import { responseJSON, responseXML } from "../../../lib/utils.ts"; +import { responseJSON, responseXML, withExpiry } from "../../../lib/utils.ts"; -export const handler = async (_req: Request, ctx: FreshContext): Promise => { +const FEED_CACHE_KEY = "feeds-cache"; + +export const handler = async ( + request: Request, + ctx: FreshContext, +): Promise => { const seriesId = ctx.params.seriesId; + const cache = await caches.open(FEED_CACHE_KEY); + const cachedResponse = await cache.match(request); + if (cachedResponse) { + console.log(`Cache hit for feed ${seriesId}`); + return cachedResponse; + } + const series = await caching.getSeries({ id: seriesId }); if (!series) { return responseJSON({ message: "Series not found" }, STATUS_CODE.NotFound); } + const feed = rss.assembleFeed(series); - const xml = responseXML(feed, STATUS_CODE.OK); + const response = withExpiry( + responseXML(feed, STATUS_CODE.OK), + // 2 hours + 60 * 60 * 2, + ); - return xml; + await cache.put(request, response.clone()); + return response; };