@timber-js/app 0.2.0-alpha.207 → 0.2.0-alpha.209
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/{primitives-DLAnvsrk.js → actions-BerlqoXA.js} +640 -410
- package/dist/_chunks/actions-BerlqoXA.js.map +1 -0
- package/dist/_chunks/{als-registry-C6kcfprT.js → als-registry-DaxkVjt5.js} +6 -2
- package/dist/_chunks/als-registry-DaxkVjt5.js.map +1 -0
- package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/{build-output-helper-9dqCv1pZ.js → build-output-helper-RV_BRcF1.js} +50 -53
- package/dist/_chunks/build-output-helper-RV_BRcF1.js.map +1 -0
- package/dist/_chunks/{cache-api-CUD9Ezaq.js → cache-api-CR23J_NC.js} +64 -32
- package/dist/_chunks/cache-api-CR23J_NC.js.map +1 -0
- package/dist/_chunks/{chains-B4bgOl2g.js → chains-CpFg56UB.js} +2 -2
- package/dist/_chunks/{chains-B4bgOl2g.js.map → chains-CpFg56UB.js.map} +1 -1
- package/dist/_chunks/{cli-check-G8o30ldg.js → cli-check-C6Ev6wBO.js} +11 -52
- package/dist/_chunks/cli-check-C6Ev6wBO.js.map +1 -0
- package/dist/_chunks/cli-context-By7k2xVo.js +70 -0
- package/dist/_chunks/cli-context-By7k2xVo.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js → cli-schema-sync-CbT2AUUI.js} +3 -3
- package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js.map → cli-schema-sync-CbT2AUUI.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-Byxl8FWP.js} +96 -53
- package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-Byxl8FWP.js.map} +1 -1
- package/dist/_chunks/{convention-lint-5KzoMg_A.js → convention-lint-jKTwKwPe.js} +21 -6
- package/dist/_chunks/convention-lint-jKTwKwPe.js.map +1 -0
- package/dist/_chunks/{dev-server-BzBxgYAV.js → dev-server-C4WZdB7L.js} +15 -167
- package/dist/_chunks/dev-server-C4WZdB7L.js.map +1 -0
- package/dist/_chunks/{error-boundary-BndF-3Td.js → error-boundary-tA7kVfs4.js} +84 -104
- package/dist/_chunks/error-boundary-tA7kVfs4.js.map +1 -0
- package/dist/_chunks/{fs-identity-D7vb6GdI.js → fs-identity-BRKuL215.js} +3 -4
- package/dist/_chunks/fs-identity-BRKuL215.js.map +1 -0
- package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-CYiUS2aS.js} +4 -5
- package/dist/_chunks/graph-cache-CYiUS2aS.js.map +1 -0
- package/dist/_chunks/{json-lossy-check-ClNvBM_3.js → json-lossy-check-ip0Qi0MT.js} +6 -3
- package/dist/_chunks/json-lossy-check-ip0Qi0MT.js.map +1 -0
- package/dist/_chunks/{live-graph-Bm5WTvcr.js → live-graph-Dv-JJCZw.js} +3 -3
- package/dist/_chunks/{live-graph-Bm5WTvcr.js.map → live-graph-Dv-JJCZw.js.map} +1 -1
- package/dist/_chunks/{logger-k2DQ4EUf.js → logger-DiDt5ppH.js} +192 -51
- package/dist/_chunks/logger-DiDt5ppH.js.map +1 -0
- package/dist/_chunks/{navigation-root-mHSK9psY.js → navigation-root-BQfo1-kG.js} +2 -2
- package/dist/_chunks/{navigation-root-mHSK9psY.js.map → navigation-root-BQfo1-kG.js.map} +1 -1
- package/dist/_chunks/{plugin-context-8idfR3UZ.js → plugin-context-BPBPc3lL.js} +28 -4
- package/dist/_chunks/plugin-context-BPBPc3lL.js.map +1 -0
- package/dist/_chunks/{poison-scan-DG5eI2_z.js → poison-scan-CpeT6_OJ.js} +2 -2
- package/dist/_chunks/{poison-scan-DG5eI2_z.js.map → poison-scan-CpeT6_OJ.js.map} +1 -1
- package/dist/_chunks/render-timeout-DOf_BBcO.js +46 -0
- package/dist/_chunks/render-timeout-DOf_BBcO.js.map +1 -0
- package/dist/_chunks/{rsc-payload-path-B_LBodc2.js → rsc-payload-path-dffcs4jp.js} +9 -3
- package/dist/_chunks/{rsc-payload-path-B_LBodc2.js.map → rsc-payload-path-dffcs4jp.js.map} +1 -1
- package/dist/_chunks/{scanner-BQ5sPIJ7.js → scanner-Bw0oq1HB.js} +2 -2
- package/dist/_chunks/{scanner-BQ5sPIJ7.js.map → scanner-Bw0oq1HB.js.map} +1 -1
- package/dist/_chunks/{navigation-context-D0TU0Jog.js → segment-context-xtPUGfdq.js} +36 -34
- package/dist/_chunks/segment-context-xtPUGfdq.js.map +1 -0
- package/dist/_chunks/{singleflight-2lUWfcAk.js → singleflight-BeVMMKi0.js} +4 -3
- package/dist/_chunks/{singleflight-2lUWfcAk.js.map → singleflight-BeVMMKi0.js.map} +1 -1
- package/dist/_chunks/{sizeof-lXffQ1Vr.js → sizeof-UwzwB1uM.js} +3 -2
- package/dist/_chunks/sizeof-UwzwB1uM.js.map +1 -0
- package/dist/_chunks/ssr-data-nA3I70_n.js +49 -0
- package/dist/_chunks/ssr-data-nA3I70_n.js.map +1 -0
- package/dist/_chunks/{state-FippDgxN.js → state-BhNGPsdi.js} +6 -8
- package/dist/_chunks/state-BhNGPsdi.js.map +1 -0
- package/dist/_chunks/{status-page-marker-DwQBrLBz.js → status-page-marker-gaihi0KZ.js} +140 -28
- package/dist/_chunks/status-page-marker-gaihi0KZ.js.map +1 -0
- package/dist/_chunks/{walkers-CN2N_bA-.js → walkers-BXExhzzk.js} +2 -2
- package/dist/_chunks/{walkers-CN2N_bA-.js.map → walkers-BXExhzzk.js.map} +1 -1
- package/dist/_chunks/write-file-atomic-D1qWqHb4.js +32 -0
- package/dist/_chunks/write-file-atomic-D1qWqHb4.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +7 -7
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/canonicalize-module.d.ts +8 -0
- package/dist/adapters/canonicalize-module.d.ts.map +1 -0
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +2 -2
- package/dist/adapters/cloudflare.d.ts +30 -8
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -2
- package/dist/adapters/compress-module.d.ts +8 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/fs-identity.d.ts +2 -3
- package/dist/adapters/fs-identity.d.ts.map +1 -1
- package/dist/adapters/nitro-presets.d.ts +0 -6
- package/dist/adapters/nitro-presets.d.ts.map +1 -1
- package/dist/adapters/nitro-preview.d.ts +43 -11
- package/dist/adapters/nitro-preview.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -13
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +291 -176
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +7 -2
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +105 -21
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/types.js +0 -0
- package/dist/analyze/crawl-entry.js +3 -3
- package/dist/analyze/graph-cache.d.ts.map +1 -1
- package/dist/analyze/graph-command.js +4 -4
- package/dist/cache/index.js +2 -2
- package/dist/cache/json-lossy-check.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/sizeof.d.ts.map +1 -1
- package/dist/cache/stores/memory.js +1 -1
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.js +1 -1
- package/dist/cli-check.d.ts +7 -37
- package/dist/cli-check.d.ts.map +1 -1
- package/dist/cli-context.d.ts +54 -0
- package/dist/cli-context.d.ts.map +1 -0
- package/dist/cli.d.ts +9 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -24
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-dev.d.ts +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +19 -29
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +7 -7
- package/dist/client/browser-entry/post-hydration.d.ts +6 -9
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +6 -11
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +4 -3
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/error-reconstituter.d.ts +3 -9
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/history.d.ts +20 -11
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -6
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +3 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +95 -108
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-commit.d.ts +20 -7
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +3 -5
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/react-root.d.ts +3 -2
- package/dist/client/react-root.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +3 -8
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +45 -39
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router-url.d.ts +24 -0
- package/dist/client/router-url.d.ts.map +1 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +8 -4
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +44 -4
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +18 -64
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +2 -5
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts +1 -1
- package/dist/config-types.d.ts +2 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +17 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +1 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts +17 -2
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/dev-tools/overlay.d.ts +6 -27
- package/dist/dev-tools/overlay.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +632 -373
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +26 -40
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +15 -2
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +19 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/post-build.d.ts +110 -0
- package/dist/plugins/post-build.d.ts.map +1 -0
- package/dist/plugins/prebuilt-capture.d.ts +11 -4
- package/dist/plugins/prebuilt-capture.d.ts.map +1 -1
- package/dist/plugins/prebuilt.d.ts +17 -1
- package/dist/plugins/prebuilt.d.ts.map +1 -1
- package/dist/plugins/prerender-sugar.d.ts.map +1 -1
- package/dist/plugins/react-resolve.d.ts +49 -0
- package/dist/plugins/react-resolve.d.ts.map +1 -0
- package/dist/plugins/rsc-assets-manifest.d.ts +33 -0
- package/dist/plugins/rsc-assets-manifest.d.ts.map +1 -0
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +32 -9
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +0 -9
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/segment-codegen.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +5 -4
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +46 -7
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +58 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +2 -23
- package/dist/server/cookie-context.d.ts.map +1 -1
- package/dist/server/csp-nonce.d.ts +0 -9
- package/dist/server/csp-nonce.d.ts.map +1 -1
- package/dist/server/csrf.d.ts +27 -4
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/debug.d.ts +0 -1
- package/dist/server/debug.d.ts.map +1 -1
- package/dist/server/default-logger.d.ts +2 -1
- package/dist/server/default-logger.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +12 -6
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +7 -3
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +9 -2
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +4 -4
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +10 -3
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/internal.d.ts +1 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +961 -581
- package/dist/server/internal.js.map +1 -1
- package/dist/server/json-status-response.d.ts +34 -0
- package/dist/server/json-status-response.d.ts.map +1 -0
- package/dist/server/logger.d.ts +26 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +11 -6
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +95 -7
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +11 -0
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts +11 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +2 -2
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +23 -35
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt/capture-render.d.ts +25 -0
- package/dist/server/prebuilt/capture-render.d.ts.map +1 -0
- package/dist/server/prebuilt/capture-routes.d.ts +37 -0
- package/dist/server/prebuilt/capture-routes.d.ts.map +1 -0
- package/dist/server/prebuilt/capture-state.d.ts +79 -35
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
- package/dist/server/prebuilt/lifetime.d.ts +38 -0
- package/dist/server/prebuilt/lifetime.d.ts.map +1 -0
- package/dist/server/prebuilt/slots.d.ts +7 -0
- package/dist/server/prebuilt/slots.d.ts.map +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/prebuilt-runtime.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +6 -81
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/react-cache-bridge.d.ts +59 -0
- package/dist/server/react-cache-bridge.d.ts.map +1 -0
- package/dist/server/react-cache-scope.d.ts +49 -0
- package/dist/server/react-cache-scope.d.ts.map +1 -0
- package/dist/server/render-timeout.d.ts +15 -0
- package/dist/server/render-timeout.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +9 -0
- package/dist/server/render-utils.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +26 -5
- 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-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +7 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +22 -22
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts +7 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts +32 -6
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +11 -18
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +5 -15
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +5 -6
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-signals.d.ts +65 -0
- package/dist/server/rsc-entry/render-signals.d.ts.map +1 -0
- package/dist/server/rsc-entry/revalidate-renderer.d.ts +6 -16
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +7 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +8 -53
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts +2 -2
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +9 -4
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/signal-identity.d.ts +43 -0
- package/dist/server/signal-identity.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +4 -3
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +91 -53
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +8 -5
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +26 -40
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +1 -1
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts +12 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +12 -16
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/version-skew.d.ts +3 -3
- package/dist/shared/csrf-reject.d.ts +16 -0
- package/dist/shared/csrf-reject.d.ts.map +1 -0
- package/dist/shared/error-digest.d.ts +28 -0
- package/dist/shared/error-digest.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +11 -10
- package/dist/shared/payload-root.d.ts.map +1 -1
- package/dist/shared/rsc-payload-path.d.ts +6 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -1
- package/dist/utils/write-file-atomic.d.ts +17 -0
- package/dist/utils/write-file-atomic.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +17 -14
- package/docs/api/32-api-cache.mdx +51 -23
- package/docs/api/34-api-config.mdx +34 -6
- package/docs/api/36-cli.mdx +11 -1
- package/docs/learn/03-fetching-data.mdx +17 -1
- package/docs/learn/04-access-control.mdx +1 -1
- package/docs/learn/10-caching.mdx +29 -11
- package/docs/learn/11-middleware.mdx +8 -2
- package/docs/learn/12-error-handling.mdx +6 -1
- package/package.json +7 -3
- package/src/adapters/build-output-helper.ts +55 -53
- package/src/adapters/canonicalize-module.ts +29 -0
- package/src/adapters/cloudflare.ts +105 -63
- package/src/adapters/compress-module.ts +16 -5
- package/src/adapters/fs-identity.ts +2 -3
- package/src/adapters/nitro-presets.ts +0 -15
- package/src/adapters/nitro-preview.ts +272 -136
- package/src/adapters/nitro.ts +119 -82
- package/src/adapters/shared.ts +23 -14
- package/src/adapters/types.ts +110 -21
- package/src/analyze/graph-cache.ts +3 -4
- package/src/cache/json-lossy-check.ts +3 -9
- package/src/cache/singleflight.ts +5 -2
- package/src/cache/sizeof.ts +3 -1
- package/src/cache/tag-aware-handler.ts +43 -26
- package/src/cli-check.ts +8 -98
- package/src/cli-context.ts +119 -0
- package/src/cli.ts +19 -28
- package/src/client/browser-dev.ts +1 -1
- package/src/client/browser-entry/action-dispatch.ts +26 -4
- package/src/client/browser-entry/hydrate.ts +53 -58
- package/src/client/browser-entry/index.ts +16 -27
- package/src/client/browser-entry/post-hydration.ts +6 -40
- package/src/client/browser-entry/router-init.ts +66 -84
- package/src/client/error-boundary.tsx +9 -41
- package/src/client/error-reconstituter.tsx +10 -5
- package/src/client/history.ts +35 -21
- package/src/client/index.ts +0 -1
- package/src/client/internal.ts +2 -7
- package/src/client/navigation-commit.ts +41 -18
- package/src/client/navigation-context.ts +3 -39
- package/src/client/react-root.ts +3 -2
- package/src/client/router-pipeline.ts +96 -132
- package/src/client/router-types.ts +46 -44
- package/src/client/router-url.ts +32 -0
- package/src/client/router.ts +62 -21
- package/src/client/rsc-fetch.ts +117 -155
- package/src/client/segment-cache.ts +110 -11
- package/src/client/ssr-data.ts +19 -80
- package/src/client/state.ts +2 -9
- package/src/client/use-pathname.ts +1 -1
- package/src/config-types.ts +2 -1
- package/src/config-validation.ts +2 -1
- package/src/dev-tools/dev-page-shell.ts +18 -0
- package/src/dev-tools/index.ts +0 -2
- package/src/dev-tools/instrumentation.ts +50 -2
- package/src/dev-tools/overlay.ts +8 -238
- package/src/index.ts +26 -14
- package/src/plugin-context.ts +39 -40
- package/src/plugins/build-manifest.ts +4 -11
- package/src/plugins/build-report.ts +70 -70
- package/src/plugins/dev-server.ts +24 -44
- package/src/plugins/entries.ts +2 -1
- package/src/plugins/fonts.ts +35 -7
- package/src/plugins/post-build.ts +327 -0
- package/src/plugins/prebuilt-capture.ts +28 -32
- package/src/plugins/prebuilt.ts +54 -39
- package/src/plugins/prerender-sugar.ts +0 -4
- package/src/plugins/react-resolve.ts +149 -0
- package/src/plugins/{adapter-build.ts → rsc-assets-manifest.ts} +20 -109
- package/src/plugins/shims.ts +1 -0
- package/src/plugins/static-build.ts +42 -74
- package/src/routing/codegen-write.ts +25 -3
- package/src/routing/segment-codegen.ts +3 -2
- package/src/rsc-runtime/vendor-types.d.ts +8 -0
- package/src/server/access-gate.tsx +2 -1
- package/src/server/action-client.ts +1 -1
- package/src/server/action-handler.ts +253 -110
- package/src/server/actions.ts +73 -25
- package/src/server/als-registry.ts +73 -5
- package/src/server/chain-url-parts.ts +1 -1
- package/src/server/children-interception.ts +2 -1
- package/src/server/cookie-context.ts +18 -48
- package/src/server/csp-nonce.ts +0 -12
- package/src/server/csrf.ts +140 -8
- package/src/server/debug.ts +0 -1
- package/src/server/default-logger.ts +2 -1
- package/src/server/deny-boundary.ts +25 -13
- package/src/server/deny-renderer.ts +30 -97
- package/src/server/error-boundary-wrapper.ts +7 -3
- package/src/server/fallback-error.ts +26 -11
- package/src/server/flight-scripts.ts +1 -1
- package/src/server/head-response.ts +4 -4
- package/src/server/html-injector-core.ts +2 -2
- package/src/server/html-injectors.ts +2 -4
- package/src/server/index.ts +0 -4
- package/src/server/instrumentation.ts +10 -3
- package/src/server/internal.ts +1 -6
- package/src/server/json-status-response.ts +105 -0
- package/src/server/logger.ts +39 -3
- package/src/server/param-coercion.ts +15 -12
- package/src/server/pipeline-helpers.ts +257 -12
- package/src/server/pipeline-interception.ts +133 -0
- package/src/server/pipeline-outcome.ts +103 -31
- package/src/server/pipeline-phases.ts +127 -237
- package/src/server/pipeline.ts +204 -207
- package/src/server/prebuilt/capture-render.ts +118 -0
- package/src/server/prebuilt/capture-routes.ts +104 -0
- package/src/server/prebuilt/capture-state.ts +155 -48
- package/src/server/prebuilt/lifetime.ts +151 -0
- package/src/server/prebuilt/slots.ts +14 -2
- package/src/server/prebuilt/synthetic-store.ts +3 -0
- package/src/server/prebuilt-builder.ts +30 -320
- package/src/server/prebuilt-runtime.ts +125 -129
- package/src/server/primitives.ts +8 -168
- package/src/server/react-cache-bridge.ts +124 -0
- package/src/server/react-cache-scope.ts +104 -0
- package/src/server/render-timeout.ts +20 -0
- package/src/server/render-utils.ts +11 -1
- package/src/server/request-context.ts +36 -9
- package/src/server/route-element-builder.ts +11 -6
- package/src/server/route-handler.ts +36 -23
- package/src/server/route-matcher.ts +15 -1
- package/src/server/rsc-entry/action-middleware-runner.ts +79 -123
- package/src/server/rsc-entry/api-handler.ts +27 -103
- package/src/server/rsc-entry/deny-fallback.ts +93 -26
- package/src/server/rsc-entry/error-renderer.ts +39 -91
- package/src/server/rsc-entry/helpers.ts +2 -4
- package/src/server/rsc-entry/index.ts +64 -58
- package/src/server/rsc-entry/render-route.ts +15 -80
- package/src/server/rsc-entry/render-signals.ts +108 -0
- package/src/server/rsc-entry/revalidate-renderer.ts +56 -57
- package/src/server/rsc-entry/rsc-payload.ts +28 -61
- package/src/server/rsc-entry/rsc-stream.ts +113 -213
- package/src/server/rsc-entry/ssr-bridge.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +182 -263
- package/src/server/rsc-entry/wrap-action-dispatch.ts +12 -14
- package/src/server/signal-identity.ts +112 -0
- package/src/server/slot-resolver.ts +18 -18
- package/src/server/slot-subtree-contain.ts +2 -1
- package/src/server/ssr-bridge-types.ts +92 -56
- package/src/server/ssr-entry.ts +87 -111
- package/src/server/ssr-render.ts +55 -64
- package/src/server/ssr-wrappers.tsx +2 -2
- package/src/server/static-generator.ts +7 -42
- package/src/server/static-not-found.ts +3 -3
- package/src/server/stream-utils.ts +51 -0
- package/src/server/tracing.ts +58 -75
- package/src/server/types.ts +1 -1
- package/src/server/version-skew.ts +5 -5
- package/src/shared/csrf-reject.ts +15 -0
- package/src/shared/error-digest.ts +46 -0
- package/src/shared/payload-root.ts +11 -10
- package/src/shared/rsc-payload-path.ts +8 -1
- package/src/utils/write-file-atomic.ts +32 -0
- package/dist/_chunks/actions-jNrXdzgs.js +0 -346
- package/dist/_chunks/actions-jNrXdzgs.js.map +0 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +0 -1
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
- package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +0 -1
- package/dist/_chunks/cache-api-CUD9Ezaq.js.map +0 -1
- package/dist/_chunks/cli-check-G8o30ldg.js.map +0 -1
- package/dist/_chunks/convention-lint-5KzoMg_A.js.map +0 -1
- package/dist/_chunks/dev-server-BzBxgYAV.js.map +0 -1
- package/dist/_chunks/error-boundary-BndF-3Td.js.map +0 -1
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +0 -1
- package/dist/_chunks/graph-cache-CP4GEmf9.js.map +0 -1
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +0 -1
- package/dist/_chunks/logger-k2DQ4EUf.js.map +0 -1
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +0 -1
- package/dist/_chunks/plugin-context-8idfR3UZ.js.map +0 -1
- package/dist/_chunks/primitives-DLAnvsrk.js.map +0 -1
- package/dist/_chunks/segment-context-D9_89u34.js +0 -34
- package/dist/_chunks/segment-context-D9_89u34.js.map +0 -1
- package/dist/_chunks/sizeof-lXffQ1Vr.js.map +0 -1
- package/dist/_chunks/ssr-data-D6T6Y3ef.js +0 -60
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +0 -1
- package/dist/_chunks/state-FippDgxN.js.map +0 -1
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +0 -1
- package/dist/client/types.d.ts +0 -14
- package/dist/client/types.d.ts.map +0 -1
- package/dist/plugins/adapter-build.d.ts +0 -38
- package/dist/plugins/adapter-build.d.ts.map +0 -1
- package/dist/server/asset-headers.d.ts +0 -32
- package/dist/server/asset-headers.d.ts.map +0 -1
- package/dist/server/dev-source-map.d.ts +0 -22
- package/dist/server/dev-source-map.d.ts.map +0 -1
- package/src/client/types.ts +0 -21
- package/src/server/asset-headers.ts +0 -59
- package/src/server/dev-source-map.ts +0 -31
package/dist/server/internal.js
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
|
+
import { r as RenderTimeoutError, t as DEFAULT_RENDER_TIMEOUT_MS } from "../_chunks/render-timeout-DOf_BBcO.js";
|
|
1
2
|
import { n as isRscContentType, r as isRscRequest } from "../_chunks/rsc-media-type-DDc7duTD.js";
|
|
2
|
-
import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-C6kcfprT.js";
|
|
3
|
-
import { D as getWaitUntil, S as getCookie, T as isDebug, _ as markResponseFlushed, b as setMutableCookieContext, f as applyRequestHeaderOverlay, g as getSegmentParams, h as getSearchParams, n as RedirectSignal, o as isDenySignal, p as getHeader, r as RenderError, s as isRedirectSignal, t as DenySignal, v as runWithRequestContext, w as getSetCookieHeaders, x as setSegmentParams, y as setMatchedSegmentPath } from "../_chunks/primitives-DLAnvsrk.js";
|
|
4
|
-
import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-ClUiXQnK.js";
|
|
5
3
|
import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-DSDjM_hJ.js";
|
|
6
4
|
import { r as canonicalize } from "../_chunks/canonicalize-CgHoscYO.js";
|
|
7
5
|
import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
|
|
8
6
|
import { t as computeSegmentTreePaths } from "../_chunks/segment-keys-D5hu1hz4.js";
|
|
9
|
-
import {
|
|
10
|
-
import { a as
|
|
11
|
-
import { i as
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
7
|
+
import { a as requestContextAls, r as prebuiltCaptureAls, s as timingAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-DaxkVjt5.js";
|
|
8
|
+
import { A as getTraceId, C as isDebug, D as generateTraceId, M as runWithTraceId, N as setSpanAttribute, O as getOtelTraceId, P as withSpan, S as isRedirectSignal, _ as swallow, a as logMiddlewareShortCircuit, b as isControlFlowSignal, c as logRequestCompleted, d as logRouteSetupError, f as logSlowRequest, g as setLogger, h as logWaitUntilUnsupported, i as logMiddlewareError, j as replaceTraceId, l as logRequestReceived, m as logWaitUntilRejected, n as logCacheMiss, o as logProxyError, p as logSwrRefetchFailed, s as logRenderError, t as getLogger, u as logRouteError, x as isDenySignal } from "../_chunks/logger-DiDt5ppH.js";
|
|
9
|
+
import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
|
|
10
|
+
import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-ClUiXQnK.js";
|
|
11
|
+
import { C as setMutableCookieContext, S as setMatchedSegmentPath, T as runOutsideReactCacheScope, b as markResponseFlushed, f as getCookie, g as getHeader, h as applyRequestHeaderOverlay, i as coerce, m as getSetCookieHeaders, o as DenySignal, s as RedirectSignal, t as executeAction, v as getSearchParams, w as setSegmentParams, x as runWithRequestContext, y as getSegmentParams } from "../_chunks/actions-BerlqoXA.js";
|
|
12
|
+
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as getWaitUntil, y as lastInvalidationEpochFor } from "../_chunks/cache-api-CR23J_NC.js";
|
|
13
|
+
import { n as createSingleflight } from "../_chunks/singleflight-BeVMMKi0.js";
|
|
14
14
|
import { n as toBracketKey } from "../_chunks/resolve-schema-CBR6Lm4i.js";
|
|
15
15
|
import { t as normalizeParamValue } from "../_chunks/param-value-C8TNYchQ.js";
|
|
16
|
-
import {
|
|
16
|
+
import { l as TIMBER_ERROR_HEADER, t as StatusPageMarker } from "../_chunks/status-page-marker-gaihi0KZ.js";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
19
19
|
import React, { createElement, useContext } from "react";
|
|
20
|
+
//#region src/server/render-utils.ts
|
|
21
|
+
/**
|
|
22
|
+
* Check if an error is an abort error (connection closed by client).
|
|
23
|
+
*
|
|
24
|
+
* When the browser aborts a request (page refresh, navigation away),
|
|
25
|
+
* the AbortSignal fires and React/streams throw an AbortError. This
|
|
26
|
+
* is not an application error — suppress it from error boundaries and logs.
|
|
27
|
+
*/
|
|
28
|
+
function isAbortError(error) {
|
|
29
|
+
if (error instanceof DOMException && error.name === "AbortError") return true;
|
|
30
|
+
if (error instanceof Error && error.name === "AbortError") return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
20
34
|
//#region src/server/server-timing.ts
|
|
21
35
|
/**
|
|
22
36
|
* Server-Timing header — dev-mode timing breakdowns for Chrome DevTools.
|
|
@@ -171,6 +185,52 @@ function wrapResponseWithStreamTiming(body, startTime) {
|
|
|
171
185
|
}
|
|
172
186
|
});
|
|
173
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Read a byte stream to completion under one overall deadline and return
|
|
190
|
+
* the concatenated bytes. Every read is raced against the time remaining,
|
|
191
|
+
* and the reader is cancelled on every exit, so a stalled upstream never
|
|
192
|
+
* outlives the deadline. See design/02-rendering-pipeline.md §"Always
|
|
193
|
+
* Timeout-Guard Stream Reads".
|
|
194
|
+
*
|
|
195
|
+
* @param label - Prefix for the timeout error message. Both ways the deadline
|
|
196
|
+
* can pass (between reads, or while a read is pending) reject with the same
|
|
197
|
+
* `${label} did not complete within ${timeoutMs}ms`.
|
|
198
|
+
*/
|
|
199
|
+
async function readAllBytesWithDeadline(stream, timeoutMs, label) {
|
|
200
|
+
const reader = stream.getReader();
|
|
201
|
+
const chunks = [];
|
|
202
|
+
let totalLength = 0;
|
|
203
|
+
const deadline = Date.now() + timeoutMs;
|
|
204
|
+
const deadlineError = () => /* @__PURE__ */ new Error(`${label} did not complete within ${timeoutMs}ms`);
|
|
205
|
+
try {
|
|
206
|
+
for (;;) {
|
|
207
|
+
const remaining = deadline - Date.now();
|
|
208
|
+
if (remaining <= 0) throw deadlineError();
|
|
209
|
+
let timer;
|
|
210
|
+
const timeout = new Promise((_, reject) => {
|
|
211
|
+
timer = setTimeout(() => reject(deadlineError()), remaining);
|
|
212
|
+
});
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = await Promise.race([reader.read(), timeout]);
|
|
216
|
+
} finally {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
}
|
|
219
|
+
if (result.done) break;
|
|
220
|
+
chunks.push(result.value);
|
|
221
|
+
totalLength += result.value.byteLength;
|
|
222
|
+
}
|
|
223
|
+
} finally {
|
|
224
|
+
reader.cancel().catch(() => {});
|
|
225
|
+
}
|
|
226
|
+
const bytes = new Uint8Array(totalLength);
|
|
227
|
+
let offset = 0;
|
|
228
|
+
for (const chunk of chunks) {
|
|
229
|
+
bytes.set(chunk, offset);
|
|
230
|
+
offset += chunk.byteLength;
|
|
231
|
+
}
|
|
232
|
+
return bytes;
|
|
233
|
+
}
|
|
174
234
|
//#endregion
|
|
175
235
|
//#region src/server/instrumentation.ts
|
|
176
236
|
/**
|
|
@@ -234,6 +294,63 @@ function hasOnRequestError() {
|
|
|
234
294
|
return _onRequestError !== null;
|
|
235
295
|
}
|
|
236
296
|
//#endregion
|
|
297
|
+
//#region src/server/safe-load.ts
|
|
298
|
+
/**
|
|
299
|
+
* Custom error class for module load failures.
|
|
300
|
+
*
|
|
301
|
+
* Preserves the original error as `cause` while providing a
|
|
302
|
+
* human-readable message with the file path.
|
|
303
|
+
*/
|
|
304
|
+
var ModuleLoadError = class extends Error {
|
|
305
|
+
/** The file path that failed to load. */
|
|
306
|
+
filePath;
|
|
307
|
+
constructor(filePath, cause) {
|
|
308
|
+
const originalMessage = cause instanceof Error ? cause.message : String(cause);
|
|
309
|
+
super(`[timber] Failed to load module ${filePath}\n ${originalMessage}`, { cause });
|
|
310
|
+
this.name = "ModuleLoadError";
|
|
311
|
+
this.filePath = filePath;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Load a route manifest module with enriched error context.
|
|
316
|
+
*
|
|
317
|
+
* On success: returns the module object (same as `loader.load()`).
|
|
318
|
+
* On failure: throws `ModuleLoadError` with file path and original cause.
|
|
319
|
+
*
|
|
320
|
+
* For error rendering paths that need fallthrough instead of throwing,
|
|
321
|
+
* callers should catch at the call site:
|
|
322
|
+
*
|
|
323
|
+
* ```ts
|
|
324
|
+
* // Throwing (default) — route-element-builder, api-handler, etc.
|
|
325
|
+
* const mod = await loadModule(segment.page);
|
|
326
|
+
*
|
|
327
|
+
* // Fallthrough — error-renderer, error-boundary-wrapper
|
|
328
|
+
* const mod = await loadModule(segment.error).catch(() => null);
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
async function loadModule(loader) {
|
|
332
|
+
try {
|
|
333
|
+
return await loader.load();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw new ModuleLoadError(loader.filePath, error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/server/route-matcher.ts
|
|
340
|
+
/**
|
|
341
|
+
* Whether a matched chain renders as an API route: its leaf has a
|
|
342
|
+
* `route.ts` and no `page.tsx`. The same test `renderRoute` dispatches on,
|
|
343
|
+
* so every answer about a route's format — early hints, deny format, error
|
|
344
|
+
* format, error reporting — agrees with what actually ran.
|
|
345
|
+
*/
|
|
346
|
+
function isApiRouteChain(segments) {
|
|
347
|
+
const leaf = segments[segments.length - 1];
|
|
348
|
+
return !!leaf?.route && !leaf.page;
|
|
349
|
+
}
|
|
350
|
+
var SERVE_PATH_TO_CONVENTION = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const [baseName, convention] of Object.entries(METADATA_ROUTE_CONVENTIONS)) if (convention.serveExtension) for (const ext of [...convention.staticExtensions, convention.serveExtension]) SERVE_PATH_TO_CONVENTION.set(`${convention.servePath}.${ext}`, baseName);
|
|
352
|
+
else SERVE_PATH_TO_CONVENTION.set(convention.servePath, baseName);
|
|
353
|
+
//#endregion
|
|
237
354
|
//#region src/server/pipeline-helpers.ts
|
|
238
355
|
/**
|
|
239
356
|
* Build a proxy resolver closure from the declared source. Called exactly
|
|
@@ -269,6 +386,30 @@ function applyCookieJar(headers) {
|
|
|
269
386
|
for (const value of getSetCookieHeaders()) headers.append("Set-Cookie", value);
|
|
270
387
|
}
|
|
271
388
|
/**
|
|
389
|
+
* Bring a bag the jar was already applied to up to date with the jar.
|
|
390
|
+
*
|
|
391
|
+
* `applied` is what `getSetCookieHeaders()` returned when the jar was
|
|
392
|
+
* applied to `headers`. Values the jar no longer holds (a later write to the
|
|
393
|
+
* same name replaced them) are removed, and values it gained are appended.
|
|
394
|
+
* Every cookie then appears once, with its last write (design/29-cookies.md
|
|
395
|
+
* §"Last-Write-Wins for Same Name"). `Set-Cookie` values that did not come
|
|
396
|
+
* from the jar, such as a middleware appending to `ctx.headers` directly,
|
|
397
|
+
* are left alone.
|
|
398
|
+
*/
|
|
399
|
+
function syncCookieJar(headers, applied) {
|
|
400
|
+
const current = getSetCookieHeaders();
|
|
401
|
+
const stale = applied.filter((value) => !current.includes(value));
|
|
402
|
+
const added = current.filter((value) => !applied.includes(value));
|
|
403
|
+
if (stale.length === 0 && added.length === 0) return;
|
|
404
|
+
const kept = headers.getSetCookie();
|
|
405
|
+
for (const value of stale) {
|
|
406
|
+
const index = kept.indexOf(value);
|
|
407
|
+
if (index !== -1) kept.splice(index, 1);
|
|
408
|
+
}
|
|
409
|
+
headers.delete("Set-Cookie");
|
|
410
|
+
for (const value of [...kept, ...added]) headers.append("Set-Cookie", value);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
272
413
|
* Merge framework-managed response headers onto a terminal response without
|
|
273
414
|
* overwriting headers the terminal response already set itself.
|
|
274
415
|
*/
|
|
@@ -302,15 +443,31 @@ function cloneWithMutableHeaders(response) {
|
|
|
302
443
|
});
|
|
303
444
|
}
|
|
304
445
|
/**
|
|
446
|
+
* A fresh response header bag for a page the pipeline renders, seeded with
|
|
447
|
+
* the default dynamic-page cache policy.
|
|
448
|
+
*
|
|
449
|
+
* Without a Cache-Control header, CDNs (particularly Cloudflare) may attempt
|
|
450
|
+
* to buffer/process the response differently, causing intermittent
|
|
451
|
+
* multi-second delays. This matches Next.js's default behavior. Every bag a
|
|
452
|
+
* rendered page can be written onto starts here — a matched route, the
|
|
453
|
+
* no-match 404, and a deny page for a proxy-phase signal — so none of them
|
|
454
|
+
* leaves without a cache policy. Middleware and route handlers may replace it.
|
|
455
|
+
*/
|
|
456
|
+
function dynamicResponseHeaders() {
|
|
457
|
+
return new Headers({ "Cache-Control": "private, no-cache, no-store, max-age=0, must-revalidate" });
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
305
460
|
* Build a redirect Response from a RedirectSignal.
|
|
306
461
|
*
|
|
307
|
-
* For RSC payload
|
|
308
|
-
* so the client router can perform a soft SPA redirect. A
|
|
309
|
-
* turned into an opaque redirect by
|
|
310
|
-
* createFromFetch. See
|
|
462
|
+
* For a client navigation (an RSC payload request for a page), returns 204 +
|
|
463
|
+
* X-Timber-Redirect so the client router can perform a soft SPA redirect. A
|
|
464
|
+
* raw 302 would be turned into an opaque redirect by
|
|
465
|
+
* fetch({redirect:'manual'}), crashing createFromFetch. See
|
|
466
|
+
* design/19-client-navigation.md. The caller decides what counts as a
|
|
467
|
+
* navigation: a route.ts match answers with a real 3xx whatever it accepts.
|
|
311
468
|
*/
|
|
312
|
-
function buildRedirectResponse(signal,
|
|
313
|
-
if (
|
|
469
|
+
function buildRedirectResponse(signal, headers, navigation) {
|
|
470
|
+
if (navigation) {
|
|
314
471
|
headers.set("X-Timber-Redirect", signal.location);
|
|
315
472
|
return new Response(null, {
|
|
316
473
|
status: 204,
|
|
@@ -325,25 +482,134 @@ function buildRedirectResponse(signal, req, headers) {
|
|
|
325
482
|
}
|
|
326
483
|
/**
|
|
327
484
|
* Fire the user's onRequestError hook with request context.
|
|
328
|
-
*
|
|
485
|
+
*
|
|
486
|
+
* The one place a pipeline phase becomes the `phase` / `routeType` the hook
|
|
487
|
+
* reports, so every caller agrees. A render-phase error on a route.ts match
|
|
488
|
+
* is phase `'route'`, because the handler is what ran. Middleware reports
|
|
489
|
+
* as `'handler'`, its public name.
|
|
490
|
+
*/
|
|
491
|
+
async function fireOnRequestError(error, req, source, segments) {
|
|
492
|
+
if (!isReportableError(error) || req.signal?.aborted) return;
|
|
493
|
+
if (isRepeatedLoadFailure(error)) return;
|
|
494
|
+
await callHook(error, req, source, segments);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Module evaluations whose failure was already reported. A module that fails
|
|
498
|
+
* to evaluate rethrows the same error on every later import, and
|
|
499
|
+
* `loadModule()` wraps each one in a new `ModuleLoadError` with that error
|
|
500
|
+
* as its `cause`. One broken module is one bug until it changes, however many
|
|
501
|
+
* loads trip over it in a request or across requests: the page's boundaries,
|
|
502
|
+
* then the fallback page, then the next request. A WeakSet, so a module that
|
|
503
|
+
* is re-evaluated (an HMR edit) gets a new cause and is reported again.
|
|
504
|
+
*/
|
|
505
|
+
var reportedLoadFailures = /* @__PURE__ */ new WeakSet();
|
|
506
|
+
/**
|
|
507
|
+
* The same for a module that threw a primitive (`throw 'broken'`), which a
|
|
508
|
+
* WeakSet cannot hold. Keyed on the file and the value instead, so a module
|
|
509
|
+
* re-broken after an edit with the very same value is not reported again.
|
|
510
|
+
* It grows with the number of broken modules, not with requests.
|
|
511
|
+
*/
|
|
512
|
+
var reportedPrimitiveLoadFailures = /* @__PURE__ */ new Set();
|
|
513
|
+
function isRepeatedLoadFailure(error) {
|
|
514
|
+
if (!(error instanceof ModuleLoadError)) return false;
|
|
515
|
+
const cause = error.cause;
|
|
516
|
+
if (typeof cause === "object" && cause !== null) {
|
|
517
|
+
if (reportedLoadFailures.has(cause)) return true;
|
|
518
|
+
reportedLoadFailures.add(cause);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
const key = `${error.filePath}\0${typeof cause}\0${String(cause)}`;
|
|
522
|
+
if (reportedPrimitiveLoadFailures.has(key)) return true;
|
|
523
|
+
reportedPrimitiveLoadFailures.add(key);
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Call the hook with request context, with no filtering. Only
|
|
528
|
+
* `fireOnRequestError()` and background reporting call it: background work
|
|
529
|
+
* runs after the response, so its request having aborted says nothing about
|
|
530
|
+
* the failure.
|
|
329
531
|
*/
|
|
330
|
-
async function
|
|
532
|
+
async function callHook(error, req, source, segments) {
|
|
533
|
+
if (!hasOnRequestError()) return;
|
|
331
534
|
const url = new URL(req.url);
|
|
332
535
|
const headersObj = {};
|
|
333
536
|
req.headers.forEach((v, k) => {
|
|
334
537
|
headersObj[k] = v;
|
|
335
538
|
});
|
|
539
|
+
const apiRoute = source === "route" || segments !== void 0 && isApiRouteChain(segments);
|
|
336
540
|
await callOnRequestError(error, {
|
|
337
541
|
method: req.method,
|
|
338
542
|
path: url.pathname,
|
|
339
543
|
headers: headersObj
|
|
340
544
|
}, {
|
|
341
|
-
phase,
|
|
545
|
+
phase: reportedPhase(source, apiRoute),
|
|
342
546
|
routePath: url.pathname,
|
|
343
|
-
routeType: "page",
|
|
547
|
+
routeType: source === "action" ? "action" : apiRoute ? "route" : "page",
|
|
344
548
|
traceId: getTraceId()
|
|
345
549
|
});
|
|
346
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Whether an error is a failure `onRequestError` should hear about.
|
|
553
|
+
*
|
|
554
|
+
* Control-flow signals are outcomes the code chose — a `deny()` picks the
|
|
555
|
+
* page's status and data — and an abort is the client leaving. Callers answering
|
|
556
|
+
* a live request also skip everything once `req.signal` has aborted: a
|
|
557
|
+
* driver that throws "connection closed" after a disconnect is not a bug.
|
|
558
|
+
*/
|
|
559
|
+
function isReportableError(error) {
|
|
560
|
+
return !isControlFlowSignal(error) && !isAbortError(error);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Fire the hook without making the caller wait, for callers that cannot:
|
|
564
|
+
* Flight's `onError` is synchronous, and background work has no caller.
|
|
565
|
+
* The promise is registered with the request's `waitUntil` so platforms
|
|
566
|
+
* that stop work when the response ends (Workers, Lambda) let the hook
|
|
567
|
+
* finish. The hook's own failures are caught in `callOnRequestError()`.
|
|
568
|
+
*/
|
|
569
|
+
function fireInBackground(report) {
|
|
570
|
+
getWaitUntil()?.(report.catch((hookError) => swallow(hookError, "onRequestError could not be called")));
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Run a `cache.component` stale-while-revalidate re-render as a singleflight
|
|
574
|
+
* body, and log and report its failure once for the flight. The stale
|
|
575
|
+
* payload already answered, so nothing else sees the failure.
|
|
576
|
+
*
|
|
577
|
+
* - **Once per flight.** A `.catch` on the flight's promise would run once
|
|
578
|
+
* per request that joined it, so the report happens inside the body. A
|
|
579
|
+
* body still running when the singleflight times out is reported then,
|
|
580
|
+
* with the timeout error (the signal's reason), and its later rejection
|
|
581
|
+
* is dropped.
|
|
582
|
+
* - **Against the request whose stale read started it**, read from the
|
|
583
|
+
* request context the work runs in. With no request (the build-time
|
|
584
|
+
* capture store) it is only logged.
|
|
585
|
+
* - **As phase `'render'`, route type `'page`.** A `cache.component` is a
|
|
586
|
+
* server component, so the read that started this was a page render (a
|
|
587
|
+
* revalidated page tree included). The request's own abort does not
|
|
588
|
+
* suppress the report: the work runs after the response either way.
|
|
589
|
+
*/
|
|
590
|
+
async function runBackgroundFlight(signal, work, log) {
|
|
591
|
+
const failed = (error) => {
|
|
592
|
+
log(error);
|
|
593
|
+
const request = requestContextAls.getStore()?.request;
|
|
594
|
+
if (request && isReportableError(error)) fireInBackground(callHook(error, request, "render"));
|
|
595
|
+
};
|
|
596
|
+
const onTimeout = () => failed(signal.reason);
|
|
597
|
+
signal.addEventListener("abort", onTimeout, { once: true });
|
|
598
|
+
try {
|
|
599
|
+
await work();
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (!signal.aborted) failed(error);
|
|
602
|
+
} finally {
|
|
603
|
+
signal.removeEventListener("abort", onTimeout);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function reportedPhase(source, apiRoute) {
|
|
607
|
+
switch (source) {
|
|
608
|
+
case "middleware": return "handler";
|
|
609
|
+
case "render": return apiRoute ? "route" : "render";
|
|
610
|
+
default: return source;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
347
613
|
//#endregion
|
|
348
614
|
//#region src/server/proxy.ts
|
|
349
615
|
/**
|
|
@@ -399,6 +665,54 @@ async function runMiddlewareChain(chain, ctx) {
|
|
|
399
665
|
}
|
|
400
666
|
}
|
|
401
667
|
//#endregion
|
|
668
|
+
//#region src/server/csp-nonce.ts
|
|
669
|
+
/**
|
|
670
|
+
* CSP nonce — per-request, generated on demand.
|
|
671
|
+
*
|
|
672
|
+
* timber emits several inline `<script>` tags per HTML response — React's
|
|
673
|
+
* bootstrap script and Fizz runtime, the Flight init/chunk scripts, the
|
|
674
|
+
* segment-tree script, and the post-flush noindex script. Under a strict
|
|
675
|
+
* `Content-Security-Policy` (`script-src 'nonce-…' 'strict-dynamic'`) every
|
|
676
|
+
* one of them needs the request's nonce or the browser refuses to run it.
|
|
677
|
+
*
|
|
678
|
+
* The app opts in by reading `ctx.nonce` in `middleware.ts` and putting it
|
|
679
|
+
* in the CSP header it sets through `ctx.headers`. The first read generates
|
|
680
|
+
* the value and stores it on the request context; the render pipeline reads
|
|
681
|
+
* it back with `getCspNonce()` and stamps it on every inline script. A
|
|
682
|
+
* request whose middleware never reads `ctx.nonce` has no nonce, and its
|
|
683
|
+
* markup is byte-identical to a render without this feature. timber does
|
|
684
|
+
* not parse the CSP header — the app owns the policy, timber owns the
|
|
685
|
+
* markup, and `ctx.nonce` is the one value both are built from.
|
|
686
|
+
*
|
|
687
|
+
* `proxy.ts` cannot participate: it only sees the finished response from
|
|
688
|
+
* `next()`, after the markup exists.
|
|
689
|
+
*
|
|
690
|
+
* See design/13-security.md §"Content Security Policy" (TIM-1414).
|
|
691
|
+
*/
|
|
692
|
+
/**
|
|
693
|
+
* Generate a CSP nonce: 128 random bits, base64. Matches CSP's
|
|
694
|
+
* `base64-value` grammar, so it is valid in the header verbatim and safe
|
|
695
|
+
* inside a double-quoted HTML attribute without escaping.
|
|
696
|
+
*/
|
|
697
|
+
function generateNonce() {
|
|
698
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
699
|
+
let binary = "";
|
|
700
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
701
|
+
return btoa(binary);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* The request's CSP nonce, creating it on first call. Backs the
|
|
705
|
+
* `ctx.nonce` getter on `MiddlewareContext`. Outside a request context
|
|
706
|
+
* (tests, build-time rendering) a fresh value is returned but not
|
|
707
|
+
* retained, since there is no request for it to belong to.
|
|
708
|
+
*/
|
|
709
|
+
function requestCspNonce() {
|
|
710
|
+
const store = requestContextAls.getStore();
|
|
711
|
+
if (!store) return generateNonce();
|
|
712
|
+
store.cspNonce ??= generateNonce();
|
|
713
|
+
return store.cspNonce;
|
|
714
|
+
}
|
|
715
|
+
//#endregion
|
|
402
716
|
//#region src/server/param-coercion.ts
|
|
403
717
|
/**
|
|
404
718
|
* Thrown when an app/schema.ts segment-param codec's parse() fails.
|
|
@@ -413,7 +727,12 @@ var ParamCoercionError = class extends Error {
|
|
|
413
727
|
/** Global schema codecs, set once at startup from virtual:timber-schema. */
|
|
414
728
|
var _globalCodecs = null;
|
|
415
729
|
/**
|
|
416
|
-
*
|
|
730
|
+
* Coerce the matched route's raw segment params and return the result.
|
|
731
|
+
*
|
|
732
|
+
* Reads `match.rawSegmentParams` and never writes to `match`: the caller
|
|
733
|
+
* decides where the coerced params go. Reading the raw record makes the call
|
|
734
|
+
* repeatable — coercing a match twice runs every codec on the URL value both
|
|
735
|
+
* times, never on its own output.
|
|
417
736
|
*
|
|
418
737
|
* Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
|
|
419
738
|
* design/41-global-params.md migration step 9). A route with no schema, or
|
|
@@ -427,16 +746,15 @@ var _globalCodecs = null;
|
|
|
427
746
|
* See design/41-global-params.md §"Pipeline Integration"
|
|
428
747
|
*
|
|
429
748
|
* Stays `async` with nothing to await: it is injected as a
|
|
430
|
-
* `CoerceSegmentParamsFn` into the revalidation renderer
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
749
|
+
* `CoerceSegmentParamsFn` into the revalidation renderer, which awaits
|
|
750
|
+
* it. Making it synchronous would
|
|
751
|
+
* change that contract and remove a microtask boundary the pipeline's
|
|
752
|
+
* ordering has always had, for no gain.
|
|
434
753
|
*/
|
|
435
754
|
async function coerceSegmentParams(match) {
|
|
436
755
|
const globalCodecs = _globalCodecs;
|
|
437
756
|
const mergeTarget = Object.create(null);
|
|
438
|
-
for (const key of Object.keys(match.
|
|
439
|
-
match.segmentParams = mergeTarget;
|
|
757
|
+
for (const key of Object.keys(match.rawSegmentParams)) if (key !== "__proto__") mergeTarget[key] = match.rawSegmentParams[key];
|
|
440
758
|
if (globalCodecs) for (const node of match.segments) {
|
|
441
759
|
const segment = effectiveUrlSegment(node);
|
|
442
760
|
if (!segment.paramName) continue;
|
|
@@ -455,6 +773,7 @@ async function coerceSegmentParams(match) {
|
|
|
455
773
|
}
|
|
456
774
|
mergeTarget[key] = normalizeParamValue(parsed, key);
|
|
457
775
|
}
|
|
776
|
+
return mergeTarget;
|
|
458
777
|
}
|
|
459
778
|
//#endregion
|
|
460
779
|
//#region src/server/pipeline-metadata.ts
|
|
@@ -522,45 +841,48 @@ function escapeXml(str) {
|
|
|
522
841
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
523
842
|
}
|
|
524
843
|
//#endregion
|
|
525
|
-
//#region src/server/
|
|
844
|
+
//#region src/server/state-tree-diff.ts
|
|
526
845
|
/**
|
|
527
|
-
*
|
|
846
|
+
* State Tree Diffing — Server-side parsing and diffing of X-Timber-State-Tree.
|
|
528
847
|
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
|
|
532
|
-
var ModuleLoadError = class extends Error {
|
|
533
|
-
/** The file path that failed to load. */
|
|
534
|
-
filePath;
|
|
535
|
-
constructor(filePath, cause) {
|
|
536
|
-
const originalMessage = cause instanceof Error ? cause.message : String(cause);
|
|
537
|
-
super(`[timber] Failed to load module ${filePath}\n ${originalMessage}`, { cause });
|
|
538
|
-
this.name = "ModuleLoadError";
|
|
539
|
-
this.filePath = filePath;
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
/**
|
|
543
|
-
* Load a route manifest module with enriched error context.
|
|
848
|
+
* The client sends X-Timber-State-Tree on navigation requests, listing
|
|
849
|
+
* the sync segments it has cached. The server diffs this against the
|
|
850
|
+
* target route's segments to skip re-rendering unchanged sync layouts.
|
|
544
851
|
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
852
|
+
* This is a performance optimization only — NOT a security boundary.
|
|
853
|
+
* All access.ts files run regardless of the state tree content.
|
|
854
|
+
* A fabricated state tree can only cause extra rendering work or stale
|
|
855
|
+
* layouts — never auth bypass.
|
|
547
856
|
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
857
|
+
* See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
858
|
+
* See design/13-security.md §"State tree manipulation"
|
|
859
|
+
*/
|
|
860
|
+
/**
|
|
861
|
+
* Parse the X-Timber-State-Tree header from a request.
|
|
550
862
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
863
|
+
* Returns a Set of segment paths the client has cached (with an
|
|
864
|
+
* additional `slots` property for cached slot paths), or null if
|
|
865
|
+
* the header is missing, malformed, or empty. Parsing happens before
|
|
866
|
+
* renderToReadableStream — not inside the React render pass.
|
|
554
867
|
*
|
|
555
|
-
*
|
|
556
|
-
* const mod = await loadModule(segment.error).catch(() => null);
|
|
557
|
-
* ```
|
|
868
|
+
* @returns ClientStateTree with segments and slots, or null if no valid state tree
|
|
558
869
|
*/
|
|
559
|
-
|
|
870
|
+
function parseClientStateTree(req) {
|
|
871
|
+
const header = req.headers.get("X-Timber-State-Tree");
|
|
872
|
+
if (!header) return null;
|
|
560
873
|
try {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
874
|
+
const parsed = JSON.parse(header);
|
|
875
|
+
const hasSegments = Array.isArray(parsed.segments) && parsed.segments.length > 0;
|
|
876
|
+
const hasSlots = Array.isArray(parsed.slots) && parsed.slots.length > 0;
|
|
877
|
+
const hasTreePaths = Array.isArray(parsed.treePaths) && parsed.treePaths.length > 0;
|
|
878
|
+
if (!hasSegments && !hasSlots && !hasTreePaths) return null;
|
|
879
|
+
const result = new Set(hasSegments ? parsed.segments : []);
|
|
880
|
+
result.slots = hasSlots ? new Set(parsed.slots) : null;
|
|
881
|
+
result.treePaths = hasTreePaths ? new Set(parsed.treePaths) : null;
|
|
882
|
+
return result;
|
|
883
|
+
} catch (err) {
|
|
884
|
+
swallow(err, "malformed X-Timber-State-Tree header");
|
|
885
|
+
return null;
|
|
564
886
|
}
|
|
565
887
|
}
|
|
566
888
|
//#endregion
|
|
@@ -679,50 +1001,62 @@ function pathnameMatchesPattern(pathname, pattern) {
|
|
|
679
1001
|
}
|
|
680
1002
|
return pi === pathParts.length;
|
|
681
1003
|
}
|
|
682
|
-
//#endregion
|
|
683
|
-
//#region src/server/state-tree-diff.ts
|
|
684
1004
|
/**
|
|
685
|
-
*
|
|
1005
|
+
* Resolve which route renders a soft navigation that an intercepting route
|
|
1006
|
+
* may claim. Returns the canonical match unchanged when no rewrite applies.
|
|
686
1007
|
*
|
|
687
|
-
*
|
|
688
|
-
* the sync segments it has cached. The server diffs this against the
|
|
689
|
-
* target route's segments to skip re-rendering unchanged sync layouts.
|
|
690
|
-
*
|
|
691
|
-
* This is a performance optimization only — NOT a security boundary.
|
|
692
|
-
* All access.ts files run regardless of the state tree content.
|
|
693
|
-
* A fabricated state tree can only cause extra rendering work or stale
|
|
694
|
-
* layouts — never auth bypass.
|
|
695
|
-
*
|
|
696
|
-
* See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
697
|
-
* See design/13-security.md §"State tree manipulation"
|
|
1008
|
+
* Runs during route setup, so anything it throws is a `'setup'` outcome.
|
|
698
1009
|
*/
|
|
1010
|
+
function resolveInterceptedRoute(config, req, canonicalPathname, canonicalMatch) {
|
|
1011
|
+
const stripTrailingSlash = config.stripTrailingSlash ?? true;
|
|
1012
|
+
let match = canonicalMatch;
|
|
1013
|
+
let interception;
|
|
1014
|
+
if (isRscRequest(req) && config.interceptionRewrites?.length) {
|
|
1015
|
+
const rawSourceUrl = req.headers.get("X-Timber-URL");
|
|
1016
|
+
const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
|
|
1017
|
+
if (validatedSourceUrl) {
|
|
1018
|
+
const resolveSourceScopes = () => {
|
|
1019
|
+
const mounted = new Set(parseClientStateTree(req)?.treePaths ?? []);
|
|
1020
|
+
const sourceRoute = config.matchRoute(validatedSourceUrl);
|
|
1021
|
+
const scopes = [mounted, new Set(sourceRoute ? computeSegmentTreePaths(sourceRoute.segments) : [])].filter((scope) => scope.size > 0);
|
|
1022
|
+
return scopes.length > 0 ? scopes : null;
|
|
1023
|
+
};
|
|
1024
|
+
const rewrite = findInterceptionMatch(canonicalPathname, resolveSourceScopes, config.interceptionRewrites);
|
|
1025
|
+
if (rewrite?.mode === "children") {
|
|
1026
|
+
const interceptedMatch = config.matchInterceptedRoute?.(canonicalPathname, rewrite.interceptingScope);
|
|
1027
|
+
if (interceptedMatch) match = interceptedMatch;
|
|
1028
|
+
} else if (rewrite) {
|
|
1029
|
+
const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
|
|
1030
|
+
if (sourceMatch !== null && computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope)) {
|
|
1031
|
+
match = sourceMatch;
|
|
1032
|
+
interception = { targetPathname: canonicalPathname };
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
match,
|
|
1039
|
+
interception
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
699
1042
|
/**
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* Returns a Set of segment paths the client has cached (with an
|
|
703
|
-
* additional `slots` property for cached slot paths), or null if
|
|
704
|
-
* the header is missing, malformed, or empty. Parsing happens before
|
|
705
|
-
* renderToReadableStream — not inside the React render pass.
|
|
1043
|
+
* Validate and canonicalize the X-Timber-URL header value.
|
|
706
1044
|
*
|
|
707
|
-
*
|
|
1045
|
+
* Returns the canonical pathname if valid, or null if rejected:
|
|
1046
|
+
* - Must start with '/' (relative pathname, no scheme/authority)
|
|
1047
|
+
* - Must not contain control characters
|
|
1048
|
+
* - Must pass canonicalization (no encoded separators, null bytes, etc.)
|
|
708
1049
|
*/
|
|
709
|
-
function
|
|
710
|
-
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
const hasSlots = Array.isArray(parsed.slots) && parsed.slots.length > 0;
|
|
716
|
-
const hasTreePaths = Array.isArray(parsed.treePaths) && parsed.treePaths.length > 0;
|
|
717
|
-
if (!hasSegments && !hasSlots && !hasTreePaths) return null;
|
|
718
|
-
const result = new Set(hasSegments ? parsed.segments : []);
|
|
719
|
-
result.slots = hasSlots ? new Set(parsed.slots) : null;
|
|
720
|
-
result.treePaths = hasTreePaths ? new Set(parsed.treePaths) : null;
|
|
721
|
-
return result;
|
|
722
|
-
} catch (err) {
|
|
723
|
-
swallow(err, "malformed X-Timber-State-Tree header");
|
|
724
|
-
return null;
|
|
1050
|
+
function validateInterceptionHeader(raw, stripTrailingSlash) {
|
|
1051
|
+
if (!raw.startsWith("/")) return null;
|
|
1052
|
+
if (raw.startsWith("//")) return null;
|
|
1053
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1054
|
+
const code = raw.charCodeAt(i);
|
|
1055
|
+
if (code <= 31 || code === 127) return null;
|
|
725
1056
|
}
|
|
1057
|
+
const result = canonicalize(raw, stripTrailingSlash);
|
|
1058
|
+
if (!result.ok) return null;
|
|
1059
|
+
return result.pathname;
|
|
726
1060
|
}
|
|
727
1061
|
//#endregion
|
|
728
1062
|
//#region src/server/response-cache-policy.ts
|
|
@@ -838,13 +1172,78 @@ function rscErrorResponse(isRsc, status, headers) {
|
|
|
838
1172
|
* `proxy.ts` that awaits `next()` and then throws, this is the only place
|
|
839
1173
|
* the cookies downstream middleware set can reach the wire
|
|
840
1174
|
* (design/29-cookies.md §"proxy.ts Cookie Access"). Middleware-phase
|
|
841
|
-
* signals throw before that phase's own application, so they need it too
|
|
1175
|
+
* signals throw before that phase's own application, so they need it too,
|
|
1176
|
+
* and so do setup-phase outcomes, which come before any phase applied it.
|
|
1177
|
+
*
|
|
1178
|
+
* `fresh` creates the bag for an outcome that arrives without one.
|
|
842
1179
|
*/
|
|
843
|
-
function outcomeHeaders(outcome, ctx) {
|
|
844
|
-
const headers = ctx.responseHeaders ??
|
|
1180
|
+
function outcomeHeaders(outcome, ctx, fresh = () => new Headers()) {
|
|
1181
|
+
const headers = ctx.responseHeaders ?? fresh();
|
|
845
1182
|
if (outcome.phase !== "render") applyCookieJar(headers);
|
|
846
1183
|
return headers;
|
|
847
1184
|
}
|
|
1185
|
+
/** Whether the outcome is for a route.ts match, which answers in its own format. */
|
|
1186
|
+
function isApiRouteMatch(ctx) {
|
|
1187
|
+
return ctx.match !== void 0 && isApiRouteChain(ctx.match.segments);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Report an error outcome once: log it under its phase, fire
|
|
1191
|
+
* `onRequestError`, and hand it to the dev overlay. An error from a
|
|
1192
|
+
* route.ts handler — or anything else in its render phase — logs as a route
|
|
1193
|
+
* error and reports as phase `'route'`.
|
|
1194
|
+
*/
|
|
1195
|
+
async function reportError(config, error, phase, ctx) {
|
|
1196
|
+
const { method, path } = ctx;
|
|
1197
|
+
switch (phase) {
|
|
1198
|
+
case "proxy":
|
|
1199
|
+
logProxyError({ error });
|
|
1200
|
+
break;
|
|
1201
|
+
case "setup":
|
|
1202
|
+
logRouteSetupError({
|
|
1203
|
+
method,
|
|
1204
|
+
path,
|
|
1205
|
+
error
|
|
1206
|
+
});
|
|
1207
|
+
break;
|
|
1208
|
+
case "middleware":
|
|
1209
|
+
logMiddlewareError({
|
|
1210
|
+
method,
|
|
1211
|
+
path,
|
|
1212
|
+
error
|
|
1213
|
+
});
|
|
1214
|
+
break;
|
|
1215
|
+
case "render": if (isApiRouteMatch(ctx)) logRouteError({
|
|
1216
|
+
method,
|
|
1217
|
+
path,
|
|
1218
|
+
error
|
|
1219
|
+
});
|
|
1220
|
+
else logRenderError({
|
|
1221
|
+
method,
|
|
1222
|
+
path,
|
|
1223
|
+
error
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
await fireOnRequestError(error, ctx.req, phase, ctx.match?.segments);
|
|
1227
|
+
if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, phase);
|
|
1228
|
+
}
|
|
1229
|
+
/** The outcome for a value a phase threw: a signal, or an error. */
|
|
1230
|
+
function thrownOutcome(phase, thrown) {
|
|
1231
|
+
if (isRedirectSignal(thrown)) return {
|
|
1232
|
+
kind: "redirect",
|
|
1233
|
+
phase,
|
|
1234
|
+
signal: thrown
|
|
1235
|
+
};
|
|
1236
|
+
if (isDenySignal(thrown)) return {
|
|
1237
|
+
kind: "deny",
|
|
1238
|
+
phase,
|
|
1239
|
+
signal: thrown
|
|
1240
|
+
};
|
|
1241
|
+
return {
|
|
1242
|
+
kind: "error",
|
|
1243
|
+
phase,
|
|
1244
|
+
error: thrown
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
848
1247
|
/**
|
|
849
1248
|
* Terminal outcome handler — converts a `PhaseOutcome` into a final
|
|
850
1249
|
* `Response`, applying cookies, building redirects, rendering deny pages
|
|
@@ -873,10 +1272,11 @@ async function outcomeToResponse(config, outcome, ctx) {
|
|
|
873
1272
|
}
|
|
874
1273
|
case "redirect": {
|
|
875
1274
|
const headers = outcomeHeaders(outcome, ctx);
|
|
876
|
-
|
|
1275
|
+
const navigation = isRscRequest(ctx.req) && !isApiRouteMatch(ctx);
|
|
1276
|
+
return buildRedirectResponse(outcome.signal, headers, navigation);
|
|
877
1277
|
}
|
|
878
1278
|
case "deny": {
|
|
879
|
-
const headers = outcomeHeaders(outcome, ctx);
|
|
1279
|
+
const headers = outcomeHeaders(outcome, ctx, dynamicResponseHeaders);
|
|
880
1280
|
if (config.renderDenyFallback) try {
|
|
881
1281
|
return cloneWithMutableHeaders(await config.renderDenyFallback(outcome.signal, ctx.req, headers, ctx.match));
|
|
882
1282
|
} catch (denyRenderError) {
|
|
@@ -885,51 +1285,31 @@ async function outcomeToResponse(config, outcome, ctx) {
|
|
|
885
1285
|
path: ctx.path,
|
|
886
1286
|
error: denyRenderError
|
|
887
1287
|
});
|
|
888
|
-
await fireOnRequestError(denyRenderError, ctx.req, "render");
|
|
1288
|
+
await fireOnRequestError(denyRenderError, ctx.req, "render", ctx.match?.segments);
|
|
889
1289
|
if (config.onPipelineError && denyRenderError instanceof Error) config.onPipelineError(denyRenderError, "render");
|
|
1290
|
+
if (isRscRequest(ctx.req)) return rscErrorEnvelope(outcome.signal.status, headers);
|
|
890
1291
|
}
|
|
891
1292
|
if (isDebug()) console.warn(`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n Request: ${ctx.method} ${ctx.path}\n Add a status-code file (e.g. 404.tsx) or error.tsx to render a custom deny page.`);
|
|
892
1293
|
return new Response(null, {
|
|
893
1294
|
status: outcome.signal.status,
|
|
894
1295
|
headers
|
|
895
1296
|
});
|
|
896
|
-
}
|
|
897
|
-
case "error": {
|
|
898
|
-
const isRsc = isRscRequest(ctx.req);
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
return rscErrorResponse(isRsc, 500, outcomeHeaders(outcome, ctx));
|
|
904
|
-
}
|
|
905
|
-
if (outcome.phase === "middleware") {
|
|
906
|
-
logMiddlewareError({
|
|
907
|
-
method: ctx.method,
|
|
908
|
-
path: ctx.path,
|
|
909
|
-
error: outcome.error
|
|
910
|
-
});
|
|
911
|
-
await fireOnRequestError(outcome.error, ctx.req, "handler");
|
|
912
|
-
if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "middleware");
|
|
913
|
-
return rscErrorResponse(isRsc, 500, outcomeHeaders(outcome, ctx));
|
|
914
|
-
}
|
|
915
|
-
const headers = outcomeHeaders(outcome, ctx);
|
|
916
|
-
logRenderError({
|
|
917
|
-
method: ctx.method,
|
|
918
|
-
path: ctx.path,
|
|
919
|
-
error: outcome.error
|
|
920
|
-
});
|
|
921
|
-
await fireOnRequestError(outcome.error, ctx.req, "render");
|
|
922
|
-
if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "render");
|
|
923
|
-
if (isRsc) return rscErrorResponse(true, 500, headers);
|
|
1297
|
+
}
|
|
1298
|
+
case "error": {
|
|
1299
|
+
const isRsc = isRscRequest(ctx.req);
|
|
1300
|
+
const headers = outcomeHeaders(outcome, ctx);
|
|
1301
|
+
await reportError(config, outcome.error, outcome.phase, ctx);
|
|
1302
|
+
if (outcome.phase === "proxy" || outcome.phase === "middleware") return rscErrorResponse(isRsc, 500, headers);
|
|
1303
|
+
if (isRsc && !isApiRouteMatch(ctx)) return rscErrorResponse(true, 500, headers);
|
|
924
1304
|
if (config.renderFallbackError) try {
|
|
925
|
-
return cloneWithMutableHeaders(await config.renderFallbackError(outcome.error, ctx.req, headers));
|
|
1305
|
+
return cloneWithMutableHeaders(await config.renderFallbackError(outcome.error, ctx.req, errorResponseHeaders(headers), ctx.match));
|
|
926
1306
|
} catch (fallbackRenderError) {
|
|
927
1307
|
logRenderError({
|
|
928
1308
|
method: ctx.method,
|
|
929
1309
|
path: ctx.path,
|
|
930
1310
|
error: fallbackRenderError
|
|
931
1311
|
});
|
|
932
|
-
await fireOnRequestError(fallbackRenderError, ctx.req, "render");
|
|
1312
|
+
await fireOnRequestError(fallbackRenderError, ctx.req, "render", ctx.match?.segments);
|
|
933
1313
|
if (config.onPipelineError && fallbackRenderError instanceof Error) config.onPipelineError(fallbackRenderError, "render");
|
|
934
1314
|
}
|
|
935
1315
|
return rscErrorResponse(false, 500, headers);
|
|
@@ -952,25 +1332,6 @@ async function outcomeToResponse(config, outcome, ctx) {
|
|
|
952
1332
|
* See design/07-routing.md §"Request Lifecycle", design/02-rendering-pipeline.md §"Request Flow".
|
|
953
1333
|
*/
|
|
954
1334
|
/**
|
|
955
|
-
* Validate and canonicalize the X-Timber-URL header value.
|
|
956
|
-
*
|
|
957
|
-
* Returns the canonical pathname if valid, or null if rejected:
|
|
958
|
-
* - Must start with '/' (relative pathname, no scheme/authority)
|
|
959
|
-
* - Must not contain control characters
|
|
960
|
-
* - Must pass canonicalization (no encoded separators, null bytes, etc.)
|
|
961
|
-
*/
|
|
962
|
-
function validateInterceptionHeader(raw, stripTrailingSlash) {
|
|
963
|
-
if (!raw.startsWith("/")) return null;
|
|
964
|
-
if (raw.startsWith("//")) return null;
|
|
965
|
-
for (let i = 0; i < raw.length; i++) {
|
|
966
|
-
const code = raw.charCodeAt(i);
|
|
967
|
-
if (code <= 31 || code === 127) return null;
|
|
968
|
-
}
|
|
969
|
-
const result = canonicalize(raw, stripTrailingSlash);
|
|
970
|
-
if (!result.ok) return null;
|
|
971
|
-
return result.pathname;
|
|
972
|
-
}
|
|
973
|
-
/**
|
|
974
1335
|
* Run the proxy.ts phase. Calls user proxy code with a `next()` continuation.
|
|
975
1336
|
*
|
|
976
1337
|
* When `next` is provided, it replaces the default `handleRequest` as the
|
|
@@ -991,21 +1352,7 @@ async function runProxyPhase(config, getProxy, req, method, path, next) {
|
|
|
991
1352
|
response: await withSpan("timber.proxy", {}, () => detailed ? withTiming("proxy", "proxy.ts", proxyFn) : proxyFn())
|
|
992
1353
|
};
|
|
993
1354
|
} catch (error) {
|
|
994
|
-
|
|
995
|
-
kind: "redirect",
|
|
996
|
-
phase: "proxy",
|
|
997
|
-
signal: error
|
|
998
|
-
};
|
|
999
|
-
if (isDenySignal(error)) return {
|
|
1000
|
-
kind: "deny",
|
|
1001
|
-
phase: "proxy",
|
|
1002
|
-
signal: error
|
|
1003
|
-
};
|
|
1004
|
-
return {
|
|
1005
|
-
kind: "error",
|
|
1006
|
-
phase: "proxy",
|
|
1007
|
-
error
|
|
1008
|
-
};
|
|
1355
|
+
return thrownOutcome("proxy", error);
|
|
1009
1356
|
}
|
|
1010
1357
|
}
|
|
1011
1358
|
/**
|
|
@@ -1013,8 +1360,9 @@ async function runProxyPhase(config, getProxy, req, method, path, next) {
|
|
|
1013
1360
|
* returns it as a 'response' outcome. Otherwise applies the request header
|
|
1014
1361
|
* overlay and falls through to the render phase.
|
|
1015
1362
|
*/
|
|
1016
|
-
async function runMiddlewarePhase(config, req, match, responseHeaders,
|
|
1363
|
+
async function runMiddlewarePhase(config, req, match, responseHeaders, renderContext) {
|
|
1017
1364
|
const detailed = config.serverTiming === "detailed";
|
|
1365
|
+
const requestHeaderOverlay = new Headers();
|
|
1018
1366
|
const ctx = {
|
|
1019
1367
|
req,
|
|
1020
1368
|
requestHeaders: requestHeaderOverlay,
|
|
@@ -1050,23 +1398,9 @@ async function runMiddlewarePhase(config, req, match, responseHeaders, requestHe
|
|
|
1050
1398
|
response: middlewareResponse
|
|
1051
1399
|
};
|
|
1052
1400
|
applyRequestHeaderOverlay(requestHeaderOverlay);
|
|
1053
|
-
return runRenderPhase(config, req, match, responseHeaders,
|
|
1401
|
+
return runRenderPhase(config, req, match, responseHeaders, renderContext);
|
|
1054
1402
|
} catch (error) {
|
|
1055
|
-
|
|
1056
|
-
kind: "redirect",
|
|
1057
|
-
phase: "middleware",
|
|
1058
|
-
signal: error
|
|
1059
|
-
};
|
|
1060
|
-
if (isDenySignal(error)) return {
|
|
1061
|
-
kind: "deny",
|
|
1062
|
-
phase: "middleware",
|
|
1063
|
-
signal: error
|
|
1064
|
-
};
|
|
1065
|
-
return {
|
|
1066
|
-
kind: "error",
|
|
1067
|
-
phase: "middleware",
|
|
1068
|
-
error
|
|
1069
|
-
};
|
|
1403
|
+
return thrownOutcome("middleware", error);
|
|
1070
1404
|
}
|
|
1071
1405
|
}
|
|
1072
1406
|
/**
|
|
@@ -1084,32 +1418,18 @@ async function runMiddlewarePhase(config, req, match, responseHeaders, requestHe
|
|
|
1084
1418
|
* while handler `Set-Cookie` values still come after them and take
|
|
1085
1419
|
* precedence.
|
|
1086
1420
|
*/
|
|
1087
|
-
async function runRenderPhase(config, req, match, responseHeaders,
|
|
1421
|
+
async function runRenderPhase(config, req, match, responseHeaders, { canonicalPathname, interception }) {
|
|
1088
1422
|
const detailed = config.serverTiming === "detailed";
|
|
1089
1423
|
applyCookieJar(responseHeaders);
|
|
1090
1424
|
try {
|
|
1091
|
-
const renderFn = () => config.render(req, match, responseHeaders,
|
|
1425
|
+
const renderFn = () => config.render(req, match, responseHeaders, interception);
|
|
1092
1426
|
return {
|
|
1093
1427
|
kind: "response",
|
|
1094
1428
|
phase: "render",
|
|
1095
1429
|
response: await withSpan("timber.render", { "http.route": canonicalPathname }, () => detailed ? withTiming("render", "RSC + SSR render", renderFn) : renderFn())
|
|
1096
1430
|
};
|
|
1097
1431
|
} catch (error) {
|
|
1098
|
-
|
|
1099
|
-
kind: "deny",
|
|
1100
|
-
phase: "render",
|
|
1101
|
-
signal: error
|
|
1102
|
-
};
|
|
1103
|
-
if (isRedirectSignal(error)) return {
|
|
1104
|
-
kind: "redirect",
|
|
1105
|
-
phase: "render",
|
|
1106
|
-
signal: error
|
|
1107
|
-
};
|
|
1108
|
-
return {
|
|
1109
|
-
kind: "error",
|
|
1110
|
-
phase: "render",
|
|
1111
|
-
error
|
|
1112
|
-
};
|
|
1432
|
+
return thrownOutcome("render", error);
|
|
1113
1433
|
}
|
|
1114
1434
|
}
|
|
1115
1435
|
/**
|
|
@@ -1175,6 +1495,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
1175
1495
|
path,
|
|
1176
1496
|
error
|
|
1177
1497
|
});
|
|
1498
|
+
await fireOnRequestError(error, req, "route");
|
|
1178
1499
|
if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "metadata-route");
|
|
1179
1500
|
return new Response(null, { status: 500 });
|
|
1180
1501
|
}
|
|
@@ -1188,6 +1509,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
1188
1509
|
path,
|
|
1189
1510
|
error
|
|
1190
1511
|
});
|
|
1512
|
+
await fireOnRequestError(error, req, "route");
|
|
1191
1513
|
if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
|
|
1192
1514
|
return new Response(null, { status: 500 });
|
|
1193
1515
|
}
|
|
@@ -1202,72 +1524,66 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
1202
1524
|
});
|
|
1203
1525
|
}
|
|
1204
1526
|
}
|
|
1205
|
-
|
|
1527
|
+
const responseHeaders = dynamicResponseHeaders();
|
|
1528
|
+
const renderNotFound = async (headers) => {
|
|
1529
|
+
if (!config.renderNoMatch) return new Response(null, { status: 404 });
|
|
1530
|
+
try {
|
|
1531
|
+
return cloneWithMutableHeaders(await config.renderNoMatch(req, headers));
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
return outcomeToResponse(config, thrownOutcome("render", error), {
|
|
1534
|
+
req,
|
|
1535
|
+
method,
|
|
1536
|
+
path,
|
|
1537
|
+
responseHeaders: headers
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
let match = null;
|
|
1206
1542
|
let interception;
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
if (
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1543
|
+
try {
|
|
1544
|
+
match = config.matchRoute(canonicalPathname);
|
|
1545
|
+
({match, interception} = resolveInterceptedRoute(config, req, canonicalPathname, match));
|
|
1546
|
+
if (!match) {
|
|
1547
|
+
if (isDebug()) console.warn(`[timber] No route matched for pathname: ${canonicalPathname}\n Input path: ${path}\n Method: ${method}`);
|
|
1548
|
+
return await renderNotFound(responseHeaders);
|
|
1549
|
+
}
|
|
1550
|
+
if (config.earlyHints && !isRsc && !isApiRouteChain(match.segments)) try {
|
|
1551
|
+
await config.earlyHints(match, req, responseHeaders);
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
swallow(err, "early hints hook threw");
|
|
1554
|
+
}
|
|
1555
|
+
try {
|
|
1556
|
+
match = {
|
|
1557
|
+
...match,
|
|
1558
|
+
segmentParams: await coerceSegmentParams(match)
|
|
1216
1559
|
};
|
|
1217
|
-
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
|
|
1223
|
-
if (sourceMatch !== null && computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope)) {
|
|
1224
|
-
match = sourceMatch;
|
|
1225
|
-
interception = { targetPathname: canonicalPathname };
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
if (error instanceof ParamCoercionError) {
|
|
1562
|
+
if (isDebug()) {
|
|
1563
|
+
const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
|
|
1564
|
+
console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a codec in app/schema.ts rejected the URL value.\n Check the codec for the bracket key of the segment named above.`);
|
|
1226
1565
|
}
|
|
1566
|
+
if (isApiRouteChain(match.segments)) return new Response(null, { status: 404 });
|
|
1567
|
+
return await renderNotFound(responseHeaders);
|
|
1227
1568
|
}
|
|
1569
|
+
throw error;
|
|
1228
1570
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (config.renderNoMatch) {
|
|
1233
|
-
const responseHeaders = new Headers();
|
|
1234
|
-
return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
|
|
1235
|
-
}
|
|
1236
|
-
return new Response(null, { status: 404 });
|
|
1237
|
-
}
|
|
1238
|
-
const responseHeaders = new Headers();
|
|
1239
|
-
const requestHeaderOverlay = new Headers();
|
|
1240
|
-
responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
|
|
1241
|
-
const leafSegment = match.segments[match.segments.length - 1];
|
|
1242
|
-
const isApiRoute = leafSegment?.route && !leafSegment?.page;
|
|
1243
|
-
if (config.earlyHints && !isRsc && !isApiRoute) try {
|
|
1244
|
-
await config.earlyHints(match, req, responseHeaders);
|
|
1245
|
-
} catch (err) {
|
|
1246
|
-
swallow(err, "early hints hook threw");
|
|
1247
|
-
}
|
|
1248
|
-
match.rawSegmentParams = { ...match.segmentParams };
|
|
1249
|
-
try {
|
|
1250
|
-
await coerceSegmentParams(match);
|
|
1571
|
+
setSegmentParams(match.segmentParams);
|
|
1572
|
+
const segmentPath = match.segments.map((s) => s.segmentName).filter(Boolean).join("/") || "/";
|
|
1573
|
+
setMatchedSegmentPath(segmentPath.startsWith("/") ? segmentPath : `/${segmentPath}`);
|
|
1251
1574
|
} catch (error) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
if (config.renderNoMatch) return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
|
|
1260
|
-
return new Response(null, { status: 404 });
|
|
1261
|
-
}
|
|
1262
|
-
throw error;
|
|
1575
|
+
return outcomeToResponse(config, thrownOutcome("setup", error), {
|
|
1576
|
+
req,
|
|
1577
|
+
method,
|
|
1578
|
+
path,
|
|
1579
|
+
responseHeaders,
|
|
1580
|
+
match: match ?? void 0
|
|
1581
|
+
});
|
|
1263
1582
|
}
|
|
1264
|
-
|
|
1265
|
-
const segmentPath = match.segments.map((s) => s.segmentName).filter(Boolean).join("/") || "/";
|
|
1266
|
-
setMatchedSegmentPath(segmentPath.startsWith("/") ? segmentPath : `/${segmentPath}`);
|
|
1267
|
-
return outcomeToResponse(config, match.middlewareChain.length > 0 ? await runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, {
|
|
1583
|
+
return outcomeToResponse(config, match.middlewareChain.length > 0 ? await runMiddlewarePhase(config, req, match, responseHeaders, {
|
|
1268
1584
|
canonicalPathname,
|
|
1269
1585
|
interception
|
|
1270
|
-
}) : await runRenderPhase(config, req, match, responseHeaders,
|
|
1586
|
+
}) : await runRenderPhase(config, req, match, responseHeaders, {
|
|
1271
1587
|
canonicalPathname,
|
|
1272
1588
|
interception
|
|
1273
1589
|
}), {
|
|
@@ -1396,10 +1712,10 @@ async function enforceRscCacheKey(req, response) {
|
|
|
1396
1712
|
*
|
|
1397
1713
|
* The pipeline applies this at its single exit so every response shape is
|
|
1398
1714
|
* covered — page render, API route, metadata route, deny page, error page,
|
|
1399
|
-
* proxy short-circuit. The
|
|
1400
|
-
* standalone
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1715
|
+
* proxy short-circuit. The unit-level producer that is also reachable as a
|
|
1716
|
+
* standalone function (`handleRouteRequest`) calls it too, so it honours HEAD
|
|
1717
|
+
* on its own; it shares this implementation rather than repeating it, and
|
|
1718
|
+
* applying it twice is a no-op.
|
|
1403
1719
|
*/
|
|
1404
1720
|
/**
|
|
1405
1721
|
* Return a bodyless copy of `response` when `method` is HEAD.
|
|
@@ -1441,80 +1757,85 @@ function createPipeline(config) {
|
|
|
1441
1757
|
const slowRequestMs = config.slowRequestMs ?? 3e3;
|
|
1442
1758
|
const serverTiming = config.serverTiming ?? "total";
|
|
1443
1759
|
let activeRequests = 0;
|
|
1444
|
-
const pipelineFn = async (req) => {
|
|
1760
|
+
const pipelineFn = async (req, cookies) => {
|
|
1445
1761
|
const url = new URL(req.url);
|
|
1446
1762
|
const method = req.method;
|
|
1447
1763
|
const path = url.pathname;
|
|
1448
1764
|
const startTime = performance.now();
|
|
1449
|
-
activeRequests++;
|
|
1450
1765
|
const traceIdValue = generateTraceId();
|
|
1451
1766
|
return runWithTraceId(traceIdValue, async () => {
|
|
1452
|
-
|
|
1767
|
+
const inRequestContext = async () => {
|
|
1453
1768
|
const runRequest = async () => {
|
|
1454
1769
|
logRequestReceived({
|
|
1455
1770
|
method,
|
|
1456
1771
|
path
|
|
1457
1772
|
});
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
if (
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const canonicalUrl = new URL(req.url);
|
|
1474
|
-
canonicalUrl.pathname = canonicalPath;
|
|
1475
|
-
canonicalReq = new Request(canonicalUrl.toString(), req);
|
|
1476
|
-
}
|
|
1477
|
-
const innerHandler = async () => {
|
|
1478
|
-
if (config.dispatchAction) {
|
|
1479
|
-
const actionResult = await config.dispatchAction(canonicalReq, canonicalPath, pipelineFn);
|
|
1480
|
-
if (actionResult) return actionResult;
|
|
1773
|
+
activeRequests++;
|
|
1774
|
+
let concurrency = activeRequests;
|
|
1775
|
+
let response;
|
|
1776
|
+
try {
|
|
1777
|
+
response = await withSpan("http.server.request", {
|
|
1778
|
+
"http.request.method": method,
|
|
1779
|
+
"url.path": path
|
|
1780
|
+
}, async () => {
|
|
1781
|
+
const otelTraceId = await getOtelTraceId();
|
|
1782
|
+
if (otelTraceId) replaceTraceId(otelTraceId);
|
|
1783
|
+
const stripTrailingSlash = config.stripTrailingSlash ?? true;
|
|
1784
|
+
const canonResult = canonicalize(url.pathname, stripTrailingSlash);
|
|
1785
|
+
if (!canonResult.ok) {
|
|
1786
|
+
if (isDebug()) console.warn(`[timber] URL canonicalization rejected ${method} ${url.pathname} — responding with ${canonResult.status}\n This usually means the URL contains encoded separators (%2f, %5c),\n null bytes (%00), path traversal (..), or malformed percent-encoding.`);
|
|
1787
|
+
return new Response(null, { status: canonResult.status });
|
|
1481
1788
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
const
|
|
1505
|
-
result.headers.
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1789
|
+
const canonicalPath = canonResult.pathname;
|
|
1790
|
+
let canonicalReq = req;
|
|
1791
|
+
if (url.pathname !== canonicalPath) {
|
|
1792
|
+
const canonicalUrl = new URL(req.url);
|
|
1793
|
+
canonicalUrl.pathname = canonicalPath;
|
|
1794
|
+
canonicalReq = new Request(canonicalUrl.toString(), req);
|
|
1795
|
+
}
|
|
1796
|
+
const innerHandler = async () => {
|
|
1797
|
+
if (config.dispatchAction) {
|
|
1798
|
+
const actionResult = await config.dispatchAction(canonicalReq, canonicalPath, pipelineFn);
|
|
1799
|
+
if (actionResult) return actionResult;
|
|
1800
|
+
}
|
|
1801
|
+
return handleRequest(config, canonicalReq, method, canonicalPath, true);
|
|
1802
|
+
};
|
|
1803
|
+
let result;
|
|
1804
|
+
if (proxyResolver) result = await outcomeToResponse(config, await runProxyPhase(config, proxyResolver, canonicalReq, method, canonicalPath, innerHandler), {
|
|
1805
|
+
req: canonicalReq,
|
|
1806
|
+
method,
|
|
1807
|
+
path: canonicalPath
|
|
1808
|
+
});
|
|
1809
|
+
else result = await innerHandler();
|
|
1810
|
+
await setSpanAttribute("http.response.status_code", result.status);
|
|
1811
|
+
const varyTokens = [...RSC_KEY_HEADERS];
|
|
1812
|
+
const existingVary = result.headers.get("Vary");
|
|
1813
|
+
const existingTokens = existingVary ? existingVary.toLowerCase().split(",").map((t) => t.trim()) : [];
|
|
1814
|
+
const newTokens = varyTokens.filter((t) => !existingTokens.includes(t.toLowerCase()));
|
|
1815
|
+
if (newTokens.length > 0) result.headers.set("Vary", existingVary ? `${existingVary}, ${newTokens.join(", ")}` : newTokens.join(", "));
|
|
1816
|
+
await enforceRscCacheKey(canonicalReq, result);
|
|
1817
|
+
hardenRscPayloadResponse(result);
|
|
1818
|
+
result = stripHeadBody(result, method);
|
|
1819
|
+
if (serverTiming === "detailed") {
|
|
1820
|
+
const timingHeader = getServerTimingHeader();
|
|
1821
|
+
if (timingHeader) result.headers.set("Server-Timing", timingHeader);
|
|
1822
|
+
} else if (serverTiming === "total") {
|
|
1823
|
+
const totalMs = Math.round(performance.now() - startTime);
|
|
1824
|
+
result.headers.set("Server-Timing", `total;dur=${totalMs}`);
|
|
1825
|
+
}
|
|
1826
|
+
if (serverTiming === "detailed" && result.body) result = new Response(wrapResponseWithStreamTiming(result.body, startTime), {
|
|
1827
|
+
status: result.status,
|
|
1828
|
+
statusText: result.statusText,
|
|
1829
|
+
headers: result.headers
|
|
1830
|
+
});
|
|
1831
|
+
return result;
|
|
1511
1832
|
});
|
|
1512
|
-
|
|
1513
|
-
|
|
1833
|
+
} finally {
|
|
1834
|
+
concurrency = activeRequests;
|
|
1835
|
+
activeRequests--;
|
|
1836
|
+
}
|
|
1514
1837
|
const durationMs = Math.round(performance.now() - startTime);
|
|
1515
1838
|
const status = response.status;
|
|
1516
|
-
const concurrency = activeRequests;
|
|
1517
|
-
activeRequests--;
|
|
1518
1839
|
logRequestCompleted({
|
|
1519
1840
|
method,
|
|
1520
1841
|
path,
|
|
@@ -1532,7 +1853,8 @@ function createPipeline(config) {
|
|
|
1532
1853
|
return response;
|
|
1533
1854
|
};
|
|
1534
1855
|
return serverTiming === "detailed" ? runWithTimingCollector(runRequest) : runRequest();
|
|
1535
|
-
}
|
|
1856
|
+
};
|
|
1857
|
+
return runWithRequestContext(req, inRequestContext, { cookies });
|
|
1536
1858
|
});
|
|
1537
1859
|
};
|
|
1538
1860
|
return pipelineFn;
|
|
@@ -1691,7 +2013,7 @@ function sendEarlyHints103(links) {
|
|
|
1691
2013
|
function ErrorReconstituter({ error: serialized, digest, reset, component, status, data }) {
|
|
1692
2014
|
const error = Object.assign(new Error(serialized.message), {
|
|
1693
2015
|
name: serialized.name,
|
|
1694
|
-
|
|
2016
|
+
stack: serialized.stack ?? `${serialized.name}: ${serialized.message}`
|
|
1695
2017
|
});
|
|
1696
2018
|
return createElement(component, {
|
|
1697
2019
|
error,
|
|
@@ -1865,11 +2187,17 @@ function renderDenyPage(entry, status, data, h) {
|
|
|
1865
2187
|
* Otherwise it throws — annotated with `ownerKey` when an ancestor owns the
|
|
1866
2188
|
* file, bare when nothing matched at all.
|
|
1867
2189
|
*
|
|
1868
|
-
* `setDenyStatus` runs on the hoist path too,
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
2190
|
+
* `setDenyStatus` runs on the hoist path too, *before* the throw, for one
|
|
2191
|
+
* case. Normally Flight's `onError` captures the rethrown signal within the
|
|
2192
|
+
* renderer's promotion cap, and the pipeline renders the deny page with
|
|
2193
|
+
* `deny.status` — the recorded status goes unread. When the signal lands
|
|
2194
|
+
* after the cap but within the detection window that follows, the renderer
|
|
2195
|
+
* discards it (never promoted) and streams the document it already has;
|
|
2196
|
+
* `getDenyStatus()`, read once as the renderer returns, gives that document
|
|
2197
|
+
* the deny's 4xx instead of a 200. A deny later than that commits 200. Pinned by
|
|
2198
|
+
* `tests/late-hoisted-deny-status.test.ts`. The RSC payload path never
|
|
2199
|
+
* reads it — a Flight payload response is always 200 and the row itself
|
|
2200
|
+
* carries the deny (TIM-1495).
|
|
1873
2201
|
*
|
|
1874
2202
|
* Either branch leaves part of the matched chain unmounted — hoisting sends
|
|
1875
2203
|
* the render to a boundary that discards everything below it, and a layout
|
|
@@ -1912,20 +2240,6 @@ function setFirstDeniedSegmentIndex(index) {
|
|
|
1912
2240
|
//#endregion
|
|
1913
2241
|
//#region src/server/access-gate.tsx
|
|
1914
2242
|
/**
|
|
1915
|
-
* AccessGate and SlotAccessGate — framework-injected async server components.
|
|
1916
|
-
*
|
|
1917
|
-
* AccessGate wraps each segment's layout in the element tree. It calls the
|
|
1918
|
-
* segment's access.ts before the layout renders. If access.ts calls deny()
|
|
1919
|
-
* or redirect(), the signal propagates as a render-phase throw — caught by
|
|
1920
|
-
* the flush controller to produce the correct HTTP status code.
|
|
1921
|
-
*
|
|
1922
|
-
* SlotAccessGate wraps parallel slot content. On denial, it renders the
|
|
1923
|
-
* graceful degradation chain: denied.tsx → default.tsx → null. Slot denial
|
|
1924
|
-
* does not affect the HTTP status code.
|
|
1925
|
-
*
|
|
1926
|
-
* See design/04-authorization.md and design/02-rendering-pipeline.md §"AccessGate"
|
|
1927
|
-
*/
|
|
1928
|
-
/**
|
|
1929
2243
|
* Framework-injected access gate for segments.
|
|
1930
2244
|
*
|
|
1931
2245
|
* Calls accessFn during render — inside `renderToReadableStream`, sharing
|
|
@@ -2170,87 +2484,6 @@ function resolveSlotDenied(slotNode) {
|
|
|
2170
2484
|
return null;
|
|
2171
2485
|
}
|
|
2172
2486
|
//#endregion
|
|
2173
|
-
//#region src/server/csrf.ts
|
|
2174
|
-
/** HTTP methods that are considered safe (no mutation). */
|
|
2175
|
-
var SAFE_METHODS = /* @__PURE__ */ new Set([
|
|
2176
|
-
"GET",
|
|
2177
|
-
"HEAD",
|
|
2178
|
-
"OPTIONS"
|
|
2179
|
-
]);
|
|
2180
|
-
/**
|
|
2181
|
-
* Derive the request's scheme from trusted sources.
|
|
2182
|
-
*
|
|
2183
|
-
* Priority:
|
|
2184
|
-
* 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
|
|
2185
|
-
* 2. Request URL protocol
|
|
2186
|
-
*
|
|
2187
|
-
* Falls back to 'https' (fail closed) if neither is available.
|
|
2188
|
-
*/
|
|
2189
|
-
function deriveRequestScheme(req) {
|
|
2190
|
-
const forwarded = req.headers.get("X-Forwarded-Proto");
|
|
2191
|
-
if (forwarded) {
|
|
2192
|
-
const proto = forwarded.split(",")[0].trim().toLowerCase();
|
|
2193
|
-
if (proto === "http" || proto === "https") return proto;
|
|
2194
|
-
}
|
|
2195
|
-
try {
|
|
2196
|
-
return new URL(req.url).protocol.replace(":", "");
|
|
2197
|
-
} catch {
|
|
2198
|
-
return "https";
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
/**
|
|
2202
|
-
* Validate the Origin header against the request's full origin
|
|
2203
|
-
* (scheme + host + port).
|
|
2204
|
-
*
|
|
2205
|
-
* For mutation methods (POST, PUT, PATCH, DELETE):
|
|
2206
|
-
* - If `csrf: false`, skip validation.
|
|
2207
|
-
* - If `allowedOrigins` is set, Origin must match one exactly (no wildcards).
|
|
2208
|
-
* - Otherwise, Origin must match the derived request origin.
|
|
2209
|
-
*
|
|
2210
|
-
* Safe methods (GET, HEAD, OPTIONS) always pass.
|
|
2211
|
-
*/
|
|
2212
|
-
function validateCsrf(req, config) {
|
|
2213
|
-
if (SAFE_METHODS.has(req.method)) return { ok: true };
|
|
2214
|
-
if (config.csrf === false) return { ok: true };
|
|
2215
|
-
const origin = req.headers.get("Origin");
|
|
2216
|
-
if (!origin) return {
|
|
2217
|
-
ok: false,
|
|
2218
|
-
status: 403
|
|
2219
|
-
};
|
|
2220
|
-
if (config.allowedOrigins) return config.allowedOrigins.includes(origin) ? { ok: true } : {
|
|
2221
|
-
ok: false,
|
|
2222
|
-
status: 403
|
|
2223
|
-
};
|
|
2224
|
-
const host = req.headers.get("Host");
|
|
2225
|
-
if (!host) return {
|
|
2226
|
-
ok: false,
|
|
2227
|
-
status: 403
|
|
2228
|
-
};
|
|
2229
|
-
let originOrigin;
|
|
2230
|
-
try {
|
|
2231
|
-
originOrigin = new URL(origin).origin;
|
|
2232
|
-
} catch {
|
|
2233
|
-
return {
|
|
2234
|
-
ok: false,
|
|
2235
|
-
status: 403
|
|
2236
|
-
};
|
|
2237
|
-
}
|
|
2238
|
-
const scheme = deriveRequestScheme(req);
|
|
2239
|
-
let expectedOrigin;
|
|
2240
|
-
try {
|
|
2241
|
-
expectedOrigin = new URL(`${scheme}://${host}`).origin;
|
|
2242
|
-
} catch {
|
|
2243
|
-
return {
|
|
2244
|
-
ok: false,
|
|
2245
|
-
status: 403
|
|
2246
|
-
};
|
|
2247
|
-
}
|
|
2248
|
-
return originOrigin === expectedOrigin ? { ok: true } : {
|
|
2249
|
-
ok: false,
|
|
2250
|
-
status: 403
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
//#endregion
|
|
2254
2487
|
//#region src/server/body-limits.ts
|
|
2255
2488
|
var KB = 1024;
|
|
2256
2489
|
var MB = 1024 * KB;
|
|
@@ -3044,37 +3277,50 @@ async function handleRouteRequest(mod, ctx) {
|
|
|
3044
3277
|
const allowHeader = resolveAllowedMethods(mod).join(", ");
|
|
3045
3278
|
if (method === "OPTIONS") {
|
|
3046
3279
|
if (mod.OPTIONS) return runHandler(mod.OPTIONS, ctx);
|
|
3047
|
-
return new Response(null, {
|
|
3280
|
+
return mergeResponseHeaders(new Response(null, {
|
|
3048
3281
|
status: 204,
|
|
3049
3282
|
headers: { Allow: allowHeader }
|
|
3050
|
-
});
|
|
3283
|
+
}), ctx.headers);
|
|
3051
3284
|
}
|
|
3052
3285
|
if (method === "HEAD") {
|
|
3053
3286
|
if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
|
|
3054
3287
|
if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
|
|
3055
3288
|
}
|
|
3056
3289
|
const handler = mod[method];
|
|
3057
|
-
if (!handler) return new Response(null, {
|
|
3290
|
+
if (!handler) return mergeResponseHeaders(new Response(null, {
|
|
3058
3291
|
status: 405,
|
|
3059
3292
|
headers: { Allow: allowHeader }
|
|
3060
|
-
});
|
|
3293
|
+
}), ctx.headers);
|
|
3061
3294
|
return runHandler(handler, ctx);
|
|
3062
3295
|
}
|
|
3063
3296
|
/**
|
|
3064
|
-
* Run a handler
|
|
3297
|
+
* Run a handler with cookie writes enabled and merge ctx.headers into its
|
|
3298
|
+
* response.
|
|
3299
|
+
*
|
|
3300
|
+
* Cookies (design/29-cookies.md §"Where Cookies Can Be Set"): the render
|
|
3301
|
+
* phase applied the jar to `ctx.headers` before the handler ran, so on the
|
|
3302
|
+
* way out only what the handler changed is synced onto that bag. It happens
|
|
3303
|
+
* whether the handler returns or throws, so a cookie set before `redirect()`
|
|
3304
|
+
* rides the redirect, and the pipeline's 500 for a throw carries the same
|
|
3305
|
+
* bag.
|
|
3306
|
+
*
|
|
3307
|
+
* Nothing is caught. A signal goes to the pipeline, whose deny fallback
|
|
3308
|
+
* answers a route.ts match in JSON and whose redirect outcome sends the 3xx
|
|
3309
|
+
* (TIM-1533). Any other error goes to the pipeline, which logs it,
|
|
3310
|
+
* reports it to `onRequestError` as phase `'route'`, and answers it in the
|
|
3311
|
+
* same format as every other route.ts error (TIM-1537).
|
|
3065
3312
|
*/
|
|
3066
3313
|
async function runHandler(handler, ctx) {
|
|
3314
|
+
const applied = getSetCookieHeaders();
|
|
3315
|
+
setMutableCookieContext(true);
|
|
3316
|
+
let res;
|
|
3067
3317
|
try {
|
|
3068
|
-
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
method: ctx.req.method,
|
|
3073
|
-
path: new URL(ctx.req.url).pathname,
|
|
3074
|
-
error
|
|
3075
|
-
});
|
|
3076
|
-
return new Response(null, { status: 500 });
|
|
3318
|
+
res = await handler(ctx);
|
|
3319
|
+
} finally {
|
|
3320
|
+
setMutableCookieContext(false);
|
|
3321
|
+
syncCookieJar(ctx.headers, applied);
|
|
3077
3322
|
}
|
|
3323
|
+
return mergeResponseHeaders(res, ctx.headers);
|
|
3078
3324
|
}
|
|
3079
3325
|
/**
|
|
3080
3326
|
* Merge response headers from ctx.headers into the handler's response.
|
|
@@ -3104,32 +3350,6 @@ function mergeResponseHeaders(res, ctxHeaders) {
|
|
|
3104
3350
|
});
|
|
3105
3351
|
}
|
|
3106
3352
|
//#endregion
|
|
3107
|
-
//#region src/server/render-timeout.ts
|
|
3108
|
-
/**
|
|
3109
|
-
* Render timeout utilities for SSR streaming pipeline.
|
|
3110
|
-
*
|
|
3111
|
-
* Provides a RenderTimeoutError class and a helper to create
|
|
3112
|
-
* timeout-guarded AbortSignals. Used to defend against hung RSC
|
|
3113
|
-
* streams and infinite SSR renders.
|
|
3114
|
-
*
|
|
3115
|
-
* Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
|
|
3116
|
-
*/
|
|
3117
|
-
/**
|
|
3118
|
-
* Error thrown when an SSR render or RSC stream read exceeds the
|
|
3119
|
-
* configured timeout. Callers can check `instanceof RenderTimeoutError`
|
|
3120
|
-
* to distinguish timeout from other errors and return a 504 or close
|
|
3121
|
-
* the connection cleanly.
|
|
3122
|
-
*/
|
|
3123
|
-
var RenderTimeoutError = class extends Error {
|
|
3124
|
-
timeoutMs;
|
|
3125
|
-
constructor(timeoutMs, context) {
|
|
3126
|
-
const message = context ? `Render timeout after ${timeoutMs}ms: ${context}` : `Render timeout after ${timeoutMs}ms`;
|
|
3127
|
-
super(message);
|
|
3128
|
-
this.name = "RenderTimeoutError";
|
|
3129
|
-
this.timeoutMs = timeoutMs;
|
|
3130
|
-
}
|
|
3131
|
-
};
|
|
3132
|
-
//#endregion
|
|
3133
3353
|
//#region src/server/prebuilt/cache-key.ts
|
|
3134
3354
|
/**
|
|
3135
3355
|
* Component cache-key hashing for prebuilt (cache.component) entries.
|
|
@@ -3395,6 +3615,107 @@ function SlotsProvider({ values, children }) {
|
|
|
3395
3615
|
} : values;
|
|
3396
3616
|
return createElement(SlotContext.Provider, { value: merged }, children);
|
|
3397
3617
|
}
|
|
3618
|
+
//#endregion
|
|
3619
|
+
//#region src/server/prebuilt/lifetime.ts
|
|
3620
|
+
/**
|
|
3621
|
+
* Lifetime declarations of a `cache.component` — `prerender`, `ttl`,
|
|
3622
|
+
* `tags` (design/45-cache-lifetimes.md) — and the nesting clamp between
|
|
3623
|
+
* two of them (§Nesting Semantics, TIM-1136).
|
|
3624
|
+
*
|
|
3625
|
+
* The clamp rule lives here once, for both places a capture can embed one
|
|
3626
|
+
* wrapped component in another: the post-build capture pass
|
|
3627
|
+
* (server/prebuilt-builder.ts) and request-time re-renders
|
|
3628
|
+
* (`captureComponentPayload()` in server/prebuilt-runtime.ts, TIM-1545).
|
|
3629
|
+
*/
|
|
3630
|
+
/** Whether the component has a runtime lifetime (ttl or tags). */
|
|
3631
|
+
function hasRuntimeLifetime(options) {
|
|
3632
|
+
return options.ttl !== void 0 || options.tags != null;
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Resolve tags from options (may be static array or function of props).
|
|
3636
|
+
* Function-form tags receive DATA props only — slot props are excluded so
|
|
3637
|
+
* the tags fn never sees SlotOutlet placeholders (capture paths pass
|
|
3638
|
+
* placeholder-substituted props) and tags stay deterministic per key.
|
|
3639
|
+
*/
|
|
3640
|
+
function resolveTags(options, props) {
|
|
3641
|
+
if (!options.tags) return [];
|
|
3642
|
+
if (Array.isArray(options.tags)) return options.tags;
|
|
3643
|
+
const slots = options.slots;
|
|
3644
|
+
const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
|
|
3645
|
+
return options.tags(fnProps);
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Why an embedded copy of `inner` goes stale inside `outer`, or null when
|
|
3649
|
+
* the outer's refreshes cover every refresh channel the inner declares:
|
|
3650
|
+
*
|
|
3651
|
+
* - `shorterTtl`: the inner's ttl is shorter than the outer's effective
|
|
3652
|
+
* lifetime (its ttl, or deploy-long without one).
|
|
3653
|
+
* - `missingTags`: tags the inner resolved to that the outer lacks —
|
|
3654
|
+
* `revalidateTag` on them refreshes the inner's own entries, never the
|
|
3655
|
+
* copy. `null` inner tags (a tags function threw) cannot be proven a
|
|
3656
|
+
* subset, so they count as missing.
|
|
3657
|
+
*
|
|
3658
|
+
* Declaring the inner's tags on the outer is the documented fix, and it
|
|
3659
|
+
* silences the tag half by construction.
|
|
3660
|
+
*/
|
|
3661
|
+
function clampReasons(outer, inner) {
|
|
3662
|
+
const outerTtl = outer.options.ttl ?? Infinity;
|
|
3663
|
+
const shorterTtl = inner.options.ttl !== void 0 && inner.options.ttl < outerTtl;
|
|
3664
|
+
const missingTags = inner.tags === null ? null : inner.tags.filter((t) => !outer.tags.includes(t));
|
|
3665
|
+
if (!shorterTtl && missingTags !== null && missingTags.length === 0) return null;
|
|
3666
|
+
return {
|
|
3667
|
+
shorterTtl,
|
|
3668
|
+
missingTags
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
/** Render a lifetime declaration for the clamp warning. */
|
|
3672
|
+
function describeLifetime(options, tags) {
|
|
3673
|
+
const parts = [];
|
|
3674
|
+
if (options.prerender) parts.push("prerender");
|
|
3675
|
+
if (options.ttl !== void 0) parts.push(`ttl: ${options.ttl}`);
|
|
3676
|
+
if (tags === null) parts.push("tags: fn");
|
|
3677
|
+
else if (tags.length > 0) parts.push(`tags: ${JSON.stringify(tags)}`);
|
|
3678
|
+
return parts.join(", ") || "no lifetime";
|
|
3679
|
+
}
|
|
3680
|
+
/** When the outer entry — and so every copy embedded in it — refreshes. */
|
|
3681
|
+
function describeRefresh(outer) {
|
|
3682
|
+
const parts = [];
|
|
3683
|
+
if (outer.options.ttl !== void 0) parts.push(`every ${outer.options.ttl}s`);
|
|
3684
|
+
if (outer.tags.length > 0) parts.push(`on revalidateTag of its own tags`);
|
|
3685
|
+
parts.push("on redeploy");
|
|
3686
|
+
return parts.join(", or ");
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Warn for each wrapped component embedded in `outer`'s captured payload
|
|
3690
|
+
* whose refresh channels the outer cannot honor. Once per (outer, inner)
|
|
3691
|
+
* pair for the lifetime of `warnedPairs` — the builder passes one per
|
|
3692
|
+
* build, the runtime one per process, since re-renders repeat. The key is
|
|
3693
|
+
* the pair alone, not the reason: resolved tags vary with props, and a key
|
|
3694
|
+
* that included them would grow with the data. So the first clamp found
|
|
3695
|
+
* for a pair is the one reported. Returns the number of warnings emitted.
|
|
3696
|
+
*/
|
|
3697
|
+
function warnNestingClamps(outer, nested, warnedPairs, log) {
|
|
3698
|
+
let count = 0;
|
|
3699
|
+
for (const inner of nested) {
|
|
3700
|
+
const reasons = clampReasons(outer, inner);
|
|
3701
|
+
if (reasons === null) continue;
|
|
3702
|
+
const pairKey = `${outer.id}\0${inner.innerId}`;
|
|
3703
|
+
if (warnedPairs.has(pairKey)) continue;
|
|
3704
|
+
warnedPairs.add(pairKey);
|
|
3705
|
+
count++;
|
|
3706
|
+
const outerLifetime = describeLifetime(outer.options, outer.tags);
|
|
3707
|
+
const innerLifetime = describeLifetime(inner.options, inner.tags);
|
|
3708
|
+
const consequences = [];
|
|
3709
|
+
if (reasons.missingTags === null) consequences.push(`${inner.innerId}'s tags could not be resolved during the capture (its tags function threw or needed request context), so revalidating them may NOT update the embedded copy.`);
|
|
3710
|
+
else if (reasons.missingTags.length > 0) {
|
|
3711
|
+
const calls = reasons.missingTags.map((t) => `revalidateTag(${JSON.stringify(t)})`);
|
|
3712
|
+
consequences.push(`${calls.join(", ")} will NOT update the embedded copy; tag ${outer.id} itself if you need that.`);
|
|
3713
|
+
}
|
|
3714
|
+
if (reasons.shorterTtl) consequences.push(`${inner.innerId}'s ttl: ${inner.options.ttl} does not apply to the embedded copy.`);
|
|
3715
|
+
log(`[timber] ${inner.innerId} (${innerLifetime}) is embedded in ${outer.options.prerender ? "prerendered" : "cached"} ${outer.id} — the embedded copy refreshes only when ${outer.id} does (${describeRefresh(outer)}). ${consequences.join(" ")} Chain: ${outer.id} (${outerLifetime}) → ${inner.innerId} (${innerLifetime})`);
|
|
3716
|
+
}
|
|
3717
|
+
return count;
|
|
3718
|
+
}
|
|
3398
3719
|
/**
|
|
3399
3720
|
* Is this value the framework-injected layout children slot outlet
|
|
3400
3721
|
* (`<SlotOutlet slot={LAYOUT_CHILDREN_SLOT} />`)? Layout components
|
|
@@ -3423,6 +3744,17 @@ function isChildrenSlotOutletElement(value) {
|
|
|
3423
3744
|
}
|
|
3424
3745
|
}
|
|
3425
3746
|
/**
|
|
3747
|
+
* `props` without the framework-injected children outlet — the key props
|
|
3748
|
+
* of a render. The outlet is the same client reference on every request,
|
|
3749
|
+
* so it never varies the cache key; user-provided children stay (and
|
|
3750
|
+
* leave the props unkeyable). See design/45 §Layout Propagation, TIM-1181.
|
|
3751
|
+
*/
|
|
3752
|
+
function withoutChildrenOutlet(props) {
|
|
3753
|
+
if (!isChildrenSlotOutletElement(props.children)) return props;
|
|
3754
|
+
const { children: _children, ...rest } = props;
|
|
3755
|
+
return rest;
|
|
3756
|
+
}
|
|
3757
|
+
/**
|
|
3426
3758
|
* Dense array of non-empty strings — no holes, no non-string entries.
|
|
3427
3759
|
* Total: the reads run against a user-controlled value at module load,
|
|
3428
3760
|
* and a throwing Proxy trap must classify as invalid, not crash startup.
|
|
@@ -3454,7 +3786,7 @@ function resolveSlotNames(id, options) {
|
|
|
3454
3786
|
return { kind: "disabled" };
|
|
3455
3787
|
}
|
|
3456
3788
|
if (slots.length === 0) return { kind: "none" };
|
|
3457
|
-
if (!(options
|
|
3789
|
+
if (!hasRuntimeLifetime(options)) {
|
|
3458
3790
|
console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
|
|
3459
3791
|
return { kind: "disabled" };
|
|
3460
3792
|
}
|
|
@@ -3560,50 +3892,133 @@ function emitKeyDisciplineWarning(id, apis) {
|
|
|
3560
3892
|
}
|
|
3561
3893
|
//#endregion
|
|
3562
3894
|
//#region src/server/prebuilt/capture-state.ts
|
|
3563
|
-
/**
|
|
3895
|
+
/**
|
|
3896
|
+
* Capture state — the channel between a payload capture and the
|
|
3897
|
+
* `__prebuilt` wrappers that render inside it (server/prebuilt-runtime.ts).
|
|
3898
|
+
*
|
|
3899
|
+
* Two callers capture a wrapped component's flight payload:
|
|
3900
|
+
*
|
|
3901
|
+
* - **Build time**: the post-build capture pass (server/prebuilt-builder.ts)
|
|
3902
|
+
* renders each registry entry in turn.
|
|
3903
|
+
* - **Request time**: `captureComponentPayload()` in prebuilt-runtime.ts
|
|
3904
|
+
* renders a component in isolation for an inline re-render (full miss,
|
|
3905
|
+
* stale entry, tombstone) or an SWR background re-render. These run
|
|
3906
|
+
* concurrently with other requests' renders — and with each other.
|
|
3907
|
+
*
|
|
3908
|
+
* During a capture, the component's subtree may render OTHER wrapped
|
|
3909
|
+
* components. Those inner wrappers render live and their output is baked
|
|
3910
|
+
* flat into the outer entry — the nesting clamp of
|
|
3911
|
+
* design/45-cache-lifetimes.md §Nesting Semantics (TIM-1136). The wrapper
|
|
3912
|
+
* reports every render that happens inside a capture:
|
|
3913
|
+
*
|
|
3914
|
+
* - **Cycle**: the inner render IS the entry being captured — same
|
|
3915
|
+
* component id AND same props key (TIM-1547). Left alone this recurses
|
|
3916
|
+
* until the render times out; instead the wrapper throws
|
|
3917
|
+
* `PrebuiltCaptureCycleError`, failing that entry with an error naming
|
|
3918
|
+
* the cycle (build continues per doc 44 skip-and-report policy; at
|
|
3919
|
+
* request time the re-render fails and is not cached, and the wrapper
|
|
3920
|
+
* falls back as for any failed re-render). A component that renders
|
|
3921
|
+
* itself with different props (a comment thread, a tree) is legitimate
|
|
3922
|
+
* recursion with a base case and bakes flat like any other nesting.
|
|
3923
|
+
* - **Nesting**: any other wrapper render is recorded per inner component
|
|
3924
|
+
* with the tags it resolved to. Both capture callers run the clamp rule
|
|
3925
|
+
* over those records when the entry completes (prebuilt/lifetime.ts).
|
|
3926
|
+
*
|
|
3927
|
+
* Identity is the capture ROOT only. A wrapper cannot see the wrappers
|
|
3928
|
+
* between it and the root: Flight renders a component's children after
|
|
3929
|
+
* the wrapper has returned, in Flight's own async context, not in a frame
|
|
3930
|
+
* the wrapper could enter. So a same-key cycle that closes below the root
|
|
3931
|
+
* (`A(p1) → A(p2) → A(p2) → …`) is not detected here; it is also an
|
|
3932
|
+
* infinite recursion without caching, and the capture's render timeout
|
|
3933
|
+
* fails it.
|
|
3934
|
+
*
|
|
3935
|
+
* The props key is the props half of the cache key (data props only,
|
|
3936
|
+
* framework children outlet stripped — the same inputs the cache key
|
|
3937
|
+
* hashes). The params half is left out: every render inside one capture
|
|
3938
|
+
* reads the same segment params, so props decide identity. Unkeyable props
|
|
3939
|
+
* (live React elements, …) cannot be proven identical and are never a
|
|
3940
|
+
* cycle.
|
|
3941
|
+
*
|
|
3942
|
+
* Each capture's state lives in an AsyncLocalStorage store entered by
|
|
3943
|
+
* `CaptureScope.run()`, so it is visible only to renders started inside
|
|
3944
|
+
* that capture — including work Flight resumes after an await, which runs
|
|
3945
|
+
* in the context the awaited promise was created in. A concurrent request
|
|
3946
|
+
* rendering the same component is not a cycle, and a concurrent request
|
|
3947
|
+
* rendering a different cached component still revives it from cache
|
|
3948
|
+
* instead of baking it live (TIM-1536). Outside any capture
|
|
3949
|
+
* `noteWrapperRender` is a single store lookup.
|
|
3950
|
+
*/
|
|
3951
|
+
/** Thrown by the wrapper when a capture renders its own entry. */
|
|
3564
3952
|
var PrebuiltCaptureCycleError = class extends Error {
|
|
3565
3953
|
constructor(componentId) {
|
|
3566
|
-
super(`[timber] cache.component "${componentId}" renders itself (directly or through other components) —
|
|
3954
|
+
super(`[timber] cache.component "${componentId}" renders itself with the same props (directly or through other components) — capturing its payload would recurse forever. Remove the cycle or stop caching this component with cache.component.`);
|
|
3567
3955
|
this.name = "PrebuiltCaptureCycleError";
|
|
3568
3956
|
}
|
|
3569
3957
|
};
|
|
3570
|
-
var active = null;
|
|
3571
3958
|
/**
|
|
3572
|
-
*
|
|
3573
|
-
*
|
|
3959
|
+
* The props half of a render's cache key, or null when unkeyable. Same
|
|
3960
|
+
* inputs `tryRevivePayload` hashes: data props only for slot components,
|
|
3961
|
+
* the framework-injected children outlet stripped.
|
|
3574
3962
|
*/
|
|
3575
|
-
function
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3963
|
+
function propsKey(options, props) {
|
|
3964
|
+
let keyProps = props;
|
|
3965
|
+
if (options.slots && options.slots.length > 0) {
|
|
3966
|
+
const split = splitSlotProps(props, options.slots);
|
|
3967
|
+
if (split === null) return null;
|
|
3968
|
+
keyProps = split.dataProps;
|
|
3969
|
+
}
|
|
3970
|
+
return computeComponentCacheKey(withoutChildrenOutlet(keyProps), {});
|
|
3580
3971
|
}
|
|
3581
3972
|
/**
|
|
3582
|
-
*
|
|
3583
|
-
*
|
|
3973
|
+
* Open a capture of component `outerId` rendered with `props`. Its state
|
|
3974
|
+
* ends with the scope object.
|
|
3584
3975
|
*/
|
|
3585
|
-
function
|
|
3586
|
-
const
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3976
|
+
function createCaptureScope(outerId, options, props) {
|
|
3977
|
+
const store = {
|
|
3978
|
+
outerId,
|
|
3979
|
+
outerPropsKey: propsKey(options, props),
|
|
3980
|
+
nested: /* @__PURE__ */ new Map()
|
|
3981
|
+
};
|
|
3982
|
+
return {
|
|
3983
|
+
run: (fn) => prebuiltCaptureAls.run(store, fn),
|
|
3984
|
+
nestedRenders: () => Array.from(store.nested, ([innerId, record]) => ({
|
|
3985
|
+
innerId,
|
|
3986
|
+
options: record.options,
|
|
3987
|
+
tags: record.tags && Array.from(record.tags)
|
|
3988
|
+
}))
|
|
3989
|
+
};
|
|
3593
3990
|
}
|
|
3594
|
-
/** Whether
|
|
3991
|
+
/** Whether the current render is inside a capture. */
|
|
3595
3992
|
function isCaptureActive() {
|
|
3596
|
-
return
|
|
3993
|
+
return prebuiltCaptureAls.getStore() !== void 0;
|
|
3597
3994
|
}
|
|
3598
3995
|
/**
|
|
3599
|
-
* Called by the `__prebuilt` wrapper on every render. No-op
|
|
3600
|
-
* capture
|
|
3601
|
-
*
|
|
3996
|
+
* Called by the `__prebuilt` wrapper on every render. No-op outside a
|
|
3997
|
+
* capture. Throws `PrebuiltCaptureCycleError` when the render is the
|
|
3998
|
+
* entry the enclosing capture is producing.
|
|
3602
3999
|
*/
|
|
3603
|
-
function noteWrapperRender(innerId, options) {
|
|
3604
|
-
|
|
3605
|
-
if (
|
|
3606
|
-
if (
|
|
4000
|
+
function noteWrapperRender(innerId, options, props) {
|
|
4001
|
+
const capture = prebuiltCaptureAls.getStore();
|
|
4002
|
+
if (capture === void 0) return;
|
|
4003
|
+
if (innerId === capture.outerId && capture.outerPropsKey !== null) {
|
|
4004
|
+
if (propsKey(options, props) === capture.outerPropsKey) throw new PrebuiltCaptureCycleError(innerId);
|
|
4005
|
+
}
|
|
4006
|
+
let record = capture.nested.get(innerId);
|
|
4007
|
+
if (record === void 0) {
|
|
4008
|
+
record = {
|
|
4009
|
+
options,
|
|
4010
|
+
tags: /* @__PURE__ */ new Set()
|
|
4011
|
+
};
|
|
4012
|
+
capture.nested.set(innerId, record);
|
|
4013
|
+
}
|
|
4014
|
+
if (record.tags !== null && options.tags) {
|
|
4015
|
+
const tags = record.tags;
|
|
4016
|
+
try {
|
|
4017
|
+
for (const tag of requestContextAls.exit(() => resolveTags(options, props))) tags.add(tag);
|
|
4018
|
+
} catch {
|
|
4019
|
+
record.tags = null;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
3607
4022
|
}
|
|
3608
4023
|
//#endregion
|
|
3609
4024
|
//#region src/server/prebuilt-runtime.ts
|
|
@@ -3642,12 +4057,19 @@ function noteWrapperRender(innerId, options) {
|
|
|
3642
4057
|
* component. `fallbackOnMissingSegmentParams: false` throws instead.
|
|
3643
4058
|
*/
|
|
3644
4059
|
/** Default timeout for component re-renders (overridden by renderTimeoutMs). */
|
|
3645
|
-
var DEFAULT_COMPONENT_TIMEOUT_MS =
|
|
4060
|
+
var DEFAULT_COMPONENT_TIMEOUT_MS = DEFAULT_RENDER_TIMEOUT_MS;
|
|
3646
4061
|
/** Singleflight for component re-renders (inline and background). */
|
|
3647
4062
|
var componentSingleflight = createSingleflight({ timeoutMs: DEFAULT_COMPONENT_TIMEOUT_MS });
|
|
3648
4063
|
var componentTimeoutMs = DEFAULT_COMPONENT_TIMEOUT_MS;
|
|
3649
4064
|
var warnedNoWaitUntil = false;
|
|
3650
4065
|
/**
|
|
4066
|
+
* (outer, inner) pairs already warned about by request-time captures.
|
|
4067
|
+
* Process-wide: re-renders of a hot component repeat, and the warning is
|
|
4068
|
+
* about the code, not the request. Bounded by the number of wrapped
|
|
4069
|
+
* components in the app.
|
|
4070
|
+
*/
|
|
4071
|
+
var warnedNestingPairs = /* @__PURE__ */ new Set();
|
|
4072
|
+
/**
|
|
3651
4073
|
* Server-side component registry, keyed by stable component ID. Populated at
|
|
3652
4074
|
* module load time by `__prebuilt` calls; consumed by the post-build capture
|
|
3653
4075
|
* pass (TIM-1119). Never serialized — component functions are not
|
|
@@ -3703,12 +4125,7 @@ function isSlotRenderKeyable(dataProps) {
|
|
|
3703
4125
|
if (!store) return false;
|
|
3704
4126
|
const keyParams = resolveKeyParams(store);
|
|
3705
4127
|
if (keyParams === null) return false;
|
|
3706
|
-
|
|
3707
|
-
if (isChildrenSlotOutletElement(keyProps.children)) {
|
|
3708
|
-
const { children: _children, ...rest } = keyProps;
|
|
3709
|
-
keyProps = rest;
|
|
3710
|
-
}
|
|
3711
|
-
return computeComponentCacheKey(keyProps, keyParams) !== null;
|
|
4128
|
+
return computeComponentCacheKey(withoutChildrenOutlet(dataProps), keyParams) !== null;
|
|
3712
4129
|
}
|
|
3713
4130
|
/** Wrap payload bytes as a single-chunk ReadableStream for the decoder. */
|
|
3714
4131
|
function bytesToStream(bytes) {
|
|
@@ -3717,23 +4134,6 @@ function bytesToStream(bytes) {
|
|
|
3717
4134
|
controller.close();
|
|
3718
4135
|
} });
|
|
3719
4136
|
}
|
|
3720
|
-
/** Whether the component has a runtime lifetime (ttl or tags). */
|
|
3721
|
-
function hasRuntimeLifetime(options) {
|
|
3722
|
-
return options.ttl !== void 0 || options.tags !== void 0 && options.tags !== null;
|
|
3723
|
-
}
|
|
3724
|
-
/**
|
|
3725
|
-
* Resolve tags from options (may be static array or function of props).
|
|
3726
|
-
* Function-form tags receive DATA props only — slot props are excluded so
|
|
3727
|
-
* the tags fn never sees SlotOutlet placeholders (capture paths pass
|
|
3728
|
-
* placeholder-substituted props) and tags stay deterministic per key.
|
|
3729
|
-
*/
|
|
3730
|
-
function resolveTags(options, props) {
|
|
3731
|
-
if (!options.tags) return [];
|
|
3732
|
-
if (Array.isArray(options.tags)) return options.tags;
|
|
3733
|
-
const slots = options.slots;
|
|
3734
|
-
const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
|
|
3735
|
-
return options.tags(fnProps);
|
|
3736
|
-
}
|
|
3737
4137
|
/**
|
|
3738
4138
|
* Serve a slot component from the cache: split live slot values out of the
|
|
3739
4139
|
* props, look up / capture the shell (data props + a deterministic
|
|
@@ -3779,53 +4179,46 @@ function getCaptureTimeoutMs() {
|
|
|
3779
4179
|
* The stream read is timeout-guarded and the reader is cancelled on
|
|
3780
4180
|
* expiry (design/13 A2, A3).
|
|
3781
4181
|
*/
|
|
3782
|
-
async function captureComponentPayload(id, component, props) {
|
|
4182
|
+
async function captureComponentPayload(id, component, props, options) {
|
|
3783
4183
|
const { renderToReadableStream } = requireRscFunctions();
|
|
3784
4184
|
const element = createElement(component, props);
|
|
3785
|
-
|
|
4185
|
+
const capture = createCaptureScope(id, options, props);
|
|
4186
|
+
let renderError;
|
|
3786
4187
|
const store = requestContextAls.getStore();
|
|
3787
4188
|
const discipline = store && shouldCheckKeyDiscipline(id) ? createKeyDisciplineProxy(store) : null;
|
|
3788
|
-
const doCapture = async () => {
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
let totalLength = 0;
|
|
3797
|
-
const deadline = Date.now() + getCaptureTimeoutMs();
|
|
3798
|
-
try {
|
|
3799
|
-
for (;;) {
|
|
3800
|
-
const remaining = deadline - Date.now();
|
|
3801
|
-
if (remaining <= 0) throw new Error(`[timber] component re-render capture exceeded ${getCaptureTimeoutMs()}ms`);
|
|
3802
|
-
let timer;
|
|
3803
|
-
const { done, value } = await Promise.race([reader.read(), new Promise((_, reject) => {
|
|
3804
|
-
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`[timber] component re-render stream read timed out`)), remaining);
|
|
3805
|
-
})]);
|
|
3806
|
-
clearTimeout(timer);
|
|
3807
|
-
if (done) break;
|
|
3808
|
-
chunks.push(value);
|
|
3809
|
-
totalLength += value.byteLength;
|
|
3810
|
-
}
|
|
3811
|
-
} finally {
|
|
3812
|
-
reader.cancel().catch(() => {});
|
|
3813
|
-
}
|
|
3814
|
-
if (renderErrored) throw new Error("[timber] component re-render produced error rows — not caching");
|
|
3815
|
-
const result = new Uint8Array(totalLength);
|
|
3816
|
-
let offset = 0;
|
|
3817
|
-
for (const chunk of chunks) {
|
|
3818
|
-
result.set(chunk, offset);
|
|
3819
|
-
offset += chunk.byteLength;
|
|
3820
|
-
}
|
|
3821
|
-
return result;
|
|
3822
|
-
} finally {
|
|
3823
|
-
endCapture();
|
|
3824
|
-
}
|
|
3825
|
-
};
|
|
4189
|
+
const doCapture = () => capture.run(async () => {
|
|
4190
|
+
const bytes = await readAllBytesWithDeadline(runOutsideReactCacheScope(() => renderToReadableStream(element, { onError(error) {
|
|
4191
|
+
renderError ??= { error };
|
|
4192
|
+
return "";
|
|
4193
|
+
} })), getCaptureTimeoutMs(), "[timber] component re-render capture");
|
|
4194
|
+
if (renderError) throw renderError.error;
|
|
4195
|
+
return bytes;
|
|
4196
|
+
});
|
|
3826
4197
|
const bytes = discipline ? await requestContextAls.run(discipline.proxy, doCapture) : await doCapture();
|
|
3827
4198
|
if (discipline && discipline.accessedApis.size > 0) emitKeyDisciplineWarning(id, discipline.accessedApis);
|
|
3828
|
-
return
|
|
4199
|
+
return {
|
|
4200
|
+
bytes,
|
|
4201
|
+
nested: capture.nestedRenders()
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
/**
|
|
4205
|
+
* Store a captured entry in the overlay, then say which embedded
|
|
4206
|
+
* components it clamps (design/45 §Nesting Semantics, TIM-1545). The
|
|
4207
|
+
* warning follows the store: a capture that is discarded (aborted,
|
|
4208
|
+
* invalidated mid-render, failed store) holds no clamped copy, and must
|
|
4209
|
+
* not spend the pair's one warning.
|
|
4210
|
+
*/
|
|
4211
|
+
async function storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed) {
|
|
4212
|
+
await storeOverlayEntry(id, cacheKey, captured.bytes, {
|
|
4213
|
+
ttl: options.ttl,
|
|
4214
|
+
tags,
|
|
4215
|
+
hasSeed
|
|
4216
|
+
});
|
|
4217
|
+
warnNestingClamps({
|
|
4218
|
+
id,
|
|
4219
|
+
options,
|
|
4220
|
+
tags
|
|
4221
|
+
}, captured.nested, warnedNestingPairs, (m) => console.warn(m));
|
|
3829
4222
|
}
|
|
3830
4223
|
/**
|
|
3831
4224
|
* Inline re-render: capture flight bytes, store in overlay, revive for
|
|
@@ -3838,13 +4231,9 @@ async function inlineRerender(id, component, props, cacheKey, options, hasSeed,
|
|
|
3838
4231
|
try {
|
|
3839
4232
|
return await revivePayload(id, await componentSingleflight.do(sfKey, async (signal) => {
|
|
3840
4233
|
const startEpoch = currentInvalidationEpoch();
|
|
3841
|
-
const
|
|
3842
|
-
if (!signal.aborted && hasRuntimeLifetime(options) && !wasInvalidatedSince(startEpoch, oKey, tags)) await
|
|
3843
|
-
|
|
3844
|
-
tags,
|
|
3845
|
-
hasSeed
|
|
3846
|
-
});
|
|
3847
|
-
return bytes;
|
|
4234
|
+
const captured = await captureComponentPayload(id, component, props, options);
|
|
4235
|
+
if (!signal.aborted && hasRuntimeLifetime(options) && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed);
|
|
4236
|
+
return captured.bytes;
|
|
3848
4237
|
}), outcome);
|
|
3849
4238
|
} catch (error) {
|
|
3850
4239
|
console.error(`[timber] cache.component re-render failed for "${id}" — ${outcome}:`, error);
|
|
@@ -3854,10 +4243,12 @@ async function inlineRerender(id, component, props, cacheKey, options, hasSeed,
|
|
|
3854
4243
|
/**
|
|
3855
4244
|
* Snapshot the request context values needed for a background re-render.
|
|
3856
4245
|
* Creates a FRESH store with copied values — the original ALS store is
|
|
3857
|
-
* not referenced after capture.
|
|
4246
|
+
* not referenced after capture. The `Request` itself is shared, not
|
|
4247
|
+
* copied: the re-render reports its failure against it.
|
|
3858
4248
|
*/
|
|
3859
4249
|
function snapshotRequestContext(store) {
|
|
3860
4250
|
return {
|
|
4251
|
+
request: store.request,
|
|
3861
4252
|
headers: new Headers(store.headers),
|
|
3862
4253
|
originalHeaders: new Headers(store.originalHeaders),
|
|
3863
4254
|
cookieHeader: store.cookieHeader,
|
|
@@ -3880,16 +4271,10 @@ function backgroundRerender(id, component, props, cacheKey, options, hasSeed, sn
|
|
|
3880
4271
|
const oKey = overlayKey(id, cacheKey);
|
|
3881
4272
|
const startEpoch = currentInvalidationEpoch();
|
|
3882
4273
|
const sfKey = `prebuilt-bg:${id}:${cacheKey}\0${lastInvalidationEpochFor(oKey, tags)}`;
|
|
3883
|
-
const work = requestContextAls.run(snapshot, () => componentSingleflight.do(sfKey, async (
|
|
3884
|
-
const
|
|
3885
|
-
if (!signal.aborted && !wasInvalidatedSince(startEpoch, oKey, tags)) await
|
|
3886
|
-
|
|
3887
|
-
tags,
|
|
3888
|
-
hasSeed
|
|
3889
|
-
});
|
|
3890
|
-
}).catch((error) => {
|
|
3891
|
-
console.error(`[timber] cache.component background re-render failed for "${id}":`, error);
|
|
3892
|
-
}));
|
|
4274
|
+
const work = requestContextAls.run(snapshot, () => componentSingleflight.do(sfKey, (signal) => runBackgroundFlight(signal, async () => {
|
|
4275
|
+
const captured = await captureComponentPayload(id, component, props, options);
|
|
4276
|
+
if (!signal.aborted && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed);
|
|
4277
|
+
}, (error) => console.error(`[timber] cache.component background re-render failed for "${id}":`, error))).catch(() => {}));
|
|
3893
4278
|
if (waitUntil) waitUntil(work);
|
|
3894
4279
|
else if (!warnedNoWaitUntil) {
|
|
3895
4280
|
warnedNoWaitUntil = true;
|
|
@@ -3910,12 +4295,7 @@ async function tryRevivePayload(id, options, props, keyPropsOverride) {
|
|
|
3910
4295
|
if (!store) return null;
|
|
3911
4296
|
const keyParams = (options.prerender ? await isParamIndependent(id) : false) ? {} : resolveKeyParams(store);
|
|
3912
4297
|
if (keyParams === null) return null;
|
|
3913
|
-
|
|
3914
|
-
if (isChildrenSlotOutletElement(keyProps.children)) {
|
|
3915
|
-
const { children: _children, ...rest } = keyProps;
|
|
3916
|
-
keyProps = rest;
|
|
3917
|
-
}
|
|
3918
|
-
const cacheKey = computeComponentCacheKey(keyProps, keyParams);
|
|
4298
|
+
const cacheKey = computeComponentCacheKey(withoutChildrenOutlet(keyPropsOverride ?? props ?? {}), keyParams);
|
|
3919
4299
|
if (cacheKey === null) return null;
|
|
3920
4300
|
const outcome = options.fallbackOnMissingSegmentParams === false ? "failing the render (fallbackOnMissingSegmentParams: false)" : "rendering dynamically";
|
|
3921
4301
|
const component = registry.get(id)?.component;
|
|
@@ -3985,7 +4365,7 @@ function __prebuilt(id, component, options = {}) {
|
|
|
3985
4365
|
options
|
|
3986
4366
|
});
|
|
3987
4367
|
const wrapper = async (props) => {
|
|
3988
|
-
noteWrapperRender(id, options);
|
|
4368
|
+
noteWrapperRender(id, options, props);
|
|
3989
4369
|
if (isCaptureActive()) return createElement(component, props);
|
|
3990
4370
|
const revived = slots ? await tryReviveSlotShell(id, options, slots, props) : await tryRevivePayload(id, options, props);
|
|
3991
4371
|
if (revived !== null) return revived.element;
|
|
@@ -4027,6 +4407,6 @@ function __prebuiltDev(name, component, options = {}) {
|
|
|
4027
4407
|
return wrapper;
|
|
4028
4408
|
}
|
|
4029
4409
|
//#endregion
|
|
4030
|
-
export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal,
|
|
4410
|
+
export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal, RenderTimeoutError, SlotAccessGate, __prebuilt, __prebuiltDev, callOnRequestError, canonicalize, classifyMetadataRoute, coerce, collectEarlyHintHeaders, computeComponentCacheKey, createPipeline, enforceBodyLimits, executeAction, formatLinkHeader, generateTraceId, getCookie, getHeader, getLogger, getMetadataRouteAutoLink, getMetadataRouteServePath, getPrebuiltRegistration, getPrebuiltRegistry, getSearchParams, getSegmentParams, getSetCookieHeaders, handleRouteRequest, hasOnRequestError, hasPrebuiltPayloadSource, loadInstrumentation, logCacheMiss, logMiddlewareError, logMiddlewareShortCircuit, logProxyError, logRenderError, logRequestCompleted, logRequestReceived, logSlowRequest, logSwrRefetchFailed, logWaitUntilRejected, logWaitUntilUnsupported, markResponseFlushed, parseBodySize, renderMetadataToElements, replaceTraceId, resolveAllowedMethods, resolveMetadata, resolveMetadataUrls, resolveSlotDenied, resolveStatusFile, resolveTitle, runMiddleware, runProxy, runWithEarlyHintsSender, runWithRequestContext, runWithTraceId, sendEarlyHints103, setLogger, setMutableCookieContext, setPrebuiltPayloadSource, setSegmentParams };
|
|
4031
4411
|
|
|
4032
4412
|
//# sourceMappingURL=internal.js.map
|