From d674de412e3687a1b19a03b747fa75c4f13edd0a Mon Sep 17 00:00:00 2001 From: James Date: Wed, 15 May 2024 17:45:50 +0100 Subject: [PATCH 1/4] Add Header Indicating Suspense Cache HIT --- .../templates/_worker.js/utils/cache.ts | 37 +++++++++++++++---- .../assets/app/api/cache/route.js | 13 +++++++ .../appFetchCache/fetch-cache.test.ts | 23 ++++++++++++ pages-e2e/features/appFetchCache/main.feature | 3 ++ pages-e2e/features/appFetchCache/setup.ts | 2 + pages-e2e/fixtures/app14.0.0/main.fixture | 4 +- pages-e2e/fixtures/appLatest/main.fixture | 11 ++++-- 7 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 pages-e2e/features/appFetchCache/assets/app/api/cache/route.js create mode 100644 pages-e2e/features/appFetchCache/fetch-cache.test.ts create mode 100644 pages-e2e/features/appFetchCache/main.feature create mode 100644 pages-e2e/features/appFetchCache/setup.ts diff --git a/packages/next-on-pages/templates/_worker.js/utils/cache.ts b/packages/next-on-pages/templates/_worker.js/utils/cache.ts index 40b8192f1..8084b8e14 100644 --- a/packages/next-on-pages/templates/_worker.js/utils/cache.ts +++ b/packages/next-on-pages/templates/_worker.js/utils/cache.ts @@ -8,6 +8,8 @@ const NEXT_CACHE_SOFT_TAGS_HEADER = 'x-next-cache-soft-tags'; const REQUEST_CONTEXT_KEY = Symbol.for('__cloudflare-request-context__'); +const CF_NEXT_SUSPENSE_CACHE_HEADER = 'cf-next-suspense-cache'; + /** * Handles an internal request to the suspense cache. * @@ -50,14 +52,17 @@ export async function handleSuspenseCacheRequest(request: Request) { const data = await cache.get(cacheKey, { softTags }); if (!data) return new Response(null, { status: 404 }); - return new Response(JSON.stringify(data.value), { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'x-vercel-cache-state': 'fresh', - age: `${(Date.now() - (data.lastModified ?? Date.now())) / 1000}`, + return new Response( + JSON.stringify(formatCacheValueForResponse(data.value)), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'x-vercel-cache-state': 'fresh', + age: `${(Date.now() - (data.lastModified ?? Date.now())) / 1000}`, + }, }, - }); + ); } case 'POST': { // Retrieve request context. @@ -124,3 +129,21 @@ async function getInternalCacheAdaptor( function getTagsFromHeader(req: Request, key: string): string[] | undefined { return req.headers.get(key)?.split(',')?.filter(Boolean); } + +function formatCacheValueForResponse(value: IncrementalCacheValue | null) { + switch (value?.kind) { + case 'FETCH': + return { + ...value, + data: { + ...value.data, + headers: { + ...value.data.headers, + [CF_NEXT_SUSPENSE_CACHE_HEADER]: 'HIT', + }, + }, + }; + default: + return value; + } +} diff --git a/pages-e2e/features/appFetchCache/assets/app/api/cache/route.js b/pages-e2e/features/appFetchCache/assets/app/api/cache/route.js new file mode 100644 index 000000000..456a183ee --- /dev/null +++ b/pages-e2e/features/appFetchCache/assets/app/api/cache/route.js @@ -0,0 +1,13 @@ +export const runtime = 'edge'; + +export async function GET(request) { + const url = new URL('/api/hello', request.url); + const data = await fetch(url.href, { next: { tags: ['cache'] } }); + + return new Response( + JSON.stringify({ + body: await data.text(), + headers: Object.fromEntries([...data.headers.entries()]), + }), + ); +} diff --git a/pages-e2e/features/appFetchCache/fetch-cache.test.ts b/pages-e2e/features/appFetchCache/fetch-cache.test.ts new file mode 100644 index 000000000..fadf7250d --- /dev/null +++ b/pages-e2e/features/appFetchCache/fetch-cache.test.ts @@ -0,0 +1,23 @@ +import { beforeAll, describe, it } from 'vitest'; + +describe('Simple Pages API Routes', () => { + it('should return a cached fetch response from the suspense cache', async ({ + expect, + }) => { + const initialResp = await fetch(`${DEPLOYMENT_URL}/api/cache`); + const initialRespJson = await initialResp.json(); + + expect(initialRespJson.body).toEqual(expect.stringMatching('Hello world')); + expect(initialRespJson.headers).toEqual( + expect.not.objectContaining({ 'cf-next-suspense-cache': 'HIT' }), + ); + + const cachedResp = await fetch(`${DEPLOYMENT_URL}/api/cache`); + const cachedRespJson = await cachedResp.json(); + + expect(cachedRespJson.body).toEqual(expect.stringMatching('Hello world')); + expect(cachedRespJson.headers).toEqual( + expect.objectContaining({ 'cf-next-suspense-cache': 'HIT' }), + ); + }); +}); diff --git a/pages-e2e/features/appFetchCache/main.feature b/pages-e2e/features/appFetchCache/main.feature new file mode 100644 index 000000000..223c7e01e --- /dev/null +++ b/pages-e2e/features/appFetchCache/main.feature @@ -0,0 +1,3 @@ +{ + "setup": "node --loader tsm setup.ts" +} diff --git a/pages-e2e/features/appFetchCache/setup.ts b/pages-e2e/features/appFetchCache/setup.ts new file mode 100644 index 000000000..d38a9cf1c --- /dev/null +++ b/pages-e2e/features/appFetchCache/setup.ts @@ -0,0 +1,2 @@ +import { copyWorkspaceAssets } from '../_utils/copyWorkspaceAssets'; +await copyWorkspaceAssets(); diff --git a/pages-e2e/fixtures/app14.0.0/main.fixture b/pages-e2e/fixtures/app14.0.0/main.fixture index 4b0cc891d..c6b519a43 100644 --- a/pages-e2e/fixtures/app14.0.0/main.fixture +++ b/pages-e2e/fixtures/app14.0.0/main.fixture @@ -16,8 +16,8 @@ "compatibilityFlags": ["nodejs_compat"], "kvNamespaces": { "MY_KV": { - "production": {"id": "00000000000000000000000000000000"}, - "staging": {"id": "00000000000000000000000000000000"} + "production": { "id": "00000000000000000000000000000000" }, + "staging": { "id": "00000000000000000000000000000000" } } } } diff --git a/pages-e2e/fixtures/appLatest/main.fixture b/pages-e2e/fixtures/appLatest/main.fixture index f95b9f12d..a936532b2 100644 --- a/pages-e2e/fixtures/appLatest/main.fixture +++ b/pages-e2e/fixtures/appLatest/main.fixture @@ -10,7 +10,8 @@ "appConfigsRewritesRedirectsHeaders", "appWasm", "appServerActions", - "appGetRequestContext" + "appGetRequestContext", + "appFetchCache" ], "localSetup": "./setup.sh", "buildConfig": { @@ -21,8 +22,12 @@ "compatibilityFlags": ["nodejs_compat"], "kvNamespaces": { "MY_KV": { - "production": {"id": "00000000000000000000000000000000"}, - "staging": {"id": "00000000000000000000000000000000"} + "production": { "id": "00000000000000000000000000000000" }, + "staging": { "id": "00000000000000000000000000000000" } + }, + "__NEXT_ON_PAGES__KV_SUSPENSE_CACHE": { + "production": { "id": "00000000000000000000000000000000" }, + "staging": { "id": "00000000000000000000000000000000" } } } } From 7dab98497246ffcf3d9754e3a332ef137b6fc2b2 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 May 2024 21:00:55 +0100 Subject: [PATCH 2/4] dario comments --- packages/next-on-pages/templates/_worker.js/utils/cache.ts | 4 ++-- pages-e2e/features/appFetchCache/fetch-cache.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next-on-pages/templates/_worker.js/utils/cache.ts b/packages/next-on-pages/templates/_worker.js/utils/cache.ts index 8084b8e14..6868d5b0c 100644 --- a/packages/next-on-pages/templates/_worker.js/utils/cache.ts +++ b/packages/next-on-pages/templates/_worker.js/utils/cache.ts @@ -53,7 +53,7 @@ export async function handleSuspenseCacheRequest(request: Request) { if (!data) return new Response(null, { status: 404 }); return new Response( - JSON.stringify(formatCacheValueForResponse(data.value)), + JSON.stringify(adjustCacheValueForResponse(data.value)), { status: 200, headers: { @@ -130,7 +130,7 @@ function getTagsFromHeader(req: Request, key: string): string[] | undefined { return req.headers.get(key)?.split(',')?.filter(Boolean); } -function formatCacheValueForResponse(value: IncrementalCacheValue | null) { +function adjustCacheValueForResponse(value: IncrementalCacheValue | null) { switch (value?.kind) { case 'FETCH': return { diff --git a/pages-e2e/features/appFetchCache/fetch-cache.test.ts b/pages-e2e/features/appFetchCache/fetch-cache.test.ts index fadf7250d..bf474aa8e 100644 --- a/pages-e2e/features/appFetchCache/fetch-cache.test.ts +++ b/pages-e2e/features/appFetchCache/fetch-cache.test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, it } from 'vitest'; -describe('Simple Pages API Routes', () => { +describe('Simple App server API route with fetch caching', () => { it('should return a cached fetch response from the suspense cache', async ({ expect, }) => { From 4eb933c3e6cd2ec9d2025146299322aa1c572783 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 22 May 2024 19:08:07 +0100 Subject: [PATCH 3/4] try without kv --- pages-e2e/fixtures/appLatest/main.fixture | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pages-e2e/fixtures/appLatest/main.fixture b/pages-e2e/fixtures/appLatest/main.fixture index a936532b2..6adb5860c 100644 --- a/pages-e2e/fixtures/appLatest/main.fixture +++ b/pages-e2e/fixtures/appLatest/main.fixture @@ -24,10 +24,6 @@ "MY_KV": { "production": { "id": "00000000000000000000000000000000" }, "staging": { "id": "00000000000000000000000000000000" } - }, - "__NEXT_ON_PAGES__KV_SUSPENSE_CACHE": { - "production": { "id": "00000000000000000000000000000000" }, - "staging": { "id": "00000000000000000000000000000000" } } } } From dadbe42309606b3b3b295cbb06bd7901570fc036 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 May 2024 19:18:57 +0100 Subject: [PATCH 4/4] artificial delay --- pages-e2e/features/appFetchCache/fetch-cache.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pages-e2e/features/appFetchCache/fetch-cache.test.ts b/pages-e2e/features/appFetchCache/fetch-cache.test.ts index bf474aa8e..7f4551524 100644 --- a/pages-e2e/features/appFetchCache/fetch-cache.test.ts +++ b/pages-e2e/features/appFetchCache/fetch-cache.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, it } from 'vitest'; +import { describe, it } from 'vitest'; describe('Simple App server API route with fetch caching', () => { it('should return a cached fetch response from the suspense cache', async ({ @@ -12,6 +12,9 @@ describe('Simple App server API route with fetch caching', () => { expect.not.objectContaining({ 'cf-next-suspense-cache': 'HIT' }), ); + // artificial delay to ensure cache entry updates + await new Promise(res => setTimeout(res, 3000)); + const cachedResp = await fetch(`${DEPLOYMENT_URL}/api/cache`); const cachedRespJson = await cachedResp.json();