@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
- package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
- package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
- package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
- package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
- package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
- package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-dVDi1GQz.js} +3 -3
- package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-dVDi1GQz.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
- package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-Ph6luW4c.js} +4 -2
- package/dist/_chunks/convention-lint-Ph6luW4c.js.map +1 -0
- package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
- package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
- package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
- package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
- package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
- package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
- package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
- package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
- package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
- package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
- package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
- package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
- package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
- package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
- package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
- package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
- package/dist/_chunks/segment-context-D9_89u34.js +34 -0
- package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
- package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
- package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
- package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.d.ts +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cache/singleflight.d.ts +2 -0
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/hydrate.d.ts +21 -15
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +4 -3
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +17 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/global-context.d.ts +15 -0
- package/dist/client/global-context.d.ts.map +1 -0
- package/dist/client/index.js +138 -35
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +0 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +206 -55
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/location-search.d.ts +12 -0
- package/dist/client/location-search.d.ts.map +1 -0
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +18 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +13 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +47 -108
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +136 -0
- package/dist/client/navigation-transition.d.ts.map +1 -0
- package/dist/client/nuqs-adapter.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +4 -5
- package/dist/client/params-context.d.ts.map +1 -1
- package/dist/client/react-root.d.ts +44 -0
- package/dist/client/react-root.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +2 -2
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +12 -2
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +39 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +25 -14
- package/dist/client/segment-outlet.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -9
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-content-cache-context.d.ts +35 -0
- package/dist/client/slot-content-cache-context.d.ts.map +1 -0
- package/dist/client/ssr-data.d.ts +8 -2
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +0 -15
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts +13 -11
- package/dist/client/use-pathname.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +13 -13
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/client/use-segment-params.d.ts +18 -68
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/config-types.d.ts +17 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +15 -10
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -43
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +2 -2
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception-overlap.d.ts +35 -0
- package/dist/routing/interception-overlap.d.ts.map +1 -0
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +3 -1
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +6 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/csp-nonce.d.ts +45 -0
- package/dist/server/csp-nonce.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +5 -2
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +17 -2
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +3 -2
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +86 -37
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-render.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +3 -17
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/nuqs-ssr-provider.d.ts +7 -3
- package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +32 -3
- package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +4 -3
- package/dist/server/render-utils.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +22 -2
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +5 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +59 -27
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shims/navigation-rsc.d.ts +21 -0
- package/dist/shims/navigation-rsc.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -0
- package/docs/api/35-api-typescript.mdx +4 -83
- package/docs/learn/03-fetching-data.mdx +1 -1
- package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
- package/docs/learn/05-the-flush-point.mdx +175 -0
- package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
- package/docs/learn/07-typed-routes.mdx +25 -49
- package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
- package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
- package/package.json +3 -3
- package/src/adapters/nitro.ts +7 -7
- package/src/cache/singleflight.ts +5 -0
- package/src/client/browser-entry/hydrate.ts +54 -104
- package/src/client/browser-entry/index.ts +16 -6
- package/src/client/browser-entry/post-hydration.ts +3 -2
- package/src/client/browser-entry/router-init.ts +84 -33
- package/src/client/global-context.ts +31 -0
- package/src/client/internal.ts +1 -2
- package/src/client/link.tsx +18 -18
- package/src/client/location-search.ts +15 -0
- package/src/client/navigation-api.ts +4 -2
- package/src/client/navigation-commit.ts +48 -2
- package/src/client/navigation-context.ts +25 -37
- package/src/client/navigation-root.tsx +55 -411
- package/src/client/navigation-transition.ts +278 -0
- package/src/client/nuqs-adapter.tsx +4 -5
- package/src/client/params-context.ts +13 -18
- package/src/client/react-root.ts +72 -0
- package/src/client/router-lifecycle.ts +1 -1
- package/src/client/router-pipeline.ts +96 -22
- package/src/client/router-types.ts +12 -2
- package/src/client/router.ts +48 -36
- package/src/client/segment-cache.ts +70 -2
- package/src/client/segment-context.ts +7 -4
- package/src/client/segment-outlet.tsx +41 -86
- package/src/client/segment-update-context.ts +7 -26
- package/src/client/slot-content-cache-context.ts +43 -0
- package/src/client/ssr-data.ts +8 -2
- package/src/client/state.ts +0 -26
- package/src/client/use-pathname.ts +21 -31
- package/src/client/use-search-params.ts +31 -29
- package/src/client/use-segment-params.ts +27 -126
- package/src/config-types.ts +17 -0
- package/src/config-validation.ts +17 -0
- package/src/dev-tools/holding-server.ts +23 -12
- package/src/index.ts +26 -11
- package/src/plugins/dev-server.ts +9 -12
- package/src/plugins/entries.ts +3 -0
- package/src/plugins/shims.ts +8 -7
- package/src/plugins/static-build.ts +9 -5
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception-overlap.ts +141 -0
- package/src/routing/interception.ts +118 -5
- package/src/rsc-runtime/ssr.ts +3 -2
- package/src/server/als-registry.ts +6 -0
- package/src/server/csp-nonce.ts +70 -0
- package/src/server/default-status-page.ts +1 -0
- package/src/server/deny-renderer.ts +7 -3
- package/src/server/flight-scripts.ts +9 -4
- package/src/server/html-injector-core.ts +26 -9
- package/src/server/html-injectors.ts +8 -8
- package/src/server/metadata-render.ts +26 -4
- package/src/server/node-stream-transforms.ts +7 -20
- package/src/server/nuqs-ssr-provider.tsx +8 -7
- package/src/server/pipeline-phases.ts +5 -0
- package/src/server/prebuilt/key-discipline.ts +82 -13
- package/src/server/prebuilt-runtime.ts +2 -2
- package/src/server/primitives.ts +4 -4
- package/src/server/render-utils.ts +8 -4
- package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
- package/src/server/rsc-entry/error-renderer.ts +5 -2
- package/src/server/rsc-entry/index.ts +8 -0
- package/src/server/rsc-entry/ssr-renderer.ts +11 -4
- package/src/server/ssr-bridge-types.ts +22 -2
- package/src/server/ssr-entry.ts +35 -28
- package/src/server/ssr-render.ts +13 -4
- package/src/server/ssr-wrappers.tsx +81 -61
- package/src/server/types.ts +10 -0
- package/src/shared/slot-params.ts +3 -4
- package/src/shims/navigation-rsc.ts +47 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
- package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
- package/dist/_chunks/convention-lint-DO10_pVl.js.map +0 -1
- package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
- package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
- package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
- package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
- package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
- package/docs/learn/04-loading-states.mdx +0 -67
- package/docs/learn/04b-the-flush-point.mdx +0 -115
- package/docs/learn/12-client-navigation.mdx +0 -176
- package/docs/learn/13-configuration.mdx +0 -166
- package/docs/more/01-advanced-routing.mdx +0 -344
- package/docs/more/02-advanced-forms.mdx +0 -137
- package/docs/more/03-coming-from-nextjs.mdx +0 -186
- package/docs/more/04-metadata-and-fonts.mdx +0 -193
- package/docs/more/04b-mdx.mdx +0 -229
- package/docs/more/05-content-collections.mdx +0 -90
- package/docs/more/06-instrumentation.mdx +0 -214
- package/docs/more/07-security.mdx +0 -129
- package/docs/more/08-developer-experience.mdx +0 -134
- package/docs/more/40-why-timber.mdx +0 -50
- package/docs/more/41-timber-vs-nextjs.mdx +0 -81
- package/docs/more/42-timber-vs-others.mdx +0 -68
- package/docs/more/50-ai-agent-instructions.mdx +0 -171
- /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
- /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
- /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
- /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NavigationRoot —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
2
|
+
* NavigationRoot — the component the router renders the page through.
|
|
3
|
+
*
|
|
4
|
+
* It is stateless. The router owns the displayed tree and drives React with
|
|
5
|
+
* `root.render(<NavigationRoot rendered={{ element, publish }} />)` inside a
|
|
6
|
+
* synchronous `startTransition` (see `client/react-root.ts`). A transition
|
|
7
|
+
* update keeps the committed tree on screen while the incoming one resolves,
|
|
8
|
+
* instead of replacing it with a Suspense fallback — the reason this
|
|
9
|
+
* component exists (TIM-1306).
|
|
10
|
+
*
|
|
11
|
+
* It used to hold the tree in `useState` and register `setState` closures
|
|
12
|
+
* into module globals during render so the router could reach into it, with a
|
|
13
|
+
* second set of stand-in closures for the window before a root existed
|
|
14
|
+
* (TIM-600). Inverting that — router calls React, not the reverse — deleted
|
|
15
|
+
* all of it (TIM-1431). Next.js's `use-action-queue.ts` says it wants this
|
|
16
|
+
* shape and cannot have it because it must decode Flight during render;
|
|
17
|
+
* timber decodes in `router-pipeline.ts`, so nothing stands in the way.
|
|
18
|
+
*
|
|
19
|
+
* What this component does own is **the commit-time publish**: a navigation's
|
|
20
|
+
* state (segment cache, address bar, history stack, pathname) is published
|
|
21
|
+
* from a layout effect keyed on the tree object, so the event that puts a
|
|
22
|
+
* route on screen is the event that makes it current (TIM-1301). Props work
|
|
23
|
+
* for that exactly as state did.
|
|
19
24
|
*
|
|
20
25
|
* This component holds no pending state. The `TopLoader` it renders and the
|
|
21
26
|
* public `usePendingNavigation()` both subscribe to the router's external
|
|
@@ -35,166 +40,39 @@
|
|
|
35
40
|
* See design/19-client-navigation.md §"NavigationContext"
|
|
36
41
|
*/
|
|
37
42
|
|
|
38
|
-
import {
|
|
39
|
-
createElement,
|
|
40
|
-
Fragment,
|
|
41
|
-
startTransition,
|
|
42
|
-
useLayoutEffect,
|
|
43
|
-
useRef,
|
|
44
|
-
useState,
|
|
45
|
-
type ReactNode,
|
|
46
|
-
} from 'react';
|
|
43
|
+
import { createElement, Fragment, useLayoutEffect, useRef, type ReactNode } from 'react';
|
|
47
44
|
import { TopLoader, type TopLoaderConfig } from './top-loader.tsx';
|
|
48
45
|
|
|
49
|
-
// ───
|
|
46
|
+
// ─── Rendered Tree ──────────────────────────────────────────────
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* type: `ReactNode` here, `unknown` in the router, which never touches it.
|
|
49
|
+
* The tree the router has handed to React, and the state publish that belongs
|
|
50
|
+
* to it. They travel as one object so React's commit of the tree is the event
|
|
51
|
+
* that publishes — see the effect in NavigationRoot. Every `render` builds a
|
|
52
|
+
* fresh one, which is what keys that effect: a tree is published once, however
|
|
53
|
+
* many times React re-runs the effect for it.
|
|
58
54
|
*/
|
|
59
|
-
export interface
|
|
60
|
-
|
|
61
|
-
element: E;
|
|
62
|
-
/** Resolves when the Flight stream finishes decoding, or null. */
|
|
63
|
-
decodePromise: Promise<void> | null;
|
|
55
|
+
export interface RenderedTree {
|
|
56
|
+
element: ReactNode;
|
|
64
57
|
/**
|
|
65
58
|
* Publishes the navigation's state — segment cache, pathname, address bar,
|
|
66
59
|
* history stack, and the client's record of the mounted tree — and
|
|
67
|
-
* announces it to listeners outside React.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* tree React is still waiting on, or one a later navigation replaces first,
|
|
71
|
-
* has been given to React without being on screen. A superseded navigation
|
|
72
|
-
* never runs it and leaves every one of those consumers describing the
|
|
73
|
-
* route still on screen (TIM-1301).
|
|
60
|
+
* announces it to listeners outside React. Runs when React commits
|
|
61
|
+
* `element`, never before (TIM-1301). Null for a render that has nothing to
|
|
62
|
+
* publish on commit (hydration, the shallow search re-wrap).
|
|
74
63
|
*/
|
|
75
|
-
commit: () => void;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The tree NavigationRoot holds in state, and the state publish that belongs
|
|
80
|
-
* to it. They travel as one object so React's commit of the tree is the event
|
|
81
|
-
* that publishes — see the effect in NavigationRoot.
|
|
82
|
-
*/
|
|
83
|
-
interface RenderedTree {
|
|
84
|
-
element: ReactNode;
|
|
85
64
|
publish: (() => void) | null;
|
|
86
65
|
}
|
|
87
66
|
|
|
88
|
-
// ─── Navigation Transition Counter ──────────────────────────────
|
|
89
|
-
// Monotonically increasing counter that increments each time
|
|
90
|
-
// navigateTransition() is called. Used to detect stale transitions:
|
|
91
|
-
// if a newer transition started while the current one's perform()
|
|
92
|
-
// was in flight, the current transition is stale and should reject.
|
|
93
|
-
//
|
|
94
|
-
// Separate from the link-pending navId (which only increments on
|
|
95
|
-
// link clicks). This counter covers all navigation types: link clicks,
|
|
96
|
-
// programmatic navigate(), refresh(), and handlePopState().
|
|
97
|
-
//
|
|
98
|
-
// Uses globalThis for singleton guarantee across chunks — same pattern
|
|
99
|
-
// as NavigationContext and the link pending store.
|
|
100
|
-
|
|
101
|
-
const NAV_TRANSITION_KEY = Symbol.for('__timber_nav_transition_counter');
|
|
102
|
-
|
|
103
67
|
/**
|
|
104
|
-
*
|
|
105
|
-
* waiting on its own payload the moment it is superseded — see
|
|
106
|
-
* `settleOnDecodeOrSupersession`.
|
|
107
|
-
*/
|
|
108
|
-
interface TransitionCounter {
|
|
109
|
-
id: number;
|
|
110
|
-
waiters: Set<() => void>;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function getTransitionCounter(): TransitionCounter {
|
|
114
|
-
const g = globalThis as Record<symbol, unknown>;
|
|
115
|
-
const existing = g[NAV_TRANSITION_KEY] as Partial<TransitionCounter> | undefined;
|
|
116
|
-
if (!existing) {
|
|
117
|
-
const created: TransitionCounter = { id: 0, waiters: new Set() };
|
|
118
|
-
g[NAV_TRANSITION_KEY] = created;
|
|
119
|
-
return created;
|
|
120
|
-
}
|
|
121
|
-
// A duplicated copy of this module may have created the singleton before
|
|
122
|
-
// `waiters` existed. The object is shared across chunks, so fill it in
|
|
123
|
-
// rather than replacing it — replacing would strand the other copy's id.
|
|
124
|
-
existing.waiters ??= new Set();
|
|
125
|
-
return existing as TransitionCounter;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Bump the counter and wake everything waiting on an older transition. */
|
|
129
|
-
function bumpTransitionCounter(): number {
|
|
130
|
-
const counter = getTransitionCounter();
|
|
131
|
-
counter.id += 1;
|
|
132
|
-
for (const wake of [...counter.waiters]) wake();
|
|
133
|
-
return counter.id;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Invalidate all in-flight navigation transitions. Any navigateTransition()
|
|
138
|
-
* call whose perform() has not yet committed will reject with AbortError
|
|
139
|
-
* instead of committing its element.
|
|
140
|
-
*
|
|
141
|
-
* Called by the router when a render supersedes in-flight navigations
|
|
142
|
-
* WITHOUT going through navigateTransition() — the cached popstate replay
|
|
143
|
-
* renders via transitionRender(), which doesn't bump the counter, so a
|
|
144
|
-
* stale forward navigation's setElement would otherwise pass the
|
|
145
|
-
* `counter.id !== transId` guard and commit the forward page over the
|
|
146
|
-
* replayed back page (TIM-1022).
|
|
147
|
-
*/
|
|
148
|
-
export function supersedeNavigationTransitions(): void {
|
|
149
|
-
bumpTransitionCounter();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Wait for the payload to finish decoding, OR for this transition to be
|
|
154
|
-
* superseded — whichever happens first.
|
|
68
|
+
* Hand a tree to React in a transition. `publish` runs when React commits it.
|
|
155
69
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* `decodePromise` settle promptly. Awaiting it bare would keep the loser's
|
|
161
|
-
* `router.navigate()` promise pending for the rest of the stream — and
|
|
162
|
-
* forever if it stalls — which is what `<Link>`'s `isPending` is timed
|
|
163
|
-
* against, so the losing link would sit spinning while the winner loaded
|
|
164
|
-
* (codex on #1004).
|
|
165
|
-
*
|
|
166
|
-
* A decode *failure* still propagates: it is a real error for this
|
|
167
|
-
* navigation, and the caller's recovery is timed against it.
|
|
70
|
+
* Every path that puts a page on screen goes through one of these — a
|
|
71
|
+
* navigation, a revalidation, a popstate replay, a shallow search re-wrap —
|
|
72
|
+
* and it is always a synchronous `startTransition` around `root.render`.
|
|
73
|
+
* The production one is `createReactRoot().render`.
|
|
168
74
|
*/
|
|
169
|
-
|
|
170
|
-
decodePromise: Promise<void>,
|
|
171
|
-
counter: TransitionCounter,
|
|
172
|
-
transId: number
|
|
173
|
-
): Promise<void> {
|
|
174
|
-
if (counter.id !== transId) return Promise.resolve();
|
|
175
|
-
return new Promise<void>((resolve, reject) => {
|
|
176
|
-
const stopWaiting = (): void => {
|
|
177
|
-
counter.waiters.delete(wake);
|
|
178
|
-
};
|
|
179
|
-
const wake = (): void => {
|
|
180
|
-
if (counter.id !== transId) {
|
|
181
|
-
stopWaiting();
|
|
182
|
-
resolve();
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
counter.waiters.add(wake);
|
|
186
|
-
decodePromise.then(
|
|
187
|
-
() => {
|
|
188
|
-
stopWaiting();
|
|
189
|
-
resolve();
|
|
190
|
-
},
|
|
191
|
-
(error: unknown) => {
|
|
192
|
-
stopWaiting();
|
|
193
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
75
|
+
export type NavigationRender = (element: ReactNode, publish: (() => void) | null) => void;
|
|
198
76
|
|
|
199
77
|
// ─── Hard Navigation Guard ──────────────────────────────────────
|
|
200
78
|
|
|
@@ -255,52 +133,25 @@ export function isHardNavigating(): boolean {
|
|
|
255
133
|
// eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable
|
|
256
134
|
const unresolvedThenable = { then() {} } as PromiseLike<never>;
|
|
257
135
|
|
|
258
|
-
// ─── Module-level functions ──────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Module-level reference to the state setter wrapped in startTransition.
|
|
262
|
-
* Used for non-navigation renders (applyRevalidation, popstate replay).
|
|
263
|
-
*/
|
|
264
|
-
let _transitionRender: ((element: ReactNode) => void) | null = null;
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Module-level reference to the navigation transition function.
|
|
268
|
-
*
|
|
269
|
-
* Runs the fetch OUTSIDE any transition scope and wraps only the state update
|
|
270
|
-
* that hands the resulting tree to React — describing this as "a full
|
|
271
|
-
* navigation in a single startTransition" is the shape TIM-1306 removed.
|
|
272
|
-
*/
|
|
273
|
-
let _navigateTransition:
|
|
274
|
-
| ((url: string, perform: () => Promise<TransitionResult>) => Promise<void>)
|
|
275
|
-
| null = null;
|
|
276
|
-
|
|
277
136
|
// ─── Component ───────────────────────────────────────────────────
|
|
278
137
|
|
|
279
138
|
/**
|
|
280
|
-
* Root
|
|
281
|
-
*
|
|
282
|
-
* Renders the TopLoader alongside the tree it holds in state. Neither adds a
|
|
283
|
-
* DOM element on the hydration path, so the tree matches the server HTML.
|
|
284
|
-
*
|
|
285
|
-
* Usage in browser-entry.ts:
|
|
286
|
-
* const rootEl = createElement(NavigationRoot, { initial: wrapped });
|
|
287
|
-
* reactRoot = hydrateRoot(document, rootEl);
|
|
139
|
+
* Root component the router renders the page through.
|
|
288
140
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
141
|
+
* Renders the TopLoader alongside the tree it is given. Neither adds a DOM
|
|
142
|
+
* element on the hydration path, so the tree matches the server HTML.
|
|
291
143
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
144
|
+
* Rendered only by `createReactRoot` (client/react-root.ts): hydration passes
|
|
145
|
+
* `{ element, publish: null }`, and every later render is a synchronous
|
|
146
|
+
* `startTransition(() => root.render(...))` with a fresh `rendered` object.
|
|
294
147
|
*/
|
|
295
148
|
export function NavigationRoot({
|
|
296
|
-
|
|
149
|
+
rendered,
|
|
297
150
|
topLoaderConfig,
|
|
298
151
|
}: {
|
|
299
|
-
|
|
152
|
+
rendered: RenderedTree;
|
|
300
153
|
topLoaderConfig?: TopLoaderConfig;
|
|
301
154
|
}): ReactNode {
|
|
302
|
-
const [rendered, setRendered] = useState<RenderedTree>({ element: initial, publish: null });
|
|
303
|
-
|
|
304
155
|
// Publish the navigation's state when React commits its tree — not when the
|
|
305
156
|
// tree is handed over. A tree React is still waiting on, or one a later
|
|
306
157
|
// navigation replaces before React renders it, has been *given* to React
|
|
@@ -324,8 +175,8 @@ export function NavigationRoot({
|
|
|
324
175
|
//
|
|
325
176
|
// Keyed on the tree object rather than on the effect run. A publish moves
|
|
326
177
|
// the address bar, which is not idempotent, so any second invocation for the
|
|
327
|
-
// same tree — an effect re-run React is entitled to perform
|
|
328
|
-
// no-op rather than a second history entry.
|
|
178
|
+
// same tree — an effect re-run React is entitled to perform, StrictMode's
|
|
179
|
+
// double-invoke — must be a no-op rather than a second history entry.
|
|
329
180
|
const publishedRef = useRef<RenderedTree | null>(null);
|
|
330
181
|
useLayoutEffect(() => {
|
|
331
182
|
if (publishedRef.current === rendered) return;
|
|
@@ -333,141 +184,17 @@ export function NavigationRoot({
|
|
|
333
184
|
rendered.publish?.();
|
|
334
185
|
}, [rendered]);
|
|
335
186
|
|
|
336
|
-
// NOTE: We use standalone `startTransition` (imported from 'react'),
|
|
337
|
-
// NOT `useTransition`. The `useTransition` hook's `startTransition`
|
|
338
|
-
// is tied to a single fiber and tracks one async callback at a time.
|
|
339
|
-
// When two navigations overlap (click slow-page, then click dashboard),
|
|
340
|
-
// calling useTransition's startTransition twice with concurrent async
|
|
341
|
-
// callbacks corrupts React's internal hook tracking — causing
|
|
342
|
-
// "Rendered more hooks than during the previous render."
|
|
343
|
-
//
|
|
344
|
-
// Standalone `startTransition` creates independent transition lanes
|
|
345
|
-
// for each call, so concurrent navigations don't interfere. We don't
|
|
346
|
-
// need useTransition's `isPending` — pending state lives in the router's
|
|
347
|
-
// external store, which TopLoader and usePendingNavigation() subscribe to.
|
|
348
|
-
//
|
|
349
|
-
// This matches the Next.js pattern (TIM-625): "No useTransition in
|
|
350
|
-
// the router at all — only standalone startTransition."
|
|
351
|
-
|
|
352
|
-
// Non-navigation render (revalidation, popstate cached replay).
|
|
353
|
-
// Non-navigation render (revalidation, popstate cached replay). Both publish
|
|
354
|
-
// synchronously in the router before calling this — neither has an in-flight
|
|
355
|
-
// window in which it could be superseded — so there is nothing to publish
|
|
356
|
-
// on commit.
|
|
357
|
-
_transitionRender = (newElement: ReactNode) => {
|
|
358
|
-
startTransition(() => {
|
|
359
|
-
setRendered({ element: newElement, publish: null });
|
|
360
|
-
});
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
// Full navigation transition.
|
|
364
|
-
//
|
|
365
|
-
// The async work runs OUTSIDE any transition scope, and each state update is
|
|
366
|
-
// its own synchronous `startTransition`. Two React behaviours force that
|
|
367
|
-
// shape; both were measured on React 19.2.7 (tests/navigation-transition-
|
|
368
|
-
// suspense.test.ts pins the observable half).
|
|
369
|
-
//
|
|
370
|
-
// 1. A transition scope does not survive an `await`. `startTransition`
|
|
371
|
-
// restores the previous scope in its `finally`, which for an async
|
|
372
|
-
// callback runs when that callback RETURNS — i.e. at its first `await`.
|
|
373
|
-
// So an update scheduled past an await inside `startTransition(async …)`
|
|
374
|
-
// is an ordinary urgent update:
|
|
375
|
-
//
|
|
376
|
-
// startTransition(() => setEl(x)) -> 'HOME' held
|
|
377
|
-
// startTransition(async () => { await p; setEl(x) }) -> fallback shown
|
|
378
|
-
//
|
|
379
|
-
// Left uncorrected, every navigation whose new tree suspends replaces the
|
|
380
|
-
// visible page with a Suspense fallback — the exact thing this component
|
|
381
|
-
// exists to prevent (TIM-1306). React documents the caveat under
|
|
382
|
-
// `startTransition`: updates after an await need their own transition.
|
|
383
|
-
//
|
|
384
|
-
// 2. Re-wrapping *inside* the async callback is not enough. Returning a
|
|
385
|
-
// thenable from `startTransition` hands it to `ReactSharedInternals.S`,
|
|
386
|
-
// which calls react-dom's `entangleAsyncAction`. That opens an action
|
|
387
|
-
// scope: `currentEntangledLane` collects EVERY transition update
|
|
388
|
-
// scheduled while the scope is open — including one from a nested,
|
|
389
|
-
// fully synchronous `startTransition` — and rendering that lane throws
|
|
390
|
-
// `currentEntangledActionThenable`, suspending until the action settles.
|
|
391
|
-
// Which is after the promise `navigateTransition` hands back, so a caller
|
|
392
|
-
// that awaits a navigation and then reads the DOM sees the departing
|
|
393
|
-
// page, and the TIM-1301 publish (a layout effect on the commit) is just
|
|
394
|
-
// as late. (Not `ReactSharedInternals.asyncTransitions` — that counter is
|
|
395
|
-
// write-only in 19.2.7.)
|
|
396
|
-
//
|
|
397
|
-
// No async action is created here, so every navigation commits as soon as
|
|
398
|
-
// React can render its destination — where TIM-1301 put it. That
|
|
399
|
-
// now holds for ALL of them. It used not to hold for `<Link>`, which wrapped
|
|
400
|
-
// `router.navigate()` in its own `useTransition`: that action scope
|
|
401
|
-
// entangled this component's updates just the same, so a Link navigation
|
|
402
|
-
// committed only once the payload had finished decoding, and with it
|
|
403
|
-
// `pushState`, the segment cache and `timber:navigation-end`. Link now calls
|
|
404
|
-
// `router.navigate()` outside any transition and tracks its own `isPending`
|
|
405
|
-
// with a plain `useState` (TIM-1307).
|
|
406
|
-
//
|
|
407
|
-
// Nothing here may reopen an action scope: no `startTransition` callback in
|
|
408
|
-
// this path may return a thenable, and no caller may invoke this from inside
|
|
409
|
-
// a React action scope. That is a rule about `startTransition`, not about
|
|
410
|
-
// `perform` — `perform` is async by contract and is awaited OUTSIDE any
|
|
411
|
-
// transition scope, which is exactly why it is safe.
|
|
412
|
-
//
|
|
413
|
-
// Standalone `startTransition` rather than `useTransition`'s is still
|
|
414
|
-
// deliberate — see the TIM-625 note above; each navigation needs an
|
|
415
|
-
// independent lane.
|
|
416
|
-
//
|
|
417
|
-
// `url` is unused: it named the pending state this component used to hold,
|
|
418
|
-
// and the router already publishes the same URL to its own pending store
|
|
419
|
-
// before calling here. It stays in the signature because the router's
|
|
420
|
-
// `RouterDeps.navigateTransition` adapter passes it and the argument reads
|
|
421
|
-
// at the call site.
|
|
422
|
-
_navigateTransition = (_url: string, perform: () => Promise<TransitionResult>) => {
|
|
423
|
-
// Increment the transition counter SYNCHRONOUSLY (before any await). Each
|
|
424
|
-
// call gets a unique transId; the counter is the same globalThis
|
|
425
|
-
// singleton, so a newer call always has a higher id.
|
|
426
|
-
const counter = getTransitionCounter();
|
|
427
|
-
const transId = bumpTransitionCounter();
|
|
428
|
-
|
|
429
|
-
const superseded = () => new DOMException('Navigation superseded', 'AbortError');
|
|
430
|
-
|
|
431
|
-
return (async () => {
|
|
432
|
-
const { element, decodePromise, commit } = await perform();
|
|
433
|
-
if (counter.id !== transId) {
|
|
434
|
-
decodePromise?.catch(() => {});
|
|
435
|
-
throw superseded();
|
|
436
|
-
}
|
|
437
|
-
// Hand the tree over with its commit attached. The effect above runs
|
|
438
|
-
// it if and when React commits this tree, so a navigation that is
|
|
439
|
-
// superseded while its payload streams publishes nothing (TIM-1301).
|
|
440
|
-
//
|
|
441
|
-
// This is THE update that must not hide the departing page (TIM-1306).
|
|
442
|
-
startTransition(() => {
|
|
443
|
-
setRendered({ element, publish: commit });
|
|
444
|
-
});
|
|
445
|
-
// React may commit the tree before this settles — that is the point:
|
|
446
|
-
// the destination reveals as React is able to render it
|
|
447
|
-
// rather than waiting for the whole Flight stream. The await is here so
|
|
448
|
-
// the promise this function hands back still means "the payload is
|
|
449
|
-
// decoded", which is what the router's scroll restoration, the
|
|
450
|
-
// Navigation API deferred and `<Link>`'s `isPending` are timed against.
|
|
451
|
-
//
|
|
452
|
-
// ...unless this navigation loses first, in which case it stops waiting
|
|
453
|
-
// on a stream that is no longer its business. See
|
|
454
|
-
// `settleOnDecodeOrSupersession`.
|
|
455
|
-
if (decodePromise) await settleOnDecodeOrSupersession(decodePromise, counter, transId);
|
|
456
|
-
if (counter.id !== transId) throw superseded();
|
|
457
|
-
})();
|
|
458
|
-
};
|
|
459
|
-
|
|
460
187
|
// ─── Hard navigation guard ─────────────────────────────────
|
|
461
188
|
// When a hard navigation is in progress (500 error, version skew),
|
|
462
189
|
// suspend forever to prevent React from rendering children during
|
|
463
190
|
// page teardown. This avoids "Rendered more hooks" crashes in
|
|
464
191
|
// CHILD components whose hook counts may shift during teardown.
|
|
465
192
|
//
|
|
466
|
-
// CRITICAL: This throw MUST come AFTER all hooks (the
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
193
|
+
// CRITICAL: This throw MUST come AFTER all hooks (the useRef and
|
|
194
|
+
// useLayoutEffect above). React requires the same hooks to run on every
|
|
195
|
+
// render. If we threw before hooks, React would see 0 hooks on the
|
|
196
|
+
// re-render vs 2 on the initial render — triggering the exact "Rendered
|
|
197
|
+
// more hooks" error we're trying to prevent.
|
|
471
198
|
//
|
|
472
199
|
// By placing it after hooks but before the return, all hooks
|
|
473
200
|
// satisfy React's rules, but the thrown thenable prevents any
|
|
@@ -495,86 +222,3 @@ export function NavigationRoot({
|
|
|
495
222
|
rendered.element
|
|
496
223
|
);
|
|
497
224
|
}
|
|
498
|
-
|
|
499
|
-
// ─── Public API ──────────────────────────────────────────────────
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Trigger a transition render for non-navigation updates.
|
|
503
|
-
* React keeps the old committed tree visible while any new Suspense
|
|
504
|
-
* boundaries in the update resolve.
|
|
505
|
-
*
|
|
506
|
-
* Used for: applyRevalidation, popstate replay with cached payload.
|
|
507
|
-
*/
|
|
508
|
-
export function transitionRender(element: ReactNode): void {
|
|
509
|
-
if (_transitionRender) {
|
|
510
|
-
_transitionRender(element);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Run a full navigation, handing its tree to React in a transition.
|
|
516
|
-
*
|
|
517
|
-
* The `perform` callback fetches the RSC payload, updates router state, and
|
|
518
|
-
* returns the wrapped React element. `perform` is async by contract and runs
|
|
519
|
-
* OUTSIDE any transition scope — only the `setRendered` that hands its result
|
|
520
|
-
* over is wrapped, in a synchronous `startTransition`.
|
|
521
|
-
*
|
|
522
|
-
* Do not call this from inside a React action scope, and never let a
|
|
523
|
-
* `startTransition` callback in this path return a thenable: either opens an
|
|
524
|
-
* action scope that entangles the update and defers the commit until the
|
|
525
|
-
* action settles (TIM-1306, TIM-1307).
|
|
526
|
-
*
|
|
527
|
-
* Returns a Promise that resolves when the async work completes — the payload
|
|
528
|
-
* is fetched, decoded, and handed to React. It does **not** wait for React to
|
|
529
|
-
* commit the tree, so the state that publishes on that commit (address bar,
|
|
530
|
-
* segment cache, history entry, pathname) may still be a beat behind when it
|
|
531
|
-
* resolves. Anything that needs the destination to be current should listen
|
|
532
|
-
* for `timber:navigation-end`, which is dispatched by the publish itself.
|
|
533
|
-
*
|
|
534
|
-
* Awaiting the publish here was tried and rejected: it makes the promise
|
|
535
|
-
* depend on a React commit, which never arrives if the root unmounts
|
|
536
|
-
* mid-navigation, and deadlocks any caller that awaits a navigation inside
|
|
537
|
-
* `act()`.
|
|
538
|
-
*
|
|
539
|
-
* Used for: navigate(), refresh(), popstate with fetch.
|
|
540
|
-
*/
|
|
541
|
-
export function navigateTransition(
|
|
542
|
-
url: string,
|
|
543
|
-
perform: () => Promise<TransitionResult>
|
|
544
|
-
): Promise<void> {
|
|
545
|
-
if (_navigateTransition) {
|
|
546
|
-
return _navigateTransition(url, perform);
|
|
547
|
-
}
|
|
548
|
-
// Fallback: no NavigationRoot mounted (shouldn't happen in production).
|
|
549
|
-
// Nothing can supersede a transition that does not exist, so the commit
|
|
550
|
-
// runs unconditionally.
|
|
551
|
-
return perform().then((result) => result.commit());
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Install one-shot deferred callbacks for the no-RSC bootstrap path (TIM-600).
|
|
556
|
-
*
|
|
557
|
-
* When there's no RSC payload, we can't create a React root immediately —
|
|
558
|
-
* `createRoot(document).render(...)` would blank the SSR HTML. Instead,
|
|
559
|
-
* this sets up `_transitionRender` and `_navigateTransition` so that the
|
|
560
|
-
* first client navigation triggers root creation via `createAndMount`.
|
|
561
|
-
*
|
|
562
|
-
* After `createAndMount` runs, NavigationRoot renders and overwrites these
|
|
563
|
-
* callbacks with its real `startTransition`-based implementations.
|
|
564
|
-
*/
|
|
565
|
-
export function installDeferredNavigation(createAndMount: (initial: ReactNode) => void): void {
|
|
566
|
-
let mounted = false;
|
|
567
|
-
const mountOnce = (element: ReactNode) => {
|
|
568
|
-
if (mounted) return;
|
|
569
|
-
mounted = true;
|
|
570
|
-
createAndMount(element);
|
|
571
|
-
};
|
|
572
|
-
_transitionRender = (element: ReactNode) => {
|
|
573
|
-
mountOnce(element);
|
|
574
|
-
};
|
|
575
|
-
_navigateTransition = async (_url: string, perform: () => Promise<TransitionResult>) => {
|
|
576
|
-
const { element, commit } = await perform();
|
|
577
|
-
commit();
|
|
578
|
-
mountOnce(element);
|
|
579
|
-
};
|
|
580
|
-
}
|