Skip to content

Commit 41e1dca

Browse files
authored
Improve internal waitUntil utility (#56720)
⚠️ This is an internal API and will be removed soon. Please do not use. Refactors #56404 to have a better internal API used by both Edge SSR and Edge Route Handlers. This new API can buffer non-synchronously created "waitUntil"s even after the response has been returned, just need to make sure that there's at least one "waitUntil" queued. E.g.: ```js async function handler() { internal_runWithWaitUntil(async () => { // ← no await await taskA() internal_runWithWaitUntil(async () => { // ← no await await longRunningTaskB() }) await taskC() }) return Response(...) } ``` Internally, the "waitUntil" promise will resolve after all tasks are finished. cc @ijjk @cramforce @feedthejim as we've synced about some of the details here. Not using ALS because of some promise-related issues.
1 parent d735d31 commit 41e1dca

File tree

3 files changed

+69
-14
lines changed

3 files changed

+69
-14
lines changed

packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,7 @@ import { SERVER_RUNTIME } from '../../../../lib/constants'
1616
import type { PrerenderManifest } from '../../..'
1717
import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths'
1818
import type { SizeLimit } from '../../../../../types'
19-
20-
const NEXT_PRIVATE_GLOBAL_WAIT_UNTIL = Symbol.for(
21-
'__next_private_global_wait_until__'
22-
)
23-
24-
// @ts-ignore
25-
globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL] =
26-
// @ts-ignore
27-
globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL] || []
19+
import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until'
2820

2921
export function getRender({
3022
dev,
@@ -161,10 +153,10 @@ export function getRender({
161153
const result = await extendedRes.toResponse()
162154

163155
if (event && event.waitUntil) {
164-
event.waitUntil(
165-
// @ts-ignore
166-
Promise.all([...globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL]])
167-
)
156+
const waitUntilPromise = internal_getCurrentFunctionWaitUntil()
157+
if (waitUntilPromise) {
158+
event.waitUntil(waitUntilPromise)
159+
}
168160
}
169161

170162
// fetchMetrics is attached to the web request that going through the server,

packages/next/src/server/web/edge-route-module-wrapper.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { RouteMatcher } from '../future/route-matchers/route-matcher'
1313
import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash'
1414
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
1515
import type { NextFetchEvent } from './spec-extension/fetch-event'
16+
import { internal_getCurrentFunctionWaitUntil } from './internal-edge-wait-until'
1617

1718
type WrapOptions = Partial<Pick<AdapterOptions, 'page'>>
1819

@@ -114,9 +115,12 @@ export class EdgeRouteModuleWrapper {
114115
// Get the response from the handler.
115116
const res = await this.routeModule.handle(request, context)
116117

118+
const waitUntilPromises = [internal_getCurrentFunctionWaitUntil()]
117119
if (context.renderOpts.waitUntil) {
118-
evt.waitUntil(context.renderOpts.waitUntil)
120+
waitUntilPromises.push(context.renderOpts.waitUntil)
119121
}
122+
evt.waitUntil(Promise.all(waitUntilPromises))
123+
120124
return res
121125
}
122126
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// An internal module to expose the "waitUntil" API to Edge SSR and Edge Route Handler functions.
2+
// This is highly experimental and subject to change.
3+
4+
// We still need a global key to bypass Webpack's layering of modules.
5+
const GLOBAL_KEY = Symbol.for('__next_internal_waitUntil__')
6+
7+
const state: {
8+
waitUntilCounter: number
9+
waitUntilResolve: () => void
10+
waitUntilPromise: Promise<void> | null
11+
} =
12+
// @ts-ignore
13+
globalThis[GLOBAL_KEY] ||
14+
// @ts-ignore
15+
(globalThis[GLOBAL_KEY] = {
16+
waitUntilCounter: 0,
17+
waitUntilResolve: undefined,
18+
waitUntilPromise: null,
19+
})
20+
21+
// No matter how many concurrent requests are being handled, we want to make sure
22+
// that the final promise is only resolved once all of the waitUntil promises have
23+
// settled.
24+
function resolveOnePromise() {
25+
state.waitUntilCounter--
26+
if (state.waitUntilCounter === 0) {
27+
state.waitUntilResolve()
28+
state.waitUntilPromise = null
29+
}
30+
}
31+
32+
export function internal_getCurrentFunctionWaitUntil() {
33+
return state.waitUntilPromise
34+
}
35+
36+
export function internal_runWithWaitUntil<T>(fn: () => T): T {
37+
const result = fn()
38+
if (
39+
result &&
40+
typeof result === 'object' &&
41+
'then' in result &&
42+
'finally' in result &&
43+
typeof result.then === 'function' &&
44+
typeof result.finally === 'function'
45+
) {
46+
if (!state.waitUntilCounter) {
47+
// Create the promise for the next batch of waitUntil calls.
48+
state.waitUntilPromise = new Promise<void>((resolve) => {
49+
state.waitUntilResolve = resolve
50+
})
51+
}
52+
state.waitUntilCounter++
53+
return result.finally(() => {
54+
resolveOnePromise()
55+
})
56+
}
57+
58+
return result
59+
}

0 commit comments

Comments
 (0)