Skip to content
16 changes: 16 additions & 0 deletions examples/app-router/app/api/after/revalidate/route.tsx
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.tsx
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 });
Copy link
Contributor

Choose a reason for hiding this comment

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

small nit

Suggested change
console.log("date", date);
return NextResponse.json({ date: date });
console.log({ date });
return NextResponse.json({ date });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think i'll remove the console.log entirely, not really useful

}
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,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);
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