From 117fca5c60ebd3dd633e3cbcecddeee31dc6b5e6 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Wed, 30 Oct 2024 23:09:06 +0100 Subject: [PATCH 01/13] move globals --- packages/open-next/src/adapters/cache.ts | 11 -- .../open-next/src/adapters/edge-adapter.ts | 4 - packages/open-next/src/adapters/logger.ts | 4 - .../open-next/src/adapters/server-adapter.ts | 3 - .../src/core/createGenericHandler.ts | 4 - .../open-next/src/core/createMainHandler.ts | 15 -- .../open-next/src/core/edgeFunctionHandler.ts | 50 +---- packages/open-next/src/core/routing/util.ts | 6 - packages/open-next/src/types/global.ts | 187 ++++++++++++++++++ .../core/routing/cacheInterceptor.test.ts | 2 + .../tests/core/routing/util.test.ts | 3 + 11 files changed, 193 insertions(+), 96 deletions(-) create mode 100644 packages/open-next/src/types/global.ts diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts index 7de1b883b..255d0d562 100644 --- a/packages/open-next/src/adapters/cache.ts +++ b/packages/open-next/src/adapters/cache.ts @@ -1,5 +1,3 @@ -import type { IncrementalCache, TagCache } from "types/overrides"; - import { isBinaryContentType } from "./binary"; import { debug, error, warn } from "./logger"; @@ -100,15 +98,6 @@ export function hasCacheExtension(key: string) { return CACHE_EXTENSION_REGEX.test(key); } -declare global { - var incrementalCache: IncrementalCache; - var tagCache: TagCache; - var disableDynamoDBCache: boolean; - var disableIncrementalCache: boolean; - var lastModified: Record; - var isNextAfter15: boolean; -} - function isFetchCache( options?: | boolean diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index 3c4fecbe9..e5b2f1168 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -11,10 +11,6 @@ import { convertToQueryString, } from "../core/routing/util"; -declare global { - var isEdgeRuntime: true; -} - const defaultHandler = async ( internalEvent: InternalEvent, ): Promise => { diff --git a/packages/open-next/src/adapters/logger.ts b/packages/open-next/src/adapters/logger.ts index 3328192f5..e05092196 100644 --- a/packages/open-next/src/adapters/logger.ts +++ b/packages/open-next/src/adapters/logger.ts @@ -1,9 +1,5 @@ import type { BaseOpenNextError } from "utils/error"; -declare global { - var openNextDebug: boolean; -} - export function debug(...args: any[]) { if (globalThis.openNextDebug) { console.log(...args); diff --git a/packages/open-next/src/adapters/server-adapter.ts b/packages/open-next/src/adapters/server-adapter.ts index 486d8a4a3..46d2269db 100644 --- a/packages/open-next/src/adapters/server-adapter.ts +++ b/packages/open-next/src/adapters/server-adapter.ts @@ -12,9 +12,6 @@ setBuildIdEnv(); setNextjsServerWorkingDirectory(); // Because next is messing with fetch, we have to make sure that we use an untouched version of fetch -declare global { - var internalFetch: typeof fetch; -} globalThis.internalFetch = fetch; ///////////// diff --git a/packages/open-next/src/core/createGenericHandler.ts b/packages/open-next/src/core/createGenericHandler.ts index 4a78d97b4..1870b5033 100644 --- a/packages/open-next/src/core/createGenericHandler.ts +++ b/packages/open-next/src/core/createGenericHandler.ts @@ -10,10 +10,6 @@ import type { OpenNextHandler } from "types/overrides"; import { debug } from "../adapters/logger"; import { resolveConverter, resolveWrapper } from "./resolve"; -declare global { - var openNextConfig: Partial; -} - type HandlerType = | "imageOptimization" | "revalidate" diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts index 44ba7ab47..addd32176 100644 --- a/packages/open-next/src/core/createMainHandler.ts +++ b/packages/open-next/src/core/createMainHandler.ts @@ -1,5 +1,3 @@ -import type { AsyncLocalStorage } from "node:async_hooks"; - import type { OpenNextConfig } from "types/open-next"; import type { IncrementalCache, Queue } from "types/overrides"; import type { DetachedPromiseRunner } from "utils/promise"; @@ -15,19 +13,6 @@ import { resolveWrapper, } from "./resolve"; -declare global { - var queue: Queue; - var incrementalCache: IncrementalCache; - var fnName: string | undefined; - var serverId: string; - var __als: AsyncLocalStorage<{ - requestId: string; - pendingPromiseRunner: DetachedPromiseRunner; - isISRRevalidation?: boolean; - mergeHeadersPriority?: "middleware" | "handler"; - }>; -} - export async function createMainHandler() { // @ts-expect-error `./open-next.config.mjs` exists only in the build output const config: OpenNextConfig = await import("./open-next.config.mjs").then( diff --git a/packages/open-next/src/core/edgeFunctionHandler.ts b/packages/open-next/src/core/edgeFunctionHandler.ts index 9924b9b45..c539d554a 100644 --- a/packages/open-next/src/core/edgeFunctionHandler.ts +++ b/packages/open-next/src/core/edgeFunctionHandler.ts @@ -1,54 +1,6 @@ // Necessary files will be imported here with banner in esbuild -import type { OutgoingHttpHeaders } from "http"; - -interface RequestData { - geo?: { - city?: string; - country?: string; - region?: string; - latitude?: string; - longitude?: string; - }; - headers: OutgoingHttpHeaders; - ip?: string; - method: string; - nextConfig?: { - basePath?: string; - i18n?: any; - trailingSlash?: boolean; - }; - page?: { - name?: string; - params?: { [key: string]: string | string[] }; - }; - url: string; - body?: ReadableStream; - signal: AbortSignal; -} - -interface Entries { - [k: string]: { - default: (props: { page: string; request: RequestData }) => Promise<{ - response: Response; - waitUntil: Promise; - }>; - }; -} -declare global { - var _ENTRIES: Entries; - var _ROUTES: EdgeRoute[]; - var __storage__: Map; - var AsyncContext: any; - //@ts-ignore - var AsyncLocalStorage: any; -} - -export interface EdgeRoute { - name: string; - page: string; - regex: string[]; -} +import type { RequestData } from "types/global"; type EdgeRequest = Omit; diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index bc3ebce27..88cc04680 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -264,12 +264,6 @@ export async function proxyRequest( // res.end(await result.text()); } -declare global { - var openNextDebug: boolean; - var openNextVersion: string; - var lastModified: Record; -} - enum CommonHeaders { CACHE_CONTROL = "cache-control", NEXT_CACHE = "x-nextjs-cache", diff --git a/packages/open-next/src/types/global.ts b/packages/open-next/src/types/global.ts new file mode 100644 index 000000000..0ef7272d1 --- /dev/null +++ b/packages/open-next/src/types/global.ts @@ -0,0 +1,187 @@ +import type { AsyncLocalStorage } from "node:async_hooks"; +import type { OutgoingHttpHeaders } from "node:http"; + +import type {IncrementalCache, Queue, TagCache} from 'types/overrides'; + +import type { DetachedPromiseRunner } from "../utils/promise"; +import type { OpenNextConfig } from "./open-next"; + +export interface RequestData { + geo?: { + city?: string; + country?: string; + region?: string; + latitude?: string; + longitude?: string; + }; + headers: OutgoingHttpHeaders; + ip?: string; + method: string; + nextConfig?: { + basePath?: string; + i18n?: any; + trailingSlash?: boolean; + }; + page?: { + name?: string; + params?: { [key: string]: string | string[] }; + }; + url: string; + body?: ReadableStream; + signal: AbortSignal; +} + +interface Entries { + [k: string]: { + default: (props: { page: string; request: RequestData }) => Promise<{ + response: Response; + waitUntil: Promise; + }>; + }; +} + +export interface EdgeRoute { + name: string; + page: string; + regex: string[]; +} + +declare global { + // Needed in the cache adapter + /** + * The cache adapter for incremental static regeneration. + * Only available in main functions or in the external middleware with `enableCacheInterception` set to `true`. + * Defined in `createMainHandler` or in `adapter/middleware.ts`. + */ + var incrementalCache: IncrementalCache; + /** + * The cache adapter for the tag cache. + * Only available in main functions, the initializationFunction or in the external middleware with `enableCacheInterception` set to `true`. + * Defined in `createMainHandler` or in `adapter/middleware.ts`. + */ + var tagCache: TagCache; + /** + * A boolean that indicates if the DynamoDB cache is disabled. + * TODO: Remove this, we already have access to the config file + * Defined in esbuild banner for the cache adapter. + */ + var disableDynamoDBCache: boolean; + /** + * A boolean that indicates if the incremental cache is disabled. + * TODO: Remove this, we already have access to the config file + * Defined in esbuild banner for the cache adapter. + */ + var disableIncrementalCache: boolean; + + /** + * An object that contains the last modified time of the pages. + * Only available in main functions. + * TODO: Integrate this directly in the AsyncLocalStorage context + * Defined in `createMainHandler`. + */ + var lastModified: Record; + + /** + * A boolean that indicates if Next is V15 or higher. + * Only available in the cache adapter. + * Defined in the esbuild banner for the cache adapter. + */ + var isNextAfter15: boolean; + + /** + * A boolean that indicates if the runtime is Edge. + * Only available in `edge` runtime functions (i.e. external middleware or function with edge runtime). + * Defined in the `edge-adapter.ts`. + */ + var isEdgeRuntime: true; + + /** + * A boolean that indicates if we are running in debug mode. + * Available in all functions. + * Defined in the esbuild banner. + */ + var openNextDebug: boolean; + + /** + * The fetch function that should be used to make requests during the execution of the function. + * Used to bypass Next intercepting and caching the fetch calls. Only available in main functions. + * Defined in the `server-adapter.ts` and in `adapters/middleware.ts`. + */ + var internalFetch: typeof fetch; + + /** + * The Open Next configuration object. + * Available in all functions. + * Defined in the `createMainHandler` or in the `createGenericHandler`. + */ + var openNextConfig: Partial; + + /** + * The name of the function that is currently being executed. + * Only available in main functions. + * Defined in the `createMainHandler`. + */ + var fnName: string | undefined; + /** + * The unique identifier of the server. + * Only available in main functions. + * Defined in the `createMainHandler`. + */ + var serverId: string; + + /** + * The AsyncLocalStorage instance that is used to store the request context. + * Only available in main functions. + * TODO: should be available everywhere in the future. + * Defined in `requestHandler.ts`. + */ + var __als: AsyncLocalStorage<{ + requestId: string; + pendingPromiseRunner: DetachedPromiseRunner; + isISRRevalidation?: boolean; + mergeHeadersPriority?: "middleware" | "handler"; + }>; + + /** + * The entries object that contains the functions that are available in the function. + * Only available in edge runtime functions. + * Defined in the esbuild edge plugin. + */ + var _ENTRIES: Entries; + /** + * The routes object that contains the routes that are available in the function. + * Only available in edge runtime functions. + * Defined in the esbuild edge plugin. + */ + var _ROUTES: EdgeRoute[]; + /** + * A map that is used in the edge runtime. + * Only available in edge runtime functions. + */ + var __storage__: Map; + /** + * AsyncContext available globally in the edge runtime. + * Only available in edge runtime functions. + */ + var AsyncContext: any; + /** + * AsyncLocalStorage available globally in the edge runtime. + * Only available in edge runtime functions. + * Defined in createEdgeBundle. + */ + var AsyncLocalStorage: any; + + /** + * The version of the Open Next runtime. + * Available everywhere. + * Defined in the esbuild banner. + */ + var openNextVersion: string; + + /** + * The queue that is used to handle ISR revalidation requests. + * Only available in main functions and in the external middleware with `enableCacheInterception` set to `true`. + * Defined in `createMainHandler` or in `adapter/middleware.ts`. + */ + var queue: Queue; +} diff --git a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts index 8dfd7346e..9ffcc9a05 100644 --- a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts +++ b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts @@ -73,6 +73,8 @@ globalThis.tagCache = tagCache; declare global { var queue: Queue; + var incrementalCache: any; + var tagCache: any; } globalThis.queue = queue; diff --git a/packages/tests-unit/tests/core/routing/util.test.ts b/packages/tests-unit/tests/core/routing/util.test.ts index 086e52487..cfeda7658 100644 --- a/packages/tests-unit/tests/core/routing/util.test.ts +++ b/packages/tests-unit/tests/core/routing/util.test.ts @@ -27,6 +27,9 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ declare global { var __als: any; + var lastModified: any; + var openNextDebug: boolean; + var openNextVersion: string; } type Res = { From cbd1b1bedac8b5c0e8763b3d3e7b41209a62e086 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 8 Nov 2024 12:15:43 +0100 Subject: [PATCH 02/13] edge, middleware and server all have access to __als --- .../open-next/src/adapters/edge-adapter.ts | 93 +++++---- packages/open-next/src/adapters/middleware.ts | 51 +++-- packages/open-next/src/adapters/util.ts | 10 + .../open-next/src/core/createMainHandler.ts | 2 - packages/open-next/src/core/requestHandler.ts | 186 +++++++++--------- packages/open-next/src/types/global.ts | 16 +- 6 files changed, 201 insertions(+), 157 deletions(-) diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index e5b2f1168..d277591db 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -10,55 +10,70 @@ import { convertBodyToReadableStream, convertToQueryString, } from "../core/routing/util"; +import { generateOpenNextRequestContext } from "./util"; + +globalThis.__als = new AsyncLocalStorage(); const defaultHandler = async ( internalEvent: InternalEvent, ): Promise => { globalThis.isEdgeRuntime = true; - const host = internalEvent.headers.host - ? `https://${internalEvent.headers.host}` - : "http://localhost:3000"; - const initialUrl = new URL(internalEvent.rawPath, host); - initialUrl.search = convertToQueryString(internalEvent.query); - const url = initialUrl.toString(); + const { requestId, pendingPromiseRunner, isISRRevalidation } = + generateOpenNextRequestContext(); - // @ts-expect-error - This is bundled - const handler = await import(`./middleware.mjs`); + // We run everything in the async local storage context so that it is available in edge runtime functions + return globalThis.__als.run( + { requestId, pendingPromiseRunner, isISRRevalidation }, + async () => { + const host = internalEvent.headers.host + ? `https://${internalEvent.headers.host}` + : "http://localhost:3000"; + const initialUrl = new URL(internalEvent.rawPath, host); + initialUrl.search = convertToQueryString(internalEvent.query); + const url = initialUrl.toString(); - const response: Response = await handler.default({ - headers: internalEvent.headers, - method: internalEvent.method || "GET", - nextConfig: { - basePath: NextConfig.basePath, - i18n: NextConfig.i18n, - trailingSlash: NextConfig.trailingSlash, - }, - url, - body: convertBodyToReadableStream(internalEvent.method, internalEvent.body), - }); - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - if (key.toLowerCase() === "set-cookie") { - responseHeaders[key] = responseHeaders[key] - ? [...responseHeaders[key], value] - : [value]; - } else { - responseHeaders[key] = value; - } - }); + // @ts-expect-error - This is bundled + const handler = await import(`./middleware.mjs`); - const body = - (response.body as ReadableStream) ?? emptyReadableStream(); + const response: Response = await handler.default({ + headers: internalEvent.headers, + method: internalEvent.method || "GET", + nextConfig: { + basePath: NextConfig.basePath, + i18n: NextConfig.i18n, + trailingSlash: NextConfig.trailingSlash, + }, + url, + body: convertBodyToReadableStream( + internalEvent.method, + internalEvent.body, + ), + }); + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + if (key.toLowerCase() === "set-cookie") { + responseHeaders[key] = responseHeaders[key] + ? [...responseHeaders[key], value] + : [value]; + } else { + responseHeaders[key] = value; + } + }); - return { - type: "core", - statusCode: response.status, - headers: responseHeaders, - body: body, - // Do we need to handle base64 encoded response? - isBase64Encoded: false, - }; + const body = + (response.body as ReadableStream) ?? emptyReadableStream(); + + return { + type: "core", + statusCode: response.status, + headers: responseHeaders, + body: body, + // Do we need to handle base64 encoded response? + isBase64Encoded: false, + }; + }, + ); }; export const handler = await createGenericHandler({ diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index b56930492..c925c9694 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -9,8 +9,10 @@ import { resolveTagCache, } from "../core/resolve"; import routingHandler from "../core/routingHandler"; +import { generateOpenNextRequestContext } from "./util"; globalThis.internalFetch = fetch; +globalThis.__als = new AsyncLocalStorage(); const defaultHandler = async (internalEvent: InternalEvent) => { const originResolver = await resolveOriginResolver( @@ -31,24 +33,37 @@ const defaultHandler = async (internalEvent: InternalEvent) => { ); //#endOverride - const result = await routingHandler(internalEvent); - if ("internalEvent" in result) { - debug("Middleware intercepted event", internalEvent); - let origin: Origin | false = false; - if (!result.isExternalRewrite) { - origin = await originResolver.resolve(result.internalEvent.rawPath); - } - return { - type: "middleware", - internalEvent: result.internalEvent, - isExternalRewrite: result.isExternalRewrite, - origin, - isISR: result.isISR, - }; - } - - debug("Middleware response", result); - return result; + const { requestId, pendingPromiseRunner, isISRRevalidation } = + generateOpenNextRequestContext(internalEvent.headers["x-isr"] === "1"); + + // We run everything in the async local storage context so that it is available in the external middleware + return globalThis.__als.run( + { + requestId, + pendingPromiseRunner, + isISRRevalidation, + }, + async () => { + const result = await routingHandler(internalEvent); + if ("internalEvent" in result) { + debug("Middleware intercepted event", internalEvent); + let origin: Origin | false = false; + if (!result.isExternalRewrite) { + origin = await originResolver.resolve(result.internalEvent.rawPath); + } + return { + type: "middleware", + internalEvent: result.internalEvent, + isExternalRewrite: result.isExternalRewrite, + origin, + isISR: result.isISR, + }; + } + + debug("Middleware response", result); + return result; + }, + ); }; export const handler = await createGenericHandler({ diff --git a/packages/open-next/src/adapters/util.ts b/packages/open-next/src/adapters/util.ts index 99a65564c..f832d6d7a 100644 --- a/packages/open-next/src/adapters/util.ts +++ b/packages/open-next/src/adapters/util.ts @@ -1,5 +1,7 @@ //TODO: We should probably move all the utils to a separate location +import { DetachedPromiseRunner } from "utils/promise"; + export function setNodeEnv() { process.env.NODE_ENV = process.env.NODE_ENV ?? "production"; } @@ -8,6 +10,14 @@ export function generateUniqueId() { return Math.random().toString(36).slice(2, 8); } +export function generateOpenNextRequestContext(isISRRevalidation = false) { + return { + requestId: Math.random().toString(36), + pendingPromiseRunner: new DetachedPromiseRunner(), + isISRRevalidation, + }; +} + /** * Create an array of arrays of size `chunkSize` from `items` * @param items Array of T diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts index addd32176..3c96d7f7a 100644 --- a/packages/open-next/src/core/createMainHandler.ts +++ b/packages/open-next/src/core/createMainHandler.ts @@ -1,6 +1,4 @@ import type { OpenNextConfig } from "types/open-next"; -import type { IncrementalCache, Queue } from "types/overrides"; -import type { DetachedPromiseRunner } from "utils/promise"; import { debug } from "../adapters/logger"; import { generateUniqueId } from "../adapters/util"; diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index 8dc393443..e4f449fd7 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -3,9 +3,9 @@ import { AsyncLocalStorage } from "node:async_hooks"; import type { OpenNextNodeResponse, StreamCreator } from "http/index.js"; import { IncomingMessage } from "http/index.js"; import type { InternalEvent, InternalResult } from "types/open-next"; -import { DetachedPromiseRunner } from "utils/promise"; import { debug, error, warn } from "../adapters/logger"; +import { generateOpenNextRequestContext } from "../adapters/util"; import { patchAsyncStorage } from "./patchAsyncStorage"; import { convertRes, createServerResponse, proxyRequest } from "./routing/util"; import type { MiddlewareOutputEvent } from "./routingHandler"; @@ -16,12 +16,7 @@ import routingHandler, { import { requestHandler, setNextjsPrebundledReact } from "./util"; // This is used to identify requests in the cache -globalThis.__als = new AsyncLocalStorage<{ - requestId: string; - pendingPromiseRunner: DetachedPromiseRunner; - isISRRevalidation?: boolean; - mergeHeadersPriority?: "middleware" | "handler"; -}>(); +globalThis.__als = new AsyncLocalStorage(); patchAsyncStorage(); @@ -29,94 +24,99 @@ export async function openNextHandler( internalEvent: InternalEvent, responseStreaming?: StreamCreator, ): Promise { - if (internalEvent.headers["x-forwarded-host"]) { - internalEvent.headers.host = internalEvent.headers["x-forwarded-host"]; - } - debug("internalEvent", internalEvent); - - let preprocessResult: InternalResult | MiddlewareOutputEvent = { - internalEvent: internalEvent, - isExternalRewrite: false, - origin: false, - isISR: false, - }; - - //#override withRouting - try { - preprocessResult = await routingHandler(internalEvent); - } catch (e) { - warn("Routing failed.", e); - } - //#endOverride - - const headers = - "type" in preprocessResult - ? preprocessResult.headers - : preprocessResult.internalEvent.headers; - - const overwrittenResponseHeaders: Record = {}; - - for (const [rawKey, value] of Object.entries(headers)) { - if (!rawKey.startsWith(MIDDLEWARE_HEADER_PREFIX)) { - continue; - } - const key = rawKey.slice(MIDDLEWARE_HEADER_PREFIX_LEN); - overwrittenResponseHeaders[key] = value; - headers[key] = value; - delete headers[rawKey]; - } - - if ("type" in preprocessResult) { - // response is used only in the streaming case - if (responseStreaming) { - const response = createServerResponse( - internalEvent, - headers, - responseStreaming, - ); - response.statusCode = preprocessResult.statusCode; - response.flushHeaders(); - const [bodyToConsume, bodyToReturn] = preprocessResult.body.tee(); - for await (const chunk of bodyToConsume) { - response.write(chunk); - } - response.end(); - preprocessResult.body = bodyToReturn; - } - return preprocessResult; - } + const { requestId, pendingPromiseRunner, isISRRevalidation } = + generateOpenNextRequestContext(internalEvent.headers["x-isr"] === "1"); - const preprocessedEvent = preprocessResult.internalEvent; - debug("preprocessedEvent", preprocessedEvent); - const reqProps = { - method: preprocessedEvent.method, - url: preprocessedEvent.url, - //WORKAROUND: We pass this header to the serverless function to mimic a prefetch request which will not trigger revalidation since we handle revalidation differently - // There is 3 way we can handle revalidation: - // 1. We could just let the revalidation go as normal, but due to race conditions the revalidation will be unreliable - // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh - // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests) - headers: { ...headers, purpose: "prefetch" }, - body: preprocessedEvent.body, - remoteAddress: preprocessedEvent.remoteAddress, - }; - const requestId = Math.random().toString(36); - const pendingPromiseRunner = new DetachedPromiseRunner(); - const isISRRevalidation = headers["x-isr"] === "1"; - const mergeHeadersPriority = globalThis.openNextConfig.dangerous - ?.headersAndCookiesPriority - ? globalThis.openNextConfig.dangerous.headersAndCookiesPriority( - preprocessedEvent, - ) - : "middleware"; - const internalResult = await globalThis.__als.run( + // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer + return globalThis.__als.run( { requestId, pendingPromiseRunner, isISRRevalidation, - mergeHeadersPriority, }, async () => { + if (internalEvent.headers["x-forwarded-host"]) { + internalEvent.headers.host = internalEvent.headers["x-forwarded-host"]; + } + debug("internalEvent", internalEvent); + + let preprocessResult: InternalResult | MiddlewareOutputEvent = { + internalEvent: internalEvent, + isExternalRewrite: false, + origin: false, + isISR: false, + }; + + //#override withRouting + try { + preprocessResult = await routingHandler(internalEvent); + } catch (e) { + warn("Routing failed.", e); + } + //#endOverride + + const headers = + "type" in preprocessResult + ? preprocessResult.headers + : preprocessResult.internalEvent.headers; + + const overwrittenResponseHeaders: Record = {}; + + for (const [rawKey, value] of Object.entries(headers)) { + if (!rawKey.startsWith(MIDDLEWARE_HEADER_PREFIX)) { + continue; + } + const key = rawKey.slice(MIDDLEWARE_HEADER_PREFIX_LEN); + overwrittenResponseHeaders[key] = value; + headers[key] = value; + delete headers[rawKey]; + } + + if ("type" in preprocessResult) { + // response is used only in the streaming case + if (responseStreaming) { + const response = createServerResponse( + internalEvent, + headers, + responseStreaming, + ); + response.statusCode = preprocessResult.statusCode; + response.flushHeaders(); + const [bodyToConsume, bodyToReturn] = preprocessResult.body.tee(); + for await (const chunk of bodyToConsume) { + response.write(chunk); + } + response.end(); + preprocessResult.body = bodyToReturn; + } + return preprocessResult; + } + const preprocessedEvent = preprocessResult.internalEvent; + debug("preprocessedEvent", preprocessedEvent); + const reqProps = { + method: preprocessedEvent.method, + url: preprocessedEvent.url, + //WORKAROUND: We pass this header to the serverless function to mimic a prefetch request which will not trigger revalidation since we handle revalidation differently + // There is 3 way we can handle revalidation: + // 1. We could just let the revalidation go as normal, but due to race condtions the revalidation will be unreliable + // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh + // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests) + headers: { ...headers, purpose: "prefetch" }, + body: preprocessedEvent.body, + remoteAddress: preprocessedEvent.remoteAddress, + }; + + const mergeHeadersPriority = globalThis.openNextConfig.dangerous + ?.headersAndCookiesPriority + ? globalThis.openNextConfig.dangerous.headersAndCookiesPriority( + preprocessedEvent, + ) + : "middleware"; + const store = globalThis.__als.getStore(); + if (store) { + store.mergeHeadersPriority = mergeHeadersPriority; + } + const preprocessedResult = preprocessResult as MiddlewareOutputEvent; const req = new IncomingMessage(reqProps); const res = createServerResponse( @@ -132,12 +132,17 @@ export async function openNextHandler( preprocessedResult.isExternalRewrite, ); - const { statusCode, headers, isBase64Encoded, body } = convertRes(res); + const { + statusCode, + headers: responseHeaders, + isBase64Encoded, + body, + } = convertRes(res); const internalResult = { type: internalEvent.type, statusCode, - headers, + headers: responseHeaders, body, isBase64Encoded, }; @@ -150,7 +155,6 @@ export async function openNextHandler( return internalResult; }, ); - return internalResult; } async function processRequest( diff --git a/packages/open-next/src/types/global.ts b/packages/open-next/src/types/global.ts index 0ef7272d1..39a7b8bf8 100644 --- a/packages/open-next/src/types/global.ts +++ b/packages/open-next/src/types/global.ts @@ -1,7 +1,7 @@ import type { AsyncLocalStorage } from "node:async_hooks"; import type { OutgoingHttpHeaders } from "node:http"; -import type {IncrementalCache, Queue, TagCache} from 'types/overrides'; +import type { IncrementalCache, Queue, TagCache } from "types/overrides"; import type { DetachedPromiseRunner } from "../utils/promise"; import type { OpenNextConfig } from "./open-next"; @@ -46,6 +46,13 @@ export interface EdgeRoute { regex: string[]; } +interface OpenNextRequestContext { + requestId: string; + pendingPromiseRunner: DetachedPromiseRunner; + isISRRevalidation?: boolean; + mergeHeadersPriority?: "middleware" | "handler"; +} + declare global { // Needed in the cache adapter /** @@ -135,12 +142,7 @@ declare global { * TODO: should be available everywhere in the future. * Defined in `requestHandler.ts`. */ - var __als: AsyncLocalStorage<{ - requestId: string; - pendingPromiseRunner: DetachedPromiseRunner; - isISRRevalidation?: boolean; - mergeHeadersPriority?: "middleware" | "handler"; - }>; + var __als: AsyncLocalStorage; /** * The entries object that contains the functions that are available in the function. From 0a341db4f9276607ac7afd271dd0c2b0163b40e0 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 8 Nov 2024 14:21:07 +0100 Subject: [PATCH 03/13] add support for a global waitUntil --- packages/open-next/src/adapters/edge-adapter.ts | 3 +++ packages/open-next/src/adapters/middleware.ts | 2 ++ packages/open-next/src/core/requestHandler.ts | 3 ++- .../open-next/src/overrides/wrappers/cloudflare.ts | 11 ++++++++++- packages/open-next/src/types/global.ts | 11 +++++++++-- packages/open-next/src/utils/promise.ts | 11 +++++++++++ 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index d277591db..4c246828b 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -1,6 +1,7 @@ import type { ReadableStream } from "node:stream/web"; import type { InternalEvent, InternalResult } from "types/open-next"; +import { awaitAllDetachedPromise } from "utils/promise"; import { emptyReadableStream } from "utils/stream"; // We import it like that so that the edge plugin can replace it @@ -64,6 +65,8 @@ const defaultHandler = async ( const body = (response.body as ReadableStream) ?? emptyReadableStream(); + await awaitAllDetachedPromise(); + return { type: "core", statusCode: response.status, diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index c925c9694..95752f12e 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -1,4 +1,5 @@ import type { InternalEvent, Origin } from "types/open-next"; +import { awaitAllDetachedPromise } from "utils/promise"; import { debug } from "../adapters/logger"; import { createGenericHandler } from "../core/createGenericHandler"; @@ -51,6 +52,7 @@ const defaultHandler = async (internalEvent: InternalEvent) => { if (!result.isExternalRewrite) { origin = await originResolver.resolve(result.internalEvent.rawPath); } + await awaitAllDetachedPromise(); return { type: "middleware", internalEvent: result.internalEvent, diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index e4f449fd7..e2460ada1 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import type { OpenNextNodeResponse, StreamCreator } from "http/index.js"; import { IncomingMessage } from "http/index.js"; import type { InternalEvent, InternalResult } from "types/open-next"; +import { awaitAllDetachedPromise } from "utils/promise"; import { debug, error, warn } from "../adapters/logger"; import { generateOpenNextRequestContext } from "../adapters/util"; @@ -150,7 +151,7 @@ export async function openNextHandler( // reset lastModified. We need to do this to avoid memory leaks delete globalThis.lastModified[requestId]; - await pendingPromiseRunner.await(); + await awaitAllDetachedPromise(); return internalResult; }, diff --git a/packages/open-next/src/overrides/wrappers/cloudflare.ts b/packages/open-next/src/overrides/wrappers/cloudflare.ts index 9320e3a99..6d52fde36 100644 --- a/packages/open-next/src/overrides/wrappers/cloudflare.ts +++ b/packages/open-next/src/overrides/wrappers/cloudflare.ts @@ -11,13 +11,22 @@ const cfPropNameToHeaderName = { longitude: "x-open-next-longitude", }; +interface WorkerContext { + waitUntil: (promise: Promise) => void; +} + const handler: WrapperHandler< InternalEvent, InternalResult | ({ type: "middleware" } & MiddlewareOutputEvent) > = async (handler, converter) => - async (request: Request, env: Record): Promise => { + async ( + request: Request, + env: Record, + ctx: WorkerContext, + ): Promise => { globalThis.process = process; + globalThis.openNextWaitUntil = ctx.waitUntil; // Set the environment variables // Cloudflare suggests to not override the process.env object but instead apply the values to it diff --git a/packages/open-next/src/types/global.ts b/packages/open-next/src/types/global.ts index 39a7b8bf8..b66361181 100644 --- a/packages/open-next/src/types/global.ts +++ b/packages/open-next/src/types/global.ts @@ -138,12 +138,19 @@ declare global { /** * The AsyncLocalStorage instance that is used to store the request context. - * Only available in main functions. + * Only available in main, middleware and edge functions. * TODO: should be available everywhere in the future. - * Defined in `requestHandler.ts`. + * Defined in `requestHandler.ts`, `middleware.ts` and `edge-adapter.ts`. */ var __als: AsyncLocalStorage; + /** + * The function that is used to run background tasks even after the response has been sent. + * This one is defined by the wrapper function as most of them don't need or support this feature. + * If not present, all the awaiting promises will be resolved before sending the response. + */ + var openNextWaitUntil: ((promise: Promise) => void) | undefined; + /** * The entries object that contains the functions that are available in the function. * Only available in edge runtime functions. diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts index d18d3410f..c612bf2a2 100644 --- a/packages/open-next/src/utils/promise.ts +++ b/packages/open-next/src/utils/promise.ts @@ -58,3 +58,14 @@ export class DetachedPromiseRunner { }); } } + +export async function awaitAllDetachedPromise() { + const promisesToAwait = + globalThis.__als.getStore()?.pendingPromiseRunner.await() ?? + Promise.resolve(); + if (globalThis.openNextWaitUntil) { + globalThis.openNextWaitUntil(promisesToAwait); + return; + } + await promisesToAwait; +} From 0e339a10104ce20ae013a405f8c0f350f20c8763 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 8 Nov 2024 15:30:47 +0100 Subject: [PATCH 04/13] next/after support --- .../open-next/src/adapters/edge-adapter.ts | 6 +++- packages/open-next/src/adapters/middleware.ts | 6 +++- packages/open-next/src/core/requestHandler.ts | 6 +++- packages/open-next/src/utils/promise.ts | 31 +++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index 4c246828b..293c276c4 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -1,7 +1,10 @@ import type { ReadableStream } from "node:stream/web"; import type { InternalEvent, InternalResult } from "types/open-next"; -import { awaitAllDetachedPromise } from "utils/promise"; +import { + awaitAllDetachedPromise, + provideNextAfterProvider, +} from "utils/promise"; import { emptyReadableStream } from "utils/stream"; // We import it like that so that the edge plugin can replace it @@ -27,6 +30,7 @@ const defaultHandler = async ( return globalThis.__als.run( { requestId, pendingPromiseRunner, isISRRevalidation }, async () => { + provideNextAfterProvider(); const host = internalEvent.headers.host ? `https://${internalEvent.headers.host}` : "http://localhost:3000"; diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index 95752f12e..996118930 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -1,5 +1,8 @@ import type { InternalEvent, Origin } from "types/open-next"; -import { awaitAllDetachedPromise } from "utils/promise"; +import { + awaitAllDetachedPromise, + provideNextAfterProvider, +} from "utils/promise"; import { debug } from "../adapters/logger"; import { createGenericHandler } from "../core/createGenericHandler"; @@ -45,6 +48,7 @@ const defaultHandler = async (internalEvent: InternalEvent) => { isISRRevalidation, }, async () => { + provideNextAfterProvider(); const result = await routingHandler(internalEvent); if ("internalEvent" in result) { debug("Middleware intercepted event", internalEvent); diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index e2460ada1..2ba5ab0b9 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -3,7 +3,10 @@ import { AsyncLocalStorage } from "node:async_hooks"; import type { OpenNextNodeResponse, StreamCreator } from "http/index.js"; import { IncomingMessage } from "http/index.js"; import type { InternalEvent, InternalResult } from "types/open-next"; -import { awaitAllDetachedPromise } from "utils/promise"; +import { + awaitAllDetachedPromise, + provideNextAfterProvider, +} from "utils/promise"; import { debug, error, warn } from "../adapters/logger"; import { generateOpenNextRequestContext } from "../adapters/util"; @@ -40,6 +43,7 @@ export async function openNextHandler( internalEvent.headers.host = internalEvent.headers["x-forwarded-host"]; } debug("internalEvent", internalEvent); + provideNextAfterProvider(); let preprocessResult: InternalResult | MiddlewareOutputEvent = { internalEvent: internalEvent, diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts index c612bf2a2..0b94c598d 100644 --- a/packages/open-next/src/utils/promise.ts +++ b/packages/open-next/src/utils/promise.ts @@ -69,3 +69,34 @@ export async function awaitAllDetachedPromise() { } await promisesToAwait; } + +export function provideNextAfterProvider() { + /** This should be considered unstable until `unstable_after` is stablized. */ + const NEXT_REQUEST_CONTEXT_SYMBOL = Symbol.for("@next/request-context"); + + // This is needed by some lib that relies on the vercel request context to properly await stuff. + // Remove this when vercel builder is updated to provide '@next/request-context'. + const VERCEL_REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context"); + + const openNextStoreContext = globalThis.__als.getStore(); + + const awaiter = + globalThis.openNextWaitUntil ?? + ((promise: Promise) => + openNextStoreContext?.pendingPromiseRunner.add(promise)); + + const nextAfterContext = { + get: () => ({ + waitUntil: awaiter, + }), + }; + + //@ts-expect-error + globalThis[NEXT_REQUEST_CONTEXT_SYMBOL] = nextAfterContext; + // We probably want to avoid providing this everytime since some lib may incorrectly think they are running in Vercel + // It may break stuff, but at the same time it will allow libs like `@vercel/otel` to work as expected + if (process.env.EMULATE_VERCEL_REQUEST_CONTEXT) { + //@ts-expect-error + globalThis[VERCEL_REQUEST_CONTEXT_SYMBOL] = nextAfterContext; + } +} From cba8b89787ce4afc308fc1d56bf642985531a8e6 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 8 Nov 2024 15:41:24 +0100 Subject: [PATCH 05/13] refactor --- .../open-next/src/adapters/edge-adapter.ts | 103 ++++++++---------- packages/open-next/src/adapters/middleware.ts | 19 +--- packages/open-next/src/adapters/util.ts | 10 -- packages/open-next/src/core/requestHandler.ts | 27 ++--- packages/open-next/src/utils/promise.ts | 23 +++- 5 files changed, 77 insertions(+), 105 deletions(-) diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index 293c276c4..0de490141 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -1,10 +1,7 @@ import type { ReadableStream } from "node:stream/web"; import type { InternalEvent, InternalResult } from "types/open-next"; -import { - awaitAllDetachedPromise, - provideNextAfterProvider, -} from "utils/promise"; +import { runWithOpenNextRequestContext } from "utils/promise"; import { emptyReadableStream } from "utils/stream"; // We import it like that so that the edge plugin can replace it @@ -14,7 +11,6 @@ import { convertBodyToReadableStream, convertToQueryString, } from "../core/routing/util"; -import { generateOpenNextRequestContext } from "./util"; globalThis.__als = new AsyncLocalStorage(); @@ -23,64 +19,55 @@ const defaultHandler = async ( ): Promise => { globalThis.isEdgeRuntime = true; - const { requestId, pendingPromiseRunner, isISRRevalidation } = - generateOpenNextRequestContext(); - // We run everything in the async local storage context so that it is available in edge runtime functions - return globalThis.__als.run( - { requestId, pendingPromiseRunner, isISRRevalidation }, - async () => { - provideNextAfterProvider(); - const host = internalEvent.headers.host - ? `https://${internalEvent.headers.host}` - : "http://localhost:3000"; - const initialUrl = new URL(internalEvent.rawPath, host); - initialUrl.search = convertToQueryString(internalEvent.query); - const url = initialUrl.toString(); - - // @ts-expect-error - This is bundled - const handler = await import(`./middleware.mjs`); + return runWithOpenNextRequestContext(false, async () => { + const host = internalEvent.headers.host + ? `https://${internalEvent.headers.host}` + : "http://localhost:3000"; + const initialUrl = new URL(internalEvent.rawPath, host); + initialUrl.search = convertToQueryString(internalEvent.query); + const url = initialUrl.toString(); - const response: Response = await handler.default({ - headers: internalEvent.headers, - method: internalEvent.method || "GET", - nextConfig: { - basePath: NextConfig.basePath, - i18n: NextConfig.i18n, - trailingSlash: NextConfig.trailingSlash, - }, - url, - body: convertBodyToReadableStream( - internalEvent.method, - internalEvent.body, - ), - }); - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - if (key.toLowerCase() === "set-cookie") { - responseHeaders[key] = responseHeaders[key] - ? [...responseHeaders[key], value] - : [value]; - } else { - responseHeaders[key] = value; - } - }); + // @ts-expect-error - This is bundled + const handler = await import(`./middleware.mjs`); - const body = - (response.body as ReadableStream) ?? emptyReadableStream(); + const response: Response = await handler.default({ + headers: internalEvent.headers, + method: internalEvent.method || "GET", + nextConfig: { + basePath: NextConfig.basePath, + i18n: NextConfig.i18n, + trailingSlash: NextConfig.trailingSlash, + }, + url, + body: convertBodyToReadableStream( + internalEvent.method, + internalEvent.body, + ), + }); + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + if (key.toLowerCase() === "set-cookie") { + responseHeaders[key] = responseHeaders[key] + ? [...responseHeaders[key], value] + : [value]; + } else { + responseHeaders[key] = value; + } + }); - await awaitAllDetachedPromise(); + const body = + (response.body as ReadableStream) ?? emptyReadableStream(); - return { - type: "core", - statusCode: response.status, - headers: responseHeaders, - body: body, - // Do we need to handle base64 encoded response? - isBase64Encoded: false, - }; - }, - ); + return { + type: "core", + statusCode: response.status, + headers: responseHeaders, + body: body, + // Do we need to handle base64 encoded response? + isBase64Encoded: false, + }; + }); }; export const handler = await createGenericHandler({ diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index 996118930..eaeeaa11f 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -1,8 +1,5 @@ import type { InternalEvent, Origin } from "types/open-next"; -import { - awaitAllDetachedPromise, - provideNextAfterProvider, -} from "utils/promise"; +import { runWithOpenNextRequestContext } from "utils/promise"; import { debug } from "../adapters/logger"; import { createGenericHandler } from "../core/createGenericHandler"; @@ -13,7 +10,6 @@ import { resolveTagCache, } from "../core/resolve"; import routingHandler from "../core/routingHandler"; -import { generateOpenNextRequestContext } from "./util"; globalThis.internalFetch = fetch; globalThis.__als = new AsyncLocalStorage(); @@ -37,18 +33,10 @@ const defaultHandler = async (internalEvent: InternalEvent) => { ); //#endOverride - const { requestId, pendingPromiseRunner, isISRRevalidation } = - generateOpenNextRequestContext(internalEvent.headers["x-isr"] === "1"); - // We run everything in the async local storage context so that it is available in the external middleware - return globalThis.__als.run( - { - requestId, - pendingPromiseRunner, - isISRRevalidation, - }, + return runWithOpenNextRequestContext( + internalEvent.headers["x-isr"] === "1", async () => { - provideNextAfterProvider(); const result = await routingHandler(internalEvent); if ("internalEvent" in result) { debug("Middleware intercepted event", internalEvent); @@ -56,7 +44,6 @@ const defaultHandler = async (internalEvent: InternalEvent) => { if (!result.isExternalRewrite) { origin = await originResolver.resolve(result.internalEvent.rawPath); } - await awaitAllDetachedPromise(); return { type: "middleware", internalEvent: result.internalEvent, diff --git a/packages/open-next/src/adapters/util.ts b/packages/open-next/src/adapters/util.ts index f832d6d7a..99a65564c 100644 --- a/packages/open-next/src/adapters/util.ts +++ b/packages/open-next/src/adapters/util.ts @@ -1,7 +1,5 @@ //TODO: We should probably move all the utils to a separate location -import { DetachedPromiseRunner } from "utils/promise"; - export function setNodeEnv() { process.env.NODE_ENV = process.env.NODE_ENV ?? "production"; } @@ -10,14 +8,6 @@ export function generateUniqueId() { return Math.random().toString(36).slice(2, 8); } -export function generateOpenNextRequestContext(isISRRevalidation = false) { - return { - requestId: Math.random().toString(36), - pendingPromiseRunner: new DetachedPromiseRunner(), - isISRRevalidation, - }; -} - /** * Create an array of arrays of size `chunkSize` from `items` * @param items Array of T diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index 2ba5ab0b9..81c743ee0 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -3,13 +3,9 @@ import { AsyncLocalStorage } from "node:async_hooks"; import type { OpenNextNodeResponse, StreamCreator } from "http/index.js"; import { IncomingMessage } from "http/index.js"; import type { InternalEvent, InternalResult } from "types/open-next"; -import { - awaitAllDetachedPromise, - provideNextAfterProvider, -} from "utils/promise"; +import { runWithOpenNextRequestContext } from "utils/promise"; import { debug, error, warn } from "../adapters/logger"; -import { generateOpenNextRequestContext } from "../adapters/util"; import { patchAsyncStorage } from "./patchAsyncStorage"; import { convertRes, createServerResponse, proxyRequest } from "./routing/util"; import type { MiddlewareOutputEvent } from "./routingHandler"; @@ -28,22 +24,14 @@ export async function openNextHandler( internalEvent: InternalEvent, responseStreaming?: StreamCreator, ): Promise { - const { requestId, pendingPromiseRunner, isISRRevalidation } = - generateOpenNextRequestContext(internalEvent.headers["x-isr"] === "1"); - // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer - return globalThis.__als.run( - { - requestId, - pendingPromiseRunner, - isISRRevalidation, - }, + return runWithOpenNextRequestContext( + internalEvent.headers["x-isr"] === "1", async () => { if (internalEvent.headers["x-forwarded-host"]) { internalEvent.headers.host = internalEvent.headers["x-forwarded-host"]; } debug("internalEvent", internalEvent); - provideNextAfterProvider(); let preprocessResult: InternalResult | MiddlewareOutputEvent = { internalEvent: internalEvent, @@ -151,11 +139,12 @@ export async function openNextHandler( body, isBase64Encoded, }; + const requestId = store?.requestId; - // reset lastModified. We need to do this to avoid memory leaks - delete globalThis.lastModified[requestId]; - - await awaitAllDetachedPromise(); + if (requestId) { + // reset lastModified. We need to do this to avoid memory leaks + delete globalThis.lastModified[requestId]; + } return internalResult; }, diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts index 0b94c598d..6d2249799 100644 --- a/packages/open-next/src/utils/promise.ts +++ b/packages/open-next/src/utils/promise.ts @@ -59,7 +59,7 @@ export class DetachedPromiseRunner { } } -export async function awaitAllDetachedPromise() { +async function awaitAllDetachedPromise() { const promisesToAwait = globalThis.__als.getStore()?.pendingPromiseRunner.await() ?? Promise.resolve(); @@ -70,7 +70,7 @@ export async function awaitAllDetachedPromise() { await promisesToAwait; } -export function provideNextAfterProvider() { +function provideNextAfterProvider() { /** This should be considered unstable until `unstable_after` is stablized. */ const NEXT_REQUEST_CONTEXT_SYMBOL = Symbol.for("@next/request-context"); @@ -100,3 +100,22 @@ export function provideNextAfterProvider() { globalThis[VERCEL_REQUEST_CONTEXT_SYMBOL] = nextAfterContext; } } + +export function runWithOpenNextRequestContext( + isISRRevalidation = false, + fn: () => Promise, +): Promise { + return globalThis.__als.run( + { + requestId: Math.random().toString(36), + pendingPromiseRunner: new DetachedPromiseRunner(), + isISRRevalidation, + }, + async () => { + provideNextAfterProvider(); + const result = await fn(); + await awaitAllDetachedPromise(); + return result; + }, + ); +} From 37d450ab40f2347d7c58e8e87d72034cdcc0db13 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 8 Nov 2024 17:03:42 +0100 Subject: [PATCH 06/13] review change --- packages/open-next/src/adapters/cache.ts | 4 +- .../open-next/src/adapters/edge-adapter.ts | 93 ++++++++++--------- packages/open-next/src/adapters/middleware.ts | 4 +- packages/open-next/src/core/requestHandler.ts | 6 +- packages/open-next/src/core/routing/util.ts | 7 +- .../open-next/src/http/openNextResponse.ts | 5 +- packages/open-next/src/types/global.ts | 2 +- packages/open-next/src/utils/promise.ts | 8 +- 8 files changed, 67 insertions(+), 62 deletions(-) diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts index 255d0d562..06da6ee8b 100644 --- a/packages/open-next/src/adapters/cache.ts +++ b/packages/open-next/src/adapters/cache.ts @@ -216,7 +216,7 @@ export default class S3Cache { // If some tags are stale we need to force revalidation return null; } - const requestId = globalThis.__als.getStore()?.requestId ?? ""; + const requestId = globalThis.__openNextAls.getStore()?.requestId ?? ""; globalThis.lastModified[requestId] = _lastModified; if (cacheData?.type === "route") { return { @@ -287,7 +287,7 @@ export default class S3Cache { } // This one might not even be necessary anymore // Better be safe than sorry - const detachedPromise = globalThis.__als + const detachedPromise = globalThis.__openNextAls .getStore() ?.pendingPromiseRunner.withResolvers(); try { diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index 0de490141..86e55a2ed 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -12,7 +12,7 @@ import { convertToQueryString, } from "../core/routing/util"; -globalThis.__als = new AsyncLocalStorage(); +globalThis.__openNextAls = new AsyncLocalStorage(); const defaultHandler = async ( internalEvent: InternalEvent, @@ -20,54 +20,57 @@ const defaultHandler = async ( globalThis.isEdgeRuntime = true; // We run everything in the async local storage context so that it is available in edge runtime functions - return runWithOpenNextRequestContext(false, async () => { - const host = internalEvent.headers.host - ? `https://${internalEvent.headers.host}` - : "http://localhost:3000"; - const initialUrl = new URL(internalEvent.rawPath, host); - initialUrl.search = convertToQueryString(internalEvent.query); - const url = initialUrl.toString(); + return runWithOpenNextRequestContext( + { isISRRevalidation: false }, + async () => { + const host = internalEvent.headers.host + ? `https://${internalEvent.headers.host}` + : "http://localhost:3000"; + const initialUrl = new URL(internalEvent.rawPath, host); + initialUrl.search = convertToQueryString(internalEvent.query); + const url = initialUrl.toString(); - // @ts-expect-error - This is bundled - const handler = await import(`./middleware.mjs`); + // @ts-expect-error - This is bundled + const handler = await import(`./middleware.mjs`); - const response: Response = await handler.default({ - headers: internalEvent.headers, - method: internalEvent.method || "GET", - nextConfig: { - basePath: NextConfig.basePath, - i18n: NextConfig.i18n, - trailingSlash: NextConfig.trailingSlash, - }, - url, - body: convertBodyToReadableStream( - internalEvent.method, - internalEvent.body, - ), - }); - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - if (key.toLowerCase() === "set-cookie") { - responseHeaders[key] = responseHeaders[key] - ? [...responseHeaders[key], value] - : [value]; - } else { - responseHeaders[key] = value; - } - }); + const response: Response = await handler.default({ + headers: internalEvent.headers, + method: internalEvent.method || "GET", + nextConfig: { + basePath: NextConfig.basePath, + i18n: NextConfig.i18n, + trailingSlash: NextConfig.trailingSlash, + }, + url, + body: convertBodyToReadableStream( + internalEvent.method, + internalEvent.body, + ), + }); + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + if (key.toLowerCase() === "set-cookie") { + responseHeaders[key] = responseHeaders[key] + ? [...responseHeaders[key], value] + : [value]; + } else { + responseHeaders[key] = value; + } + }); - const body = - (response.body as ReadableStream) ?? emptyReadableStream(); + const body = + (response.body as ReadableStream) ?? emptyReadableStream(); - return { - type: "core", - statusCode: response.status, - headers: responseHeaders, - body: body, - // Do we need to handle base64 encoded response? - isBase64Encoded: false, - }; - }); + return { + type: "core", + statusCode: response.status, + headers: responseHeaders, + body: body, + // Do we need to handle base64 encoded response? + isBase64Encoded: false, + }; + }, + ); }; export const handler = await createGenericHandler({ diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index eaeeaa11f..5ae669518 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -12,7 +12,7 @@ import { import routingHandler from "../core/routingHandler"; globalThis.internalFetch = fetch; -globalThis.__als = new AsyncLocalStorage(); +globalThis.__openNextAls = new AsyncLocalStorage(); const defaultHandler = async (internalEvent: InternalEvent) => { const originResolver = await resolveOriginResolver( @@ -35,7 +35,7 @@ const defaultHandler = async (internalEvent: InternalEvent) => { // We run everything in the async local storage context so that it is available in the external middleware return runWithOpenNextRequestContext( - internalEvent.headers["x-isr"] === "1", + { isISRRevalidation: internalEvent.headers["x-isr"] === "1" }, async () => { const result = await routingHandler(internalEvent); if ("internalEvent" in result) { diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index 81c743ee0..ffce10383 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -16,7 +16,7 @@ import routingHandler, { import { requestHandler, setNextjsPrebundledReact } from "./util"; // This is used to identify requests in the cache -globalThis.__als = new AsyncLocalStorage(); +globalThis.__openNextAls = new AsyncLocalStorage(); patchAsyncStorage(); @@ -26,7 +26,7 @@ export async function openNextHandler( ): Promise { // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer return runWithOpenNextRequestContext( - internalEvent.headers["x-isr"] === "1", + { isISRRevalidation: internalEvent.headers["x-isr"] === "1" }, async () => { if (internalEvent.headers["x-forwarded-host"]) { internalEvent.headers.host = internalEvent.headers["x-forwarded-host"]; @@ -105,7 +105,7 @@ export async function openNextHandler( preprocessedEvent, ) : "middleware"; - const store = globalThis.__als.getStore(); + const store = globalThis.__openNextAls.getStore(); if (store) { store.mergeHeadersPriority = mergeHeadersPriority; } diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index 88cc04680..312be2e54 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -322,7 +322,8 @@ export function addOpenNextHeader(headers: OutgoingHttpHeaders) { } if (globalThis.openNextDebug) { headers["X-OpenNext-Version"] = globalThis.openNextVersion; - headers["X-OpenNext-RequestId"] = globalThis.__als.getStore()?.requestId; + headers["X-OpenNext-RequestId"] = + globalThis.__openNextAls.getStore()?.requestId; } } @@ -362,7 +363,7 @@ export async function revalidateIfRequired( try { const hash = (str: string) => crypto.createHash("md5").update(str).digest("hex"); - const requestId = globalThis.__als.getStore()?.requestId ?? ""; + const requestId = globalThis.__openNextAls.getStore()?.requestId ?? ""; const lastModified = globalThis.lastModified[requestId] > 0 @@ -438,7 +439,7 @@ export function fixISRHeaders(headers: OutgoingHttpHeaders) { "private, no-cache, no-store, max-age=0, must-revalidate"; return; } - const requestId = globalThis.__als.getStore()?.requestId ?? ""; + const requestId = globalThis.__openNextAls.getStore()?.requestId ?? ""; const _lastModified = globalThis.lastModified[requestId] ?? 0; if (headers[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) { // calculate age diff --git a/packages/open-next/src/http/openNextResponse.ts b/packages/open-next/src/http/openNextResponse.ts index d2c89cf73..6b3c370ff 100644 --- a/packages/open-next/src/http/openNextResponse.ts +++ b/packages/open-next/src/http/openNextResponse.ts @@ -89,7 +89,7 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse { } // In some cases we might not have a store i.e. for example in the image optimization function // We may want to reconsider this in the future, it might be intersting to have access to this store everywhere - globalThis.__als + globalThis.__openNextAls ?.getStore() ?.pendingPromiseRunner.add(onEnd(this.headers)); const bodyLength = this.getBody().length; @@ -161,7 +161,8 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse { // Initial headers should be merged with the new headers // These initial headers are the one created either in the middleware or in next.config.js const mergeHeadersPriority = - globalThis.__als?.getStore()?.mergeHeadersPriority ?? "middleware"; + globalThis.__openNextAls?.getStore()?.mergeHeadersPriority ?? + "middleware"; if (this.initialHeaders) { this.headers = mergeHeadersPriority === "middleware" diff --git a/packages/open-next/src/types/global.ts b/packages/open-next/src/types/global.ts index b66361181..b8c061176 100644 --- a/packages/open-next/src/types/global.ts +++ b/packages/open-next/src/types/global.ts @@ -142,7 +142,7 @@ declare global { * TODO: should be available everywhere in the future. * Defined in `requestHandler.ts`, `middleware.ts` and `edge-adapter.ts`. */ - var __als: AsyncLocalStorage; + var __openNextAls: AsyncLocalStorage; /** * The function that is used to run background tasks even after the response has been sent. diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts index 6d2249799..386bf62a3 100644 --- a/packages/open-next/src/utils/promise.ts +++ b/packages/open-next/src/utils/promise.ts @@ -61,7 +61,7 @@ export class DetachedPromiseRunner { async function awaitAllDetachedPromise() { const promisesToAwait = - globalThis.__als.getStore()?.pendingPromiseRunner.await() ?? + globalThis.__openNextAls.getStore()?.pendingPromiseRunner.await() ?? Promise.resolve(); if (globalThis.openNextWaitUntil) { globalThis.openNextWaitUntil(promisesToAwait); @@ -78,7 +78,7 @@ function provideNextAfterProvider() { // Remove this when vercel builder is updated to provide '@next/request-context'. const VERCEL_REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context"); - const openNextStoreContext = globalThis.__als.getStore(); + const openNextStoreContext = globalThis.__openNextAls.getStore(); const awaiter = globalThis.openNextWaitUntil ?? @@ -102,10 +102,10 @@ function provideNextAfterProvider() { } export function runWithOpenNextRequestContext( - isISRRevalidation = false, + { isISRRevalidation }: { isISRRevalidation: boolean }, fn: () => Promise, ): Promise { - return globalThis.__als.run( + return globalThis.__openNextAls.run( { requestId: Math.random().toString(36), pendingPromiseRunner: new DetachedPromiseRunner(), From 3735c52d6a26162c25c99f6b689fdf42a369a547 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 8 Nov 2024 17:13:25 +0100 Subject: [PATCH 07/13] biome fix --- packages/open-next/src/types/global.ts | 1 + .../tests-unit/tests/core/routing/cacheInterceptor.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/open-next/src/types/global.ts b/packages/open-next/src/types/global.ts index b8c061176..2f9191a31 100644 --- a/packages/open-next/src/types/global.ts +++ b/packages/open-next/src/types/global.ts @@ -178,6 +178,7 @@ declare global { * Only available in edge runtime functions. * Defined in createEdgeBundle. */ + // biome-ignore lint/suspicious/noRedeclare: This is only needed in the edge runtime var AsyncLocalStorage: any; /** diff --git a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts index 9ffcc9a05..f460e7bc3 100644 --- a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts +++ b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts @@ -68,14 +68,14 @@ const queue = { send: vi.fn(), }; -globalThis.incrementalCache = incrementalCache; -globalThis.tagCache = tagCache; - declare global { var queue: Queue; var incrementalCache: any; var tagCache: any; } + +globalThis.incrementalCache = incrementalCache; +globalThis.tagCache = tagCache; globalThis.queue = queue; beforeEach(() => { From 14c5be3e6a545d62fc51b95a951179833ad1fd7e Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Fri, 8 Nov 2024 17:18:26 +0100 Subject: [PATCH 08/13] fix test and patchedAsyncStorage --- packages/open-next/src/build/patch/patchedAsyncStorage.ts | 2 +- packages/tests-unit/tests/adapters/cache.test.ts | 7 ++++++- packages/tests-unit/tests/core/routing/util.test.ts | 8 ++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/open-next/src/build/patch/patchedAsyncStorage.ts b/packages/open-next/src/build/patch/patchedAsyncStorage.ts index 64bbf6a51..70deae019 100644 --- a/packages/open-next/src/build/patch/patchedAsyncStorage.ts +++ b/packages/open-next/src/build/patch/patchedAsyncStorage.ts @@ -10,7 +10,7 @@ const staticGenerationAsyncStorage = { if (store) { store.isOnDemandRevalidate = store.isOnDemandRevalidate && - !globalThis.__als.getStore().isISRRevalidation; + !globalThis.__openNextAls.getStore().isISRRevalidation; } return store; }, diff --git a/packages/tests-unit/tests/adapters/cache.test.ts b/packages/tests-unit/tests/adapters/cache.test.ts index f24b6179b..242c4cb53 100644 --- a/packages/tests-unit/tests/adapters/cache.test.ts +++ b/packages/tests-unit/tests/adapters/cache.test.ts @@ -2,6 +2,11 @@ import S3Cache, { hasCacheExtension } from "@opennextjs/aws/adapters/cache.js"; import { vi } from "vitest"; +declare global { + var disableIncrementalCache: boolean; + var isNextAfter15: boolean; +} + describe("hasCacheExtension", () => { it("Should returns true if has an extension and it is a CacheExtension", () => { expect(hasCacheExtension("hello.cache")).toBeTruthy(); @@ -51,7 +56,7 @@ describe("S3Cache", () => { }; globalThis.tagCache = tagCache; - globalThis.__als = { + globalThis.__openNextAls = { getStore: vi.fn().mockReturnValue({ requestId: "123", pendingPromiseRunner: { diff --git a/packages/tests-unit/tests/core/routing/util.test.ts b/packages/tests-unit/tests/core/routing/util.test.ts index cfeda7658..b90d69f48 100644 --- a/packages/tests-unit/tests/core/routing/util.test.ts +++ b/packages/tests-unit/tests/core/routing/util.test.ts @@ -26,7 +26,7 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ })); declare global { - var __als: any; + var __openNextAls: any; var lastModified: any; var openNextDebug: boolean; var openNextVersion: string; @@ -531,7 +531,7 @@ describe("addOpenNextHeader", () => { delete config.NextConfig["poweredByHeader"]; globalThis.openNextDebug = false; globalThis.openNextVersion = "1.0.0"; - globalThis.__als = { + globalThis.__openNextAls = { getStore: () => ({ requestId: "123", }), @@ -583,7 +583,7 @@ describe("revalidateIfRequired", () => { name: "mock", }; - globalThis.__als = { + globalThis.__openNextAls = { getStore: vi.fn(), }; @@ -628,7 +628,7 @@ describe("revalidateIfRequired", () => { describe("fixISRHeaders", () => { beforeEach(() => { vi.useFakeTimers().setSystemTime("2024-01-02T00:00:00Z"); - globalThis.__als = { + globalThis.__openNextAls = { getStore: () => ({ requestId: "123", }), From 470a17dce88af44926d2411b3a2b8ac506557b85 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 11 Nov 2024 14:51:49 +0100 Subject: [PATCH 09/13] added some e2e test for next/after --- .../app/api/after/revalidate/route.tsx | 16 ++++++++++ .../app-router/app/api/after/ssg/route.tsx | 13 +++++++++ examples/app-router/next.config.ts | 3 ++ .../tests-e2e/tests/appRouter/after.test.ts | 29 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 examples/app-router/app/api/after/revalidate/route.tsx create mode 100644 examples/app-router/app/api/after/ssg/route.tsx create mode 100644 packages/tests-e2e/tests/appRouter/after.test.ts diff --git a/examples/app-router/app/api/after/revalidate/route.tsx b/examples/app-router/app/api/after/revalidate/route.tsx new file mode 100644 index 000000000..ec67cfcd3 --- /dev/null +++ b/examples/app-router/app/api/after/revalidate/route.tsx @@ -0,0 +1,16 @@ +import { revalidateTag } from "next/cache"; +import { NextResponse, unstable_after as after } from "next/server"; + +export function POST() { + after( + () => + new Promise((resolve) => + setTimeout(() => { + revalidateTag("date"); + resolve(); + }, 5000), + ), + ); + + return NextResponse.json({ success: true }); +} diff --git a/examples/app-router/app/api/after/ssg/route.tsx b/examples/app-router/app/api/after/ssg/route.tsx new file mode 100644 index 000000000..25e37e43c --- /dev/null +++ b/examples/app-router/app/api/after/ssg/route.tsx @@ -0,0 +1,13 @@ +import { unstable_cache } from "next/cache"; +import { NextResponse } from "next/server"; + +export const dynamic = "force-static"; + +export async function GET() { + const dateFn = unstable_cache(() => new Date().toISOString(), ["date"], { + tags: ["date"], + }); + const date = await dateFn(); + console.log("date", date); + return NextResponse.json({ date: date }); +} diff --git a/examples/app-router/next.config.ts b/examples/app-router/next.config.ts index 8a85d80a7..66cb0d30a 100644 --- a/examples/app-router/next.config.ts +++ b/examples/app-router/next.config.ts @@ -17,6 +17,9 @@ const nextConfig: NextConfig = { }, ], }, + experimental: { + after: true, + }, redirects: async () => { return [ { diff --git a/packages/tests-e2e/tests/appRouter/after.test.ts b/packages/tests-e2e/tests/appRouter/after.test.ts new file mode 100644 index 000000000..71ec3608e --- /dev/null +++ b/packages/tests-e2e/tests/appRouter/after.test.ts @@ -0,0 +1,29 @@ +import { test, expect } from "@playwright/test"; + +test("Next after", async ({ request }) => { + const initialSSG = await request.get("/api/after/ssg"); + expect(initialSSG.status()).toEqual(200); + const initialSSGJson = await initialSSG.json(); + + // We then fire a post request that will revalidate the SSG page 5 seconds after, but should respond immediately + const dateNow = Date.now(); + const revalidateSSG = await request.post("/api/after/revalidate"); + expect(revalidateSSG.status()).toEqual(200); + const revalidateSSGJson = await revalidateSSG.json(); + expect(revalidateSSGJson.success).toEqual(true); + // This request should take less than 5 seconds to respond + expect(Date.now() - dateNow).toBeLessThan(5000); + + // We want to immediately check if the SSG page has been revalidated, it should not have been + const notRevalidatedSSG = await request.get("/api/after/ssg"); + expect(notRevalidatedSSG.status()).toEqual(200); + const notRevalidatedSSGJson = await notRevalidatedSSG.json(); + expect(notRevalidatedSSGJson.date).toEqual(initialSSGJson.date); + + // We then wait for 5 seconds to ensure the SSG page has been revalidated + await new Promise((resolve) => setTimeout(resolve, 5000)); + const revalidatedSSG = await request.get("/api/after/ssg"); + expect(revalidatedSSG.status()).toEqual(200); + const revalidatedSSGJson = await revalidatedSSG.json(); + expect(revalidatedSSGJson.date).not.toEqual(initialSSGJson.date); +}); From 016c7adddc5cb114de762d25b0f07a06e42bdbfa Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 11 Nov 2024 14:53:18 +0100 Subject: [PATCH 10/13] changeset --- .changeset/new-dolphins-sleep.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-dolphins-sleep.md diff --git a/.changeset/new-dolphins-sleep.md b/.changeset/new-dolphins-sleep.md new file mode 100644 index 000000000..2811b86c4 --- /dev/null +++ b/.changeset/new-dolphins-sleep.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +add support for next/after From ecc820b4568455fceabeb7da0e9861b400356db3 Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Mon, 11 Nov 2024 15:06:28 +0100 Subject: [PATCH 11/13] fix lint --- packages/tests-e2e/tests/appRouter/after.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests-e2e/tests/appRouter/after.test.ts b/packages/tests-e2e/tests/appRouter/after.test.ts index 71ec3608e..c41b4c051 100644 --- a/packages/tests-e2e/tests/appRouter/after.test.ts +++ b/packages/tests-e2e/tests/appRouter/after.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; test("Next after", async ({ request }) => { const initialSSG = await request.get("/api/after/ssg"); From 919284b6ca5ccdf20db9b2bb10d37cb68e9f359e Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Tue, 12 Nov 2024 12:00:08 +0100 Subject: [PATCH 12/13] review fix --- .../app-router/app/api/after/revalidate/{route.tsx => route.ts} | 0 examples/app-router/app/api/after/ssg/{route.tsx => route.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/app-router/app/api/after/revalidate/{route.tsx => route.ts} (100%) rename examples/app-router/app/api/after/ssg/{route.tsx => route.ts} (100%) diff --git a/examples/app-router/app/api/after/revalidate/route.tsx b/examples/app-router/app/api/after/revalidate/route.ts similarity index 100% rename from examples/app-router/app/api/after/revalidate/route.tsx rename to examples/app-router/app/api/after/revalidate/route.ts diff --git a/examples/app-router/app/api/after/ssg/route.tsx b/examples/app-router/app/api/after/ssg/route.ts similarity index 100% rename from examples/app-router/app/api/after/ssg/route.tsx rename to examples/app-router/app/api/after/ssg/route.ts From 18457d709195b94fc1d329a5614b9a72281203ed Mon Sep 17 00:00:00 2001 From: Dorseuil Nicolas Date: Tue, 12 Nov 2024 19:00:23 +0100 Subject: [PATCH 13/13] review --- .changeset/new-dolphins-sleep.md | 1 + examples/app-router/app/api/after/ssg/route.ts | 3 +-- packages/open-next/src/utils/promise.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/new-dolphins-sleep.md b/.changeset/new-dolphins-sleep.md index 2811b86c4..d8831422a 100644 --- a/.changeset/new-dolphins-sleep.md +++ b/.changeset/new-dolphins-sleep.md @@ -3,3 +3,4 @@ --- add support for next/after +It can also be used to emulate vercel request context (the waitUntil) for lib that may rely on it on serverless env. It needs this env variable EMULATE_VERCEL_REQUEST_CONTEXT to be set to be enabled diff --git a/examples/app-router/app/api/after/ssg/route.ts b/examples/app-router/app/api/after/ssg/route.ts index 25e37e43c..2acf353fb 100644 --- a/examples/app-router/app/api/after/ssg/route.ts +++ b/examples/app-router/app/api/after/ssg/route.ts @@ -8,6 +8,5 @@ export async function GET() { tags: ["date"], }); const date = await dateFn(); - console.log("date", date); - return NextResponse.json({ date: date }); + return NextResponse.json({ date }); } diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts index 386bf62a3..e80f83acd 100644 --- a/packages/open-next/src/utils/promise.ts +++ b/packages/open-next/src/utils/promise.ts @@ -80,14 +80,14 @@ function provideNextAfterProvider() { const openNextStoreContext = globalThis.__openNextAls.getStore(); - const awaiter = + const waitUntil = globalThis.openNextWaitUntil ?? ((promise: Promise) => openNextStoreContext?.pendingPromiseRunner.add(promise)); const nextAfterContext = { get: () => ({ - waitUntil: awaiter, + waitUntil, }), };