diff options
author | Alan Pearce | 2024-04-11 10:29:48 +0200 |
---|---|---|
committer | Alan Pearce | 2024-04-11 10:29:48 +0200 |
commit | 2f11a7ab0513891dc1eb11f7abfadf0f7a6972e1 (patch) | |
tree | 48d74a97f1fc801002edd2ffe5cfea6050caa891 /src/app.ts | |
parent | 8beec515146fc8cc323d9de722a405372801e1df (diff) | |
download | website-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.ts | 28 |
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 + "/")) { |