mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
v0.0.49
20 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b631b5da4a |
chore(deps): bump next.js version (#1112)
* chore(deps): bump next.js version * fix tests |
||
|
|
2009afde51 |
feat(app-router): trace approved visible commit outcomes (#1101)
Visible router state currently has an approved commit path, but the raw reducer remains an exported mutation escape hatch and approval traces only explain low-level reasons. That makes it harder to audit whether a payload became visible, was discarded, or forced a hard navigation. The violated invariant is that visibleCommitVersion should have one owner behind ApprovedVisibleCommit, with every approval decision carrying an operator-visible transaction outcome. Move the reducer implementation behind applyApprovedVisibleCommit, keep visibleCommitVersion advancement in that boundary, and prepend transaction trace codes for visible commits, no-commit decisions, and hard navigations. |
||
|
|
9469e7798e |
fix(router): gate refresh and traverse outcomes (#1102)
* fix(router): gate refresh and traverse outcomes Implements #726-CORE-05/06A and #726-CORE-06B from issue #726. Compare pending operation visible commit versions against the latest visible state before applying refresh, traverse, or same-URL navigation payloads. Return explicit navigation payload outcomes so stale and hard-navigation results settle without seeding visited-response cache entries. Make router.prefetch entries expose a cache-seeded outcome only after the snapshot is ready and pending work is cleared, and keep the ambient next/navigation declarations aligned with the runtime cache shape. * chore: retrigger ci |
||
|
|
ceb846f5b0 |
fix(router): reject stale same-url server action commits (#1100)
* fix(router): reject stale same-url server action commits Same-URL server action payloads could commit visible router state after newer visible work had already won. URL equality and activeNavigationId alone did not distinguish two different visible worlds at the same href. The lifecycle gate now compares a pending operation's started visibleCommitVersion against the current router state's visibleCommitVersion at approval time. Stale results keep returning their action value, but they no longer mutate visible state or trigger stale hard-navigation recovery. Tests cover out-of-order same-URL server actions and the visible-version stale path taking priority over root-boundary hard navigation. * fix(router): preserve server action initiation version Server action POST handling captured the router state before fetch but the same-URL commit helper previously rebased the pending operation when the response was decoded. If a newer visible commit landed while the POST was in flight, the old action could still look current. Thread the initiation state through same-URL action commit handling so startedVisibleCommitVersion reflects the world that submitted the action. Approval still compares against the live router state after payload decoding, so stale results keep their action value without mutating visible state. Add a hostile race test where the action response is processed after a newer same-URL visible commit. |
||
|
|
41d62e984e |
refactor(app-router): gate browser state writes with approved visible commits (#1090)
* refactor(app-router): gate browser state writes with approved visible commits Browser router commits currently mutate visible state from raw pending navigation actions after separate disposition checks. That leaves lifecycle approval implicit at each writer, which makes later stale-operation and planner work easier to bypass by accident. Introduce CommitDecision and ApprovedVisibleCommit as the proof object for visible browser commits. Route navigation, same-URL action, and HMR state writes through the approved commit path while preserving the legacy dispatch, skip, and hard-navigate dispositions for current callers. Tests cover approved navigate, replace, traverse, stale no-commit, and hard-navigation decisions, alongside the existing browser lifecycle regression coverage. * refactor(app-router): close visible commit approval escape hatch * ci: rerun checks * refactor(app-router): isolate visible commit approval authority * refactor(app-router): remove legacy commit disposition mirror Visible commit classification still exposed a translated legacy disposition beside the authoritative decision. That left same-URL server action handling able to read two equivalent fields and made the approval boundary less clear than it needs to be. Use the commit decision directly, remove the translation helper, keep the old state-level disposition private, and assert the non-visible trace reason codes in the approval tests. |
||
|
|
4342311377 |
test(app-router): lock navigation lifecycle settlement and root-layout hard navigation behaviour (#1053)
* test(app-router): lock navigation lifecycle settlement and root-layout hard navigation behaviour Adds 13 unit tests covering two Wave 0 baseline areas from issue #726: BASE-02 — Lock current lifecycle settlement behaviour (9 tests): - Three concurrent navigations with reverse-order payload resolution - Rapid sequential navigation: stale deferred payload does not overwrite - Stale cross-root navigation is skipped instead of hard-navigating - resolvePendingNavigationCommitDisposition exhausts skip/dispatch/hard-navigate - resolveAndClassifyNavigationCommit classifies skip when IDs diverge - Failed RSC payload settles pending router state cleanly BASE-03 — Lock root-layout hard navigation behaviour (4 tests): - renderNavigationPayload calls window.location.assign on root change - Hard-navigate settles the pending router state before navigating away - shouldHardNavigate negative/null path exhaustiveness - shouldHardNavigate positive path with distinct roots These tests close the App Router concurrency gap: the Pages Router has 10 race-condition tests in shims.test.ts but the App Router (which uses a completely different lifecycle mechanism) had zero equivalent tests for out-of-order payload resolution and stale commit rejection. * test: remove hallucinatory comments from app-browser-entry tests * test: remove duplicate shouldHardNavigate null-root assertion * test: remove low-signal helper truth-table tests, fix hard-navigate ordering assertion --------- Co-authored-by: James <james@eli.cx> |
||
|
|
5a18d36bfd |
feat(app-router): add navigation trace reason-code shell (#1063)
* feat(app-router): add navigation trace reason-code shell Navigation commit classification can now expose compact reason-code traces for the existing stale, root-boundary, unknown-root, and dispatch outcomes. The plain disposition-only path remains allocation-light for the browser navigation hot path. The old implicit string result made it hard for follow-up lifecycle work in #726 to attach structured operational context without inventing narrative logs at each call site. This adds a narrow trace contract and threads it through the same-URL classification helper while preserving current commit behavior. Covered by app-browser-entry tests for stale commit skips, root-boundary hard navigation, unknown root-layout fallback, same-URL classification traces, and trace field ownership. * fix(app-router): tighten navigation trace shell The OPS-01 trace shell exposed an append helper before any source path needed multi-entry traces, typed reason-code values too loosely, and let the combined same-URL classifier allocate a trace that the controller never consumed. Remove the unused append helper, derive reason-code unions from a literal key-to-code map, keep the classifier on the allocation-light disposition path, and add the missing normal dispatch trace coverage. |
||
|
|
57e6b7001e |
feat(app-router): track visible commit versions for browser operations (#1061)
Browser router state changed without a versioned visible commit baseline or a typed operation record, leaving later stale-result gates without a stable comparison point. Add pending and committed OperationRecord metadata, thread explicit operation lanes through browser navigation, refresh, traverse, same-URL server actions, and HMR, and advance visibleCommitVersion through the reducer path that accepts a new visible router-state commit. Also harden the prefetch-cache unit test to wait for async router.prefetch setup rather than relying on a fixed scheduler tick. |
||
|
|
9d5d1ce50d |
fix(app-router): validate RSC cache-busting params (#991)
* fix(app-router): validate RSC cache-busting params App Router RSC responses could vary by request headers while client fetches and server validation still relied on Vary-aware caches. CDNs that key only by URL could reuse a mounted-slot or interception-context RSC payload for a different request. Next.js validates a cache-busting _rsc search param against the RSC request headers and redirects malformed requests to the canonical URL. Vinext now applies the same invariant to its .rsc request model, centralizes the RSC Vary header set in normal server modules, and de-duplicates middleware Vary additions. Tests cover missing and stale _rsc values, variant-header hashes, generated-entry delegation, browser RSC request markers, response Vary propagation, and App Router RSC integration. * fix(app-router): hide RSC cache tokens from navigation URLs * fix(app-router): hide RSC cache token from searchParams * fix(app-router): address RSC cache-busting review * fix(app-router): preserve HTML redirect destination for direct .rsc navigations Config redirects from next.config.js were unconditionally converted to .rsc destinations for any request whose URL ended in .rsc. This broke direct browser navigations to .rsc URLs (e.g. a bookmark or external link) because the browser was redirected to an RSC payload it could not render. Client-side RSC fetches always send the RSC: 1 header, so restrict the .rsc destination conversion to requests that carry that header. Direct browser hits without the header continue to redirect to the canonical HTML URL, matching the pre-PR behaviour for config redirects. |
||
|
|
fffda16106 |
feat(dev): React-based runtime error overlay (#1029)
Surfaces three error sources that previously only reached the console: React render errors caught by an error.tsx boundary (onCaughtError), render errors with no boundary above them (onUncaughtError), and plain script errors / unhandled promise rejections via window listeners. The overlay renders into a separate createRoot mounted on a detached div appended to body, so it survives an unmount of the main hydrateRoot tree — necessary because most errors we want to surface take that tree down. Includes a /dev-overlay-test fixture and Playwright spec covering all three error sources, dismiss, and prev/next pagination across multiple errors. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ce3af6aa21 |
refactor(app-router): extract browser navigation lifecycle controller (#1019)
* refactor(app-router): extract browser navigation lifecycle controller * fix(app-router): separate render ids from navigation ids * fix(app-router): satisfy ci lint rules * fix(app-router): narrow controller surface, add hmrReplaceTree, restore lost comments, fix tests |
||
|
|
04befb5342 |
fix(navigation): hard-nav to target when render error tears down the tree (#1028)
When an RSC payload contains an error reference for a slot (e.g. a server component throws), createPendingNavigationCommit only awaits the top-level elements promise, so the dispatch goes through and React renders. The slot throws when use() unwraps the rejected reference. Without an error.tsx boundary above NavigationCommitSignal, the entire root unmounts — and because the URL update lives in NavigationCommitSignal's pre-paint layout effect, the browser is left on the previous URL with a blank page and a console-only error. Track the in-flight navigation target in a module-level pendingNavigationRecoveryHref, set it just before dispatchBrowserTree and clear it once the commit effect updates history. Add an onUncaughtError handler to hydrateRoot that reads the recovery href at fire time and window.location.assign()s to it, so the user lands on the URL they clicked and the server can render its actual error UI for that route. Wire the handler in both dev and prod (previously only onCaughtError was registered in dev). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e81820279e |
fix: preserve console output for caught app errors in dev (#862)
* fix: preserve console output for caught app errors in dev * address bonk review comments on PR 862 --------- Co-authored-by: James <james@eli.cx> |
||
|
|
8f224051b7 |
Preserve intercepted app-router state across server actions (#854)
* Preserve interception context on app-router actions * Keep intercept slot overrides on action rerenders * Pin React versions in static-export fixture * refactor(app-page-request): unify intercept resolution into a single state machine Collapse the parallel GET/POST intercept lookup paths into one shared internal helper that returns a tagged union: none, current-route, or source-route. Both resolveAppPageInterceptMatch and resolveAppPageActionRerenderTarget now read from the same lookup result, eliminating the double findIntercept() scan on current-route fallbacks. Also add the two missing resolveAppPageActionRerenderTarget fallthrough test cases (no intercept, current-route intercept with no matching action route) and a regression assertion that the current-route branch only performs one intercept lookup. |
||
|
|
18237955fa |
feat: static/dynamic layout detection for skip-header optimization (#767)
* feat: add classifyLayoutSegmentConfig for layout segment config detection
Reads `export const dynamic` and `export const revalidate` from layout
source files to classify them as static or dynamic. Unlike page
classification, positive revalidate values return null (ISR is a page
concept), deferring to module graph analysis for layout skip decisions.
* feat: add module graph layout classification (Layer 2)
BFS traversal of each layout's dependency tree via Vite's module graph.
If no transitive dynamic shim import (headers, cache, server) is found,
the layout is provably static. Otherwise it needs a runtime probe.
classifyAllRouteLayouts combines Layer 1 (segment config, from prior
commit) with Layer 2 (module graph), deduplicating shared layouts.
* feat: per-layout dynamic detection in probe phase (Layer 3)
Extends probeAppPageLayouts to return per-layout flags ("s"/"d")
alongside the existing Response. Three paths per layout:
- Build-time classified: pass flag through, still probe for errors
- Needs probe: run with isolated dynamic scope, detect usage
- No classification: original behavior (backward compat)
probeAppPageBeforeRender propagates layoutFlags through the result.
renderAppPageLifecycle updated to destructure the new return type.
* feat: add __layoutFlags payload metadata and thread through router state
Adds APP_LAYOUT_FLAGS_KEY to the RSC payload metadata, carrying
per-layout static/dynamic flags ("s"/"d"). readAppElementsMetadata
now parses layoutFlags with a type predicate guard.
AppRouterState and AppRouterAction carry layoutFlags. Navigate merges
flags (preserving previously-seen layouts), replace replaces them.
All dispatchBrowserTree call sites updated to pass layoutFlags.
* refactor: group classification options into single LayoutClassificationOptions type
The three optional fields (buildTimeClassifications, getLayoutId,
runWithIsolatedDynamicScope) had an all-or-nothing invariant enforced
only at runtime. Grouping them into a single optional `classification`
object makes the constraint type-safe — you either provide the full
classification context or nothing.
Also deduplicates the LayoutFlags type: canonical definition lives in
app-elements.ts, re-exported from app-page-execution.ts.
* fix: default to dynamic flag when layout probe throws non-special error
When runWithIsolatedDynamicScope throws and the error is non-special
(onLayoutError returns null), the layout was silently omitted from
layoutFlags. Now conservatively defaults to "d" — if probing failed,
the layout cannot be proven static.
* refactor: replace parallel arrays with LayoutEntry struct, tighten names
- Introduce LayoutEntry (moduleId, treePosition, segmentConfig) in
layout-classification.ts, collapsing the three parallel arrays
(layouts/layoutTreePositions/layoutSegmentConfigs) in
RouteForClassification into a single struct. Switches the inner loop
to for...of, removing the ?? 0 fallback that masked index mismatches.
- Update all five classifyAllRouteLayouts test fixtures to the struct shape.
- Add a JSDoc on LayoutFlags clarifying that "s" = static, "d" = dynamic.
- Tighten the buildTimeClassifications comment to say "keyed by layout index".
https://claude.ai/code/session_01MDJzCNao3dX9tmmVtSqsw5
* style: fix prettier formatting
https://claude.ai/code/session_01MDJzCNao3dX9tmmVtSqsw5
* Revert "style: fix prettier formatting"
This reverts commit
|
||
|
|
5f6bccbd03 |
feat: track previousNextUrl for intercepted App Router entries (#755)
* docs: refresh stale app browser entry comments * feat: track previousNextUrl for intercepted App Router entries * refactor: bind getBrowserRouterState() once in refresh case Avoid calling getBrowserRouterState() twice in the refresh branch of getRequestState by binding previousNextUrl to a local. Behavior-preserving nit raised in PR review. * refactor: drop dead history-state interception-context read path After previousNextUrl became the source of truth for interception context in history state, nothing in production writes the legacy __vinext_interceptionContext key anymore. The fall-through read in getCurrentInterceptionContext was vestigial and getCurrentInterceptionContext now derives strictly from the current pathname. - Removes the unused VINEXT_INTERCEPTION_CONTEXT_HISTORY_STATE_KEY export - Removes readHistoryStateInterceptionContext - Updates the prefetch-cache unit test to assert the new pathname-derived contract instead of the legacy history-state override * test(e2e): cover back/forward restoration of intercepted modal view Adds the back-then-forward case for App Router intercepting routes: after intercepted navigation from /feed to /photos/42, going back to /feed and then forward should restore the modal (rather than rendering the full /photos/42 page). Directly exercises the previousNextUrl read in the traverse branch of getRequestState. * docs: reword stale 'belongs to PR 5' comments Two inline comments in app-browser-entry.ts referenced "PR 5" as a future PR, but this is PR 5. Reworded to describe the deferred scope as-is: interception context on server-action POSTs and HMR re-renders is intentionally not propagated. * fix: distinguish explicit null from undefined in previousNextUrl fallback --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: James <james@eli.cx> |
||
|
|
b8c44fdd52 |
feat: encode interception context in App Router payload IDs and caches (#753)
* test: add E2E verification for layout persistence flat payload pipeline Prove the flat keyed map architecture works end-to-end: - Layout state persists across sibling navigation (counter survives) - Template remounts on segment boundary change, persists within segment - Error boundary clears on navigate-away-and-back - Back/forward preserves layout state through history - Parallel slots persist on soft nav, show default.tsx on hard nav Zero production code changes — test fixtures and Playwright specs only. * test: harden layout persistence hydration assertions * test: address layout-persistence spec gaps * test: tighten not.toBeAttached() assertions for absent elements * chore: trigger CI * feat: encode interception context in App Router payload IDs and caches * test: update App Router entry snapshots for interception encoding * fix: reuse committed interception context for soft navigations * chore: fix formatting in app-browser-entry after rebase * fix: update mounted-slot test helper calls for 4-arg createResolvedElements signature PR 3 added mounted-slot tests using createResolvedElements(routeId, root, extraEntries). PR 4 inserted interceptionContext as the 3rd param, shifting extraEntries to 4th. The object was being swallowed as interceptionContext, causing getMountedSlotIds to return []. Also add missing renderId to the interception context pending-commit test. * chore: document intentional interception context omission in HMR handler Add a comment to the HMR rsc:update handler noting that interception context is intentionally omitted, matching the existing documentation pattern in the server action handler. https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * fix: prefetch cache test now exercises interception-context partitioning The test was passing pre-computed cache keys as the rscUrl parameter to storePrefetchResponse(), which internally calls createAppPayloadCacheKey again. With the default interceptionContext=null the key passed through unchanged, so the test only verified Map key uniqueness rather than the actual partitioning logic. Pass raw RSC URLs with separate interception context parameters instead. Also add a comment clarifying that PendingNavigationCommit's top-level interceptionContext/rootLayoutTreePath/routeId are convenience aliases for the corresponding action fields. https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * fix: strip NUL bytes from interception context request header Defensively remove \0 from the incoming X-Vinext-Interception-Context header to prevent a malicious client from crafting cache keys that contain the internal NUL separator. https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * fix: resolve rebase conflicts in slot preservation tests After rebase onto upstream main, two fixes needed: - Pass extraEntries as 4th arg to createResolvedElements (3rd is now interceptionContext) - Add missing interceptionContext field to test action objects https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * test: update entry template snapshots for NUL-byte header sanitization https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
19f4fac338 |
fix: clear stale parallel slots on traversal in mergeElements (#816)
* fix: clear stale parallel slots on traversal in mergeElements Add "traverse" as a first-class action type in the router reducer. When the browser navigates back/forward, the action type is now "traverse" instead of "navigate", which tells mergeElements to clear slot keys absent from the server response. On soft forward navigation, absent slots are still preserved (the server may omit parent layout slots that haven't changed). Only traversals clear them, because the server renders the full destination route tree on popstate. Closes #814 * refactor: collapse navigate/traverse reducer cases; add UNMATCHED_SLOT traversal test Address review feedback on #816: - Collapse the identical navigate/traverse switch cases in routerReducer, passing `action.type === "traverse"` as the clearAbsentSlots boolean. Keeps exhaustive switch and never guard intact. - Add a test pinning the subtle interaction between clearAbsentSlots=true and the UNMATCHED_SLOT preservation loop: a slot sent as UNMATCHED_SLOT during traversal (key present in next) is restored from prev real content and is not cleared by the absent-slot loop. https://claude.ai/code/session_01RhqyMQWoHBr8qrLZoUjoZj * refactor: extract toActionType helper; add reducer-level traverse tests Address second-round review feedback on #816: - Extract toActionType(kind: NavigationKind) helper to consolidate the duplicated `navigationKind === "traverse" ? "traverse" : "navigate"` ternary that appeared at two call sites in renderNavigationPayload. A comment documents the "refresh" → "navigate" mapping intent and flags both sites as needing updates if NavigationKind gains new values. - Add routerReducer integration tests in app-browser-entry.test.ts: "traverse" clears stale parallel slots; "navigate" preserves them. These pin the wiring between the reducer and mergeElements. https://claude.ai/code/session_01RhqyMQWoHBr8qrLZoUjoZj * fix ci * fix ci again --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: James <james@eli.cx> |
||
|
|
ca0d542947 |
Fix parallel slot persistence and cache variants (#811)
* Fix parallel slot persistence and cache variants * Address App Router review follow-ups * Fix generated mounted-slot regex escaping * Address review feedback on slot normalization and cache comments Align generated entry's __normalizeMountedSlotsHeader regex with the typed module in app-elements.ts (both now use /\s+/). Clarify the HTML vs RSC cache asymmetry comment in stale regen path. * Remove redundant .map(trim) and clarify regen slot context split(/\s+/) never produces tokens with leading/trailing whitespace, so .map(trim) was a no-op in both normalizeMountedSlotsHeader and its generated counterpart. Also add a comment clarifying that background regen intentionally inherits the triggering request's slot context. * Clarify regen dedup and prefetch slot-mismatch tradeoffs Add comments explaining that pathname-only regen dedup is intentional (avoids concurrent regen storms across slot variants) and that consumePrefetchResponse unconditionally removes the entry before the slot-mismatch check (simplest correct behavior, prefetch is wasted on mismatch). * Document storePrefetchResponse slot-awareness gap Add JSDoc note that storePrefetchResponse is slot-unaware: the snapshot's mountedSlotsHeader comes from the response headers rather than the caller, so consumePrefetchResponse may reject the entry on slot context mismatch. |
||
|
|
0dbfeff1af |
feat: flat keyed payload for App Router layout persistence (#750)
* Extract app page route wiring helpers
* Add slot client primitives
* Fix app page error boundary serialization
* Fix client error boundary pathname reset
* Document Next.js error boundary verification
* Implement flat App Router payload for layout persistence
* fix: address review findings in flat payload implementation
- Fix stale closure on readBrowserRouterState by using a useRef updated
synchronously during render instead of a closure captured in
useLayoutEffect. External callers (navigate, server actions, HMR) now
always read the current router state.
- Restore GlobalErrorBoundary wrapping that was dropped when switching
from buildPageElement to buildAppPageElements. Apps with
app/global-error.tsx now get their global error boundary back.
- Add exhaustive default case to routerReducer so new action types
produce a compile error and a runtime throw instead of silent undefined.
- Remove dead code: createRouteNodeSnapshot, AppRouteNodeSnapshot,
AppRouteNodeValue were defined but never imported.
- Remove deprecated buildAppPageRouteElement and its test — no
production callers remain after the flat payload cutover.
- Short-circuit normalizeAppElements when no slot keys need rewriting
to avoid unnecessary allocation on every payload.
- Align test data in error boundary RSC payload test (matchedParams
slug: "post" -> "missing" to match requestUrl /posts/missing).
* fix: normalize flat payload after use(), not before
createFromReadableStream() returns a React thenable whose .then()
returns undefined (not a Promise). Chaining .then(normalizeAppElements)
broke SSR by assigning undefined to flightRoot.
Fix: call use() on the raw thenable, then normalize synchronously
after resolution. Also widen renderAppPageLifecycle element type to
accept flat map payloads.
* fix: produce flat RSC payload on all rendering paths
The SSR entry always expects a flat Record<string, ReactNode> with
__route and __rootLayout metadata from the RSC stream. Three paths
were still producing bare ReactNode payloads:
1. renderAppPageBoundaryElementResponse only created the flat map for
isRscRequest=true, but HTML requests also flow through RSC→SSR
2. buildPageElements "no default export" early return
3. Server action "Page not found" fallback
All three now produce the flat keyed element map, fixing 17 test
failures across 404/not-found, forbidden/unauthorized, error boundary,
production build, rewrite, and encoded-slash paths.
* test: update unit tests for flat RSC payload on all paths
- Update renderElementToStream mock to extract the route element from
the flat map before rendering to HTML (mirrors real SSR entry flow)
- Update entry template snapshots for the buildPageElements changes
* fix: wrap Flight thenable in Promise.resolve() before chaining .then()
createFromReadableStream() returns a React Flight thenable whose
.then() returns undefined instead of a new Promise. The browser
entry's normalizeAppElementsPromise chained .then() on this raw
thenable, producing undefined — which crashed use() during hydration
with "An unsupported type was passed to use(): undefined".
Wrapping in Promise.resolve() first converts the Flight thenable
into a real Promise, making .then() chains work correctly.
The same fix was already applied to the SSR entry in
|