diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index 216adb2bb0540..ac0819c2765c6 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -32,25 +32,19 @@ const routeModule = new AppRouteRouteModule({ // Pull out the exports that we need to expose from the module. This should // be eliminated when we've moved the other routes to the new format. These // are used to hook into the route. -const { - requestAsyncStorage, - workAsyncStorage, - prerenderAsyncStorage, - serverHooks, -} = routeModule +const { workAsyncStorage, workUnitAsyncStorage, serverHooks } = routeModule function patchFetch() { return _patchFetch({ workAsyncStorage, - requestAsyncStorage, - prerenderAsyncStorage, + workUnitAsyncStorage, }) } export { routeModule, - requestAsyncStorage, workAsyncStorage, + workUnitAsyncStorage, serverHooks, patchFetch, } diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index fedf22331bec4..e737f0d817f87 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -131,7 +131,7 @@ const browserNonTranspileModules = [ const precompileRegex = /[\\/]next[\\/]dist[\\/]compiled[\\/]/ const asyncStoragesRegex = - /next[\\/]dist[\\/](esm[\\/])?client[\\/]components[\\/](work-async-storage|action-async-storage|request-async-storage)/ + /next[\\/]dist[\\/](esm[\\/])?(client[\\/]components|server[\\/]app-render|)[\\/](work-async-storage|action-async-storage|work-unit-async-storage)/ // Support for NODE_PATH const nodePathList = (process.env.NODE_PATH || '') diff --git a/packages/next/src/client/components/request-async-storage-instance.ts b/packages/next/src/client/components/request-async-storage-instance.ts deleted file mode 100644 index a9b6708432987..0000000000000 --- a/packages/next/src/client/components/request-async-storage-instance.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAsyncLocalStorage } from './async-local-storage' -import type { RequestAsyncStorage } from './request-async-storage.external' - -export const requestAsyncStorage: RequestAsyncStorage = - createAsyncLocalStorage() diff --git a/packages/next/src/client/components/request-async-storage.external.ts b/packages/next/src/client/components/request-async-storage.external.ts deleted file mode 100644 index 041869f9b6f7c..0000000000000 --- a/packages/next/src/client/components/request-async-storage.external.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { AsyncLocalStorage } from 'async_hooks' -import type { DraftModeProvider } from '../../server/async-storage/draft-mode-provider' -import type { ResponseCookies } from '../../server/web/spec-extension/cookies' -import type { ReadonlyHeaders } from '../../server/web/spec-extension/adapters/headers' -import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/adapters/request-cookies' - -// Share the instance module in the next-shared layer -import { requestAsyncStorage } from './request-async-storage-instance' with { 'turbopack-transition': 'next-shared' } -import type { ServerComponentsHmrCache } from '../../server/response-cache' - -import { cacheAsyncStorage } from '../../server/app-render/cache-async-storage.external' -import { prerenderAsyncStorage } from '../../server/app-render/prerender-async-storage.external' - -export interface RequestStore { - /** - * The URL of the request. This only specifies the pathname and the search - * part of the URL. - */ - readonly url: { - /** - * The pathname of the requested URL. - */ - readonly pathname: string - - /** - * The search part of the requested URL. If the request did not provide a - * search part, this will be an empty string. - */ - readonly search: string - } - - readonly headers: ReadonlyHeaders - readonly cookies: ReadonlyRequestCookies - readonly mutableCookies: ResponseCookies - readonly draftMode: DraftModeProvider - readonly isHmrRefresh?: boolean - readonly serverComponentsHmrCache?: ServerComponentsHmrCache -} - -export type RequestAsyncStorage = AsyncLocalStorage - -export { requestAsyncStorage } - -export function getExpectedRequestStore(callingExpression: string) { - const prerenderStore = prerenderAsyncStorage.getStore() - if (prerenderStore) { - // This should not happen because we should have checked it already. - throw new Error( - `\`${callingExpression}\` cannot be called inside a prerender. This is a bug in Next.js.` - ) - } - const store = requestAsyncStorage.getStore() - if (store) return store - const cacheStore = cacheAsyncStorage.getStore() - if (cacheStore) { - if (cacheStore.type === 'cache') { - throw new Error( - `\`${callingExpression}\` cannot be called inside "use cache". Call it outside and pass an argument instead. Read more: https://nextjs.org/docs/messages/next-request-in-use-cache` - ) - } else if (cacheStore.type === 'unstable-cache') { - throw new Error( - `\`${callingExpression}\` cannot be called inside unstable_cache. Call it outside and pass an argument instead. Read more: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` - ) - } - } - throw new Error( - `\`${callingExpression}\` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` - ) -} diff --git a/packages/next/src/server/after/after-context.ts b/packages/next/src/server/after/after-context.ts index 36427f180f479..d23d518dbf4ee 100644 --- a/packages/next/src/server/after/after-context.ts +++ b/packages/next/src/server/after/after-context.ts @@ -1,8 +1,9 @@ import PromiseQueue from 'next/dist/compiled/p-queue' import { - requestAsyncStorage, + workUnitAsyncStorage, type RequestStore, -} from '../../client/components/request-async-storage.external' + type WorkUnitStore, +} from '../../server/app-render/work-unit-async-storage.external' import { ResponseCookies } from '../web/spec-extension/cookies' import type { RequestLifecycleOpts } from '../base-server' import type { AfterCallback, AfterTask } from './after' @@ -20,7 +21,7 @@ export class AfterContext { private waitUntil: RequestLifecycleOpts['waitUntil'] | undefined private onClose: RequestLifecycleOpts['onClose'] | undefined - private requestStore: RequestStore | undefined + private workUnitStore: WorkUnitStore | undefined private runCallbacksOnClosePromise: Promise | undefined private callbackQueue: PromiseQueue @@ -55,11 +56,11 @@ export class AfterContext { if (!this.waitUntil) { errorWaitUntilNotAvailable() } - if (!this.requestStore) { + if (!this.workUnitStore) { // We just stash the first request store we have but this is not sufficient. // TODO: We should store a request store per callback since each callback might // be inside a different store. E.g. inside different batched actions, prerenders or caches. - this.requestStore = requestAsyncStorage.getStore() + this.workUnitStore = workUnitAsyncStorage.getStore() } if (!this.onClose) { throw new InvariantError( @@ -72,7 +73,7 @@ export class AfterContext { // NOTE: We're creating a promise here, which means that // we will propagate any AsyncLocalStorage contexts we're currently in // to the callbacks that'll execute later. - // This includes e.g. `requestAsyncStorage` and React's `requestStorage` (which backs `React.cache()`). + // This includes e.g. `workUnitAsyncStorage` and React's `requestStorage` (which backs `React.cache()`). this.runCallbacksOnClosePromise = this.runCallbacksOnClose() this.waitUntil(this.runCallbacksOnClosePromise) } @@ -94,19 +95,19 @@ export class AfterContext { private async runCallbacksOnClose() { await new Promise((resolve) => this.onClose!(resolve)) - return this.runCallbacks(this.requestStore) + return this.runCallbacks(this.workUnitStore) } private async runCallbacks( - requestStore: undefined | RequestStore + workUnitStore: undefined | WorkUnitStore ): Promise { if (this.callbackQueue.size === 0) return - const readonlyRequestStore: undefined | RequestStore = - requestStore === undefined + const readonlyRequestStore: undefined | WorkUnitStore = + workUnitStore === undefined || workUnitStore.type !== 'request' ? undefined : // TODO: This is not sufficient. It should just be the same store that mutates. - wrapRequestStoreForAfterCallbacks(requestStore) + wrapRequestStoreForAfterCallbacks(workUnitStore) const workStore = workAsyncStorage.getStore() @@ -114,7 +115,7 @@ export class AfterContext { // Clearing it out or running the first request store. // TODO: This needs to be the request store that was active at the time the // callback was scheduled but p-queue makes this hard so need further refactoring. - requestAsyncStorage.run(readonlyRequestStore as any, async () => { + workUnitAsyncStorage.run(readonlyRequestStore as any, async () => { this.callbackQueue.start() await this.callbackQueue.onIdle() }) @@ -133,6 +134,7 @@ function wrapRequestStoreForAfterCallbacks( requestStore: RequestStore ): RequestStore { return { + type: 'request', url: requestStore.url, get headers() { return requestStore.headers diff --git a/packages/next/src/server/after/after.ts b/packages/next/src/server/after/after.ts index 45f38c9f0e511..ced3dae2167aa 100644 --- a/packages/next/src/server/after/after.ts +++ b/packages/next/src/server/after/after.ts @@ -1,5 +1,5 @@ import { workAsyncStorage } from '../../client/components/work-async-storage.external' -import { cacheAsyncStorage } from '../../server/app-render/cache-async-storage.external' +import { workUnitAsyncStorage } from '../../server/app-render/work-unit-async-storage.external' import { StaticGenBailoutError } from '../../client/components/static-generation-bailout' import { markCurrentScopeAsDynamic } from '../app-render/dynamic-rendering' @@ -12,7 +12,7 @@ export type AfterCallback = () => T | Promise */ export function unstable_after(task: AfterTask): void { const workStore = workAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if (workStore) { const { afterContext } = workStore @@ -29,7 +29,7 @@ export function unstable_after(task: AfterTask): void { `Route ${workStore.route} with \`dynamic = "force-static"\` couldn't be rendered statically because it used \`${callingExpression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) } else { - markCurrentScopeAsDynamic(workStore, cacheStore, callingExpression) + markCurrentScopeAsDynamic(workStore, workUnitStore, callingExpression) } afterContext.after(task) diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index 80ffd3bfa9038..59b5fd5e5768b 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -1,6 +1,6 @@ import type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http' import type { SizeLimit } from '../../types' -import type { RequestStore } from '../../client/components/request-async-storage.external' +import type { RequestStore } from '../../server/app-render/work-unit-async-storage.external' import type { AppRenderContext, GenerateFlight } from './app-render' import type { AppPageModule } from '../../server/route-modules/app-page/module' import type { BaseNextRequest, BaseNextResponse } from '../base-http' diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 4cf35c54fb399..d9c3c8d9a1776 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -12,7 +12,7 @@ import type { InitialRSCPayload, } from './types' import type { WorkStore } from '../../client/components/work-async-storage.external' -import type { RequestStore } from '../../client/components/request-async-storage.external' +import type { RequestStore } from '../../server/app-render/work-unit-async-storage.external' import type { NextParsedUrlQuery } from '../request-meta' import type { LoaderTree } from '../lib/app-dir-module' import type { AppPageModule } from '../route-modules/app-page/module' @@ -150,9 +150,9 @@ import { } from '../app-render/app-render-prerender-utils' import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler' import { - prerenderAsyncStorage, + workUnitAsyncStorage, type PrerenderStore, -} from './prerender-async-storage.external' +} from './work-unit-async-storage.external' import { CacheSignal } from './cache-signal' import { getTracedMetadata } from '../lib/trace/utils' @@ -921,7 +921,7 @@ async function renderToHTMLOrFlightImpl( ) { req.originalRequest.on('end', () => { const staticGenStore = ComponentMod.workAsyncStorage.getStore() - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() const isPPR = prerenderStore && prerenderStore.type === 'prerender' ? !!prerenderStore.dynamicTracking?.dynamicAccesses?.length @@ -1128,7 +1128,7 @@ async function renderToHTMLOrFlightImpl( ]) } - addImplicitTags(workStore, requestStore, undefined, undefined) + addImplicitTags(workStore, requestStore) if (workStore.tags) { metadata.fetchTags = workStore.tags.join(',') @@ -1237,7 +1237,7 @@ async function renderToHTMLOrFlightImpl( ]) } - addImplicitTags(workStore, requestStore, undefined, undefined) + addImplicitTags(workStore, requestStore) if (workStore.tags) { metadata.fetchTags = workStore.tags.join(',') @@ -1300,7 +1300,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( } return withRequestStore( - renderOpts.ComponentMod.requestAsyncStorage, + renderOpts.ComponentMod.workUnitAsyncStorage, { req, url, @@ -1309,8 +1309,12 @@ export const renderToHTMLOrFlight: AppPageRender = ( isHmrRefresh, serverComponentsHmrCache, }, - (requestStore) => - withWorkStore( + (requestStore) => { + if (requestStore.type !== 'request') { + // TODO: Refactor to not need a RequestStore for prerenders. + throw new Error('This should never happen.') + } + return withWorkStore( renderOpts.ComponentMod.workAsyncStorage, { page: renderOpts.routeModule.definition.page, @@ -1333,6 +1337,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( postponedState ) ) + } ) } @@ -1881,7 +1886,7 @@ async function prerenderToStream( // We're not going to use the result of this render because the only time it could be used // is if it completes in a microtask and that's likely very rare for any non-trivial app - const firstAttemptRSCPayload = await prerenderAsyncStorage.run( + const firstAttemptRSCPayload = await workUnitAsyncStorage.run( prospectiveRenderPrerenderStore, getRSCPayload, tree, @@ -1892,7 +1897,7 @@ async function prerenderToStream( let didError = false let prospectiveRenderError: unknown = null ;( - prerenderAsyncStorage.run( + workUnitAsyncStorage.run( // The store to scope prospectiveRenderPrerenderStore, // The function to run @@ -1961,7 +1966,7 @@ async function prerenderToStream( reactServerIsDynamic = true } } - const finalAttemptRSCPayload = await prerenderAsyncStorage.run( + const finalAttemptRSCPayload = await workUnitAsyncStorage.run( finalRenderPrerenderStore, getRSCPayload, tree, @@ -1972,7 +1977,7 @@ async function prerenderToStream( await createReactServerPrerenderResult( prerenderAndAbortInSequentialTasks( () => - prerenderAsyncStorage.run( + workUnitAsyncStorage.run( // The store to scope finalRenderPrerenderStore, // The function to run @@ -2047,7 +2052,7 @@ async function prerenderToStream( .prerender as (typeof import('react-dom/static.edge'))['prerender'] const { prelude, postponed } = await prerenderAndAbortInSequentialTasks( () => - prerenderAsyncStorage.run( + workUnitAsyncStorage.run( ssrPrerenderStore, prerender, { const teedStream = ( - prerenderAsyncStorage.run( + workUnitAsyncStorage.run( // The store to scope finalRenderPrerenderStore, // The function to run @@ -2361,7 +2366,7 @@ async function prerenderToStream( const renderToReadableStream = require('react-dom/server.edge') .renderToReadableStream as (typeof import('react-dom/server.edge'))['renderToReadableStream'] - const pendingHTMLStream = prerenderAsyncStorage.run( + const pendingHTMLStream = workUnitAsyncStorage.run( ssrPrerenderStore, renderToReadableStream, -export { cacheAsyncStorage } diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 9e5cf7d2647f7..a2efa15bdfcc5 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -21,7 +21,7 @@ */ import type { WorkStore } from '../../client/components/work-async-storage.external' -import type { CacheStore } from '../../server/app-render/cache-async-storage.external' +import type { WorkUnitStore } from '../../server/app-render/work-unit-async-storage.external' // Once postpone is in stable we should switch to importing the postpone export directly import React from 'react' @@ -30,10 +30,9 @@ import { DynamicServerError } from '../../client/components/hooks-server-context import { StaticGenBailoutError } from '../../client/components/static-generation-bailout' import { isDynamicIOPrerender, - prerenderAsyncStorage, + workUnitAsyncStorage, type PrerenderStoreModern, -} from './prerender-async-storage.external' -import { cacheAsyncStorage } from './cache-async-storage.external' +} from './work-unit-async-storage.external' import { workAsyncStorage } from '../../client/components/work-async-storage.external' import { makeHangingPromise } from '../dynamic-rendering-utils' import { @@ -117,11 +116,14 @@ export function getFirstDynamicReason( */ export function markCurrentScopeAsDynamic( store: WorkStore, - cacheStore: void | CacheStore, + workUnitStore: undefined | WorkUnitStore, expression: string ): void { - if (cacheStore) { - if (cacheStore.type === 'cache' || cacheStore.type === 'unstable-cache') { + if (workUnitStore) { + if ( + workUnitStore.type === 'cache' || + workUnitStore.type === 'unstable-cache' + ) { // inside cache scopes marking a scope as dynamic has no effect because the outer cache scope // creates a cache boundary. This is subtly different from reading a dynamic data source which is // forbidden inside a cache scope. @@ -140,7 +142,7 @@ export function markCurrentScopeAsDynamic( ) } - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (prerenderStore && prerenderStore.type === 'prerender') { if (isDynamicIOPrerender(prerenderStore)) { // We're prerendering the RSC stream with dynamicIO enabled and we need to abort the @@ -187,7 +189,7 @@ export function trackFallbackParamAccessed( store: WorkStore, expression: string ): void { - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (!prerenderStore || prerenderStore.type !== 'prerender') return postponeWithTracking(store.route, expression, prerenderStore.dynamicTracking) @@ -204,15 +206,15 @@ export function trackFallbackParamAccessed( */ export function trackDynamicDataAccessed( store: WorkStore, - cacheStore: void | CacheStore, + workUnitStore: undefined | WorkUnitStore, expression: string ): void { - if (cacheStore) { - if (cacheStore.type === 'cache') { + if (workUnitStore) { + if (workUnitStore.type === 'cache') { throw new Error( `Route ${store.route} used "${expression}" inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache` ) - } else if (cacheStore.type === 'unstable-cache') { + } else if (workUnitStore.type === 'unstable-cache') { throw new Error( `Route ${store.route} used "${expression}" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) @@ -224,7 +226,7 @@ export function trackDynamicDataAccessed( ) } - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (prerenderStore && prerenderStore.type === 'prerender') { if (isDynamicIOPrerender(prerenderStore)) { // We're prerendering the RSC stream with dynamicIO enabled and we need to abort the @@ -268,15 +270,18 @@ export function trackDynamicDataAccessed( export function throwToInterruptStaticGeneration( expression: string, store: WorkStore, - cacheStore: void | CacheStore + workUnitStore: undefined | WorkUnitStore ): never { // We aren't prerendering but we are generating a static page. We need to bail out of static generation const err = new DynamicServerError( `Route ${store.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` ) - if (cacheStore) { - if (cacheStore.type === 'cache' || cacheStore.type === 'unstable-cache') { + if (workUnitStore) { + if ( + workUnitStore.type === 'cache' || + workUnitStore.type === 'unstable-cache' + ) { // inside cache scopes marking a scope as dynamic has no effect because the outer cache scope // creates a cache boundary. This is subtly different from reading a dynamic data source which is // forbidden inside a cache scope. @@ -301,10 +306,13 @@ export function throwToInterruptStaticGeneration( */ export function trackDynamicDataInDynamicRender( store: WorkStore, - cacheStore: void | CacheStore + workUnitStore: void | WorkUnitStore ) { - if (cacheStore) { - if (cacheStore.type === 'cache' || cacheStore.type === 'unstable-cache') { + if (workUnitStore) { + if ( + workUnitStore.type === 'cache' || + workUnitStore.type === 'unstable-cache' + ) { // inside cache scopes marking a scope as dynamic has no effect because the outer cache scope // creates a cache boundary. This is subtly different from reading a dynamic data source which is // forbidden inside a cache scope. @@ -379,7 +387,7 @@ type PostponeProps = { route: string } export function Postpone({ reason, route }: PostponeProps): never { - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() const dynamicTracking = prerenderStore && prerenderStore.type === 'prerender' ? prerenderStore.dynamicTracking @@ -567,7 +575,7 @@ export function useDynamicRouteParams(expression: string) { ) { // There are fallback route params, we should track these as dynamic // accesses. - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (prerenderStore && prerenderStore.type === 'prerender') { // We're prerendering with dynamicIO or PPR or both if (isDynamicIOPrerender(prerenderStore)) { @@ -585,8 +593,8 @@ export function useDynamicRouteParams(expression: string) { } } else { // We're prerendering in legacy mode - const cacheStore = cacheAsyncStorage.getStore() - throwToInterruptStaticGeneration(expression, workStore, cacheStore) + const workUnitStore = workUnitAsyncStorage.getStore() + throwToInterruptStaticGeneration(expression, workStore, workUnitStore) } } } diff --git a/packages/next/src/server/app-render/entry-base.ts b/packages/next/src/server/app-render/entry-base.ts index a048a9b03f9d9..21e4a1c2857c1 100644 --- a/packages/next/src/server/app-render/entry-base.ts +++ b/packages/next/src/server/app-render/entry-base.ts @@ -12,8 +12,7 @@ export { prerender } from 'react-server-dom-webpack/static.edge' import LayoutRouter from '../../client/components/layout-router' import RenderFromTemplateContext from '../../client/components/render-from-template-context' import { workAsyncStorage } from '../../client/components/work-async-storage.external' -import { requestAsyncStorage } from '../../client/components/request-async-storage.external' -import { prerenderAsyncStorage } from './prerender-async-storage.external' +import { workUnitAsyncStorage } from './work-unit-async-storage.external' import { actionAsyncStorage } from '../../client/components/action-async-storage.external' import { ClientPageRoot } from '../../client/components/client-page' import { ClientSegmentRoot } from '../../client/components/client-segment' @@ -48,8 +47,7 @@ import { taintObjectReference } from './rsc/taint' function patchFetch() { return _patchFetch({ workAsyncStorage, - requestAsyncStorage, - prerenderAsyncStorage, + workUnitAsyncStorage, }) } @@ -57,7 +55,7 @@ export { LayoutRouter, RenderFromTemplateContext, workAsyncStorage, - requestAsyncStorage, + workUnitAsyncStorage, actionAsyncStorage, createServerSearchParamsForServerPage, createServerSearchParamsForMetadata, diff --git a/packages/next/src/server/app-render/prerender-async-storage-instance.ts b/packages/next/src/server/app-render/prerender-async-storage-instance.ts deleted file mode 100644 index 6e12e78abee1e..0000000000000 --- a/packages/next/src/server/app-render/prerender-async-storage-instance.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PrerenderAsyncStorage } from './prerender-async-storage.external' -import { createAsyncLocalStorage } from '../../client/components/async-local-storage' - -export const prerenderAsyncStorage: PrerenderAsyncStorage = - createAsyncLocalStorage() diff --git a/packages/next/src/server/app-render/prerender-async-storage.external.ts b/packages/next/src/server/app-render/prerender-async-storage.external.ts deleted file mode 100644 index 69690b8fcb72c..0000000000000 --- a/packages/next/src/server/app-render/prerender-async-storage.external.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { AsyncLocalStorage } from 'async_hooks' - -import type { CacheSignal } from './cache-signal' -import type { DynamicTrackingState } from './dynamic-rendering' - -// Share the instance module in the next-shared layer -import { prerenderAsyncStorage } from './prerender-async-storage-instance' with { 'turbopack-transition': 'next-shared' } - -/** - * The Prerender store is for tracking information related to prerenders. - * - * It can be used for both RSC and SSR prerendering and should be scoped as close - * to the individual `renderTo...` API call as possible. To keep the type simple - * we don't distinguish between RSC and SSR prerendering explicitly but instead - * use conditional object properties to infer which mode we are in. For instance cache tracking - * only needs to happen during the RSC prerender when we are prospectively prerendering - * to fill all caches. - */ -export type PrerenderStoreModern = { - type: 'prerender' - pathname: string | undefined - /** - * This is the AbortController passed to React. It can be used to abort the prerender - * if we encounter conditions that do not require further rendering - */ - readonly controller: null | AbortController - - /** - * when not null this signal is used to track cache reads during prerendering and - * to await all cache reads completing before aborting the prerender. - */ - readonly cacheSignal: null | CacheSignal - - /** - * During some prerenders we want to track dynamic access. - */ - readonly dynamicTracking: null | DynamicTrackingState -} - -export type PrerenderStoreLegacy = { - type: 'prerender-legacy' - pathname: string | undefined -} - -export type PrerenderStore = PrerenderStoreLegacy | PrerenderStoreModern - -export function isDynamicIOPrerender(prerenderStore: PrerenderStore): boolean { - return ( - prerenderStore.type === 'prerender' && - !!(prerenderStore.controller || prerenderStore.cacheSignal) - ) -} - -export type PrerenderAsyncStorage = AsyncLocalStorage -export { prerenderAsyncStorage } diff --git a/packages/next/src/server/app-render/work-unit-async-storage-instance.ts b/packages/next/src/server/app-render/work-unit-async-storage-instance.ts new file mode 100644 index 0000000000000..f7d9a22361d1b --- /dev/null +++ b/packages/next/src/server/app-render/work-unit-async-storage-instance.ts @@ -0,0 +1,5 @@ +import { createAsyncLocalStorage } from '../../client/components/async-local-storage' +import type { WorkUnitAsyncStorage } from './work-unit-async-storage.external' + +export const workUnitAsyncStorage: WorkUnitAsyncStorage = + createAsyncLocalStorage() diff --git a/packages/next/src/server/app-render/work-unit-async-storage.external.ts b/packages/next/src/server/app-render/work-unit-async-storage.external.ts new file mode 100644 index 0000000000000..42b1f0e6664a7 --- /dev/null +++ b/packages/next/src/server/app-render/work-unit-async-storage.external.ts @@ -0,0 +1,137 @@ +import type { AsyncLocalStorage } from 'async_hooks' +import type { DraftModeProvider } from '../../server/async-storage/draft-mode-provider' +import type { ResponseCookies } from '../../server/web/spec-extension/cookies' +import type { ReadonlyHeaders } from '../../server/web/spec-extension/adapters/headers' +import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/adapters/request-cookies' +import type { CacheSignal } from './cache-signal' +import type { DynamicTrackingState } from './dynamic-rendering' + +// Share the instance module in the next-shared layer +import { workUnitAsyncStorage } from './work-unit-async-storage-instance' with { 'turbopack-transition': 'next-shared' } +import type { ServerComponentsHmrCache } from '../../server/response-cache' + +export type RequestStore = { + type: 'request' + + /** + * The URL of the request. This only specifies the pathname and the search + * part of the URL. + */ + readonly url: { + /** + * The pathname of the requested URL. + */ + readonly pathname: string + + /** + * The search part of the requested URL. If the request did not provide a + * search part, this will be an empty string. + */ + readonly search: string + } + + readonly headers: ReadonlyHeaders + readonly cookies: ReadonlyRequestCookies + readonly mutableCookies: ResponseCookies + readonly draftMode: DraftModeProvider + readonly isHmrRefresh?: boolean + readonly serverComponentsHmrCache?: ServerComponentsHmrCache +} + +/** + * The Prerender store is for tracking information related to prerenders. + * + * It can be used for both RSC and SSR prerendering and should be scoped as close + * to the individual `renderTo...` API call as possible. To keep the type simple + * we don't distinguish between RSC and SSR prerendering explicitly but instead + * use conditional object properties to infer which mode we are in. For instance cache tracking + * only needs to happen during the RSC prerender when we are prospectively prerendering + * to fill all caches. + */ +export type PrerenderStoreModern = { + type: 'prerender' + pathname: string | undefined + /** + * This is the AbortController passed to React. It can be used to abort the prerender + * if we encounter conditions that do not require further rendering + */ + readonly controller: null | AbortController + + /** + * when not null this signal is used to track cache reads during prerendering and + * to await all cache reads completing before aborting the prerender. + */ + readonly cacheSignal: null | CacheSignal + + /** + * During some prerenders we want to track dynamic access. + */ + readonly dynamicTracking: null | DynamicTrackingState +} + +export type PrerenderStoreLegacy = { + type: 'prerender-legacy' + pathname: string | undefined +} + +export type PrerenderStore = PrerenderStoreLegacy | PrerenderStoreModern + +export function isDynamicIOPrerender(workUnitStore: WorkUnitStore): boolean { + return ( + workUnitStore.type === 'prerender' && + !!(workUnitStore.controller || workUnitStore.cacheSignal) + ) +} + +export type UseCacheStore = { + type: 'cache' + // TODO: Inside this scope we'll track tags and life times of this scope. +} + +export type UnstableCacheStore = { + type: 'unstable-cache' +} + +/** + * The Cache store is for tracking information inside a "use cache" or unstable_cache context. + * Inside this context we should never expose any request or page specific information. + */ +export type CacheStore = UseCacheStore | UnstableCacheStore + +export type WorkUnitStore = RequestStore | CacheStore | PrerenderStore + +export type WorkUnitAsyncStorage = AsyncLocalStorage + +export { workUnitAsyncStorage } + +export function getExpectedRequestStore( + callingExpression: string +): RequestStore { + const workUnitStore = workUnitAsyncStorage.getStore() + if (workUnitStore) { + if (workUnitStore.type === 'request') { + return workUnitStore + } + if ( + workUnitStore.type === 'prerender' || + workUnitStore.type === 'prerender-legacy' + ) { + // This should not happen because we should have checked it already. + throw new Error( + `\`${callingExpression}\` cannot be called inside a prerender. This is a bug in Next.js.` + ) + } + if (workUnitStore.type === 'cache') { + throw new Error( + `\`${callingExpression}\` cannot be called inside "use cache". Call it outside and pass an argument instead. Read more: https://nextjs.org/docs/messages/next-request-in-use-cache` + ) + } else if (workUnitStore.type === 'unstable-cache') { + throw new Error( + `\`${callingExpression}\` cannot be called inside unstable_cache. Call it outside and pass an argument instead. Read more: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` + ) + } + } + throw new Error( + `\`${callingExpression}\` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` + ) +} diff --git a/packages/next/src/server/async-storage/with-request-store.ts b/packages/next/src/server/async-storage/with-request-store.ts index 7078eb46b2ede..5b5ddf733428c 100644 --- a/packages/next/src/server/async-storage/with-request-store.ts +++ b/packages/next/src/server/async-storage/with-request-store.ts @@ -1,7 +1,10 @@ import type { BaseNextRequest, BaseNextResponse } from '../base-http' import type { IncomingHttpHeaders } from 'http' import type { AsyncLocalStorage } from 'async_hooks' -import type { RequestStore } from '../../client/components/request-async-storage.external' +import type { + RequestStore, + WorkUnitStore, +} from '../../server/app-render/work-unit-async-storage.external' import type { RenderOpts } from '../app-render/types' import type { WithStore } from './with-store' import type { NextRequest } from '../web/spec-extension/request' @@ -99,10 +102,10 @@ function mergeMiddlewareCookies( } } -export const withRequestStore: WithStore = < +export const withRequestStore: WithStore = < Result, >( - storage: AsyncLocalStorage, + storage: AsyncLocalStorage, { req, url, @@ -127,6 +130,7 @@ export const withRequestStore: WithStore = < } = {} const store: RequestStore = { + type: 'request', // Rather than just using the whole `url` here, we pull the parts we want // to ensure we don't use parts of the URL that we shouldn't. This also // lets us avoid requiring an empty string for `search` in the type. diff --git a/packages/next/src/server/lib/patch-fetch.test.ts b/packages/next/src/server/lib/patch-fetch.test.ts index 471a3476fe46d..c684594ba5a5c 100644 --- a/packages/next/src/server/lib/patch-fetch.test.ts +++ b/packages/next/src/server/lib/patch-fetch.test.ts @@ -1,9 +1,8 @@ import { AsyncLocalStorage } from 'node:async_hooks' -import type { RequestStore } from '../../client/components/request-async-storage.external' +import type { WorkUnitStore } from '../../server/app-render/work-unit-async-storage.external' import type { WorkStore } from '../../client/components/work-async-storage.external' import type { IncrementalCache } from './incremental-cache' import { createPatchedFetcher } from './patch-fetch' -import type { PrerenderStore } from '../app-render/prerender-async-storage.external' describe('createPatchedFetcher', () => { it('should not buffer a streamed response', async () => { @@ -24,13 +23,12 @@ describe('createPatchedFetcher', () => { const workAsyncStorage = new AsyncLocalStorage() - const prerenderAsyncStorage = new AsyncLocalStorage() + const workUnitAsyncStorage = new AsyncLocalStorage() const patchedFetch = createPatchedFetcher(mockFetch, { - // requestAsyncStorage does not need to provide a store for this test. - requestAsyncStorage: new AsyncLocalStorage(), + // workUnitAsyncStorage does not need to provide a store for this test. workAsyncStorage, - prerenderAsyncStorage, + workUnitAsyncStorage, }) let resolveIncrementalCacheSet: () => void diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 6a9e97152fa77..2e59402706a9e 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -15,20 +15,15 @@ import { markCurrentScopeAsDynamic } from '../app-render/dynamic-rendering' import type { FetchMetric } from '../base-http' import { createDedupeFetch } from './dedupe-fetch' import type { - RequestAsyncStorage, + WorkUnitAsyncStorage, + WorkUnitStore, RequestStore, -} from '../../client/components/request-async-storage.external' -import type { PrerenderStore } from '../app-render/prerender-async-storage.external' -import { - cacheAsyncStorage, - type CacheStore, -} from '../../server/app-render/cache-async-storage.external' +} from '../../server/app-render/work-unit-async-storage.external' import { CachedRouteKind, IncrementalCacheKind, type CachedFetchData, } from '../response-cache' -import type { PrerenderAsyncStorage } from '../app-render/prerender-async-storage.external' import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -144,9 +139,7 @@ const getDerivedTags = (pathname: string): string[] => { export function addImplicitTags( workStore: WorkStore, - requestStore: RequestStore | undefined, - prerenderStore: PrerenderStore | undefined, - cacheStore: CacheStore | undefined + workUnitStore: WorkUnitStore | undefined ) { const newTags: string[] = [] const { page, fallbackRouteParams } = workStore @@ -155,8 +148,8 @@ export function addImplicitTags( // Ini the tags array if it doesn't exist. if ( - !cacheStore || - (cacheStore.type !== 'cache' && cacheStore.type !== 'unstable-cache') + !workUnitStore || + (workUnitStore.type !== 'cache' && workUnitStore.type !== 'unstable-cache') ) { workStore.tags ??= [] } @@ -166,8 +159,9 @@ export function addImplicitTags( for (let tag of derivedTags) { tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}` if ( - !cacheStore || - (cacheStore.type !== 'cache' && cacheStore.type !== 'unstable-cache') + !workUnitStore || + (workUnitStore.type !== 'cache' && + workUnitStore.type !== 'unstable-cache') ) { if (!workStore.tags?.includes(tag)) { workStore.tags?.push(tag) @@ -177,23 +171,21 @@ export function addImplicitTags( } const renderedPathname = - requestStore !== undefined - ? requestStore.url.pathname - : prerenderStore !== undefined - ? prerenderStore.pathname - : undefined + workUnitStore !== undefined + ? workUnitStore.type === 'request' + ? workUnitStore.url.pathname + : workUnitStore.type === 'prerender' || + workUnitStore.type === 'prerender-legacy' + ? workUnitStore.pathname + : undefined + : undefined // Add the tags from the pathname. If the route has unknown params, we don't // want to add the pathname as a tag, as it will be invalid. if (renderedPathname && !hasFallbackRouteParams) { const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${renderedPathname}` - if ( - !cacheStore || - (cacheStore.type !== 'cache' && cacheStore.type !== 'unstable-cache') - ) { - if (!workStore.tags?.includes(tag)) { - workStore.tags?.push(tag) - } + if (!workStore.tags?.includes(tag)) { + workStore.tags?.push(tag) } newTags.push(tag) } @@ -235,17 +227,12 @@ function trackFetchMetric( interface PatchableModule { workAsyncStorage: WorkAsyncStorage - requestAsyncStorage: RequestAsyncStorage - prerenderAsyncStorage: PrerenderAsyncStorage + workUnitAsyncStorage: WorkUnitAsyncStorage } export function createPatchedFetcher( originFetch: Fetcher, - { - workAsyncStorage, - requestAsyncStorage, - prerenderAsyncStorage, - }: PatchableModule + { workAsyncStorage, workUnitAsyncStorage }: PatchableModule ): PatchedFetcher { // Create the patched fetch function. We don't set the type here, as it's // verified as the return value of this function. @@ -272,7 +259,7 @@ export function createPatchedFetcher( const hideSpan = process.env.NEXT_OTEL_FETCH_DISABLED === '1' const workStore = workAsyncStorage.getStore() - const prerenderStore = prerenderAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() const result = getTracer().trace( isInternal ? NextNodeServerSpan.internalFetch : AppRenderSpan.fetch, @@ -293,9 +280,6 @@ export function createPatchedFetcher( return originFetch(input, init) } - const requestStore = requestAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() - // If the workStore is not available, we can't do any // special treatment of fetch, therefore fallback to the original // fetch implementation. @@ -337,8 +321,9 @@ export function createPatchedFetcher( ) if ( - !cacheStore || - (cacheStore.type !== 'cache' && cacheStore.type !== 'unstable-cache') + !workUnitStore || + (workUnitStore.type !== 'cache' && + workUnitStore.type !== 'unstable-cache') ) { if (Array.isArray(tags)) { if (!workStore.tags) { @@ -352,16 +337,11 @@ export function createPatchedFetcher( } } - const implicitTags = addImplicitTags( - workStore, - requestStore, - prerenderStore, - cacheStore - ) + const implicitTags = addImplicitTags(workStore, workUnitStore) // Inside unstable-cache we treat it the same as force-no-store on the page. const pageFetchCacheMode = - cacheStore && cacheStore.type === 'unstable-cache' + workUnitStore && workUnitStore.type === 'unstable-cache' ? 'force-no-store' : workStore.fetchCache const isUsingNoStore = !!workStore.isUnstableNoStore @@ -535,16 +515,16 @@ export function createPatchedFetcher( finalRevalidate < workStore.revalidate)))) ) { if ( - !cacheStore || - (cacheStore.type !== 'cache' && - cacheStore.type !== 'unstable-cache') + !workUnitStore || + (workUnitStore.type !== 'cache' && + workUnitStore.type !== 'unstable-cache') ) { // If we were setting the revalidate value to 0, we should try to // postpone instead first. if (finalRevalidate === 0) { markCurrentScopeAsDynamic( workStore, - cacheStore, + workUnitStore, `revalidate: 0 fetch ${input} ${workStore.route}` ) } @@ -560,6 +540,11 @@ export function createPatchedFetcher( let cacheKey: string | undefined const { incrementalCache } = workStore + const requestStore: undefined | RequestStore = + workUnitStore !== undefined && workUnitStore.type === 'request' + ? workUnitStore + : undefined + if ( incrementalCache && (isCacheableRevalidate || requestStore?.serverComponentsHmrCache) @@ -652,7 +637,7 @@ export function createPatchedFetcher( cacheKey && (isCacheableRevalidate || requestStore?.serverComponentsHmrCache) ) { - if (prerenderStore) { + if (workUnitStore && workUnitStore.type === 'prerender') { // We are prerendering at build time or revalidate time so we need to // buffer the response so we can guarantee it can be read in a microtask @@ -783,7 +768,7 @@ export function createPatchedFetcher( // We sometimes use the cache to dedupe fetches that do not specify a cache configuration // In these cases we want to make sure we still exclude them from prerenders if dynamicIO is on // so we introduce an artificial Task boundary here. - if (prerenderStore) { + if (workUnitStore && workUnitStore.type === 'prerender') { await waitAtLeastOneReactRenderTask() } } @@ -857,7 +842,7 @@ export function createPatchedFetcher( // If enabled, we should bail out of static generation. markCurrentScopeAsDynamic( workStore, - cacheStore, + workUnitStore, `no-store fetch ${input} ${workStore.route}` ) } @@ -874,16 +859,16 @@ export function createPatchedFetcher( // If enabled, we should bail out of static generation. markCurrentScopeAsDynamic( workStore, - cacheStore, + workUnitStore, `revalidate: 0 fetch ${input} ${workStore.route}` ) } if (!workStore.forceStatic || next.revalidate !== 0) { if ( - !cacheStore || - (cacheStore.type !== 'cache' && - cacheStore.type !== 'unstable-cache') + !workUnitStore || + (workUnitStore.type !== 'cache' && + workUnitStore.type !== 'unstable-cache') ) { workStore.revalidate = next.revalidate } @@ -964,12 +949,12 @@ export function createPatchedFetcher( ) if ( - prerenderStore && - prerenderStore.type === 'prerender' && - prerenderStore.cacheSignal + workUnitStore && + workUnitStore.type === 'prerender' && + workUnitStore.cacheSignal ) { // During static generation we track cache reads so we can reason about when they fill - const cacheSignal = prerenderStore.cacheSignal + const cacheSignal = workUnitStore.cacheSignal cacheSignal.beginRead() try { return await result diff --git a/packages/next/src/server/request/connection.ts b/packages/next/src/server/request/connection.ts index 5f4eef4221caf..18a7f09005bac 100644 --- a/packages/next/src/server/request/connection.ts +++ b/packages/next/src/server/request/connection.ts @@ -1,9 +1,8 @@ import { workAsyncStorage } from '../../client/components/work-async-storage.external' import { isDynamicIOPrerender, - prerenderAsyncStorage, -} from '../app-render/prerender-async-storage.external' -import { cacheAsyncStorage } from '../../server/app-render/cache-async-storage.external' + workUnitAsyncStorage, +} from '../app-render/work-unit-async-storage.external' import { postponeWithTracking, throwToInterruptStaticGeneration, @@ -19,8 +18,7 @@ import { makeHangingPromise } from '../dynamic-rendering-utils' */ export function connection(): Promise { const workStore = workAsyncStorage.getStore() - const prerenderStore = prerenderAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if (workStore) { if (workStore.forceStatic) { @@ -29,12 +27,12 @@ export function connection(): Promise { return Promise.resolve(undefined) } - if (cacheStore) { - if (cacheStore.type === 'cache') { + if (workUnitStore) { + if (workUnitStore.type === 'cache') { throw new Error( `Route ${workStore.route} used "connection" inside "use cache". The \`connection()\` function is used to indicate the subsequent code must only run when there is an actual Request, but caches must be able to be produced before a Request so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache` ) - } else if (cacheStore.type === 'unstable-cache') { + } else if (workUnitStore.type === 'unstable-cache') { throw new Error( `Route ${workStore.route} used "connection" inside a function cached with "unstable_cache(...)". The \`connection()\` function is used to indicate the subsequent code must only run when there is an actual Request, but caches must be able to be produced before a Request so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) @@ -46,10 +44,10 @@ export function connection(): Promise { ) } - if (prerenderStore && prerenderStore.type === 'prerender') { + if (workUnitStore && workUnitStore.type === 'prerender') { // We are in PPR and/or dynamicIO mode and prerendering - if (isDynamicIOPrerender(prerenderStore)) { + if (isDynamicIOPrerender(workUnitStore)) { // We use the controller and cacheSignal as an indication we are in dynamicIO mode. // When resolving headers for a prerender with dynamic IO we return a forever promise // along with property access tracked synchronous headers. @@ -64,17 +62,17 @@ export function connection(): Promise { postponeWithTracking( workStore.route, 'connection', - prerenderStore.dynamicTracking + workUnitStore.dynamicTracking ) } } else if (workStore.isStaticGeneration) { // We are in a legacy static generation mode while prerendering // We treat this function call as a bailout of static generation - throwToInterruptStaticGeneration('connection', workStore, cacheStore) + throwToInterruptStaticGeneration('connection', workStore, workUnitStore) } // We fall through to the dynamic context below but we still track dynamic access // because in dev we can still error for things like using headers inside a cache context - trackDynamicDataInDynamicRender(workStore, cacheStore) + trackDynamicDataInDynamicRender(workStore, workUnitStore) } return Promise.resolve(undefined) diff --git a/packages/next/src/server/request/cookies.ts b/packages/next/src/server/request/cookies.ts index c6c0daef43d9b..78c8371ff56d1 100644 --- a/packages/next/src/server/request/cookies.ts +++ b/packages/next/src/server/request/cookies.ts @@ -7,17 +7,16 @@ import { RequestCookies } from '../../server/web/spec-extension/cookies' import { workAsyncStorage } from '../../client/components/work-async-storage.external' import { isDynamicIOPrerender, - prerenderAsyncStorage, + workUnitAsyncStorage, type PrerenderStoreModern, -} from '../app-render/prerender-async-storage.external' -import { cacheAsyncStorage } from '../../server/app-render/cache-async-storage.external' +} from '../app-render/work-unit-async-storage.external' import { postponeWithTracking, abortAndThrowOnSynchronousDynamicDataAccess, throwToInterruptStaticGeneration, trackDynamicDataInDynamicRender, } from '../../server/app-render/dynamic-rendering' -import { getExpectedRequestStore } from '../../client/components/request-async-storage.external' +import { getExpectedRequestStore } from '../../server/app-render/work-unit-async-storage.external' import { actionAsyncStorage } from '../../client/components/action-async-storage.external' import { StaticGenBailoutError } from '../../client/components/static-generation-bailout' import { makeResolvedReactPromise } from './utils' @@ -53,8 +52,7 @@ export type UnsafeUnwrappedCookies = ReadonlyRequestCookies export function cookies(): Promise { const callingExpression = 'cookies' const workStore = workAsyncStorage.getStore() - const prerenderStore = prerenderAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if (workStore) { if (workStore.forceStatic) { @@ -64,12 +62,12 @@ export function cookies(): Promise { return makeUntrackedExoticCookies(underlyingCookies) } - if (cacheStore) { - if (cacheStore.type === 'cache') { + if (workUnitStore) { + if (workUnitStore.type === 'cache') { throw new Error( `Route ${workStore.route} used "cookies" inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "cookies" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache` ) - } else if (cacheStore.type === 'unstable-cache') { + } else if (workUnitStore.type === 'unstable-cache') { throw new Error( `Route ${workStore.route} used "cookies" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "cookies" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) @@ -81,11 +79,11 @@ export function cookies(): Promise { ) } - if (prerenderStore) { - if (prerenderStore.type === 'prerender') { + if (workUnitStore) { + if (workUnitStore.type === 'prerender') { // We are in PPR and/or dynamicIO mode and prerendering - if (isDynamicIOPrerender(prerenderStore)) { + if (isDynamicIOPrerender(workUnitStore)) { // We use the controller and cacheSignal as an indication we are in dynamicIO mode. // When resolving cookies for a prerender with dynamic IO we return a forever promise // along with property access tracked synchronous cookies. @@ -94,7 +92,7 @@ export function cookies(): Promise { // one of the properties of the cookies object. return makeDynamicallyTrackedExoticCookies( workStore.route, - prerenderStore + workUnitStore ) } else { // We are prerendering with PPR. We need track dynamic access here eagerly @@ -103,23 +101,23 @@ export function cookies(): Promise { postponeWithTracking( workStore.route, callingExpression, - prerenderStore.dynamicTracking + workUnitStore.dynamicTracking ) } - } else if (prerenderStore.type === 'prerender-legacy') { + } else if (workUnitStore.type === 'prerender-legacy') { // We are in a legacy static generation mode while prerendering // We track dynamic access here so we don't need to wrap the cookies in // individual property access tracking. throwToInterruptStaticGeneration( callingExpression, workStore, - cacheStore + workUnitStore ) } } // We fall through to the dynamic context below but we still track dynamic access // because in dev we can still error for things like using cookies inside a cache context - trackDynamicDataInDynamicRender(workStore, cacheStore) + trackDynamicDataInDynamicRender(workStore, workUnitStore) } // cookies is being called in a dynamic context diff --git a/packages/next/src/server/request/draft-mode.ts b/packages/next/src/server/request/draft-mode.ts index 50864ac1dc0c1..0a0e813d4f6c7 100644 --- a/packages/next/src/server/request/draft-mode.ts +++ b/packages/next/src/server/request/draft-mode.ts @@ -1,10 +1,9 @@ -import { getExpectedRequestStore } from '../../client/components/request-async-storage.external' +import { getExpectedRequestStore } from '../../server/app-render/work-unit-async-storage.external' import type { DraftModeProvider } from '../../server/async-storage/draft-mode-provider' import { workAsyncStorage } from '../../client/components/work-async-storage.external' -import { cacheAsyncStorage } from '../../server/app-render/cache-async-storage.external' -import { prerenderAsyncStorage } from '../../server/app-render/prerender-async-storage.external' +import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external' import { trackDynamicDataAccessed } from '../app-render/dynamic-rendering' import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-loger' @@ -37,15 +36,14 @@ export type UnsafeUnwrappedDraftMode = DraftMode export function draftMode(): Promise { const callingExpression = 'draftMode' const workStore = workAsyncStorage.getStore() - const prerenderStore = prerenderAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if ( - (cacheStore && - (cacheStore.type === 'cache' || cacheStore.type === 'unstable-cache')) || - (prerenderStore && - (prerenderStore.type === 'prerender' || - prerenderStore.type === 'prerender-legacy')) + workUnitStore && + (workUnitStore.type === 'cache' || + workUnitStore.type === 'unstable-cache' || + workUnitStore.type === 'prerender' || + workUnitStore.type === 'prerender-legacy') ) { // Return empty draft mode if ( @@ -169,11 +167,11 @@ class DraftMode { } public enable() { const store = workAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if (store) { // We we have a store we want to track dynamic data access to ensure we // don't statically generate routes that manipulate draft mode. - trackDynamicDataAccessed(store, cacheStore, 'draftMode().enable()') + trackDynamicDataAccessed(store, workUnitStore, 'draftMode().enable()') } if (this._provider !== null) { this._provider.enable() @@ -181,11 +179,11 @@ class DraftMode { } public disable() { const store = workAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if (store) { // We we have a store we want to track dynamic data access to ensure we // don't statically generate routes that manipulate draft mode. - trackDynamicDataAccessed(store, cacheStore, 'draftMode().disable()') + trackDynamicDataAccessed(store, workUnitStore, 'draftMode().disable()') } if (this._provider !== null) { this._provider.disable() diff --git a/packages/next/src/server/request/headers.ts b/packages/next/src/server/request/headers.ts index c27dc53d5aecb..c256f32e93e21 100644 --- a/packages/next/src/server/request/headers.ts +++ b/packages/next/src/server/request/headers.ts @@ -3,13 +3,12 @@ import { type ReadonlyHeaders, } from '../../server/web/spec-extension/adapters/headers' import { workAsyncStorage } from '../../client/components/work-async-storage.external' -import { getExpectedRequestStore } from '../../client/components/request-async-storage.external' +import { getExpectedRequestStore } from '../app-render/work-unit-async-storage.external' import { isDynamicIOPrerender, - prerenderAsyncStorage, + workUnitAsyncStorage, type PrerenderStoreModern, -} from '../app-render/prerender-async-storage.external' -import { cacheAsyncStorage } from '../../server/app-render/cache-async-storage.external' +} from '../app-render/work-unit-async-storage.external' import { postponeWithTracking, abortAndThrowOnSynchronousDynamicDataAccess, @@ -58,8 +57,7 @@ export type UnsafeUnwrappedHeaders = ReadonlyHeaders */ export function headers(): Promise { const workStore = workAsyncStorage.getStore() - const prerenderStore = prerenderAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if (workStore) { if (workStore.forceStatic) { @@ -69,12 +67,12 @@ export function headers(): Promise { return makeUntrackedExoticHeaders(underlyingHeaders) } - if (cacheStore) { - if (cacheStore.type === 'cache') { + if (workUnitStore) { + if (workUnitStore.type === 'cache') { throw new Error( `Route ${workStore.route} used "headers" inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "headers" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache` ) - } else if (cacheStore.type === 'unstable-cache') { + } else if (workUnitStore.type === 'unstable-cache') { throw new Error( `Route ${workStore.route} used "headers" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "headers" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) @@ -86,11 +84,11 @@ export function headers(): Promise { ) } - if (prerenderStore) { - if (prerenderStore.type === 'prerender') { + if (workUnitStore) { + if (workUnitStore.type === 'prerender') { // We are in PPR and/or dynamicIO mode and prerendering - if (isDynamicIOPrerender(prerenderStore)) { + if (isDynamicIOPrerender(workUnitStore)) { // We use the controller and cacheSignal as an indication we are in dynamicIO mode. // When resolving headers for a prerender with dynamic IO we return a forever promise // along with property access tracked synchronous headers. @@ -99,7 +97,7 @@ export function headers(): Promise { // one of the properties of the headers object. return makeDynamicallyTrackedExoticHeaders( workStore.route, - prerenderStore + workUnitStore ) } else { // We are prerendering with PPR. We need track dynamic access here eagerly @@ -108,19 +106,19 @@ export function headers(): Promise { postponeWithTracking( workStore.route, 'headers', - prerenderStore.dynamicTracking + workUnitStore.dynamicTracking ) } - } else if (prerenderStore.type === 'prerender-legacy') { + } else if (workUnitStore.type === 'prerender-legacy') { // We are in a legacy static generation mode while prerendering // We track dynamic access here so we don't need to wrap the headers in // individual property access tracking. - throwToInterruptStaticGeneration('headers', workStore, cacheStore) + throwToInterruptStaticGeneration('headers', workStore, workUnitStore) } } // We fall through to the dynamic context below but we still track dynamic access // because in dev we can still error for things like using headers inside a cache context - trackDynamicDataInDynamicRender(workStore, cacheStore) + trackDynamicDataInDynamicRender(workStore, workUnitStore) } const requestStore = getExpectedRequestStore('headers') diff --git a/packages/next/src/server/request/params.ts b/packages/next/src/server/request/params.ts index e19b672e1fba8..8c54248f0c383 100644 --- a/packages/next/src/server/request/params.ts +++ b/packages/next/src/server/request/params.ts @@ -10,10 +10,10 @@ import { import { isDynamicIOPrerender, - prerenderAsyncStorage, - type PrerenderStore, + workUnitAsyncStorage, + type WorkUnitStore, type PrerenderStoreModern, -} from '../app-render/prerender-async-storage.external' +} from '../app-render/work-unit-async-storage.external' import { InvariantError } from '../../shared/lib/invariant-error' import { makeResolvedReactPromise, describeStringPropertyAccess } from './utils' import { makeHangingPromise } from '../dynamic-rendering-utils' @@ -94,7 +94,7 @@ export function createPrerenderParamsForClientSegment( underlyingParams: Params, workStore: WorkStore ): Promise { - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (prerenderStore && prerenderStore.type === 'prerender') { if (isDynamicIOPrerender(prerenderStore)) { const fallbackParams = workStore.fallbackRouteParams @@ -132,7 +132,7 @@ function createPrerenderParams( if (hasSomeFallbackParams) { // params need to be treated as dynamic because we have at least one fallback param - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (prerenderStore && prerenderStore.type === 'prerender') { if (isDynamicIOPrerender(prerenderStore)) { // We are in a dynamicIO (PPR or otherwise) prerender @@ -249,7 +249,7 @@ function makeErroringExoticParams( underlyingParams: Params, fallbackParams: FallbackRouteParams, workStore: WorkStore, - prerenderStore: undefined | PrerenderStore + prerenderStore: undefined | WorkUnitStore ): Promise { const cachedParams = CachedParams.get(underlyingParams) if (cachedParams) { @@ -311,7 +311,11 @@ function makeErroringExoticParams( prerenderStore.dynamicTracking ) } else { - throwToInterruptStaticGeneration(expression, workStore) + throwToInterruptStaticGeneration( + expression, + workStore, + undefined + ) } }, enumerable: true, @@ -332,7 +336,11 @@ function makeErroringExoticParams( prerenderStore.dynamicTracking ) } else { - throwToInterruptStaticGeneration(expression, workStore) + throwToInterruptStaticGeneration( + expression, + workStore, + undefined + ) } }, set(newValue) { diff --git a/packages/next/src/server/request/search-params.ts b/packages/next/src/server/request/search-params.ts index a00e22eaa8be3..c109266c86b46 100644 --- a/packages/next/src/server/request/search-params.ts +++ b/packages/next/src/server/request/search-params.ts @@ -11,11 +11,10 @@ import { import { isDynamicIOPrerender, - prerenderAsyncStorage, - type PrerenderStore, + workUnitAsyncStorage, + type WorkUnitStore, type PrerenderStoreModern, -} from '../app-render/prerender-async-storage.external' -import { cacheAsyncStorage } from '../app-render/cache-async-storage.external' +} from '../app-render/work-unit-async-storage.external' import { InvariantError } from '../../shared/lib/invariant-error' import { makeHangingPromise } from '../dynamic-rendering-utils' import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-loger' @@ -90,7 +89,7 @@ export function createPrerenderSearchParamsForClientPage( return Promise.resolve({}) } - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (prerenderStore) { if (isDynamicIOPrerender(prerenderStore)) { // We're prerendering in a mode that aborts (dynamicIO) and should stall @@ -113,7 +112,7 @@ function createPrerenderSearchParams( return Promise.resolve({}) } - const prerenderStore = prerenderAsyncStorage.getStore() + const prerenderStore = workUnitAsyncStorage.getStore() if (prerenderStore && prerenderStore.type === 'prerender') { if (prerenderStore.controller || prerenderStore.cacheSignal) { // We are in a dynamicIO (PPR or otherwise) prerender @@ -259,7 +258,7 @@ function makeAbortingExoticSearchParams( function makeErroringExoticSearchParams( workStore: WorkStore, - prerenderStore: undefined | PrerenderStore + prerenderStore: undefined | WorkUnitStore ): Promise { const cachedSearchParams = CachedSearchParams.get(workStore) if (cachedSearchParams) { @@ -319,8 +318,12 @@ function makeErroringExoticSearchParams( prerenderStore.dynamicTracking ) } else { - const cacheStore = cacheAsyncStorage.getStore() - throwToInterruptStaticGeneration(expression, workStore, cacheStore) + const workUnitStore = workUnitAsyncStorage.getStore() + throwToInterruptStaticGeneration( + expression, + workStore, + workUnitStore + ) } return } @@ -339,8 +342,12 @@ function makeErroringExoticSearchParams( prerenderStore.dynamicTracking ) } else { - const cacheStore = cacheAsyncStorage.getStore() - throwToInterruptStaticGeneration(expression, workStore, cacheStore) + const workUnitStore = workUnitAsyncStorage.getStore() + throwToInterruptStaticGeneration( + expression, + workStore, + workUnitStore + ) } return } @@ -362,11 +369,11 @@ function makeErroringExoticSearchParams( prerenderStore.dynamicTracking ) } else { - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() throwToInterruptStaticGeneration( expression, workStore, - cacheStore + workUnitStore ) } } @@ -396,8 +403,8 @@ function makeErroringExoticSearchParams( prerenderStore.dynamicTracking ) } else { - const cacheStore = cacheAsyncStorage.getStore() - throwToInterruptStaticGeneration(expression, workStore, cacheStore) + const workUnitStore = workUnitAsyncStorage.getStore() + throwToInterruptStaticGeneration(expression, workStore, workUnitStore) } return false } @@ -418,8 +425,8 @@ function makeErroringExoticSearchParams( prerenderStore.dynamicTracking ) } else { - const cacheStore = cacheAsyncStorage.getStore() - throwToInterruptStaticGeneration(expression, workStore, cacheStore) + const workUnitStore = workUnitAsyncStorage.getStore() + throwToInterruptStaticGeneration(expression, workStore, workUnitStore) } }, }) @@ -475,8 +482,8 @@ function makeUntrackedExoticSearchParams( default: { Object.defineProperty(promise, prop, { get() { - const cacheStore = cacheAsyncStorage.getStore() - trackDynamicDataInDynamicRender(store, cacheStore) + const workUnitStore = workUnitAsyncStorage.getStore() + trackDynamicDataInDynamicRender(store, workUnitStore) return underlyingSearchParams[prop] }, set(value) { @@ -525,8 +532,8 @@ function makeDynamicallyTrackedExoticSearchParamsWithDevWarnings( expression ) } - const cacheStore = cacheAsyncStorage.getStore() - trackDynamicDataInDynamicRender(store, cacheStore) + const workUnitStore = workUnitAsyncStorage.getStore() + trackDynamicDataInDynamicRender(store, workUnitStore) } return ReflectAdapter.get(target, prop, receiver) }, diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index ef07a5c839a67..a2839593a907d 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -37,23 +37,19 @@ import { parsedUrlQueryToParams } from './helpers/parsed-url-query-to-params' import * as serverHooks from '../../../client/components/hooks-server-context' import { DynamicServerError } from '../../../client/components/hooks-server-context' -import { - requestAsyncStorage, - type RequestStore, -} from '../../../client/components/request-async-storage.external' import { workAsyncStorage, type WorkStore, } from '../../../client/components/work-async-storage.external' import { - prerenderAsyncStorage, + workUnitAsyncStorage, + type WorkUnitStore, type PrerenderStore, -} from '../../app-render/prerender-async-storage.external' +} from '../../app-render/work-unit-async-storage.external' import { actionAsyncStorage, type ActionStore, } from '../../../client/components/action-async-storage.external' -import { cacheAsyncStorage } from '../../../server/app-render/cache-async-storage.external' import * as sharedModules from './shared-modules' import { getIsServerAction } from '../../lib/server-action-request-meta' import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies' @@ -167,19 +163,13 @@ export class AppRouteRouteModule extends RouteModule< /** * A reference to the request async storage. */ - public readonly requestAsyncStorage = requestAsyncStorage + public readonly workUnitAsyncStorage = workUnitAsyncStorage /** * A reference to the static generation async storage. */ public readonly workAsyncStorage = workAsyncStorage - /** - * prerenderAsyncStorage is used to scope a prerender context for renders ocurring - * during a build or revalidate. - */ - public readonly prerenderAsyncStorage = prerenderAsyncStorage - /** * An interface to call server hooks which interact with the underlying * storage. @@ -287,7 +277,7 @@ export class AppRouteRouteModule extends RouteModule< handler: AppRouteHandlerFn, actionStore: ActionStore, workStore: WorkStore, - requestStore: RequestStore, + workUnitStore: WorkUnitStore, request: NextRequest, context: AppRouteRouteHandlerContext ) { @@ -297,8 +287,7 @@ export class AppRouteRouteModule extends RouteModule< // Patch the global fetch. patchFetch({ workAsyncStorage: this.workAsyncStorage, - requestAsyncStorage: this.requestAsyncStorage, - prerenderAsyncStorage: this.prerenderAsyncStorage, + workUnitAsyncStorage: this.workUnitAsyncStorage, }) const handlerContext: AppRouteHandlerFnContext = { @@ -310,6 +299,14 @@ export class AppRouteRouteModule extends RouteModule< : undefined, } + const pathname = + workUnitStore.type === 'request' + ? workUnitStore.url.pathname + : workUnitStore.type === 'prerender' || + workUnitStore.type === 'prerender-legacy' + ? workUnitStore.pathname + : undefined + let res: unknown try { if (isStaticGeneration && dynamicIOEnabled) { @@ -334,9 +331,10 @@ export class AppRouteRouteModule extends RouteModule< let prospectiveRenderIsDynamic = false const cacheSignal = new CacheSignal() let dynamicTracking = createDynamicTrackingState(undefined) + const prospectiveRoutePrerenderStore: PrerenderStore = { type: 'prerender', - pathname: requestStore.url.pathname, + pathname, cacheSignal, // During prospective render we don't use a controller // because we need to let all caches fill. @@ -345,7 +343,7 @@ export class AppRouteRouteModule extends RouteModule< } let prospectiveResult try { - prospectiveResult = this.prerenderAsyncStorage.run( + prospectiveResult = this.workUnitAsyncStorage.run( prospectiveRoutePrerenderStore, handler, request, @@ -404,7 +402,7 @@ export class AppRouteRouteModule extends RouteModule< const finalRoutePrerenderStore: PrerenderStore = { type: 'prerender', - pathname: requestStore.url.pathname, + pathname, cacheSignal: null, controller, dynamicTracking, @@ -414,7 +412,7 @@ export class AppRouteRouteModule extends RouteModule< res = await new Promise((resolve, reject) => { scheduleImmediate(async () => { try { - const result = await (this.prerenderAsyncStorage.run( + const result = await (this.workUnitAsyncStorage.run( finalRoutePrerenderStore, handler, request, @@ -465,10 +463,10 @@ export class AppRouteRouteModule extends RouteModule< }) }) } else if (isStaticGeneration) { - res = await prerenderAsyncStorage.run( + res = await workUnitAsyncStorage.run( { type: 'prerender-legacy', - pathname: requestStore.url.pathname, + pathname, }, handler, request, @@ -490,7 +488,9 @@ export class AppRouteRouteModule extends RouteModule< // Let's append any cookies that were added by the // cookie API. - appendMutableCookies(headers, requestStore.mutableCookies) + if (workUnitStore.type === 'request') { + appendMutableCookies(headers, workUnitStore.mutableCookies) + } // Return the redirect response. return new Response(null, { @@ -525,14 +525,17 @@ export class AppRouteRouteModule extends RouteModule< ...Object.values(workStore.pendingRevalidates || {}), ]) - addImplicitTags(workStore, requestStore, undefined, undefined) + addImplicitTags(workStore, workUnitStore) ;(context.renderOpts as any).fetchTags = workStore.tags?.join(',') // It's possible cookies were set in the handler, so we need // to merge the modified cookies and the returned response // here. const headers = new Headers(res.headers) - if (appendMutableCookies(headers, requestStore.mutableCookies)) { + if ( + workUnitStore.type === 'request' && + appendMutableCookies(headers, workUnitStore.mutableCookies) + ) { return new Response(res.body, { status: res.status, statusText: res.statusText, @@ -583,9 +586,9 @@ export class AppRouteRouteModule extends RouteModule< actionStore, () => withRequestStore( - this.requestAsyncStorage, + this.workUnitAsyncStorage, requestContext, - (requestStore) => + (workUnitStore) => withWorkStore( this.workAsyncStorage, staticGenerationContext, @@ -667,7 +670,7 @@ export class AppRouteRouteModule extends RouteModule< handler, actionStore, workStore, - requestStore, + workUnitStore, request, context ) @@ -873,8 +876,8 @@ function proxyNextRequest(request: NextRequest, workStore: WorkStore) { case 'toJSON': case 'toString': case 'origin': { - const cacheStore = cacheAsyncStorage.getStore() - trackDynamicDataAccessed(workStore, cacheStore, `nextUrl.${prop}`) + const workUnitStore = workUnitAsyncStorage.getStore() + trackDynamicDataAccessed(workStore, workUnitStore, `nextUrl.${prop}`) return ReflectAdapter.get(target, prop, receiver) } case 'clone': @@ -909,8 +912,8 @@ function proxyNextRequest(request: NextRequest, workStore: WorkStore) { case 'text': case 'arrayBuffer': case 'formData': { - const cacheStore = cacheAsyncStorage.getStore() - trackDynamicDataAccessed(workStore, cacheStore, `request.${prop}`) + const workUnitStore = workUnitAsyncStorage.getStore() + trackDynamicDataAccessed(workStore, workUnitStore, `request.${prop}`) // The receiver arg is intentionally the same as the target to fix an issue with // edge runtime, where attempting to access internal slots with the wrong `this` context // results in an error. diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts index 96b0e8849f07e..1e6e564bc4031 100644 --- a/packages/next/src/server/use-cache/use-cache-wrapper.ts +++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts @@ -14,8 +14,8 @@ import { import type { WorkStore } from '../../client/components/work-async-storage.external' import { workAsyncStorage } from '../../client/components/work-async-storage.external' -import type { UseCacheStore } from '../app-render/cache-async-storage.external' -import { cacheAsyncStorage } from '../app-render/cache-async-storage.external' +import type { UseCacheStore } from '../app-render/work-unit-async-storage.external' +import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external' import { runInCleanSnapshot } from '../app-render/clean-async-snapshot.external' import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin' @@ -136,7 +136,7 @@ function generateCacheEntryWithCacheContext( ) { // Initialize the Store for this Cache entry. const cacheStore: UseCacheStore = { type: 'cache' } - return cacheAsyncStorage.run( + return workUnitAsyncStorage.run( cacheStore, generateCacheEntryImpl, workStore, diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 08d15985906f2..b0a4ce59e6b8a 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -13,7 +13,7 @@ import { normalizeRscURL } from '../../shared/lib/router/utils/app-paths' import { FLIGHT_HEADERS } from '../../client/components/app-router-headers' import { ensureInstrumentationRegistered } from './globals' import { withRequestStore } from '../async-storage/with-request-store' -import { requestAsyncStorage } from '../../client/components/request-async-storage.external' +import { workUnitAsyncStorage } from '../../server/app-render/work-unit-async-storage.external' import { withWorkStore, type WorkStoreContext, @@ -263,7 +263,7 @@ export async function adapter( }, () => withRequestStore( - requestAsyncStorage, + workUnitAsyncStorage, { req: request, res: undefined, diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index 5d7fb7f3d3883..f87bcffc91b76 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -5,7 +5,7 @@ import { NEXT_CACHE_SOFT_TAG_MAX_LENGTH, } from '../../../lib/constants' import { workAsyncStorage } from '../../../client/components/work-async-storage.external' -import { cacheAsyncStorage } from '../../../server/app-render/cache-async-storage.external' +import { workUnitAsyncStorage } from '../../../server/app-render/work-unit-async-storage.external' /** * This function allows you to purge [cached data](https://nextjs.org/docs/app/building-your-application/caching) on-demand for a specific cache tag. @@ -49,13 +49,13 @@ function revalidate(tag: string, expression: string) { ) } - const cacheStore = cacheAsyncStorage.getStore() - if (cacheStore) { - if (cacheStore.type === 'cache') { + const workUnitStore = workUnitAsyncStorage.getStore() + if (workUnitStore) { + if (workUnitStore.type === 'cache') { throw new Error( `Route ${store.route} used "${expression}" inside a "use cache" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) - } else if (cacheStore.type === 'unstable-cache') { + } else if (workUnitStore.type === 'unstable-cache') { throw new Error( `Route ${store.route} used "${expression}" inside a function cached with "unstable_cache(...)" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) @@ -64,7 +64,7 @@ function revalidate(tag: string, expression: string) { // a route that makes use of revalidation APIs should be considered dynamic // as otherwise it would be impossible to revalidate - trackDynamicDataAccessed(store, cacheStore, expression) + trackDynamicDataAccessed(store, workUnitStore, expression) if (!store.revalidatedTags) { store.revalidatedTags = [] diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index 20c050aa0fcef..c75ff9fb4d97e 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -7,15 +7,13 @@ import { validateTags, } from '../../lib/patch-fetch' import { workAsyncStorage } from '../../../client/components/work-async-storage.external' -import { requestAsyncStorage } from '../../../client/components/request-async-storage.external' +import { workUnitAsyncStorage } from '../../app-render/work-unit-async-storage.external' import { CachedRouteKind, IncrementalCacheKind, type CachedFetchData, } from '../../response-cache' -import { prerenderAsyncStorage } from '../../app-render/prerender-async-storage.external' -import type { UnstableCacheStore } from '../../app-render/cache-async-storage.external' -import { cacheAsyncStorage } from '../../app-render/cache-async-storage.external' +import type { UnstableCacheStore } from '../../app-render/work-unit-async-storage.external' type Callback = (...args: any[]) => Promise @@ -100,8 +98,7 @@ export function unstable_cache( const cachedCb = async (...args: any[]) => { const workStore = workAsyncStorage.getStore() - const requestStore = requestAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() // We must be able to find the incremental cache otherwise we throw const maybeIncrementalCache: @@ -116,10 +113,9 @@ export function unstable_cache( } const incrementalCache = maybeIncrementalCache - const prerenderStore = prerenderAsyncStorage.getStore() const cacheSignal = - prerenderStore && prerenderStore.type === 'prerender' - ? prerenderStore.cacheSignal + workUnitStore && workUnitStore.type === 'prerender' + ? workUnitStore.cacheSignal : null if (cacheSignal) { cacheSignal.beginRead() @@ -129,6 +125,10 @@ export function unstable_cache( // router) and if there's no static generation store, we aren't in app // router. Default to an empty pathname and search params when there's no // request store or static generation store available. + const requestStore = + workUnitStore && workUnitStore.type === 'request' + ? workUnitStore + : undefined const pathname = requestStore?.url.pathname ?? workStore?.route ?? '' const searchParams = new URLSearchParams(requestStore?.url.search ?? '') @@ -153,8 +153,9 @@ export function unstable_cache( workStore.nextFetchId = fetchIdx + 1 const isNestedCache = - cacheStore && - (cacheStore.type === 'unstable-cache' || cacheStore.type === 'cache') + workUnitStore && + (workUnitStore.type === 'unstable-cache' || + workUnitStore.type === 'cache') // We are in an App Router context. We try to return the cached entry if it exists and is valid // If the entry is fresh we return it. If the entry is stale we return it but revalidate the entry in @@ -193,12 +194,7 @@ export function unstable_cache( } // @TODO check on this API. addImplicitTags mutates the store and returns the implicit tags. The naming // of this function is potentially a little confusing - const implicitTags = addImplicitTags( - workStore, - requestStore, - prerenderStore, - cacheStore - ) + const implicitTags = addImplicitTags(workStore, workUnitStore) if ( // when we are nested inside of other unstable_cache's @@ -247,26 +243,27 @@ export function unstable_cache( type: 'unstable-cache', } // We run the cache function asynchronously and save the result when it completes - workStore.pendingRevalidates[invocationKey] = cacheAsyncStorage - .run(innerCacheStore, cb, ...args) - .then((result) => { - return cacheNewResult( - result, - incrementalCache, - cacheKey, - tags, - options.revalidate, - fetchIdx, - fetchUrl - ) - }) - // @TODO This error handling seems wrong. We swallow the error? - .catch((err) => - console.error( - `revalidating cache with key: ${invocationKey}`, - err + workStore.pendingRevalidates[invocationKey] = + workUnitAsyncStorage + .run(innerCacheStore, cb, ...args) + .then((result) => { + return cacheNewResult( + result, + incrementalCache, + cacheKey, + tags, + options.revalidate, + fetchIdx, + fetchUrl + ) + }) + // @TODO This error handling seems wrong. We swallow the error? + .catch((err) => + console.error( + `revalidating cache with key: ${invocationKey}`, + err + ) ) - ) } // We had a valid cache entry so we return it here return cachedResponse @@ -278,7 +275,11 @@ export function unstable_cache( type: 'unstable-cache', } // If we got this far then we had an invalid cache entry and need to generate a new one - const result = await cacheAsyncStorage.run(innerCacheStore, cb, ...args) + const result = await workUnitAsyncStorage.run( + innerCacheStore, + cb, + ...args + ) if (!workStore.isDraftMode) { cacheNewResult( @@ -306,8 +307,7 @@ export function unstable_cache( // @TODO check on this API. addImplicitTags mutates the store and returns the implicit tags. The naming // of this function is potentially a little confusing const implicitTags = - workStore && - addImplicitTags(workStore, requestStore, prerenderStore, cacheStore) + workStore && addImplicitTags(workStore, workUnitStore) const cacheEntry = await incrementalCache.get(cacheKey, { kind: IncrementalCacheKind.FETCH, @@ -342,7 +342,11 @@ export function unstable_cache( type: 'unstable-cache', } // If we got this far then we had an invalid cache entry and need to generate a new one - const result = await cacheAsyncStorage.run(innerCacheStore, cb, ...args) + const result = await workUnitAsyncStorage.run( + innerCacheStore, + cb, + ...args + ) cacheNewResult( result, incrementalCache, diff --git a/packages/next/src/server/web/spec-extension/unstable-no-store.ts b/packages/next/src/server/web/spec-extension/unstable-no-store.ts index aa014b57914d8..16fcab1080d32 100644 --- a/packages/next/src/server/web/spec-extension/unstable-no-store.ts +++ b/packages/next/src/server/web/spec-extension/unstable-no-store.ts @@ -1,5 +1,5 @@ import { workAsyncStorage } from '../../../client/components/work-async-storage.external' -import { cacheAsyncStorage } from '../../../server/app-render/cache-async-storage.external' +import { workUnitAsyncStorage } from '../../../server/app-render/work-unit-async-storage.external' import { markCurrentScopeAsDynamic } from '../../app-render/dynamic-rendering' /** @@ -20,7 +20,7 @@ import { markCurrentScopeAsDynamic } from '../../app-render/dynamic-rendering' export function unstable_noStore() { const callingExpression = 'unstable_noStore()' const store = workAsyncStorage.getStore() - const cacheStore = cacheAsyncStorage.getStore() + const workUnitStore = workUnitAsyncStorage.getStore() if (!store) { // This generally implies we are being called in Pages router. We should probably not support // unstable_noStore in contexts outside of `react-server` condition but since we historically @@ -30,6 +30,6 @@ export function unstable_noStore() { return } else { store.isUnstableNoStore = true - markCurrentScopeAsDynamic(store, cacheStore, callingExpression) + markCurrentScopeAsDynamic(store, workUnitStore, callingExpression) } } diff --git a/test/e2e/app-dir/app-external/app/async-storage/page.js b/test/e2e/app-dir/app-external/app/async-storage/page.js index 4ec8618ff52b3..d04c7f04faacb 100644 --- a/test/e2e/app-dir/app-external/app/async-storage/page.js +++ b/test/e2e/app-dir/app-external/app/async-storage/page.js @@ -1,8 +1,8 @@ -import { requestAsyncStorage } from 'next/dist/client/components/request-async-storage.external' +import { workUnitAsyncStorage } from 'next/dist/server/app-render/work-unit-async-storage.external' export default async function Page() { // cookies is undefined if not set - return !!requestAsyncStorage.getStore().cookies ? 'success' : 'fail' + return !!workUnitAsyncStorage.getStore().cookies ? 'success' : 'fail' } export const dynamic = 'force-dynamic' diff --git a/test/e2e/app-dir/app-middleware/app-middleware.test.ts b/test/e2e/app-dir/app-middleware/app-middleware.test.ts index 8cd24b2555790..8325dc0382f4b 100644 --- a/test/e2e/app-dir/app-middleware/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware/app-middleware.test.ts @@ -261,7 +261,7 @@ describe('app dir - middleware with middleware in src dir', () => { }, }) - it('works without crashing when using requestAsyncStorage', async () => { + it('works without crashing when using RequestStore', async () => { const browser = await next.browser('/') await browser.addCookie({ name: 'test-cookie',