* feat(ppr): add fallback shell render lifecycle
* fix(ppr): address review feedback on PR #1715
- Make await params resolvable during fallback-shell prerendering by keeping
the base promise as Promise.resolve(plain) and only throwing the hanging
promise on sync fallback-key access.
- Defer createPprFallbackShellSuspensePromise to first fallback-key access so
hasDynamicBoundary and cache-task ignoring only fire when a fallback param is
actually read, matching the suspend only on access contract.
- Store and clear pendingAbortCleanup in preparePprFallbackShellFinalRender
to avoid leaking uncancellable abort timers.
- Add tests for await params resolving known params and for lazy dynamic-boundary
marking when only non-fallback params are accessed.
* fix(ppr): make awaited params preserve fallback-key suspension and getOwnPropertyDescriptor fully lazy
- Promise now resolves to a Proxy (resolvedParams) instead of raw plain.
The inner Proxy throws a hanging promise when a fallback param is accessed
after await, so `const resolved = await params; resolved.slug` suspends.
- Both outer and inner Proxy getOwnPropertyDescriptor traps return a getter
that creates the hanging promise lazily. Object.keys(params) no longer
triggers side effects merely by enumerating keys.
- Added tests for awaited-params suspension and for enumeration not marking
hasDynamicBoundary eagerly.
* fix(ppr): restructure thenable-params with typed Proxy handlers and conditional observer semantics
- Extract createResolvedParamsProxy with ProxyHandler<T> (no cast needed)
- Extract createThenableParamsProxy with single as ThenableParams<T> boundary cast
- Remove all as unknown as double casts
- In fallback-shell mode, Promise continuations no longer observe all keys;
let the resolved params proxy report actual property access lazily
- Fix getOwnPropertyDescriptor to return throwing getter instead of
throwing synchronously, preventing Object.keys from triggering suspension
- Restructure awaited-params test to wrap entire async flow in ALS scope
- Add test: awaiting params during fallback-shell observes only accessed known params
* test(ppr): assert hasDynamicBoundary stays false after known-param access in fallback-shell observer test
* fix(ppr): suspend escaped fallback params and settle warmup cache-ready without in-scope task
Addresses bonk review on #1715:
- Capture the originating fallback-shell state at params construction and
create the suspense promise against it (via the new
createPprFallbackShellSuspensePromiseForState), instead of re-reading the
ALS store lazily. A params object that escapes the shell scope now still
suspends on fallback-key access rather than leaking the raw `[slug]`
placeholder, and the `get` / `getOwnPropertyDescriptor` traps no longer
diverge on the out-of-scope path.
- During warmup, always call scheduleCacheReadyIfSettled when a dynamic
boundary is hit (not only in the final phase), so a bare headers()/cookies()
access with an empty cache-task stack still re-schedules the warmup
cache-ready settle. The call is a no-op while cache tasks are pending.
Adds focused tests for both paths.
* fix(ppr): keep cache-task counter and fallback suspension correct across phase transition
Addresses follow-up bonk review on #1715:
- Add a per-state cache epoch, bumped on the warmup->final transition. Cache
tasks capture the epoch at creation; a task that settles in a later epoch no
longer decrements the freshly-reset pendingCacheTasks counter below zero
(which would permanently block resolveCacheReadyIfSettled).
- Re-derive the memoized fallback-shell hanging promise when the live
AbortController changes (preparePprFallbackShellFinalRender swaps it on the
phase transition), so suspension always tracks the controller the lifecycle
will actually abort instead of a stale warmup signal.
Adds tests for the late warmup-task settle and the post-transition re-derive.
---------
Co-authored-by: James Anderson <james@eli.cx>