about summary refs log tree commit diff stats
path: root/src/app.ts
diff options
context:
space:
mode:
authorAlan Pearce2024-04-11 10:29:48 +0200
committerAlan Pearce2024-04-11 10:29:48 +0200
commit2f11a7ab0513891dc1eb11f7abfadf0f7a6972e1 (patch)
tree48d74a97f1fc801002edd2ffe5cfea6050caa891 /src/app.ts
parent8beec515146fc8cc323d9de722a405372801e1df (diff)
downloadwebsite-2f11a7ab0513891dc1eb11f7abfadf0f7a6972e1.tar.lz
website-2f11a7ab0513891dc1eb11f7abfadf0f7a6972e1.tar.zst
website-2f11a7ab0513891dc1eb11f7abfadf0f7a6972e1.zip
Enable ETag-based browser caching
Diffstat (limited to 'src/app.ts')
-rw-r--r--src/app.ts28
1 files changed, 21 insertions, 7 deletions
diff --git a/src/app.ts b/src/app.ts
index a656ca5..d3a676f 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -38,6 +38,7 @@ type File = {
   type: string;
   size: number;
   mtime: Date;
+  etag: string;
 };
 
 const metrics = {
@@ -50,6 +51,7 @@ const metrics = {
       "hostname",
       "method",
       "content_encoding",
+      "cache_basis",
     ] as const,
   }),
   requestDuration: new prom.Histogram({
@@ -61,18 +63,25 @@ const metrics = {
 
 let files = new Map<string, File>();
 
-function registerFile(
+async function hashFile(file: BunFile): Promise<string> {
+  return new Bun.CryptoHasher("sha256")
+    .update(await file.arrayBuffer())
+    .digest("base64");
+}
+
+async function registerFile(
   path: string,
   pathname: string,
   filename: string,
   stat: Stats,
-): void {
+): Promise<void> {
   pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname);
 
   if (files.get(pathname) !== undefined) {
     log.warn("File already registered:", pathname);
   }
   const handle = Bun.file(filename);
+
   files.set(pathname, {
     filename,
     relPath: "/" + path,
@@ -84,6 +93,7 @@ function registerFile(
         : undefined,
     size: stat.size,
     mtime: stat.mtime,
+    etag: `W/"${await hashFile(handle)}"`,
   });
 }
 
@@ -94,9 +104,9 @@ async function walkDirectory(root: string) {
     if (stat.isFile()) {
       if (relPath.includes("index.html")) {
         const dir = relPath.replace("index.html", "");
-        registerFile(relPath, dir, absPath, stat);
+        await registerFile(relPath, dir, absPath, stat);
       } else {
-        registerFile(relPath, relPath, absPath, stat);
+        await registerFile(relPath, relPath, absPath, stat);
       }
     }
   }
@@ -180,17 +190,19 @@ export const server = {
       let contentEncoding = "identity";
       let suffix = "";
       if (file && (await file.handle.exists())) {
-        if (
+        let etagMatch = request.headers.get("if-none-match") === file.etag;
+        let mtimeMatch =
           parseIfModifiedSinceHeader(
             request.headers.get("if-modified-since"),
-          ) >= file?.mtime.getTime()
-        ) {
+          ) >= file?.mtime.getTime();
+        if (etagMatch || mtimeMatch) {
           metrics.requests.inc({
             method: request.method,
             hostname,
             content_encoding: contentEncoding,
             path: pathname,
             status_code: (status = 304),
+            cache_basis: etagMatch ? "etag" : "mtime",
           });
           transaction.setHttpStatus(304);
           return new Response("", { status: status, headers: defaultHeaders });
@@ -225,6 +237,8 @@ export const server = {
         return serveFile(endFile, status, {
           "content-encoding": contentEncoding,
           "content-type": file.type,
+          // weak etags can be used for multiple equivalent representations
+          etag: file.etag,
         });
       } else {
         if (files.has(pathname + "/")) {