Skip to content

Commit b59027a

Browse files
vicbconico974
andauthored
refactor: waitUntil passed around via ALS (#733)
Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
1 parent 50b3559 commit b59027a

File tree

16 files changed

+105
-46
lines changed

16 files changed

+105
-46
lines changed

.changeset/pink-papayas-smoke.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@opennextjs/aws": minor
3+
---
4+
5+
refactor: `waitUntil` passed around via ALS and `OpenNextHandler` signature has changed
6+
7+
BREAKING CHANGE: `waitUntil` is passed around via ALS to fix #713.
8+
9+
`globalThis.openNextWaitUntil` is no more available, you can access `waitUntil`
10+
on the ALS context: `globalThis.__openNextAls.getStore()`
11+
12+
The `OpenNextHandler` signature has changed: the second parameter was a `StreamCreator`.
13+
It was changed to be of type `OpenNextHandlerOptions` which has both a `streamCreator` key
14+
and a `waitUntil` key.
15+
16+
If you use a custom wrapper, you need to update the call to the handler as follow:
17+
18+
```ts
19+
// before
20+
globalThis.openNextWaitUntil = myWaitUntil;
21+
handler(internalEvent, myStreamCreator);
22+
23+
// after
24+
handler(internalEvent, { streamCreator: myStreamCreator, waitUntil: myWaitUntil });
25+
```

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { InternalEvent, InternalResult } from "types/open-next";
44
import { runWithOpenNextRequestContext } from "utils/promise";
55
import { emptyReadableStream } from "utils/stream";
66

7+
import type { OpenNextHandlerOptions } from "types/overrides";
78
// We import it like that so that the edge plugin can replace it
89
import { NextConfig } from "../adapters/config";
910
import { createGenericHandler } from "../core/createGenericHandler";
@@ -16,12 +17,13 @@ globalThis.__openNextAls = new AsyncLocalStorage();
1617

1718
const defaultHandler = async (
1819
internalEvent: InternalEvent,
20+
options?: OpenNextHandlerOptions,
1921
): Promise<InternalResult> => {
2022
globalThis.isEdgeRuntime = true;
2123

2224
// We run everything in the async local storage context so that it is available in edge runtime functions
2325
return runWithOpenNextRequestContext(
24-
{ isISRRevalidation: false },
26+
{ isISRRevalidation: false, waitUntil: options?.waitUntil },
2527
async () => {
2628
const host = internalEvent.headers.host
2729
? `https://${internalEvent.headers.host}`

packages/open-next/src/adapters/image-optimization-adapter.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
} from "types/open-next.js";
2626
import { emptyReadableStream, toReadableStream } from "utils/stream.js";
2727

28+
import type { OpenNextHandlerOptions } from "types/overrides.js";
2829
import { createGenericHandler } from "../core/createGenericHandler.js";
2930
import { resolveImageLoader } from "../core/resolve.js";
3031
import { debug, error } from "./logger.js";
@@ -58,7 +59,7 @@ export const handler = await createGenericHandler({
5859

5960
export async function defaultHandler(
6061
event: InternalEvent,
61-
streamCreator?: StreamCreator,
62+
options?: OpenNextHandlerOptions,
6263
): Promise<InternalResult> {
6364
// Images are handled via header and query param information.
6465
debug("handler event", event);
@@ -99,9 +100,9 @@ export async function defaultHandler(
99100
downloadHandler,
100101
);
101102

102-
return buildSuccessResponse(result, streamCreator, etag);
103+
return buildSuccessResponse(result, options?.streamCreator, etag);
103104
} catch (e: any) {
104-
return buildFailureResponse(e, streamCreator);
105+
return buildFailureResponse(e, options?.streamCreator);
105106
}
106107
}
107108

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
} from "types/open-next";
66
import { runWithOpenNextRequestContext } from "utils/promise";
77

8+
import type { OpenNextHandlerOptions } from "types/overrides";
89
import { debug, error } from "../adapters/logger";
910
import { createGenericHandler } from "../core/createGenericHandler";
1011
import {
@@ -24,6 +25,7 @@ globalThis.__openNextAls = new AsyncLocalStorage();
2425

2526
const defaultHandler = async (
2627
internalEvent: InternalEvent,
28+
options?: OpenNextHandlerOptions,
2729
): Promise<InternalResult | MiddlewareResult> => {
2830
const originResolver = await resolveOriginResolver(
2931
globalThis.openNextConfig.middleware?.originResolver,
@@ -49,7 +51,10 @@ const defaultHandler = async (
4951

5052
// We run everything in the async local storage context so that it is available in the external middleware
5153
return runWithOpenNextRequestContext(
52-
{ isISRRevalidation: internalEvent.headers["x-isr"] === "1" },
54+
{
55+
isISRRevalidation: internalEvent.headers["x-isr"] === "1",
56+
waitUntil: options?.waitUntil,
57+
},
5358
async () => {
5459
const result = await routingHandler(internalEvent);
5560
if ("internalEvent" in result) {

packages/open-next/src/core/requestHandler.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import type {
77
InternalResult,
88
ResolvedRoute,
99
RoutingResult,
10-
StreamCreator,
1110
} from "types/open-next";
1211
import { runWithOpenNextRequestContext } from "utils/promise";
1312

13+
import type { OpenNextHandlerOptions } from "types/overrides";
1414
import { debug, error, warn } from "../adapters/logger";
1515
import { patchAsyncStorage } from "./patchAsyncStorage";
1616
import { convertRes, createServerResponse } from "./routing/util";
@@ -29,12 +29,15 @@ patchAsyncStorage();
2929

3030
export async function openNextHandler(
3131
internalEvent: InternalEvent,
32-
responseStreaming?: StreamCreator,
32+
options?: OpenNextHandlerOptions,
3333
): Promise<InternalResult> {
3434
const initialHeaders = internalEvent.headers;
3535
// We run everything in the async local storage context so that it is available in the middleware as well as in NextServer
3636
return runWithOpenNextRequestContext(
37-
{ isISRRevalidation: initialHeaders["x-isr"] === "1" },
37+
{
38+
isISRRevalidation: initialHeaders["x-isr"] === "1",
39+
waitUntil: options?.waitUntil,
40+
},
3841
async () => {
3942
if (initialHeaders["x-forwarded-host"]) {
4043
initialHeaders.host = initialHeaders["x-forwarded-host"];
@@ -116,7 +119,7 @@ export async function openNextHandler(
116119

117120
if ("type" in routingResult) {
118121
// response is used only in the streaming case
119-
if (responseStreaming) {
122+
if (options?.streamCreator) {
120123
const response = createServerResponse(
121124
{
122125
internalEvent,
@@ -127,7 +130,7 @@ export async function openNextHandler(
127130
initialPath: internalEvent.rawPath,
128131
},
129132
headers,
130-
responseStreaming,
133+
options.streamCreator,
131134
);
132135
response.statusCode = routingResult.statusCode;
133136
response.flushHeaders();
@@ -171,7 +174,7 @@ export async function openNextHandler(
171174
const res = createServerResponse(
172175
routingResult,
173176
overwrittenResponseHeaders,
174-
responseStreaming,
177+
options?.streamCreator,
175178
);
176179

177180
await processRequest(req, res, preprocessedEvent);

packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const handler: WrapperHandler = async (handler, converter) =>
9494
},
9595
};
9696

97-
const response = await handler(internalEvent, streamCreator);
97+
const response = await handler(internalEvent, { streamCreator });
9898

9999
const isUsingEdge = globalThis.isEdgeRuntime ?? false;
100100
if (isUsingEdge) {

packages/open-next/src/overrides/wrappers/aws-lambda.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ const handler: WrapperHandler =
6161
},
6262
};
6363

64-
const response = await handler(internalEvent, fakeStream);
64+
const response = await handler(internalEvent, {
65+
streamCreator: fakeStream,
66+
});
6567

6668
return converter.convertTo(response, event);
6769
};

packages/open-next/src/overrides/wrappers/cloudflare-edge.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ const handler: WrapperHandler<
3333
ctx: WorkerContext,
3434
): Promise<Response> => {
3535
globalThis.process = process;
36-
globalThis.openNextWaitUntil = ctx.waitUntil.bind(ctx);
3736

3837
// Set the environment variables
3938
// Cloudflare suggests to not override the process.env object but instead apply the values to it
@@ -63,7 +62,9 @@ const handler: WrapperHandler<
6362
}
6463
}
6564

66-
const response = await handler(internalEvent);
65+
const response = await handler(internalEvent, {
66+
waitUntil: ctx.waitUntil.bind(ctx),
67+
});
6768

6869
const result: Response = await converter.convertTo(response);
6970

packages/open-next/src/overrides/wrappers/cloudflare-node.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const handler: WrapperHandler<InternalEvent, InternalResult> =
1818
ctx: any,
1919
): Promise<Response> => {
2020
globalThis.process = process;
21-
globalThis.openNextWaitUntil = ctx.waitUntil.bind(ctx);
2221

2322
// Set the environment variables
2423
// Cloudflare suggests to not override the process.env object but instead apply the values to it
@@ -75,7 +74,12 @@ const handler: WrapperHandler<InternalEvent, InternalResult> =
7574
},
7675
};
7776

78-
ctx.waitUntil(handler(internalEvent, streamCreator));
77+
ctx.waitUntil(
78+
handler(internalEvent, {
79+
streamCreator,
80+
waitUntil: ctx.waitUntil.bind(ctx),
81+
}),
82+
);
7983

8084
return promiseResponse;
8185
};

packages/open-next/src/overrides/wrappers/dummy.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import type { InternalEvent, StreamCreator } from "types/open-next";
2-
import type { Wrapper, WrapperHandler } from "types/overrides";
1+
import type { InternalEvent } from "types/open-next";
2+
import type {
3+
OpenNextHandlerOptions,
4+
Wrapper,
5+
WrapperHandler,
6+
} from "types/overrides";
37

48
const dummyWrapper: WrapperHandler = async (handler, converter) => {
5-
return async (event: InternalEvent, responseStream?: StreamCreator) => {
6-
return await handler(event, responseStream);
9+
return async (event: InternalEvent, options?: OpenNextHandlerOptions) => {
10+
return await handler(event, options);
711
};
812
};
913

packages/open-next/src/overrides/wrappers/express-dev.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,25 @@ const wrapper: WrapperHandler = async (handler, converter) => {
1313

1414
app.all("/_next/image", async (req, res) => {
1515
const internalEvent = await converter.convertFrom(req);
16-
const _res: StreamCreator = {
16+
const streamCreator: StreamCreator = {
1717
writeHeaders: (prelude) => {
1818
res.writeHead(prelude.statusCode, prelude.headers);
1919
return res;
2020
},
2121
};
22-
await imageHandler(internalEvent, _res);
22+
await imageHandler(internalEvent, { streamCreator });
2323
});
2424

2525
app.all("*paths", async (req, res) => {
2626
const internalEvent = await converter.convertFrom(req);
27-
const _res: StreamCreator = {
27+
const streamCreator: StreamCreator = {
2828
writeHeaders: (prelude) => {
2929
res.writeHead(prelude.statusCode, prelude.headers);
3030
return res;
3131
},
3232
onFinish: () => {},
3333
};
34-
await handler(internalEvent, _res);
34+
await handler(internalEvent, { streamCreator });
3535
});
3636

3737
const server = app.listen(

packages/open-next/src/overrides/wrappers/node.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { debug, error } from "../../adapters/logger";
88
const wrapper: WrapperHandler = async (handler, converter) => {
99
const server = createServer(async (req, res) => {
1010
const internalEvent = await converter.convertFrom(req);
11-
const _res: StreamCreator = {
11+
const streamCreator: StreamCreator = {
1212
writeHeaders: (prelude) => {
1313
res.setHeader("Set-Cookie", prelude.cookies);
1414
res.writeHead(prelude.statusCode, prelude.headers);
@@ -23,7 +23,7 @@ const wrapper: WrapperHandler = async (handler, converter) => {
2323
});
2424
res.end("OK");
2525
} else {
26-
await handler(internalEvent, _res);
26+
await handler(internalEvent, { streamCreator });
2727
}
2828
});
2929

packages/open-next/src/types/global.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
} from "types/overrides";
1111

1212
import type { DetachedPromiseRunner } from "../utils/promise";
13-
import type { OpenNextConfig } from "./open-next";
13+
import type { OpenNextConfig, WaitUntil } from "./open-next";
1414

1515
export interface RequestData {
1616
geo?: {
@@ -59,6 +59,7 @@ interface OpenNextRequestContext {
5959
pendingPromiseRunner: DetachedPromiseRunner;
6060
isISRRevalidation?: boolean;
6161
mergeHeadersPriority?: "middleware" | "handler";
62+
waitUntil?: WaitUntil;
6263
}
6364

6465
declare global {
@@ -152,13 +153,6 @@ declare global {
152153
*/
153154
var __openNextAls: AsyncLocalStorage<OpenNextRequestContext>;
154155

155-
/**
156-
* The function that is used to run background tasks even after the response has been sent.
157-
* This one is defined by the wrapper function as most of them don't need or support this feature.
158-
* If not present, all the awaiting promises will be resolved before sending the response.
159-
*/
160-
var openNextWaitUntil: ((promise: Promise<void>) => void) | undefined;
161-
162156
/**
163157
* The entries object that contains the functions that are available in the function.
164158
* Only available in edge runtime functions.

packages/open-next/src/types/open-next.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface StreamCreator {
4848
onFinish?: (length: number) => void;
4949
}
5050

51+
export type WaitUntil = (promise: Promise<void>) => void;
5152
export interface DangerousOptions {
5253
/**
5354
* The tag cache is used for revalidateTags and revalidatePath.

packages/open-next/src/types/overrides.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
Origin,
1111
ResolvedRoute,
1212
StreamCreator,
13+
WaitUntil,
1314
} from "./open-next";
1415

1516
// Queue
@@ -122,10 +123,17 @@ export type Wrapper<
122123
edgeRuntime?: boolean;
123124
};
124125

126+
export type OpenNextHandlerOptions = {
127+
// Create a `Writeable` for streaming responses.
128+
streamCreator?: StreamCreator;
129+
// Extends the liftetime of the runtime after the response is returned.
130+
waitUntil?: WaitUntil;
131+
};
132+
125133
export type OpenNextHandler<
126134
E extends BaseEventOrResult = InternalEvent,
127135
R extends BaseEventOrResult = InternalResult,
128-
> = (event: E, responseStream?: StreamCreator) => Promise<R>;
136+
> = (event: E, options?: OpenNextHandlerOptions) => Promise<R>;
129137

130138
export type Converter<
131139
E extends BaseEventOrResult = InternalEvent,

0 commit comments

Comments
 (0)