@timber-js/app 0.2.0-alpha.6 → 0.2.0-alpha.61
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/LICENSE +8 -0
- package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-CT98cU9c.js +121 -0
- package/dist/_chunks/define-CT98cU9c.js.map +1 -0
- package/dist/_chunks/define-TK8C1M3x.js +279 -0
- package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
- package/dist/_chunks/define-cookie-BWr_52kY.js +93 -0
- package/dist/_chunks/define-cookie-BWr_52kY.js.map +1 -0
- package/dist/_chunks/error-boundary-DpZJBCqh.js +211 -0
- package/dist/_chunks/error-boundary-DpZJBCqh.js.map +1 -0
- package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-Cey5DCGr.js} +129 -77
- package/dist/_chunks/interception-Cey5DCGr.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-rju2rbga.js} +97 -69
- package/dist/_chunks/request-context-rju2rbga.js.map +1 -0
- package/dist/_chunks/segment-context-CyaM1mrD.js +72 -0
- package/dist/_chunks/segment-context-CyaM1mrD.js.map +1 -0
- package/dist/_chunks/stale-reload-BSSym1MJ.js +64 -0
- package/dist/_chunks/stale-reload-BSSym1MJ.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-VYETCQsg.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-VYETCQsg.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
- package/dist/_chunks/use-query-states-wEXY2JQB.js.map +1 -0
- package/dist/_chunks/wrappers-BaG1bnM3.js +63 -0
- package/dist/_chunks/wrappers-BaG1bnM3.js.map +1 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -13
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +90 -20
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- package/dist/client/error-reconstituter.d.ts +54 -0
- package/dist/client/error-reconstituter.d.ts.map +1 -0
- package/dist/client/form.d.ts +2 -2
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +433 -252
- package/dist/client/index.js.map +1 -1
- package/dist/client/link-pending-store.d.ts +78 -0
- package/dist/client/link-pending-store.d.ts.map +1 -0
- package/dist/client/link.d.ts +23 -9
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +36 -2
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +63 -0
- package/dist/client/segment-outlet.d.ts.map +1 -0
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +3 -3
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +34 -13
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -83
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/index.d.ts +127 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -242
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +100 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +4 -0
- package/dist/plugins/adapter-build.d.ts +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +2 -2
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +3 -3
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/content.d.ts +1 -1
- package/dist/plugins/content.d.ts.map +1 -1
- package/dist/plugins/dev-browser-logs.d.ts +84 -0
- package/dist/plugins/dev-browser-logs.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/dev-logs.d.ts +1 -1
- package/dist/plugins/dev-logs.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +9 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +6 -5
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +16 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +159 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +4 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +1 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +25 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/debug.d.ts +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +4 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +4 -3
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +66 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +42 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +51 -11
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1977 -1648
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +25 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +113 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/pipeline-interception.d.ts +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +20 -6
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +65 -38
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- 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 +9 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts +2 -2
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +26 -13
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +48 -5
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +8 -3
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +10 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +19 -4
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +22 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +39 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +50 -0
- package/dist/server/ssr-wrappers.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +1 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts +36 -0
- package/dist/server/stream-utils.d.ts.map +1 -0
- package/dist/server/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +20 -13
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/server/waituntil-bridge.d.ts.map +1 -1
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/dist/shims/font-google.d.ts +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +17 -17
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +58 -9
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/index.ts +5 -2
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/timber-cache.ts +40 -29
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +151 -99
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/error-reconstituter.tsx +65 -0
- package/src/client/form.tsx +2 -2
- package/src/client/index.ts +10 -1
- package/src/client/link-pending-store.ts +136 -0
- package/src/client/link.tsx +137 -22
- package/src/client/navigation-context.ts +6 -5
- package/src/client/router.ts +117 -60
- package/src/client/rsc-fetch.ts +90 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/segment-outlet.tsx +86 -0
- package/src/client/stale-reload.ts +73 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +20 -2
- package/src/client/use-params.ts +4 -4
- package/src/client/use-query-states.ts +2 -2
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +71 -20
- package/src/fonts/css.ts +2 -1
- package/src/index.ts +297 -85
- package/src/params/define.ts +327 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +8 -2
- package/src/plugins/build-manifest.ts +13 -2
- package/src/plugins/build-report.ts +3 -3
- package/src/plugins/cache-transform.ts +1 -1
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/content.ts +1 -1
- package/src/plugins/dev-browser-logs.ts +284 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-logs.ts +1 -1
- package/src/plugins/dev-server.ts +41 -7
- package/src/plugins/entries.ts +6 -8
- package/src/plugins/fonts.ts +102 -55
- package/src/plugins/mdx.ts +1 -1
- package/src/plugins/routing.ts +57 -17
- package/src/plugins/server-action-exports.ts +1 -1
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +69 -31
- package/src/plugins/static-build.ts +10 -6
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +86 -7
- package/src/routing/status-file-lint.ts +3 -2
- package/src/routing/types.ts +17 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/search-params/codecs.ts +1 -1
- package/src/search-params/define.ts +518 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +40 -9
- package/src/server/action-client.ts +8 -2
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +20 -3
- package/src/server/actions.ts +1 -1
- package/src/server/als-registry.ts +25 -4
- package/src/server/build-manifest.ts +10 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +1 -1
- package/src/server/default-logger.ts +99 -0
- package/src/server/deny-renderer.ts +5 -3
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +58 -15
- package/src/server/fallback-error.ts +29 -14
- package/src/server/flight-injection-state.ts +113 -0
- package/src/server/flight-scripts.ts +62 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +277 -117
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +44 -36
- package/src/server/node-stream-transforms.ts +509 -0
- package/src/server/pipeline-interception.ts +1 -1
- package/src/server/pipeline.ts +148 -41
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +125 -119
- package/src/server/route-element-builder.ts +107 -115
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +9 -2
- package/src/server/rsc-entry/api-handler.ts +8 -8
- package/src/server/rsc-entry/error-renderer.ts +286 -81
- package/src/server/rsc-entry/helpers.ts +134 -5
- package/src/server/rsc-entry/index.ts +177 -76
- package/src/server/rsc-entry/rsc-payload.ts +91 -18
- package/src/server/rsc-entry/rsc-stream.ts +74 -18
- package/src/server/rsc-entry/ssr-bridge.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +152 -34
- package/src/server/slot-resolver.ts +231 -220
- package/src/server/ssr-entry.ts +211 -32
- package/src/server/ssr-render.ts +289 -67
- package/src/server/ssr-wrappers.tsx +139 -0
- package/src/server/status-code-resolver.ts +1 -1
- package/src/server/stream-utils.ts +213 -0
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +92 -58
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/src/shared/merge-search-params.ts +55 -0
- package/src/shims/font-google.ts +1 -1
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +2 -1
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/_chunks/use-query-states-D5KaffOK.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/client/link-status-provider.d.ts +0 -11
- package/dist/client/link-status-provider.d.ts.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/dist/server/response-cache.d.ts +0 -53
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/client/link-status-provider.tsx +0 -30
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
- package/src/server/response-cache.ts +0 -277
package/src/client/link.tsx
CHANGED
|
@@ -18,10 +18,42 @@
|
|
|
18
18
|
// - params and fully-resolved string href are mutually exclusive
|
|
19
19
|
// - searchParams and inline query string are mutually exclusive
|
|
20
20
|
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
import {
|
|
22
|
+
useState,
|
|
23
|
+
useEffect,
|
|
24
|
+
useRef,
|
|
25
|
+
type AnchorHTMLAttributes,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
type MouseEvent as ReactMouseEvent,
|
|
28
|
+
} from 'react';
|
|
29
|
+
import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
30
|
+
import { LinkStatusContext } from './use-link-status.js';
|
|
24
31
|
import { getRouterOrNull } from './router-ref.js';
|
|
32
|
+
import { getSsrData } from './ssr-data.js';
|
|
33
|
+
import { mergePreservedSearchParams } from '../shared/merge-search-params.js';
|
|
34
|
+
import {
|
|
35
|
+
setLinkForCurrentNavigation,
|
|
36
|
+
unmountLinkForCurrentNavigation,
|
|
37
|
+
IDLE_LINK_STATUS,
|
|
38
|
+
PENDING_LINK_STATUS,
|
|
39
|
+
type LinkPendingInstance,
|
|
40
|
+
} from './link-pending-store.js';
|
|
41
|
+
|
|
42
|
+
// ─── Current Search Params ────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read the current URL's search string without requiring a React hook.
|
|
46
|
+
* On the client, reads window.location.search. During SSR, reads from
|
|
47
|
+
* the request context (getSsrData). Returns empty string if unavailable.
|
|
48
|
+
*/
|
|
49
|
+
function getCurrentSearch(): string {
|
|
50
|
+
if (typeof window !== 'undefined') return window.location.search;
|
|
51
|
+
const data = getSsrData();
|
|
52
|
+
if (!data) return '';
|
|
53
|
+
const sp = new URLSearchParams(data.searchParams);
|
|
54
|
+
const str = sp.toString();
|
|
55
|
+
return str ? `?${str}` : '';
|
|
56
|
+
}
|
|
25
57
|
|
|
26
58
|
// ─── Types ───────────────────────────────────────────────────────
|
|
27
59
|
|
|
@@ -42,6 +74,20 @@ interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'h
|
|
|
42
74
|
* Set to false for tabbed interfaces where content changes within a fixed layout.
|
|
43
75
|
*/
|
|
44
76
|
scroll?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Preserve search params from the current URL across navigation.
|
|
79
|
+
*
|
|
80
|
+
* - `true` — preserve ALL current search params (target params take precedence)
|
|
81
|
+
* - `string[]` — preserve only the named params (e.g. `['private', 'token']`)
|
|
82
|
+
*
|
|
83
|
+
* Useful for route-group gating where a search param (e.g. `?private=access`)
|
|
84
|
+
* must persist across internal navigations. The target href's own search params
|
|
85
|
+
* always take precedence over preserved ones.
|
|
86
|
+
*
|
|
87
|
+
* During SSR, reads search params from the request context. On the client,
|
|
88
|
+
* reads from the current URL and updates reactively when the URL changes.
|
|
89
|
+
*/
|
|
90
|
+
preserveSearchParams?: true | string[];
|
|
45
91
|
/**
|
|
46
92
|
* Called before client-side navigation commits. Call `e.preventDefault()`
|
|
47
93
|
* to cancel the default navigation — the caller is then responsible for
|
|
@@ -61,7 +107,7 @@ interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'h
|
|
|
61
107
|
*/
|
|
62
108
|
export interface LinkPropsWithHref extends LinkBaseProps {
|
|
63
109
|
href: string;
|
|
64
|
-
|
|
110
|
+
segmentParams?: never;
|
|
65
111
|
/**
|
|
66
112
|
* Typed search params — serialized via the route's SearchParamsDefinition.
|
|
67
113
|
* Mutually exclusive with an inline query string in href.
|
|
@@ -73,9 +119,9 @@ export interface LinkPropsWithHref extends LinkBaseProps {
|
|
|
73
119
|
}
|
|
74
120
|
|
|
75
121
|
/**
|
|
76
|
-
* Link with a route pattern +
|
|
77
|
-
* e.g. <Link href="/products/[id]"
|
|
78
|
-
* <Link href="/products/[id]"
|
|
122
|
+
* Link with a route pattern + segmentParams for interpolation.
|
|
123
|
+
* e.g. <Link href="/products/[id]" segmentParams={{ id: "123" }}>
|
|
124
|
+
* <Link href="/products/[id]" segmentParams={{ id: 123 }}>
|
|
79
125
|
*/
|
|
80
126
|
export interface LinkPropsWithParams extends LinkBaseProps {
|
|
81
127
|
/** Route pattern with dynamic segments (e.g. "/products/[id]") */
|
|
@@ -85,7 +131,7 @@ export interface LinkPropsWithParams extends LinkBaseProps {
|
|
|
85
131
|
* Single dynamic segments accept string | number (numbers are stringified).
|
|
86
132
|
* Catch-all segments accept string[].
|
|
87
133
|
*/
|
|
88
|
-
|
|
134
|
+
segmentParams: Record<string, string | number | string[]>;
|
|
89
135
|
/**
|
|
90
136
|
* Typed search params — serialized via the route's SearchParamsDefinition.
|
|
91
137
|
*/
|
|
@@ -303,22 +349,66 @@ function shouldInterceptClick(
|
|
|
303
349
|
* its own click handling.
|
|
304
350
|
*
|
|
305
351
|
* Supports typed routes via codegen overloads. At runtime:
|
|
306
|
-
* - `
|
|
352
|
+
* - `segmentParams` prop interpolates dynamic segments in the href pattern
|
|
307
353
|
* - `searchParams` prop serializes query parameters via a SearchParamsDefinition
|
|
308
354
|
*/
|
|
309
355
|
export function Link({
|
|
310
356
|
href,
|
|
311
357
|
prefetch,
|
|
312
358
|
scroll,
|
|
313
|
-
|
|
359
|
+
segmentParams,
|
|
314
360
|
searchParams,
|
|
361
|
+
preserveSearchParams,
|
|
315
362
|
onNavigate,
|
|
316
363
|
onClick: userOnClick,
|
|
317
364
|
onMouseEnter: userOnMouseEnter,
|
|
318
365
|
children,
|
|
319
366
|
...rest
|
|
320
367
|
}: LinkProps) {
|
|
321
|
-
const { href:
|
|
368
|
+
const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });
|
|
369
|
+
|
|
370
|
+
// ─── Per-link pending state (useState) ────────────────────────
|
|
371
|
+
// Each Link has its own pending state. Only the clicked link's
|
|
372
|
+
// setter is invoked during navigation — zero other links re-render.
|
|
373
|
+
//
|
|
374
|
+
// Eager show: click handler calls setLinkStatus(PENDING) directly (urgent).
|
|
375
|
+
// Atomic clear: TransitionRoot calls resetLinkPending(navId) inside
|
|
376
|
+
// startTransition — batched with the new tree commit.
|
|
377
|
+
//
|
|
378
|
+
// See design/19-client-navigation.md §"Per-Link Pending State"
|
|
379
|
+
const [linkStatus, setLinkStatus] = useState(IDLE_LINK_STATUS);
|
|
380
|
+
|
|
381
|
+
// Build the link instance ref for the pending store.
|
|
382
|
+
// The ref is stable across renders — we update the setter on each
|
|
383
|
+
// render to keep it current.
|
|
384
|
+
const linkInstanceRef = useRef<LinkPendingInstance | null>(null);
|
|
385
|
+
if (!linkInstanceRef.current) {
|
|
386
|
+
linkInstanceRef.current = { setLinkStatus };
|
|
387
|
+
} else {
|
|
388
|
+
linkInstanceRef.current.setLinkStatus = setLinkStatus;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Clean up if this link unmounts while it's the current navigation link.
|
|
392
|
+
// Prevents calling setOptimistic on an unmounted component.
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
const instance = linkInstanceRef.current;
|
|
395
|
+
return () => {
|
|
396
|
+
if (instance) {
|
|
397
|
+
unmountLinkForCurrentNavigation(instance);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}, []);
|
|
401
|
+
|
|
402
|
+
// Preserve search params from the current URL when requested.
|
|
403
|
+
// useSearchParams() works during both SSR (reads from request context)
|
|
404
|
+
// and on the client (reads from window.location, reactive to URL changes).
|
|
405
|
+
// We read current search params directly to avoid unconditional hook calls.
|
|
406
|
+
// On the client, window.location.search is always current; during SSR,
|
|
407
|
+
// getSsrData() provides the request's search params.
|
|
408
|
+
const resolvedHref = preserveSearchParams
|
|
409
|
+
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
410
|
+
: baseHref;
|
|
411
|
+
|
|
322
412
|
const internal = isInternalHref(resolvedHref);
|
|
323
413
|
|
|
324
414
|
// ─── Click handler ───────────────────────────────────────────
|
|
@@ -335,7 +425,11 @@ export function Link({
|
|
|
335
425
|
// Call onNavigate if provided — allows caller to cancel
|
|
336
426
|
if (onNavigate) {
|
|
337
427
|
let prevented = false;
|
|
338
|
-
onNavigate({
|
|
428
|
+
onNavigate({
|
|
429
|
+
preventDefault: () => {
|
|
430
|
+
prevented = true;
|
|
431
|
+
},
|
|
432
|
+
});
|
|
339
433
|
if (prevented) {
|
|
340
434
|
event.preventDefault();
|
|
341
435
|
return;
|
|
@@ -347,24 +441,45 @@ export function Link({
|
|
|
347
441
|
|
|
348
442
|
event.preventDefault();
|
|
349
443
|
const shouldScroll = scroll !== false;
|
|
350
|
-
|
|
444
|
+
|
|
445
|
+
// Re-merge preserved search params at click time to pick up any
|
|
446
|
+
// URL changes since render (e.g. from other navigations or pushState).
|
|
447
|
+
const navHref = preserveSearchParams
|
|
448
|
+
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
449
|
+
: resolvedHref;
|
|
450
|
+
|
|
451
|
+
// Eagerly show pending state on this link (urgent update, immediate).
|
|
452
|
+
// Only this Link re-renders — all other Links are unaffected.
|
|
453
|
+
setLinkStatus(PENDING_LINK_STATUS);
|
|
454
|
+
|
|
455
|
+
// Register this link in the pending store so TransitionRoot can
|
|
456
|
+
// reset it to IDLE inside startTransition (atomic with new tree).
|
|
457
|
+
// Also resets any previous pending link to IDLE.
|
|
458
|
+
setLinkForCurrentNavigation(linkInstanceRef.current);
|
|
459
|
+
|
|
460
|
+
void router.navigate(navHref, { scroll: shouldScroll });
|
|
351
461
|
}
|
|
352
462
|
: userOnClick; // External links — just pass through user's onClick
|
|
353
463
|
|
|
354
464
|
// ─── Hover prefetch ──────────────────────────────────────────
|
|
355
|
-
const handleMouseEnter =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
router
|
|
465
|
+
const handleMouseEnter =
|
|
466
|
+
internal && prefetch
|
|
467
|
+
? (event: ReactMouseEvent<HTMLAnchorElement>) => {
|
|
468
|
+
userOnMouseEnter?.(event);
|
|
469
|
+
const router = getRouterOrNull();
|
|
470
|
+
if (router) {
|
|
471
|
+
// Re-merge preserved search params at hover time for fresh prefetch URL
|
|
472
|
+
const prefetchHref = preserveSearchParams
|
|
473
|
+
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
474
|
+
: resolvedHref;
|
|
475
|
+
router.prefetch(prefetchHref);
|
|
476
|
+
}
|
|
361
477
|
}
|
|
362
|
-
|
|
363
|
-
: userOnMouseEnter;
|
|
478
|
+
: userOnMouseEnter;
|
|
364
479
|
|
|
365
480
|
return (
|
|
366
481
|
<a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>
|
|
367
|
-
<
|
|
482
|
+
<LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>
|
|
368
483
|
</a>
|
|
369
484
|
);
|
|
370
485
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Holds the current route params and pathname, updated atomically
|
|
7
7
|
* with the RSC tree on each navigation. This replaces the previous
|
|
8
|
-
* useSyncExternalStore approach for
|
|
8
|
+
* useSyncExternalStore approach for useSegmentParams() and usePathname(),
|
|
9
9
|
* which suffered from a timing gap: the new tree could commit before
|
|
10
10
|
* the external store re-renders fired, causing a frame where both
|
|
11
11
|
* old and new active states were visible simultaneously.
|
|
@@ -63,7 +63,7 @@ export interface NavigationState {
|
|
|
63
63
|
* variables) because the ESM bundler can duplicate this module across
|
|
64
64
|
* chunks. Module-level variables would create separate instances per
|
|
65
65
|
* chunk — the provider in TransitionRoot (index chunk) would use
|
|
66
|
-
* context A while the consumer in
|
|
66
|
+
* context A while the consumer in useNavigationPending (shared chunk)
|
|
67
67
|
* reads from context B. globalThis guarantees a single instance.
|
|
68
68
|
*
|
|
69
69
|
* See design/27-chunking-strategy.md §"Singleton Safety"
|
|
@@ -90,7 +90,7 @@ function getOrCreateContext(): React.Context<NavigationState | null> | undefined
|
|
|
90
90
|
/**
|
|
91
91
|
* Read the navigation context. Returns null during SSR (no provider)
|
|
92
92
|
* or in the RSC environment (no context available).
|
|
93
|
-
* Internal — used by
|
|
93
|
+
* Internal — used by useSegmentParams() and usePathname().
|
|
94
94
|
*/
|
|
95
95
|
export function useNavigationContext(): NavigationState | null {
|
|
96
96
|
const ctx = getOrCreateContext();
|
|
@@ -168,8 +168,9 @@ export function getNavigationState(): NavigationState {
|
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Separate context for the in-flight navigation URL. Provided by
|
|
171
|
-
* TransitionRoot (urgent useState), consumed by
|
|
172
|
-
* and
|
|
171
|
+
* TransitionRoot (urgent useState), consumed by useNavigationPending
|
|
172
|
+
* and TopLoader. Per-link pending state uses useOptimistic instead
|
|
173
|
+
* (see link-pending-store.ts).
|
|
173
174
|
*
|
|
174
175
|
* Uses globalThis via Symbol.for for the same reason as NavigationContext
|
|
175
176
|
* above — the bundler may duplicate this module across chunks, and module-
|
package/src/client/router.ts
CHANGED
|
@@ -6,13 +6,18 @@ import type { SegmentInfo } from './segment-cache';
|
|
|
6
6
|
import { HistoryStack } from './history';
|
|
7
7
|
import type { HeadElement } from './head';
|
|
8
8
|
import { setCurrentParams } from './use-params.js';
|
|
9
|
-
import { setNavigationState } from './navigation-context.js';
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from './
|
|
15
|
-
import {
|
|
10
|
+
setNavigationState,
|
|
11
|
+
getNavigationState,
|
|
12
|
+
type NavigationState,
|
|
13
|
+
} from './navigation-context.js';
|
|
14
|
+
import { SegmentElementCache, cacheSegmentElements, mergeSegmentTree } from './segment-merger.js';
|
|
15
|
+
import {
|
|
16
|
+
fetchRscPayload,
|
|
17
|
+
RedirectError,
|
|
18
|
+
ServerErrorResponse,
|
|
19
|
+
VersionSkewError,
|
|
20
|
+
} from './rsc-fetch.js';
|
|
16
21
|
import type { FetchResult } from './rsc-fetch.js';
|
|
17
22
|
|
|
18
23
|
// ─── Types ───────────────────────────────────────────────────────
|
|
@@ -35,8 +40,12 @@ export type RscDecoder = (fetchPromise: Promise<Response>) => unknown;
|
|
|
35
40
|
* Function that renders a decoded RSC element tree into the DOM.
|
|
36
41
|
* In production: reactRoot.render(element).
|
|
37
42
|
* In tests: a no-op or mock.
|
|
43
|
+
*
|
|
44
|
+
* Receives the current NavigationState explicitly — no temporal
|
|
45
|
+
* coupling with setNavigationState/getNavigationState. The renderer
|
|
46
|
+
* wraps the element in NavigationProvider with this state.
|
|
38
47
|
*/
|
|
39
|
-
export type RootRenderer = (element: unknown) => void;
|
|
48
|
+
export type RootRenderer = (element: unknown, navState: NavigationState) => void;
|
|
40
49
|
|
|
41
50
|
/**
|
|
42
51
|
* Platform dependencies injected for testability. In production these
|
|
@@ -68,13 +77,17 @@ export interface RouterDeps {
|
|
|
68
77
|
*
|
|
69
78
|
* The `perform` callback receives a `wrapPayload` function to wrap the
|
|
70
79
|
* decoded RSC payload with NavigationProvider + NuqsAdapter before
|
|
71
|
-
* TransitionRoot sets it as the new element.
|
|
80
|
+
* TransitionRoot sets it as the new element. The `wrapPayload` function
|
|
81
|
+
* receives the NavigationState explicitly — no temporal coupling with
|
|
82
|
+
* getNavigationState().
|
|
72
83
|
*
|
|
73
84
|
* If not provided (tests), the router falls back to renderRoot.
|
|
74
85
|
*/
|
|
75
86
|
navigateTransition?: (
|
|
76
87
|
pendingUrl: string,
|
|
77
|
-
perform: (
|
|
88
|
+
perform: (
|
|
89
|
+
wrapPayload: (payload: unknown, navState: NavigationState) => unknown
|
|
90
|
+
) => Promise<unknown>
|
|
78
91
|
) => Promise<void>;
|
|
79
92
|
}
|
|
80
93
|
|
|
@@ -134,21 +147,40 @@ function isAbortError(error: unknown): boolean {
|
|
|
134
147
|
* Create a router instance. In production, called once at app hydration
|
|
135
148
|
* with real browser APIs. In tests, called with mock dependencies.
|
|
136
149
|
*/
|
|
150
|
+
/**
|
|
151
|
+
* Router navigation phase — discriminated union replacing scattered
|
|
152
|
+
* `pending` + `pendingUrl` boolean flags.
|
|
153
|
+
*
|
|
154
|
+
* - `idle`: No navigation in flight. The committed params/pathname
|
|
155
|
+
* are current.
|
|
156
|
+
* - `navigating`: A fetch or render is in progress. `targetUrl` is
|
|
157
|
+
* the destination being navigated to.
|
|
158
|
+
*/
|
|
159
|
+
export type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };
|
|
160
|
+
|
|
137
161
|
export function createRouter(deps: RouterDeps): RouterInstance {
|
|
138
162
|
const segmentCache = new SegmentCache();
|
|
139
163
|
const prefetchCache = new PrefetchCache();
|
|
140
164
|
const historyStack = new HistoryStack();
|
|
141
165
|
const segmentElementCache = new SegmentElementCache();
|
|
142
166
|
|
|
143
|
-
let
|
|
144
|
-
let pendingUrl: string | null = null;
|
|
167
|
+
let routerPhase: RouterPhase = { phase: 'idle' };
|
|
145
168
|
const pendingListeners = new Set<(pending: boolean) => void>();
|
|
146
169
|
|
|
147
170
|
function setPending(value: boolean, url?: string): void {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
171
|
+
const next: RouterPhase =
|
|
172
|
+
value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };
|
|
173
|
+
// Skip no-op updates
|
|
174
|
+
if (
|
|
175
|
+
routerPhase.phase === next.phase &&
|
|
176
|
+
(routerPhase.phase === 'idle' ||
|
|
177
|
+
(routerPhase.phase === 'navigating' &&
|
|
178
|
+
next.phase === 'navigating' &&
|
|
179
|
+
routerPhase.targetUrl === next.targetUrl))
|
|
180
|
+
) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
routerPhase = next;
|
|
152
184
|
// Notify external store listeners (non-React consumers).
|
|
153
185
|
// React-facing pending state is handled by useOptimistic in
|
|
154
186
|
// TransitionRoot via navigateTransition — not this function.
|
|
@@ -167,9 +199,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
167
199
|
}
|
|
168
200
|
|
|
169
201
|
/** Render a decoded RSC payload into the DOM if a renderer is available. */
|
|
170
|
-
function renderPayload(payload: unknown): void {
|
|
202
|
+
function renderPayload(payload: unknown, navState: NavigationState): void {
|
|
171
203
|
if (deps.renderRoot) {
|
|
172
|
-
deps.renderRoot(payload);
|
|
204
|
+
deps.renderRoot(payload, navState);
|
|
173
205
|
}
|
|
174
206
|
}
|
|
175
207
|
|
|
@@ -198,32 +230,34 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
198
230
|
/**
|
|
199
231
|
* Update navigation state (params + pathname) for the next render.
|
|
200
232
|
*
|
|
201
|
-
* Sets
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
233
|
+
* Sets the module-level fallback (for tests and SSR) and the
|
|
234
|
+
* globalThis bridge, then returns the NavigationState so callers
|
|
235
|
+
* can pass it explicitly to renderRoot/wrapPayload — eliminating
|
|
236
|
+
* temporal coupling with getNavigationState().
|
|
205
237
|
*/
|
|
206
238
|
function updateNavigationState(
|
|
207
239
|
params: Record<string, string | string[]> | null | undefined,
|
|
208
240
|
url: string
|
|
209
|
-
):
|
|
241
|
+
): NavigationState {
|
|
210
242
|
const resolvedParams = params ?? {};
|
|
211
243
|
// Module-level fallback for tests (no NavigationProvider) and SSR
|
|
212
244
|
setCurrentParams(resolvedParams);
|
|
213
|
-
//
|
|
245
|
+
// globalThis bridge — kept for backward compat
|
|
214
246
|
const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0] || '/';
|
|
215
|
-
|
|
247
|
+
const navState: NavigationState = { params: resolvedParams, pathname };
|
|
248
|
+
setNavigationState(navState);
|
|
249
|
+
return navState;
|
|
216
250
|
}
|
|
217
251
|
|
|
218
252
|
/**
|
|
219
253
|
* Render a payload via navigateTransition (production) or renderRoot (tests).
|
|
220
|
-
* The perform callback should fetch data, update state, and return the
|
|
221
|
-
*
|
|
222
|
-
*
|
|
254
|
+
* The perform callback should fetch data, update state, and return the
|
|
255
|
+
* FetchResult plus the NavigationState (so it can be passed explicitly
|
|
256
|
+
* to wrapPayload/renderRoot without temporal coupling).
|
|
223
257
|
*/
|
|
224
258
|
async function renderViaTransition(
|
|
225
259
|
url: string,
|
|
226
|
-
perform: () => Promise<FetchResult>
|
|
260
|
+
perform: () => Promise<FetchResult & { navState: NavigationState }>
|
|
227
261
|
): Promise<HeadElement[] | null> {
|
|
228
262
|
if (deps.navigateTransition) {
|
|
229
263
|
let headElements: HeadElement[] | null = null;
|
|
@@ -239,7 +273,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
239
273
|
headElements: result.headElements,
|
|
240
274
|
params: result.params,
|
|
241
275
|
});
|
|
242
|
-
|
|
276
|
+
// Pass navState explicitly — wrapPayload wraps element in
|
|
277
|
+
// NavigationProvider with this state, no getNavigationState() needed.
|
|
278
|
+
return wrapPayload(merged, result.navState);
|
|
243
279
|
});
|
|
244
280
|
return headElements;
|
|
245
281
|
}
|
|
@@ -253,7 +289,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
253
289
|
headElements: result.headElements,
|
|
254
290
|
params: result.params,
|
|
255
291
|
});
|
|
256
|
-
renderPayload(merged);
|
|
292
|
+
renderPayload(merged, result.navState);
|
|
257
293
|
return result.headElements;
|
|
258
294
|
}
|
|
259
295
|
|
|
@@ -273,6 +309,17 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
273
309
|
}
|
|
274
310
|
}
|
|
275
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Schedule scroll restoration after the next paint and fire the
|
|
314
|
+
* scroll-restored event. Used by navigate, popstate, and refresh.
|
|
315
|
+
*/
|
|
316
|
+
function restoreScrollAfterPaint(scrollY: number): void {
|
|
317
|
+
afterPaint(() => {
|
|
318
|
+
deps.scrollTo(0, scrollY);
|
|
319
|
+
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
276
323
|
/**
|
|
277
324
|
* Core navigation logic shared between the transition and fallback paths.
|
|
278
325
|
* Fetches the RSC payload, updates all state, and returns the result.
|
|
@@ -280,7 +327,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
280
327
|
async function performNavigationFetch(
|
|
281
328
|
url: string,
|
|
282
329
|
options: { replace: boolean }
|
|
283
|
-
): Promise<FetchResult> {
|
|
330
|
+
): Promise<FetchResult & { navState: NavigationState }> {
|
|
284
331
|
// Check prefetch cache first. PrefetchResult has optional segmentInfo/params
|
|
285
332
|
// fields — normalize to null for FetchResult compatibility.
|
|
286
333
|
const prefetched = prefetchCache.consume(url);
|
|
@@ -320,10 +367,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
320
367
|
// Update the segment cache with the new route's segment tree.
|
|
321
368
|
updateSegmentCache(result.segmentInfo);
|
|
322
369
|
|
|
323
|
-
// Update navigation state
|
|
324
|
-
updateNavigationState(result.params, url);
|
|
370
|
+
// Update navigation state and capture it for explicit passing.
|
|
371
|
+
const navState = updateNavigationState(result.params, url);
|
|
325
372
|
|
|
326
|
-
return result;
|
|
373
|
+
return { ...result, navState };
|
|
327
374
|
}
|
|
328
375
|
|
|
329
376
|
async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {
|
|
@@ -354,21 +401,31 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
354
401
|
// Scroll-to-top on forward navigation, or restore captured position
|
|
355
402
|
// for scroll={false}. React's render() on the document root can reset
|
|
356
403
|
// scroll during DOM reconciliation, so all scroll must be actively managed.
|
|
357
|
-
|
|
358
|
-
if (scroll) {
|
|
359
|
-
deps.scrollTo(0, 0);
|
|
360
|
-
} else {
|
|
361
|
-
deps.scrollTo(0, currentScrollY);
|
|
362
|
-
}
|
|
363
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
364
|
-
});
|
|
404
|
+
restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
365
405
|
} catch (error) {
|
|
406
|
+
// Version skew — server has been redeployed. Trigger full page reload
|
|
407
|
+
// so the browser fetches the new bundle. See TIM-446.
|
|
408
|
+
if (error instanceof VersionSkewError) {
|
|
409
|
+
// Import triggerStaleReload dynamically to avoid circular deps
|
|
410
|
+
// and keep the reload logic centralized with its loop guard.
|
|
411
|
+
const { triggerStaleReload } = await import('./stale-reload.js');
|
|
412
|
+
triggerStaleReload();
|
|
413
|
+
// Return a never-resolving promise — page is reloading.
|
|
414
|
+
return new Promise(() => {}) as never;
|
|
415
|
+
}
|
|
366
416
|
// Server-side redirect during RSC fetch → soft router navigation.
|
|
367
417
|
if (error instanceof RedirectError) {
|
|
368
418
|
setPending(false);
|
|
369
419
|
await navigate(error.redirectUrl, { replace: true });
|
|
370
420
|
return;
|
|
371
421
|
}
|
|
422
|
+
// Server 5xx error — hard-navigate so the server renders the
|
|
423
|
+
// error page as HTML. See design/10-error-handling.md
|
|
424
|
+
// §"Error Page Rendering for Client Navigation".
|
|
425
|
+
if (error instanceof ServerErrorResponse) {
|
|
426
|
+
window.location.href = error.url;
|
|
427
|
+
return new Promise(() => {}) as never;
|
|
428
|
+
}
|
|
372
429
|
// Abort errors are not application errors — swallow silently.
|
|
373
430
|
if (isAbortError(error)) return;
|
|
374
431
|
throw error;
|
|
@@ -388,8 +445,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
388
445
|
const result = await fetchRscPayload(currentUrl, deps);
|
|
389
446
|
// History push handled by renderViaTransition (stores merged payload)
|
|
390
447
|
updateSegmentCache(result.segmentInfo);
|
|
391
|
-
updateNavigationState(result.params, currentUrl);
|
|
392
|
-
return result;
|
|
448
|
+
const navState = updateNavigationState(result.params, currentUrl);
|
|
449
|
+
return { ...result, navState };
|
|
393
450
|
});
|
|
394
451
|
|
|
395
452
|
applyHead(headElements);
|
|
@@ -406,13 +463,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
406
463
|
|
|
407
464
|
if (entry && entry.payload !== null) {
|
|
408
465
|
// Replay cached payload — no server roundtrip
|
|
409
|
-
updateNavigationState(entry.params, url);
|
|
410
|
-
renderPayload(entry.payload);
|
|
466
|
+
const navState = updateNavigationState(entry.params, url);
|
|
467
|
+
renderPayload(entry.payload, navState);
|
|
411
468
|
applyHead(entry.headElements);
|
|
412
|
-
|
|
413
|
-
deps.scrollTo(0, scrollY);
|
|
414
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
415
|
-
});
|
|
469
|
+
restoreScrollAfterPaint(scrollY);
|
|
416
470
|
} else {
|
|
417
471
|
// No cached payload — fetch from server.
|
|
418
472
|
// This happens when navigating back to the initial SSR'd page
|
|
@@ -421,19 +475,18 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
421
475
|
setPending(true, url);
|
|
422
476
|
try {
|
|
423
477
|
const headElements = await renderViaTransition(url, async () => {
|
|
424
|
-
const stateTree = segmentCache.serializeStateTree(
|
|
478
|
+
const stateTree = segmentCache.serializeStateTree(
|
|
479
|
+
segmentElementCache.getMergeablePaths()
|
|
480
|
+
);
|
|
425
481
|
const result = await fetchRscPayload(url, deps, stateTree);
|
|
426
482
|
updateSegmentCache(result.segmentInfo);
|
|
427
|
-
updateNavigationState(result.params, url);
|
|
483
|
+
const navState = updateNavigationState(result.params, url);
|
|
428
484
|
// History push handled by renderViaTransition (stores merged payload)
|
|
429
|
-
return result;
|
|
485
|
+
return { ...result, navState };
|
|
430
486
|
});
|
|
431
487
|
|
|
432
488
|
applyHead(headElements);
|
|
433
|
-
|
|
434
|
-
deps.scrollTo(0, scrollY);
|
|
435
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
436
|
-
});
|
|
489
|
+
restoreScrollAfterPaint(scrollY);
|
|
437
490
|
} finally {
|
|
438
491
|
setPending(false);
|
|
439
492
|
}
|
|
@@ -465,8 +518,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
465
518
|
navigate,
|
|
466
519
|
refresh,
|
|
467
520
|
handlePopState,
|
|
468
|
-
isPending: () =>
|
|
469
|
-
getPendingUrl: () =>
|
|
521
|
+
isPending: () => routerPhase.phase === 'navigating',
|
|
522
|
+
getPendingUrl: () => (routerPhase.phase === 'navigating' ? routerPhase.targetUrl : null),
|
|
470
523
|
onPendingChange(listener) {
|
|
471
524
|
pendingListeners.add(listener);
|
|
472
525
|
return () => pendingListeners.delete(listener);
|
|
@@ -483,7 +536,11 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
483
536
|
payload: merged,
|
|
484
537
|
headElements,
|
|
485
538
|
});
|
|
486
|
-
|
|
539
|
+
// Revalidation doesn't change params/pathname — preserve current state.
|
|
540
|
+
// DO NOT call updateNavigationState(null, ...) here: that normalizes
|
|
541
|
+
// params to {}, clearing dynamic route params on every action response.
|
|
542
|
+
const navState = getNavigationState();
|
|
543
|
+
renderPayload(merged, navState);
|
|
487
544
|
applyHead(headElements);
|
|
488
545
|
},
|
|
489
546
|
initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),
|