Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve internal waitUntil utility #56720

Merged
merged 3 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,7 @@ import { SERVER_RUNTIME } from '../../../../lib/constants'
import type { PrerenderManifest } from '../../..'
import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths'
import type { SizeLimit } from '../../../../../types'

const NEXT_PRIVATE_GLOBAL_WAIT_UNTIL = Symbol.for(
'__next_private_global_wait_until__'
)

// @ts-ignore
globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL] =
// @ts-ignore
globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL] || []
import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until'

export function getRender({
dev,
Expand Down Expand Up @@ -161,10 +153,10 @@ export function getRender({
const result = await extendedRes.toResponse()

if (event && event.waitUntil) {
event.waitUntil(
// @ts-ignore
Promise.all([...globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL]])
)
const waitUntilPromise = internal_getCurrentFunctionWaitUntil()
if (waitUntilPromise) {
event.waitUntil(waitUntilPromise)
}
}

// fetchMetrics is attached to the web request that going through the server,
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/server/web/edge-route-module-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RouteMatcher } from '../future/route-matchers/route-matcher'
import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash'
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
import type { NextFetchEvent } from './spec-extension/fetch-event'
import { internal_getCurrentFunctionWaitUntil } from './internal-edge-wait-until'

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

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

const waitUntilPromises = [internal_getCurrentFunctionWaitUntil()]
if (context.renderOpts.waitUntil) {
evt.waitUntil(context.renderOpts.waitUntil)
waitUntilPromises.push(context.renderOpts.waitUntil)
}
evt.waitUntil(Promise.all(waitUntilPromises))

return res
}
}
59 changes: 59 additions & 0 deletions packages/next/src/server/web/internal-edge-wait-until.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// An internal module to expose the "waitUntil" API to Edge SSR and Edge Route Handler functions.
// This is highly experimental and subject to change.

// We still need a global key to bypass Webpack's layering of modules.
const GLOBAL_KEY = Symbol.for('__next_internal_waitUntil__')

const state: {
waitUntilCounter: number
waitUntilResolve: () => void
waitUntilPromise: Promise<void> | null
} =
// @ts-ignore
globalThis[GLOBAL_KEY] ||
// @ts-ignore
(globalThis[GLOBAL_KEY] = {
waitUntilCounter: 0,
waitUntilResolve: undefined,
waitUntilPromise: null,
})

// No matter how many concurrent requests are being handled, we want to make sure
// that the final promise is only resolved once all of the waitUntil promises have
// settled.
function resolveOnePromise() {
state.waitUntilCounter--
if (state.waitUntilCounter === 0) {
state.waitUntilResolve()
state.waitUntilPromise = null
}
}

export function internal_getCurrentFunctionWaitUntil() {
return state.waitUntilPromise
}

export function internal_runWithWaitUntil<T>(fn: () => T): T {
const result = fn()
ijjk marked this conversation as resolved.
Show resolved Hide resolved
if (
result &&
typeof result === 'object' &&
'then' in result &&
'finally' in result &&
typeof result.then === 'function' &&
typeof result.finally === 'function'
) {
if (!state.waitUntilCounter) {
// Create the promise for the next batch of waitUntil calls.
state.waitUntilPromise = new Promise<void>((resolve) => {
state.waitUntilResolve = resolve
})
}
state.waitUntilCounter++
return result.finally(() => {
resolveOnePromise()
})
}

return result
}
Loading