Skip to content

Commit 5f0cbc8

Browse files
authored
next/after support and better AsyncLocalStorage context in general (#626)
* move globals * edge, middleware and server all have access to __als * add support for a global waitUntil * next/after support * refactor * review change * biome fix * fix test and patchedAsyncStorage * added some e2e test for next/after * changeset * fix lint * review fix * review
1 parent 08874fb commit 5f0cbc8

File tree

23 files changed

+533
-267
lines changed

23 files changed

+533
-267
lines changed

.changeset/new-dolphins-sleep.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
add support for next/after
6+
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
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { revalidateTag } from "next/cache";
2+
import { NextResponse, unstable_after as after } from "next/server";
3+
4+
export function POST() {
5+
after(
6+
() =>
7+
new Promise<void>((resolve) =>
8+
setTimeout(() => {
9+
revalidateTag("date");
10+
resolve();
11+
}, 5000),
12+
),
13+
);
14+
15+
return NextResponse.json({ success: true });
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { unstable_cache } from "next/cache";
2+
import { NextResponse } from "next/server";
3+
4+
export const dynamic = "force-static";
5+
6+
export async function GET() {
7+
const dateFn = unstable_cache(() => new Date().toISOString(), ["date"], {
8+
tags: ["date"],
9+
});
10+
const date = await dateFn();
11+
return NextResponse.json({ date });
12+
}

examples/app-router/next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const nextConfig: NextConfig = {
1717
},
1818
],
1919
},
20+
experimental: {
21+
after: true,
22+
},
2023
redirects: async () => {
2124
return [
2225
{

packages/open-next/src/adapters/cache.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { IncrementalCache, TagCache } from "types/overrides";
2-
31
import { isBinaryContentType } from "./binary";
42
import { debug, error, warn } from "./logger";
53

@@ -100,15 +98,6 @@ export function hasCacheExtension(key: string) {
10098
return CACHE_EXTENSION_REGEX.test(key);
10199
}
102100

103-
declare global {
104-
var incrementalCache: IncrementalCache;
105-
var tagCache: TagCache;
106-
var disableDynamoDBCache: boolean;
107-
var disableIncrementalCache: boolean;
108-
var lastModified: Record<string, number>;
109-
var isNextAfter15: boolean;
110-
}
111-
112101
function isFetchCache(
113102
options?:
114103
| boolean
@@ -227,7 +216,7 @@ export default class S3Cache {
227216
// If some tags are stale we need to force revalidation
228217
return null;
229218
}
230-
const requestId = globalThis.__als.getStore()?.requestId ?? "";
219+
const requestId = globalThis.__openNextAls.getStore()?.requestId ?? "";
231220
globalThis.lastModified[requestId] = _lastModified;
232221
if (cacheData?.type === "route") {
233222
return {
@@ -298,7 +287,7 @@ export default class S3Cache {
298287
}
299288
// This one might not even be necessary anymore
300289
// Better be safe than sorry
301-
const detachedPromise = globalThis.__als
290+
const detachedPromise = globalThis.__openNextAls
302291
.getStore()
303292
?.pendingPromiseRunner.withResolvers<void>();
304293
try {

packages/open-next/src/adapters/edge-adapter.ts

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ReadableStream } from "node:stream/web";
22

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

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

14-
declare global {
15-
var isEdgeRuntime: true;
16-
}
15+
globalThis.__openNextAls = new AsyncLocalStorage();
1716

1817
const defaultHandler = async (
1918
internalEvent: InternalEvent,
2019
): Promise<InternalResult> => {
2120
globalThis.isEdgeRuntime = true;
2221

23-
const host = internalEvent.headers.host
24-
? `https://${internalEvent.headers.host}`
25-
: "http://localhost:3000";
26-
const initialUrl = new URL(internalEvent.rawPath, host);
27-
initialUrl.search = convertToQueryString(internalEvent.query);
28-
const url = initialUrl.toString();
22+
// We run everything in the async local storage context so that it is available in edge runtime functions
23+
return runWithOpenNextRequestContext(
24+
{ isISRRevalidation: false },
25+
async () => {
26+
const host = internalEvent.headers.host
27+
? `https://${internalEvent.headers.host}`
28+
: "http://localhost:3000";
29+
const initialUrl = new URL(internalEvent.rawPath, host);
30+
initialUrl.search = convertToQueryString(internalEvent.query);
31+
const url = initialUrl.toString();
2932

30-
// @ts-expect-error - This is bundled
31-
const handler = await import(`./middleware.mjs`);
33+
// @ts-expect-error - This is bundled
34+
const handler = await import(`./middleware.mjs`);
3235

33-
const response: Response = await handler.default({
34-
headers: internalEvent.headers,
35-
method: internalEvent.method || "GET",
36-
nextConfig: {
37-
basePath: NextConfig.basePath,
38-
i18n: NextConfig.i18n,
39-
trailingSlash: NextConfig.trailingSlash,
40-
},
41-
url,
42-
body: convertBodyToReadableStream(internalEvent.method, internalEvent.body),
43-
});
44-
const responseHeaders: Record<string, string | string[]> = {};
45-
response.headers.forEach((value, key) => {
46-
if (key.toLowerCase() === "set-cookie") {
47-
responseHeaders[key] = responseHeaders[key]
48-
? [...responseHeaders[key], value]
49-
: [value];
50-
} else {
51-
responseHeaders[key] = value;
52-
}
53-
});
36+
const response: Response = await handler.default({
37+
headers: internalEvent.headers,
38+
method: internalEvent.method || "GET",
39+
nextConfig: {
40+
basePath: NextConfig.basePath,
41+
i18n: NextConfig.i18n,
42+
trailingSlash: NextConfig.trailingSlash,
43+
},
44+
url,
45+
body: convertBodyToReadableStream(
46+
internalEvent.method,
47+
internalEvent.body,
48+
),
49+
});
50+
const responseHeaders: Record<string, string | string[]> = {};
51+
response.headers.forEach((value, key) => {
52+
if (key.toLowerCase() === "set-cookie") {
53+
responseHeaders[key] = responseHeaders[key]
54+
? [...responseHeaders[key], value]
55+
: [value];
56+
} else {
57+
responseHeaders[key] = value;
58+
}
59+
});
5460

55-
const body =
56-
(response.body as ReadableStream<Uint8Array>) ?? emptyReadableStream();
61+
const body =
62+
(response.body as ReadableStream<Uint8Array>) ?? emptyReadableStream();
5763

58-
return {
59-
type: "core",
60-
statusCode: response.status,
61-
headers: responseHeaders,
62-
body: body,
63-
// Do we need to handle base64 encoded response?
64-
isBase64Encoded: false,
65-
};
64+
return {
65+
type: "core",
66+
statusCode: response.status,
67+
headers: responseHeaders,
68+
body: body,
69+
// Do we need to handle base64 encoded response?
70+
isBase64Encoded: false,
71+
};
72+
},
73+
);
6674
};
6775

6876
export const handler = await createGenericHandler({

packages/open-next/src/adapters/logger.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import type { BaseOpenNextError } from "utils/error";
22

3-
declare global {
4-
var openNextDebug: boolean;
5-
}
6-
73
export function debug(...args: any[]) {
84
if (globalThis.openNextDebug) {
95
console.log(...args);

packages/open-next/src/adapters/middleware.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { InternalEvent, Origin } from "types/open-next";
2+
import { runWithOpenNextRequestContext } from "utils/promise";
23

34
import { debug } from "../adapters/logger";
45
import { createGenericHandler } from "../core/createGenericHandler";
@@ -11,6 +12,7 @@ import {
1112
import routingHandler from "../core/routingHandler";
1213

1314
globalThis.internalFetch = fetch;
15+
globalThis.__openNextAls = new AsyncLocalStorage();
1416

1517
const defaultHandler = async (internalEvent: InternalEvent) => {
1618
const originResolver = await resolveOriginResolver(
@@ -31,24 +33,30 @@ const defaultHandler = async (internalEvent: InternalEvent) => {
3133
);
3234
//#endOverride
3335

34-
const result = await routingHandler(internalEvent);
35-
if ("internalEvent" in result) {
36-
debug("Middleware intercepted event", internalEvent);
37-
let origin: Origin | false = false;
38-
if (!result.isExternalRewrite) {
39-
origin = await originResolver.resolve(result.internalEvent.rawPath);
40-
}
41-
return {
42-
type: "middleware",
43-
internalEvent: result.internalEvent,
44-
isExternalRewrite: result.isExternalRewrite,
45-
origin,
46-
isISR: result.isISR,
47-
};
48-
}
49-
50-
debug("Middleware response", result);
51-
return result;
36+
// We run everything in the async local storage context so that it is available in the external middleware
37+
return runWithOpenNextRequestContext(
38+
{ isISRRevalidation: internalEvent.headers["x-isr"] === "1" },
39+
async () => {
40+
const result = await routingHandler(internalEvent);
41+
if ("internalEvent" in result) {
42+
debug("Middleware intercepted event", internalEvent);
43+
let origin: Origin | false = false;
44+
if (!result.isExternalRewrite) {
45+
origin = await originResolver.resolve(result.internalEvent.rawPath);
46+
}
47+
return {
48+
type: "middleware",
49+
internalEvent: result.internalEvent,
50+
isExternalRewrite: result.isExternalRewrite,
51+
origin,
52+
isISR: result.isISR,
53+
};
54+
}
55+
56+
debug("Middleware response", result);
57+
return result;
58+
},
59+
);
5260
};
5361

5462
export const handler = await createGenericHandler({

packages/open-next/src/adapters/server-adapter.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ setBuildIdEnv();
1212
setNextjsServerWorkingDirectory();
1313

1414
// Because next is messing with fetch, we have to make sure that we use an untouched version of fetch
15-
declare global {
16-
var internalFetch: typeof fetch;
17-
}
1815
globalThis.internalFetch = fetch;
1916

2017
/////////////

packages/open-next/src/build/patch/patchedAsyncStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const staticGenerationAsyncStorage = {
1010
if (store) {
1111
store.isOnDemandRevalidate =
1212
store.isOnDemandRevalidate &&
13-
!globalThis.__als.getStore().isISRRevalidation;
13+
!globalThis.__openNextAls.getStore().isISRRevalidation;
1414
}
1515
return store;
1616
},

0 commit comments

Comments
 (0)