@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/actions-CQ8Z8VGL.js +1061 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js.map +1 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js +61 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js.map +1 -0
- package/dist/_chunks/{define-Itxvcd7F.js → define-B-Q_UMOD.js} +19 -23
- package/dist/_chunks/define-B-Q_UMOD.js.map +1 -0
- package/dist/_chunks/{define-C77ScO0m.js → define-CfBPoJb0.js} +24 -7
- package/dist/_chunks/define-CfBPoJb0.js.map +1 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js +194 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js.map +1 -0
- package/dist/_chunks/{format-CYBGxKtc.js → format-Bcn-Iv1x.js} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js.map → format-Bcn-Iv1x.js.map} +1 -1
- package/dist/_chunks/handler-store-B-lqaGyh.js +54 -0
- package/dist/_chunks/handler-store-B-lqaGyh.js.map +1 -0
- package/dist/_chunks/logger-0m8MsKdc.js +291 -0
- package/dist/_chunks/logger-0m8MsKdc.js.map +1 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js +122 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js.map +1 -0
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/navigation-root-BCYczjml.js +96 -0
- package/dist/_chunks/navigation-root-BCYczjml.js.map +1 -0
- package/dist/_chunks/registry-I2ss-lvy.js +20 -0
- package/dist/_chunks/registry-I2ss-lvy.js.map +1 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js +28 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-Cxu4l-7p.js} +1 -1
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-Cxu4l-7p.js.map} +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
- package/dist/_chunks/{segment-context-fHFLF1PE.js → segment-context-Dx_OizxD.js} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js.map → segment-context-Dx_OizxD.js.map} +1 -1
- package/dist/_chunks/{router-ref-C8OCm7g7.js → ssr-data-B4CdH7rE.js} +2 -26
- package/dist/_chunks/ssr-data-B4CdH7rE.js.map +1 -0
- package/dist/_chunks/{stale-reload-BX5gL1r-.js → stale-reload-Bab885FO.js} +1 -1
- package/dist/_chunks/{stale-reload-BX5gL1r-.js.map → stale-reload-Bab885FO.js.map} +1 -1
- package/dist/_chunks/tracing-C8V-YGsP.js +329 -0
- package/dist/_chunks/tracing-C8V-YGsP.js.map +1 -0
- package/dist/_chunks/{use-query-states-BiV5GJgm.js → use-query-states-B2XTqxDR.js} +3 -19
- package/dist/_chunks/use-query-states-B2XTqxDR.js.map +1 -0
- package/dist/_chunks/{use-params-IOPu7E8t.js → use-segment-params-BkpKAQ7D.js} +9 -95
- package/dist/_chunks/use-segment-params-BkpKAQ7D.js.map +1 -0
- package/dist/_chunks/{interception-BbqMCVXa.js → walkers-Tg0Alwcg.js} +66 -87
- package/dist/_chunks/walkers-Tg0Alwcg.js.map +1 -0
- package/dist/_chunks/{dev-warnings-DpGRGoDi.js → warnings-Cg47l5sk.js} +3 -3
- package/dist/_chunks/warnings-Cg47l5sk.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +28 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +8 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +63 -31
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +16 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/cache/index.js +9 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -1
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/form.d.ts +10 -24
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +41 -91
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +2 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +81 -7
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/state.d.ts +1 -1
- package/dist/client/use-cookie.d.ts +8 -0
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +1 -1
- package/dist/client/use-segment-params.d.ts.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -2
- package/dist/config-types.d.ts +28 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts +87 -35
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +2 -1
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +48 -2
- package/dist/cookies/index.js.map +1 -0
- package/dist/cookies/json-cookie.d.ts +64 -0
- package/dist/cookies/json-cookie.d.ts.map +1 -0
- package/dist/cookies/validation.d.ts +46 -0
- package/dist/cookies/validation.d.ts.map +1 -0
- package/dist/{plugins/dev-404-page.d.ts → dev-tools/404-page.d.ts} +9 -19
- package/dist/dev-tools/404-page.d.ts.map +1 -0
- package/dist/{plugins/dev-browser-logs.d.ts → dev-tools/browser-logs.d.ts} +1 -1
- package/dist/dev-tools/browser-logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-page.d.ts → dev-tools/error-page.d.ts} +2 -2
- package/dist/dev-tools/error-page.d.ts.map +1 -0
- package/dist/{server/dev-holding-server.d.ts → dev-tools/holding-server.d.ts} +5 -3
- package/dist/dev-tools/holding-server.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +31 -0
- package/dist/dev-tools/index.d.ts.map +1 -0
- package/dist/{server/dev-span-processor.d.ts → dev-tools/instrumentation.d.ts} +26 -6
- package/dist/dev-tools/instrumentation.d.ts.map +1 -0
- package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -0
- package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +3 -12
- package/dist/dev-tools/overlay.d.ts.map +1 -0
- package/dist/dev-tools/stack-classifier.d.ts +34 -0
- package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
- package/dist/{plugins/dev-terminal-error.d.ts → dev-tools/terminal.d.ts} +2 -2
- package/dist/dev-tools/terminal.d.ts.map +1 -0
- package/dist/{server/dev-warnings.d.ts → dev-tools/warnings.d.ts} +1 -1
- package/dist/dev-tools/warnings.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +285 -133
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +6 -4
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.d.ts +5 -3
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/scanner.d.ts +1 -10
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +37 -8
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +63 -23
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +51 -0
- package/dist/routing/walkers.d.ts.map +1 -0
- package/dist/search-params/define.d.ts +25 -7
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +5 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +23 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts +4 -3
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +15 -6
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +5 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/asset-headers.d.ts +1 -15
- package/dist/server/asset-headers.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +170 -0
- package/dist/server/cookie-context.d.ts.map +1 -0
- package/dist/server/cookie-parsing.d.ts +51 -0
- package/dist/server/cookie-parsing.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +90 -0
- package/dist/server/deny-boundary.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +212 -0
- package/dist/server/html-injector-core.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +59 -59
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -149
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +6 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +852 -852
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +14 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware-runner.d.ts +17 -0
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +46 -49
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +26 -0
- package/dist/server/param-coercion.d.ts.map +1 -0
- package/dist/server/pipeline-helpers.d.ts +95 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -0
- package/dist/server/pipeline-outcome.d.ts +49 -0
- package/dist/server/pipeline-outcome.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +52 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +51 -32
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +117 -0
- package/dist/server/port-resolution.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +22 -159
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +20 -47
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +1 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +50 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +119 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +16 -11
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +1 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +45 -16
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +48 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/escape-html.d.ts +14 -0
- package/dist/server/utils/escape-html.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +2 -2
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +3 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +9 -4
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/directive-parser.d.ts +0 -45
- package/dist/utils/directive-parser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +77 -0
- package/src/adapters/cloudflare.ts +10 -50
- package/src/adapters/nitro.ts +66 -50
- package/src/adapters/shared.ts +40 -0
- package/src/cache/timber-cache.ts +3 -2
- package/src/client/form.tsx +17 -25
- package/src/client/index.ts +16 -9
- package/src/client/internal.ts +3 -2
- package/src/client/router.ts +1 -1
- package/src/client/rsc-fetch.ts +15 -0
- package/src/client/state.ts +2 -2
- package/src/client/use-cookie.ts +29 -0
- package/src/codec.ts +3 -7
- package/src/config-types.ts +28 -0
- package/src/cookies/define-cookie.ts +271 -78
- package/src/cookies/index.ts +11 -8
- package/src/cookies/json-cookie.ts +105 -0
- package/src/cookies/validation.ts +134 -0
- package/src/{plugins/dev-404-page.ts → dev-tools/404-page.ts} +17 -48
- package/src/{plugins/dev-error-page.ts → dev-tools/error-page.ts} +5 -32
- package/src/{server/dev-holding-server.ts → dev-tools/holding-server.ts} +4 -2
- package/src/dev-tools/index.ts +90 -0
- package/src/dev-tools/instrumentation.ts +176 -0
- package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +2 -2
- package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +5 -23
- package/src/dev-tools/stack-classifier.ts +75 -0
- package/src/{plugins/dev-terminal-error.ts → dev-tools/terminal.ts} +4 -38
- package/src/{server/dev-warnings.ts → dev-tools/warnings.ts} +1 -1
- package/src/index.ts +95 -34
- package/src/plugin-context.ts +1 -1
- package/src/plugins/adapter-build.ts +3 -1
- package/src/plugins/build-report.ts +13 -22
- package/src/plugins/dev-server.ts +3 -3
- package/src/plugins/routing.ts +14 -12
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +1 -1
- package/src/routing/codegen.ts +1 -1
- package/src/routing/convention-lint.ts +9 -8
- package/src/routing/index.ts +5 -3
- package/src/routing/interception.ts +1 -1
- package/src/routing/scanner.ts +22 -95
- package/src/routing/segment-classify.ts +107 -8
- package/src/routing/status-file-lint.ts +7 -5
- package/src/routing/types.ts +63 -23
- package/src/routing/walkers.ts +90 -0
- package/src/search-params/define.ts +71 -15
- package/src/search-params/wrappers.ts +9 -2
- package/src/segment-params/define.ts +66 -13
- package/src/server/access-gate.tsx +9 -8
- package/src/server/action-handler.ts +34 -38
- package/src/server/als-registry.ts +5 -5
- package/src/server/asset-headers.ts +8 -34
- package/src/server/cookie-context.ts +468 -0
- package/src/server/cookie-parsing.ts +135 -0
- package/src/server/{deny-page-resolver.ts → deny-boundary.ts} +78 -14
- package/src/server/deny-renderer.ts +7 -12
- package/src/server/early-hints-sender.ts +3 -2
- package/src/server/fallback-error.ts +2 -2
- package/src/server/html-injector-core.ts +403 -0
- package/src/server/html-injectors.ts +158 -297
- package/src/server/index.ts +13 -14
- package/src/server/internal.ts +10 -3
- package/src/server/logger.ts +23 -0
- package/src/server/middleware-runner.ts +44 -0
- package/src/server/node-stream-transforms.ts +108 -248
- package/src/server/param-coercion.ts +76 -0
- package/src/server/pipeline-helpers.ts +204 -0
- package/src/server/pipeline-outcome.ts +167 -0
- package/src/server/pipeline-phases.ts +409 -0
- package/src/server/pipeline.ts +70 -540
- package/src/server/port-resolution.ts +215 -0
- package/src/server/request-context.ts +46 -451
- package/src/server/route-element-builder.ts +8 -4
- package/src/server/route-matcher.ts +28 -60
- package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
- package/src/server/rsc-entry/api-handler.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +2 -2
- package/src/server/rsc-entry/helpers.ts +2 -7
- package/src/server/rsc-entry/index.ts +81 -366
- package/src/server/rsc-entry/render-route.ts +304 -0
- package/src/server/rsc-entry/rsc-payload.ts +1 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/rsc-entry/wrap-action-dispatch.ts +449 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/state-tree-diff.ts +4 -1
- package/src/server/status-code-resolver.ts +112 -128
- package/src/server/tracing.ts +3 -3
- package/src/server/tree-builder.ts +134 -56
- package/src/server/types.ts +52 -0
- package/src/server/utils/escape-html.ts +20 -0
- package/src/shims/headers.ts +3 -3
- package/src/shims/navigation-client.ts +4 -3
- package/src/shims/navigation.ts +9 -7
- package/src/utils/directive-parser.ts +0 -392
- package/dist/_chunks/actions-DLnUaR65.js +0 -421
- package/dist/_chunks/actions-DLnUaR65.js.map +0 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js +0 -41
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +0 -1
- package/dist/_chunks/debug-ECi_61pb.js +0 -108
- package/dist/_chunks/debug-ECi_61pb.js.map +0 -1
- package/dist/_chunks/define-C77ScO0m.js.map +0 -1
- package/dist/_chunks/define-Itxvcd7F.js.map +0 -1
- package/dist/_chunks/define-cookie-BowvzoP0.js +0 -94
- package/dist/_chunks/define-cookie-BowvzoP0.js.map +0 -1
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +0 -1
- package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js +0 -41
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +0 -1
- package/dist/_chunks/request-context-CK5tZqIP.js +0 -478
- package/dist/_chunks/request-context-CK5tZqIP.js.map +0 -1
- package/dist/_chunks/router-ref-C8OCm7g7.js.map +0 -1
- package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
- package/dist/_chunks/tracing-CCYbKn5n.js +0 -238
- package/dist/_chunks/tracing-CCYbKn5n.js.map +0 -1
- package/dist/_chunks/use-params-IOPu7E8t.js.map +0 -1
- package/dist/_chunks/use-query-states-BiV5GJgm.js.map +0 -1
- package/dist/client/use-params.d.ts.map +0 -1
- package/dist/plugins/dev-404-page.d.ts.map +0 -1
- package/dist/plugins/dev-browser-logs.d.ts.map +0 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
- package/dist/plugins/dev-error-page.d.ts.map +0 -1
- package/dist/plugins/dev-logs.d.ts.map +0 -1
- package/dist/plugins/dev-terminal-error.d.ts.map +0 -1
- package/dist/server/deny-page-resolver.d.ts +0 -52
- package/dist/server/deny-page-resolver.d.ts.map +0 -1
- package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
- package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
- package/dist/server/dev-holding-server.d.ts.map +0 -1
- package/dist/server/dev-logger.d.ts.map +0 -1
- package/dist/server/dev-span-processor.d.ts.map +0 -1
- package/dist/server/dev-warnings.d.ts.map +0 -1
- package/dist/server/manifest-status-resolver.d.ts +0 -58
- package/dist/server/manifest-status-resolver.d.ts.map +0 -1
- package/dist/server/page-deny-boundary.d.ts +0 -31
- package/dist/server/page-deny-boundary.d.ts.map +0 -1
- package/src/server/dev-fetch-instrumentation.ts +0 -96
- package/src/server/dev-span-processor.ts +0 -78
- package/src/server/manifest-status-resolver.ts +0 -215
- package/src/server/page-deny-boundary.tsx +0 -56
- /package/src/client/{use-params.ts → use-segment-params.ts} +0 -0
- /package/src/{plugins/dev-browser-logs.ts → dev-tools/browser-logs.ts} +0 -0
- /package/src/{server/dev-logger.ts → dev-tools/logger.ts} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal.js","names":[],"sources":["../../src/client/segment-cache.ts","../../src/client/history.ts","../../src/client/segment-merger.ts","../../src/client/rsc-fetch.ts","../../src/client/router.ts"],"sourcesContent":["// Segment Cache — stores the mounted segment tree and prefetched payloads\n// See design/19-client-navigation.md for architecture details.\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** A prefetched RSC result with optional head elements and segment metadata. */\nexport interface PrefetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo?: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params?: Record<string, string | string[]> | null;\n /** Segment paths skipped by the server (for client-side merging). */\n skippedSegments?: string[] | null;\n}\n\n/**\n * A node in the client-side segment tree. Each node represents a mounted\n * layout or page segment with its RSC flight payload.\n */\nexport interface SegmentNode {\n /** The segment's URL pattern (e.g., \"/\", \"/dashboard\", \"/projects/[id]\") */\n segment: string;\n /** The RSC flight payload for this segment (opaque to the cache) */\n payload: unknown;\n /** Whether the segment is async (async layouts always re-render on navigation) */\n isAsync: boolean;\n /** Child segments keyed by segment path */\n children: Map<string, SegmentNode>;\n}\n\n/**\n * Serialized state tree sent via X-Timber-State-Tree header.\n * Only sync segments are included — async segments always re-render.\n */\nexport interface StateTree {\n segments: string[];\n}\n\n// ─── Segment Cache ───────────────────────────────────────────────\n\n/**\n * Maintains the client-side segment tree representing currently mounted\n * layouts and pages. Used for navigation reconciliation — the router diffs\n * new routes against this tree to determine which segments to re-fetch.\n */\nexport class SegmentCache {\n private root: SegmentNode | undefined;\n\n get(segment: string): SegmentNode | undefined {\n if (segment === '/' || segment === this.root?.segment) {\n return this.root;\n }\n return undefined;\n }\n\n set(segment: string, node: SegmentNode): void {\n if (segment === '/' || !this.root) {\n this.root = node;\n }\n }\n\n clear(): void {\n this.root = undefined;\n }\n\n /**\n * Serialize the mounted segment tree for the X-Timber-State-Tree header.\n * Only includes sync segments — async segments are excluded because the\n * server must always re-render them (they may depend on request context).\n *\n * When mergeableFilter is provided, only segments whose paths are in the\n * set are included. This ensures the server only skips segments that the\n * client can actually merge (i.e., segments whose cached element tree\n * contains an inner SegmentProvider the merger can splice into).\n *\n * This is a performance optimization only, NOT a security boundary.\n * The server always runs all access.ts files regardless of the state tree.\n */\n serializeStateTree(mergeableFilter?: Set<string>): StateTree {\n const segments: string[] = [];\n if (this.root) {\n collectSyncSegments(this.root, segments, mergeableFilter);\n }\n return { segments };\n }\n}\n\n/** Recursively collect sync segment paths from the tree */\nfunction collectSyncSegments(\n node: SegmentNode,\n out: string[],\n mergeableFilter?: Set<string>\n): void {\n if (!node.isAsync && (!mergeableFilter || mergeableFilter.has(node.segment))) {\n out.push(node.segment);\n }\n for (const child of node.children.values()) {\n collectSyncSegments(child, out, mergeableFilter);\n }\n}\n\n// ─── Segment Tree Builder ────────────────────────────────────────\n\n/**\n * Segment metadata from the server, sent via X-Timber-Segments header.\n * Describes a rendered segment's path and whether it's async.\n */\nexport interface SegmentInfo {\n path: string;\n isAsync: boolean;\n}\n\n/**\n * Build a SegmentNode tree from flat segment metadata.\n *\n * Takes an ordered list of segment descriptors (root → leaf) from the\n * server's X-Timber-Segments header and constructs the hierarchical\n * tree structure that SegmentCache expects.\n *\n * Each segment is nested as a child of the previous one, forming a\n * linear chain from root to leaf. The leaf segment (page) is excluded\n * from the tree — pages are never cached across navigations.\n */\nexport function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefined {\n // Need at least a root segment to build a tree\n if (segments.length === 0) return undefined;\n\n // Exclude the leaf (page) — pages always re-render on navigation.\n // Only layouts are cached in the segment tree.\n const layouts = segments.length > 1 ? segments.slice(0, -1) : segments;\n\n let root: SegmentNode | undefined;\n let parent: SegmentNode | undefined;\n\n for (const info of layouts) {\n const node: SegmentNode = {\n segment: info.path,\n payload: null,\n isAsync: info.isAsync,\n children: new Map(),\n };\n\n if (!root) {\n root = node;\n }\n\n if (parent) {\n parent.children.set(info.path, node);\n }\n\n parent = node;\n }\n\n return root;\n}\n\n// ─── Prefetch Cache ──────────────────────────────────────────────\n\ninterface PrefetchEntry {\n result: PrefetchResult;\n expiresAt: number;\n}\n\n/**\n * Short-lived cache for hover-triggered prefetches. Entries expire after\n * 30 seconds. When a link is clicked, the prefetched payload is consumed\n * (moved to the history stack) and removed from this cache.\n *\n * timber.js does NOT prefetch on viewport intersection — only explicit\n * hover on <Link prefetch> triggers a prefetch.\n */\nexport class PrefetchCache {\n private static readonly TTL_MS = 30_000;\n private entries = new Map<string, PrefetchEntry>();\n\n set(url: string, result: PrefetchResult): void {\n this.entries.set(url, {\n result,\n expiresAt: Date.now() + PrefetchCache.TTL_MS,\n });\n }\n\n get(url: string): PrefetchResult | undefined {\n const entry = this.entries.get(url);\n if (!entry) return undefined;\n if (Date.now() >= entry.expiresAt) {\n this.entries.delete(url);\n return undefined;\n }\n return entry.result;\n }\n\n /** Get and remove the entry (used when navigation consumes a prefetch) */\n consume(url: string): PrefetchResult | undefined {\n const result = this.get(url);\n if (result !== undefined) {\n this.entries.delete(url);\n }\n return result;\n }\n}\n","// History Stack — stores RSC payloads by URL for instant back/forward navigation\n// See design/19-client-navigation.md § History Stack\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface HistoryEntry {\n /** The complete segment tree payload at the time of navigation */\n payload: unknown;\n /** Resolved head elements for this page (title, meta tags). Null for SSR'd initial page. */\n headElements?: HeadElement[] | null;\n /** Route params for this page (for useParams). Null for SSR'd initial page. */\n params?: Record<string, string | string[]> | null;\n}\n\n// ─── History Stack ───────────────────────────────────────────────\n\n/**\n * Session-lived history stack keyed by URL. Enables instant back/forward\n * navigation without a server roundtrip.\n *\n * On forward navigation, the new page's payload is pushed onto the stack.\n * On popstate, the cached payload is replayed instantly.\n *\n * Supports two keying modes:\n * - **URL-keyed** (default): entries keyed by pathname + search.\n * Used with the History API fallback.\n * - **Entry-key + URL**: when the Navigation API is available,\n * entries can also be stored by Navigation entry key for\n * disambiguation of duplicate URLs in the history stack.\n * Falls back to URL lookup when entry key is not found.\n *\n * Scroll positions are stored in history.state or Navigation API entry\n * state, not in this stack — see design/19-client-navigation.md §Scroll Restoration.\n *\n * Entries persist for the session duration (no expiry) and are cleared\n * when the tab is closed — matching browser back-button behavior.\n */\nexport class HistoryStack {\n private entries = new Map<string, HistoryEntry>();\n /** Entries keyed by Navigation API entry key for duplicate URL disambiguation. */\n private entryKeyMap = new Map<string, HistoryEntry>();\n\n push(url: string, entry: HistoryEntry, entryKey?: string): void {\n this.entries.set(url, entry);\n if (entryKey) {\n this.entryKeyMap.set(entryKey, entry);\n }\n }\n\n /**\n * Get an entry. When an entry key is provided (Navigation API),\n * tries the entry-key map first for accurate disambiguation of\n * duplicate URLs, then falls back to URL lookup.\n */\n get(url: string, entryKey?: string): HistoryEntry | undefined {\n if (entryKey) {\n const byKey = this.entryKeyMap.get(entryKey);\n if (byKey) return byKey;\n }\n return this.entries.get(url);\n }\n\n has(url: string): boolean {\n return this.entries.has(url);\n }\n}\n","/**\n * Segment Merger — client-side tree merging for partial RSC payloads.\n *\n * When the server skips rendering sync layouts (because the client already\n * has them cached), the RSC payload is missing outer segment wrappers.\n * This module reconstructs the full element tree by splicing the partial\n * payload into cached segment subtrees.\n *\n * The approach:\n * 1. After each full RSC payload render, walk the decoded element tree\n * and cache each segment's subtree (identified by SegmentProvider boundaries)\n * 2. When a partial payload arrives, wrap it with cached segment elements\n * using React.cloneElement to preserve component identity\n *\n * React.cloneElement preserves the element's `type` — React sees the same\n * component at the same tree position and reconciles (preserving state)\n * rather than remounting. This is how layout state survives navigations.\n *\n * Design docs: 19-client-navigation.md §\"Navigation Reconciliation\"\n * Security: access.ts runs on the server regardless of skipping — this\n * is a performance optimization only. See 13-security.md.\n */\n\nimport { cloneElement, isValidElement, type ReactElement, type ReactNode } from 'react';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/**\n * A cached segment entry. Stores the full subtree rooted at a SegmentProvider\n * and the path through the tree to the next SegmentProvider (or leaf).\n */\nexport interface CachedSegmentEntry {\n /** The segment's URL path (e.g., \"/\", \"/dashboard\") */\n segmentPath: string;\n /** The SegmentProvider element for this segment */\n element: ReactElement;\n /**\n * Whether this segment's cached element contains a nested SegmentProvider.\n * Only segments with inner SegmentProviders are safe to skip — the merger\n * can only replace inner SegmentProviders, not pages embedded in layout output.\n * Used by the state tree serialization to exclude non-mergeable segments.\n */\n hasMergeableChild: boolean;\n}\n\n// ─── Segment Element Cache ───────────────────────────────────────\n\n/**\n * Cache of React element subtrees per segment path.\n * Updated after each navigation with the full decoded RSC element tree.\n */\nexport class SegmentElementCache {\n private entries = new Map<string, CachedSegmentEntry>();\n\n get(segmentPath: string): CachedSegmentEntry | undefined {\n return this.entries.get(segmentPath);\n }\n\n set(segmentPath: string, entry: CachedSegmentEntry): void {\n this.entries.set(segmentPath, entry);\n }\n\n has(segmentPath: string): boolean {\n return this.entries.has(segmentPath);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /**\n * Get the set of segment paths that are safe for the server to skip.\n * Only segments with an inner SegmentProvider (hasMergeableChild) are\n * included — the merger can only replace inner SegmentProviders, not\n * pages embedded in layout output. Used to filter the state tree.\n *\n * Returns an empty set if the element cache is empty (no elements\n * cached yet). This is the safe default — an empty set means no\n * segments pass the filter, so the state tree is empty and the server\n * does a full render. The element cache is populated lazily after the\n * first SPA navigation (RSC-decoded elements from hydration are\n * thenables that can't be walked until React resolves them).\n */\n getMergeablePaths(): Set<string> {\n const paths = new Set<string>();\n for (const [, entry] of this.entries) {\n if (entry.hasMergeableChild) {\n paths.add(entry.segmentPath);\n }\n }\n return paths;\n }\n}\n\n// ─── SegmentProvider Detection ───────────────────────────────────\n\n/**\n * Check if a React element is a SegmentProvider by looking for the\n * `segments` prop (an array of path segments). This is the only\n * component that receives this prop shape.\n */\nexport function isSegmentProvider(element: unknown): element is ReactElement {\n if (!isValidElement(element)) return false;\n const props = element.props as Record<string, unknown>;\n return Array.isArray(props.segments);\n}\n\n/**\n * Extract the segment path from a SegmentProvider element.\n *\n * Uses the `segmentId` prop if available (set by the server for route groups\n * to distinguish siblings that share the same urlPath). Falls back to\n * reconstructing from the `segments` array prop.\n */\nexport function getSegmentPath(element: ReactElement): string {\n const props = element.props as { segments: string[]; segmentId?: string };\n // segmentId is the authoritative key — includes group name for route groups\n if (props.segmentId) return props.segmentId;\n const filtered = props.segments.filter(Boolean);\n return filtered.length === 0 ? '/' : '/' + filtered.join('/');\n}\n\n// ─── Tree Walking ────────────────────────────────────────────────\n\n/**\n * Walk a React element tree and extract all SegmentProvider boundaries.\n * Returns an ordered list of segment entries from outermost to innermost.\n *\n * This only finds SegmentProviders along the main children path — it does\n * not descend into parallel routes/slots (those are separate subtrees).\n */\nexport function extractSegments(element: unknown): CachedSegmentEntry[] {\n const segments: CachedSegmentEntry[] = [];\n walkForSegments(element, segments);\n // Compute hasMergeableChild: a segment is mergeable if there's another\n // SegmentProvider nested below it. The segments list is ordered outermost\n // to innermost, so each segment's child is the next entry.\n for (let i = 0; i < segments.length; i++) {\n segments[i].hasMergeableChild = i < segments.length - 1;\n }\n return segments;\n}\n\nfunction walkForSegments(node: unknown, out: CachedSegmentEntry[]): void {\n if (!isValidElement(node)) return;\n\n // Use a local binding to avoid TypeScript narrowing issues with\n // isSegmentProvider's type predicate on the same variable.\n const el: ReactElement = node as ReactElement;\n const props = el.props as Record<string, unknown>;\n\n if (isSegmentProvider(node)) {\n out.push({\n segmentPath: getSegmentPath(el),\n element: el,\n hasMergeableChild: false, // computed after collection in extractSegments\n });\n // Continue walking into children to find nested segments\n walkChildren(props.children as ReactNode, out);\n return;\n }\n\n // Not a SegmentProvider — walk children looking for one\n walkChildren(props.children as ReactNode, out);\n}\n\nfunction walkChildren(children: ReactNode, out: CachedSegmentEntry[]): void {\n if (children == null) return;\n\n if (Array.isArray(children)) {\n for (const child of children) {\n walkForSegments(child, out);\n }\n } else {\n walkForSegments(children, out);\n }\n}\n\n// ─── Cache Population ────────────────────────────────────────────\n\n/**\n * Cache all segment subtrees from a fully-rendered RSC element tree.\n * Call this after every full RSC payload render (navigate, refresh, hydration).\n */\nexport function cacheSegmentElements(element: unknown, cache: SegmentElementCache): void {\n const segments = extractSegments(element);\n for (const entry of segments) {\n cache.set(entry.segmentPath, entry);\n }\n}\n\n// ─── Tree Merging ────────────────────────────────────────────────\n\n/**\n * Find a SegmentProvider nested in the children of a React element.\n * Returns the path of elements from the given element down to the\n * SegmentProvider, enabling reconstruction via cloneElement.\n *\n * The path is an array of [element, childIndex] pairs. childIndex is -1\n * for single-child (non-array) props.children.\n */\ntype TreePath = Array<{ element: ReactElement; childIndex: number }>;\n\nfunction findSegmentProviderPath(node: ReactElement, targetPath?: string): TreePath | null {\n const children = (node.props as { children?: ReactNode }).children;\n if (children == null) return null;\n\n if (Array.isArray(children)) {\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (!isValidElement(child)) continue;\n\n if (isSegmentProvider(child)) {\n if (!targetPath || getSegmentPath(child) === targetPath) {\n return [{ element: node, childIndex: i }];\n }\n }\n\n const deeper = findSegmentProviderPath(child, targetPath);\n if (deeper) {\n return [{ element: node, childIndex: i }, ...deeper];\n }\n }\n } else if (isValidElement(children)) {\n if (isSegmentProvider(children)) {\n if (!targetPath || getSegmentPath(children) === targetPath) {\n return [{ element: node, childIndex: -1 }];\n }\n }\n\n const deeper = findSegmentProviderPath(children, targetPath);\n if (deeper) {\n return [{ element: node, childIndex: -1 }, ...deeper];\n }\n }\n\n return null;\n}\n\n/**\n * Replace a nested SegmentProvider within a cached element tree with\n * new content. Uses cloneElement along the path to produce a new tree\n * with preserved component identity at every level except the replaced node.\n *\n * @param cachedElement The cached SegmentProvider element for this segment\n * @param newInnerContent The new React element to splice in at the inner segment position\n * @param innerSegmentPath The path of the inner segment to replace (optional — replaces first found)\n * @returns New element tree with the inner segment replaced\n */\nexport function replaceInnerSegment(\n cachedElement: ReactElement,\n newInnerContent: ReactNode,\n innerSegmentPath?: string\n): ReactElement {\n const path = findSegmentProviderPath(cachedElement, innerSegmentPath);\n\n if (!path || path.length === 0) {\n // No inner SegmentProvider found — this segment's cached element\n // wraps a page directly (no child layout with a SegmentProvider).\n // We CANNOT safely replace the page because it's embedded deep in\n // the layout's server-rendered output tree and we don't know its\n // position. Return the cached element unchanged as a safety fallback.\n //\n // The server should not skip segments without a child layout below\n // them (enforced by hasRenderedLayoutBelow in buildRouteElement).\n // If this codepath is reached, it indicates a server/client mismatch.\n return cachedElement;\n }\n\n // Reconstruct bottom-up: replace the innermost element first, then\n // clone each ancestor with the updated child.\n let replacement: ReactNode = newInnerContent;\n\n for (let i = path.length - 1; i >= 0; i--) {\n const { element, childIndex } = path[i];\n\n if (childIndex === -1) {\n // Single child — replace it\n replacement = cloneElement(element, {}, replacement);\n } else {\n // Array children — replace the specific index\n const children = (element.props as { children: ReactNode[] }).children;\n const newChildren = [...children];\n newChildren[childIndex] = replacement;\n replacement = cloneElement(element, {}, ...newChildren);\n }\n }\n\n return replacement as ReactElement;\n}\n\n/**\n * Merge a partial RSC payload with cached segment elements.\n *\n * When the server skips segments, the partial payload starts from the\n * first non-skipped segment. This function wraps it with cached elements\n * for the skipped segments, producing a full tree that React can\n * reconcile with the mounted tree (preserving layout state).\n *\n * @param partialPayload The RSC payload element (may be partial)\n * @param skippedSegments Ordered list of segment paths that were skipped (outermost first)\n * @param cache The segment element cache\n * @returns The merged full element tree, or the partial payload if merging isn't possible\n */\nexport function mergeSegmentTree(\n partialPayload: unknown,\n skippedSegments: string[],\n cache: SegmentElementCache\n): unknown {\n if (!isValidElement(partialPayload)) return partialPayload;\n if (skippedSegments.length === 0) return partialPayload;\n\n // Build from outermost to innermost: each skipped segment's cached\n // element wraps the next, with the partial payload at the center.\n let result: ReactNode = partialPayload;\n\n // Process from innermost skipped segment to outermost\n for (let i = skippedSegments.length - 1; i >= 0; i--) {\n const segmentPath = skippedSegments[i];\n const cached = cache.get(segmentPath);\n\n if (!cached) {\n // No cached element for this segment — can't merge.\n // This shouldn't happen (server only skips segments the client\n // has cached), but if it does, return the partial payload as-is.\n return partialPayload;\n }\n\n // Replace the inner content of the cached segment with our current result.\n // The inner content is either the next SegmentProvider or the page.\n result = replaceInnerSegment(cached.element, result);\n }\n\n return result;\n}\n","/**\n * RSC Fetch — handles fetching and parsing RSC Flight payloads.\n *\n * Extracted from router.ts to keep both files under the 500-line limit.\n * This module handles:\n * - Cache-busting URL generation for RSC requests\n * - Building RSC request headers (Accept, X-Timber-State-Tree)\n * - Extracting metadata from RSC response headers\n * - Fetching and decoding RSC payloads\n *\n * See design/19-client-navigation.md §\"RSC Payload Handling\"\n */\n\nimport type { SegmentInfo } from './segment-cache';\nimport type { HeadElement } from './head';\nimport type { RouterDeps } from './router';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** Result of fetching an RSC payload — includes head elements and segment metadata. */\nexport interface FetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params: Record<string, string | string[]> | null;\n /** Segment paths that were skipped by the server (for client-side merging). */\n skippedSegments: string[] | null;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nexport const RSC_CONTENT_TYPE = 'text/x-component';\n\n// ─── URL Helpers ─────────────────────────────────────────────────\n\n/**\n * Generate a short random cache-busting ID (5 chars, a-z0-9).\n * Matches the format Next.js uses for _rsc params.\n */\nfunction generateCacheBustId(): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let id = '';\n for (let i = 0; i < 5; i++) {\n id += chars[(Math.random() * 36) | 0];\n }\n return id;\n}\n\n/**\n * Append a `_rsc=<id>` query parameter to the URL.\n * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML\n * for RSC navigation requests and signals that this is an RSC fetch.\n */\nfunction appendRscParam(url: string): string {\n const separator = url.includes('?') ? '&' : '?';\n return `${url}${separator}_rsc=${generateCacheBustId()}`;\n}\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The client's deployment ID, set at bootstrap from the runtime config.\n * Sent with every RSC/action request for version skew detection.\n * Null in dev mode. See TIM-446.\n */\nlet clientDeploymentId: string | null = null;\n\n/** Set the client deployment ID. Called once at bootstrap. */\nexport function setClientDeploymentId(id: string | null): void {\n clientDeploymentId = id;\n}\n\n/** Get the client deployment ID. */\nexport function getClientDeploymentId(): string | null {\n return clientDeploymentId;\n}\n\n// ─── Reload Signal ───────────────────────────────────────────────\n\n/** Header name used by the server to signal a version skew reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n/** Header name for the client's deployment ID. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/**\n * Check if a response signals a version skew reload.\n * Triggers a full page reload if the server indicates the client is stale.\n */\nexport function checkReloadSignal(response: Response): boolean {\n return response.headers.get(RELOAD_HEADER) === '1';\n}\n\n// ─── Header Builder ──────────────────────────────────────────────\n\nexport function buildRscHeaders(\n stateTree: { segments: string[] } | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n // Send deployment ID for version skew detection (TIM-446).\n // The server compares this against the current build's ID.\n // On mismatch, the server signals a reload instead of returning\n // an RSC payload with mismatched module references.\n if (clientDeploymentId) {\n headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;\n }\n return headers;\n}\n\n// ─── Response Header Extraction ──────────────────────────────────\n\n/**\n * Extract head elements from the X-Timber-Head response header.\n * Returns null if the header is missing or malformed.\n */\nexport function extractHeadElements(response: Response): HeadElement[] | null {\n const header = response.headers.get('X-Timber-Head');\n if (!header) return null;\n try {\n return JSON.parse(decodeURIComponent(header));\n } catch {\n return null;\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isAsync} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nexport function extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n return null;\n }\n}\n\n/**\n * Extract skipped segment paths from the X-Timber-Skipped-Segments header.\n * Returns null if the header is missing or malformed.\n *\n * When the server skips sync layouts the client already has cached,\n * it sends this header listing the skipped segment paths (outermost first).\n * The client uses this to merge the partial payload with cached segments.\n */\nexport function extractSkippedSegments(response: Response): string[] | null {\n const header = response.headers.get('X-Timber-Skipped-Segments');\n if (!header) return null;\n try {\n const parsed = JSON.parse(header);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Extract route params from the X-Timber-Params response header.\n * Returns null if the header is missing or malformed.\n *\n * Used to populate useSegmentParams() after client-side navigation.\n */\nexport function extractParams(response: Response): Record<string, string | string[]> | null {\n const header = response.headers.get('X-Timber-Params');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n return null;\n }\n}\n\n// ─── Redirect Error ──────────────────────────────────────────────\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nexport class RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Thrown when the server signals a version skew (X-Timber-Reload header).\n * Caught in navigate() to trigger a full page reload via triggerStaleReload().\n * See TIM-446.\n */\nexport class VersionSkewError extends Error {\n constructor() {\n super('Version skew detected — server has been redeployed');\n }\n}\n\n/**\n * Thrown when the server returns an error for an RSC payload request.\n * The server sends X-Timber-Error header and a JSON body instead of a\n * broken RSC stream for any RenderError (4xx or 5xx). Caught in\n * navigate() to trigger a hard navigation so the server can render\n * the error page as HTML.\n *\n * See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n */\nexport class ServerErrorResponse extends Error {\n readonly status: number;\n readonly url: string;\n constructor(status: number, url: string) {\n super(`Server error ${status} during navigation to ${url}`);\n this.status = status;\n this.url = url;\n }\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n *\n * Also extracts head elements from the X-Timber-Head response header\n * so the client can update document.title and <meta> tags after navigation.\n */\nexport async function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string,\n signal?: AbortSignal\n): Promise<FetchResult> {\n const rscUrl = appendRscParam(url);\n const headers = buildRscHeaders(stateTree, currentUrl);\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read X-Timber-Head before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n let headElements: HeadElement[] | null = null;\n let segmentInfo: SegmentInfo[] | null = null;\n let params: Record<string, string | string[]> | null = null;\n let skippedSegments: string[] | null = null;\n const wrappedPromise = fetchPromise.then((response) => {\n // Version skew detection (TIM-446): if the server signals a reload,\n // throw VersionSkewError so the caller (router navigate) can trigger\n // a full page reload via triggerStaleReload().\n if (checkReloadSignal(response)) {\n throw new VersionSkewError();\n }\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n // Detect server error responses. The server returns X-Timber-Error header\n // with a JSON body instead of a broken RSC stream for any RenderError\n // (4xx or 5xx). Hard-navigate so the server renders the error page as HTML.\n // See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n if (response.headers.get('X-Timber-Error') === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n headElements = extractHeadElements(response);\n segmentInfo = extractSegmentInfo(response);\n params = extractParams(response);\n skippedSegments = extractSkippedSegments(response);\n return response;\n });\n // Await headers so headElements/segmentInfo/params are populated.\n await wrappedPromise;\n // Await the decoded payload — createFromFetch returns a thenable\n // that resolves to the React element tree once the Flight stream\n // has enough data to produce the shell.\n const payload = await deps.decodeRsc(wrappedPromise);\n return { payload, headElements, segmentInfo, params, skippedSegments };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n return {\n payload: await response.text(),\n headElements: extractHeadElements(response),\n segmentInfo: extractSegmentInfo(response),\n params: extractParams(response),\n skippedSegments: extractSkippedSegments(response),\n };\n}\n","// Segment Router — manages client-side navigation and RSC payload fetching\n// See design/19-client-navigation.md for the full architecture.\n\nimport { SegmentCache, PrefetchCache, buildSegmentTree } from './segment-cache';\nimport type { SegmentInfo } from './segment-cache';\nimport { HistoryStack } from './history';\nimport type { HeadElement } from './head';\nimport { setCurrentParams } from './use-params.js';\nimport {\n setNavigationState,\n getNavigationState,\n type NavigationState,\n} from './navigation-context.js';\nimport { SegmentElementCache, cacheSegmentElements, mergeSegmentTree } from './segment-merger.js';\nimport {\n fetchRscPayload,\n RedirectError,\n ServerErrorResponse,\n VersionSkewError,\n} from './rsc-fetch.js';\nimport { setHardNavigating } from './navigation-root.js';\nimport type { FetchResult } from './rsc-fetch.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface NavigationOptions {\n /** Set to false to prevent scroll-to-top on forward navigation */\n scroll?: boolean;\n /** Use replaceState instead of pushState (replaces current history entry) */\n replace?: boolean;\n /**\n * @internal AbortSignal from the Navigation API's NavigateEvent.\n * When provided, the signal is linked to the router's per-navigation\n * AbortController so in-flight RSC fetches are cancelled when a new\n * navigation starts.\n */\n _signal?: AbortSignal;\n /**\n * @internal Skip pushState/replaceState — the Navigation API has already\n * updated the URL via event.intercept(). Used for external navigations\n * intercepted by the navigate event handler.\n */\n _skipHistory?: boolean;\n}\n\n/**\n * Function that decodes an RSC Flight stream into a React element tree.\n * In production: createFromFetch from @vitejs/plugin-rsc/browser.\n * In tests: a mock that returns the raw payload.\n */\nexport type RscDecoder = (fetchPromise: Promise<Response>) => unknown;\n\n/**\n * Function that renders a decoded RSC element tree into the DOM.\n * In production: reactRoot.render(element).\n * In tests: a no-op or mock.\n *\n * Receives the current NavigationState explicitly — no temporal\n * coupling with setNavigationState/getNavigationState. The renderer\n * wraps the element in NavigationProvider with this state.\n */\nexport type RootRenderer = (element: unknown, navState: NavigationState) => void;\n\n/**\n * Platform dependencies injected for testability. In production these\n * map to browser APIs; in tests they're replaced with mocks.\n */\nexport interface RouterDeps {\n fetch: (url: string, init: RequestInit) => Promise<Response>;\n pushState: (data: unknown, unused: string, url: string) => void;\n replaceState: (data: unknown, unused: string, url: string) => void;\n scrollTo: (x: number, y: number) => void;\n getCurrentUrl: () => string;\n getScrollY: () => number;\n /** Decode RSC Flight stream into React elements. If not provided, raw response text is stored. */\n decodeRsc?: RscDecoder;\n /** Render decoded RSC tree into the DOM. If not provided, rendering is a no-op. */\n renderRoot?: RootRenderer;\n /**\n * Schedule a callback after the next paint. In the browser, this is\n * requestAnimationFrame + setTimeout(0) to run after React commits.\n * In tests, this runs the callback synchronously.\n */\n afterPaint?: (callback: () => void) => void;\n /** Apply resolved head elements (title, meta tags) to the DOM after navigation. */\n applyHead?: (elements: HeadElement[]) => void;\n /**\n * Run a navigation inside a React transition with optimistic pending URL.\n * The pending URL shows immediately (useOptimistic urgent update) and\n * reverts when the transition commits (atomic with the new tree).\n *\n * The `perform` callback receives a `wrapPayload` function to wrap the\n * decoded RSC payload with NavigationProvider + NuqsAdapter before\n * NavigationRoot sets it as the new element. The `wrapPayload` function\n * receives the NavigationState explicitly — no temporal coupling with\n * getNavigationState().\n *\n * If not provided (tests), the router falls back to renderRoot.\n */\n navigateTransition?: (\n pendingUrl: string,\n perform: (\n wrapPayload: (payload: unknown, navState: NavigationState) => unknown\n ) => Promise<unknown>\n ) => Promise<void>;\n\n /**\n * Whether the Navigation API is active and handling traversals.\n * When true, the popstate handler is a no-op — the Navigation API's\n * navigate event covers back/forward button presses.\n */\n navigationApiActive?: boolean;\n\n /**\n * Called around pushState/replaceState to set a flag that prevents\n * the Navigation API's navigate listener from double-handling\n * router-initiated navigations.\n */\n setRouterNavigating?: (value: boolean) => void;\n\n /**\n * Save scroll position via the Navigation API's per-entry state.\n * When provided, used instead of history.replaceState for scroll storage.\n */\n saveNavigationEntryScroll?: (scrollY: number) => void;\n\n /**\n * Signal that a router-initiated navigation has completed. Resolves the\n * deferred promise that ties the browser's native loading state to the\n * navigation lifecycle. Called in the finally block of navigate/refresh,\n * aligned with when the TopLoader's pendingUrl clears.\n */\n completeRouterNavigation?: () => void;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Fires the navigate event BEFORE committing the URL, allowing Chrome\n * to show its native loading indicator. Falls back to pushState when\n * unavailable.\n */\n navigationNavigate?: (url: string, replace: boolean) => void;\n}\n\nexport interface RouterInstance {\n /** Navigate to a new URL (forward navigation) */\n navigate(url: string, options?: NavigationOptions): Promise<void>;\n /** Full re-render of the current URL — no state tree sent */\n refresh(): Promise<void>;\n /** Handle a popstate event (back/forward button). scrollY is read from history.state. */\n handlePopState(url: string, scrollY?: number, externalSignal?: AbortSignal): Promise<void>;\n /** Whether a navigation is currently in flight */\n isPending(): boolean;\n /** The URL currently being navigated to, or null if idle */\n getPendingUrl(): string | null;\n /** Subscribe to pending state changes */\n onPendingChange(listener: (pending: boolean) => void): () => void;\n /** Prefetch an RSC payload for a URL (used by Link hover) */\n prefetch(url: string): void;\n /**\n * Apply a piggybacked revalidation payload from a server action response.\n * Renders the element tree and updates head elements without a server fetch.\n * See design/08-forms-and-actions.md §\"Single-Roundtrip Revalidation\".\n */\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void;\n /**\n * Populate the segment cache from server-provided segment metadata.\n * Called on initial hydration with segment info embedded in the HTML.\n */\n initSegmentCache(segments: SegmentInfo[]): void;\n /**\n * Cache segment elements from a decoded RSC element tree.\n * Called on initial hydration to populate the element cache so the\n * first client navigation can use partial payloads.\n */\n cacheElementTree(element: unknown): void;\n /** The segment cache (exposed for tests and <Link> prefetch) */\n segmentCache: SegmentCache;\n /** The prefetch cache (exposed for tests and <Link> prefetch) */\n prefetchCache: PrefetchCache;\n /** The history stack (exposed for tests) */\n historyStack: HistoryStack;\n}\n\n/**\n * Check if an error is an abort error (connection closed / fetch aborted).\n * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof DOMException && error.name === 'AbortError') return true;\n if (error instanceof Error && error.name === 'AbortError') return true;\n return false;\n}\n\n// ─── Router Factory ──────────────────────────────────────────────\n\n/**\n * Create a router instance. In production, called once at app hydration\n * with real browser APIs. In tests, called with mock dependencies.\n */\n/**\n * Router navigation phase — discriminated union replacing scattered\n * `pending` + `pendingUrl` boolean flags.\n *\n * - `idle`: No navigation in flight. The committed params/pathname\n * are current.\n * - `navigating`: A fetch or render is in progress. `targetUrl` is\n * the destination being navigated to.\n */\nexport type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };\n\nexport function createRouter(deps: RouterDeps): RouterInstance {\n const segmentCache = new SegmentCache();\n const prefetchCache = new PrefetchCache();\n const historyStack = new HistoryStack();\n const segmentElementCache = new SegmentElementCache();\n\n let routerPhase: RouterPhase = { phase: 'idle' };\n const pendingListeners = new Set<(pending: boolean) => void>();\n\n // AbortController for the current in-flight navigation.\n // When a new navigation starts, the previous controller is aborted,\n // cancelling any in-progress RSC fetch. This provides automatic\n // cancellation of stale fetches regardless of Navigation API support.\n let currentNavAbort: AbortController | null = null;\n\n /**\n * Create a new AbortController for a navigation, aborting any\n * previous in-flight navigation. Optionally links to an external\n * signal (e.g., from the Navigation API's NavigateEvent.signal).\n */\n function createNavAbort(externalSignal?: AbortSignal): AbortController {\n // Abort previous navigation's fetch\n currentNavAbort?.abort();\n const controller = new AbortController();\n currentNavAbort = controller;\n\n // If an external signal is provided (e.g., Navigation API),\n // forward its abort to our controller.\n if (externalSignal) {\n if (externalSignal.aborted) {\n controller.abort();\n } else {\n externalSignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return controller;\n }\n\n function setPending(value: boolean, url?: string): void {\n const next: RouterPhase =\n value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };\n // Skip no-op updates\n if (\n routerPhase.phase === next.phase &&\n (routerPhase.phase === 'idle' ||\n (routerPhase.phase === 'navigating' &&\n next.phase === 'navigating' &&\n routerPhase.targetUrl === next.targetUrl))\n ) {\n return;\n }\n routerPhase = next;\n // Notify external store listeners (non-React consumers).\n // React-facing pending state is handled by useOptimistic in\n // NavigationRoot via navigateTransition — not this function.\n for (const listener of pendingListeners) {\n listener(value);\n }\n }\n\n /** Update the segment cache from server-provided segment metadata. */\n function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {\n if (!segmentInfo || segmentInfo.length === 0) return;\n const tree = buildSegmentTree(segmentInfo);\n if (tree) {\n segmentCache.set('/', tree);\n }\n }\n\n /** Render a decoded RSC payload into the DOM if a renderer is available. */\n function renderPayload(payload: unknown, navState: NavigationState): void {\n if (deps.renderRoot) {\n deps.renderRoot(payload, navState);\n }\n }\n\n /**\n * Merge a partial RSC payload with cached segment elements if segments\n * were skipped, then cache segments from the (merged) payload.\n * Returns the merged payload ready for rendering.\n */\n function mergeAndCachePayload(\n payload: unknown,\n skippedSegments: string[] | null | undefined\n ): unknown {\n let merged = payload;\n\n // If segments were skipped, merge the partial payload with cached segments\n if (skippedSegments && skippedSegments.length > 0) {\n merged = mergeSegmentTree(payload, skippedSegments, segmentElementCache);\n }\n\n // Cache segment elements from the (merged) payload for future merges\n cacheSegmentElements(merged, segmentElementCache);\n\n return merged;\n }\n\n /**\n * Update navigation state (params + pathname) for the next render.\n *\n * Sets the module-level fallback (for tests and SSR) and the\n * globalThis bridge, then returns the NavigationState so callers\n * can pass it explicitly to renderRoot/wrapPayload — eliminating\n * temporal coupling with getNavigationState().\n */\n function updateNavigationState(\n params: Record<string, string | string[]> | null | undefined,\n url: string\n ): NavigationState {\n const resolvedParams = params ?? {};\n // Module-level fallback for tests (no NavigationProvider) and SSR\n setCurrentParams(resolvedParams);\n // globalThis bridge — kept for backward compat\n const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0] || '/';\n const navState: NavigationState = { params: resolvedParams, pathname };\n setNavigationState(navState);\n return navState;\n }\n\n /**\n * Render a payload via navigateTransition (production) or renderRoot (tests).\n * The perform callback should fetch data, update state, and return the\n * FetchResult plus the NavigationState (so it can be passed explicitly\n * to wrapPayload/renderRoot without temporal coupling).\n */\n async function renderViaTransition(\n url: string,\n perform: () => Promise<FetchResult & { navState: NavigationState }>\n ): Promise<HeadElement[] | null> {\n if (deps.navigateTransition) {\n let headElements: HeadElement[] | null = null;\n await deps.navigateTransition(url, async (wrapPayload) => {\n const result = await perform();\n headElements = result.headElements;\n // Merge partial payload with cached segments before wrapping\n const merged = mergeAndCachePayload(result.payload, result.skippedSegments);\n // Store the MERGED payload in history — not the partial pre-merge tree.\n // This ensures handlePopState replays the complete tree on back/forward.\n historyStack.push(url, {\n payload: merged,\n headElements: result.headElements,\n params: result.params,\n });\n // Pass navState explicitly — wrapPayload wraps element in\n // NavigationProvider with this state, no getNavigationState() needed.\n return wrapPayload(merged, result.navState);\n });\n return headElements;\n }\n // Fallback: no transition (tests, no React tree)\n const result = await perform();\n // Merge partial payload with cached segments before rendering\n const merged = mergeAndCachePayload(result.payload, result.skippedSegments);\n // Store merged payload in history\n historyStack.push(url, {\n payload: merged,\n headElements: result.headElements,\n params: result.params,\n });\n renderPayload(merged, result.navState);\n return result.headElements;\n }\n\n /** Apply head elements (title, meta tags) to the DOM if available. */\n function applyHead(elements: HeadElement[] | null | undefined): void {\n if (elements && deps.applyHead) {\n deps.applyHead(elements);\n }\n }\n\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n /**\n * Schedule scroll restoration after the next paint and fire the\n * scroll-restored event. Used by navigate, popstate, and refresh.\n */\n function restoreScrollAfterPaint(scrollY: number): void {\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n /**\n * Core navigation logic shared between the transition and fallback paths.\n * Fetches the RSC payload, updates all state, and returns the result.\n */\n async function performNavigationFetch(\n url: string,\n options: { replace: boolean; signal?: AbortSignal; skipHistory?: boolean }\n ): Promise<FetchResult & { navState: NavigationState }> {\n // Check prefetch cache first. PrefetchResult has optional segmentInfo/params\n // fields — normalize to null for FetchResult compatibility.\n const prefetched = prefetchCache.consume(url);\n let result: FetchResult | undefined = prefetched\n ? {\n payload: prefetched.payload,\n headElements: prefetched.headElements,\n segmentInfo: prefetched.segmentInfo ?? null,\n params: prefetched.params ?? null,\n skippedSegments: prefetched.skippedSegments ?? null,\n }\n : undefined;\n\n if (result === undefined) {\n // Fetch RSC payload with state tree for partial rendering.\n // Send current URL for intercepting route resolution (modal pattern).\n const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());\n const rawCurrentUrl = deps.getCurrentUrl();\n const currentUrl = rawCurrentUrl.startsWith('http')\n ? new URL(rawCurrentUrl).pathname\n : new URL(rawCurrentUrl, 'http://localhost').pathname;\n result = await fetchRscPayload(url, deps, stateTree, currentUrl, options.signal);\n }\n\n // Update the browser history — skip when the Navigation API has already\n // updated the URL via event.intercept() (external navigations).\n if (!options.skipHistory) {\n // Set the router-navigating flag so the Navigation API's navigate\n // listener doesn't double-intercept this pushState/replaceState.\n deps.setRouterNavigating?.(true);\n if (options.replace) {\n deps.replaceState({ timber: true, scrollY: 0 }, '', url);\n } else {\n deps.pushState({ timber: true, scrollY: 0 }, '', url);\n }\n deps.setRouterNavigating?.(false);\n }\n\n // NOTE: History push is deferred — the merged payload (after segment\n // merging in renderViaTransition) is stored by the caller, not here.\n // Storing result.payload here would record the partial (pre-merge)\n // RSC tree, causing handlePopState to replay an incomplete tree.\n\n // Update the segment cache with the new route's segment tree.\n updateSegmentCache(result.segmentInfo);\n\n // Update navigation state and capture it for explicit passing.\n const navState = updateNavigationState(result.params, url);\n\n return { ...result, navState };\n }\n\n async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {\n const scroll = options.scroll !== false;\n const replace = options.replace === true;\n const externalSignal = options._signal as AbortSignal | undefined;\n const skipHistory = options._skipHistory === true;\n\n // Create an abort controller for this navigation. Links to the external\n // signal (Navigation API's event.signal) when provided.\n const navAbort = createNavAbort(externalSignal);\n\n // Capture the departing page's scroll position for scroll={false} preservation.\n const currentScrollY = deps.getScrollY();\n\n // Save the departing page's scroll position — use Navigation API entry\n // state when available, otherwise fall back to history.state.\n if (deps.saveNavigationEntryScroll) {\n deps.saveNavigationEntryScroll(currentScrollY);\n } else {\n deps.replaceState({ timber: true, scrollY: currentScrollY }, '', deps.getCurrentUrl());\n }\n\n // When Navigation API is active, initiate the navigation via\n // navigation.navigate() BEFORE the fetch. Unlike history.pushState()\n // which commits the URL synchronously (so Chrome sees it as \"done\"),\n // navigation.navigate() fires the navigate event before committing.\n // Our handler intercepts with a deferred promise, and Chrome shows\n // its native loading indicator until completeRouterNavigation()\n // resolves it in the finally block (same time as TopLoader clears).\n let effectiveSkipHistory = skipHistory;\n if (!skipHistory && deps.navigationNavigate) {\n deps.setRouterNavigating?.(true);\n deps.navigationNavigate(url, replace);\n deps.setRouterNavigating?.(false);\n effectiveSkipHistory = true;\n }\n\n setPending(true, url);\n\n try {\n const headElements = await renderViaTransition(url, () =>\n performNavigationFetch(url, {\n replace,\n signal: navAbort.signal,\n skipHistory: effectiveSkipHistory,\n })\n );\n\n // Update document.title and <meta> tags with the new page's metadata\n applyHead(headElements);\n\n // Notify nuqs adapter (and any other listeners) that navigation completed.\n window.dispatchEvent(new Event('timber:navigation-end'));\n\n // Scroll-to-top on forward navigation, or restore captured position\n // for scroll={false}. React's render() on the document root can reset\n // scroll during DOM reconciliation, so all scroll must be actively managed.\n restoreScrollAfterPaint(scroll ? 0 : currentScrollY);\n } catch (error) {\n // Version skew — server has been redeployed. Trigger full page reload\n // so the browser fetches the new bundle. See TIM-446.\n // Set hard-navigating flag to prevent Navigation API interception\n // and React from rendering during page teardown. See TIM-626.\n if (error instanceof VersionSkewError) {\n setHardNavigating(true);\n // Import triggerStaleReload dynamically to avoid circular deps\n // and keep the reload logic centralized with its loop guard.\n const { triggerStaleReload } = await import('./stale-reload.js');\n triggerStaleReload();\n // Return a never-resolving promise — page is reloading.\n return new Promise(() => {}) as never;\n }\n // Server-side redirect during RSC fetch → soft router navigation.\n // The redirect navigate will push/replace its own URL.\n if (error instanceof RedirectError) {\n setPending(false);\n deps.completeRouterNavigation?.();\n await navigate(error.redirectUrl, { replace: true });\n return;\n }\n // Server 5xx error — hard-navigate so the server renders the\n // error page as HTML. See design/10-error-handling.md\n // §\"Error Page Rendering for Client Navigation\".\n //\n // Set hard-navigating flag BEFORE setting window.location.href:\n // 1. Prevents Navigation API from intercepting → infinite loop\n // 2. Causes NavigationRoot to throw unresolvedThenable → prevents\n // React from rendering children during page teardown (avoids\n // \"Rendered more hooks\" crashes). See TIM-626.\n if (error instanceof ServerErrorResponse) {\n setHardNavigating(true);\n window.location.href = error.url;\n return new Promise(() => {}) as never;\n }\n // Abort errors are not application errors — swallow silently.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n // Clear the abort controller so we don't abort a completed navigation\n // when the next one starts. In dev mode, the RSC body stream stays\n // open after data arrives (React's Flight client waits for debug rows).\n // Aborting a \"completed\" navigation kills the open stream reader →\n // \"BodyStreamBuffer was aborted\". By clearing the controller here,\n // createNavAbort() becomes a no-op for completed navigations.\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n }\n setPending(false);\n // Resolve the Navigation API deferred — clears the browser's native\n // loading state (tab spinner) at the same time as the TopLoader.\n deps.completeRouterNavigation?.();\n }\n }\n\n async function refresh(): Promise<void> {\n const currentUrl = deps.getCurrentUrl();\n const navAbort = createNavAbort();\n\n setPending(true, currentUrl);\n\n try {\n const headElements = await renderViaTransition(currentUrl, async () => {\n // No state tree sent — server renders the complete RSC payload\n const result = await fetchRscPayload(\n currentUrl,\n deps,\n undefined,\n undefined,\n navAbort.signal\n );\n // History push handled by renderViaTransition (stores merged payload)\n updateSegmentCache(result.segmentInfo);\n const navState = updateNavigationState(result.params, currentUrl);\n return { ...result, navState };\n });\n\n applyHead(headElements);\n } catch (error) {\n // Stale transition (superseded by a newer navigation) or aborted\n // fetch — silently ignore. See TIM-629.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n }\n setPending(false);\n deps.completeRouterNavigation?.();\n }\n }\n\n async function handlePopState(\n url: string,\n scrollY: number = 0,\n externalSignal?: AbortSignal\n ): Promise<void> {\n // Scroll position is read from history.state by the caller (browser-entry.ts)\n // and passed in. This is more reliable than tracking scroll per-URL in memory\n // because the browser maintains per-entry state even with duplicate URLs.\n const entry = historyStack.get(url);\n\n if (entry && entry.payload !== null) {\n // Replay cached payload — no server roundtrip\n const navState = updateNavigationState(entry.params, url);\n renderPayload(entry.payload, navState);\n applyHead(entry.headElements);\n restoreScrollAfterPaint(scrollY);\n } else {\n // No cached payload — fetch from server.\n // This happens when navigating back to the initial SSR'd page\n // (its payload is null since it was rendered via SSR, not RSC fetch)\n // or when the entry doesn't exist at all.\n const navAbort = createNavAbort(externalSignal);\n setPending(true, url);\n try {\n const headElements = await renderViaTransition(url, async () => {\n const stateTree = segmentCache.serializeStateTree(\n segmentElementCache.getMergeablePaths()\n );\n const result = await fetchRscPayload(url, deps, stateTree, undefined, navAbort.signal);\n updateSegmentCache(result.segmentInfo);\n const navState = updateNavigationState(result.params, url);\n // History push handled by renderViaTransition (stores merged payload)\n return { ...result, navState };\n });\n\n applyHead(headElements);\n restoreScrollAfterPaint(scrollY);\n } catch (error) {\n // Stale transition (superseded by a newer navigation) or aborted\n // fetch — silently ignore. See TIM-629.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n }\n setPending(false);\n }\n }\n }\n\n /**\n * Prefetch an RSC payload for a URL and store it in the prefetch cache.\n * Called on hover of <Link prefetch> elements.\n */\n function prefetch(url: string): void {\n // Don't prefetch if already cached\n if (prefetchCache.get(url) !== undefined) return;\n if (historyStack.has(url)) return;\n\n // Fire-and-forget fetch\n const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());\n void fetchRscPayload(url, deps, stateTree).then(\n (result) => {\n prefetchCache.set(url, result);\n },\n () => {\n // Prefetch failure is non-fatal — navigation will fetch fresh\n }\n );\n }\n\n return {\n navigate,\n refresh,\n handlePopState,\n isPending: () => routerPhase.phase === 'navigating',\n getPendingUrl: () => (routerPhase.phase === 'navigating' ? routerPhase.targetUrl : null),\n onPendingChange(listener) {\n pendingListeners.add(listener);\n return () => pendingListeners.delete(listener);\n },\n prefetch,\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void {\n // Render the piggybacked element tree from a server action response.\n // Updates the current history entry with the fresh payload and applies\n // head elements — same as refresh() but without a server fetch.\n // Cache segment elements for future partial merges.\n const currentUrl = deps.getCurrentUrl();\n const merged = mergeAndCachePayload(element, null);\n historyStack.push(currentUrl, {\n payload: merged,\n headElements,\n });\n // Revalidation doesn't change params/pathname — preserve current state.\n // DO NOT call updateNavigationState(null, ...) here: that normalizes\n // params to {}, clearing dynamic route params on every action response.\n const navState = getNavigationState();\n renderPayload(merged, navState);\n applyHead(headElements);\n },\n initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),\n cacheElementTree: (element: unknown) => cacheSegmentElements(element, segmentElementCache),\n segmentCache,\n prefetchCache,\n historyStack,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"internal.js","names":[],"sources":["../../src/client/segment-cache.ts","../../src/client/history.ts","../../src/client/segment-merger.ts","../../src/client/rsc-fetch.ts","../../src/client/router.ts","../../src/client/use-search-params.ts"],"sourcesContent":["// Segment Cache — stores the mounted segment tree and prefetched payloads\n// See design/19-client-navigation.md for architecture details.\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** A prefetched RSC result with optional head elements and segment metadata. */\nexport interface PrefetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo?: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params?: Record<string, string | string[]> | null;\n /** Segment paths skipped by the server (for client-side merging). */\n skippedSegments?: string[] | null;\n}\n\n/**\n * A node in the client-side segment tree. Each node represents a mounted\n * layout or page segment with its RSC flight payload.\n */\nexport interface SegmentNode {\n /** The segment's URL pattern (e.g., \"/\", \"/dashboard\", \"/projects/[id]\") */\n segment: string;\n /** The RSC flight payload for this segment (opaque to the cache) */\n payload: unknown;\n /** Whether the segment is async (async layouts always re-render on navigation) */\n isAsync: boolean;\n /** Child segments keyed by segment path */\n children: Map<string, SegmentNode>;\n}\n\n/**\n * Serialized state tree sent via X-Timber-State-Tree header.\n * Only sync segments are included — async segments always re-render.\n */\nexport interface StateTree {\n segments: string[];\n}\n\n// ─── Segment Cache ───────────────────────────────────────────────\n\n/**\n * Maintains the client-side segment tree representing currently mounted\n * layouts and pages. Used for navigation reconciliation — the router diffs\n * new routes against this tree to determine which segments to re-fetch.\n */\nexport class SegmentCache {\n private root: SegmentNode | undefined;\n\n get(segment: string): SegmentNode | undefined {\n if (segment === '/' || segment === this.root?.segment) {\n return this.root;\n }\n return undefined;\n }\n\n set(segment: string, node: SegmentNode): void {\n if (segment === '/' || !this.root) {\n this.root = node;\n }\n }\n\n clear(): void {\n this.root = undefined;\n }\n\n /**\n * Serialize the mounted segment tree for the X-Timber-State-Tree header.\n * Only includes sync segments — async segments are excluded because the\n * server must always re-render them (they may depend on request context).\n *\n * When mergeableFilter is provided, only segments whose paths are in the\n * set are included. This ensures the server only skips segments that the\n * client can actually merge (i.e., segments whose cached element tree\n * contains an inner SegmentProvider the merger can splice into).\n *\n * This is a performance optimization only, NOT a security boundary.\n * The server always runs all access.ts files regardless of the state tree.\n */\n serializeStateTree(mergeableFilter?: Set<string>): StateTree {\n const segments: string[] = [];\n if (this.root) {\n collectSyncSegments(this.root, segments, mergeableFilter);\n }\n return { segments };\n }\n}\n\n/** Recursively collect sync segment paths from the tree */\nfunction collectSyncSegments(\n node: SegmentNode,\n out: string[],\n mergeableFilter?: Set<string>\n): void {\n if (!node.isAsync && (!mergeableFilter || mergeableFilter.has(node.segment))) {\n out.push(node.segment);\n }\n for (const child of node.children.values()) {\n collectSyncSegments(child, out, mergeableFilter);\n }\n}\n\n// ─── Segment Tree Builder ────────────────────────────────────────\n\n/**\n * Segment metadata from the server, sent via X-Timber-Segments header.\n * Describes a rendered segment's path and whether it's async.\n */\nexport interface SegmentInfo {\n path: string;\n isAsync: boolean;\n}\n\n/**\n * Build a SegmentNode tree from flat segment metadata.\n *\n * Takes an ordered list of segment descriptors (root → leaf) from the\n * server's X-Timber-Segments header and constructs the hierarchical\n * tree structure that SegmentCache expects.\n *\n * Each segment is nested as a child of the previous one, forming a\n * linear chain from root to leaf. The leaf segment (page) is excluded\n * from the tree — pages are never cached across navigations.\n */\nexport function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefined {\n // Need at least a root segment to build a tree\n if (segments.length === 0) return undefined;\n\n // Exclude the leaf (page) — pages always re-render on navigation.\n // Only layouts are cached in the segment tree.\n const layouts = segments.length > 1 ? segments.slice(0, -1) : segments;\n\n let root: SegmentNode | undefined;\n let parent: SegmentNode | undefined;\n\n for (const info of layouts) {\n const node: SegmentNode = {\n segment: info.path,\n payload: null,\n isAsync: info.isAsync,\n children: new Map(),\n };\n\n if (!root) {\n root = node;\n }\n\n if (parent) {\n parent.children.set(info.path, node);\n }\n\n parent = node;\n }\n\n return root;\n}\n\n// ─── Prefetch Cache ──────────────────────────────────────────────\n\ninterface PrefetchEntry {\n result: PrefetchResult;\n expiresAt: number;\n}\n\n/**\n * Short-lived cache for hover-triggered prefetches. Entries expire after\n * 30 seconds. When a link is clicked, the prefetched payload is consumed\n * (moved to the history stack) and removed from this cache.\n *\n * timber.js does NOT prefetch on viewport intersection — only explicit\n * hover on <Link prefetch> triggers a prefetch.\n */\nexport class PrefetchCache {\n private static readonly TTL_MS = 30_000;\n private entries = new Map<string, PrefetchEntry>();\n\n set(url: string, result: PrefetchResult): void {\n this.entries.set(url, {\n result,\n expiresAt: Date.now() + PrefetchCache.TTL_MS,\n });\n }\n\n get(url: string): PrefetchResult | undefined {\n const entry = this.entries.get(url);\n if (!entry) return undefined;\n if (Date.now() >= entry.expiresAt) {\n this.entries.delete(url);\n return undefined;\n }\n return entry.result;\n }\n\n /** Get and remove the entry (used when navigation consumes a prefetch) */\n consume(url: string): PrefetchResult | undefined {\n const result = this.get(url);\n if (result !== undefined) {\n this.entries.delete(url);\n }\n return result;\n }\n}\n","// History Stack — stores RSC payloads by URL for instant back/forward navigation\n// See design/19-client-navigation.md § History Stack\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface HistoryEntry {\n /** The complete segment tree payload at the time of navigation */\n payload: unknown;\n /** Resolved head elements for this page (title, meta tags). Null for SSR'd initial page. */\n headElements?: HeadElement[] | null;\n /** Route params for this page (for useParams). Null for SSR'd initial page. */\n params?: Record<string, string | string[]> | null;\n}\n\n// ─── History Stack ───────────────────────────────────────────────\n\n/**\n * Session-lived history stack keyed by URL. Enables instant back/forward\n * navigation without a server roundtrip.\n *\n * On forward navigation, the new page's payload is pushed onto the stack.\n * On popstate, the cached payload is replayed instantly.\n *\n * Supports two keying modes:\n * - **URL-keyed** (default): entries keyed by pathname + search.\n * Used with the History API fallback.\n * - **Entry-key + URL**: when the Navigation API is available,\n * entries can also be stored by Navigation entry key for\n * disambiguation of duplicate URLs in the history stack.\n * Falls back to URL lookup when entry key is not found.\n *\n * Scroll positions are stored in history.state or Navigation API entry\n * state, not in this stack — see design/19-client-navigation.md §Scroll Restoration.\n *\n * Entries persist for the session duration (no expiry) and are cleared\n * when the tab is closed — matching browser back-button behavior.\n */\nexport class HistoryStack {\n private entries = new Map<string, HistoryEntry>();\n /** Entries keyed by Navigation API entry key for duplicate URL disambiguation. */\n private entryKeyMap = new Map<string, HistoryEntry>();\n\n push(url: string, entry: HistoryEntry, entryKey?: string): void {\n this.entries.set(url, entry);\n if (entryKey) {\n this.entryKeyMap.set(entryKey, entry);\n }\n }\n\n /**\n * Get an entry. When an entry key is provided (Navigation API),\n * tries the entry-key map first for accurate disambiguation of\n * duplicate URLs, then falls back to URL lookup.\n */\n get(url: string, entryKey?: string): HistoryEntry | undefined {\n if (entryKey) {\n const byKey = this.entryKeyMap.get(entryKey);\n if (byKey) return byKey;\n }\n return this.entries.get(url);\n }\n\n has(url: string): boolean {\n return this.entries.has(url);\n }\n}\n","/**\n * Segment Merger — client-side tree merging for partial RSC payloads.\n *\n * When the server skips rendering sync layouts (because the client already\n * has them cached), the RSC payload is missing outer segment wrappers.\n * This module reconstructs the full element tree by splicing the partial\n * payload into cached segment subtrees.\n *\n * The approach:\n * 1. After each full RSC payload render, walk the decoded element tree\n * and cache each segment's subtree (identified by SegmentProvider boundaries)\n * 2. When a partial payload arrives, wrap it with cached segment elements\n * using React.cloneElement to preserve component identity\n *\n * React.cloneElement preserves the element's `type` — React sees the same\n * component at the same tree position and reconciles (preserving state)\n * rather than remounting. This is how layout state survives navigations.\n *\n * Design docs: 19-client-navigation.md §\"Navigation Reconciliation\"\n * Security: access.ts runs on the server regardless of skipping — this\n * is a performance optimization only. See 13-security.md.\n */\n\nimport { cloneElement, isValidElement, type ReactElement, type ReactNode } from 'react';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/**\n * A cached segment entry. Stores the full subtree rooted at a SegmentProvider\n * and the path through the tree to the next SegmentProvider (or leaf).\n */\nexport interface CachedSegmentEntry {\n /** The segment's URL path (e.g., \"/\", \"/dashboard\") */\n segmentPath: string;\n /** The SegmentProvider element for this segment */\n element: ReactElement;\n /**\n * Whether this segment's cached element contains a nested SegmentProvider.\n * Only segments with inner SegmentProviders are safe to skip — the merger\n * can only replace inner SegmentProviders, not pages embedded in layout output.\n * Used by the state tree serialization to exclude non-mergeable segments.\n */\n hasMergeableChild: boolean;\n}\n\n// ─── Segment Element Cache ───────────────────────────────────────\n\n/**\n * Cache of React element subtrees per segment path.\n * Updated after each navigation with the full decoded RSC element tree.\n */\nexport class SegmentElementCache {\n private entries = new Map<string, CachedSegmentEntry>();\n\n get(segmentPath: string): CachedSegmentEntry | undefined {\n return this.entries.get(segmentPath);\n }\n\n set(segmentPath: string, entry: CachedSegmentEntry): void {\n this.entries.set(segmentPath, entry);\n }\n\n has(segmentPath: string): boolean {\n return this.entries.has(segmentPath);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /**\n * Get the set of segment paths that are safe for the server to skip.\n * Only segments with an inner SegmentProvider (hasMergeableChild) are\n * included — the merger can only replace inner SegmentProviders, not\n * pages embedded in layout output. Used to filter the state tree.\n *\n * Returns an empty set if the element cache is empty (no elements\n * cached yet). This is the safe default — an empty set means no\n * segments pass the filter, so the state tree is empty and the server\n * does a full render. The element cache is populated lazily after the\n * first SPA navigation (RSC-decoded elements from hydration are\n * thenables that can't be walked until React resolves them).\n */\n getMergeablePaths(): Set<string> {\n const paths = new Set<string>();\n for (const [, entry] of this.entries) {\n if (entry.hasMergeableChild) {\n paths.add(entry.segmentPath);\n }\n }\n return paths;\n }\n}\n\n// ─── SegmentProvider Detection ───────────────────────────────────\n\n/**\n * Check if a React element is a SegmentProvider by looking for the\n * `segments` prop (an array of path segments). This is the only\n * component that receives this prop shape.\n */\nexport function isSegmentProvider(element: unknown): element is ReactElement {\n if (!isValidElement(element)) return false;\n const props = element.props as Record<string, unknown>;\n return Array.isArray(props.segments);\n}\n\n/**\n * Extract the segment path from a SegmentProvider element.\n *\n * Uses the `segmentId` prop if available (set by the server for route groups\n * to distinguish siblings that share the same urlPath). Falls back to\n * reconstructing from the `segments` array prop.\n */\nexport function getSegmentPath(element: ReactElement): string {\n const props = element.props as { segments: string[]; segmentId?: string };\n // segmentId is the authoritative key — includes group name for route groups\n if (props.segmentId) return props.segmentId;\n const filtered = props.segments.filter(Boolean);\n return filtered.length === 0 ? '/' : '/' + filtered.join('/');\n}\n\n// ─── Tree Walking ────────────────────────────────────────────────\n\n/**\n * Walk a React element tree and extract all SegmentProvider boundaries.\n * Returns an ordered list of segment entries from outermost to innermost.\n *\n * This only finds SegmentProviders along the main children path — it does\n * not descend into parallel routes/slots (those are separate subtrees).\n */\nexport function extractSegments(element: unknown): CachedSegmentEntry[] {\n const segments: CachedSegmentEntry[] = [];\n walkForSegments(element, segments);\n // Compute hasMergeableChild: a segment is mergeable if there's another\n // SegmentProvider nested below it. The segments list is ordered outermost\n // to innermost, so each segment's child is the next entry.\n for (let i = 0; i < segments.length; i++) {\n segments[i].hasMergeableChild = i < segments.length - 1;\n }\n return segments;\n}\n\nfunction walkForSegments(node: unknown, out: CachedSegmentEntry[]): void {\n if (!isValidElement(node)) return;\n\n // Use a local binding to avoid TypeScript narrowing issues with\n // isSegmentProvider's type predicate on the same variable.\n const el: ReactElement = node as ReactElement;\n const props = el.props as Record<string, unknown>;\n\n if (isSegmentProvider(node)) {\n out.push({\n segmentPath: getSegmentPath(el),\n element: el,\n hasMergeableChild: false, // computed after collection in extractSegments\n });\n // Continue walking into children to find nested segments\n walkChildren(props.children as ReactNode, out);\n return;\n }\n\n // Not a SegmentProvider — walk children looking for one\n walkChildren(props.children as ReactNode, out);\n}\n\nfunction walkChildren(children: ReactNode, out: CachedSegmentEntry[]): void {\n if (children == null) return;\n\n if (Array.isArray(children)) {\n for (const child of children) {\n walkForSegments(child, out);\n }\n } else {\n walkForSegments(children, out);\n }\n}\n\n// ─── Cache Population ────────────────────────────────────────────\n\n/**\n * Cache all segment subtrees from a fully-rendered RSC element tree.\n * Call this after every full RSC payload render (navigate, refresh, hydration).\n */\nexport function cacheSegmentElements(element: unknown, cache: SegmentElementCache): void {\n const segments = extractSegments(element);\n for (const entry of segments) {\n cache.set(entry.segmentPath, entry);\n }\n}\n\n// ─── Tree Merging ────────────────────────────────────────────────\n\n/**\n * Find a SegmentProvider nested in the children of a React element.\n * Returns the path of elements from the given element down to the\n * SegmentProvider, enabling reconstruction via cloneElement.\n *\n * The path is an array of [element, childIndex] pairs. childIndex is -1\n * for single-child (non-array) props.children.\n */\ntype TreePath = Array<{ element: ReactElement; childIndex: number }>;\n\nfunction findSegmentProviderPath(node: ReactElement, targetPath?: string): TreePath | null {\n const children = (node.props as { children?: ReactNode }).children;\n if (children == null) return null;\n\n if (Array.isArray(children)) {\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (!isValidElement(child)) continue;\n\n if (isSegmentProvider(child)) {\n if (!targetPath || getSegmentPath(child) === targetPath) {\n return [{ element: node, childIndex: i }];\n }\n }\n\n const deeper = findSegmentProviderPath(child, targetPath);\n if (deeper) {\n return [{ element: node, childIndex: i }, ...deeper];\n }\n }\n } else if (isValidElement(children)) {\n if (isSegmentProvider(children)) {\n if (!targetPath || getSegmentPath(children) === targetPath) {\n return [{ element: node, childIndex: -1 }];\n }\n }\n\n const deeper = findSegmentProviderPath(children, targetPath);\n if (deeper) {\n return [{ element: node, childIndex: -1 }, ...deeper];\n }\n }\n\n return null;\n}\n\n/**\n * Replace a nested SegmentProvider within a cached element tree with\n * new content. Uses cloneElement along the path to produce a new tree\n * with preserved component identity at every level except the replaced node.\n *\n * @param cachedElement The cached SegmentProvider element for this segment\n * @param newInnerContent The new React element to splice in at the inner segment position\n * @param innerSegmentPath The path of the inner segment to replace (optional — replaces first found)\n * @returns New element tree with the inner segment replaced\n */\nexport function replaceInnerSegment(\n cachedElement: ReactElement,\n newInnerContent: ReactNode,\n innerSegmentPath?: string\n): ReactElement {\n const path = findSegmentProviderPath(cachedElement, innerSegmentPath);\n\n if (!path || path.length === 0) {\n // No inner SegmentProvider found — this segment's cached element\n // wraps a page directly (no child layout with a SegmentProvider).\n // We CANNOT safely replace the page because it's embedded deep in\n // the layout's server-rendered output tree and we don't know its\n // position. Return the cached element unchanged as a safety fallback.\n //\n // The server should not skip segments without a child layout below\n // them (enforced by hasRenderedLayoutBelow in buildRouteElement).\n // If this codepath is reached, it indicates a server/client mismatch.\n return cachedElement;\n }\n\n // Reconstruct bottom-up: replace the innermost element first, then\n // clone each ancestor with the updated child.\n let replacement: ReactNode = newInnerContent;\n\n for (let i = path.length - 1; i >= 0; i--) {\n const { element, childIndex } = path[i];\n\n if (childIndex === -1) {\n // Single child — replace it\n replacement = cloneElement(element, {}, replacement);\n } else {\n // Array children — replace the specific index\n const children = (element.props as { children: ReactNode[] }).children;\n const newChildren = [...children];\n newChildren[childIndex] = replacement;\n replacement = cloneElement(element, {}, ...newChildren);\n }\n }\n\n return replacement as ReactElement;\n}\n\n/**\n * Merge a partial RSC payload with cached segment elements.\n *\n * When the server skips segments, the partial payload starts from the\n * first non-skipped segment. This function wraps it with cached elements\n * for the skipped segments, producing a full tree that React can\n * reconcile with the mounted tree (preserving layout state).\n *\n * @param partialPayload The RSC payload element (may be partial)\n * @param skippedSegments Ordered list of segment paths that were skipped (outermost first)\n * @param cache The segment element cache\n * @returns The merged full element tree, or the partial payload if merging isn't possible\n */\nexport function mergeSegmentTree(\n partialPayload: unknown,\n skippedSegments: string[],\n cache: SegmentElementCache\n): unknown {\n if (!isValidElement(partialPayload)) return partialPayload;\n if (skippedSegments.length === 0) return partialPayload;\n\n // Build from outermost to innermost: each skipped segment's cached\n // element wraps the next, with the partial payload at the center.\n let result: ReactNode = partialPayload;\n\n // Process from innermost skipped segment to outermost\n for (let i = skippedSegments.length - 1; i >= 0; i--) {\n const segmentPath = skippedSegments[i];\n const cached = cache.get(segmentPath);\n\n if (!cached) {\n // No cached element for this segment — can't merge.\n // This shouldn't happen (server only skips segments the client\n // has cached), but if it does, return the partial payload as-is.\n return partialPayload;\n }\n\n // Replace the inner content of the cached segment with our current result.\n // The inner content is either the next SegmentProvider or the page.\n result = replaceInnerSegment(cached.element, result);\n }\n\n return result;\n}\n","/**\n * RSC Fetch — handles fetching and parsing RSC Flight payloads.\n *\n * Extracted from router.ts to keep both files under the 500-line limit.\n * This module handles:\n * - Cache-busting URL generation for RSC requests\n * - Building RSC request headers (Accept, X-Timber-State-Tree)\n * - Extracting metadata from RSC response headers\n * - Fetching and decoding RSC payloads\n *\n * See design/19-client-navigation.md §\"RSC Payload Handling\"\n */\n\nimport type { SegmentInfo } from './segment-cache';\nimport type { HeadElement } from './head';\nimport type { RouterDeps } from './router';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** Result of fetching an RSC payload — includes head elements and segment metadata. */\nexport interface FetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useSegmentParams(). */\n params: Record<string, string | string[]> | null;\n /** Segment paths that were skipped by the server (for client-side merging). */\n skippedSegments: string[] | null;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nexport const RSC_CONTENT_TYPE = 'text/x-component';\n\n// ─── URL Helpers ─────────────────────────────────────────────────\n\n/**\n * Generate a short random cache-busting ID (5 chars, a-z0-9).\n * Matches the format Next.js uses for _rsc params.\n */\nfunction generateCacheBustId(): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let id = '';\n for (let i = 0; i < 5; i++) {\n id += chars[(Math.random() * 36) | 0];\n }\n return id;\n}\n\n/**\n * Append a `_rsc=<id>` query parameter to the URL.\n * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML\n * for RSC navigation requests and signals that this is an RSC fetch.\n */\nfunction appendRscParam(url: string): string {\n const separator = url.includes('?') ? '&' : '?';\n return `${url}${separator}_rsc=${generateCacheBustId()}`;\n}\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The client's deployment ID, set at bootstrap from the runtime config.\n * Sent with every RSC/action request for version skew detection.\n * Null in dev mode. See TIM-446.\n */\nlet clientDeploymentId: string | null = null;\n\n/** Set the client deployment ID. Called once at bootstrap. */\nexport function setClientDeploymentId(id: string | null): void {\n clientDeploymentId = id;\n}\n\n/** Get the client deployment ID. */\nexport function getClientDeploymentId(): string | null {\n return clientDeploymentId;\n}\n\n// ─── Reload Signal ───────────────────────────────────────────────\n\n/** Header name used by the server to signal a version skew reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n/** Header name for the client's deployment ID. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/**\n * Check if a response signals a version skew reload.\n * Triggers a full page reload if the server indicates the client is stale.\n */\nexport function checkReloadSignal(response: Response): boolean {\n return response.headers.get(RELOAD_HEADER) === '1';\n}\n\n// ─── Header Builder ──────────────────────────────────────────────\n\nexport function buildRscHeaders(\n stateTree: { segments: string[] } | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n // Send deployment ID for version skew detection (TIM-446).\n // The server compares this against the current build's ID.\n // On mismatch, the server signals a reload instead of returning\n // an RSC payload with mismatched module references.\n if (clientDeploymentId) {\n headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;\n }\n return headers;\n}\n\n// ─── Response Header Extraction ──────────────────────────────────\n\n/** Dev-only warning for malformed framework headers. Tree-shaken in production. */\nfunction warnMalformedHeader(headerName: string, raw: string): void {\n if (process.env.NODE_ENV !== 'production') {\n const preview = raw.length > 200 ? raw.slice(0, 200) + '…' : raw;\n console.warn(\n `[timber] Malformed ${headerName} header \\u2014 JSON.parse failed. ` +\n `This indicates a framework bug or header corruption. Raw (first 200 chars): ${preview}`\n );\n }\n}\n\n/**\n * Extract head elements from the X-Timber-Head response header.\n * Returns null if the header is missing or malformed.\n */\nexport function extractHeadElements(response: Response): HeadElement[] | null {\n const header = response.headers.get('X-Timber-Head');\n if (!header) return null;\n try {\n return JSON.parse(decodeURIComponent(header));\n } catch {\n warnMalformedHeader('X-Timber-Head', header);\n return null;\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isAsync} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nexport function extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract skipped segment paths from the X-Timber-Skipped-Segments header.\n * Returns null if the header is missing or malformed.\n *\n * When the server skips sync layouts the client already has cached,\n * it sends this header listing the skipped segment paths (outermost first).\n * The client uses this to merge the partial payload with cached segments.\n */\nexport function extractSkippedSegments(response: Response): string[] | null {\n const header = response.headers.get('X-Timber-Skipped-Segments');\n if (!header) return null;\n try {\n const parsed = JSON.parse(header);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n warnMalformedHeader('X-Timber-Skipped-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract route params from the X-Timber-Params response header.\n * Returns null if the header is missing or malformed.\n *\n * Used to populate useSegmentParams() after client-side navigation.\n */\nexport function extractParams(response: Response): Record<string, string | string[]> | null {\n const header = response.headers.get('X-Timber-Params');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Params', header);\n return null;\n }\n}\n\n// ─── Redirect Error ──────────────────────────────────────────────\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nexport class RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Thrown when the server signals a version skew (X-Timber-Reload header).\n * Caught in navigate() to trigger a full page reload via triggerStaleReload().\n * See TIM-446.\n */\nexport class VersionSkewError extends Error {\n constructor() {\n super('Version skew detected — server has been redeployed');\n }\n}\n\n/**\n * Thrown when the server returns an error for an RSC payload request.\n * The server sends X-Timber-Error header and a JSON body instead of a\n * broken RSC stream for any RenderError (4xx or 5xx). Caught in\n * navigate() to trigger a hard navigation so the server can render\n * the error page as HTML.\n *\n * See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n */\nexport class ServerErrorResponse extends Error {\n readonly status: number;\n readonly url: string;\n constructor(status: number, url: string) {\n super(`Server error ${status} during navigation to ${url}`);\n this.status = status;\n this.url = url;\n }\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n *\n * Also extracts head elements from the X-Timber-Head response header\n * so the client can update document.title and <meta> tags after navigation.\n */\nexport async function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string,\n signal?: AbortSignal\n): Promise<FetchResult> {\n const rscUrl = appendRscParam(url);\n const headers = buildRscHeaders(stateTree, currentUrl);\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read X-Timber-Head before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n let headElements: HeadElement[] | null = null;\n let segmentInfo: SegmentInfo[] | null = null;\n let params: Record<string, string | string[]> | null = null;\n let skippedSegments: string[] | null = null;\n const wrappedPromise = fetchPromise.then((response) => {\n // Version skew detection (TIM-446): if the server signals a reload,\n // throw VersionSkewError so the caller (router navigate) can trigger\n // a full page reload via triggerStaleReload().\n if (checkReloadSignal(response)) {\n throw new VersionSkewError();\n }\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n // Detect server error responses. The server returns X-Timber-Error header\n // with a JSON body instead of a broken RSC stream for any RenderError\n // (4xx or 5xx). Hard-navigate so the server renders the error page as HTML.\n // See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n if (response.headers.get('X-Timber-Error') === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n headElements = extractHeadElements(response);\n segmentInfo = extractSegmentInfo(response);\n params = extractParams(response);\n skippedSegments = extractSkippedSegments(response);\n return response;\n });\n // Await headers so headElements/segmentInfo/params are populated.\n await wrappedPromise;\n // Await the decoded payload — createFromFetch returns a thenable\n // that resolves to the React element tree once the Flight stream\n // has enough data to produce the shell.\n const payload = await deps.decodeRsc(wrappedPromise);\n return { payload, headElements, segmentInfo, params, skippedSegments };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n return {\n payload: await response.text(),\n headElements: extractHeadElements(response),\n segmentInfo: extractSegmentInfo(response),\n params: extractParams(response),\n skippedSegments: extractSkippedSegments(response),\n };\n}\n","// Segment Router — manages client-side navigation and RSC payload fetching\n// See design/19-client-navigation.md for the full architecture.\n\nimport { SegmentCache, PrefetchCache, buildSegmentTree } from './segment-cache';\nimport type { SegmentInfo } from './segment-cache';\nimport { HistoryStack } from './history';\nimport type { HeadElement } from './head';\nimport { setCurrentParams } from './use-segment-params.js';\nimport {\n setNavigationState,\n getNavigationState,\n type NavigationState,\n} from './navigation-context.js';\nimport { SegmentElementCache, cacheSegmentElements, mergeSegmentTree } from './segment-merger.js';\nimport {\n fetchRscPayload,\n RedirectError,\n ServerErrorResponse,\n VersionSkewError,\n} from './rsc-fetch.js';\nimport { setHardNavigating } from './navigation-root.js';\nimport type { FetchResult } from './rsc-fetch.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface NavigationOptions {\n /** Set to false to prevent scroll-to-top on forward navigation */\n scroll?: boolean;\n /** Use replaceState instead of pushState (replaces current history entry) */\n replace?: boolean;\n /**\n * @internal AbortSignal from the Navigation API's NavigateEvent.\n * When provided, the signal is linked to the router's per-navigation\n * AbortController so in-flight RSC fetches are cancelled when a new\n * navigation starts.\n */\n _signal?: AbortSignal;\n /**\n * @internal Skip pushState/replaceState — the Navigation API has already\n * updated the URL via event.intercept(). Used for external navigations\n * intercepted by the navigate event handler.\n */\n _skipHistory?: boolean;\n}\n\n/**\n * Function that decodes an RSC Flight stream into a React element tree.\n * In production: createFromFetch from @vitejs/plugin-rsc/browser.\n * In tests: a mock that returns the raw payload.\n */\nexport type RscDecoder = (fetchPromise: Promise<Response>) => unknown;\n\n/**\n * Function that renders a decoded RSC element tree into the DOM.\n * In production: reactRoot.render(element).\n * In tests: a no-op or mock.\n *\n * Receives the current NavigationState explicitly — no temporal\n * coupling with setNavigationState/getNavigationState. The renderer\n * wraps the element in NavigationProvider with this state.\n */\nexport type RootRenderer = (element: unknown, navState: NavigationState) => void;\n\n/**\n * Platform dependencies injected for testability. In production these\n * map to browser APIs; in tests they're replaced with mocks.\n */\nexport interface RouterDeps {\n fetch: (url: string, init: RequestInit) => Promise<Response>;\n pushState: (data: unknown, unused: string, url: string) => void;\n replaceState: (data: unknown, unused: string, url: string) => void;\n scrollTo: (x: number, y: number) => void;\n getCurrentUrl: () => string;\n getScrollY: () => number;\n /** Decode RSC Flight stream into React elements. If not provided, raw response text is stored. */\n decodeRsc?: RscDecoder;\n /** Render decoded RSC tree into the DOM. If not provided, rendering is a no-op. */\n renderRoot?: RootRenderer;\n /**\n * Schedule a callback after the next paint. In the browser, this is\n * requestAnimationFrame + setTimeout(0) to run after React commits.\n * In tests, this runs the callback synchronously.\n */\n afterPaint?: (callback: () => void) => void;\n /** Apply resolved head elements (title, meta tags) to the DOM after navigation. */\n applyHead?: (elements: HeadElement[]) => void;\n /**\n * Run a navigation inside a React transition with optimistic pending URL.\n * The pending URL shows immediately (useOptimistic urgent update) and\n * reverts when the transition commits (atomic with the new tree).\n *\n * The `perform` callback receives a `wrapPayload` function to wrap the\n * decoded RSC payload with NavigationProvider + NuqsAdapter before\n * NavigationRoot sets it as the new element. The `wrapPayload` function\n * receives the NavigationState explicitly — no temporal coupling with\n * getNavigationState().\n *\n * If not provided (tests), the router falls back to renderRoot.\n */\n navigateTransition?: (\n pendingUrl: string,\n perform: (\n wrapPayload: (payload: unknown, navState: NavigationState) => unknown\n ) => Promise<unknown>\n ) => Promise<void>;\n\n /**\n * Whether the Navigation API is active and handling traversals.\n * When true, the popstate handler is a no-op — the Navigation API's\n * navigate event covers back/forward button presses.\n */\n navigationApiActive?: boolean;\n\n /**\n * Called around pushState/replaceState to set a flag that prevents\n * the Navigation API's navigate listener from double-handling\n * router-initiated navigations.\n */\n setRouterNavigating?: (value: boolean) => void;\n\n /**\n * Save scroll position via the Navigation API's per-entry state.\n * When provided, used instead of history.replaceState for scroll storage.\n */\n saveNavigationEntryScroll?: (scrollY: number) => void;\n\n /**\n * Signal that a router-initiated navigation has completed. Resolves the\n * deferred promise that ties the browser's native loading state to the\n * navigation lifecycle. Called in the finally block of navigate/refresh,\n * aligned with when the TopLoader's pendingUrl clears.\n */\n completeRouterNavigation?: () => void;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Fires the navigate event BEFORE committing the URL, allowing Chrome\n * to show its native loading indicator. Falls back to pushState when\n * unavailable.\n */\n navigationNavigate?: (url: string, replace: boolean) => void;\n}\n\nexport interface RouterInstance {\n /** Navigate to a new URL (forward navigation) */\n navigate(url: string, options?: NavigationOptions): Promise<void>;\n /** Full re-render of the current URL — no state tree sent */\n refresh(): Promise<void>;\n /** Handle a popstate event (back/forward button). scrollY is read from history.state. */\n handlePopState(url: string, scrollY?: number, externalSignal?: AbortSignal): Promise<void>;\n /** Whether a navigation is currently in flight */\n isPending(): boolean;\n /** The URL currently being navigated to, or null if idle */\n getPendingUrl(): string | null;\n /** Subscribe to pending state changes */\n onPendingChange(listener: (pending: boolean) => void): () => void;\n /** Prefetch an RSC payload for a URL (used by Link hover) */\n prefetch(url: string): void;\n /**\n * Apply a piggybacked revalidation payload from a server action response.\n * Renders the element tree and updates head elements without a server fetch.\n * See design/08-forms-and-actions.md §\"Single-Roundtrip Revalidation\".\n */\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void;\n /**\n * Populate the segment cache from server-provided segment metadata.\n * Called on initial hydration with segment info embedded in the HTML.\n */\n initSegmentCache(segments: SegmentInfo[]): void;\n /**\n * Cache segment elements from a decoded RSC element tree.\n * Called on initial hydration to populate the element cache so the\n * first client navigation can use partial payloads.\n */\n cacheElementTree(element: unknown): void;\n /** The segment cache (exposed for tests and <Link> prefetch) */\n segmentCache: SegmentCache;\n /** The prefetch cache (exposed for tests and <Link> prefetch) */\n prefetchCache: PrefetchCache;\n /** The history stack (exposed for tests) */\n historyStack: HistoryStack;\n}\n\n/**\n * Check if an error is an abort error (connection closed / fetch aborted).\n * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof DOMException && error.name === 'AbortError') return true;\n if (error instanceof Error && error.name === 'AbortError') return true;\n return false;\n}\n\n// ─── Router Factory ──────────────────────────────────────────────\n\n/**\n * Create a router instance. In production, called once at app hydration\n * with real browser APIs. In tests, called with mock dependencies.\n */\n/**\n * Router navigation phase — discriminated union replacing scattered\n * `pending` + `pendingUrl` boolean flags.\n *\n * - `idle`: No navigation in flight. The committed params/pathname\n * are current.\n * - `navigating`: A fetch or render is in progress. `targetUrl` is\n * the destination being navigated to.\n */\nexport type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };\n\nexport function createRouter(deps: RouterDeps): RouterInstance {\n const segmentCache = new SegmentCache();\n const prefetchCache = new PrefetchCache();\n const historyStack = new HistoryStack();\n const segmentElementCache = new SegmentElementCache();\n\n let routerPhase: RouterPhase = { phase: 'idle' };\n const pendingListeners = new Set<(pending: boolean) => void>();\n\n // AbortController for the current in-flight navigation.\n // When a new navigation starts, the previous controller is aborted,\n // cancelling any in-progress RSC fetch. This provides automatic\n // cancellation of stale fetches regardless of Navigation API support.\n let currentNavAbort: AbortController | null = null;\n\n /**\n * Create a new AbortController for a navigation, aborting any\n * previous in-flight navigation. Optionally links to an external\n * signal (e.g., from the Navigation API's NavigateEvent.signal).\n */\n function createNavAbort(externalSignal?: AbortSignal): AbortController {\n // Abort previous navigation's fetch\n currentNavAbort?.abort();\n const controller = new AbortController();\n currentNavAbort = controller;\n\n // If an external signal is provided (e.g., Navigation API),\n // forward its abort to our controller.\n if (externalSignal) {\n if (externalSignal.aborted) {\n controller.abort();\n } else {\n externalSignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return controller;\n }\n\n function setPending(value: boolean, url?: string): void {\n const next: RouterPhase =\n value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };\n // Skip no-op updates\n if (\n routerPhase.phase === next.phase &&\n (routerPhase.phase === 'idle' ||\n (routerPhase.phase === 'navigating' &&\n next.phase === 'navigating' &&\n routerPhase.targetUrl === next.targetUrl))\n ) {\n return;\n }\n routerPhase = next;\n // Notify external store listeners (non-React consumers).\n // React-facing pending state is handled by useOptimistic in\n // NavigationRoot via navigateTransition — not this function.\n for (const listener of pendingListeners) {\n listener(value);\n }\n }\n\n /** Update the segment cache from server-provided segment metadata. */\n function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {\n if (!segmentInfo || segmentInfo.length === 0) return;\n const tree = buildSegmentTree(segmentInfo);\n if (tree) {\n segmentCache.set('/', tree);\n }\n }\n\n /** Render a decoded RSC payload into the DOM if a renderer is available. */\n function renderPayload(payload: unknown, navState: NavigationState): void {\n if (deps.renderRoot) {\n deps.renderRoot(payload, navState);\n }\n }\n\n /**\n * Merge a partial RSC payload with cached segment elements if segments\n * were skipped, then cache segments from the (merged) payload.\n * Returns the merged payload ready for rendering.\n */\n function mergeAndCachePayload(\n payload: unknown,\n skippedSegments: string[] | null | undefined\n ): unknown {\n let merged = payload;\n\n // If segments were skipped, merge the partial payload with cached segments\n if (skippedSegments && skippedSegments.length > 0) {\n merged = mergeSegmentTree(payload, skippedSegments, segmentElementCache);\n }\n\n // Cache segment elements from the (merged) payload for future merges\n cacheSegmentElements(merged, segmentElementCache);\n\n return merged;\n }\n\n /**\n * Update navigation state (params + pathname) for the next render.\n *\n * Sets the module-level fallback (for tests and SSR) and the\n * globalThis bridge, then returns the NavigationState so callers\n * can pass it explicitly to renderRoot/wrapPayload — eliminating\n * temporal coupling with getNavigationState().\n */\n function updateNavigationState(\n params: Record<string, string | string[]> | null | undefined,\n url: string\n ): NavigationState {\n const resolvedParams = params ?? {};\n // Module-level fallback for tests (no NavigationProvider) and SSR\n setCurrentParams(resolvedParams);\n // globalThis bridge — kept for backward compat\n const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0] || '/';\n const navState: NavigationState = { params: resolvedParams, pathname };\n setNavigationState(navState);\n return navState;\n }\n\n /**\n * Render a payload via navigateTransition (production) or renderRoot (tests).\n * The perform callback should fetch data, update state, and return the\n * FetchResult plus the NavigationState (so it can be passed explicitly\n * to wrapPayload/renderRoot without temporal coupling).\n */\n async function renderViaTransition(\n url: string,\n perform: () => Promise<FetchResult & { navState: NavigationState }>\n ): Promise<HeadElement[] | null> {\n if (deps.navigateTransition) {\n let headElements: HeadElement[] | null = null;\n await deps.navigateTransition(url, async (wrapPayload) => {\n const result = await perform();\n headElements = result.headElements;\n // Merge partial payload with cached segments before wrapping\n const merged = mergeAndCachePayload(result.payload, result.skippedSegments);\n // Store the MERGED payload in history — not the partial pre-merge tree.\n // This ensures handlePopState replays the complete tree on back/forward.\n historyStack.push(url, {\n payload: merged,\n headElements: result.headElements,\n params: result.params,\n });\n // Pass navState explicitly — wrapPayload wraps element in\n // NavigationProvider with this state, no getNavigationState() needed.\n return wrapPayload(merged, result.navState);\n });\n return headElements;\n }\n // Fallback: no transition (tests, no React tree)\n const result = await perform();\n // Merge partial payload with cached segments before rendering\n const merged = mergeAndCachePayload(result.payload, result.skippedSegments);\n // Store merged payload in history\n historyStack.push(url, {\n payload: merged,\n headElements: result.headElements,\n params: result.params,\n });\n renderPayload(merged, result.navState);\n return result.headElements;\n }\n\n /** Apply head elements (title, meta tags) to the DOM if available. */\n function applyHead(elements: HeadElement[] | null | undefined): void {\n if (elements && deps.applyHead) {\n deps.applyHead(elements);\n }\n }\n\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n /**\n * Schedule scroll restoration after the next paint and fire the\n * scroll-restored event. Used by navigate, popstate, and refresh.\n */\n function restoreScrollAfterPaint(scrollY: number): void {\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n /**\n * Core navigation logic shared between the transition and fallback paths.\n * Fetches the RSC payload, updates all state, and returns the result.\n */\n async function performNavigationFetch(\n url: string,\n options: { replace: boolean; signal?: AbortSignal; skipHistory?: boolean }\n ): Promise<FetchResult & { navState: NavigationState }> {\n // Check prefetch cache first. PrefetchResult has optional segmentInfo/params\n // fields — normalize to null for FetchResult compatibility.\n const prefetched = prefetchCache.consume(url);\n let result: FetchResult | undefined = prefetched\n ? {\n payload: prefetched.payload,\n headElements: prefetched.headElements,\n segmentInfo: prefetched.segmentInfo ?? null,\n params: prefetched.params ?? null,\n skippedSegments: prefetched.skippedSegments ?? null,\n }\n : undefined;\n\n if (result === undefined) {\n // Fetch RSC payload with state tree for partial rendering.\n // Send current URL for intercepting route resolution (modal pattern).\n const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());\n const rawCurrentUrl = deps.getCurrentUrl();\n const currentUrl = rawCurrentUrl.startsWith('http')\n ? new URL(rawCurrentUrl).pathname\n : new URL(rawCurrentUrl, 'http://localhost').pathname;\n result = await fetchRscPayload(url, deps, stateTree, currentUrl, options.signal);\n }\n\n // Update the browser history — skip when the Navigation API has already\n // updated the URL via event.intercept() (external navigations).\n if (!options.skipHistory) {\n // Set the router-navigating flag so the Navigation API's navigate\n // listener doesn't double-intercept this pushState/replaceState.\n deps.setRouterNavigating?.(true);\n if (options.replace) {\n deps.replaceState({ timber: true, scrollY: 0 }, '', url);\n } else {\n deps.pushState({ timber: true, scrollY: 0 }, '', url);\n }\n deps.setRouterNavigating?.(false);\n }\n\n // NOTE: History push is deferred — the merged payload (after segment\n // merging in renderViaTransition) is stored by the caller, not here.\n // Storing result.payload here would record the partial (pre-merge)\n // RSC tree, causing handlePopState to replay an incomplete tree.\n\n // Update the segment cache with the new route's segment tree.\n updateSegmentCache(result.segmentInfo);\n\n // Update navigation state and capture it for explicit passing.\n const navState = updateNavigationState(result.params, url);\n\n return { ...result, navState };\n }\n\n async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {\n const scroll = options.scroll !== false;\n const replace = options.replace === true;\n const externalSignal = options._signal as AbortSignal | undefined;\n const skipHistory = options._skipHistory === true;\n\n // Create an abort controller for this navigation. Links to the external\n // signal (Navigation API's event.signal) when provided.\n const navAbort = createNavAbort(externalSignal);\n\n // Capture the departing page's scroll position for scroll={false} preservation.\n const currentScrollY = deps.getScrollY();\n\n // Save the departing page's scroll position — use Navigation API entry\n // state when available, otherwise fall back to history.state.\n if (deps.saveNavigationEntryScroll) {\n deps.saveNavigationEntryScroll(currentScrollY);\n } else {\n deps.replaceState({ timber: true, scrollY: currentScrollY }, '', deps.getCurrentUrl());\n }\n\n // When Navigation API is active, initiate the navigation via\n // navigation.navigate() BEFORE the fetch. Unlike history.pushState()\n // which commits the URL synchronously (so Chrome sees it as \"done\"),\n // navigation.navigate() fires the navigate event before committing.\n // Our handler intercepts with a deferred promise, and Chrome shows\n // its native loading indicator until completeRouterNavigation()\n // resolves it in the finally block (same time as TopLoader clears).\n let effectiveSkipHistory = skipHistory;\n if (!skipHistory && deps.navigationNavigate) {\n deps.setRouterNavigating?.(true);\n deps.navigationNavigate(url, replace);\n deps.setRouterNavigating?.(false);\n effectiveSkipHistory = true;\n }\n\n setPending(true, url);\n\n try {\n const headElements = await renderViaTransition(url, () =>\n performNavigationFetch(url, {\n replace,\n signal: navAbort.signal,\n skipHistory: effectiveSkipHistory,\n })\n );\n\n // Update document.title and <meta> tags with the new page's metadata\n applyHead(headElements);\n\n // Notify nuqs adapter (and any other listeners) that navigation completed.\n window.dispatchEvent(new Event('timber:navigation-end'));\n\n // Scroll-to-top on forward navigation, or restore captured position\n // for scroll={false}. React's render() on the document root can reset\n // scroll during DOM reconciliation, so all scroll must be actively managed.\n restoreScrollAfterPaint(scroll ? 0 : currentScrollY);\n } catch (error) {\n // Version skew — server has been redeployed. Trigger full page reload\n // so the browser fetches the new bundle. See TIM-446.\n // Set hard-navigating flag to prevent Navigation API interception\n // and React from rendering during page teardown. See TIM-626.\n if (error instanceof VersionSkewError) {\n setHardNavigating(true);\n // Import triggerStaleReload dynamically to avoid circular deps\n // and keep the reload logic centralized with its loop guard.\n const { triggerStaleReload } = await import('./stale-reload.js');\n triggerStaleReload();\n // Return a never-resolving promise — page is reloading.\n return new Promise(() => {}) as never;\n }\n // Server-side redirect during RSC fetch → soft router navigation.\n // The redirect navigate will push/replace its own URL.\n if (error instanceof RedirectError) {\n setPending(false);\n deps.completeRouterNavigation?.();\n await navigate(error.redirectUrl, { replace: true });\n return;\n }\n // Server 5xx error — hard-navigate so the server renders the\n // error page as HTML. See design/10-error-handling.md\n // §\"Error Page Rendering for Client Navigation\".\n //\n // Set hard-navigating flag BEFORE setting window.location.href:\n // 1. Prevents Navigation API from intercepting → infinite loop\n // 2. Causes NavigationRoot to throw unresolvedThenable → prevents\n // React from rendering children during page teardown (avoids\n // \"Rendered more hooks\" crashes). See TIM-626.\n if (error instanceof ServerErrorResponse) {\n setHardNavigating(true);\n window.location.href = error.url;\n return new Promise(() => {}) as never;\n }\n // Abort errors are not application errors — swallow silently.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n // Clear the abort controller so we don't abort a completed navigation\n // when the next one starts. In dev mode, the RSC body stream stays\n // open after data arrives (React's Flight client waits for debug rows).\n // Aborting a \"completed\" navigation kills the open stream reader →\n // \"BodyStreamBuffer was aborted\". By clearing the controller here,\n // createNavAbort() becomes a no-op for completed navigations.\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n }\n setPending(false);\n // Resolve the Navigation API deferred — clears the browser's native\n // loading state (tab spinner) at the same time as the TopLoader.\n deps.completeRouterNavigation?.();\n }\n }\n\n async function refresh(): Promise<void> {\n const currentUrl = deps.getCurrentUrl();\n const navAbort = createNavAbort();\n\n setPending(true, currentUrl);\n\n try {\n const headElements = await renderViaTransition(currentUrl, async () => {\n // No state tree sent — server renders the complete RSC payload\n const result = await fetchRscPayload(\n currentUrl,\n deps,\n undefined,\n undefined,\n navAbort.signal\n );\n // History push handled by renderViaTransition (stores merged payload)\n updateSegmentCache(result.segmentInfo);\n const navState = updateNavigationState(result.params, currentUrl);\n return { ...result, navState };\n });\n\n applyHead(headElements);\n } catch (error) {\n // Stale transition (superseded by a newer navigation) or aborted\n // fetch — silently ignore. See TIM-629.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n }\n setPending(false);\n deps.completeRouterNavigation?.();\n }\n }\n\n async function handlePopState(\n url: string,\n scrollY: number = 0,\n externalSignal?: AbortSignal\n ): Promise<void> {\n // Scroll position is read from history.state by the caller (browser-entry.ts)\n // and passed in. This is more reliable than tracking scroll per-URL in memory\n // because the browser maintains per-entry state even with duplicate URLs.\n const entry = historyStack.get(url);\n\n if (entry && entry.payload !== null) {\n // Replay cached payload — no server roundtrip\n const navState = updateNavigationState(entry.params, url);\n renderPayload(entry.payload, navState);\n applyHead(entry.headElements);\n restoreScrollAfterPaint(scrollY);\n } else {\n // No cached payload — fetch from server.\n // This happens when navigating back to the initial SSR'd page\n // (its payload is null since it was rendered via SSR, not RSC fetch)\n // or when the entry doesn't exist at all.\n const navAbort = createNavAbort(externalSignal);\n setPending(true, url);\n try {\n const headElements = await renderViaTransition(url, async () => {\n const stateTree = segmentCache.serializeStateTree(\n segmentElementCache.getMergeablePaths()\n );\n const result = await fetchRscPayload(url, deps, stateTree, undefined, navAbort.signal);\n updateSegmentCache(result.segmentInfo);\n const navState = updateNavigationState(result.params, url);\n // History push handled by renderViaTransition (stores merged payload)\n return { ...result, navState };\n });\n\n applyHead(headElements);\n restoreScrollAfterPaint(scrollY);\n } catch (error) {\n // Stale transition (superseded by a newer navigation) or aborted\n // fetch — silently ignore. See TIM-629.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n if (currentNavAbort === navAbort) {\n currentNavAbort = null;\n }\n setPending(false);\n }\n }\n }\n\n /**\n * Prefetch an RSC payload for a URL and store it in the prefetch cache.\n * Called on hover of <Link prefetch> elements.\n */\n function prefetch(url: string): void {\n // Don't prefetch if already cached\n if (prefetchCache.get(url) !== undefined) return;\n if (historyStack.has(url)) return;\n\n // Fire-and-forget fetch\n const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());\n void fetchRscPayload(url, deps, stateTree).then(\n (result) => {\n prefetchCache.set(url, result);\n },\n () => {\n // Prefetch failure is non-fatal — navigation will fetch fresh\n }\n );\n }\n\n return {\n navigate,\n refresh,\n handlePopState,\n isPending: () => routerPhase.phase === 'navigating',\n getPendingUrl: () => (routerPhase.phase === 'navigating' ? routerPhase.targetUrl : null),\n onPendingChange(listener) {\n pendingListeners.add(listener);\n return () => pendingListeners.delete(listener);\n },\n prefetch,\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void {\n // Render the piggybacked element tree from a server action response.\n // Updates the current history entry with the fresh payload and applies\n // head elements — same as refresh() but without a server fetch.\n // Cache segment elements for future partial merges.\n const currentUrl = deps.getCurrentUrl();\n const merged = mergeAndCachePayload(element, null);\n historyStack.push(currentUrl, {\n payload: merged,\n headElements,\n });\n // Revalidation doesn't change params/pathname — preserve current state.\n // DO NOT call updateNavigationState(null, ...) here: that normalizes\n // params to {}, clearing dynamic route params on every action response.\n const navState = getNavigationState();\n renderPayload(merged, navState);\n applyHead(headElements);\n },\n initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),\n cacheElementTree: (element: unknown) => cacheSegmentElements(element, segmentElementCache),\n segmentCache,\n prefetchCache,\n historyStack,\n };\n}\n","/**\n * useSearchParams() — client-side hook for reading URL search params.\n *\n * Returns a read-only URLSearchParams instance reflecting the current\n * URL's query string. Updates when client-side navigation changes the URL.\n *\n * This is a thin wrapper over window.location.search, provided for\n * Next.js API compatibility (libraries like nuqs import useSearchParams\n * from next/navigation).\n *\n * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard\n * URLSearchParams. Mutation methods (set, delete, append) work on the\n * local copy but do NOT affect the URL — use the router or nuqs for that.\n *\n * During SSR, reads the request search params from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';\n\nfunction getSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction getServerSearch(): string {\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction subscribe(callback: () => void): () => void {\n window.addEventListener('popstate', callback);\n return () => window.removeEventListener('popstate', callback);\n}\n\n// Cache the last search string and its parsed URLSearchParams to avoid\n// creating a new object on every render when the URL hasn't changed.\n// State lives in client/state.ts for singleton guarantees.\n\nfunction getSearchParams(): URLSearchParams {\n const search = getSearch();\n if (search !== cachedSearch) {\n const params = new URLSearchParams(search);\n _setCachedSearch(search, params);\n return params;\n }\n return cachedSearchParams;\n}\n\nfunction getServerSearchParams(): URLSearchParams {\n const data = getSsrData();\n return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();\n}\n\n/**\n * Read the current URL search params.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\nexport function useSearchParams(): URLSearchParams {\n // useSyncExternalStore needs a primitive snapshot for comparison.\n // We use the raw search string as the snapshot, then return the\n // parsed URLSearchParams.\n useSyncExternalStore(subscribe, getSearch, getServerSearch);\n return typeof window !== 'undefined' ? getSearchParams() : getServerSearchParams();\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc-fetch.d.ts","sourceRoot":"","sources":["../../src/client/rsc-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,uFAAuF;IACvF,WAAW,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAClC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IACjD,+EAA+E;IAC/E,eAAe,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAClC;AAID,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAoCnD,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE7D;AAED,oCAAoC;AACpC,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAErD;AAID,sEAAsE;AACtE,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C,kDAAkD;AAClD,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAE7D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAE7D;AAID,wBAAgB,eAAe,CAC7B,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,EAC7C,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBxB;
|
|
1
|
+
{"version":3,"file":"rsc-fetch.d.ts","sourceRoot":"","sources":["../../src/client/rsc-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,uFAAuF;IACvF,WAAW,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAClC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IACjD,+EAA+E;IAC/E,eAAe,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAClC;AAID,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAoCnD,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE7D;AAED,oCAAoC;AACpC,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAErD;AAID,sEAAsE;AACtE,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C,kDAAkD;AAClD,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAE7D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAE7D;AAID,wBAAgB,eAAe,CAC7B,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,EAC7C,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBxB;AAeD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,GAAG,IAAI,CAS5E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,GAAG,IAAI,CAS3E;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,EAAE,GAAG,IAAI,CAU1E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAS1F;AAID;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBACjB,GAAG,EAAE,MAAM;CAIxB;AAED;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;;CAI1C;AAED;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBACT,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;CAKxC;AAID;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,UAAU,EAChB,SAAS,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,EAClC,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAqEtB"}
|
package/dist/client/state.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ALL mutable module-level state that must have singleton semantics across
|
|
5
5
|
* the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,
|
|
6
|
-
* use-params.ts, use-search-params.ts, unload-guard.ts) import from this file
|
|
6
|
+
* use-segment-params.ts, use-search-params.ts, unload-guard.ts) import from this file
|
|
7
7
|
* and re-export thin wrapper functions.
|
|
8
8
|
*
|
|
9
9
|
* Why: In Vite dev, a module is instantiated separately if reached via different
|
|
@@ -22,6 +22,14 @@ export interface ClientCookieOptions {
|
|
|
22
22
|
secure?: boolean;
|
|
23
23
|
}
|
|
24
24
|
export type CookieSetter = (value: string, options?: ClientCookieOptions) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Notify useCookie subscribers that a cookie value changed.
|
|
27
|
+
* Called by defineCookie's imperative client .set()/.delete() methods
|
|
28
|
+
* so mounted useCookie() consumers re-render.
|
|
29
|
+
*
|
|
30
|
+
* @internal — framework use only
|
|
31
|
+
*/
|
|
32
|
+
export declare function notifyCookieChange(name: string): void;
|
|
25
33
|
/**
|
|
26
34
|
* Reactive hook for reading/writing a client-side cookie.
|
|
27
35
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cookie.d.ts","sourceRoot":"","sources":["../../src/client/use-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"use-cookie.d.ts","sourceRoot":"","sources":["../../src/client/use-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,yCAAyC;IACzC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACrC,yDAAyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAwClF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAErD;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,mBAAmB,GACnC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC,CAoDhF"}
|
|
@@ -91,4 +91,4 @@ export declare function useSegmentParams<R extends keyof Routes>(route: R): Rout
|
|
|
91
91
|
segmentParams: infer P;
|
|
92
92
|
} ? P : Record<string, string | string[]>;
|
|
93
93
|
export declare function useSegmentParams(route?: string): Record<string, string | string[]>;
|
|
94
|
-
//# sourceMappingURL=use-params.d.ts.map
|
|
94
|
+
//# sourceMappingURL=use-segment-params.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-segment-params.d.ts","sourceRoot":"","sources":["../../src/client/use-segment-params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAS1C;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAG1D;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAE/D;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAEhF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,MAAM,EACrD,KAAK,EAAE,CAAC,GACP,MAAM,CAAC,CAAC,CAAC,SAAS;IAAE,aAAa,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;AACxF,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC"}
|
package/dist/codec.d.ts
CHANGED
|
@@ -28,6 +28,6 @@ export interface Codec<T> {
|
|
|
28
28
|
export type JsonSerializable = string | number | boolean | null | JsonSerializable[] | {
|
|
29
29
|
[key: string]: JsonSerializable;
|
|
30
30
|
};
|
|
31
|
-
export {
|
|
31
|
+
export { fromArraySchema, validateSync, isStandardSchema, isCodec } from './schema-bridge.js';
|
|
32
32
|
export type { StandardSchemaV1, StandardSchemaResult } from './schema-bridge.js';
|
|
33
33
|
//# sourceMappingURL=codec.d.ts.map
|
package/dist/codec.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;GAKG;AACH,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB,yEAAyE;IACzE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,CAAC,CAAC;IAC/C,uDAAuD;IACvD,SAAS,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,gBAAgB,EAAE,GAClB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;GAKG;AACH,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB,yEAAyE;IACzE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,CAAC,CAAC;IAC/C,uDAAuD;IACvD,SAAS,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,gBAAgB,EAAE,GAClB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAAA;CAAE,CAAC;AAMxC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC9F,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/codec.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as validateSync, i as isStandardSchema,
|
|
2
|
-
export { fromArraySchema,
|
|
1
|
+
import { a as validateSync, i as isStandardSchema, r as isCodec, t as fromArraySchema } from "./_chunks/schema-bridge-Cxu4l-7p.js";
|
|
2
|
+
export { fromArraySchema, isCodec, isStandardSchema, validateSync };
|
package/dist/config-types.d.ts
CHANGED
|
@@ -58,6 +58,34 @@ export interface TimberUserConfig {
|
|
|
58
58
|
* See design/08-forms-and-actions.md §"Validation errors" and
|
|
59
59
|
* design/13-security.md §"Sensitive field stripping".
|
|
60
60
|
*/
|
|
61
|
+
/**
|
|
62
|
+
* Server action runtime behavior.
|
|
63
|
+
*
|
|
64
|
+
* See design/08-forms-and-actions.md §"Middleware for Server Actions".
|
|
65
|
+
*/
|
|
66
|
+
actions?: {
|
|
67
|
+
/**
|
|
68
|
+
* Run `middleware.ts` on server action requests before dispatching.
|
|
69
|
+
*
|
|
70
|
+
* **Default: `true`** — middleware runs on every action POST so
|
|
71
|
+
* authentication, rate limiting, tenant isolation, IP allow-listing,
|
|
72
|
+
* and request-header injection apply uniformly to page renders, route
|
|
73
|
+
* handlers, and server actions. This closes the auth-bypass class of
|
|
74
|
+
* issue identified by Next.js CVE-2025-29927: developers can put a
|
|
75
|
+
* single auth check in `middleware.ts` and trust that it gates every
|
|
76
|
+
* unsafe-method request.
|
|
77
|
+
*
|
|
78
|
+
* Set to `false` to restore the legacy behavior where actions skip
|
|
79
|
+
* middleware entirely. This is **not recommended** outside of niche
|
|
80
|
+
* cases (e.g. middleware that rewrites POST bodies and would corrupt
|
|
81
|
+
* action submissions). When false, you are responsible for placing
|
|
82
|
+
* auth, rate limiting, and other cross-cutting checks inside every
|
|
83
|
+
* action — typically via `createActionClient({ middleware: ... })`.
|
|
84
|
+
*
|
|
85
|
+
* See TIM-871.
|
|
86
|
+
*/
|
|
87
|
+
runMiddleware?: boolean;
|
|
88
|
+
};
|
|
61
89
|
forms?: {
|
|
62
90
|
/**
|
|
63
91
|
* Strip sensitive fields (passwords, tokens, CVV, SSN, etc.) from the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-types.d.ts","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config-types.d.ts","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"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"}
|