Skip to content

Commit d3d3f98

Browse files
committed
use already existing trackBackgroundWork helper from request context to handle next/after
1 parent 1c274df commit d3d3f98

File tree

5 files changed

+38
-29
lines changed

5 files changed

+38
-29
lines changed

src/build/templates/handler-monorepo.tmpl.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default async function (req, context) {
1616
tracing.start()
1717
}
1818

19-
const requestContext = createRequestContext(req)
19+
const requestContext = createRequestContext(req, context)
2020
const tracer = getTracer()
2121

2222
const handlerResponse = await runWithRequestContext(requestContext, () => {

src/build/templates/handler.tmpl.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default async function handler(req, context) {
1313
if (process.env.NETLIFY_OTLP_TRACE_EXPORTER_URL) {
1414
tracing.start()
1515
}
16-
const requestContext = createRequestContext(req)
16+
const requestContext = createRequestContext(req, context)
1717
const tracer = getTracer()
1818

1919
const handlerResponse = await runWithRequestContext(requestContext, () => {

src/run/handlers/request-context.cts

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { AsyncLocalStorage } from 'node:async_hooks'
22

3+
import type { Context } from '@netlify/functions'
34
import { LogLevel, systemLogger } from '@netlify/functions/internal'
45

56
import type { NetlifyCachedRouteValue } from '../../shared/cache-types.cjs'
67

78
type SystemLogger = typeof systemLogger
89

10+
// TODO: remove once https://github.com/netlify/serverless-functions-api/pull/219
11+
// is released and public types are updated
12+
export interface FutureContext extends Context {
13+
waitUntil?: (promise: Promise<unknown>) => void
14+
}
15+
916
export type RequestContext = {
1017
captureServerTiming: boolean
1118
responseCacheGetLastModified?: number
@@ -28,13 +35,17 @@ export type RequestContext = {
2835

2936
type RequestContextAsyncLocalStorage = AsyncLocalStorage<RequestContext>
3037

31-
export function createRequestContext(request?: Request): RequestContext {
38+
export function createRequestContext(request?: Request, context?: FutureContext): RequestContext {
3239
const backgroundWorkPromises: Promise<unknown>[] = []
3340

3441
return {
3542
captureServerTiming: request?.headers.has('x-next-debug-logging') ?? false,
3643
trackBackgroundWork: (promise) => {
37-
backgroundWorkPromises.push(promise)
44+
if (context?.waitUntil) {
45+
context.waitUntil(promise)
46+
} else {
47+
backgroundWorkPromises.push(promise)
48+
}
3849
},
3950
get backgroundWorkPromise() {
4051
return Promise.allSettled(backgroundWorkPromises)

src/run/handlers/server.ts

+19-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { OutgoingHttpHeaders } from 'http'
22

33
import { ComputeJsOutgoingMessage, toComputeResponse, toReqRes } from '@fastly/http-compute-js'
4-
import { Context } from '@netlify/functions'
54
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
65
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
76

@@ -14,12 +13,19 @@ import {
1413
} from '../headers.js'
1514
import { nextResponseProxy } from '../revalidate.js'
1615

17-
import { createRequestContext, getLogger, getRequestContext } from './request-context.cjs'
16+
import {
17+
createRequestContext,
18+
FutureContext,
19+
getLogger,
20+
getRequestContext,
21+
} from './request-context.cjs'
1822
import { getTracer } from './tracer.cjs'
19-
import { setWaitUntil } from './wait-until.cjs'
23+
import { setupWaitUntil } from './wait-until.cjs'
2024

2125
const nextImportPromise = import('../next.cjs')
2226

27+
setupWaitUntil()
28+
2329
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete
2430

2531
/**
@@ -45,15 +51,9 @@ const disableFaultyTransferEncodingHandling = (res: ComputeJsOutgoingMessage) =>
4551
}
4652
}
4753

48-
// TODO: remove once https://github.com/netlify/serverless-functions-api/pull/219
49-
// is released and public types are updated
50-
interface FutureContext extends Context {
51-
waitUntil?: (promise: Promise<unknown>) => void
52-
}
53-
5454
export default async (request: Request, context: FutureContext) => {
5555
const tracer = getTracer()
56-
setWaitUntil(context)
56+
5757
if (!nextHandler) {
5858
await tracer.withActiveSpan('initialize next server', async () => {
5959
// set the server config
@@ -129,19 +129,19 @@ export default async (request: Request, context: FutureContext) => {
129129
return new Response(body || null, response)
130130
}
131131

132-
if (context.waitUntil) {
133-
context.waitUntil(requestContext.backgroundWorkPromise)
134-
}
135-
136132
const keepOpenUntilNextFullyRendered = new TransformStream({
137133
async flush() {
138134
// it's important to keep the stream open until the next handler has finished
139135
await nextHandlerPromise
140-
if (!context.waitUntil) {
141-
// if waitUntil is not available, we have to keep response stream open until background promises are resolved
142-
// to ensure that all background work executes
143-
await requestContext.backgroundWorkPromise
144-
}
136+
137+
// Next.js relies on `close` event emitted by response to trigger running callback variant of `next/after`
138+
// however @fastly/http-compute-js never actually emits that event - so we have to emit it ourselves, otherwise
139+
// Next would never run the callback variant of `next/after`
140+
res.emit('close')
141+
142+
// if waitUntil is not available, we have to keep response stream open until background promises are resolved
143+
// to ensure that all background work executes
144+
await requestContext.backgroundWorkPromise
145145
},
146146
})
147147

src/run/handlers/wait-until.cts

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getRequestContext } from './request-context.cjs'
2+
13
/**
24
* @see https://github.com/vercel/next.js/blob/canary/packages/next/src/server/after/builtin-request-context.ts
35
*/
@@ -14,15 +16,11 @@ type GlobalThisWithRequestContext = typeof globalThis & {
1416
/**
1517
* Registers a `waitUntil` to be used by Next.js for next/after
1618
*/
17-
18-
export function setWaitUntil({ waitUntil }: { waitUntil?: (promise: Promise<unknown>) => void }) {
19-
if (!waitUntil) {
20-
return
21-
}
19+
export function setupWaitUntil() {
2220
// eslint-disable-next-line @typescript-eslint/no-extra-semi
2321
;(globalThis as GlobalThisWithRequestContext)[NEXT_REQUEST_CONTEXT_SYMBOL] = {
2422
get() {
25-
return { waitUntil }
23+
return { waitUntil: getRequestContext()?.trackBackgroundWork }
2624
},
2725
}
2826
}

0 commit comments

Comments
 (0)