[Cache Components] Dev - restart render on cache miss (#84088)
This PR replaces the previous approach to the dev-time cache warmup. On
full-page requests, we
1. We attempt an initial RSC render. It uses a RequestStore, but
includes a `cacheSignal` and a `prerenderResumeDataCache` to be filled
1. if there's no cache misses, we use the RSC render as is, and move
onto SSR
2. If there's any cache misses in the static stage (i.e. during the
first timeout), we treat the inital render as a prospective render, use
it only for filling caches, and discard the result
3. Once caches are filled, we render RSC again (using a fresh
RequestStore, with a filled `renderResumeDataCache`), and use this
second stream for SSR instead.
With this strategy, we minimize the amount of work we need to do for
cache warming -- once caches for a page are filled, we can render it in
one go, with no separate cache-filling render necessary.
---
A lot of the effort here goes into trying to reflect the behavior of a
static prerender into what we do during a dynamic render -- if something
would be a dynamic hole (hanging promise) in a prerender, we shouldn't
resolve it microtaskily (in the static stage). Instead, we have to delay
it into a future timeout (the dynamic stage). In this PR, we're still
using `makeDevtoolsIOAwarePromise` for this (i.e. just `new
Promise((resolve) => setTimeout(resolve))`), though this will change to
a more precise mechanism in #84644.
The timing of when promises resolve is currently tested in
`cache-components.dev-warmup.test.ts`, where we check the environment
labels on the server logs replayed in the browser, and use that to
verify which "phase" (Static/Dynamic) a given API resolves in.
This will be the foundation for prefetch validation, where we'll need to
snapshot what was rendered in each stage (Static/Runtime/Dynamic) and
use that to validate whether a prefetch would result in an instant
navigation.
Note that there's currently a bug involving `params` and `searchParams`
-- they can currently incorrectly resolve in the static phase (because
those promises are created before we start the actual render). We're not
(yet) relying on the timing of these promises for anything critical, so
it's fine to leave it for now. This bug will be addressed in #84644,
where I introduce a more precise mechanism for controlling the timing of
promise resolution, which also lets us separate "runtime" APIs like
`cookies` into a separate phase.
---
I've also left the current `spawnDynamicValidationInDev` codepath as is,
so after the we're done with all the render restarting, we'll still kick
off a validation prerender. This will also change in the future (and
could be optimized -- we've already ensured that all the caches are
filled, so we could e.g. skip the prospective render there) but I'm
trying not to do everything at once.
---------
Co-authored-by: Josh Story <story@hey.com>