[Cache Components] Fast setImmediate (#86018)
> This place is not a place of honor... no highly esteemed deed is
commemorated here... nothing valued is here.
> What is here was dangerous and repulsive to us. This message is a
warning about danger.
you think you know when setImmediate is supposed to run? no you don't
---
This PR introduces a patch to the Node `setImmediate` builtin. The patch
is enabled by calling
`DANGEROUSLY_runPendingImmediatesAfterCurrentTask()`. All immediates
scheduled after that point (until the end of the task) will be captured
and executed right after that task (after `process.nextTick` and
`microtasks`). This applies to immediates scheduled from immediates as
well.
This is relevant when scheduling back-to-back timeouts for staged
rendering in Cache Components:
```ts
setTimeout(() => {
// runs first
DANGEROUSLY_runPendingImmediatesAfterCurrentTask() // enable the patch
setImmediate(() => {
// runs second (normally, it'd run last!)
})
})
setTimeout(() => {
// runs third
})
```
the immediate scheduled from inside the first timeout will **always**
run before the second timeout.
A side-effect of this is that `setImmediate` will no longer be
considered IO in the Cache Components rendering model, because
immediates will always run before we advance the stage (or abort a
prerender), and thus can't result in a dynamic hole. This brings the
runtime behavior in line with React Devtools, which does not show
`setImmediate` as IO.
This patch also has some observable differences in behavior from native
`setImmediate`, mostly to do with uncaught errors:
1. sync errors in `process.nextTick` no longer interrupt
`processTicksAndRejections` (which would make us move onto the next
event loop step, and run the rest of the nextTick queue after the next
task, breaking our scheduling). They're rethrown in a microtask, which
changes the timing of `uncaughtException` a bit.
2. unhandled rejections will trigger `unhandledRejection` after _all_
fast immediates are done executing, not after each immediate. This
happens because our userspace immediate scheduling relies on nextTick,
and [rejections are only processed after everything else in
`processTicksAndRejections` is
done](https://github.com/nodejs/node/blob/d546e7fd0bc3cbb4bcc2baae6f3aa44d2e81a413/lib/internal/process/task_queues.js#L104-L105).
We hope that these divergences in behavior are niche enough to not
affect any real world code. Both are potentially fixable by managing our
own nextTick queue, more, but we'll try to avoid that complexity for
now.