@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/actions-CQ8Z8VGL.js +1061 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js.map +1 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js +61 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js.map +1 -0
- package/dist/_chunks/{define-Itxvcd7F.js → define-B-Q_UMOD.js} +19 -23
- package/dist/_chunks/define-B-Q_UMOD.js.map +1 -0
- package/dist/_chunks/{define-C77ScO0m.js → define-CfBPoJb0.js} +24 -7
- package/dist/_chunks/define-CfBPoJb0.js.map +1 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js +194 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js.map +1 -0
- package/dist/_chunks/{format-CYBGxKtc.js → format-Bcn-Iv1x.js} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js.map → format-Bcn-Iv1x.js.map} +1 -1
- package/dist/_chunks/handler-store-B-lqaGyh.js +54 -0
- package/dist/_chunks/handler-store-B-lqaGyh.js.map +1 -0
- package/dist/_chunks/logger-0m8MsKdc.js +291 -0
- package/dist/_chunks/logger-0m8MsKdc.js.map +1 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js +122 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js.map +1 -0
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/navigation-root-BCYczjml.js +96 -0
- package/dist/_chunks/navigation-root-BCYczjml.js.map +1 -0
- package/dist/_chunks/registry-I2ss-lvy.js +20 -0
- package/dist/_chunks/registry-I2ss-lvy.js.map +1 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js +28 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-Cxu4l-7p.js} +1 -1
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-Cxu4l-7p.js.map} +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
- package/dist/_chunks/{segment-context-fHFLF1PE.js → segment-context-Dx_OizxD.js} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js.map → segment-context-Dx_OizxD.js.map} +1 -1
- package/dist/_chunks/{router-ref-C8OCm7g7.js → ssr-data-B4CdH7rE.js} +2 -26
- package/dist/_chunks/ssr-data-B4CdH7rE.js.map +1 -0
- package/dist/_chunks/{stale-reload-BX5gL1r-.js → stale-reload-Bab885FO.js} +1 -1
- package/dist/_chunks/{stale-reload-BX5gL1r-.js.map → stale-reload-Bab885FO.js.map} +1 -1
- package/dist/_chunks/tracing-C8V-YGsP.js +329 -0
- package/dist/_chunks/tracing-C8V-YGsP.js.map +1 -0
- package/dist/_chunks/{use-query-states-BiV5GJgm.js → use-query-states-B2XTqxDR.js} +3 -19
- package/dist/_chunks/use-query-states-B2XTqxDR.js.map +1 -0
- package/dist/_chunks/{use-params-IOPu7E8t.js → use-segment-params-BkpKAQ7D.js} +9 -95
- package/dist/_chunks/use-segment-params-BkpKAQ7D.js.map +1 -0
- package/dist/_chunks/{interception-BbqMCVXa.js → walkers-Tg0Alwcg.js} +66 -87
- package/dist/_chunks/walkers-Tg0Alwcg.js.map +1 -0
- package/dist/_chunks/{dev-warnings-DpGRGoDi.js → warnings-Cg47l5sk.js} +3 -3
- package/dist/_chunks/warnings-Cg47l5sk.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +28 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +8 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +63 -31
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +16 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/cache/index.js +9 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -1
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/form.d.ts +10 -24
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +41 -91
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +2 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +81 -7
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/state.d.ts +1 -1
- package/dist/client/use-cookie.d.ts +8 -0
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +1 -1
- package/dist/client/use-segment-params.d.ts.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -2
- package/dist/config-types.d.ts +28 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts +87 -35
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +2 -1
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +48 -2
- package/dist/cookies/index.js.map +1 -0
- package/dist/cookies/json-cookie.d.ts +64 -0
- package/dist/cookies/json-cookie.d.ts.map +1 -0
- package/dist/cookies/validation.d.ts +46 -0
- package/dist/cookies/validation.d.ts.map +1 -0
- package/dist/{plugins/dev-404-page.d.ts → dev-tools/404-page.d.ts} +9 -19
- package/dist/dev-tools/404-page.d.ts.map +1 -0
- package/dist/{plugins/dev-browser-logs.d.ts → dev-tools/browser-logs.d.ts} +1 -1
- package/dist/dev-tools/browser-logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-page.d.ts → dev-tools/error-page.d.ts} +2 -2
- package/dist/dev-tools/error-page.d.ts.map +1 -0
- package/dist/{server/dev-holding-server.d.ts → dev-tools/holding-server.d.ts} +5 -3
- package/dist/dev-tools/holding-server.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +31 -0
- package/dist/dev-tools/index.d.ts.map +1 -0
- package/dist/{server/dev-span-processor.d.ts → dev-tools/instrumentation.d.ts} +26 -6
- package/dist/dev-tools/instrumentation.d.ts.map +1 -0
- package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -0
- package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +3 -12
- package/dist/dev-tools/overlay.d.ts.map +1 -0
- package/dist/dev-tools/stack-classifier.d.ts +34 -0
- package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
- package/dist/{plugins/dev-terminal-error.d.ts → dev-tools/terminal.d.ts} +2 -2
- package/dist/dev-tools/terminal.d.ts.map +1 -0
- package/dist/{server/dev-warnings.d.ts → dev-tools/warnings.d.ts} +1 -1
- package/dist/dev-tools/warnings.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +285 -133
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +6 -4
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.d.ts +5 -3
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/scanner.d.ts +1 -10
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +37 -8
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +63 -23
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +51 -0
- package/dist/routing/walkers.d.ts.map +1 -0
- package/dist/search-params/define.d.ts +25 -7
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +5 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +23 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts +4 -3
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +15 -6
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +5 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/asset-headers.d.ts +1 -15
- package/dist/server/asset-headers.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +170 -0
- package/dist/server/cookie-context.d.ts.map +1 -0
- package/dist/server/cookie-parsing.d.ts +51 -0
- package/dist/server/cookie-parsing.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +90 -0
- package/dist/server/deny-boundary.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +212 -0
- package/dist/server/html-injector-core.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +59 -59
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -149
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +6 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +852 -852
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +14 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware-runner.d.ts +17 -0
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +46 -49
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +26 -0
- package/dist/server/param-coercion.d.ts.map +1 -0
- package/dist/server/pipeline-helpers.d.ts +95 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -0
- package/dist/server/pipeline-outcome.d.ts +49 -0
- package/dist/server/pipeline-outcome.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +52 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +51 -32
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +117 -0
- package/dist/server/port-resolution.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +22 -159
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +20 -47
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +1 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +50 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +119 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +16 -11
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +1 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +45 -16
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +48 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/escape-html.d.ts +14 -0
- package/dist/server/utils/escape-html.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +2 -2
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +3 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +9 -4
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/directive-parser.d.ts +0 -45
- package/dist/utils/directive-parser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +77 -0
- package/src/adapters/cloudflare.ts +10 -50
- package/src/adapters/nitro.ts +66 -50
- package/src/adapters/shared.ts +40 -0
- package/src/cache/timber-cache.ts +3 -2
- package/src/client/form.tsx +17 -25
- package/src/client/index.ts +16 -9
- package/src/client/internal.ts +3 -2
- package/src/client/router.ts +1 -1
- package/src/client/rsc-fetch.ts +15 -0
- package/src/client/state.ts +2 -2
- package/src/client/use-cookie.ts +29 -0
- package/src/codec.ts +3 -7
- package/src/config-types.ts +28 -0
- package/src/cookies/define-cookie.ts +271 -78
- package/src/cookies/index.ts +11 -8
- package/src/cookies/json-cookie.ts +105 -0
- package/src/cookies/validation.ts +134 -0
- package/src/{plugins/dev-404-page.ts → dev-tools/404-page.ts} +17 -48
- package/src/{plugins/dev-error-page.ts → dev-tools/error-page.ts} +5 -32
- package/src/{server/dev-holding-server.ts → dev-tools/holding-server.ts} +4 -2
- package/src/dev-tools/index.ts +90 -0
- package/src/dev-tools/instrumentation.ts +176 -0
- package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +2 -2
- package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +5 -23
- package/src/dev-tools/stack-classifier.ts +75 -0
- package/src/{plugins/dev-terminal-error.ts → dev-tools/terminal.ts} +4 -38
- package/src/{server/dev-warnings.ts → dev-tools/warnings.ts} +1 -1
- package/src/index.ts +95 -34
- package/src/plugin-context.ts +1 -1
- package/src/plugins/adapter-build.ts +3 -1
- package/src/plugins/build-report.ts +13 -22
- package/src/plugins/dev-server.ts +3 -3
- package/src/plugins/routing.ts +14 -12
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +1 -1
- package/src/routing/codegen.ts +1 -1
- package/src/routing/convention-lint.ts +9 -8
- package/src/routing/index.ts +5 -3
- package/src/routing/interception.ts +1 -1
- package/src/routing/scanner.ts +22 -95
- package/src/routing/segment-classify.ts +107 -8
- package/src/routing/status-file-lint.ts +7 -5
- package/src/routing/types.ts +63 -23
- package/src/routing/walkers.ts +90 -0
- package/src/search-params/define.ts +71 -15
- package/src/search-params/wrappers.ts +9 -2
- package/src/segment-params/define.ts +66 -13
- package/src/server/access-gate.tsx +9 -8
- package/src/server/action-handler.ts +34 -38
- package/src/server/als-registry.ts +5 -5
- package/src/server/asset-headers.ts +8 -34
- package/src/server/cookie-context.ts +468 -0
- package/src/server/cookie-parsing.ts +135 -0
- package/src/server/{deny-page-resolver.ts → deny-boundary.ts} +78 -14
- package/src/server/deny-renderer.ts +7 -12
- package/src/server/early-hints-sender.ts +3 -2
- package/src/server/fallback-error.ts +2 -2
- package/src/server/html-injector-core.ts +403 -0
- package/src/server/html-injectors.ts +158 -297
- package/src/server/index.ts +13 -14
- package/src/server/internal.ts +10 -3
- package/src/server/logger.ts +23 -0
- package/src/server/middleware-runner.ts +44 -0
- package/src/server/node-stream-transforms.ts +108 -248
- package/src/server/param-coercion.ts +76 -0
- package/src/server/pipeline-helpers.ts +204 -0
- package/src/server/pipeline-outcome.ts +167 -0
- package/src/server/pipeline-phases.ts +409 -0
- package/src/server/pipeline.ts +70 -540
- package/src/server/port-resolution.ts +215 -0
- package/src/server/request-context.ts +46 -451
- package/src/server/route-element-builder.ts +8 -4
- package/src/server/route-matcher.ts +28 -60
- package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
- package/src/server/rsc-entry/api-handler.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +2 -2
- package/src/server/rsc-entry/helpers.ts +2 -7
- package/src/server/rsc-entry/index.ts +81 -366
- package/src/server/rsc-entry/render-route.ts +304 -0
- package/src/server/rsc-entry/rsc-payload.ts +1 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/rsc-entry/wrap-action-dispatch.ts +449 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/state-tree-diff.ts +4 -1
- package/src/server/status-code-resolver.ts +112 -128
- package/src/server/tracing.ts +3 -3
- package/src/server/tree-builder.ts +134 -56
- package/src/server/types.ts +52 -0
- package/src/server/utils/escape-html.ts +20 -0
- package/src/shims/headers.ts +3 -3
- package/src/shims/navigation-client.ts +4 -3
- package/src/shims/navigation.ts +9 -7
- package/src/utils/directive-parser.ts +0 -392
- package/dist/_chunks/actions-DLnUaR65.js +0 -421
- package/dist/_chunks/actions-DLnUaR65.js.map +0 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js +0 -41
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +0 -1
- package/dist/_chunks/debug-ECi_61pb.js +0 -108
- package/dist/_chunks/debug-ECi_61pb.js.map +0 -1
- package/dist/_chunks/define-C77ScO0m.js.map +0 -1
- package/dist/_chunks/define-Itxvcd7F.js.map +0 -1
- package/dist/_chunks/define-cookie-BowvzoP0.js +0 -94
- package/dist/_chunks/define-cookie-BowvzoP0.js.map +0 -1
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +0 -1
- package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js +0 -41
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +0 -1
- package/dist/_chunks/request-context-CK5tZqIP.js +0 -478
- package/dist/_chunks/request-context-CK5tZqIP.js.map +0 -1
- package/dist/_chunks/router-ref-C8OCm7g7.js.map +0 -1
- package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
- package/dist/_chunks/tracing-CCYbKn5n.js +0 -238
- package/dist/_chunks/tracing-CCYbKn5n.js.map +0 -1
- package/dist/_chunks/use-params-IOPu7E8t.js.map +0 -1
- package/dist/_chunks/use-query-states-BiV5GJgm.js.map +0 -1
- package/dist/client/use-params.d.ts.map +0 -1
- package/dist/plugins/dev-404-page.d.ts.map +0 -1
- package/dist/plugins/dev-browser-logs.d.ts.map +0 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
- package/dist/plugins/dev-error-page.d.ts.map +0 -1
- package/dist/plugins/dev-logs.d.ts.map +0 -1
- package/dist/plugins/dev-terminal-error.d.ts.map +0 -1
- package/dist/server/deny-page-resolver.d.ts +0 -52
- package/dist/server/deny-page-resolver.d.ts.map +0 -1
- package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
- package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
- package/dist/server/dev-holding-server.d.ts.map +0 -1
- package/dist/server/dev-logger.d.ts.map +0 -1
- package/dist/server/dev-span-processor.d.ts.map +0 -1
- package/dist/server/dev-warnings.d.ts.map +0 -1
- package/dist/server/manifest-status-resolver.d.ts +0 -58
- package/dist/server/manifest-status-resolver.d.ts.map +0 -1
- package/dist/server/page-deny-boundary.d.ts +0 -31
- package/dist/server/page-deny-boundary.d.ts.map +0 -1
- package/src/server/dev-fetch-instrumentation.ts +0 -96
- package/src/server/dev-span-processor.ts +0 -78
- package/src/server/manifest-status-resolver.ts +0 -215
- package/src/server/page-deny-boundary.tsx +0 -56
- /package/src/client/{use-params.ts → use-segment-params.ts} +0 -0
- /package/src/{plugins/dev-browser-logs.ts → dev-tools/browser-logs.ts} +0 -0
- /package/src/{server/dev-logger.ts → dev-tools/logger.ts} +0 -0
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deny
|
|
2
|
+
* Deny boundary subsystem — the in-tree DenySignal flow.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* render the matching deny page (403.tsx, 4xx.tsx, error.tsx) as a normal
|
|
6
|
-
* element in the React tree. This module resolves the deny page chain from
|
|
7
|
-
* the segment chain — a list of fallback components ordered by specificity.
|
|
4
|
+
* Three things live together here because they form a single flow:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* 1. **Chain construction** (`buildDenyPageChain`) — walks the matched
|
|
7
|
+
* segment chain at element-tree build time and produces a list of
|
|
8
|
+
* `DenyPageEntry` records ordered by specificity (specific status →
|
|
9
|
+
* category catch-all → `error.tsx`).
|
|
12
10
|
*
|
|
13
|
-
*
|
|
11
|
+
* 2. **Runtime matching** (`renderMatchingDenyPage`) — picks the first
|
|
12
|
+
* chain entry whose status filter matches the thrown DenySignal and
|
|
13
|
+
* returns a React element for the matching component. Used by
|
|
14
|
+
* `AccessGate` and `PageDenyBoundary` when they catch a deny.
|
|
15
|
+
*
|
|
16
|
+
* 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
|
|
17
|
+
* component that wraps a server-component page, calls it, and catches
|
|
18
|
+
* `DenySignal` so the deny page renders in-tree (no throw reaches
|
|
19
|
+
* React Flight, single render pass).
|
|
20
|
+
*
|
|
21
|
+
* Plus the ALS helpers (`setDenyStatus` / `getDenyStatus`) the boundary
|
|
22
|
+
* uses to thread the matched status code back to the pipeline so the
|
|
23
|
+
* HTTP status reflects the deny.
|
|
24
|
+
*
|
|
25
|
+
* Folded into one module from the former `deny-page-resolver.ts` and
|
|
26
|
+
* `page-deny-boundary.tsx` (TIM-853) — the names were misleading and the
|
|
27
|
+
* three pieces only made sense together.
|
|
28
|
+
*
|
|
29
|
+
* See design/04-authorization.md, design/10-error-handling.md, TIM-666.
|
|
14
30
|
*/
|
|
15
31
|
|
|
16
32
|
import { createElement } from 'react';
|
|
17
33
|
|
|
18
|
-
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
19
|
-
import { loadModule } from './safe-load.js';
|
|
20
34
|
import { requestContextAls } from './als-registry.js';
|
|
35
|
+
import { DenySignal } from './primitives.js';
|
|
36
|
+
import { loadModule } from './safe-load.js';
|
|
37
|
+
import { withSpan } from './tracing.js';
|
|
38
|
+
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
21
39
|
|
|
22
40
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
23
41
|
|
|
@@ -29,7 +47,7 @@ export interface DenyPageEntry {
|
|
|
29
47
|
component: (...args: unknown[]) => unknown;
|
|
30
48
|
}
|
|
31
49
|
|
|
32
|
-
// ───
|
|
50
|
+
// ─── Chain Construction ──────────────────────────────────────────────────
|
|
33
51
|
|
|
34
52
|
/**
|
|
35
53
|
* Build the deny page fallback chain from the segment chain.
|
|
@@ -101,7 +119,7 @@ export async function buildDenyPageChain(
|
|
|
101
119
|
return chain;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
// ─── Matcher
|
|
122
|
+
// ─── Runtime Matcher ──────────────────────────────────────────────────────
|
|
105
123
|
|
|
106
124
|
/**
|
|
107
125
|
* Find the first deny page in the chain that matches the given status code.
|
|
@@ -131,7 +149,53 @@ export function renderMatchingDenyPage(
|
|
|
131
149
|
return null;
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
// ───
|
|
152
|
+
// ─── Page Boundary ────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Async server component that wraps a page call with DenySignal catching.
|
|
156
|
+
*
|
|
157
|
+
* Calls the page component as an async function (the same thing React
|
|
158
|
+
* Flight does internally), awaits it, and catches DenySignal. On catch,
|
|
159
|
+
* renders the matching deny page in-tree. On success, returns the page's
|
|
160
|
+
* rendered output normally.
|
|
161
|
+
*
|
|
162
|
+
* Client component pages ('use client') are NOT wrapped — they can't call
|
|
163
|
+
* deny() (server-only API) and must go through createElement normally.
|
|
164
|
+
*
|
|
165
|
+
* No error reaches React Flight — the Flight stream is clean, SSR succeeds,
|
|
166
|
+
* and the entire request uses a single renderToReadableStream call.
|
|
167
|
+
*/
|
|
168
|
+
export async function PageDenyBoundary({
|
|
169
|
+
Page,
|
|
170
|
+
route,
|
|
171
|
+
denyPages,
|
|
172
|
+
}: {
|
|
173
|
+
/** The page server component function. */
|
|
174
|
+
Page: (...args: unknown[]) => unknown;
|
|
175
|
+
/** Route path for OTEL tracing. */
|
|
176
|
+
route: string;
|
|
177
|
+
/** Deny page fallback chain from the segment chain. */
|
|
178
|
+
denyPages: DenyPageEntry[];
|
|
179
|
+
}): Promise<React.ReactElement> {
|
|
180
|
+
try {
|
|
181
|
+
// Call the page as an async function — same as React Flight does.
|
|
182
|
+
// Wrap in OTEL span for tracing (replaces the TracedPage wrapper).
|
|
183
|
+
const result = await withSpan('timber.page', { 'timber.route': route }, () => Page({}));
|
|
184
|
+
return result as React.ReactElement;
|
|
185
|
+
} catch (error: unknown) {
|
|
186
|
+
if (error instanceof DenySignal) {
|
|
187
|
+
const denyElement = renderMatchingDenyPage(denyPages, error.status, error.data);
|
|
188
|
+
if (denyElement) {
|
|
189
|
+
setDenyStatus(error.status);
|
|
190
|
+
return denyElement;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Non-deny errors (RedirectSignal, runtime errors) propagate normally.
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── ALS Helpers ──────────────────────────────────────────────────────────
|
|
135
199
|
|
|
136
200
|
/**
|
|
137
201
|
* Set the deny status in the request context ALS.
|
|
@@ -22,8 +22,9 @@ import { DenySignal } from './primitives.js';
|
|
|
22
22
|
import { logRenderError } from './logger.js';
|
|
23
23
|
import { loadModule } from './safe-load.js';
|
|
24
24
|
import { isDebug } from './debug.js';
|
|
25
|
+
import { escapeHtml } from './utils/escape-html.js';
|
|
25
26
|
import { resolveMetadata, renderMetadataToElements } from './metadata.js';
|
|
26
|
-
import {
|
|
27
|
+
import { resolveStatusFile } from './status-code-resolver.js';
|
|
27
28
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
28
29
|
import type { RouteMatch } from './pipeline.js';
|
|
29
30
|
import type { NavContext } from './ssr-entry.js';
|
|
@@ -87,7 +88,7 @@ export async function renderDenyPage(
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
// Page routes → component chain first, JSON fallback only if no component found.
|
|
90
|
-
const resolution =
|
|
91
|
+
const resolution = resolveStatusFile(deny.status, segments, 'component');
|
|
91
92
|
|
|
92
93
|
// No component status file — try JSON chain before bare fallback
|
|
93
94
|
if (!resolution) {
|
|
@@ -99,7 +100,7 @@ export async function renderDenyPage(
|
|
|
99
100
|
// Dev warning: JSON status file exists but is shadowed by the component chain.
|
|
100
101
|
// This helps developers understand why their .json file isn't being served.
|
|
101
102
|
if (isDebug()) {
|
|
102
|
-
const jsonResolution =
|
|
103
|
+
const jsonResolution = resolveStatusFile(deny.status, segments, 'json');
|
|
103
104
|
if (jsonResolution) {
|
|
104
105
|
console.warn(
|
|
105
106
|
`[timber] ${jsonResolution.file.filePath} exists but is shadowed by ` +
|
|
@@ -190,7 +191,7 @@ export async function renderDenyPageAsRsc(
|
|
|
190
191
|
responseHeaders: Headers,
|
|
191
192
|
createDebugChannelSink: DebugChannelFactory
|
|
192
193
|
): Promise<Response> {
|
|
193
|
-
const resolution =
|
|
194
|
+
const resolution = resolveStatusFile(deny.status, segments, 'component');
|
|
194
195
|
|
|
195
196
|
if (!resolution) {
|
|
196
197
|
responseHeaders.set('content-type', `${RSC_CONTENT_TYPE}; charset=utf-8`);
|
|
@@ -249,7 +250,7 @@ async function renderDenyPageJson(
|
|
|
249
250
|
segments: ManifestSegmentNode[],
|
|
250
251
|
responseHeaders: Headers
|
|
251
252
|
): Promise<Response | null> {
|
|
252
|
-
const resolution =
|
|
253
|
+
const resolution = resolveStatusFile(deny.status, segments, 'json');
|
|
253
254
|
|
|
254
255
|
if (!resolution) {
|
|
255
256
|
return null;
|
|
@@ -280,10 +281,4 @@ function bareJsonResponse(status: number, responseHeaders: Headers): Response {
|
|
|
280
281
|
});
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
|
|
284
|
-
return str
|
|
285
|
-
.replace(/&/g, '&')
|
|
286
|
-
.replace(/</g, '<')
|
|
287
|
-
.replace(/>/g, '>')
|
|
288
|
-
.replace(/"/g, '"');
|
|
289
|
-
}
|
|
284
|
+
// escapeHtml imported from server/utils/escape-html.ts
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { earlyHintsSenderAls } from './als-registry.js';
|
|
22
|
+
import { swallow } from './logger.js';
|
|
22
23
|
|
|
23
24
|
/** Function that sends Link header values as a 103 Early Hints response. */
|
|
24
25
|
export type EarlyHintsSenderFn = (links: string[]) => void;
|
|
@@ -47,7 +48,7 @@ export function sendEarlyHints103(links: string[]): void {
|
|
|
47
48
|
if (!sender) return;
|
|
48
49
|
try {
|
|
49
50
|
sender(links);
|
|
50
|
-
} catch {
|
|
51
|
-
|
|
51
|
+
} catch (err) {
|
|
52
|
+
swallow(err, 'early hints 103 send failed');
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -66,7 +66,7 @@ export async function renderFallbackError(
|
|
|
66
66
|
// then fall through to global-error.tsx if those also fail.
|
|
67
67
|
logRenderError({ method: req.method, path: new URL(req.url).pathname, error: layoutError });
|
|
68
68
|
}
|
|
69
|
-
const match: RouteMatch = { segments
|
|
69
|
+
const match: RouteMatch = { segments, segmentParams: {}, middlewareChain: [] };
|
|
70
70
|
return renderErrorPage(
|
|
71
71
|
error,
|
|
72
72
|
500,
|
|
@@ -98,7 +98,7 @@ export async function renderDevErrorPage(error: unknown, projectRoot?: string):
|
|
|
98
98
|
try {
|
|
99
99
|
// Dynamic import — keeps dev-error-page.ts and its transitive deps
|
|
100
100
|
// (dev-error-overlay.ts, @jridgewell/trace-mapping) out of production bundles.
|
|
101
|
-
const { generateDevErrorPage } = await import('../
|
|
101
|
+
const { generateDevErrorPage } = await import('../dev-tools/error-page.js');
|
|
102
102
|
html = generateDevErrorPage(err, 'render', root);
|
|
103
103
|
// Inject Vite client script so the error overlay fires when HMR connects.
|
|
104
104
|
html = html.replace(
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML injector core — pure stateful helpers for streaming HTML post-processing.
|
|
3
|
+
*
|
|
4
|
+
* These helpers contain the actual byte/string logic for the held-flush
|
|
5
|
+
* streaming pipeline (buffered transforms, suffix moving, head injection,
|
|
6
|
+
* RSC flight interleaving). They are stream-shape agnostic: they know
|
|
7
|
+
* nothing about Web `TransformStream` or Node.js `Transform` and contain
|
|
8
|
+
* no `setImmediate` scheduling or controller plumbing.
|
|
9
|
+
*
|
|
10
|
+
* The Web Stream wrapper (`html-injectors.ts`) and the Node Stream wrapper
|
|
11
|
+
* (`node-stream-transforms.ts`) compose these helpers behind whichever
|
|
12
|
+
* stream shape their target runtime needs:
|
|
13
|
+
*
|
|
14
|
+
* - On Cloudflare/edge: Web `TransformStream` wraps the helpers.
|
|
15
|
+
* - On Node.js/Bun: Node `Transform` wraps the helpers (faster — C++ streams).
|
|
16
|
+
*
|
|
17
|
+
* Keeping this module split per-shape (rather than one file exporting both)
|
|
18
|
+
* preserves the build-time tree-shake: Cloudflare bundles never import
|
|
19
|
+
* `node:stream` even transitively. Only the helpers below are shared.
|
|
20
|
+
*
|
|
21
|
+
* **Pure functions only.** Anything that touches a stream API, schedules
|
|
22
|
+
* a tick, or owns a controller belongs in the wrapper, not here.
|
|
23
|
+
*
|
|
24
|
+
* Design docs: 02-rendering-pipeline.md §"Streaming Constraints", 18-build-system.md §"Entry Files"
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createMachine, type Machine } from '../utils/state-machine.js';
|
|
28
|
+
import { flightChunkScript } from './flight-scripts.js';
|
|
29
|
+
import {
|
|
30
|
+
flightInjectionTransitions,
|
|
31
|
+
isHtmlDone,
|
|
32
|
+
isPullDone,
|
|
33
|
+
type FlightInjectionState,
|
|
34
|
+
type FlightInjectionEvent,
|
|
35
|
+
} from './flight-injection-state.js';
|
|
36
|
+
import { withTimeout, RenderTimeoutError } from './render-timeout.js';
|
|
37
|
+
|
|
38
|
+
// ─── Encoders / Constants ────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
|
|
42
|
+
/** Closing tags React Fizz emits inside the shell chunk. */
|
|
43
|
+
export const SUFFIX = '</body></html>';
|
|
44
|
+
/** UTF-8 bytes of {@link SUFFIX}. Cached so wrappers don't re-encode per request. */
|
|
45
|
+
export const SUFFIX_BYTES: Uint8Array = encoder.encode(SUFFIX);
|
|
46
|
+
|
|
47
|
+
/** Encode a UTF-8 string. Convenience for wrappers that need a single source of truth. */
|
|
48
|
+
export function encodeUtf8(text: string): Uint8Array {
|
|
49
|
+
return encoder.encode(text);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Buffered Aggregator ─────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pure buffer state for the buffered transform.
|
|
56
|
+
*
|
|
57
|
+
* Both Web and Node wrappers want the same behaviour: collect chunks that
|
|
58
|
+
* arrive in the same event-loop tick, decide whether the buffer has grown
|
|
59
|
+
* beyond `maxBufferByteLength` and must flush synchronously, then emit a
|
|
60
|
+
* single concatenated chunk on the next tick.
|
|
61
|
+
*
|
|
62
|
+
* The only thing the two wrappers disagree on is *who schedules the tick*
|
|
63
|
+
* (`setImmediate` callback vs Promise-based) and *how to push the merged
|
|
64
|
+
* chunk* (`controller.enqueue` vs `transform.push`). This class owns
|
|
65
|
+
* everything else.
|
|
66
|
+
*/
|
|
67
|
+
export class BufferAggregator {
|
|
68
|
+
private chunks: Uint8Array[] = [];
|
|
69
|
+
private byteLength = 0;
|
|
70
|
+
|
|
71
|
+
constructor(private readonly maxBufferByteLength: number = Infinity) {}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Append a chunk. Returns `true` if the caller should flush synchronously
|
|
75
|
+
* right now (the buffer has reached the byte cap), `false` if a deferred
|
|
76
|
+
* tick-end flush is sufficient.
|
|
77
|
+
*/
|
|
78
|
+
append(chunk: Uint8Array): boolean {
|
|
79
|
+
this.chunks.push(chunk);
|
|
80
|
+
this.byteLength += chunk.byteLength;
|
|
81
|
+
return this.byteLength >= this.maxBufferByteLength;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Drain the buffer into a single concatenated chunk, or `null` if empty. */
|
|
85
|
+
drain(): Uint8Array | null {
|
|
86
|
+
if (this.chunks.length === 0) return null;
|
|
87
|
+
|
|
88
|
+
const merged = new Uint8Array(this.byteLength);
|
|
89
|
+
let offset = 0;
|
|
90
|
+
for (const chunk of this.chunks) {
|
|
91
|
+
merged.set(chunk, offset);
|
|
92
|
+
offset += chunk.byteLength;
|
|
93
|
+
}
|
|
94
|
+
this.chunks = [];
|
|
95
|
+
this.byteLength = 0;
|
|
96
|
+
return merged;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get bufferedByteLength(): number {
|
|
100
|
+
return this.byteLength;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get isEmpty(): boolean {
|
|
104
|
+
return this.chunks.length === 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Suffix Mover ────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Result of feeding one chunk through the suffix-mover state.
|
|
112
|
+
*
|
|
113
|
+
* - `passthrough` — already found and removed the suffix on a previous chunk.
|
|
114
|
+
* Caller emits the chunk unchanged.
|
|
115
|
+
* - `noSuffix` — current chunk does not contain the suffix. Caller emits unchanged.
|
|
116
|
+
* - `suffixFound` — caller emits `before` and `after` (skipping the suffix
|
|
117
|
+
* itself). The suffix will be re-emitted later by {@link suffixFlush}.
|
|
118
|
+
*/
|
|
119
|
+
export type SuffixChunkResult =
|
|
120
|
+
| { kind: 'passthrough' }
|
|
121
|
+
| { kind: 'noSuffix' }
|
|
122
|
+
| { kind: 'suffixFound'; before: Uint8Array | null; after: Uint8Array | null };
|
|
123
|
+
|
|
124
|
+
export interface SuffixState {
|
|
125
|
+
found: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createSuffixState(): SuffixState {
|
|
129
|
+
return { found: false };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Search a chunk for {@link SUFFIX}. If found, mark state and return the
|
|
134
|
+
* surrounding bytes (the caller emits them; the suffix is held for the
|
|
135
|
+
* end of the stream). The bytes-vs-text dance matches what both Web and
|
|
136
|
+
* Node implementations did before extraction — search via decoded UTF-8
|
|
137
|
+
* to avoid splitting multi-byte characters at the suffix boundary.
|
|
138
|
+
*/
|
|
139
|
+
export function processSuffixChunk(state: SuffixState, chunk: Uint8Array): SuffixChunkResult {
|
|
140
|
+
if (state.found) return { kind: 'passthrough' };
|
|
141
|
+
|
|
142
|
+
const text = new TextDecoder().decode(chunk, { stream: true });
|
|
143
|
+
const idx = text.indexOf(SUFFIX);
|
|
144
|
+
if (idx === -1) return { kind: 'noSuffix' };
|
|
145
|
+
|
|
146
|
+
state.found = true;
|
|
147
|
+
|
|
148
|
+
// Whole chunk was the suffix — nothing to emit before/after.
|
|
149
|
+
if (chunk.byteLength === SUFFIX_BYTES.byteLength) {
|
|
150
|
+
return { kind: 'suffixFound', before: null, after: null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const beforeText = text.slice(0, idx);
|
|
154
|
+
const afterText = text.slice(idx + SUFFIX.length);
|
|
155
|
+
return {
|
|
156
|
+
kind: 'suffixFound',
|
|
157
|
+
before: beforeText ? encoder.encode(beforeText) : null,
|
|
158
|
+
after: afterText ? encoder.encode(afterText) : null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Final emit for the suffix mover. Always returns the suffix bytes — even
|
|
164
|
+
* if no suffix was ever observed in the input — so output is well-formed.
|
|
165
|
+
*/
|
|
166
|
+
export function suffixFlush(_state: SuffixState): Uint8Array {
|
|
167
|
+
return SUFFIX_BYTES;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Tag Injector (head / body) ──────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pure state for `injectHead`-style transforms: scan for a target tag,
|
|
174
|
+
* inject `content` once at the matching position, then pass everything
|
|
175
|
+
* else through. We keep a small trailing string buffer (just under the
|
|
176
|
+
* length of the target tag) so the tag can be detected even when split
|
|
177
|
+
* across chunk boundaries.
|
|
178
|
+
*
|
|
179
|
+
* The `decoder` is a long-lived `TextDecoder` reused across every chunk
|
|
180
|
+
* with `{ stream: true }`. This is required so a UTF-8 codepoint that
|
|
181
|
+
* spans two chunks (e.g. the bytes of `é` arriving in two halves) is
|
|
182
|
+
* reassembled correctly. A fresh decoder per chunk would drop or
|
|
183
|
+
* replace the partial codepoint and corrupt streamed HTML.
|
|
184
|
+
*/
|
|
185
|
+
export interface InjectorState {
|
|
186
|
+
injected: boolean;
|
|
187
|
+
tail: string;
|
|
188
|
+
readonly content: string;
|
|
189
|
+
readonly targetTag: string;
|
|
190
|
+
readonly position: 'before' | 'after';
|
|
191
|
+
readonly tailLen: number;
|
|
192
|
+
readonly decoder: TextDecoder;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface InjectorOptions {
|
|
196
|
+
content: string;
|
|
197
|
+
targetTag: string;
|
|
198
|
+
position?: 'before' | 'after';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createInjectorState(options: InjectorOptions): InjectorState {
|
|
202
|
+
return {
|
|
203
|
+
injected: false,
|
|
204
|
+
tail: '',
|
|
205
|
+
content: options.content,
|
|
206
|
+
targetTag: options.targetTag,
|
|
207
|
+
position: options.position ?? 'before',
|
|
208
|
+
tailLen: options.targetTag.length - 1,
|
|
209
|
+
decoder: new TextDecoder('utf-8'),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Result of feeding one chunk through {@link processInjectorChunk}.
|
|
215
|
+
*
|
|
216
|
+
* - `passthrough` — already injected. Emit `chunk` unchanged.
|
|
217
|
+
* - `emit` — emit the returned bytes (or nothing if `null`).
|
|
218
|
+
*
|
|
219
|
+
* Note: when not yet injected and the target tag is not present, the
|
|
220
|
+
* helper still emits the *safe head* of the buffer — everything except
|
|
221
|
+
* the trailing `tailLen` characters that might be the start of the
|
|
222
|
+
* target tag spilled into the next chunk. The unsafe tail is held in
|
|
223
|
+
* `state.tail` for the next call.
|
|
224
|
+
*/
|
|
225
|
+
export type InjectorChunkResult =
|
|
226
|
+
| { kind: 'passthrough' }
|
|
227
|
+
| { kind: 'emit'; bytes: Uint8Array | null };
|
|
228
|
+
|
|
229
|
+
export function processInjectorChunk(state: InjectorState, chunk: Uint8Array): InjectorChunkResult {
|
|
230
|
+
if (state.injected) return { kind: 'passthrough' };
|
|
231
|
+
|
|
232
|
+
// Reuse the long-lived decoder so split UTF-8 codepoints are preserved
|
|
233
|
+
// across chunk boundaries. See comment on InjectorState.decoder.
|
|
234
|
+
const decoded = state.decoder.decode(chunk, { stream: true });
|
|
235
|
+
const text = state.tail + decoded;
|
|
236
|
+
const tagIndex = text.indexOf(state.targetTag);
|
|
237
|
+
|
|
238
|
+
if (tagIndex !== -1) {
|
|
239
|
+
const splitPoint = state.position === 'before' ? tagIndex : tagIndex + state.targetTag.length;
|
|
240
|
+
const before = text.slice(0, splitPoint);
|
|
241
|
+
const after = text.slice(splitPoint);
|
|
242
|
+
state.injected = true;
|
|
243
|
+
state.tail = '';
|
|
244
|
+
return { kind: 'emit', bytes: encoder.encode(before + state.content + after) };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Tag not yet seen — flush everything except the last tailLen chars,
|
|
248
|
+
// which might be the start of the target tag split across chunks.
|
|
249
|
+
const safeEnd = Math.max(0, text.length - state.tailLen);
|
|
250
|
+
const safe = safeEnd > 0 ? text.slice(0, safeEnd) : '';
|
|
251
|
+
state.tail = text.slice(safeEnd);
|
|
252
|
+
return { kind: 'emit', bytes: safe ? encoder.encode(safe) : null };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Final emit for the injector. If the target tag never appeared, the
|
|
257
|
+
* leftover tail bytes are returned so they aren't lost. (The injection
|
|
258
|
+
* itself is silently skipped — same behaviour as before extraction.)
|
|
259
|
+
*/
|
|
260
|
+
export function injectorFlush(state: InjectorState): Uint8Array | null {
|
|
261
|
+
if (!state.injected && state.tail) return encoder.encode(state.tail);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Flight Injection Core ───────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Pure async core of the RSC flight-injection transform.
|
|
269
|
+
*
|
|
270
|
+
* Both wrappers (Web `TransformStream` and Node `Transform`) implement the
|
|
271
|
+
* exact same protocol around an RSC reader:
|
|
272
|
+
*
|
|
273
|
+
* 1. On the first HTML chunk, send `FIRST_CHUNK` and start the pull loop.
|
|
274
|
+
* 2. The pull loop reads from `rscReader` (timeout-guarded), wraps each
|
|
275
|
+
* chunk in a `<script>` via `flightChunkScript`, and pushes it onto a
|
|
276
|
+
* pending queue. Between reads, it yields with a single-shot
|
|
277
|
+
* `setImmediate` so HTML chunks get event-loop priority — and after
|
|
278
|
+
* yielding, it drains the queue (via the wrapper's `onYieldDrain`)
|
|
279
|
+
* so flight data reaches the client at shell-flush time.
|
|
280
|
+
* 3. After every emitted HTML chunk, the wrapper drains the queue.
|
|
281
|
+
* 4. On `flush()` the wrapper sends `HTML_DONE` and awaits `pullPromise`.
|
|
282
|
+
* No polling — just `.then()`.
|
|
283
|
+
*
|
|
284
|
+
* The wrappers only differ in:
|
|
285
|
+
* - How they push bytes (controller.enqueue vs transform.push).
|
|
286
|
+
* - How they signal a terminal error (controller.error vs transform.destroy).
|
|
287
|
+
*
|
|
288
|
+
* This class owns the state machine, the pending queue, and the pull
|
|
289
|
+
* loop itself. The wrapper supplies the drain callback and consumes the
|
|
290
|
+
* pending queue + machine state to decide what to push.
|
|
291
|
+
*
|
|
292
|
+
* **No polling.** See design/02-rendering-pipeline.md §"No Polling".
|
|
293
|
+
* **Always timeout-guarded reads.** Per design/02 §"Streaming Constraints".
|
|
294
|
+
*/
|
|
295
|
+
export class FlightInjectorCore {
|
|
296
|
+
readonly machine: Machine<FlightInjectionState, FlightInjectionEvent>;
|
|
297
|
+
/** Script chunks waiting to be drained between HTML chunks. */
|
|
298
|
+
readonly pending: Uint8Array[] = [];
|
|
299
|
+
|
|
300
|
+
private readonly rscReader: ReadableStreamDefaultReader<Uint8Array>;
|
|
301
|
+
private readonly timeoutMs: number;
|
|
302
|
+
private readonly decoder = new TextDecoder('utf-8', { fatal: true });
|
|
303
|
+
private pullPromise: Promise<void> | null = null;
|
|
304
|
+
|
|
305
|
+
constructor(rscStream: ReadableStream<Uint8Array>, renderTimeoutMs?: number) {
|
|
306
|
+
this.rscReader = rscStream.getReader();
|
|
307
|
+
this.timeoutMs = renderTimeoutMs ?? 30_000;
|
|
308
|
+
this.machine = createMachine<FlightInjectionState, FlightInjectionEvent>({
|
|
309
|
+
initial: { phase: 'init' },
|
|
310
|
+
transitions: flightInjectionTransitions,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Tell the machine the first HTML chunk has arrived. Idempotent-ish: caller checks. */
|
|
315
|
+
notifyFirstChunk(): void {
|
|
316
|
+
this.machine.send({ type: 'FIRST_CHUNK' });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Tell the machine HTML output is finished — pull loop will stop yielding. */
|
|
320
|
+
notifyHtmlDone(): void {
|
|
321
|
+
this.machine.send({ type: 'HTML_DONE' });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
get isInit(): boolean {
|
|
325
|
+
return this.machine.state.phase === 'init';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
get isPullDone(): boolean {
|
|
329
|
+
return isPullDone(this.machine.state);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
get hasPullPromise(): boolean {
|
|
333
|
+
return this.pullPromise !== null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get (or start) the pull-loop promise. The wrapper calls this from
|
|
338
|
+
* `flush()` and chains `.then()` onto it — never `await` without an
|
|
339
|
+
* existing finish callback (we don't want to block transform()).
|
|
340
|
+
*
|
|
341
|
+
* `onYieldDrain` is invoked between RSC reads while HTML is still
|
|
342
|
+
* streaming. It must drain the pending queue into the output stream.
|
|
343
|
+
*/
|
|
344
|
+
ensurePullLoop(onYieldDrain: () => void): Promise<void> {
|
|
345
|
+
if (!this.pullPromise) {
|
|
346
|
+
this.pullPromise = this.runPullLoop(onYieldDrain);
|
|
347
|
+
}
|
|
348
|
+
return this.pullPromise;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get the current terminal error if the machine is in the error phase,
|
|
353
|
+
* else `null`. Wrappers use this after a drain to decide whether to
|
|
354
|
+
* `controller.error()` / `transform.destroy()`.
|
|
355
|
+
*/
|
|
356
|
+
get terminalError(): unknown {
|
|
357
|
+
return this.machine.state.phase === 'error' ? this.machine.state.error : null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async runPullLoop(onYieldDrain: () => void): Promise<void> {
|
|
361
|
+
// Yield once so the first HTML shell chunk flows through the wrapper
|
|
362
|
+
// before we start reading RSC data.
|
|
363
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
for (;;) {
|
|
367
|
+
// Guard each read with a timeout so a permanently hung RSC stream
|
|
368
|
+
// eventually aborts. See design/02 §"Streaming Constraints".
|
|
369
|
+
const readPromise = this.rscReader.read();
|
|
370
|
+
const { done, value } =
|
|
371
|
+
this.timeoutMs > 0
|
|
372
|
+
? await withTimeout(readPromise, this.timeoutMs, 'RSC stream read timed out')
|
|
373
|
+
: await readPromise;
|
|
374
|
+
|
|
375
|
+
if (done) {
|
|
376
|
+
this.machine.send({ type: 'PULL_DONE' });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const decoded = this.decoder.decode(value, { stream: true });
|
|
381
|
+
this.pending.push(encoder.encode(flightChunkScript(decoded)));
|
|
382
|
+
|
|
383
|
+
// Yield between reads so HTML chunks get priority — but only while
|
|
384
|
+
// HTML is still streaming. Once flush() fires (HTML_DONE), drain
|
|
385
|
+
// without yielding so remaining RSC data doesn't stall.
|
|
386
|
+
if (!isHtmlDone(this.machine.state)) {
|
|
387
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
388
|
+
// After yielding, drain. Without this, hydration blocks waiting
|
|
389
|
+
// for flight data stuck in pending[]. Safe because we're between
|
|
390
|
+
// event-loop ticks (no transform() call mid-execution → no
|
|
391
|
+
// mid-tag risk; the buffered transform upstream coalesced HTML
|
|
392
|
+
// chunks to coherent fragments).
|
|
393
|
+
if (this.pending.length > 0) onYieldDrain();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (err instanceof RenderTimeoutError) {
|
|
398
|
+
this.rscReader.cancel(err).catch(() => {});
|
|
399
|
+
}
|
|
400
|
+
this.machine.send({ type: 'PULL_ERROR', error: err });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|