@timber-js/app 0.2.0-alpha.184 → 0.2.0-alpha.186
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-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
- package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
- package/dist/_chunks/als-slots-mFweg276.js +27 -0
- package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
- package/dist/_chunks/base-path-DaQrzbez.js +80 -0
- package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
- package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
- package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
- package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
- package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
- package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
- package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
- package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
- package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
- package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
- package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
- package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
- package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
- package/dist/_chunks/{walkers-BtTlKviE.js → walkers-BhhwI9TD.js} +21 -87
- package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +37 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/fs-identity.d.ts +64 -0
- package/dist/adapters/fs-identity.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +2 -3
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +57 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +13 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/fast-hash.d.ts +5 -0
- package/dist/cache/fast-hash.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +20 -9
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +4 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +3 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +1 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -162
- package/dist/client/history.d.ts +13 -6
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +18 -11
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +4 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +872 -305
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts +2 -2
- package/dist/client/navigation-commit.d.ts +71 -0
- package/dist/client/navigation-commit.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +37 -20
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +63 -23
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +81 -0
- package/dist/client/params-context.d.ts.map +1 -0
- package/dist/client/router-effects.d.ts +129 -0
- package/dist/client/router-effects.d.ts.map +1 -0
- package/dist/client/router-lifecycle.d.ts +55 -0
- package/dist/client/router-lifecycle.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +84 -0
- package/dist/client/router-pipeline.d.ts.map +1 -0
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router-skew.d.ts +21 -0
- package/dist/client/router-skew.d.ts.map +1 -0
- package/dist/client/router-types.d.ts +207 -0
- package/dist/client/router-types.d.ts.map +1 -0
- package/dist/client/router.d.ts +1 -182
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +39 -16
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +79 -34
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +6 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/stale-client.d.ts +69 -0
- package/dist/client/stale-client.d.ts.map +1 -0
- package/dist/client/state.d.ts +10 -1
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +2 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +5 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +18 -8
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/cookies/index.js +3 -3
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +915 -320
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +33 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +106 -0
- package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
- package/dist/plugins/request-dep/index.d.ts +75 -0
- package/dist/plugins/request-dep/index.d.ts.map +1 -0
- package/dist/plugins/request-dep/origin.d.ts +61 -0
- package/dist/plugins/request-dep/origin.d.ts.map +1 -0
- package/dist/plugins/routing.d.ts +9 -0
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +149 -8
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +18 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/collision-probe.d.ts +104 -0
- package/dist/routing/collision-probe.d.ts.map +1 -0
- package/dist/routing/collision-spaces.d.ts +26 -0
- package/dist/routing/collision-spaces.d.ts.map +1 -0
- package/dist/routing/index.js +3 -3
- package/dist/routing/interception.d.ts +235 -6
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +33 -0
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +95 -0
- package/dist/routing/segment-keys.d.ts.map +1 -0
- package/dist/routing/slot-placement.d.ts +48 -0
- package/dist/routing/slot-placement.d.ts.map +1 -0
- package/dist/routing/types.d.ts +1 -3
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +0 -6
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +176 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/segment-params/define.d.ts +0 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +69 -1
- package/dist/segment-params/index.js.map +1 -0
- package/dist/server/actions.d.ts +6 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +2 -1
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +13 -1
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +82 -0
- package/dist/server/chain-url-parts.d.ts.map +1 -0
- package/dist/server/children-interception.d.ts +49 -0
- package/dist/server/children-interception.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts +45 -0
- package/dist/server/default-status-page.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +4 -7
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +14 -3
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +35 -0
- package/dist/server/head-response.d.ts.map +1 -0
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +528 -373
- package/dist/server/internal.js.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +0 -18
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +59 -9
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +0 -12
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/publish-params.d.ts +33 -0
- package/dist/server/publish-params.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +48 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +32 -2
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +13 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts +78 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -10
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +23 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -0
- package/dist/server/slot-interception.d.ts +80 -0
- package/dist/server/slot-interception.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +70 -8
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +6 -0
- 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-wrappers.d.ts +24 -13
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +6 -27
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +6 -0
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/static-not-found.d.ts +52 -0
- package/dist/server/static-not-found.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +3 -10
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/shared/als-slots.d.ts +41 -0
- package/dist/shared/als-slots.d.ts.map +1 -0
- package/dist/shared/base-path.d.ts +56 -0
- package/dist/shared/base-path.d.ts.map +1 -0
- package/dist/shared/param-value.d.ts +80 -0
- package/dist/shared/param-value.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +108 -0
- package/dist/shared/payload-root.d.ts.map +1 -0
- package/dist/shared/rsc-cache-key.d.ts +163 -0
- package/dist/shared/rsc-cache-key.d.ts.map +1 -0
- package/dist/shared/rsc-error-envelope.d.ts +25 -0
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
- package/dist/shared/rsc-manifest.d.ts +28 -0
- package/dist/shared/rsc-manifest.d.ts.map +1 -0
- package/dist/shared/rsc-payload-path.d.ts +42 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +59 -0
- package/dist/shared/segment-info.d.ts.map +1 -0
- package/dist/shared/slot-params.d.ts +51 -0
- package/dist/shared/slot-params.d.ts.map +1 -0
- package/dist/shared/static-platform-files.d.ts +71 -0
- package/dist/shared/static-platform-files.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +8 -19
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +0 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/docs/api/31-api-client.mdx +4 -2
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/11-error-handling.mdx +4 -3
- package/docs/learn/12-client-navigation.mdx +2 -2
- package/docs/learn/14-deploying.mdx +23 -0
- package/docs/more/01-advanced-routing.mdx +172 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/package.json +2 -2
- package/src/adapters/build-output-helper.ts +116 -3
- package/src/adapters/cloudflare.ts +86 -11
- package/src/adapters/fs-identity.ts +156 -0
- package/src/adapters/nitro.ts +6 -6
- package/src/adapters/shared.ts +196 -21
- package/src/adapters/types.ts +13 -1
- package/src/cache/fast-hash.ts +5 -0
- package/src/client/browser-entry/action-dispatch.ts +9 -4
- package/src/client/browser-entry/hydrate.ts +40 -29
- package/src/client/browser-entry/index.ts +15 -1
- package/src/client/browser-entry/post-hydration.ts +10 -9
- package/src/client/browser-entry/router-init.ts +104 -48
- package/src/client/browser-entry/rsc-stream.ts +11 -2
- package/src/client/error-boundary.tsx +20 -15
- package/src/client/history.ts +13 -6
- package/src/client/internal.ts +4 -3
- package/src/client/link.tsx +55 -8
- package/src/client/navigation-api.ts +3 -3
- package/src/client/navigation-commit.ts +174 -0
- package/src/client/navigation-context.ts +40 -67
- package/src/client/navigation-root.tsx +326 -101
- package/src/client/params-context.ts +192 -0
- package/src/client/router-effects.ts +231 -0
- package/src/client/router-lifecycle.ts +288 -0
- package/src/client/router-pipeline.ts +389 -0
- package/src/client/router-ref.ts +1 -1
- package/src/client/router-skew.ts +30 -0
- package/src/client/router-types.ts +230 -0
- package/src/client/router.ts +242 -690
- package/src/client/rsc-fetch.ts +180 -122
- package/src/client/segment-cache.ts +121 -44
- package/src/client/ssr-data.ts +6 -0
- package/src/client/stale-client.ts +191 -0
- package/src/client/state.ts +14 -1
- package/src/client/top-loader.tsx +2 -1
- package/src/client/unload-guard.ts +34 -2
- package/src/client/use-cookie.ts +9 -2
- package/src/client/use-router.ts +1 -1
- package/src/client/use-segment-params.ts +41 -18
- package/src/index.ts +5 -1
- package/src/plugin-context.ts +34 -1
- package/src/plugins/adapter-build.ts +4 -0
- package/src/plugins/build-manifest.ts +34 -13
- package/src/plugins/entries.ts +4 -0
- package/src/plugins/fonts.ts +11 -8
- package/src/plugins/request-dep/analysis.ts +447 -0
- package/src/plugins/request-dep/index.ts +376 -0
- package/src/plugins/request-dep/origin.ts +310 -0
- package/src/plugins/routing.ts +60 -3
- package/src/plugins/static-build.ts +333 -63
- package/src/routing/codegen-types.ts +18 -0
- package/src/routing/codegen.ts +50 -10
- package/src/routing/collision-probe.ts +438 -0
- package/src/routing/collision-spaces.ts +281 -0
- package/src/routing/interception.ts +896 -50
- package/src/routing/manifest-codegen.ts +6 -9
- package/src/routing/scanner.ts +114 -113
- package/src/routing/segment-classify.ts +57 -0
- package/src/routing/segment-keys.ts +146 -0
- package/src/routing/slot-placement.ts +97 -0
- package/src/routing/types.ts +1 -3
- package/src/routing/walkers.ts +8 -0
- package/src/search-params/define.ts +6 -29
- package/src/segment-params/define.ts +4 -27
- package/src/server/action-handler.ts +3 -1
- package/src/server/actions.ts +6 -3
- package/src/server/als-registry.ts +2 -1
- package/src/server/build-manifest.ts +14 -1
- package/src/server/chain-url-parts.ts +163 -0
- package/src/server/children-interception.ts +115 -0
- package/src/server/default-status-page.ts +127 -0
- package/src/server/deny-boundary.ts +5 -31
- package/src/server/deny-renderer.ts +83 -22
- package/src/server/head-response.ts +54 -0
- package/src/server/param-coercion.ts +48 -18
- package/src/server/pipeline-helpers.ts +0 -45
- package/src/server/pipeline-interception.ts +81 -32
- package/src/server/pipeline-outcome.ts +3 -5
- package/src/server/pipeline-phases.ts +68 -16
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-builder.ts +1 -7
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/primitives.ts +0 -18
- package/src/server/publish-params.ts +42 -0
- package/src/server/request-context.ts +97 -18
- package/src/server/route-element-builder.ts +89 -98
- package/src/server/route-handler.ts +8 -8
- package/src/server/route-matcher.ts +21 -9
- package/src/server/rsc-cache-key-guard.ts +170 -0
- package/src/server/rsc-entry/api-handler.ts +6 -8
- package/src/server/rsc-entry/error-renderer.ts +64 -25
- package/src/server/rsc-entry/helpers.ts +31 -16
- package/src/server/rsc-entry/index.ts +36 -28
- package/src/server/rsc-entry/render-route.ts +12 -7
- package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
- package/src/server/rsc-entry/rsc-payload.ts +14 -25
- package/src/server/rsc-entry/rsc-stream.ts +4 -3
- package/src/server/rsc-entry/ssr-renderer.ts +13 -9
- package/src/server/sitemap-generator.ts +1 -7
- package/src/server/skippable-prefix.ts +92 -0
- package/src/server/slot-interception.ts +168 -0
- package/src/server/slot-resolver.ts +253 -156
- package/src/server/ssr-bridge-types.ts +6 -0
- package/src/server/ssr-entry.ts +12 -4
- package/src/server/ssr-wrappers.tsx +33 -32
- package/src/server/state-tree-diff.ts +14 -55
- package/src/server/static-generator.ts +67 -45
- package/src/server/static-not-found.ts +134 -0
- package/src/server/status-code-resolver.ts +4 -47
- package/src/shared/als-slots.ts +72 -0
- package/src/shared/base-path.ts +80 -0
- package/src/shared/param-value.ts +244 -0
- package/src/shared/payload-root.ts +151 -0
- package/src/shared/rsc-cache-key.ts +269 -0
- package/src/shared/rsc-error-envelope.ts +31 -0
- package/src/shared/rsc-manifest.ts +29 -0
- package/src/shared/rsc-payload-path.ts +45 -0
- package/src/shared/segment-info.ts +60 -0
- package/src/shared/slot-params.ts +111 -0
- package/src/shared/static-platform-files.ts +75 -0
- package/src/shims/navigation-client.ts +8 -26
- package/src/shims/navigation.ts +0 -4
- package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
- package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-D3naS3eH.js +0 -688
- package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
- package/dist/_chunks/define-COtkxMRT.js +0 -191
- package/dist/_chunks/define-COtkxMRT.js.map +0 -1
- package/dist/_chunks/define-c4au4I9R.js +0 -82
- package/dist/_chunks/define-c4au4I9R.js.map +0 -1
- package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
- package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
- package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
- package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
- package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
- package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
- package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
- package/dist/_chunks/walkers-BtTlKviE.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/plugins/request-dep.d.ts +0 -32
- package/dist/plugins/request-dep.d.ts.map +0 -1
- package/src/plugins/request-dep.ts +0 -499
package/dist/client/internal.js
CHANGED
|
@@ -1,10 +1,79 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
1
|
+
import { a as randomRscCacheKey, n as RSC_KEY_PARAM, o as recordLookup, s as rscCacheKey } from "../_chunks/rsc-cache-key-DD0fl_-s.js";
|
|
2
|
+
import { i as withBasePath, r as stripBasePath } from "../_chunks/base-path-DaQrzbez.js";
|
|
3
|
+
import { t as rscPayloadPath } from "../_chunks/rsc-payload-path-B_LBodc2.js";
|
|
4
|
+
import { i as _setCachedSearch, l as cachedSearch, n as getSsrData, r as setSsrData, t as clearSsrData, u as cachedSearchParams } from "../_chunks/ssr-data-14MXm7Pj.js";
|
|
5
|
+
import { n as getRouterOrNull, r as setGlobalRouter, t as getRouter } from "../_chunks/router-ref-DuYuV_0Q.js";
|
|
6
|
+
import { i as markStaleFromError, n as isClientStale, r as markClientStale, t as TimberErrorBoundary } from "../_chunks/error-boundary-DpYRI_I1.js";
|
|
6
7
|
import { n as useSegmentContext, t as SegmentProvider } from "../_chunks/segment-context-ZDnXDkbz.js";
|
|
7
|
-
import
|
|
8
|
+
import "../_chunks/rsc-error-envelope-tT5PJs4q.js";
|
|
9
|
+
import { a as setNavigationState, i as getNavigationState, n as supersedeNavigationTransitions, o as useNavigationContext, r as NavigationProvider, t as setHardNavigating } from "../_chunks/navigation-root-B29qg0_T.js";
|
|
10
|
+
import { n as setCurrentSlotParams, t as setCurrentParams } from "../_chunks/use-segment-params-ClyUNq4d.js";
|
|
11
|
+
import { t as bindUseQueryStates } from "../_chunks/use-query-states-DFvWd-EA.js";
|
|
12
|
+
//#region src/shared/payload-root.ts
|
|
13
|
+
/**
|
|
14
|
+
* What a reader gets when the value it was handed is not a payload root.
|
|
15
|
+
*
|
|
16
|
+
* In the browser this is unreachable for a real response: all four producers
|
|
17
|
+
* of a route payload go through `withPublishedParams`, and a client talking to
|
|
18
|
+
* a different build is answered with 204 at Stage 1c
|
|
19
|
+
* (`server/pipeline-phases.ts`) before any payload exists. It is reachable in
|
|
20
|
+
* the router's test/fallback path, where `decodeRsc` is absent and the
|
|
21
|
+
* "payload" is the raw response text.
|
|
22
|
+
*/
|
|
23
|
+
var NO_PUBLISHED_PARAMS = {
|
|
24
|
+
params: {},
|
|
25
|
+
slotParams: null
|
|
26
|
+
};
|
|
27
|
+
/** True when `value` carries published params — a root, or a read of one. */
|
|
28
|
+
function hasPublishedParams(value) {
|
|
29
|
+
return typeof value === "object" && value !== null && "params" in value && typeof value.params === "object" && value.params !== null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the params published beside a tree.
|
|
33
|
+
*
|
|
34
|
+
* Accepts a payload root *or* a `PublishedParams` already split off one, so
|
|
35
|
+
* the client can pre-resolve on the navigation path and still hand the same
|
|
36
|
+
* value to the same provider. Never throws.
|
|
37
|
+
*/
|
|
38
|
+
function readPublishedParams(source) {
|
|
39
|
+
if (!hasPublishedParams(source)) return NO_PUBLISHED_PARAMS;
|
|
40
|
+
return {
|
|
41
|
+
params: source.params,
|
|
42
|
+
slotParams: source.slotParams ?? null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the renderable tree out of a payload root.
|
|
47
|
+
*
|
|
48
|
+
* Returns the value unchanged when it is not a root — the router's fallback
|
|
49
|
+
* path stores raw response text under the same name, and a test asserting on
|
|
50
|
+
* that text should see the text.
|
|
51
|
+
*/
|
|
52
|
+
function readPayloadTree(root) {
|
|
53
|
+
if (typeof root === "object" && root !== null && "tree" in root && hasPublishedParams(root)) return root.tree;
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Split a decoded payload root into its tree and its params.
|
|
58
|
+
*
|
|
59
|
+
* Accepts a settled root or a thenable of one, and preserves which it was: a
|
|
60
|
+
* settled root splits synchronously, so no code path gains a suspend point it
|
|
61
|
+
* did not already have.
|
|
62
|
+
*/
|
|
63
|
+
function splitPayloadRoot(root) {
|
|
64
|
+
if (typeof root !== "object" || root === null || typeof root.then !== "function") return {
|
|
65
|
+
tree: readPayloadTree(root),
|
|
66
|
+
params: readPublishedParams(root)
|
|
67
|
+
};
|
|
68
|
+
const settled = root;
|
|
69
|
+
const tree = Promise.resolve(settled).then(readPayloadTree);
|
|
70
|
+
tree.catch(() => {});
|
|
71
|
+
return {
|
|
72
|
+
tree,
|
|
73
|
+
params: Promise.resolve(settled).then(readPublishedParams, () => NO_PUBLISHED_PARAMS)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
8
77
|
//#region src/client/segment-cache.ts
|
|
9
78
|
/**
|
|
10
79
|
* Maintains the client-side segment tree representing currently mounted
|
|
@@ -32,18 +101,28 @@ var SegmentCache = class {
|
|
|
32
101
|
* client can actually merge (i.e., segments whose cached element tree
|
|
33
102
|
* contains an inner SegmentProvider the merger can splice into).
|
|
34
103
|
*
|
|
104
|
+
* `treePaths` is collected UNFILTERED, unlike `segments` and `slots`. Those
|
|
105
|
+
* two answer "what may the server skip re-rendering?", so every reason a
|
|
106
|
+
* segment cannot be reused is a reason to leave it out. `treePaths` answers
|
|
107
|
+
* "what is mounted right now?" for interception scoping — a request-dependent
|
|
108
|
+
* layout is still on screen, and dropping it would silently narrow the scope
|
|
109
|
+
* the server sees and cancel a modal that should open (TIM-1282).
|
|
110
|
+
*
|
|
35
111
|
* This is a performance optimization only, NOT a security boundary.
|
|
36
112
|
* The server always runs all access.ts files regardless of the state tree.
|
|
37
113
|
*/
|
|
38
114
|
serializeStateTree(mergeableFilter) {
|
|
39
115
|
const segments = [];
|
|
40
116
|
const slots = [];
|
|
117
|
+
const treePaths = [];
|
|
41
118
|
if (this.root) {
|
|
42
119
|
collectSyncSegments(this.root, segments, mergeableFilter);
|
|
43
120
|
collectSyncSlots(this.root, slots);
|
|
121
|
+
collectTreePaths(this.root, treePaths);
|
|
44
122
|
}
|
|
45
123
|
const tree = { segments };
|
|
46
124
|
if (slots.length > 0) tree.slots = slots;
|
|
125
|
+
if (treePaths.length > 0) tree.treePaths = treePaths;
|
|
47
126
|
return tree;
|
|
48
127
|
}
|
|
49
128
|
};
|
|
@@ -52,6 +131,17 @@ function collectSyncSegments(node, out, mergeableFilter) {
|
|
|
52
131
|
if (!node.isRequestDependent && (!mergeableFilter || mergeableFilter.has(node.segment))) out.push(node.segment);
|
|
53
132
|
for (const child of node.children.values()) collectSyncSegments(child, out, mergeableFilter);
|
|
54
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Recursively collect the `app/` directory path of every mounted segment.
|
|
136
|
+
*
|
|
137
|
+
* Slots are not walked: a slot's own sub-tree can never own an intercepting
|
|
138
|
+
* slot's scope, because scopes are always ordinary segments (`interception.ts`
|
|
139
|
+
* derives one from the slot owner's ancestor chain).
|
|
140
|
+
*/
|
|
141
|
+
function collectTreePaths(node, out) {
|
|
142
|
+
if (node.treePath) out.push(node.treePath);
|
|
143
|
+
for (const child of node.children.values()) collectTreePaths(child, out);
|
|
144
|
+
}
|
|
55
145
|
/** Recursively collect cacheable slot paths from the tree */
|
|
56
146
|
function collectSyncSlots(node, out) {
|
|
57
147
|
if (node.slots) {
|
|
@@ -83,6 +173,7 @@ function buildSegmentTree(segments) {
|
|
|
83
173
|
const id = info.segmentId ?? info.path;
|
|
84
174
|
const node = {
|
|
85
175
|
segment: id,
|
|
176
|
+
treePath: info.treePath,
|
|
86
177
|
payload: null,
|
|
87
178
|
isRequestDependent: info.isRequestDependent,
|
|
88
179
|
children: /* @__PURE__ */ new Map()
|
|
@@ -112,6 +203,22 @@ function buildSegmentTree(segments) {
|
|
|
112
203
|
/** Sentinel value for negative cache entries (URL is not a route). */
|
|
113
204
|
var NEGATIVE_ENTRY = Object.freeze({ payload: null });
|
|
114
205
|
/**
|
|
206
|
+
* Compose the map key. Length-prefixed rather than delimiter-joined for the
|
|
207
|
+
* same reason `shared/rsc-cache-key.ts` is: any delimiter is forgeable by a
|
|
208
|
+
* value containing it, and a URL may contain any character a delimiter could.
|
|
209
|
+
*/
|
|
210
|
+
function prefetchMapKey(key) {
|
|
211
|
+
return `${key.from.length}:${key.from}:${key.scope.length}:${key.scope}:${key.url}`;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The `scope` half of a `PrefetchKey`, derived from the state tree that will
|
|
215
|
+
* actually be sent. Taking it from the header value rather than re-walking the
|
|
216
|
+
* cache is the point: the key varies by exactly what the request varies by.
|
|
217
|
+
*/
|
|
218
|
+
function prefetchScopeOf(stateTree) {
|
|
219
|
+
return stateTree?.treePaths?.join("\0") ?? "";
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
115
222
|
* Short-lived cache for hover-triggered prefetches. Entries expire after
|
|
116
223
|
* 30 seconds. When a link is clicked, the prefetched payload is consumed
|
|
117
224
|
* (moved to the history stack) and removed from this cache.
|
|
@@ -122,34 +229,42 @@ var NEGATIVE_ENTRY = Object.freeze({ payload: null });
|
|
|
122
229
|
var PrefetchCache = class PrefetchCache {
|
|
123
230
|
static TTL_MS = 3e4;
|
|
124
231
|
entries = /* @__PURE__ */ new Map();
|
|
125
|
-
set(
|
|
126
|
-
this.entries.set(
|
|
232
|
+
set(key, result) {
|
|
233
|
+
this.entries.set(prefetchMapKey(key), {
|
|
127
234
|
result,
|
|
128
235
|
expiresAt: Date.now() + PrefetchCache.TTL_MS
|
|
129
236
|
});
|
|
130
237
|
}
|
|
131
|
-
get(
|
|
132
|
-
const
|
|
238
|
+
get(key) {
|
|
239
|
+
const mapKey = prefetchMapKey(key);
|
|
240
|
+
const entry = this.entries.get(mapKey);
|
|
133
241
|
if (!entry) return void 0;
|
|
134
242
|
if (Date.now() >= entry.expiresAt) {
|
|
135
|
-
this.entries.delete(
|
|
243
|
+
this.entries.delete(mapKey);
|
|
136
244
|
return;
|
|
137
245
|
}
|
|
138
246
|
return entry.result;
|
|
139
247
|
}
|
|
140
248
|
/** Get and remove the entry (used when navigation consumes a prefetch) */
|
|
141
|
-
consume(
|
|
142
|
-
const result = this.get(
|
|
143
|
-
if (result !== void 0) this.entries.delete(
|
|
249
|
+
consume(key) {
|
|
250
|
+
const result = this.get(key);
|
|
251
|
+
if (result !== void 0) this.entries.delete(prefetchMapKey(key));
|
|
144
252
|
return result;
|
|
145
253
|
}
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Store a negative entry — the URL is not a route (non-RSC Content-Type).
|
|
256
|
+
*
|
|
257
|
+
* Keyed like every other entry even though "not a route" does not actually
|
|
258
|
+
* vary by departing URL: a source-independent negative would be a second
|
|
259
|
+
* keying rule to keep correct, and the only cost of the uniform one is a
|
|
260
|
+
* repeated fetch for a link hovered from a second page.
|
|
261
|
+
*/
|
|
262
|
+
setNegative(key) {
|
|
263
|
+
this.set(key, NEGATIVE_ENTRY);
|
|
149
264
|
}
|
|
150
265
|
/** Check if the entry is a negative cache entry (URL is not a route). */
|
|
151
|
-
isNegative(
|
|
152
|
-
return this.get(
|
|
266
|
+
isNegative(key) {
|
|
267
|
+
return this.get(key) === NEGATIVE_ENTRY;
|
|
153
268
|
}
|
|
154
269
|
};
|
|
155
270
|
//#endregion
|
|
@@ -183,30 +298,164 @@ var HistoryStack = class {
|
|
|
183
298
|
}
|
|
184
299
|
};
|
|
185
300
|
//#endregion
|
|
186
|
-
//#region src/client/
|
|
187
|
-
var RSC_CONTENT_TYPE = "text/x-component";
|
|
301
|
+
//#region src/client/navigation-commit.ts
|
|
188
302
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
303
|
+
* What it means for a page to be "current" on the client.
|
|
304
|
+
*
|
|
305
|
+
* One module owns every write that makes a navigation's destination the
|
|
306
|
+
* current page, and the read that describes the current page back to the
|
|
307
|
+
* server. Keeping them together is the point: `X-Timber-State-Tree` is
|
|
308
|
+
* derived from the segment cache, so the thing that publishes and the thing
|
|
309
|
+
* that reports have to agree about when a page becomes current.
|
|
310
|
+
*
|
|
311
|
+
* See design/19-client-navigation.md §"State Update Invariants".
|
|
312
|
+
*/
|
|
313
|
+
/**
|
|
314
|
+
* Derive the navigation state (pathname + search) a URL renders with.
|
|
315
|
+
*
|
|
316
|
+
* Pure: publishing it — the module-level fallback for tests and SSR, and the
|
|
317
|
+
* globalThis bridge — is `prepareNavigation`'s commit, which runs only once
|
|
318
|
+
* the render is known to have won (TIM-1301). Callers pass the returned value
|
|
319
|
+
* explicitly to renderRoot/wrapPayload, so a render never depends on the
|
|
320
|
+
* publish having happened first.
|
|
191
321
|
*/
|
|
192
|
-
function
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
322
|
+
function deriveNavigationState(url) {
|
|
323
|
+
const parsed = new URL(url, "http://localhost");
|
|
324
|
+
return {
|
|
325
|
+
pathname: parsed.pathname || "/",
|
|
326
|
+
search: parsed.search
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function createNavigationCommitter(deps) {
|
|
330
|
+
const { segmentCache, historyStack } = deps;
|
|
331
|
+
/**
|
|
332
|
+
* Update the segment cache from server-provided segment metadata.
|
|
333
|
+
*
|
|
334
|
+
* Runs whether or not `clientSegmentCache` is enabled: the tree is also
|
|
335
|
+
* where the mounted tree paths come from, and those cross on every
|
|
336
|
+
* navigation. What the flag gates is which parts get serialized — see
|
|
337
|
+
* `currentStateTree`.
|
|
338
|
+
*/
|
|
339
|
+
function updateSegmentCache(segmentInfo) {
|
|
340
|
+
if (!segmentInfo || segmentInfo.length === 0) return;
|
|
341
|
+
const tree = buildSegmentTree(segmentInfo);
|
|
342
|
+
if (tree) segmentCache.set("/", tree);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
updateSegmentCache,
|
|
346
|
+
/**
|
|
347
|
+
* The X-Timber-State-Tree to send with the next RSC request.
|
|
348
|
+
*
|
|
349
|
+
* Two different things travel on one header. `segments`/`slots` are the
|
|
350
|
+
* caching opt-in and are omitted unless `clientSegmentCache` is on.
|
|
351
|
+
* `treePaths` is how the server learns which route is actually mounted,
|
|
352
|
+
* which stops being derivable from the address bar as soon as a modal is
|
|
353
|
+
* open — so it crosses regardless (TIM-1282). Undefined when there is
|
|
354
|
+
* nothing to say, so a no-op header never enters the `_rsc` cache key.
|
|
355
|
+
*
|
|
356
|
+
* Reads what the last *committed* navigation published, so a superseded
|
|
357
|
+
* navigation's destination can never end up describing the mounted tree
|
|
358
|
+
* (TIM-1301).
|
|
359
|
+
*/
|
|
360
|
+
currentStateTree() {
|
|
361
|
+
const tree = segmentCache.serializeStateTree();
|
|
362
|
+
if (deps.clientSegmentCache()) return tree;
|
|
363
|
+
return tree.treePaths ? {
|
|
364
|
+
segments: [],
|
|
365
|
+
treePaths: tree.treePaths
|
|
366
|
+
} : void 0;
|
|
367
|
+
},
|
|
368
|
+
/**
|
|
369
|
+
* Prepare all navigation-owned state for a new page — without publishing
|
|
370
|
+
* any of it. Every code path that changes the "current page" must go
|
|
371
|
+
* through this function, which is what makes "forgot a field" impossible
|
|
372
|
+
* by construction.
|
|
373
|
+
*
|
|
374
|
+
* Returns the destination's `NavigationState`, which is a pure function of
|
|
375
|
+
* the URL and is what the incoming tree is *rendered* with, plus a
|
|
376
|
+
* `commit` thunk that performs the writes that make a page current:
|
|
377
|
+
* 1. Segment cache — update from server-provided segment metadata
|
|
378
|
+
* 2. Navigation state — pathname/search for usePathname/useSearchParams
|
|
379
|
+
* 3. History stack — store the payload for instant back/forward replay
|
|
380
|
+
*
|
|
381
|
+
* Nothing is written until `commit()` runs, and on a transitioned
|
|
382
|
+
* navigation it runs only once the transition is known to have won — see
|
|
383
|
+
* `renderViaTransition` and `NavigationRoot`. A navigation superseded
|
|
384
|
+
* while its payload was in flight never calls it, so the cache, the
|
|
385
|
+
* pathname the client reports and the history stack all keep describing
|
|
386
|
+
* the route still on screen (TIM-1301).
|
|
387
|
+
*
|
|
388
|
+
* Callers with state of their own to publish — the address bar, the
|
|
389
|
+
* client's record of the mounted tree — wrap this thunk rather than
|
|
390
|
+
* commit beside it, so there stays exactly one moment at which a page
|
|
391
|
+
* becomes current.
|
|
392
|
+
*/
|
|
393
|
+
prepareNavigation(url, opts) {
|
|
394
|
+
const navState = deriveNavigationState(url);
|
|
395
|
+
return {
|
|
396
|
+
navState,
|
|
397
|
+
commit() {
|
|
398
|
+
if (opts.segmentInfo && opts.segmentInfo.length > 0) updateSegmentCache(opts.segmentInfo);
|
|
399
|
+
else if (opts.segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) segmentCache.clear();
|
|
400
|
+
setNavigationState(navState);
|
|
401
|
+
historyStack.push(url, {
|
|
402
|
+
payload: opts.payload,
|
|
403
|
+
params: opts.params,
|
|
404
|
+
segmentInfo: opts.segmentInfo
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
};
|
|
197
410
|
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/client/rsc-fetch.ts
|
|
198
413
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
414
|
+
* RSC Fetch — handles fetching and parsing RSC Flight payloads.
|
|
415
|
+
*
|
|
416
|
+
* Extracted from router.ts to keep both files under the 500-line limit.
|
|
417
|
+
* This module handles:
|
|
418
|
+
* - Cache-busting URL generation for RSC requests
|
|
419
|
+
* - Building RSC request headers (Accept, X-Timber-State-Tree)
|
|
420
|
+
* - Extracting metadata from RSC response headers
|
|
421
|
+
* - Fetching and decoding RSC payloads
|
|
422
|
+
*
|
|
423
|
+
* See design/19-client-navigation.md §"RSC Payload Handling"
|
|
424
|
+
*/
|
|
425
|
+
var RSC_CONTENT_TYPE = "text/x-component";
|
|
426
|
+
/**
|
|
427
|
+
* Append a `_rsc=<key>` query parameter to the URL (TIM-1268).
|
|
428
|
+
*
|
|
429
|
+
* The key is a deterministic hash of the RSC request headers, so identical
|
|
430
|
+
* requests share a URL and the payload becomes cacheable. It also keeps the
|
|
431
|
+
* document and the payload on different URLs, so a shared cache cannot
|
|
432
|
+
* return HTML for an RSC request (or vice versa) even where `Vary` is
|
|
433
|
+
* opt-in configuration rather than default behaviour — Cloudflare and
|
|
434
|
+
* CloudFront both. See GHSA-wfc6-r584-vfw7, design/13-security.md.
|
|
435
|
+
*
|
|
436
|
+
* The origin recomputes this key from the received headers and refuses to
|
|
437
|
+
* let a mismatched response be shared-cached, so a caller cannot claim
|
|
438
|
+
* another client's key while sending its own headers.
|
|
439
|
+
*
|
|
440
|
+
* Falls back to a random value when no key can be derived — `crypto.subtle`
|
|
441
|
+
* is absent in non-secure browsing contexts (plain `http://` on a LAN
|
|
442
|
+
* address). Dropping the parameter instead would put the payload back on
|
|
443
|
+
* the document URL, where a query-keyed cache that ignores `Vary` could
|
|
444
|
+
* store a Flight response under `/about` and serve it to an HTML
|
|
445
|
+
* navigation. The random parameter this feature replaced did separate those
|
|
446
|
+
* representations, and the fallback must not be worse than what it
|
|
447
|
+
* replaced. The origin cannot verify a random key, so it refuses to
|
|
448
|
+
* share-cache the response — which is exactly the old behaviour, where a
|
|
449
|
+
* unique URL was never a cache hit anyway.
|
|
202
450
|
*
|
|
203
451
|
* Strips any #fragment before appending — fragments are client-only and
|
|
204
452
|
* fetch() discards them, so _rsc would land inside the hash and be lost.
|
|
205
453
|
*/
|
|
206
|
-
function appendRscParam(url) {
|
|
454
|
+
async function appendRscParam(url, headers) {
|
|
207
455
|
const hashIndex = url.indexOf("#");
|
|
208
456
|
const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
209
|
-
|
|
457
|
+
const key = await rscCacheKey(recordLookup(headers)) ?? randomRscCacheKey();
|
|
458
|
+
return `${urlWithoutHash}${urlWithoutHash.includes("?") ? "&" : "?"}${RSC_KEY_PARAM}=${key}`;
|
|
210
459
|
}
|
|
211
460
|
/**
|
|
212
461
|
* The client's deployment ID, set at bootstrap from the runtime config.
|
|
@@ -215,6 +464,17 @@ function appendRscParam(url) {
|
|
|
215
464
|
*/
|
|
216
465
|
var clientDeploymentId = null;
|
|
217
466
|
/**
|
|
467
|
+
* The deployment base path (Vite's resolved `base`), normalized.
|
|
468
|
+
* Set at bootstrap from `virtual:timber-config`; `'/'` unless the app is
|
|
469
|
+
* deployed under a sub-path. See design/11-platform.md, TIM-1261.
|
|
470
|
+
*
|
|
471
|
+
* **The base is applied in exactly one place: at fetch time**
|
|
472
|
+
* (`toStaticRscUrl`). The static build writes the RSC manifest with
|
|
473
|
+
* root-relative keys *and* root-relative URLs, so the generated site stays
|
|
474
|
+
* relocatable and there is no second copy of the base to drift from this one.
|
|
475
|
+
*/
|
|
476
|
+
var basePath = "/";
|
|
477
|
+
/**
|
|
218
478
|
* When true, RSC fetches use _rsc/*.rsc file URLs instead of
|
|
219
479
|
* the route URL with Accept headers. Static hosts ignore Accept
|
|
220
480
|
* headers, so the client must fetch the pre-generated .rsc files
|
|
@@ -222,9 +482,10 @@ var clientDeploymentId = null;
|
|
|
222
482
|
*/
|
|
223
483
|
var staticMode = false;
|
|
224
484
|
/**
|
|
225
|
-
* RSC manifest mapping unhashed → hashed URLs. Populated from
|
|
226
|
-
* `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during
|
|
227
|
-
*
|
|
485
|
+
* RSC manifest mapping unhashed → content-hashed `.rsc` URLs. Populated from
|
|
486
|
+
* `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during static
|
|
487
|
+
* generation). Shape shared with the static build that writes it — see
|
|
488
|
+
* `shared/rsc-manifest.ts`. See TIM-1254.
|
|
228
489
|
*/
|
|
229
490
|
var rscManifest = null;
|
|
230
491
|
function getRscManifest() {
|
|
@@ -233,22 +494,35 @@ function getRscManifest() {
|
|
|
233
494
|
return rscManifest;
|
|
234
495
|
}
|
|
235
496
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
497
|
+
* Resolve a route URL to the `_rsc/*.rsc` file to fetch. The naming rule is
|
|
498
|
+
* `shared/rsc-payload-path.ts`, shared with the build that writes the files.
|
|
238
499
|
*
|
|
239
500
|
* When an RSC manifest is available (hashed filenames from TIM-1254),
|
|
240
501
|
* the manifest is consulted to resolve to the hashed path.
|
|
241
502
|
*
|
|
242
503
|
* / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)
|
|
243
|
-
* /about → /_rsc/about.rsc (or /_rsc/about-C8ZzFLfO.rsc
|
|
244
|
-
* /blog/hello → /_rsc/blog/hello.rsc
|
|
504
|
+
* /about → /_rsc/about/index.rsc (or /_rsc/about/index-C8ZzFLfO.rsc)
|
|
505
|
+
* /blog/hello → /_rsc/blog/hello/index.rsc
|
|
506
|
+
*
|
|
507
|
+
* The manifest is written and read in route space (no base prefix), so the
|
|
508
|
+
* base is applied here — once, on the way out. See client/base-path.ts.
|
|
245
509
|
*/
|
|
246
510
|
function toStaticRscUrl(url) {
|
|
247
511
|
const unhashed = toUnhashedRscUrl(url);
|
|
248
|
-
|
|
512
|
+
const hashedUrl = manifestLookup(unhashed);
|
|
513
|
+
return {
|
|
514
|
+
url: withBasePath(basePath, hashedUrl ?? unhashed),
|
|
515
|
+
hashed: hashedUrl !== null
|
|
516
|
+
};
|
|
249
517
|
}
|
|
250
518
|
/**
|
|
251
|
-
* Compute the unhashed _rsc/*.rsc
|
|
519
|
+
* Compute the unhashed `_rsc/*.rsc` path for a route URL.
|
|
520
|
+
*
|
|
521
|
+
* Returns a **route-space** path: the deployment base is stripped from the
|
|
522
|
+
* incoming URL and is NOT re-applied, because this value doubles as the RSC
|
|
523
|
+
* manifest key, and the manifest is base-less. `toStaticRscUrl` applies the
|
|
524
|
+
* base to whatever is actually fetched.
|
|
525
|
+
*
|
|
252
526
|
* @internal Exported for testing.
|
|
253
527
|
*/
|
|
254
528
|
function toUnhashedRscUrl(url) {
|
|
@@ -257,22 +531,15 @@ function toUnhashedRscUrl(url) {
|
|
|
257
531
|
const hashEnd = hashIndex === -1 ? url.length : hashIndex;
|
|
258
532
|
const queryEnd = queryIndex === -1 ? url.length : queryIndex;
|
|
259
533
|
const end = Math.min(hashEnd, queryEnd);
|
|
260
|
-
let pathname = url.slice(0, end);
|
|
534
|
+
let pathname = stripBasePath(basePath, url.slice(0, end));
|
|
261
535
|
if (pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
262
|
-
return
|
|
536
|
+
return rscPayloadPath(pathname);
|
|
263
537
|
}
|
|
264
538
|
/**
|
|
265
|
-
* Convert a route URL to the corresponding _rsc/*.params.json sidecar path.
|
|
266
|
-
* Used in static mode to fetch route params that are normally carried
|
|
267
|
-
* in the X-Timber-Params response header. See TIM-1246.
|
|
268
|
-
*
|
|
269
|
-
* Consults the RSC manifest for hashed filenames (TIM-1254).
|
|
270
|
-
*/
|
|
271
|
-
/**
|
|
272
539
|
* Look up a key in the RSC manifest, falling back to percent-decoded
|
|
273
|
-
* lookup for encoded browser URLs. Returns
|
|
540
|
+
* lookup for encoded browser URLs. Returns the entry or null.
|
|
274
541
|
*/
|
|
275
|
-
function
|
|
542
|
+
function manifestEntry(key) {
|
|
276
543
|
const manifest = getRscManifest();
|
|
277
544
|
if (!manifest) return null;
|
|
278
545
|
if (manifest[key]) return manifest[key];
|
|
@@ -282,9 +549,11 @@ function manifestLookup(key) {
|
|
|
282
549
|
} catch {}
|
|
283
550
|
return null;
|
|
284
551
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
552
|
+
/**
|
|
553
|
+
* Look up the hashed URL for an unhashed RSC path.
|
|
554
|
+
*/
|
|
555
|
+
function manifestLookup(key) {
|
|
556
|
+
return manifestEntry(key)?.url ?? null;
|
|
288
557
|
}
|
|
289
558
|
/** Header name used by the server to signal a version skew reload. */
|
|
290
559
|
var RELOAD_HEADER = "X-Timber-Reload";
|
|
@@ -297,6 +566,17 @@ var DEPLOYMENT_ID_HEADER = "X-Timber-Deployment-Id";
|
|
|
297
566
|
function checkReloadSignal(response) {
|
|
298
567
|
return response.headers.get(RELOAD_HEADER) === "1";
|
|
299
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* Build the headers for an RSC payload request.
|
|
571
|
+
*
|
|
572
|
+
* **Every header added here is hashed into the payload URL** as the `_rsc`
|
|
573
|
+
* cache key (see `rscCacheKey`), and URLs are logged by proxies, CDNs, and
|
|
574
|
+
* origin access logs. So: no credentials, no session identifiers, no
|
|
575
|
+
* user-supplied content. `fnv1aHash` is a cache-key hash, not a KDF — a
|
|
576
|
+
* low-entropy secret would be recoverable from the URL by brute force.
|
|
577
|
+
* Authentication already travels on cookies, which are not part of this
|
|
578
|
+
* object and never enter the key.
|
|
579
|
+
*/
|
|
300
580
|
function buildRscHeaders(stateTree, currentUrl) {
|
|
301
581
|
const headers = { Accept: RSC_CONTENT_TYPE };
|
|
302
582
|
if (stateTree) headers["X-Timber-State-Tree"] = JSON.stringify(stateTree);
|
|
@@ -349,22 +629,6 @@ function extractSkippedSegments(response) {
|
|
|
349
629
|
}
|
|
350
630
|
}
|
|
351
631
|
/**
|
|
352
|
-
* Extract route params from the X-Timber-Params response header.
|
|
353
|
-
* Returns null if the header is missing or malformed.
|
|
354
|
-
*
|
|
355
|
-
* Used to populate useSegmentParams() after client-side navigation.
|
|
356
|
-
*/
|
|
357
|
-
function extractParams(response) {
|
|
358
|
-
const header = response.headers.get("X-Timber-Params");
|
|
359
|
-
if (!header) return null;
|
|
360
|
-
try {
|
|
361
|
-
return JSON.parse(header);
|
|
362
|
-
} catch {
|
|
363
|
-
warnMalformedHeader("X-Timber-Params", header);
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
632
|
* Thrown when an RSC payload response contains X-Timber-Redirect header.
|
|
369
633
|
* Caught in navigate() to trigger a soft router navigation to the redirect target.
|
|
370
634
|
*/
|
|
@@ -426,9 +690,10 @@ var NonRscResponse = class extends Error {
|
|
|
426
690
|
* a `done` promise that resolves when the last chunk is read (or
|
|
427
691
|
* rejects if the stream errors).
|
|
428
692
|
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
693
|
+
* createFromFetch's thenable resolves on shell arrival, but callers need to
|
|
694
|
+
* know when the stream is fully decoded: that is what `navigateTransition`'s
|
|
695
|
+
* returned promise means, and what the router's pending store, the Navigation
|
|
696
|
+
* API deferred and `<Link>`'s `isPending` are all timed against.
|
|
432
697
|
*/
|
|
433
698
|
function trackStreamCompletion(body) {
|
|
434
699
|
let resolveDone;
|
|
@@ -466,22 +731,18 @@ function trackStreamCompletion(body) {
|
|
|
466
731
|
* Otherwise, the raw response text is returned (test mode).
|
|
467
732
|
*/
|
|
468
733
|
async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
const headers =
|
|
734
|
+
const staticTarget = staticMode ? toStaticRscUrl(url) : null;
|
|
735
|
+
const fetchTarget = staticTarget ? staticTarget.url : url;
|
|
736
|
+
const headers = staticMode ? buildRscHeaders(void 0, void 0) : buildRscHeaders(stateTree, currentUrl);
|
|
737
|
+
const rscUrl = staticTarget?.hashed ? fetchTarget : await appendRscParam(fetchTarget, headers);
|
|
738
|
+
signal?.throwIfAborted();
|
|
472
739
|
if (deps.decodeRsc) {
|
|
473
740
|
const fetchPromise = deps.fetch(rscUrl, {
|
|
474
741
|
headers,
|
|
475
742
|
redirect: "manual",
|
|
476
743
|
signal
|
|
477
744
|
});
|
|
478
|
-
const staticParamsFetch = staticMode ? deps.fetch(toStaticParamsUrl(url), { signal }).then((r) => r.ok ? r.json() : null).catch((e) => {
|
|
479
|
-
if (e instanceof DOMException && e.name === "AbortError") throw e;
|
|
480
|
-
return null;
|
|
481
|
-
}) : null;
|
|
482
|
-
staticParamsFetch?.catch(() => {});
|
|
483
745
|
let segmentInfo = null;
|
|
484
|
-
let params = null;
|
|
485
746
|
let skippedSegments = null;
|
|
486
747
|
let streamDone = Promise.resolve();
|
|
487
748
|
const wrappedPromise = fetchPromise.then((response) => {
|
|
@@ -503,7 +764,6 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
503
764
|
}
|
|
504
765
|
}
|
|
505
766
|
segmentInfo = extractSegmentInfo(response);
|
|
506
|
-
params = extractParams(response);
|
|
507
767
|
skippedSegments = extractSkippedSegments(response);
|
|
508
768
|
if (response.body) {
|
|
509
769
|
const tracked = trackStreamCompletion(response.body);
|
|
@@ -517,17 +777,19 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
517
777
|
return response;
|
|
518
778
|
});
|
|
519
779
|
await wrappedPromise;
|
|
520
|
-
|
|
521
|
-
const payload =
|
|
780
|
+
const root = deps.decodeRsc(wrappedPromise);
|
|
781
|
+
const { tree: payload, params } = splitPayloadRoot(root);
|
|
522
782
|
const payloadError = new Promise((_, reject) => {
|
|
523
|
-
Promise.resolve(
|
|
783
|
+
Promise.resolve(root).then(() => {}, reject);
|
|
524
784
|
});
|
|
525
785
|
payloadError.catch(() => {});
|
|
786
|
+
const decodePromise = Promise.race([streamDone, payloadError]);
|
|
787
|
+
decodePromise.catch(() => {});
|
|
526
788
|
return {
|
|
527
789
|
payload,
|
|
528
|
-
decodePromise: Promise.race([streamDone, payloadError]),
|
|
529
|
-
segmentInfo,
|
|
530
790
|
params,
|
|
791
|
+
decodePromise,
|
|
792
|
+
segmentInfo,
|
|
531
793
|
skippedSegments
|
|
532
794
|
};
|
|
533
795
|
}
|
|
@@ -554,23 +816,191 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl, signal) {
|
|
|
554
816
|
throw new NonRscResponse(url);
|
|
555
817
|
}
|
|
556
818
|
}
|
|
557
|
-
let fallbackParams = extractParams(response);
|
|
558
|
-
if (staticMode && !fallbackParams) try {
|
|
559
|
-
const paramsResponse = await deps.fetch(toStaticParamsUrl(url), { signal });
|
|
560
|
-
if (paramsResponse.ok) fallbackParams = await paramsResponse.json();
|
|
561
|
-
} catch (e) {
|
|
562
|
-
if (e instanceof DOMException && e.name === "AbortError") throw e;
|
|
563
|
-
}
|
|
564
819
|
return {
|
|
565
820
|
payload: await response.text(),
|
|
821
|
+
params: readPublishedParams(void 0),
|
|
566
822
|
decodePromise: null,
|
|
567
823
|
segmentInfo: extractSegmentInfo(response),
|
|
568
|
-
params: fallbackParams,
|
|
569
824
|
skippedSegments: extractSkippedSegments(response)
|
|
570
825
|
};
|
|
571
826
|
}
|
|
572
827
|
//#endregion
|
|
573
|
-
//#region src/client/router.ts
|
|
828
|
+
//#region src/client/router-skew.ts
|
|
829
|
+
/**
|
|
830
|
+
* Router Skew Recording — turning a rejection into "this bundle is superseded".
|
|
831
|
+
*
|
|
832
|
+
* Detection lives in `stale-client.ts`; this is the one place the router (and
|
|
833
|
+
* anything else holding an RSC rejection) turns an error into that verdict.
|
|
834
|
+
* Recovery is not here: every path that gives up on client-side navigation —
|
|
835
|
+
* skew or not — leaves through `createSpaExits()` in `router-effects.ts`.
|
|
836
|
+
*
|
|
837
|
+
* See design/33-version-skew.md §"Recovery on the next navigation"
|
|
838
|
+
*/
|
|
839
|
+
/**
|
|
840
|
+
* Record a version skew if `error` is one, and report whether it was.
|
|
841
|
+
*
|
|
842
|
+
* `VersionSkewError` is the origin saying so outright; the reactive shapes are
|
|
843
|
+
* inferred from the error text and are a no-op in dev.
|
|
844
|
+
*
|
|
845
|
+
* Closes over nothing — anywhere an RSC payload can fail needs it, including
|
|
846
|
+
* places that have no router instance to hand.
|
|
847
|
+
*/
|
|
848
|
+
function recordSkew(error) {
|
|
849
|
+
if (error instanceof VersionSkewError) {
|
|
850
|
+
markClientStale();
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
return markStaleFromError(error);
|
|
854
|
+
}
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/client/router-effects.ts
|
|
857
|
+
/**
|
|
858
|
+
* Router Effects — full-page navigation and post-paint scroll.
|
|
859
|
+
*
|
|
860
|
+
* The two things the router does *to the document* rather than to its own
|
|
861
|
+
* state: leaving the SPA entirely, and moving the scroll position once React
|
|
862
|
+
* has committed. Extracted from `router.ts` to keep that file focused on
|
|
863
|
+
* navigation state (see design/18-build-system.md §"No file >500 lines").
|
|
864
|
+
*
|
|
865
|
+
* See design/19-client-navigation.md §"Scroll Restoration"
|
|
866
|
+
*/
|
|
867
|
+
function createScrollEffects(deps) {
|
|
868
|
+
/** Run a callback after the next paint (after React commit). */
|
|
869
|
+
function afterPaint(callback) {
|
|
870
|
+
if (deps.afterPaint) deps.afterPaint(callback);
|
|
871
|
+
else callback();
|
|
872
|
+
}
|
|
873
|
+
function restoreScrollAfterPaint(scrollY) {
|
|
874
|
+
afterPaint(() => {
|
|
875
|
+
deps.scrollTo(0, scrollY);
|
|
876
|
+
window.dispatchEvent(new Event("timber:scroll-restored"));
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
function scrollToHashAfterPaint(hash) {
|
|
880
|
+
afterPaint(() => {
|
|
881
|
+
if (deps.scrollToHash?.(hash) !== true) deps.scrollTo(0, 0);
|
|
882
|
+
window.dispatchEvent(new Event("timber:scroll-restored"));
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
restoreScrollAfterPaint,
|
|
887
|
+
scrollToHashAfterPaint
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Leave the SPA and never come back.
|
|
892
|
+
*
|
|
893
|
+
* Every router path that gives up on client-side navigation does the same
|
|
894
|
+
* three things: flag the hard navigation so the Navigation API and React stop
|
|
895
|
+
* acting on a dying document, perform the document load, then block forever so
|
|
896
|
+
* the caller cannot carry on rendering into it. The returned promise is
|
|
897
|
+
* deliberately unresolvable — `location` assignment does not stop this turn of
|
|
898
|
+
* the event loop.
|
|
899
|
+
*
|
|
900
|
+
* Callers: server error, non-RSC response, and every version skew path
|
|
901
|
+
* ([33-version-skew.md](../../../../design/33-version-skew.md)).
|
|
902
|
+
*/
|
|
903
|
+
function leaveSpa(url, fromUrl) {
|
|
904
|
+
setHardNavigating(true);
|
|
905
|
+
hardNavigate(url, fromUrl);
|
|
906
|
+
return new Promise(() => {});
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* The two ownership-aware ways out of the SPA.
|
|
910
|
+
*
|
|
911
|
+
* Both wrap {@link leaveSpa} with the question "is this navigation still the
|
|
912
|
+
* one the user is waiting for?" — asked before leaving on a failure, answered
|
|
913
|
+
* by force on a path that decided before any navigation began. They live
|
|
914
|
+
* together because every caller of `leaveSpa()` needs one or the other, and
|
|
915
|
+
* an unguarded call is the bug (TIM-1275, TIM-1276).
|
|
916
|
+
*/
|
|
917
|
+
function createSpaExits({ currentNavAbort, supersede }) {
|
|
918
|
+
async function leaveSpaIfOwned(navAbort, url, fromUrl) {
|
|
919
|
+
if (currentNavAbort() !== navAbort) return;
|
|
920
|
+
await leaveSpa(url, fromUrl);
|
|
921
|
+
}
|
|
922
|
+
function leaveSpaSuperseding(url, fromUrl) {
|
|
923
|
+
supersede();
|
|
924
|
+
return leaveSpa(url, fromUrl);
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
leaveSpaIfOwned,
|
|
928
|
+
leaveSpaSuperseding
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Answer the failures an RSC fetch can end on, or report that this one is not
|
|
933
|
+
* ours to answer.
|
|
934
|
+
*
|
|
935
|
+
* Returns `true` when the error was handled; the caller rethrows on `false`.
|
|
936
|
+
* Handling a failure by leaving the SPA never resolves at all — the document
|
|
937
|
+
* is going away and the caller must not carry on rendering into it.
|
|
938
|
+
*
|
|
939
|
+
* There is exactly one family of these and one response to it, so it lives in
|
|
940
|
+
* one function rather than once per fetch path. `navigate()`, `refresh()` and
|
|
941
|
+
* an uncached traversal all reach it: the latter two used to rethrow instead,
|
|
942
|
+
* and since nobody awaits a traversal that meant pressing Back on a 500 left
|
|
943
|
+
* the user on the old document with an unhandled rejection (TIM-1277).
|
|
944
|
+
*
|
|
945
|
+
* Order matters in exactly one way: `recordSkew()` ends in error-*text*
|
|
946
|
+
* matching, so it goes last and can never pre-empt a framework control-flow
|
|
947
|
+
* signal whose message happens to read like a stale bundle. The class checks
|
|
948
|
+
* above it are mutually exclusive, so their order is free.
|
|
949
|
+
*
|
|
950
|
+
* Must be called from inside the `runNavigation()` callback — see
|
|
951
|
+
* {@link SpaExits.leaveSpaIfOwned}.
|
|
952
|
+
*/
|
|
953
|
+
function createNavigationRecovery({ currentNavAbort, leaveSpaIfOwned, navigate }) {
|
|
954
|
+
return async function recoverFromNavigationError(error, navAbort, url, fromUrl) {
|
|
955
|
+
if (error instanceof RedirectError) {
|
|
956
|
+
if (currentNavAbort() !== navAbort) return true;
|
|
957
|
+
await navigate(error.redirectUrl);
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
if (error instanceof ServerErrorResponse || error instanceof NonRscResponse || recordSkew(error)) {
|
|
961
|
+
await leaveSpaIfOwned(navAbort, url, fromUrl);
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
return false;
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Perform the full document load.
|
|
969
|
+
*
|
|
970
|
+
* `fromUrl` is the URL the navigation departed from, passed explicitly
|
|
971
|
+
* because the address bar may have already been updated.
|
|
972
|
+
*
|
|
973
|
+
* When the target differs from the departure point only by #fragment,
|
|
974
|
+
* assigning `href` is a hash change, not a load — so assign first (to update
|
|
975
|
+
* the address bar) and then reload (TIM-1235).
|
|
976
|
+
*/
|
|
977
|
+
function hardNavigate(url, fromUrl) {
|
|
978
|
+
const current = new URL(fromUrl, window.location.origin);
|
|
979
|
+
const target = new URL(url, window.location.origin);
|
|
980
|
+
if (target.pathname === current.pathname && target.search === current.search) {
|
|
981
|
+
window.location.href = url;
|
|
982
|
+
window.location.reload();
|
|
983
|
+
} else window.location.href = url;
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region src/client/router-lifecycle.ts
|
|
987
|
+
/**
|
|
988
|
+
* Navigation Lifecycle — who owns the router, and when a fetch may be cut.
|
|
989
|
+
*
|
|
990
|
+
* One navigation at a time owns the router. This module holds that ownership
|
|
991
|
+
* (`currentNavAbort`), the rule for taking it (`createNavAbort` supersedes),
|
|
992
|
+
* the wrapper every navigation runs inside (`runNavigation`), the set of
|
|
993
|
+
* navigations whose trees are on screen and therefore unabortable
|
|
994
|
+
* (`handedOffNavAborts`), and the pending store that `TopLoader` and
|
|
995
|
+
* `usePendingNavigation()` subscribe to.
|
|
996
|
+
*
|
|
997
|
+
* These belong together because every one of them is a read or a write of
|
|
998
|
+
* "which navigation is the user waiting for?" — the question `router.ts` used
|
|
999
|
+
* to answer in four places. Split out of `router.ts` for the same reason
|
|
1000
|
+
* `router-effects.ts` was (design/18-build-system.md §"No file >500 lines").
|
|
1001
|
+
*
|
|
1002
|
+
* See design/19-client-navigation.md §"How Pending State Works".
|
|
1003
|
+
*/
|
|
574
1004
|
/**
|
|
575
1005
|
* Check if an error is an abort error (connection closed / fetch aborted).
|
|
576
1006
|
* Browsers throw DOMException with name 'AbortError' when a fetch is aborted.
|
|
@@ -580,22 +1010,82 @@ function isAbortError(error) {
|
|
|
580
1010
|
if (error instanceof Error && error.name === "AbortError") return true;
|
|
581
1011
|
return false;
|
|
582
1012
|
}
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
const prefetchCache = new PrefetchCache();
|
|
586
|
-
const historyStack = new HistoryStack();
|
|
1013
|
+
function createNavigationLifecycle(deps) {
|
|
1014
|
+
let currentNavAbort = null;
|
|
587
1015
|
let routerPhase = { phase: "idle" };
|
|
588
1016
|
const pendingListeners = /* @__PURE__ */ new Set();
|
|
589
|
-
|
|
1017
|
+
/**
|
|
1018
|
+
* The controllers of navigations whose trees have been HANDED TO REACT.
|
|
1019
|
+
*
|
|
1020
|
+
* Its response may still be streaming: a destination reveals as soon as
|
|
1021
|
+
* React can render it, so the tree on screen routinely has Suspense
|
|
1022
|
+
* boundaries still waiting on later Flight rows. Aborting that response
|
|
1023
|
+
* rejects those rows, and the rejection surfaces through the tree into
|
|
1024
|
+
* whatever error boundary the app has — replacing the page the user is
|
|
1025
|
+
* looking at with an error state, while the successor navigation is still
|
|
1026
|
+
* in flight (codex on #1004).
|
|
1027
|
+
*
|
|
1028
|
+
* So such a navigation's stream is allowed to finish. It is finite and
|
|
1029
|
+
* already in flight; the alternative is a visible error on the page being
|
|
1030
|
+
* departed from. Cancelling is still correct for a navigation whose payload
|
|
1031
|
+
* never reached React at all, which is the case the abort was written for.
|
|
1032
|
+
*
|
|
1033
|
+
* **Handed over, not committed.** This is set when the tree is given to
|
|
1034
|
+
* React, not when React commits it. Marking on commit is a whole React
|
|
1035
|
+
* commit phase too late: the notification would be `NavigationRoot`'s
|
|
1036
|
+
* layout effect, and React runs *descendant* layout effects first — so a
|
|
1037
|
+
* destination that navigates from its own mount layout effect (a redirect
|
|
1038
|
+
* guard) runs before the mark and its just-committed stream gets torn out
|
|
1039
|
+
* from under it. `tests/navigation-supersede.test.ts` already pins that
|
|
1040
|
+
* ordering, and there is no earlier hook short of an extra sibling fiber,
|
|
1041
|
+
* which would shift every `useId` in the payload (see `server/ssr-wrappers`).
|
|
1042
|
+
*
|
|
1043
|
+
* Handing over is the right moment on its own terms, not merely a safe
|
|
1044
|
+
* over-approximation: from the instant React holds the tree it may commit
|
|
1045
|
+
* it without asking, so there is no later point at which "not on screen"
|
|
1046
|
+
* is still knowable from out here. The residue is that a navigation
|
|
1047
|
+
* superseded in the window between handover and commit keeps streaming a
|
|
1048
|
+
* payload nobody sees — bandwidth on a finite response, against a visible
|
|
1049
|
+
* error page the other way (codex on #1004, second round).
|
|
1050
|
+
*
|
|
1051
|
+
* A SET, not a single slot. Between a successor's handover and its commit
|
|
1052
|
+
* React is still showing the previous tree — that is what the transition
|
|
1053
|
+
* buys — so the previous navigation's stream is still feeding the screen and
|
|
1054
|
+
* must stay unabortable. Entries are dropped when a later tree actually
|
|
1055
|
+
* commits (`forgetOlderHandoffs`), which is the moment the trees they fed
|
|
1056
|
+
* are gone. A single slot made a legitimate handover steal protection from
|
|
1057
|
+
* a stream still on screen, and let a superseded navigation overwrite the
|
|
1058
|
+
* winner's entry outright.
|
|
1059
|
+
*/
|
|
1060
|
+
const handedOffNavAborts = /* @__PURE__ */ new Set();
|
|
1061
|
+
/**
|
|
1062
|
+
* Cancel a navigation's RSC fetch — unless its tree is the one on screen.
|
|
1063
|
+
*
|
|
1064
|
+
* THE only place a navigation controller is aborted. Every path that gives
|
|
1065
|
+
* up on a navigation calls this, so the "is this tree displayed?" question
|
|
1066
|
+
* is asked once rather than at each site (the same reason `createSpaExits`
|
|
1067
|
+
* exists for the SPA exits). A new abort path is a call to this, not a copy
|
|
1068
|
+
* of `controller.abort()`.
|
|
1069
|
+
*
|
|
1070
|
+
* See `handedOffNavAborts` for why such a navigation keeps its stream.
|
|
1071
|
+
*/
|
|
1072
|
+
function abortUnlessHandedOff(controller) {
|
|
1073
|
+
if (handedOffNavAborts.has(controller)) return;
|
|
1074
|
+
controller.abort();
|
|
1075
|
+
}
|
|
590
1076
|
/**
|
|
591
1077
|
* Create a new AbortController for a navigation, superseding any
|
|
592
1078
|
* previous in-flight navigation. Optionally links to an external
|
|
593
1079
|
* signal (e.g., from the Navigation API's NavigateEvent.signal).
|
|
594
1080
|
*
|
|
595
1081
|
* Superseding is one operation with three parts:
|
|
596
|
-
* 1. Abort the previous navigation's fetch
|
|
1082
|
+
* 1. Abort the previous navigation's fetch — UNLESS its tree is the one on
|
|
1083
|
+
* screen, in which case tearing the stream down would error the page the
|
|
1084
|
+
* user is currently looking at. See `handedOffNavAborts`.
|
|
597
1085
|
* 2. Invalidate its render transition so a response that already
|
|
598
1086
|
* arrived can't commit a stale tree (NavigationRoot's transId guard).
|
|
1087
|
+
* This happens either way, so skipping the abort cannot let a stale tree
|
|
1088
|
+
* displace the successor.
|
|
599
1089
|
* 3. Resolve its Navigation API deferred — the superseded navigation's
|
|
600
1090
|
* finally block is staleness-guarded (see TIM-1034) and no longer
|
|
601
1091
|
* cleans up after itself, so the browser's native loading state for
|
|
@@ -603,14 +1093,14 @@ function createRouter(deps) {
|
|
|
603
1093
|
*/
|
|
604
1094
|
function createNavAbort(externalSignal) {
|
|
605
1095
|
if (currentNavAbort) {
|
|
606
|
-
currentNavAbort
|
|
1096
|
+
abortUnlessHandedOff(currentNavAbort);
|
|
607
1097
|
supersedeNavigationTransitions();
|
|
608
1098
|
deps.completeRouterNavigation?.();
|
|
609
1099
|
}
|
|
610
1100
|
const controller = new AbortController();
|
|
611
1101
|
currentNavAbort = controller;
|
|
612
|
-
if (externalSignal) if (externalSignal.aborted) controller
|
|
613
|
-
else externalSignal.addEventListener("abort", () => controller
|
|
1102
|
+
if (externalSignal) if (externalSignal.aborted) abortUnlessHandedOff(controller);
|
|
1103
|
+
else externalSignal.addEventListener("abort", () => abortUnlessHandedOff(controller), { once: true });
|
|
614
1104
|
return controller;
|
|
615
1105
|
}
|
|
616
1106
|
function setPending(value, url) {
|
|
@@ -622,38 +1112,6 @@ function createRouter(deps) {
|
|
|
622
1112
|
routerPhase = next;
|
|
623
1113
|
for (const listener of pendingListeners) listener(value);
|
|
624
1114
|
}
|
|
625
|
-
/** Update the segment cache from server-provided segment metadata. */
|
|
626
|
-
function updateSegmentCache(segmentInfo) {
|
|
627
|
-
if (!deps.clientSegmentCache) return;
|
|
628
|
-
if (!segmentInfo || segmentInfo.length === 0) return;
|
|
629
|
-
const tree = buildSegmentTree(segmentInfo);
|
|
630
|
-
if (tree) segmentCache.set("/", tree);
|
|
631
|
-
}
|
|
632
|
-
/** Render a decoded RSC payload into the DOM if a renderer is available. */
|
|
633
|
-
function renderPayload(payload, navState) {
|
|
634
|
-
if (deps.renderRoot) deps.renderRoot(payload, navState);
|
|
635
|
-
}
|
|
636
|
-
/**
|
|
637
|
-
* Atomically update all navigation-owned state for a new page. Every
|
|
638
|
-
* code path that changes the "current page" must go through this
|
|
639
|
-
* function — making "forgot a field" impossible by construction.
|
|
640
|
-
*
|
|
641
|
-
* The three operations:
|
|
642
|
-
* 1. Segment cache — update from server-provided segment metadata
|
|
643
|
-
* 2. Navigation state — params + pathname for useSegmentParams/usePathname
|
|
644
|
-
* 3. History stack — store the payload for instant back/forward replay
|
|
645
|
-
*/
|
|
646
|
-
function commitNavigation(url, opts) {
|
|
647
|
-
if (opts.segmentInfo && opts.segmentInfo.length > 0) updateSegmentCache(opts.segmentInfo);
|
|
648
|
-
else if (opts.clearSegmentCacheOnEmpty) segmentCache.clear();
|
|
649
|
-
const navState = updateNavigationState(opts.params, url);
|
|
650
|
-
historyStack.push(url, {
|
|
651
|
-
payload: opts.payload,
|
|
652
|
-
params: navState.params,
|
|
653
|
-
segmentInfo: opts.segmentInfo
|
|
654
|
-
});
|
|
655
|
-
return navState;
|
|
656
|
-
}
|
|
657
1115
|
/**
|
|
658
1116
|
* Wrap a navigation in the standard abort/pending/cleanup lifecycle.
|
|
659
1117
|
* Consolidates the createNavAbort + setPending + staleness-guarded
|
|
@@ -677,6 +1135,117 @@ function createRouter(deps) {
|
|
|
677
1135
|
}
|
|
678
1136
|
}
|
|
679
1137
|
}
|
|
1138
|
+
return {
|
|
1139
|
+
currentNavAbort: () => currentNavAbort,
|
|
1140
|
+
createNavAbort,
|
|
1141
|
+
runNavigation,
|
|
1142
|
+
markHandedOff(owner) {
|
|
1143
|
+
if (currentNavAbort !== owner) return;
|
|
1144
|
+
handedOffNavAborts.add(owner);
|
|
1145
|
+
},
|
|
1146
|
+
forgetOlderHandoffs(owner) {
|
|
1147
|
+
for (const controller of handedOffNavAborts) if (controller !== owner) handedOffNavAborts.delete(controller);
|
|
1148
|
+
},
|
|
1149
|
+
isPending: () => routerPhase.phase === "navigating",
|
|
1150
|
+
getPendingUrl: () => routerPhase.phase === "navigating" ? routerPhase.targetUrl : null,
|
|
1151
|
+
onPendingChange(listener) {
|
|
1152
|
+
pendingListeners.add(listener);
|
|
1153
|
+
return () => pendingListeners.delete(listener);
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region src/client/router-pipeline.ts
|
|
1159
|
+
/**
|
|
1160
|
+
* Navigation Pipeline — URL in, tree in React's hands.
|
|
1161
|
+
*
|
|
1162
|
+
* Everything between "the user asked for this URL" and "React is holding the
|
|
1163
|
+
* destination's tree": consult the prefetch cache or fetch, prepare the commit
|
|
1164
|
+
* that makes the page current, and hand the result over inside a transition.
|
|
1165
|
+
*
|
|
1166
|
+
* `router.ts` keeps the *operations* — `navigate`, `refresh`, `handlePopState`,
|
|
1167
|
+
* `prefetch`, `applyRevalidation` — and each of them is a call into here. The
|
|
1168
|
+
* split is the same one `router-effects.ts` and `router-lifecycle.ts` made
|
|
1169
|
+
* (design/18-build-system.md §"No file >500 lines").
|
|
1170
|
+
*
|
|
1171
|
+
* See design/19-client-navigation.md §"Partial Rendering" and §"Prefetching".
|
|
1172
|
+
*/
|
|
1173
|
+
/**
|
|
1174
|
+
* Normalize a departing URL to the pathname the server is told about.
|
|
1175
|
+
*
|
|
1176
|
+
* This is the value sent as `X-Timber-URL` *and* the source half of the
|
|
1177
|
+
* prefetch cache key — one function so a prefetch and the click that consumes
|
|
1178
|
+
* it can never disagree about what "where we came from" means (TIM-1265). The
|
|
1179
|
+
* query is dropped deliberately: it would land in the `_rsc` digest, which is
|
|
1180
|
+
* logged (TIM-1268), and nothing downstream reads it — slot-skip resolution
|
|
1181
|
+
* and interception scope both compare pathnames.
|
|
1182
|
+
*
|
|
1183
|
+
* Absolute inputs parse against the base and ignore it, so one call handles
|
|
1184
|
+
* both `location.href` and a bare pathname.
|
|
1185
|
+
*/
|
|
1186
|
+
function departingPathname(raw) {
|
|
1187
|
+
return new URL(raw, "http://localhost").pathname;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* The prefetch cache key for a destination.
|
|
1191
|
+
*
|
|
1192
|
+
* Both halves of the cache are keyed through here — the hover that writes an
|
|
1193
|
+
* entry (`router.ts`'s `prefetch()`) and the click that consumes it
|
|
1194
|
+
* (`performNavigationFetch` below) — so the two cannot disagree about what
|
|
1195
|
+
* identifies a payload. That is the whole point of the key: `from` scopes it
|
|
1196
|
+
* to the page the link was on (TIM-1265) and `scope` to the tree that was
|
|
1197
|
+
* mounted (TIM-1282), and an entry written under one reading of either would
|
|
1198
|
+
* simply never be found under the other, silently costing every prefetch.
|
|
1199
|
+
*
|
|
1200
|
+
* `departingPathname` is applied here rather than by callers for the same
|
|
1201
|
+
* reason: it is also what goes out as `X-Timber-URL`, so the key and the
|
|
1202
|
+
* request describe the same departure point.
|
|
1203
|
+
*/
|
|
1204
|
+
function prefetchKeyFor(url, departingUrl, stateTree) {
|
|
1205
|
+
return {
|
|
1206
|
+
url,
|
|
1207
|
+
from: departingPathname(departingUrl),
|
|
1208
|
+
scope: prefetchScopeOf(stateTree)
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function isPartialNavigation(skippedSegments) {
|
|
1212
|
+
return skippedSegments != null && skippedSegments.length > 0;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Build a segment updates map for partial navigation. Identifies the
|
|
1216
|
+
* first non-skipped segment and maps it to the payload content.
|
|
1217
|
+
*/
|
|
1218
|
+
function buildSegmentUpdates(result) {
|
|
1219
|
+
const skipped = new Set(result.skippedSegments);
|
|
1220
|
+
const segmentInfo = result.segmentInfo;
|
|
1221
|
+
const updates = /* @__PURE__ */ new Map();
|
|
1222
|
+
if (segmentInfo) {
|
|
1223
|
+
for (const info of segmentInfo) if (!skipped.has(info.segmentId ?? info.path)) {
|
|
1224
|
+
updates.set(info.segmentId ?? info.path, result.payload);
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return updates;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Inspect a decode rejection for version skew before handing the promise on,
|
|
1232
|
+
* without changing what the consumer sees.
|
|
1233
|
+
*
|
|
1234
|
+
* The decode rejects independently of the fetch, and not every consumer
|
|
1235
|
+
* awaits it — NavigationRoot drops it outright when the transition has been
|
|
1236
|
+
* superseded. A stale client reference arriving that way is still evidence
|
|
1237
|
+
* the bundle is superseded, so record it here where every consumer is fed
|
|
1238
|
+
* from, rather than at each place that might drop it (TIM-1275).
|
|
1239
|
+
*/
|
|
1240
|
+
function observeSkew(decodePromise) {
|
|
1241
|
+
decodePromise?.catch((error) => void recordSkew(error));
|
|
1242
|
+
return decodePromise;
|
|
1243
|
+
}
|
|
1244
|
+
function createNavigationPipeline({ deps, prefetchCache, currentStateTree, prepareNavigation, markHandedOff, forgetOlderHandoffs }) {
|
|
1245
|
+
/** Render a decoded RSC payload into the DOM if a renderer is available. */
|
|
1246
|
+
function renderPayload(payload, navState, params) {
|
|
1247
|
+
if (deps.renderRoot) deps.renderRoot(payload, navState, params);
|
|
1248
|
+
}
|
|
680
1249
|
/**
|
|
681
1250
|
* Resolve thenable payloads in the test/fallback path (no navigateTransition).
|
|
682
1251
|
* In production, React handles thenables from createFromFetch directly via
|
|
@@ -686,124 +1255,75 @@ function createRouter(deps) {
|
|
|
686
1255
|
if (!deps.navigateTransition && payload != null && typeof payload === "object" && "then" in payload) return await payload;
|
|
687
1256
|
return payload;
|
|
688
1257
|
}
|
|
689
|
-
function isPartialNavigation(skippedSegments) {
|
|
690
|
-
return skippedSegments != null && skippedSegments.length > 0;
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Build a segment updates map for partial navigation. Identifies the
|
|
694
|
-
* first non-skipped segment and maps it to the payload content.
|
|
695
|
-
*/
|
|
696
|
-
function buildSegmentUpdates(result) {
|
|
697
|
-
const skipped = new Set(result.skippedSegments);
|
|
698
|
-
const segmentInfo = result.segmentInfo;
|
|
699
|
-
const updates = /* @__PURE__ */ new Map();
|
|
700
|
-
if (segmentInfo) {
|
|
701
|
-
for (const info of segmentInfo) if (!skipped.has(info.segmentId ?? info.path)) {
|
|
702
|
-
updates.set(info.segmentId ?? info.path, result.payload);
|
|
703
|
-
break;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return updates;
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Update navigation state (params + pathname) for the next render.
|
|
710
|
-
*
|
|
711
|
-
* Sets the module-level fallback (for tests and SSR) and the
|
|
712
|
-
* globalThis bridge, then returns the NavigationState so callers
|
|
713
|
-
* can pass it explicitly to renderRoot/wrapPayload — eliminating
|
|
714
|
-
* temporal coupling with getNavigationState().
|
|
715
|
-
*/
|
|
716
|
-
function updateNavigationState(params, url) {
|
|
717
|
-
const resolvedParams = params ?? {};
|
|
718
|
-
setCurrentParams(resolvedParams);
|
|
719
|
-
const parsed = new URL(url, "http://localhost");
|
|
720
|
-
const navState = {
|
|
721
|
-
params: resolvedParams,
|
|
722
|
-
pathname: parsed.pathname || "/",
|
|
723
|
-
search: parsed.search
|
|
724
|
-
};
|
|
725
|
-
setNavigationState(navState);
|
|
726
|
-
return navState;
|
|
727
|
-
}
|
|
728
1258
|
/**
|
|
729
1259
|
* Render a payload via navigateTransition (production) or renderRoot (tests).
|
|
730
|
-
* The perform callback should fetch data,
|
|
731
|
-
*
|
|
1260
|
+
* The perform callback should fetch data, prepare the commit, and return the
|
|
1261
|
+
* FetchResult plus the NavigationState and the commit thunk.
|
|
732
1262
|
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
1263
|
+
* The thunk is handed *through* to NavigationRoot rather than called here:
|
|
1264
|
+
* state management (segmentCache, navState, historyStack) has to happen on
|
|
1265
|
+
* the far side of the supersession check, which only NavigationRoot can
|
|
1266
|
+
* make (TIM-1301). The fallback path below has no transition to be
|
|
1267
|
+
* superseded by, so it commits directly.
|
|
735
1268
|
*/
|
|
736
|
-
async function renderViaTransition(url, perform) {
|
|
1269
|
+
async function renderViaTransition(url, owner, perform) {
|
|
1270
|
+
const handOff = () => markHandedOff(owner);
|
|
1271
|
+
const commitAndForget = (commit) => () => {
|
|
1272
|
+
forgetOlderHandoffs(owner);
|
|
1273
|
+
commit();
|
|
1274
|
+
};
|
|
737
1275
|
if (deps.navigateTransition) {
|
|
738
1276
|
await deps.navigateTransition(url, async (wrapPayload) => {
|
|
739
1277
|
const result = await perform();
|
|
1278
|
+
const params = await result.params;
|
|
740
1279
|
if (isPartialNavigation(result.skippedSegments)) {
|
|
741
1280
|
const segmentUpdates = buildSegmentUpdates(result);
|
|
1281
|
+
const element = wrapPayload(deps._getCurrentPayload?.() ?? result.payload, result.navState, params, segmentUpdates);
|
|
1282
|
+
handOff();
|
|
742
1283
|
return {
|
|
743
|
-
element
|
|
744
|
-
decodePromise: result.decodePromise
|
|
1284
|
+
element,
|
|
1285
|
+
decodePromise: observeSkew(result.decodePromise),
|
|
1286
|
+
commit: commitAndForget(result.commit)
|
|
745
1287
|
};
|
|
746
1288
|
}
|
|
1289
|
+
const element = wrapPayload(result.payload, result.navState, params);
|
|
1290
|
+
handOff();
|
|
747
1291
|
return {
|
|
748
|
-
element
|
|
749
|
-
decodePromise: result.decodePromise
|
|
1292
|
+
element,
|
|
1293
|
+
decodePromise: observeSkew(result.decodePromise),
|
|
1294
|
+
commit: commitAndForget(result.commit)
|
|
750
1295
|
};
|
|
751
1296
|
});
|
|
752
1297
|
return;
|
|
753
1298
|
}
|
|
754
1299
|
const result = await perform();
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
function afterPaint(callback) {
|
|
759
|
-
if (deps.afterPaint) deps.afterPaint(callback);
|
|
760
|
-
else callback();
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Schedule scroll restoration after the next paint and fire the
|
|
764
|
-
* scroll-restored event. Used by navigate, popstate, and refresh.
|
|
765
|
-
*/
|
|
766
|
-
function restoreScrollAfterPaint(scrollY) {
|
|
767
|
-
afterPaint(() => {
|
|
768
|
-
deps.scrollTo(0, scrollY);
|
|
769
|
-
window.dispatchEvent(new Event("timber:scroll-restored"));
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
/**
|
|
773
|
-
* Scroll to the element matching the URL #fragment after paint, falling
|
|
774
|
-
* back to scroll-to-top when no element matches (matching full-page-load
|
|
775
|
-
* behavior for an unknown fragment). Used by forward navigation to a
|
|
776
|
-
* hash-bearing URL (TIM-1035).
|
|
777
|
-
*/
|
|
778
|
-
function scrollToHashAfterPaint(hash) {
|
|
779
|
-
afterPaint(() => {
|
|
780
|
-
if (deps.scrollToHash?.(hash) !== true) deps.scrollTo(0, 0);
|
|
781
|
-
window.dispatchEvent(new Event("timber:scroll-restored"));
|
|
782
|
-
});
|
|
1300
|
+
handOff();
|
|
1301
|
+
commitAndForget(result.commit)();
|
|
1302
|
+
if (!isPartialNavigation(result.skippedSegments)) renderPayload(result.payload, result.navState, await result.params);
|
|
783
1303
|
}
|
|
784
1304
|
/**
|
|
785
1305
|
* Core navigation logic shared between the transition and fallback paths.
|
|
786
1306
|
* Fetches the RSC payload, updates all state, and returns the result.
|
|
787
1307
|
*/
|
|
788
1308
|
async function performNavigationFetch(url, options) {
|
|
789
|
-
|
|
790
|
-
|
|
1309
|
+
const stateTree = currentStateTree();
|
|
1310
|
+
const cacheKey = prefetchKeyFor(url, options.departingUrl ?? deps.getCurrentUrl(), stateTree);
|
|
1311
|
+
const currentUrl = cacheKey.from;
|
|
1312
|
+
if (prefetchCache.isNegative(cacheKey)) {
|
|
1313
|
+
prefetchCache.consume(cacheKey);
|
|
791
1314
|
throw new NonRscResponse(url);
|
|
792
1315
|
}
|
|
793
|
-
const prefetched = prefetchCache.consume(
|
|
1316
|
+
const prefetched = prefetchCache.consume(cacheKey);
|
|
794
1317
|
let result = prefetched ? {
|
|
795
1318
|
payload: prefetched.payload,
|
|
796
|
-
|
|
1319
|
+
params: prefetched.params ?? readPublishedParams(void 0),
|
|
1320
|
+
decodePromise: prefetched.decodePromise ?? null,
|
|
797
1321
|
segmentInfo: prefetched.segmentInfo ?? null,
|
|
798
|
-
params: prefetched.params ?? null,
|
|
799
1322
|
skippedSegments: prefetched.skippedSegments ?? null
|
|
800
1323
|
} : void 0;
|
|
801
|
-
if (result === void 0)
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
result = await fetchRscPayload(url, deps, stateTree, rawDepartingUrl.startsWith("http") ? new URL(rawDepartingUrl).pathname : new URL(rawDepartingUrl, "http://localhost").pathname, options.signal);
|
|
805
|
-
}
|
|
806
|
-
if (!options.skipHistory) {
|
|
1324
|
+
if (result === void 0) result = await fetchRscPayload(url, deps, stateTree, currentUrl, options.signal);
|
|
1325
|
+
const commitHistoryUrl = () => {
|
|
1326
|
+
if (options.skipHistory) return;
|
|
807
1327
|
const commitUrl = options.commitUrl ?? url;
|
|
808
1328
|
deps.setRouterNavigating?.(true);
|
|
809
1329
|
if (options.replace) deps.replaceState({
|
|
@@ -815,21 +1335,69 @@ function createRouter(deps) {
|
|
|
815
1335
|
scrollY: 0
|
|
816
1336
|
}, "", commitUrl);
|
|
817
1337
|
deps.setRouterNavigating?.(false);
|
|
818
|
-
}
|
|
1338
|
+
};
|
|
819
1339
|
const payload = await resolveForFallback(result.payload);
|
|
1340
|
+
const params = await result.params;
|
|
820
1341
|
const isPartial = isPartialNavigation(result.skippedSegments);
|
|
821
1342
|
const hasSkippedSlots = result.segmentInfo?.some((s) => s.slot && s.skipped) ?? false;
|
|
822
|
-
const navState =
|
|
1343
|
+
const { navState, commit } = prepareNavigation(url, {
|
|
823
1344
|
payload: isPartial || hasSkippedSlots ? null : payload,
|
|
824
|
-
params
|
|
1345
|
+
params,
|
|
825
1346
|
segmentInfo: result.segmentInfo
|
|
826
1347
|
});
|
|
827
1348
|
return {
|
|
828
1349
|
...result,
|
|
829
1350
|
payload,
|
|
830
|
-
|
|
1351
|
+
params,
|
|
1352
|
+
navState,
|
|
1353
|
+
commit: () => {
|
|
1354
|
+
commitHistoryUrl();
|
|
1355
|
+
commit();
|
|
1356
|
+
window.dispatchEvent(new Event("timber:navigation-end"));
|
|
1357
|
+
}
|
|
831
1358
|
};
|
|
832
1359
|
}
|
|
1360
|
+
return {
|
|
1361
|
+
performNavigationFetch,
|
|
1362
|
+
renderViaTransition,
|
|
1363
|
+
renderPayload,
|
|
1364
|
+
resolveForFallback
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
//#endregion
|
|
1368
|
+
//#region src/client/router.ts
|
|
1369
|
+
/**
|
|
1370
|
+
* Create a router instance. In production, called once at app hydration
|
|
1371
|
+
* with real browser APIs. In tests, called with mock dependencies.
|
|
1372
|
+
*/
|
|
1373
|
+
function createRouter(deps) {
|
|
1374
|
+
const segmentCache = new SegmentCache();
|
|
1375
|
+
const prefetchCache = new PrefetchCache();
|
|
1376
|
+
const historyStack = new HistoryStack();
|
|
1377
|
+
const { prepareNavigation, currentStateTree, updateSegmentCache } = createNavigationCommitter({
|
|
1378
|
+
segmentCache,
|
|
1379
|
+
historyStack,
|
|
1380
|
+
clientSegmentCache: () => deps.clientSegmentCache
|
|
1381
|
+
});
|
|
1382
|
+
const { currentNavAbort, createNavAbort, runNavigation, markHandedOff, forgetOlderHandoffs, isPending, getPendingUrl, onPendingChange } = createNavigationLifecycle(deps);
|
|
1383
|
+
const { performNavigationFetch, renderViaTransition, renderPayload, resolveForFallback } = createNavigationPipeline({
|
|
1384
|
+
deps,
|
|
1385
|
+
prefetchCache,
|
|
1386
|
+
currentStateTree,
|
|
1387
|
+
prepareNavigation,
|
|
1388
|
+
markHandedOff,
|
|
1389
|
+
forgetOlderHandoffs
|
|
1390
|
+
});
|
|
1391
|
+
const { restoreScrollAfterPaint, scrollToHashAfterPaint } = createScrollEffects(deps);
|
|
1392
|
+
const { leaveSpaIfOwned, leaveSpaSuperseding } = createSpaExits({
|
|
1393
|
+
currentNavAbort,
|
|
1394
|
+
supersede: () => void createNavAbort()
|
|
1395
|
+
});
|
|
1396
|
+
const recoverFromNavigationError = createNavigationRecovery({
|
|
1397
|
+
currentNavAbort,
|
|
1398
|
+
leaveSpaIfOwned,
|
|
1399
|
+
navigate: (url) => navigate(url, { replace: true })
|
|
1400
|
+
});
|
|
833
1401
|
async function navigate(url, options = {}) {
|
|
834
1402
|
const scroll = options.scroll !== false;
|
|
835
1403
|
const replace = options.replace === true;
|
|
@@ -845,6 +1413,7 @@ function createRouter(deps) {
|
|
|
845
1413
|
timber: true,
|
|
846
1414
|
scrollY: currentScrollY
|
|
847
1415
|
}, "", deps.getCurrentUrl());
|
|
1416
|
+
if (isClientStale()) await leaveSpaSuperseding(url, departingUrl);
|
|
848
1417
|
let effectiveSkipHistory = skipHistory;
|
|
849
1418
|
await runNavigation(url, async (navAbort) => {
|
|
850
1419
|
if (!effectiveSkipHistory && deps.navigationNavigate) {
|
|
@@ -854,128 +1423,126 @@ function createRouter(deps) {
|
|
|
854
1423
|
effectiveSkipHistory = true;
|
|
855
1424
|
}
|
|
856
1425
|
try {
|
|
857
|
-
await renderViaTransition(fetchUrl, () => performNavigationFetch(fetchUrl, {
|
|
1426
|
+
await renderViaTransition(fetchUrl, navAbort, () => performNavigationFetch(fetchUrl, {
|
|
858
1427
|
replace,
|
|
859
1428
|
commitUrl: url,
|
|
860
1429
|
signal: navAbort.signal,
|
|
861
1430
|
skipHistory: effectiveSkipHistory,
|
|
862
1431
|
departingUrl
|
|
863
1432
|
}));
|
|
864
|
-
window.dispatchEvent(new Event("timber:navigation-end"));
|
|
865
1433
|
if (scroll && hash) scrollToHashAfterPaint(hash);
|
|
866
1434
|
else restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
867
1435
|
} catch (error) {
|
|
868
|
-
if (error
|
|
869
|
-
setHardNavigating(true);
|
|
870
|
-
window.location.reload();
|
|
871
|
-
await new Promise(() => {});
|
|
872
|
-
}
|
|
873
|
-
if (error instanceof RedirectError) {
|
|
874
|
-
if (currentNavAbort !== navAbort) return;
|
|
875
|
-
await navigate(error.redirectUrl, { replace: true });
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
if (error instanceof ServerErrorResponse) {
|
|
879
|
-
setHardNavigating(true);
|
|
880
|
-
window.location.href = url;
|
|
881
|
-
await new Promise(() => {});
|
|
882
|
-
}
|
|
883
|
-
if (error instanceof NonRscResponse) {
|
|
884
|
-
setHardNavigating(true);
|
|
885
|
-
window.location.href = url;
|
|
886
|
-
await new Promise(() => {});
|
|
887
|
-
}
|
|
1436
|
+
if (await recoverFromNavigationError(error, navAbort, url, departingUrl)) return;
|
|
888
1437
|
throw error;
|
|
889
1438
|
}
|
|
890
1439
|
}, externalSignal);
|
|
891
1440
|
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Fetch a URL's RSC payload, commit it, and render — the pipeline shared by
|
|
1443
|
+
* refresh() and an uncached back/forward traversal. Both can be the path
|
|
1444
|
+
* that discovers a version skew, and both recover the same way (TIM-1275).
|
|
1445
|
+
*
|
|
1446
|
+
* Not `performNavigationFetch`: neither caller consults the prefetch cache
|
|
1447
|
+
* (there is no link to have hovered) and neither moves the address bar (the
|
|
1448
|
+
* browser is already where it is going), so the fetch is a plain one.
|
|
1449
|
+
*/
|
|
1450
|
+
async function fetchCommitAndRender(url, opts = {}) {
|
|
1451
|
+
await runNavigation(url, async (navAbort) => {
|
|
1452
|
+
try {
|
|
1453
|
+
await renderViaTransition(url, navAbort, async () => {
|
|
1454
|
+
const result = await fetchRscPayload(url, deps, opts.stateTree, void 0, navAbort.signal);
|
|
1455
|
+
const payload = await resolveForFallback(result.payload);
|
|
1456
|
+
const params = await result.params;
|
|
1457
|
+
const { navState, commit } = prepareNavigation(url, {
|
|
1458
|
+
payload,
|
|
1459
|
+
params,
|
|
1460
|
+
segmentInfo: result.segmentInfo
|
|
1461
|
+
});
|
|
1462
|
+
return {
|
|
1463
|
+
...result,
|
|
1464
|
+
payload,
|
|
1465
|
+
params,
|
|
1466
|
+
navState,
|
|
1467
|
+
commit
|
|
1468
|
+
};
|
|
1469
|
+
});
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (await recoverFromNavigationError(error, navAbort, url, url)) return;
|
|
1472
|
+
throw error;
|
|
1473
|
+
}
|
|
1474
|
+
if (opts.scrollY !== void 0) restoreScrollAfterPaint(opts.scrollY);
|
|
1475
|
+
}, opts.externalSignal);
|
|
1476
|
+
}
|
|
892
1477
|
async function refresh() {
|
|
893
1478
|
const currentUrl = deps.getCurrentUrl();
|
|
894
|
-
await
|
|
895
|
-
|
|
896
|
-
const result = await fetchRscPayload(currentUrl, deps, void 0, void 0, navAbort.signal);
|
|
897
|
-
const payload = await resolveForFallback(result.payload);
|
|
898
|
-
const navState = commitNavigation(currentUrl, {
|
|
899
|
-
payload,
|
|
900
|
-
params: result.params,
|
|
901
|
-
segmentInfo: result.segmentInfo
|
|
902
|
-
});
|
|
903
|
-
return {
|
|
904
|
-
...result,
|
|
905
|
-
payload,
|
|
906
|
-
navState
|
|
907
|
-
};
|
|
908
|
-
});
|
|
909
|
-
});
|
|
1479
|
+
if (isClientStale()) await leaveSpaSuperseding(currentUrl, currentUrl);
|
|
1480
|
+
await fetchCommitAndRender(currentUrl);
|
|
910
1481
|
}
|
|
911
1482
|
async function handlePopState(url, scrollY = 0, externalSignal) {
|
|
1483
|
+
if (isClientStale()) await leaveSpaSuperseding(url, url);
|
|
912
1484
|
const entry = historyStack.get(url);
|
|
913
1485
|
if (entry && entry.payload !== null) await runNavigation(url, async () => {
|
|
914
|
-
const navState =
|
|
1486
|
+
const { navState, commit } = prepareNavigation(url, {
|
|
915
1487
|
payload: entry.payload,
|
|
916
1488
|
params: entry.params,
|
|
917
1489
|
segmentInfo: entry.segmentInfo,
|
|
918
1490
|
clearSegmentCacheOnEmpty: true
|
|
919
1491
|
});
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}, externalSignal);
|
|
923
|
-
else await runNavigation(url, async (navAbort) => {
|
|
924
|
-
await renderViaTransition(url, async () => {
|
|
925
|
-
const result = await fetchRscPayload(url, deps, deps.clientSegmentCache ? segmentCache.serializeStateTree() : void 0, void 0, navAbort.signal);
|
|
926
|
-
const payload = await resolveForFallback(result.payload);
|
|
927
|
-
const navState = commitNavigation(url, {
|
|
928
|
-
payload,
|
|
929
|
-
params: result.params,
|
|
930
|
-
segmentInfo: result.segmentInfo
|
|
931
|
-
});
|
|
932
|
-
return {
|
|
933
|
-
...result,
|
|
934
|
-
payload,
|
|
935
|
-
navState
|
|
936
|
-
};
|
|
937
|
-
});
|
|
1492
|
+
commit();
|
|
1493
|
+
renderPayload(entry.payload, navState, entry.params);
|
|
938
1494
|
restoreScrollAfterPaint(scrollY);
|
|
939
1495
|
}, externalSignal);
|
|
1496
|
+
else await fetchCommitAndRender(url, {
|
|
1497
|
+
stateTree: currentStateTree(),
|
|
1498
|
+
scrollY,
|
|
1499
|
+
externalSignal
|
|
1500
|
+
});
|
|
940
1501
|
}
|
|
941
1502
|
/**
|
|
942
1503
|
* Prefetch an RSC payload for a URL and store it in the prefetch cache.
|
|
943
1504
|
* Called on hover of <Link prefetch> elements.
|
|
944
1505
|
*/
|
|
945
1506
|
function prefetch(url) {
|
|
1507
|
+
if (isClientStale()) return;
|
|
946
1508
|
const hashIndex = url.indexOf("#");
|
|
947
1509
|
const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
948
|
-
|
|
1510
|
+
const stateTree = currentStateTree();
|
|
1511
|
+
const cacheKey = prefetchKeyFor(fetchUrl, deps.getCurrentUrl(), stateTree);
|
|
1512
|
+
const from = cacheKey.from;
|
|
1513
|
+
if (prefetchCache.get(cacheKey) !== void 0) return;
|
|
949
1514
|
if (historyStack.has(fetchUrl)) return;
|
|
950
|
-
fetchRscPayload(fetchUrl, deps,
|
|
951
|
-
result.decodePromise?.catch(() =>
|
|
952
|
-
prefetchCache.set(
|
|
1515
|
+
fetchRscPayload(fetchUrl, deps, stateTree, from).then((result) => {
|
|
1516
|
+
result.decodePromise?.catch((error) => void recordSkew(error));
|
|
1517
|
+
prefetchCache.set(cacheKey, result);
|
|
953
1518
|
}, (error) => {
|
|
954
1519
|
if (error instanceof NonRscResponse) {
|
|
955
|
-
prefetchCache.setNegative(
|
|
1520
|
+
prefetchCache.setNegative(cacheKey);
|
|
956
1521
|
return;
|
|
957
1522
|
}
|
|
1523
|
+
recordSkew(error);
|
|
958
1524
|
});
|
|
959
1525
|
}
|
|
960
1526
|
return {
|
|
961
1527
|
navigate,
|
|
962
1528
|
refresh,
|
|
963
1529
|
handlePopState,
|
|
964
|
-
isPending
|
|
965
|
-
getPendingUrl
|
|
966
|
-
onPendingChange
|
|
967
|
-
pendingListeners.add(listener);
|
|
968
|
-
return () => pendingListeners.delete(listener);
|
|
969
|
-
},
|
|
1530
|
+
isPending,
|
|
1531
|
+
getPendingUrl,
|
|
1532
|
+
onPendingChange,
|
|
970
1533
|
prefetch,
|
|
971
|
-
applyRevalidation(
|
|
1534
|
+
applyRevalidation(payloadRoot) {
|
|
972
1535
|
const currentUrl = deps.getCurrentUrl();
|
|
1536
|
+
const tree = readPayloadTree(payloadRoot);
|
|
1537
|
+
const params = readPublishedParams(payloadRoot);
|
|
973
1538
|
const existingEntry = historyStack.get(currentUrl);
|
|
974
|
-
|
|
975
|
-
payload:
|
|
976
|
-
params
|
|
1539
|
+
const { navState, commit } = prepareNavigation(currentUrl, {
|
|
1540
|
+
payload: tree,
|
|
1541
|
+
params,
|
|
977
1542
|
segmentInfo: existingEntry?.segmentInfo
|
|
978
|
-
})
|
|
1543
|
+
});
|
|
1544
|
+
commit();
|
|
1545
|
+
renderPayload(tree, navState, params);
|
|
979
1546
|
},
|
|
980
1547
|
initSegmentCache: (segments) => updateSegmentCache(segments),
|
|
981
1548
|
segmentCache,
|
|
@@ -1036,6 +1603,6 @@ function useSearchParams() {
|
|
|
1036
1603
|
return new URLSearchParams();
|
|
1037
1604
|
}
|
|
1038
1605
|
//#endregion
|
|
1039
|
-
export { HistoryStack, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, clearSsrData, createRouter, getNavigationState, getRouter, getRouterOrNull, getSsrData, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useSearchParams, useSegmentContext };
|
|
1606
|
+
export { HistoryStack, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, clearSsrData, createRouter, getNavigationState, getRouter, getRouterOrNull, getSsrData, setCurrentParams, setCurrentSlotParams, setGlobalRouter, setNavigationState, setSsrData, useSearchParams, useSegmentContext };
|
|
1040
1607
|
|
|
1041
1608
|
//# sourceMappingURL=internal.js.map
|