Icons are cached at the edge
An icon's address is its content's hash, so each data centre keeps it after the first view: storage is read about once per place, not once per visitor.
1 file+25−130/1 viewed
| 25 | 25 | }; | |
| 26 | 26 | ||
| 27 | 27 | export default { | |
| 28 | − | async fetch(request, env) { | |
| 28 | + | async fetch(request, env, ctx) { | |
| 29 | 29 | const { pathname } = new URL(request.url); | |
| 30 | 30 | // Git over HTTPS shares this hostname but belongs to the repos service. | |
| 31 | 31 | if (GIT_PATH.test(pathname)) { | |
| 33 | 33 | } | |
| 34 | 34 | const avatar = AVATAR_PATH.exec(pathname); | |
| 35 | 35 | if (avatar) { | |
| 36 | − | return serveAvatar(env, avatar[1], request.method); | |
| 36 | + | return serveAvatar(env, ctx, request, avatar[1]); | |
| 37 | 37 | } | |
| 38 | 38 | // The documentation is its own site. | |
| 39 | 39 | if (pathname === "/docs" || pathname.startsWith("/docs/")) { | |
| 50 | 50 | * kept for good. It is served as nothing but an image: the stored type, | |
| 51 | 51 | * no sniffing, and a policy that lets nothing in it run. | |
| 52 | 52 | */ | |
| 53 | − | async function serveAvatar(env: Env, hash: string, method: string): Promise<Response> { | |
| 53 | + | /** | |
| 54 | + | * An uploaded icon. Its address is its content's hash, so it never changes: | |
| 55 | + | * each data centre keeps it in its cache after the first view, and storage | |
| 56 | + | * is read about once per place, not once per visitor. | |
| 57 | + | */ | |
| 58 | + | async function serveAvatar(env: Env, ctx: ExecutionContext, request: Request, hash: string): Promise<Response> { | |
| 59 | + | const method = request.method; | |
| 54 | 60 | if (method !== "GET" && method !== "HEAD") { | |
| 55 | 61 | return new Response("Method not allowed", { status: 405, headers: { allow: "GET, HEAD" } }); | |
| 56 | 62 | } | |
| 63 | + | const cache = caches.default; | |
| 64 | + | const key = new Request(new URL(`/avatars/${hash}`, request.url).toString(), { method: "GET" }); | |
| 65 | + | const cached = await cache.match(key); | |
| 66 | + | if (cached) { | |
| 67 | + | return method === "HEAD" ? new Response(null, { headers: cached.headers }) : cached; | |
| 68 | + | } | |
| 57 | 69 | const { value, metadata } = await env.AVATARS.getWithMetadata<{ contentType?: string }>(hash, { | |
| 58 | 70 | type: "arrayBuffer", | |
| 59 | 71 | cacheTtl: 86400, | |
| 65 | 77 | headers: { "cache-control": "public, max-age=60" }, | |
| 66 | 78 | }); | |
| 67 | 79 | } | |
| 68 | − | return new Response(method === "HEAD" ? null : value, { | |
| 69 | − | headers: { | |
| 70 | − | "content-type": contentType, | |
| 71 | − | "content-length": String(value.byteLength), | |
| 72 | − | "cache-control": "public, max-age=31536000, immutable", | |
| 73 | − | "x-content-type-options": "nosniff", | |
| 74 | − | "content-security-policy": "default-src 'none'; sandbox", | |
| 75 | − | "cross-origin-resource-policy": "cross-origin", | |
| 76 | − | }, | |
| 77 | − | }); | |
| 80 | + | const headers = { | |
| 81 | + | "content-type": contentType, | |
| 82 | + | "content-length": String(value.byteLength), | |
| 83 | + | "cache-control": "public, max-age=31536000, immutable", | |
| 84 | + | "x-content-type-options": "nosniff", | |
| 85 | + | "content-security-policy": "default-src 'none'; sandbox", | |
| 86 | + | "cross-origin-resource-policy": "cross-origin", | |
| 87 | + | }; | |
| 88 | + | ctx.waitUntil(cache.put(key, new Response(value, { headers }))); | |
| 89 | + | return new Response(method === "HEAD" ? null : value, { headers }); | |
| 78 | 90 | } |