Skip to content
5 changes: 5 additions & 0 deletions .changeset/new-dolphins-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/aws": patch
---

add support for next/after
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add

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

16 changes: 16 additions & 0 deletions examples/app-router/app/api/after/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { revalidateTag } from "next/cache";
import { NextResponse, unstable_after as after } from "next/server";

export function POST() {
after(
() =>
new Promise<void>((resolve) =>
setTimeout(() => {
revalidateTag("date");
resolve();
}, 5000),
),
);

return NextResponse.json({ success: true });
}
13 changes: 13 additions & 0 deletions examples/app-router/app/api/after/ssg/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
3 changes: 3 additions & 0 deletions examples/app-router/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const nextConfig: NextConfig = {
},
],
},
experimental: {
after: true,
},
redirects: async () => {
return [
{
Expand Down
15 changes: 2 additions & 13 deletions packages/open-next/src/adapters/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { IncrementalCache, TagCache } from "types/overrides";

import { isBinaryContentType } from "./binary";
import { debug, error, warn } from "./logger";

Expand Down Expand Up @@ -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<string, number>;
var isNextAfter15: boolean;
}

function isFetchCache(
options?:
| boolean
Expand Down Expand Up @@ -227,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 {
Expand Down Expand Up @@ -298,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<void>();
try {
Expand Down
92 changes: 50 additions & 42 deletions packages/open-next/src/adapters/edge-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReadableStream } from "node:stream/web";

import type { InternalEvent, InternalResult } from "types/open-next";
import { runWithOpenNextRequestContext } from "utils/promise";
import { emptyReadableStream } from "utils/stream";

// We import it like that so that the edge plugin can replace it
Expand All @@ -11,58 +12,65 @@ import {
convertToQueryString,
} from "../core/routing/util";

declare global {
var isEdgeRuntime: true;
}
globalThis.__openNextAls = new AsyncLocalStorage();

const defaultHandler = async (
internalEvent: InternalEvent,
): Promise<InternalResult> => {
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();
// We run everything in the async local storage context so that it is available in edge runtime functions
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<string, string | string[]> = {};
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<string, string | string[]> = {};
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<Uint8Array>) ?? emptyReadableStream();
const body =
(response.body as ReadableStream<Uint8Array>) ?? 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({
Expand Down
4 changes: 0 additions & 4 deletions packages/open-next/src/adapters/logger.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { BaseOpenNextError } from "utils/error";

declare global {
var openNextDebug: boolean;
}

export function debug(...args: any[]) {

Check notice on line 3 in packages/open-next/src/adapters/logger.ts

View workflow job for this annotation

GitHub Actions / validate

lint/suspicious/noExplicitAny

Unexpected any. Specify a different type.
if (globalThis.openNextDebug) {
console.log(...args);
}
Expand Down
44 changes: 26 additions & 18 deletions packages/open-next/src/adapters/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { InternalEvent, Origin } from "types/open-next";
import { runWithOpenNextRequestContext } from "utils/promise";

import { debug } from "../adapters/logger";
import { createGenericHandler } from "../core/createGenericHandler";
Expand All @@ -11,6 +12,7 @@ import {
import routingHandler from "../core/routingHandler";

globalThis.internalFetch = fetch;
globalThis.__openNextAls = new AsyncLocalStorage();

const defaultHandler = async (internalEvent: InternalEvent) => {
const originResolver = await resolveOriginResolver(
Expand All @@ -31,24 +33,30 @@ 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;
// We run everything in the async local storage context so that it is available in the external middleware
return runWithOpenNextRequestContext(
{ isISRRevalidation: internalEvent.headers["x-isr"] === "1" },
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({
Expand Down
3 changes: 0 additions & 3 deletions packages/open-next/src/adapters/server-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/////////////
Expand Down
2 changes: 1 addition & 1 deletion packages/open-next/src/build/patch/patchedAsyncStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const staticGenerationAsyncStorage = {
if (store) {
store.isOnDemandRevalidate =
store.isOnDemandRevalidate &&
!globalThis.__als.getStore().isISRRevalidation;
!globalThis.__openNextAls.getStore().isISRRevalidation;
}
return store;
},
Expand Down
4 changes: 0 additions & 4 deletions packages/open-next/src/core/createGenericHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenNextConfig>;
}

type HandlerType =
| "imageOptimization"
| "revalidate"
Expand Down
17 changes: 0 additions & 17 deletions packages/open-next/src/core/createMainHandler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
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";

import { debug } from "../adapters/logger";
import { generateUniqueId } from "../adapters/util";
Expand All @@ -15,19 +11,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(
Expand Down
50 changes: 1 addition & 49 deletions packages/open-next/src/core/edgeFunctionHandler.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array>;
signal: AbortSignal;
}

interface Entries {
[k: string]: {
default: (props: { page: string; request: RequestData }) => Promise<{
response: Response;
waitUntil: Promise<void>;
}>;
};
}
declare global {
var _ENTRIES: Entries;
var _ROUTES: EdgeRoute[];
var __storage__: Map<unknown, unknown>;
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<RequestData, "page">;

Expand Down
Loading
Loading