-
Notifications
You must be signed in to change notification settings - Fork 89
feat: support after() #2717
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
feat: support after() #2717
Changes from 5 commits
d5aca78
beec061
e13fa25
8352645
81583b7
819be98
80de44f
c938bb6
1a8a135
7217c6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import type { OutgoingHttpHeaders } from 'http' | ||
|
||
import { ComputeJsOutgoingMessage, toComputeResponse, toReqRes } from '@fastly/http-compute-js' | ||
import { Context } from '@netlify/functions' | ||
import type { NextConfigComplete } from 'next/dist/server/config-shared.js' | ||
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js' | ||
|
||
|
@@ -16,9 +15,12 @@ import { nextResponseProxy } from '../revalidate.js' | |
|
||
import { createRequestContext, getLogger, getRequestContext } from './request-context.cjs' | ||
import { getTracer } from './tracer.cjs' | ||
import { setupWaitUntil } from './wait-until.cjs' | ||
|
||
const nextImportPromise = import('../next.cjs') | ||
|
||
setupWaitUntil() | ||
|
||
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete | ||
|
||
/** | ||
|
@@ -44,13 +46,7 @@ const disableFaultyTransferEncodingHandling = (res: ComputeJsOutgoingMessage) => | |
} | ||
} | ||
|
||
// TODO: remove once https://github.com/netlify/serverless-functions-api/pull/219 | ||
// is released and public types are updated | ||
interface FutureContext extends Context { | ||
waitUntil?: (promise: Promise<unknown>) => void | ||
} | ||
|
||
export default async (request: Request, context: FutureContext) => { | ||
export default async (request: Request) => { | ||
const tracer = getTracer() | ||
|
||
if (!nextHandler) { | ||
|
@@ -128,19 +124,19 @@ export default async (request: Request, context: FutureContext) => { | |
return new Response(body || null, response) | ||
} | ||
|
||
if (context.waitUntil) { | ||
context.waitUntil(requestContext.backgroundWorkPromise) | ||
} | ||
|
||
const keepOpenUntilNextFullyRendered = new TransformStream({ | ||
async flush() { | ||
// it's important to keep the stream open until the next handler has finished | ||
await nextHandlerPromise | ||
if (!context.waitUntil) { | ||
// if waitUntil is not available, we have to keep response stream open until background promises are resolved | ||
// to ensure that all background work executes | ||
await requestContext.backgroundWorkPromise | ||
} | ||
|
||
// Next.js relies on `close` event emitted by response to trigger running callback variant of `next/after` | ||
// however @fastly/http-compute-js never actually emits that event - so we have to emit it ourselves, | ||
// otherwise Next would never run the callback variant of `next/after` | ||
res.emit('close') | ||
Comment on lines
+132
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to support callback variant of
This is not needed to support promise variant like so:
|
||
|
||
// if waitUntil is not available, we have to keep response stream open until background promises are resolved | ||
// to ensure that all background work executes | ||
await requestContext.backgroundWorkPromise | ||
mrstork marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
}) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { getRequestContext } from './request-context.cjs' | ||
|
||
/** | ||
* @see https://github.com/vercel/next.js/blob/canary/packages/next/src/server/after/builtin-request-context.ts | ||
*/ | ||
const NEXT_REQUEST_CONTEXT_SYMBOL = Symbol.for('@next/request-context') | ||
|
||
export type NextJsRequestContext = { | ||
get(): { waitUntil?: (promise: Promise<unknown>) => void } | undefined | ||
} | ||
|
||
type GlobalThisWithRequestContext = typeof globalThis & { | ||
[NEXT_REQUEST_CONTEXT_SYMBOL]?: NextJsRequestContext | ||
} | ||
|
||
/** | ||
* Registers a `waitUntil` to be used by Next.js for next/after | ||
*/ | ||
export function setupWaitUntil() { | ||
// eslint-disable-next-line @typescript-eslint/no-extra-semi | ||
;(globalThis as GlobalThisWithRequestContext)[NEXT_REQUEST_CONTEXT_SYMBOL] = { | ||
get() { | ||
return { waitUntil: getRequestContext()?.trackBackgroundWork } | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export const revalidate = 3600 // arbitrarily long, just so that it doesn't happen during a test run | ||
|
||
export default async function Page() { | ||
const data = { | ||
timestamp: Date.now(), | ||
} | ||
console.log('/timestamp/key/[key] rendered', data) | ||
|
||
return <div id="page-info">{JSON.stringify(data)}</div> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { revalidatePath } from 'next/cache' | ||
import { unstable_after as after, connection } from 'next/server' | ||
|
||
export default async function Page() { | ||
await connection() | ||
after(async () => { | ||
// this will run after response was sent | ||
console.log('after() triggered') | ||
console.log('after() sleep 1s') | ||
await new Promise((resolve) => setTimeout(resolve, 1000)) | ||
|
||
console.log('after() revalidatePath /after/check') | ||
revalidatePath('/after/check') | ||
}) | ||
|
||
return <div>Page with after()</div> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,9 +71,13 @@ describe('version check', () => { | |
) | ||
}, | ||
) | ||
test('yarn monorepo multiple next versions site is compatible', { retry: 0 }, async () => { | ||
await smokeTest(selfCleaningFixtureFactories.yarnMonorepoMultipleNextVersionsSiteCompatible) | ||
}) | ||
test( | ||
'yarn monorepo multiple next versions site is compatible', | ||
{ retry: 0, timeout: 1_000 * 60 * 5 }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this change just bumps/sets timeout - we have 3 extensions being installed which we don't even use because site is in Netlify-testing account and that's pretty slow and this test case in particular was already slow before, but those 2 things combined meant that it was always timing out now |
||
async () => { | ||
await smokeTest(selfCleaningFixtureFactories.yarnMonorepoMultipleNextVersionsSiteCompatible) | ||
}, | ||
) | ||
|
||
test( | ||
'yarn monorepo multiple next versions site is incompatible should not deploy', | ||
|
Uh oh!
There was an error while loading. Please reload this page.