diff --git a/packages/next/package.json b/packages/next/package.json index 30b172c4e4aed..a1a57967d1ad4 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -271,6 +271,7 @@ "ora": "4.0.4", "os-browserify": "0.3.0", "p-limit": "3.1.0", + "p-queue": "6.6.2", "path-browserify": "1.0.1", "path-to-regexp": "6.1.0", "picomatch": "4.0.1", diff --git a/packages/next/src/compiled/p-queue/LICENSE b/packages/next/src/compiled/p-queue/LICENSE new file mode 100644 index 0000000000000..fa7ceba3eb4a9 --- /dev/null +++ b/packages/next/src/compiled/p-queue/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/next/src/compiled/p-queue/index.js b/packages/next/src/compiled/p-queue/index.js new file mode 100644 index 0000000000000..e28bcbfdc17b7 --- /dev/null +++ b/packages/next/src/compiled/p-queue/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={993:e=>{var t=Object.prototype.hasOwnProperty,n="~";function Events(){}if(Object.create){Events.prototype=Object.create(null);if(!(new Events).__proto__)n=false}function EE(e,t,n){this.fn=e;this.context=t;this.once=n||false}function addListener(e,t,r,i,s){if(typeof r!=="function"){throw new TypeError("The listener must be a function")}var o=new EE(r,i||e,s),u=n?n+t:t;if(!e._events[u])e._events[u]=o,e._eventsCount++;else if(!e._events[u].fn)e._events[u].push(o);else e._events[u]=[e._events[u],o];return e}function clearEvent(e,t){if(--e._eventsCount===0)e._events=new Events;else delete e._events[t]}function EventEmitter(){this._events=new Events;this._eventsCount=0}EventEmitter.prototype.eventNames=function eventNames(){var e=[],r,i;if(this._eventsCount===0)return e;for(i in r=this._events){if(t.call(r,i))e.push(n?i.slice(1):i)}if(Object.getOwnPropertySymbols){return e.concat(Object.getOwnPropertySymbols(r))}return e};EventEmitter.prototype.listeners=function listeners(e){var t=n?n+e:e,r=this._events[t];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,s=r.length,o=new Array(s);i{e.exports=(e,t)=>{t=t||(()=>{});return e.then((e=>new Promise((e=>{e(t())})).then((()=>e))),(e=>new Promise((e=>{e(t())})).then((()=>{throw e}))))}},574:(e,t)=>{Object.defineProperty(t,"__esModule",{value:true});function lowerBound(e,t,n){let r=0;let i=e.length;while(i>0){const s=i/2|0;let o=r+s;if(n(e[o],t)<=0){r=++o;i-=s+1}else{i=s}}return r}t["default"]=lowerBound},821:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:true});const r=n(574);class PriorityQueue{constructor(){this._queue=[]}enqueue(e,t){t=Object.assign({priority:0},t);const n={priority:t.priority,run:e};if(this.size&&this._queue[this.size-1].priority>=t.priority){this._queue.push(n);return}const i=r.default(this._queue,n,((e,t)=>t.priority-e.priority));this._queue.splice(i,0,n)}dequeue(){const e=this._queue.shift();return e===null||e===void 0?void 0:e.run}filter(e){return this._queue.filter((t=>t.priority===e.priority)).map((e=>e.run))}get size(){return this._queue.length}}t["default"]=PriorityQueue},816:(e,t,n)=>{const r=n(213);class TimeoutError extends Error{constructor(e){super(e);this.name="TimeoutError"}}const pTimeout=(e,t,n)=>new Promise(((i,s)=>{if(typeof t!=="number"||t<0){throw new TypeError("Expected `milliseconds` to be a positive number")}if(t===Infinity){i(e);return}const o=setTimeout((()=>{if(typeof n==="function"){try{i(n())}catch(e){s(e)}return}const r=typeof n==="string"?n:`Promise timed out after ${t} milliseconds`;const o=n instanceof Error?n:new TimeoutError(r);if(typeof e.cancel==="function"){e.cancel()}s(o)}),t);r(e.then(i,s),(()=>{clearTimeout(o)}))}));e.exports=pTimeout;e.exports["default"]=pTimeout;e.exports.TimeoutError=TimeoutError}};var t={};function __nccwpck_require__(n){var r=t[n];if(r!==undefined){return r.exports}var i=t[n]={exports:{}};var s=true;try{e[n](i,i.exports,__nccwpck_require__);s=false}finally{if(s)delete t[n]}return i.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var n={};(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:true});const t=__nccwpck_require__(993);const r=__nccwpck_require__(816);const i=__nccwpck_require__(821);const empty=()=>{};const s=new r.TimeoutError;class PQueue extends t{constructor(e){var t,n,r,s;super();this._intervalCount=0;this._intervalEnd=0;this._pendingCount=0;this._resolveEmpty=empty;this._resolveIdle=empty;e=Object.assign({carryoverConcurrencyCount:false,intervalCap:Infinity,interval:0,concurrency:Infinity,autoStart:true,queueClass:i.default},e);if(!(typeof e.intervalCap==="number"&&e.intervalCap>=1)){throw new TypeError(`Expected \`intervalCap\` to be a number from 1 and up, got \`${(n=(t=e.intervalCap)===null||t===void 0?void 0:t.toString())!==null&&n!==void 0?n:""}\` (${typeof e.intervalCap})`)}if(e.interval===undefined||!(Number.isFinite(e.interval)&&e.interval>=0)){throw new TypeError(`Expected \`interval\` to be a finite number >= 0, got \`${(s=(r=e.interval)===null||r===void 0?void 0:r.toString())!==null&&s!==void 0?s:""}\` (${typeof e.interval})`)}this._carryoverConcurrencyCount=e.carryoverConcurrencyCount;this._isIntervalIgnored=e.intervalCap===Infinity||e.interval===0;this._intervalCap=e.intervalCap;this._interval=e.interval;this._queue=new e.queueClass;this._queueClass=e.queueClass;this.concurrency=e.concurrency;this._timeout=e.timeout;this._throwOnTimeout=e.throwOnTimeout===true;this._isPaused=e.autoStart===false}get _doesIntervalAllowAnother(){return this._isIntervalIgnored||this._intervalCount{this._onResumeInterval()}),t)}return true}}return false}_tryToStartAnother(){if(this._queue.size===0){if(this._intervalId){clearInterval(this._intervalId)}this._intervalId=undefined;this._resolvePromises();return false}if(!this._isPaused){const e=!this._isIntervalPaused();if(this._doesIntervalAllowAnother&&this._doesConcurrentAllowAnother){const t=this._queue.dequeue();if(!t){return false}this.emit("active");t();if(e){this._initializeIntervalIfNeeded()}return true}}return false}_initializeIntervalIfNeeded(){if(this._isIntervalIgnored||this._intervalId!==undefined){return}this._intervalId=setInterval((()=>{this._onInterval()}),this._interval);this._intervalEnd=Date.now()+this._interval}_onInterval(){if(this._intervalCount===0&&this._pendingCount===0&&this._intervalId){clearInterval(this._intervalId);this._intervalId=undefined}this._intervalCount=this._carryoverConcurrencyCount?this._pendingCount:0;this._processQueue()}_processQueue(){while(this._tryToStartAnother()){}}get concurrency(){return this._concurrency}set concurrency(e){if(!(typeof e==="number"&&e>=1)){throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${e}\` (${typeof e})`)}this._concurrency=e;this._processQueue()}async add(e,t={}){return new Promise(((n,i)=>{const run=async()=>{this._pendingCount++;this._intervalCount++;try{const o=this._timeout===undefined&&t.timeout===undefined?e():r.default(Promise.resolve(e()),t.timeout===undefined?this._timeout:t.timeout,(()=>{if(t.throwOnTimeout===undefined?this._throwOnTimeout:t.throwOnTimeout){i(s)}return undefined}));n(await o)}catch(e){i(e)}this._next()};this._queue.enqueue(run,t);this._tryToStartAnother();this.emit("add")}))}async addAll(e,t){return Promise.all(e.map((async e=>this.add(e,t))))}start(){if(!this._isPaused){return this}this._isPaused=false;this._processQueue();return this}pause(){this._isPaused=true}clear(){this._queue=new this._queueClass}async onEmpty(){if(this._queue.size===0){return}return new Promise((e=>{const t=this._resolveEmpty;this._resolveEmpty=()=>{t();e()}}))}async onIdle(){if(this._pendingCount===0&&this._queue.size===0){return}return new Promise((e=>{const t=this._resolveIdle;this._resolveIdle=()=>{t();e()}}))}get size(){return this._queue.size}sizeBy(e){return this._queue.filter(e).length}get pending(){return this._pendingCount}get isPaused(){return this._isPaused}get timeout(){return this._timeout}set timeout(e){this._timeout=e}}e["default"]=PQueue})();module.exports=n})(); \ No newline at end of file diff --git a/packages/next/src/compiled/p-queue/package.json b/packages/next/src/compiled/p-queue/package.json new file mode 100644 index 0000000000000..86ebb70ed08a7 --- /dev/null +++ b/packages/next/src/compiled/p-queue/package.json @@ -0,0 +1 @@ +{"name":"p-queue","main":"index.js","license":"MIT"} diff --git a/packages/next/src/server/after/after-context.test.ts b/packages/next/src/server/after/after-context.test.ts index fc4f751ea81ba..8ca2e25ea13ad 100644 --- a/packages/next/src/server/after/after-context.test.ts +++ b/packages/next/src/server/after/after-context.test.ts @@ -246,6 +246,67 @@ describe('createAfterContext', () => { expect(results).toEqual([undefined]) }) + it('runs after() callbacks added within an after()', async () => { + const waitUntilPromises: Promise[] = [] + const waitUntil = jest.fn((promise) => waitUntilPromises.push(promise)) + + let onCloseCallback: (() => void) | undefined = undefined + const onClose = jest.fn((cb) => { + onCloseCallback = cb + }) + + const afterContext = createAfterContext({ + waitUntil, + onClose, + cacheScope: undefined, + }) + + const requestStore = createMockRequestStore(afterContext) + const run = createRun(afterContext, requestStore) + + // ================================== + + const promise1 = new DetachedPromise() + const afterCallback1 = jest.fn(async () => { + await promise1.promise + after(afterCallback2) + }) + + const promise2 = new DetachedPromise() + const afterCallback2 = jest.fn(() => promise2.promise) + + await run(async () => { + after(afterCallback1) + expect(onClose).toHaveBeenCalledTimes(1) + expect(waitUntil).toHaveBeenCalledTimes(1) // just runCallbacksOnClose + }) + + expect(onClose).toHaveBeenCalledTimes(1) + expect(afterCallback1).not.toHaveBeenCalled() + expect(afterCallback2).not.toHaveBeenCalled() + + // the response is done. + onCloseCallback!() + await Promise.resolve(null) + + expect(afterCallback1).toHaveBeenCalledTimes(1) + expect(afterCallback2).toHaveBeenCalledTimes(0) + expect(waitUntil).toHaveBeenCalledTimes(1) + + promise1.resolve('1') + await Promise.resolve(null) + + expect(afterCallback1).toHaveBeenCalledTimes(1) + expect(afterCallback2).toHaveBeenCalledTimes(1) + expect(waitUntil).toHaveBeenCalledTimes(1) + promise2.resolve('2') + + const results = await Promise.all(waitUntilPromises) + expect(results).toEqual([ + undefined, // callbacks all get collected into a big void promise + ]) + }) + it('does not hang forever if onClose failed', async () => { const waitUntilPromises: Promise[] = [] const waitUntil = jest.fn((promise) => waitUntilPromises.push(promise)) diff --git a/packages/next/src/server/after/after-context.ts b/packages/next/src/server/after/after-context.ts index d91309b845327..874390a07097e 100644 --- a/packages/next/src/server/after/after-context.ts +++ b/packages/next/src/server/after/after-context.ts @@ -1,3 +1,4 @@ +import PromiseQueue from 'next/dist/compiled/p-queue' import { requestAsyncStorage, type RequestStore, @@ -30,12 +31,16 @@ export class AfterContextImpl implements AfterContext { private requestStore: RequestStore | undefined - private afterCallbacks: AfterCallback[] = [] + private runCallbacksOnClosePromise: Promise | undefined + private callbackQueue: PromiseQueue constructor({ waitUntil, onClose, cacheScope }: AfterContextOpts) { this.waitUntil = waitUntil this.onClose = onClose this.cacheScope = cacheScope + + this.callbackQueue = new PromiseQueue() + this.callbackQueue.pause() } public run(requestStore: RequestStore, callback: () => T): T { @@ -79,10 +84,26 @@ export class AfterContextImpl implements AfterContext { 'unstable_after: Missing `onClose` implementation' ) } - if (this.afterCallbacks.length === 0) { - this.waitUntil(this.runCallbacksOnClose()) + + // this should only happen once. + if (!this.runCallbacksOnClosePromise) { + this.runCallbacksOnClosePromise = this.runCallbacksOnClose() + this.waitUntil(this.runCallbacksOnClosePromise) + } + + const wrappedCallback = async () => { + try { + await callback() + } catch (err) { + // TODO(after): this is fine for now, but will need better intergration with our error reporting. + console.error( + 'An error occurred in a function passed to `unstable_after()`:', + err + ) + } } - this.afterCallbacks.push(callback) + + this.callbackQueue.add(wrappedCallback) } private async runCallbacksOnClose() { @@ -91,24 +112,11 @@ export class AfterContextImpl implements AfterContext { } private async runCallbacks(requestStore: RequestStore): Promise { - if (this.afterCallbacks.length === 0) return + if (this.callbackQueue.size === 0) return const runCallbacksImpl = async () => { - // TODO(after): we should consider limiting the parallelism here via something like `p-queue`. - // (having a queue will also be needed for after-within-after, so this'd solve two problems at once). - await Promise.all( - this.afterCallbacks.map(async (afterCallback) => { - try { - await afterCallback() - } catch (err) { - // TODO(after): this is fine for now, but will need better intergration with our error reporting. - console.error( - 'An error occurred in a function passed to `unstable_after()`:', - err - ) - } - }) - ) + this.callbackQueue.start() + return this.callbackQueue.onIdle() } const readonlyRequestStore: RequestStore = @@ -148,19 +156,7 @@ function wrapRequestStoreForAfterCallbacks( mutableCookies: new ResponseCookies(new Headers()), assetPrefix: requestStore.assetPrefix, reactLoadableManifest: requestStore.reactLoadableManifest, - - afterContext: { - after: () => { - throw new Error( - 'Calling `unstable_after()` from within `unstable_after()` is not supported yet.' - ) - }, - run: () => { - throw new InvariantError( - 'unstable_after: Cannot call `AfterContext.run()` from within an `unstable_after()` callback' - ) - }, - }, + afterContext: requestStore.afterContext, } } diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 81df6ff31065a..3e5e518b302d1 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -598,7 +598,7 @@ export class IncrementalCache implements IncrementalCacheType { // Set the value for the revalidate seconds so if it changes we can // update the cache with the new value. if (typeof ctx.revalidate !== 'undefined' && !ctx.fetchCache) { - this.revalidateTimings.set(pathname, ctx.revalidate) + this.revalidateTimings.set(toRoute(pathname), ctx.revalidate) } await this.cacheHandler?.set(pathname, data, ctx) diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 7d2a47fa4e734..f0e3cae30b9cf 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -646,6 +646,15 @@ export async function ncc_p_limit(task, opts) { .target('src/compiled/p-limit') } +// eslint-disable-next-line camelcase +externals['p-queue'] = 'next/dist/compiled/p-queue' +export async function ncc_p_queue(task, opts) { + await task + .source(relative(__dirname, require.resolve('p-queue'))) + .ncc({ packageName: 'p-queue', externals }) + .target('src/compiled/p-queue') +} + // eslint-disable-next-line camelcase externals['raw-body'] = 'next/dist/compiled/raw-body' export async function ncc_raw_body(task, opts) { @@ -2171,6 +2180,7 @@ export async function ncc(task, opts) { 'ncc_node_html_parser', 'ncc_napirs_triples', 'ncc_p_limit', + 'ncc_p_queue', 'ncc_raw_body', 'ncc_image_size', 'ncc_hapi_accept', diff --git a/packages/next/types/$$compiled.internal.d.ts b/packages/next/types/$$compiled.internal.d.ts index 68cd1bab2670d..099d00d76d408 100644 --- a/packages/next/types/$$compiled.internal.d.ts +++ b/packages/next/types/$$compiled.internal.d.ts @@ -79,6 +79,11 @@ declare module 'next/dist/compiled/p-limit' { export = m } +declare module 'next/dist/compiled/p-queue' { + import m from 'p-queue' + export = m +} + declare module 'next/dist/compiled/raw-body' { import m from 'raw-body' export = m diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebddd6a6400d4..43182a6f825ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1279,6 +1279,9 @@ importers: p-limit: specifier: 3.1.0 version: 3.1.0 + p-queue: + specifier: 6.6.2 + version: 6.6.2 path-browserify: specifier: 1.0.1 version: 1.0.1 @@ -9845,6 +9848,7 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + requiresBuild: true /color-string@1.5.4: resolution: {integrity: sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==} @@ -22754,6 +22758,7 @@ packages: /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + requiresBuild: true dependencies: is-arrayish: 0.3.2 diff --git a/test/e2e/app-dir/next-after-app/app/nested-after/page.js b/test/e2e/app-dir/next-after-app/app/nested-after/page.js new file mode 100644 index 0000000000000..39b1dbaffe49c --- /dev/null +++ b/test/e2e/app-dir/next-after-app/app/nested-after/page.js @@ -0,0 +1,54 @@ +import { unstable_after as after } from 'next/server' +import { cache } from 'react' +import { cliLog } from '../../utils/log' +import { headers } from 'next/headers' + +const thing = cache(() => Symbol('cache me please')) + +export default function Index({ params }) { + const hostFromRender = headers().get('host') + const valueFromRender = thing() + + after(async () => { + const hostFromAfter = headers().get('host') + const valueFromAfter = thing() + + cliLog({ + source: '[page] /nested-after (after #1)', + assertions: { + 'cache() works in after()': valueFromRender === valueFromAfter, + 'headers() works in after()': hostFromRender === hostFromAfter, + }, + }) + + after(() => { + const hostFromAfter = headers().get('host') + const valueFromAfter = thing() + + cliLog({ + source: '[page] /nested-after (after #2)', + assertions: { + 'cache() works in after()': valueFromRender === valueFromAfter, + 'headers() works in after()': hostFromRender === hostFromAfter, + }, + }) + }) + + await new Promise((resolve) => setTimeout(resolve, 500)) + + after(() => { + const hostFromAfter = headers().get('host') + const valueFromAfter = thing() + + cliLog({ + source: '[page] /nested-after (after #3)', + assertions: { + 'cache() works in after()': valueFromRender === valueFromAfter, + 'headers() works in after()': hostFromRender === hostFromAfter, + }, + }) + }) + }) + + return
Page with nested after()
+} diff --git a/test/e2e/app-dir/next-after-app/index.test.ts b/test/e2e/app-dir/next-after-app/index.test.ts index 35d1f1b6f8fa2..99f31aba34e6d 100644 --- a/test/e2e/app-dir/next-after-app/index.test.ts +++ b/test/e2e/app-dir/next-after-app/index.test.ts @@ -104,6 +104,22 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => { // TODO: server seems to close before the response fully returns? }) + it('runs callbacks from nested unstable_after calls', async () => { + await next.browser('/nested-after') + + await retry(() => { + for (const id of [1, 2, 3]) { + expect(getLogs()).toContainEqual({ + source: `[page] /nested-after (after #${id})`, + assertions: { + 'cache() works in after()': true, + 'headers() works in after()': true, + }, + }) + } + }) + }) + describe('interrupted RSC renders', () => { it('runs callbacks if redirect() was called', async () => { await next.browser('/interrupted/calls-redirect') diff --git a/test/integration/root-catchall-cache/app/[[...slug]]/page.js b/test/integration/root-catchall-cache/app/[[...slug]]/page.js new file mode 100644 index 0000000000000..cc06fead52960 --- /dev/null +++ b/test/integration/root-catchall-cache/app/[[...slug]]/page.js @@ -0,0 +1,6 @@ +export const revalidate = 2 +export const dynamic = 'error' + +export default function Page() { + return
{Math.random()}
+} diff --git a/test/integration/root-catchall-cache/app/layout.js b/test/integration/root-catchall-cache/app/layout.js new file mode 100644 index 0000000000000..8525f5f8c0b2a --- /dev/null +++ b/test/integration/root-catchall-cache/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/integration/root-catchall-cache/next.config.js b/test/integration/root-catchall-cache/next.config.js new file mode 100644 index 0000000000000..89a39aed7bdb3 --- /dev/null +++ b/test/integration/root-catchall-cache/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + assetPrefix: '/', +} diff --git a/test/integration/root-catchall-cache/test/index.test.js b/test/integration/root-catchall-cache/test/index.test.js new file mode 100644 index 0000000000000..68cc8629b2ca6 --- /dev/null +++ b/test/integration/root-catchall-cache/test/index.test.js @@ -0,0 +1,63 @@ +/* eslint-env jest */ + +import { join } from 'path' +import cheerio from 'cheerio' +import { + killApp, + findPort, + nextBuild, + nextStart, + renderViaHTTP, + waitFor, +} from 'next-test-utils' + +const appDir = join(__dirname, '../') +let app +let appPort + +const getRandom = async (path) => { + const html = await renderViaHTTP(appPort, path) + const $ = cheerio.load(html) + return $('#random').text() +} + +const runTests = () => { + it('should cache / correctly', async () => { + const random = await getRandom('/') + + { + //cached response (revalidate is 2 seconds) + await waitFor(1000) + const newRandom = await getRandom('/') + expect(random).toBe(newRandom) + } + { + //stale response, triggers revalidate + await waitFor(1000) + const newRandom = await getRandom('/') + expect(random).toBe(newRandom) + } + { + //new response + await waitFor(100) + const newRandom = await getRandom('/') + expect(random).not.toBe(newRandom) + } + }) +} + +describe('Root Catch-all Cache', () => { + ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( + 'production mode', + () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + } + ) +})