* 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>
* feat(skip): add layout safety observation foundations
Introduce the per-layout observation primitives that later slices need
to decide whether retained static layouts can be reused. This slice
adds the machinery only; nothing reads the observations yet, so
behavior is unchanged.
- app-layout-param-observation: per-layout completeness, param keys,
structural scope, finite revalidate, request API, cacheLife, cache
tag, cacheable fetch, dynamic fetch, and unstable_cache observation
tracker.
- app-page-probe: bounded React server subtree probe that walks
layout-returned children including memo, forwardRef, and React.lazy
wrappers, enforces depth and node limits, and refuses single-use
iterable children.
- shims: thenable params observer for sync, await, destructure, and
enumeration. Cache, fetch, and unified request context record
unstable_cache, cacheable fetch, and request-scoped observation
state.
- app-page-params: structural segment-param scope so empty optional
catch-all targets are not treated as param-free downstream.
No skip transport behavior. Later slices wire the planner, manifest,
and encoder onto these observations.
* fix(skip): address review feedback on observation foundations
- Add currentFetchSoftTags reset in runWithIsolatedProbeDependencies to prevent soft tag leak from parent scope (matches runWithFetchCache isolation)
- Replace unnecessary JSON.stringify in cacheLife check with direct null comparison
- Add comment documenting intentional omission of markProbeComplete on probe errors (keeps completeness as unknown, safe fallback to render-and-send)
* fix(skip): record cacheable-fetch observation synchronously before first await
The patched fetch deferred observation and tag collection past
await buildFetchCacheKey(). If a layout probe started a cacheable
fetch and returned synchronously without awaiting it, the probe
snapshot would see cacheableFetchCount: 0 -- a false proof that
could let the skip planner reuse a stale retained layout.
Move observation and tag recording before the first await in the
cacheable branch. If key generation later fails and the fetch falls
back to dynamic, recording both is conservative (false unsafe costs
performance, not correctness).
Add an integration test that exercises the real patched fetch shim
without mocks to verify synchronous observation capture.
* fix(test): remove polluting fetch restore and catch background promise
Do not restore globalThis.fetch in test cleanup. ensureFetchPatch
sets a global _PATCH_KEY guard; restoring fetch without clearing
that marker makes later calls skip reinstallation silently.
Catch the background fetch promise inside the probe so the patched
fetch continuation (cache lookup, network fetch) cannot produce an
unhandled rejection after the synchronous assertions complete.
* chore: empty commit to trigger CI
* chore: empty commit to trigger CI
Route handlers used a local Object.assign(Promise.resolve(...), params)
helper that did not protect any well-known properties from shadowing.
Page params used a Proxy but only guarded then/catch/finally, missing
React's Promise status field and other well-known properties.
Next.js explicitly reserves a well-known property set (then, catch,
finally, status, toString, hasOwnProperty, etc.) so that params with
those names do not break Promise behaviour or React introspection.
This change:
- Expands the shared makeThenableParams shim to protect the full
well-known property set, matching Next.js reflect-utils.ts.
- Replaces the weak route-handler local helper with the shared shim.
- Adds unit tests for status and Object.prototype method shadowing.
- Adds App Router integration tests for pages and route handlers with
params named then, catch, finally, and status.
Ported from Next.js:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/utils/reflect-utils.ts
- https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/cache-components/cache-components.params.test.ts