diff --git a/lerna.json b/lerna.json index da599b557614..f79f4e9a5a58 100644 --- a/lerna.json +++ b/lerna.json @@ -16,4 +16,4 @@ } }, "version": "16.3.0-canary.4" -} \ No newline at end of file +} diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index dd4973048e9e..056fbf21319d 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -50,4 +50,4 @@ "engines": { "node": ">=20.9.0" } -} \ No newline at end of file +} diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 21b8993cb1d1..a0edb5d2f6ed 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -60,4 +60,4 @@ "default": "./dist/parser.js" } } -} \ No newline at end of file +} diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index bd83cdf0c0e5..b7d5fa0093d0 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -18,4 +18,4 @@ "eslint": "9.37.0" }, "scripts": {} -} \ No newline at end of file +} diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 8cb7624f5e11..261f15f2755d 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -25,4 +25,4 @@ "types": "tsc --project tsconfig.json --skipLibCheck --declaration --emitDeclarationOnly --esModuleInterop --declarationDir dist", "prepublishOnly": "cd ../../ && turbo run build" } -} \ No newline at end of file +} diff --git a/packages/font/package.json b/packages/font/package.json index 87b730f115e5..f11bd9ea7f2d 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -27,4 +27,4 @@ "fontkit": "2.0.2", "typescript": "6.0.2" } -} \ No newline at end of file +} diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index e8be6e98be98..8032b7703409 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -14,4 +14,4 @@ "scripts": { "pack-for-isolated-tests": "pnpm pack --out ./packed.tgz" } -} \ No newline at end of file +} diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 90e51376e5a3..6f1bfbf0ff94 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -42,4 +42,4 @@ "@types/semver": "7.3.1", "typescript": "6.0.2" } -} \ No newline at end of file +} diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 9d8033cf179f..01a5a3b768b5 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -34,4 +34,4 @@ "dotenv": "16.3.1", "dotenv-expand": "10.0.0" } -} \ No newline at end of file +} diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index bf66b5d1e9ec..bb027703f214 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -25,4 +25,4 @@ "scripts": { "pack-for-isolated-tests": "pnpm pack --out ./packed.tgz" } -} \ No newline at end of file +} diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index aa81fc985dd6..a9888468903c 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -29,4 +29,4 @@ "@playwright/test": "1.58.2", "typescript": "6.0.2" } -} \ No newline at end of file +} diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index fb382a6d393e..19df21707a2e 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -6,4 +6,4 @@ "directory": "packages/next-plugin-storybook" }, "scripts": {} -} \ No newline at end of file +} diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 306cc6115dce..c2962c4a8153 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -16,4 +16,4 @@ "devDependencies": { "microbundle": "0.15.0" } -} \ No newline at end of file +} diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 83a423babf2a..03987548d1de 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -19,4 +19,4 @@ "object-assign": "4.1.1", "whatwg-fetch": "3.0.0" } -} \ No newline at end of file +} diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index 30a4c95dd1a6..591cb0cace6c 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -36,4 +36,4 @@ "jest": "^29.5.0", "ts-jest": "^29.1.0" } -} \ No newline at end of file +} diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index 37552b4871b1..a6a3b3128b83 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -12,4 +12,4 @@ "scripts": { "pack-for-isolated-tests": "pnpm pack --out ./packed.tgz" } -} \ No newline at end of file +} diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index fb268bec4358..5e1e60726a25 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -41,4 +41,4 @@ "cross-env": "6.0.3", "wasm-pack": "0.13.1" } -} \ No newline at end of file +} diff --git a/packages/next/package.json b/packages/next/package.json index 23c93bf4f7a6..7601c6fedc03 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -373,4 +373,4 @@ "engines": { "node": ">=20.9.0" } -} \ No newline at end of file +} diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index 183e4f34832f..9d0599202428 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -408,7 +408,7 @@ export async function handler( nodeNextReq, nodeNextRes, response, - context.renderOpts.pendingWaitUntil + pendingWaitUntil ) return null } diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 7ccdac890ebe..140fc51b3cdd 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -29,4 +29,4 @@ "react-refresh": "0.12.0", "webpack": "^4 || ^5" } -} \ No newline at end of file +} diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index bb85cede8a19..008ce607a2c4 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -36,4 +36,4 @@ "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eac89cdff1aa..e5fa90b87c52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38554,4 +38554,4 @@ snapshots: zwitch@1.0.5: {} - zwitch@2.0.4: {} \ No newline at end of file + zwitch@2.0.4: {} diff --git a/scripts/release-github-api.js b/scripts/release-github-api.js index 3d650a9032db..db4d85e8a2fb 100644 --- a/scripts/release-github-api.js +++ b/scripts/release-github-api.js @@ -143,6 +143,7 @@ async function createBlobForFile(token, commitSha, filePath) { const content = await git(['show', `${commitSha}:${filePath}`], { captureOutput: true, encoding: null, + stripFinalNewline: false, maxBuffer: 1024 * 1024 * 100, }) const blob = await githubRequest( diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 71d200f3f529..984258fa2d8a 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -3342,9 +3342,6 @@ describe('app-dir static/dynamic handling', () => { // Prime the cache. let res = await next.fetch(path) expect(res.status).toBe(200) - - // Consume the cache, the revalidations are completed on the end of the - // stream so we need to wait for that to complete. await res.text() for (let i = 0; i < 6; i++) { @@ -3374,6 +3371,7 @@ describe('app-dir static/dynamic handling', () => { ) } } + const finishedAt = Date.now() const startedResponding = +data.start if (Number.isNaN(startedResponding)) { @@ -3387,12 +3385,17 @@ describe('app-dir static/dynamic handling', () => { ) } - // We just want to ensure the response isn't blocked on revalidating the fetch. - // So we use the start time when route started processing not when we - // send off the response because that includes cold boots of the infra. + // The response must not be blocked on the 3s background revalidation: + // neither the first byte (TTFB) nor the terminating chunk (res.end). + // Using the route-start time excludes cold-boot/infra latency. if (startedStreaming - startedResponding >= 3000) { throw new Error( - `Response #${i} took too long to complete: ${startedStreaming - startedResponding}ms` + `Response #${i} first byte took too long: ${startedStreaming - startedResponding}ms` + ) + } + if (finishedAt - startedResponding >= 3000) { + throw new Error( + `Response #${i} took too long to complete: ${finishedAt - startedResponding}ms` ) } } diff --git a/test/e2e/app-dir/use-cache-swr/app/delayed-route/route.ts b/test/e2e/app-dir/use-cache-swr/app/delayed-route/route.ts new file mode 100644 index 000000000000..56df5ea66fd5 --- /dev/null +++ b/test/e2e/app-dir/use-cache-swr/app/delayed-route/route.ts @@ -0,0 +1,19 @@ +import { cacheLife } from 'next/cache' +import { setTimeout } from 'timers/promises' + +async function getCachedData() { + 'use cache' + + cacheLife('seconds') + + await setTimeout(1000) + + return new Date().toISOString() +} + +export async function GET() { + const cached = await getCachedData() + const dynamic = new Date().toISOString() + + return Response.json({ cached, dynamic }) +} diff --git a/test/e2e/app-dir/use-cache-swr/use-cache-swr.test.ts b/test/e2e/app-dir/use-cache-swr/use-cache-swr.test.ts index fc73230a58be..0726e2b9e551 100644 --- a/test/e2e/app-dir/use-cache-swr/use-cache-swr.test.ts +++ b/test/e2e/app-dir/use-cache-swr/use-cache-swr.test.ts @@ -103,6 +103,49 @@ describe('use-cache-swr', () => { expect(duration3).toBeLessThan(1000) }) + it('should serve stale data without blocking on the background regeneration (route handler)', async () => { + // Fetch 1: cold cache. The cached function blocks for ~1s. + const res1 = await next.fetch('/delayed-route') + const { cached: cached1, dynamic: dynamic1 } = await res1.json() + expect(cached1).toBeDateString() + expect(dynamic1).toBeDateString() + + // Wait past the 1s revalidate window (cacheLife('seconds')). + await new Promise((resolve) => setTimeout(resolve, 1200)) + + outputIndex = next.cliOutput.length + + // Fetch 2: stale hit. Should return the stale entry immediately and kick + // off the regeneration in the background, rather than blocking on the 1s + // delay in the cached function. + const start2 = Date.now() + const res2 = await next.fetch('/delayed-route') + const { cached: cached2, dynamic: dynamic2 } = await res2.json() + const duration2 = Date.now() - start2 + + expect(cached2).toBe(cached1) + expect(dynamic2).not.toBe(dynamic1) + expect(duration2).toBeLessThan(1000) + + // Wait for the background regen to finish writing the fresh entry. + await retry(() => { + expect(next.cliOutput.slice(outputIndex)).toMatch( + /PersistentCacheHandler::set/ + ) + }) + + // Fetch 3: should serve the pre-warmed fresh entry from the background + // regen, not a new stale value. + const start3 = Date.now() + const res3 = await next.fetch('/delayed-route') + const { cached: cached3, dynamic: dynamic3 } = await res3.json() + const duration3 = Date.now() - start3 + + expect(cached3).not.toBe(cached1) + expect(dynamic3).not.toBe(dynamic2) + expect(duration3).toBeLessThan(1000) + }) + it('should pass implicit tags to cache handler get() for nested caches during SWR', async () => { const browser = await next.browser('/') await browser.elementById('outer-data').text()