-
Notifications
You must be signed in to change notification settings - Fork 27.1k
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(after): allow request APIs in after (actions/handlers) #73345
Changes from all commits
3cbb05c
668a3b3
c81f577
4ae7751
761fa76
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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { AfterTaskAsyncStorage } from './after-task-async-storage.external' | ||
import { createAsyncLocalStorage } from './async-local-storage' | ||
|
||
export const afterTaskAsyncStorageInstance: AfterTaskAsyncStorage = | ||
createAsyncLocalStorage() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { AsyncLocalStorage } from 'async_hooks' | ||
|
||
// Share the instance module in the next-shared layer | ||
import { afterTaskAsyncStorageInstance as afterTaskAsyncStorage } from './after-task-async-storage-instance' with { 'turbopack-transition': 'next-shared' } | ||
import type { WorkUnitStore } from './work-unit-async-storage.external' | ||
|
||
export interface AfterTaskStore { | ||
/** The phase in which the topmost `unstable_after` was called. | ||
* | ||
* NOTE: Can be undefined when running `generateStaticParams`, | ||
* where we only have a `workStore`, no `workUnitStore`. | ||
Comment on lines
+10
to
+11
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. I'd argue that we should not permit 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. i guess maybe we merged #73217 while you were away? 😅 the use case here could be e.g.
i see no reason we should forbid it rly. if you feel strongly abt this we can revisit #73217 offline, but there's nothing new in this PR that's related to that, this is just a comment explaining why it might happen |
||
*/ | ||
readonly rootTaskSpawnPhase: WorkUnitStore['phase'] | undefined | ||
} | ||
|
||
export type AfterTaskAsyncStorage = AsyncLocalStorage<AfterTaskStore> | ||
|
||
export { afterTaskAsyncStorage } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,64 @@ | ||
import { cookies, headers } from 'next/headers' | ||
import { unstable_after as after, connection } from 'next/server' | ||
|
||
export function testRequestAPIs() { | ||
export function testRequestAPIs(/** @type {string} */ route) { | ||
after(async () => { | ||
try { | ||
await headers() | ||
console.log('headers(): ok') | ||
console.log(`[${route}] headers(): ok`) | ||
} catch (err) { | ||
console.error(err) | ||
console.error(`[${route}] headers(): error:`, err) | ||
} | ||
}) | ||
|
||
after(() => | ||
after(async () => { | ||
try { | ||
await headers() | ||
console.log(`[${route}] nested headers(): ok`) | ||
} catch (err) { | ||
console.error(`[${route}] nested headers(): error:`, err) | ||
} | ||
}) | ||
) | ||
|
||
after(async () => { | ||
try { | ||
await cookies() | ||
console.log('cookies(): ok') | ||
console.log(`[${route}] cookies(): ok`) | ||
} catch (err) { | ||
console.error(err) | ||
console.error(`[${route}] cookies(): error:`, err) | ||
} | ||
}) | ||
|
||
after(() => | ||
after(async () => { | ||
try { | ||
await cookies() | ||
console.log(`[${route}] nested cookies(): ok`) | ||
} catch (err) { | ||
console.error(`[${route}] nested cookies(): error:`, err) | ||
} | ||
}) | ||
) | ||
|
||
after(async () => { | ||
try { | ||
await connection() | ||
console.log('connection(): ok') | ||
console.log(`[${route}] connection(): ok`) | ||
} catch (err) { | ||
console.error(err) | ||
console.error(`[${route}] connection(): error:`, err) | ||
} | ||
}) | ||
|
||
after(() => | ||
after(async () => { | ||
try { | ||
await connection() | ||
console.log(`[${route}] nested connection(): ok`) | ||
} catch (err) { | ||
console.error(`[${route}] nested connection(): error:`, err) | ||
} | ||
}) | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import { testRequestAPIs } from '../helpers' | ||
|
||
export async function GET() { | ||
testRequestAPIs() | ||
testRequestAPIs('/request-apis/route-handler-dynamic') | ||
return new Response() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have great isolation already between action/render. If you put a non-blocking async function in an action and it is delayed enough that it doesn't run until the render phase has started you could get racey behavior where the speed of resolution determines the semantics of the after call. I think we decided this was ok b/c what useful thing could you do by spawning non-blockign async work in an action.
But I think you are right we need this new storage however unfortunate it might be though maybe it would be simpler to model it as a non-shadowing storage. Like conditionally run
bindSnapshot
inside aafterTaskAsyncStorage
only at the top level when you are inside a store that isrender
phase. If there are any nested after calls they won't create deeper afterTask store instances because they're not inrender
phase so they'll naturally inherit the root phaseThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure yeah but i don't think we can do a lot about that, it's racey bc the user wrote racey code 🤷♀️
soon enough i'm going to shadow it anyway to pass along callstack information (in dev)
https://github.com/vercel/next.js/pull/73449/files#diff-d6514c30f67dede0884866e69aa5c1ce48326f9fa2d515b5eb5e7162b5b0eb14R107
(i need this so that we can stitch together a sensible stack for the
after
callback -- instead of just showingp-queue
at the top, i want to tie it back to the place that initiated it)so even if i do it like that for this PR, i'd need to change it in the next one