Skip to content

Commit 51e5373

Browse files
piehascorbic
andauthored
feat: support after() (#2717)
* test: upgrade deno used in tests * test: e2e simple next/after test * feat: support waitUntil * use already existing trackBackgroundWork helper from request context to handle next/after * test: increase timeout for one of smoke tests due to team-wide extensions installation time making it timeout * test: move test to dedicated fixture * chore: update outdated comment * chore: clarify awaiting backgroundWorkPromise * test: update assertion for next >=15.0.4-canary.18 * fix: support changed shape of getRequestHandlers return --------- Co-authored-by: Matt Kane <[email protected]>
1 parent 00e3a4b commit 51e5373

File tree

20 files changed

+208
-44
lines changed

20 files changed

+208
-44
lines changed

.github/workflows/pre-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ jobs:
2323
- name: Install Deno
2424
uses: denoland/setup-deno@v1
2525
with:
26-
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/e55f825bd985d3c92e21d1b765d71e70d5628fba/node/bridge.ts#L17
27-
deno-version: v1.37.0
26+
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/build/blob/main/packages/edge-bundler/node/bridge.ts#L20
27+
deno-version: v1.44.4
2828
- name: Extract tag and version
2929
id: extract
3030
run: |-

.github/workflows/release-please.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ jobs:
3131
- name: Install Deno
3232
uses: denoland/setup-deno@v1
3333
with:
34-
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/e55f825bd985d3c92e21d1b765d71e70d5628fba/node/bridge.ts#L17
35-
deno-version: v1.37.0
34+
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/build/blob/main/packages/edge-bundler/node/bridge.ts#L20
35+
deno-version: v1.44.4
3636
- name: Build
3737
run: npm run build
3838
if: ${{ steps.release.outputs.release_created }}

.github/workflows/run-tests.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ jobs:
6565
- name: Install Deno
6666
uses: denoland/setup-deno@v1
6767
with:
68-
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/main/node/bridge.ts#L17
69-
deno-version: v1.37.0
68+
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/build/blob/main/packages/edge-bundler/node/bridge.ts#L20
69+
deno-version: v1.44.4
7070
- name: 'Install dependencies'
7171
run: npm ci
7272
- name: 'Prepare Netlify CLI'
@@ -134,7 +134,7 @@ jobs:
134134
uses: denoland/setup-deno@v1
135135
with:
136136
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/e55f825bd985d3c92e21d1b765d71e70d5628fba/node/bridge.ts#L17
137-
deno-version: v1.37.0
137+
deno-version: v1.44.4
138138
- name: 'Install dependencies'
139139
run: npm ci
140140
- name: 'Build'
@@ -198,8 +198,8 @@ jobs:
198198
- name: Install Deno
199199
uses: denoland/setup-deno@v1
200200
with:
201-
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/main/node/bridge.ts#L17
202-
deno-version: v1.37.0
201+
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/build/blob/main/packages/edge-bundler/node/bridge.ts#L20
202+
deno-version: v1.44.4
203203
- name: 'Install dependencies'
204204
run: npm ci
205205
- name: 'Build'

.github/workflows/size-check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ jobs:
2323
- name: Install Deno
2424
uses: denoland/setup-deno@v1
2525
with:
26-
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/main/node/bridge.ts#L17
27-
deno-version: v1.37.0
26+
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/build/blob/main/packages/edge-bundler/node/bridge.ts#L20
27+
deno-version: v1.44.4
2828
- run: npm ci
2929

3030
- name: Package size report

.github/workflows/test-e2e.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ jobs:
161161
- name: Install Deno
162162
uses: denoland/setup-deno@v1
163163
with:
164-
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/edge-bundler/blob/main/node/bridge.ts#L17
165-
deno-version: v1.37.0
164+
# Should match the `DENO_VERSION_RANGE` from https://github.com/netlify/build/blob/main/packages/edge-bundler/node/bridge.ts#L20
165+
deno-version: v1.44.4
166166

167167
- name: install runtime
168168
run: npm install --ignore-scripts

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
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 public types are updated
11+
export interface FutureContext extends Context {
12+
waitUntil?: (promise: Promise<unknown>) => void
13+
}
14+
915
export type RequestContext = {
1016
captureServerTiming: boolean
1117
responseCacheGetLastModified?: number
@@ -16,25 +22,33 @@ export type RequestContext = {
1622
serverTiming?: string
1723
routeHandlerRevalidate?: NetlifyCachedRouteValue['revalidate']
1824
/**
19-
* Track promise running in the background and need to be waited for
25+
* Track promise running in the background and need to be waited for.
26+
* Uses `context.waitUntil` if available, otherwise stores promises to
27+
* await on.
2028
*/
2129
trackBackgroundWork: (promise: Promise<unknown>) => void
2230
/**
23-
* Promise that need to be executed even if response was already sent
31+
* Promise that need to be executed even if response was already sent.
32+
* If `context.waitUntil` is available this promise will be always resolved
33+
* because background work tracking was offloaded to `context.waitUntil`.
2434
*/
2535
backgroundWorkPromise: Promise<unknown>
2636
logger: SystemLogger
2737
}
2838

2939
type RequestContextAsyncLocalStorage = AsyncLocalStorage<RequestContext>
3040

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

3444
return {
3545
captureServerTiming: request?.headers.has('x-next-debug-logging') ?? false,
3646
trackBackgroundWork: (promise) => {
37-
backgroundWorkPromises.push(promise)
47+
if (context?.waitUntil) {
48+
context.waitUntil(promise)
49+
} else {
50+
backgroundWorkPromises.push(promise)
51+
}
3852
},
3953
get backgroundWorkPromise() {
4054
return Promise.allSettled(backgroundWorkPromises)

src/run/handlers/server.ts

Lines changed: 16 additions & 19 deletions
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

@@ -16,9 +15,12 @@ import { nextResponseProxy } from '../revalidate.js'
1615

1716
import { createRequestContext, getLogger, getRequestContext } from './request-context.cjs'
1817
import { getTracer } from './tracer.cjs'
18+
import { setupWaitUntil } from './wait-until.cjs'
1919

2020
const nextImportPromise = import('../next.cjs')
2121

22+
setupWaitUntil()
23+
2224
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete
2325

2426
/**
@@ -44,13 +46,7 @@ const disableFaultyTransferEncodingHandling = (res: ComputeJsOutgoingMessage) =>
4446
}
4547
}
4648

47-
// TODO: remove once https://github.com/netlify/serverless-functions-api/pull/219
48-
// is released and public types are updated
49-
interface FutureContext extends Context {
50-
waitUntil?: (promise: Promise<unknown>) => void
51-
}
52-
53-
export default async (request: Request, context: FutureContext) => {
49+
export default async (request: Request) => {
5450
const tracer = getTracer()
5551

5652
if (!nextHandler) {
@@ -60,10 +56,10 @@ export default async (request: Request, context: FutureContext) => {
6056
nextConfig = await getRunConfig()
6157
setRunConfig(nextConfig)
6258

63-
const { getMockedRequestHandlers } = await nextImportPromise
59+
const { getMockedRequestHandler } = await nextImportPromise
6460
const url = new URL(request.url)
6561

66-
;[nextHandler] = await getMockedRequestHandlers({
62+
nextHandler = await getMockedRequestHandler({
6763
port: Number(url.port) || 443,
6864
hostname: url.hostname,
6965
dir: process.cwd(),
@@ -128,19 +124,20 @@ export default async (request: Request, context: FutureContext) => {
128124
return new Response(body || null, response)
129125
}
130126

131-
if (context.waitUntil) {
132-
context.waitUntil(requestContext.backgroundWorkPromise)
133-
}
134-
135127
const keepOpenUntilNextFullyRendered = new TransformStream({
136128
async flush() {
137129
// it's important to keep the stream open until the next handler has finished
138130
await nextHandlerPromise
139-
if (!context.waitUntil) {
140-
// if waitUntil is not available, we have to keep response stream open until background promises are resolved
141-
// to ensure that all background work executes
142-
await requestContext.backgroundWorkPromise
143-
}
131+
132+
// Next.js relies on `close` event emitted by response to trigger running callback variant of `next/after`
133+
// however @fastly/http-compute-js never actually emits that event - so we have to emit it ourselves,
134+
// otherwise Next would never run the callback variant of `next/after`
135+
res.emit('close')
136+
137+
// We have to keep response stream open until tracked background promises that are don't use `context.waitUntil`
138+
// are resolved. If `context.waitUntil` is available, `requestContext.backgroundWorkPromise` will be empty
139+
// resolved promised and so awaiting it is no-op
140+
await requestContext.backgroundWorkPromise
144141
},
145142
})
146143

src/run/handlers/wait-until.cts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getRequestContext } from './request-context.cjs'
2+
3+
/**
4+
* @see https://github.com/vercel/next.js/blob/canary/packages/next/src/server/after/builtin-request-context.ts
5+
*/
6+
const NEXT_REQUEST_CONTEXT_SYMBOL = Symbol.for('@next/request-context')
7+
8+
export type NextJsRequestContext = {
9+
get(): { waitUntil?: (promise: Promise<unknown>) => void } | undefined
10+
}
11+
12+
type GlobalThisWithRequestContext = typeof globalThis & {
13+
[NEXT_REQUEST_CONTEXT_SYMBOL]?: NextJsRequestContext
14+
}
15+
16+
/**
17+
* Registers a `waitUntil` to be used by Next.js for next/after
18+
*/
19+
export function setupWaitUntil() {
20+
// eslint-disable-next-line @typescript-eslint/no-extra-semi
21+
;(globalThis as GlobalThisWithRequestContext)[NEXT_REQUEST_CONTEXT_SYMBOL] = {
22+
get() {
23+
return { waitUntil: getRequestContext()?.trackBackgroundWork }
24+
},
25+
}
26+
}

0 commit comments

Comments
 (0)