@timber-js/app 0.2.0-alpha.6 → 0.2.0-alpha.60
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-D5STJpIr.js +121 -0
- package/dist/_chunks/define-D5STJpIr.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-DtAavax4.js +93 -0
- package/dist/_chunks/define-cookie-DtAavax4.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-0wfZsnhh.js} +97 -69
- package/dist/_chunks/request-context-0wfZsnhh.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-DKN3aXxR.js +61 -0
- package/dist/_chunks/stale-reload-DKN3aXxR.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 +663 -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 +1975 -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 +139 -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 +88 -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 +56 -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 +320 -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 +274 -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 +10 -7
- 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 +139 -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 +277 -81
- package/src/server/rsc-entry/helpers.ts +134 -5
- package/src/server/rsc-entry/index.ts +165 -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 +141 -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 +209 -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/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),
|
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface FetchResult {
|
|
|
23
23
|
headElements: HeadElement[] | null;
|
|
24
24
|
/** Segment metadata from X-Timber-Segments header for populating the segment cache. */
|
|
25
25
|
segmentInfo: SegmentInfo[] | null;
|
|
26
|
-
/** Route params from X-Timber-Params header for populating
|
|
26
|
+
/** Route params from X-Timber-Params header for populating useSegmentParams(). */
|
|
27
27
|
params: Record<string, string | string[]> | null;
|
|
28
28
|
/** Segment paths that were skipped by the server (for client-side merging). */
|
|
29
29
|
skippedSegments: string[] | null;
|
|
@@ -58,6 +58,43 @@ function appendRscParam(url: string): string {
|
|
|
58
58
|
return `${url}${separator}_rsc=${generateCacheBustId()}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// ─── Deployment ID ───────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The client's deployment ID, set at bootstrap from the runtime config.
|
|
65
|
+
* Sent with every RSC/action request for version skew detection.
|
|
66
|
+
* Null in dev mode. See TIM-446.
|
|
67
|
+
*/
|
|
68
|
+
let clientDeploymentId: string | null = null;
|
|
69
|
+
|
|
70
|
+
/** Set the client deployment ID. Called once at bootstrap. */
|
|
71
|
+
export function setClientDeploymentId(id: string | null): void {
|
|
72
|
+
clientDeploymentId = id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Get the client deployment ID. */
|
|
76
|
+
export function getClientDeploymentId(): string | null {
|
|
77
|
+
return clientDeploymentId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Reload Signal ───────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** Header name used by the server to signal a version skew reload. */
|
|
83
|
+
export const RELOAD_HEADER = 'X-Timber-Reload';
|
|
84
|
+
|
|
85
|
+
/** Header name for the client's deployment ID. */
|
|
86
|
+
export const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a response signals a version skew reload.
|
|
90
|
+
* Triggers a full page reload if the server indicates the client is stale.
|
|
91
|
+
*/
|
|
92
|
+
export function checkReloadSignal(response: Response): boolean {
|
|
93
|
+
return response.headers.get(RELOAD_HEADER) === '1';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Header Builder ──────────────────────────────────────────────
|
|
97
|
+
|
|
61
98
|
export function buildRscHeaders(
|
|
62
99
|
stateTree: { segments: string[] } | undefined,
|
|
63
100
|
currentUrl?: string
|
|
@@ -75,6 +112,13 @@ export function buildRscHeaders(
|
|
|
75
112
|
if (currentUrl) {
|
|
76
113
|
headers['X-Timber-URL'] = currentUrl;
|
|
77
114
|
}
|
|
115
|
+
// Send deployment ID for version skew detection (TIM-446).
|
|
116
|
+
// The server compares this against the current build's ID.
|
|
117
|
+
// On mismatch, the server signals a reload instead of returning
|
|
118
|
+
// an RSC payload with mismatched module references.
|
|
119
|
+
if (clientDeploymentId) {
|
|
120
|
+
headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;
|
|
121
|
+
}
|
|
78
122
|
return headers;
|
|
79
123
|
}
|
|
80
124
|
|
|
@@ -135,7 +179,7 @@ export function extractSkippedSegments(response: Response): string[] | null {
|
|
|
135
179
|
* Extract route params from the X-Timber-Params response header.
|
|
136
180
|
* Returns null if the header is missing or malformed.
|
|
137
181
|
*
|
|
138
|
-
* Used to populate
|
|
182
|
+
* Used to populate useSegmentParams() after client-side navigation.
|
|
139
183
|
*/
|
|
140
184
|
export function extractParams(response: Response): Record<string, string | string[]> | null {
|
|
141
185
|
const header = response.headers.get('X-Timber-Params');
|
|
@@ -161,6 +205,35 @@ export class RedirectError extends Error {
|
|
|
161
205
|
}
|
|
162
206
|
}
|
|
163
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Thrown when the server signals a version skew (X-Timber-Reload header).
|
|
210
|
+
* Caught in navigate() to trigger a full page reload via triggerStaleReload().
|
|
211
|
+
* See TIM-446.
|
|
212
|
+
*/
|
|
213
|
+
export class VersionSkewError extends Error {
|
|
214
|
+
constructor() {
|
|
215
|
+
super('Version skew detected — server has been redeployed');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Thrown when the server returns a 5xx error for an RSC payload request.
|
|
221
|
+
* The server sends X-Timber-Error header and a JSON body instead of a
|
|
222
|
+
* broken RSC stream. Caught in navigate() to trigger a hard navigation
|
|
223
|
+
* so the server can render the error page as HTML.
|
|
224
|
+
*
|
|
225
|
+
* See design/10-error-handling.md §"Error Page Rendering for Client Navigation"
|
|
226
|
+
*/
|
|
227
|
+
export class ServerErrorResponse extends Error {
|
|
228
|
+
readonly status: number;
|
|
229
|
+
readonly url: string;
|
|
230
|
+
constructor(status: number, url: string) {
|
|
231
|
+
super(`Server error ${status} during navigation to ${url}`);
|
|
232
|
+
this.status = status;
|
|
233
|
+
this.url = url;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
164
237
|
// ─── Fetch ───────────────────────────────────────────────────────
|
|
165
238
|
|
|
166
239
|
/**
|
|
@@ -192,6 +265,12 @@ export async function fetchRscPayload(
|
|
|
192
265
|
let params: Record<string, string | string[]> | null = null;
|
|
193
266
|
let skippedSegments: string[] | null = null;
|
|
194
267
|
const wrappedPromise = fetchPromise.then((response) => {
|
|
268
|
+
// Version skew detection (TIM-446): if the server signals a reload,
|
|
269
|
+
// throw VersionSkewError so the caller (router navigate) can trigger
|
|
270
|
+
// a full page reload via triggerStaleReload().
|
|
271
|
+
if (checkReloadSignal(response)) {
|
|
272
|
+
throw new VersionSkewError();
|
|
273
|
+
}
|
|
195
274
|
// Detect server-side redirects. The server returns 204 + X-Timber-Redirect
|
|
196
275
|
// for RSC payload requests instead of a raw 302, because fetch with
|
|
197
276
|
// redirect: "manual" turns 302s into opaque redirects (status 0, null body)
|
|
@@ -202,6 +281,13 @@ export async function fetchRscPayload(
|
|
|
202
281
|
if (redirectLocation) {
|
|
203
282
|
throw new RedirectError(redirectLocation);
|
|
204
283
|
}
|
|
284
|
+
// Detect server 5xx errors. The server returns X-Timber-Error header
|
|
285
|
+
// with a JSON body instead of a broken RSC stream. Hard-navigate so
|
|
286
|
+
// the server renders the error page as HTML via the SSR-only path.
|
|
287
|
+
// See design/10-error-handling.md §"Error Page Rendering for Client Navigation"
|
|
288
|
+
if (response.headers.get('X-Timber-Error') === '1') {
|
|
289
|
+
throw new ServerErrorResponse(response.status, url);
|
|
290
|
+
}
|
|
205
291
|
headElements = extractHeadElements(response);
|
|
206
292
|
segmentInfo = extractSegmentInfo(response);
|
|
207
293
|
params = extractParams(response);
|
|
@@ -11,7 +11,7 @@ export interface PrefetchResult {
|
|
|
11
11
|
headElements: HeadElement[] | null;
|
|
12
12
|
/** Segment metadata from X-Timber-Segments header for populating the segment cache. */
|
|
13
13
|
segmentInfo?: SegmentInfo[] | null;
|
|
14
|
-
/** Route params from X-Timber-Params header for populating
|
|
14
|
+
/** Route params from X-Timber-Params header for populating useSegmentParams(). */
|
|
15
15
|
params?: Record<string, string | string[]> | null;
|
|
16
16
|
/** Segment paths skipped by the server (for client-side merging). */
|
|
17
17
|
skippedSegments?: string[] | null;
|
|
@@ -52,7 +52,12 @@ interface SegmentProviderProps {
|
|
|
52
52
|
* Wraps each layout to provide segment position context.
|
|
53
53
|
* Injected by rsc-entry.ts during element tree construction.
|
|
54
54
|
*/
|
|
55
|
-
export function SegmentProvider({
|
|
55
|
+
export function SegmentProvider({
|
|
56
|
+
segments,
|
|
57
|
+
segmentId: _segmentId,
|
|
58
|
+
parallelRouteKeys,
|
|
59
|
+
children,
|
|
60
|
+
}: SegmentProviderProps) {
|
|
56
61
|
const value = useMemo(
|
|
57
62
|
() => ({ segments, parallelRouteKeys }),
|
|
58
63
|
// segments and parallelRouteKeys are static per layout — they don't change
|
|
@@ -186,10 +186,7 @@ function walkChildren(children: ReactNode, out: CachedSegmentEntry[]): void {
|
|
|
186
186
|
* Cache all segment subtrees from a fully-rendered RSC element tree.
|
|
187
187
|
* Call this after every full RSC payload render (navigate, refresh, hydration).
|
|
188
188
|
*/
|
|
189
|
-
export function cacheSegmentElements(
|
|
190
|
-
element: unknown,
|
|
191
|
-
cache: SegmentElementCache
|
|
192
|
-
): void {
|
|
189
|
+
export function cacheSegmentElements(element: unknown, cache: SegmentElementCache): void {
|
|
193
190
|
const segments = extractSegments(element);
|
|
194
191
|
for (const entry of segments) {
|
|
195
192
|
cache.set(entry.segmentPath, entry);
|
|
@@ -208,10 +205,7 @@ export function cacheSegmentElements(
|
|
|
208
205
|
*/
|
|
209
206
|
type TreePath = Array<{ element: ReactElement; childIndex: number }>;
|
|
210
207
|
|
|
211
|
-
function findSegmentProviderPath(
|
|
212
|
-
node: ReactElement,
|
|
213
|
-
targetPath?: string
|
|
214
|
-
): TreePath | null {
|
|
208
|
+
function findSegmentProviderPath(node: ReactElement, targetPath?: string): TreePath | null {
|
|
215
209
|
const children = (node.props as { children?: ReactNode }).children;
|
|
216
210
|
if (children == null) return null;
|
|
217
211
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SegmentOutlet — client component boundary at each layout segment.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the post-hoc tree walking in segment-merger.ts with an explicit
|
|
5
|
+
* client component at each segment boundary. Each outlet:
|
|
6
|
+
*
|
|
7
|
+
* 1. Knows its own segment path (prop from the server)
|
|
8
|
+
* 2. Caches its children in a ref across navigations
|
|
9
|
+
* 3. When `keepCurrent` is true (partial navigation, this segment skipped),
|
|
10
|
+
* returns the previously cached children — layout state is preserved
|
|
11
|
+
* 4. When `keepCurrent` is false (full navigation or this segment changed),
|
|
12
|
+
* stores and renders the new children
|
|
13
|
+
*
|
|
14
|
+
* This eliminates the need for client-side element tree walking, which
|
|
15
|
+
* breaks on real RSC trees due to opaque client component lazy refs,
|
|
16
|
+
* Suspense thenables, and AccessGate wrappers.
|
|
17
|
+
*
|
|
18
|
+
* Architecture is similar to Next.js's `<LayoutRouter>` client component —
|
|
19
|
+
* each layout boundary is an explicit client component that manages its
|
|
20
|
+
* own subtree. See design/19-client-navigation.md.
|
|
21
|
+
*
|
|
22
|
+
* Security: This is a performance optimization only. The server always
|
|
23
|
+
* runs all access.ts files regardless of segment skipping. A fabricated
|
|
24
|
+
* keepCurrent prop can only cause stale layouts — never auth bypass.
|
|
25
|
+
* See design/13-security.md §"State tree manipulation".
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use client';
|
|
29
|
+
|
|
30
|
+
import { useRef, type ReactNode } from 'react';
|
|
31
|
+
|
|
32
|
+
export interface SegmentOutletProps {
|
|
33
|
+
/**
|
|
34
|
+
* Unique identifier for this segment. For normal segments this is the
|
|
35
|
+
* urlPath (e.g., "/", "/dashboard"). For route groups this includes the
|
|
36
|
+
* group name (e.g., "/(marketing)") to distinguish siblings that share
|
|
37
|
+
* the same urlPath. Must match the segmentId used in state-tree-diff.ts.
|
|
38
|
+
*/
|
|
39
|
+
segmentPath: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* When true, the outlet returns its previously cached children instead
|
|
43
|
+
* of rendering the new children prop. Set by the server when this
|
|
44
|
+
* segment was skipped (the client already has the layout mounted).
|
|
45
|
+
*
|
|
46
|
+
* On the first render (SSR/hydration), this is always false — there's
|
|
47
|
+
* no cached content yet. On subsequent partial navigations, the server
|
|
48
|
+
* sets this to true for segments it skipped rendering.
|
|
49
|
+
*/
|
|
50
|
+
keepCurrent?: boolean;
|
|
51
|
+
|
|
52
|
+
/** The segment's React subtree (layout + inner content). */
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Client component boundary at each layout segment in the element tree.
|
|
58
|
+
*
|
|
59
|
+
* On full navigation: receives new children, stores them, renders them.
|
|
60
|
+
* On partial navigation (keepCurrent=true): ignores children prop,
|
|
61
|
+
* returns previously stored content — React reconciles the same elements,
|
|
62
|
+
* preserving all client component state in the layout subtree.
|
|
63
|
+
*
|
|
64
|
+
* React preserves the ref across `reactRoot.render()` calls because:
|
|
65
|
+
* - SegmentOutlet has a stable type (client component module reference)
|
|
66
|
+
* - It appears at the same tree position on every navigation
|
|
67
|
+
* - React reconciles same-type, same-position → instance preserved
|
|
68
|
+
*/
|
|
69
|
+
export function SegmentOutlet({
|
|
70
|
+
segmentPath: _segmentPath,
|
|
71
|
+
keepCurrent = false,
|
|
72
|
+
children,
|
|
73
|
+
}: SegmentOutletProps) {
|
|
74
|
+
// Store content in a ref to avoid triggering re-renders on cache updates.
|
|
75
|
+
// The ref persists across reactRoot.render() calls because React reconciles
|
|
76
|
+
// the same component type at the same tree position.
|
|
77
|
+
const contentRef = useRef<ReactNode>(null);
|
|
78
|
+
|
|
79
|
+
if (!keepCurrent) {
|
|
80
|
+
// Full render or this segment was re-rendered — store and render new content
|
|
81
|
+
contentRef.current = children;
|
|
82
|
+
}
|
|
83
|
+
// else: keepCurrent=true — return previously cached content
|
|
84
|
+
|
|
85
|
+
return contentRef.current;
|
|
86
|
+
}
|