diff --git a/docs/architecture.md b/docs/architecture.md index 0e3ffc6a..b2c65803 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,10 +8,10 @@ A high-level overview of how a request flows through Node.js' infrastructure: ```mermaid flowchart LR - request[Request] --> cloudflare(Cloudflare Routing Rules) + request[Request] --> cloudflare(Cloudflare Routing & Cache) cloudflare -- /dist/, /download/, /docs/, /api/, /metrics/ --> worker@{ shape: procs, label: "Release Worker"} cloudflare -- /... --> website(Website) - worker -- Cache miss --> r2[(R2 bucket)] + worker --> r2[(R2 bucket)] worker -- Error --> originServer(Origin Server) originServer website @@ -31,7 +31,6 @@ This goes on until the request is handled or we run out of middlewares to handle We currently have the following middlewares (in no particular order): -- [CacheMiddleware](../src/middleware/cacheMiddleware.ts) - Caches responses to GET request. - [R2Middleware](../src/middleware/r2Middleware.ts) - Fetches resource from R2. - [OriginMiddleware](../src/middleware/originMiddleware.ts) - Fetches resource from the origin server. Used as a fallback if the R2 middleware fails. @@ -45,11 +44,10 @@ We currently have the following middlewares (in no particular order): flowchart TD request[Request] --> worker(Release Worker) worker --> routerHandle("Router.handle") - routerHandle -- HTTP GET --> cacheMiddleware("Cache Middleware") + routerHandle -- HTTP GET --> r2Middleware("R2 Middleware") routerHandle -- HTTP HEAD --> r2Middleware routerHandle -- HTTP OPTIONS --> optionsMiddleware("Options Middleware") routerHandle -- Request --> substituteMiddleware("Substitute Middleware") substituteMiddleware -- Substituted Request --> routerHandle - cacheMiddleware -- Cache miss --> r2Middleware("R2 Middleware") r2Middleware -- Error --> originMiddleware("Origin Middleware") ``` diff --git a/src/env.ts b/src/env.ts index 9d5400dc..080a2efe 100644 --- a/src/env.ts +++ b/src/env.ts @@ -4,11 +4,6 @@ export interface Env { */ ENVIRONMENT: 'dev' | 'staging' | 'prod' | 'e2e-tests'; - /** - * Should caching be enabled? - */ - CACHING: boolean; - LOG_ERRORS?: boolean; /** diff --git a/src/middleware/cacheMiddleware.test.ts b/src/middleware/cacheMiddleware.test.ts deleted file mode 100644 index 46940320..00000000 --- a/src/middleware/cacheMiddleware.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { expect, test, vi } from 'vitest'; -import { cached } from './cacheMiddleware'; -import type { Context } from '../context'; -import type { Middleware } from './middleware'; - -class TestMiddleware implements Middleware { - #successful = true; - - constructor(successful = true) { - this.#successful = successful; - } - - async handle(): Promise { - return new Response('asd', { status: this.#successful ? 200 : 500 }); - } -} - -test('bypasses cache when caching is disabled', async () => { - const ctx: Context = { - // @ts-expect-error don't care - env: { - CACHING: false, - }, - // @ts-expect-error don't care - execution: { - waitUntil: (): void => { - throw new Error('should not hit this'); - }, - }, - }; - - const middleware = cached(new TestMiddleware()); - - // @ts-expect-error don't need WorkerRequest instance here - await middleware.handle(new Request('https://localhost'), ctx); -}); - -test('returns cached response', async () => { - const request = new Request('https://localhost'); - const cachedResponse = new Response('cached'); - - vi.stubGlobal( - 'caches', - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - (() => ({ - open: vi.fn(() => ({ - match(requestToMatch: Request): Promise { - expect(requestToMatch).toStrictEqual(request); - return Promise.resolve(cachedResponse); - }, - put(): Promise { - throw new Error('should not hit this'); - }, - })), - }))() - ); - - const ctx: Context = { - // @ts-expect-error don't care - env: { - CACHING: true, - }, - }; - - try { - const middleware = cached(new TestMiddleware()); - - // @ts-expect-error don't need WorkerRequest instance here - const res = await middleware.handle(new Request('https://localhost'), ctx); - expect(res).toStrictEqual(cachedResponse); - } finally { - vi.unstubAllGlobals(); - } -}); - -test('caches successful response', async () => { - let responseCached = false; - const request = new Request('https://localhost'); - - vi.stubGlobal( - 'caches', - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - (() => ({ - open: vi.fn(() => ({ - match(): Promise { - return Promise.resolve(undefined); - }, - put(requestToCache: Request, _: Response): Promise { - responseCached = true; - expect(requestToCache).toStrictEqual(request); - - return Promise.resolve(); - }, - })), - }))() - ); - - const ctx: Context = { - // @ts-expect-error don't care - env: { - CACHING: true, - }, - // @ts-expect-error don't care - execution: { - waitUntil(_: Promise): void {}, - }, - }; - - try { - const middleware = cached(new TestMiddleware()); - - // @ts-expect-error don't need WorkerRequest instance here - const res = await middleware.handle(request, ctx); - expect(await res.text()).toStrictEqual('asd'); - - expect(responseCached).toBeTruthy(); - } finally { - vi.unstubAllGlobals(); - } -}); - -test('does not cache non-200 response', async () => { - vi.stubGlobal( - 'caches', - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - (() => ({ - open: vi.fn(() => ({ - match(): Promise { - return Promise.resolve(undefined); - }, - put(): Promise { - throw new Error('should not hit this'); - }, - })), - }))() - ); - - const ctx: Context = { - // @ts-expect-error don't care - env: { - CACHING: true, - }, - // @ts-expect-error don't care - execution: { - waitUntil(_: Promise): void {}, - }, - }; - - try { - const middleware = cached(new TestMiddleware(false)); - - // @ts-expect-error don't need WorkerRequest instance here - const res = await middleware.handle(new Request('https://localhost'), ctx); - expect(await res.text()).toStrictEqual('asd'); - } finally { - vi.unstubAllGlobals(); - } -}); diff --git a/src/middleware/cacheMiddleware.ts b/src/middleware/cacheMiddleware.ts deleted file mode 100644 index 9df502c5..00000000 --- a/src/middleware/cacheMiddleware.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as Sentry from '@sentry/cloudflare'; -import type { Middleware } from './middleware'; - -/** - * Caches the response of a {@link Middleware} given that, - * 1. Caching is enabled - * 2. The middleware's next() callback wasn't called (we only want to cache - * responses for this specific middleware) - * 3. The response is successful (HTTP 200) - * - * Don't use this for non-GET requests (not enforced progammatically here but - * workerd will throw an error if it's called for anything other than GET). - * - * Cloudflare's cache api respects the cache-control header set in the response - * - * @see https://developers.cloudflare.com/workers/runtime-apis/cache/ - * - * @param middleware Middleware to cache the response of - */ -export function cached(middleware: Middleware): Middleware { - // Cache specifically for this middleware - let cache: Cache; - - const wrapper: Middleware = { - async handle(request, ctx) { - Sentry.addBreadcrumb({ - category: 'CacheMiddleware', - data: { - underlyingMiddleware: middleware.constructor.name, - }, - }); - - // Caching globally disabled, skip - if (!ctx.env.CACHING) { - return middleware.handle(request, ctx); - } - - cache ??= await caches.open(middleware.constructor.name); - - // Check if the request is in the cache already, return it if so - let response = await cache.match(request); - if (response !== undefined) { - return response; - } - - response = await middleware.handle(request, ctx); - - if (response instanceof Response && response.status === 200) { - // Successful request, let's cache it for next time - const cachedResponse = response.clone(); - - ctx.execution.waitUntil(cache.put(request, cachedResponse)); - } - - return response; - }, - }; - - return wrapper; -} diff --git a/src/middleware/originMiddleware.ts b/src/middleware/originMiddleware.ts index 1d09803d..4be87771 100644 --- a/src/middleware/originMiddleware.ts +++ b/src/middleware/originMiddleware.ts @@ -56,6 +56,7 @@ export class OriginMiddleware implements Middleware { 'cache-control': isDirectoryPath(request.urlObj.pathname) ? CACHE_HEADERS.failure : CACHE_HEADERS.success, + 'cache-tag': 'release-worker,release-worker:origin', }, }); } diff --git a/src/middleware/r2Middleware.ts b/src/middleware/r2Middleware.ts index 1b81912c..e197c72d 100644 --- a/src/middleware/r2Middleware.ts +++ b/src/middleware/r2Middleware.ts @@ -69,6 +69,7 @@ async function handleDirectory( 'last-modified': result.lastModified.toUTCString(), 'content-type': 'text/html', 'cache-control': CACHE_HEADERS.success, + 'cache-tag': 'release-worker,release-worker:directory', }, }); } diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 7f36d0da..a9c5131a 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -24,6 +24,7 @@ export type HttpResponseHeaders = { 'accept-range': string; 'access-control-allow-origin': string; 'cache-control': string; + 'cache-tag': string; expires: string; 'last-modified': string; 'content-encoding': string; diff --git a/src/providers/r2Provider.ts b/src/providers/r2Provider.ts index 3b361207..d8eb449f 100644 --- a/src/providers/r2Provider.ts +++ b/src/providers/r2Provider.ts @@ -166,6 +166,7 @@ function r2MetadataToHeaders( 'access-control-allow-origin': object.key.endsWith('.json') ? '*' : '', 'cache-control': httpStatusCode === 200 ? CACHE_HEADERS.success : CACHE_HEADERS.failure, + 'cache-tag': 'release-worker,release-worker:file', expires: httpMetadata?.cacheExpiry?.toUTCString() ?? '', 'last-modified': getLastModified(object), 'content-language': httpMetadata?.contentLanguage ?? '', diff --git a/src/routes/index.ts b/src/routes/index.ts index 253b20ac..2bfb2ef5 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,4 @@ import latestVersions from '../constants/latestVersions.json' assert { type: 'json' }; -import { cached } from '../middleware/cacheMiddleware'; import { MethodNotAllowedMiddleware } from '../middleware/methodNotAllowedMiddleware'; import { NotFoundMiddleware } from '../middleware/notFoundMiddleware'; import { OptionsMiddleware } from '../middleware/optionsMiddleware'; @@ -12,7 +11,6 @@ import type { Router } from './router'; export function registerRoutes(router: Router): void { const r2Middleware = new R2Middleware(); - const cachedR2Middleware = cached(r2Middleware); const originMiddleware = new OriginMiddleware(); const corepackRedirectMiddleware = new RedirectionMiddleware( @@ -22,7 +20,7 @@ export function registerRoutes(router: Router): void { router.options('*', new OptionsMiddleware()); router.head('/metrics/?:filePath+', r2Middleware, originMiddleware); - router.get('/metrics/?:filePath+', cachedR2Middleware, originMiddleware); + router.get('/metrics/?:filePath+', r2Middleware, originMiddleware); router.all('/api/corepack.html', corepackRedirectMiddleware); router.all('/docs/latest/api/corepack.html', corepackRedirectMiddleware); @@ -53,20 +51,16 @@ export function registerRoutes(router: Router): void { router.get('/llms.txt', r2Middleware); router.head('/dist/?:filePath+', r2Middleware, originMiddleware); - router.get('/dist/?:filePath+', cachedR2Middleware, originMiddleware); + router.get('/dist/?:filePath+', r2Middleware, originMiddleware); router.head('/download/?:filePath+', r2Middleware, originMiddleware); - router.get('/download/?:filePath+', cachedR2Middleware, originMiddleware); + router.get('/download/?:filePath+', r2Middleware, originMiddleware); router.head('/api/?:filePath+', r2Middleware, originMiddleware); - router.get('/api/?:filePath+', cachedR2Middleware, originMiddleware); + router.get('/api/?:filePath+', r2Middleware, originMiddleware); router.head('/docs/?:version?/:filePath+?', r2Middleware, originMiddleware); - router.get( - '/docs/?:version?/:filePath+?', - cachedR2Middleware, - originMiddleware - ); + router.get('/docs/?:version?/:filePath+?', r2Middleware, originMiddleware); router.post('/_throw', new ThrowMiddleware()); diff --git a/wrangler.jsonc b/wrangler.jsonc index 8d26d998..c2982b88 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -11,7 +11,6 @@ "vars": { "workers_dev": true, "ENVIRONMENT": "dev", - "CACHING": false, "LOG_ERRORS": true, "S3_ENDPOINT": "https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com", "BUCKET_NAME": "dist-prod", @@ -30,7 +29,6 @@ "vars": { "workers_dev": true, "ENVIRONMENT": "staging", - "CACHING": true, "USE_KV": true, "LOG_ERRORS": false, "S3_ENDPOINT": "https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com", @@ -50,6 +48,9 @@ "id": "a8378d5444a2465ca7202ebaa5d8deb1", }, ], + "cache": { + "enabled": true, + }, }, "prod": { "account_id": "07be8d2fbc940503ca1be344714cb0d1", @@ -82,7 +83,6 @@ "vars": { "workers_dev": false, "ENVIRONMENT": "prod", - "CACHING": true, "LOG_ERRORS": false, "S3_ENDPOINT": "https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com", "BUCKET_NAME": "dist-prod", @@ -101,6 +101,9 @@ "id": "a8378d5444a2465ca7202ebaa5d8deb1", }, ], + "cache": { + "enabled": true, + }, }, }, }