@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140
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/AGENTS.md +17 -0
- package/README.md +432 -7
- package/dist/bin/rango.js +2073 -213
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +7258 -2714
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +140 -67
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +329 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +487 -0
- package/skills/caching/SKILL.md +357 -25
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +246 -0
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +16 -10
- package/skills/document-cache/SKILL.md +87 -62
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +557 -79
- package/skills/host-router/SKILL.md +320 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +207 -15
- package/skills/layout/SKILL.md +146 -6
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +616 -54
- package/skills/middleware/SKILL.md +217 -37
- package/skills/migrate-nextjs/SKILL.md +611 -0
- package/skills/migrate-react-router/SKILL.md +927 -0
- package/skills/mime-routes/SKILL.md +42 -11
- package/skills/observability/SKILL.md +194 -0
- package/skills/parallel/SKILL.md +284 -3
- package/skills/ppr/SKILL.md +426 -0
- package/skills/prerender/SKILL.md +437 -52
- package/skills/rango/SKILL.md +369 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +263 -121
- package/skills/route/SKILL.md +350 -21
- package/skills/router-setup/SKILL.md +246 -33
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +126 -222
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +532 -103
- package/skills/use-cache/SKILL.md +367 -0
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +77 -44
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +114 -0
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +293 -202
- package/src/browser/history-state.ts +101 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +385 -576
- package/src/browser/navigation-client.ts +245 -75
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +184 -118
- package/src/browser/navigation-transaction.ts +247 -0
- package/src/browser/network-error-handler.ts +88 -0
- package/src/browser/partial-update.ts +412 -364
- package/src/browser/prefetch/cache.ts +359 -0
- package/src/browser/prefetch/fetch.ts +452 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +209 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +194 -0
- package/src/browser/react/Link.tsx +275 -68
- package/src/browser/react/NavigationProvider.tsx +265 -109
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +70 -0
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +272 -60
- package/src/browser/react/location-state.ts +90 -20
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +35 -66
- package/src/browser/react/use-handle.ts +39 -126
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +39 -13
- package/src/browser/react/use-navigation.ts +53 -69
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +98 -0
- package/src/browser/react/use-search-params.ts +51 -0
- package/src/browser/react/use-segments.ts +72 -99
- package/src/browser/response-adapter.ts +164 -0
- package/src/browser/rsc-router.tsx +300 -72
- package/src/browser/scroll-restoration.ts +138 -50
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +17 -1
- package/src/browser/server-action-bridge.ts +668 -613
- package/src/browser/types.ts +223 -51
- package/src/browser/validate-redirect-origin.ts +56 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +252 -161
- package/src/build/generate-route-types.ts +41 -1038
- package/src/build/index.ts +12 -7
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +225 -42
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +105 -0
- package/src/build/route-types/codegen.ts +113 -0
- package/src/build/route-types/include-resolution.ts +812 -0
- package/src/build/route-types/param-extraction.ts +51 -0
- package/src/build/route-types/per-module-writer.ts +144 -0
- package/src/build/route-types/router-processing.ts +695 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +223 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +60 -0
- package/src/cache/cache-policy.ts +199 -0
- package/src/cache/cache-runtime.ts +525 -0
- package/src/cache/cache-scope.ts +298 -332
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2500 -158
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +17 -17
- package/src/cache/document-cache.ts +199 -92
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +111 -0
- package/src/cache/index.ts +29 -35
- package/src/cache/memory-segment-store.ts +363 -30
- package/src/cache/profile-registry.ts +88 -0
- package/src/cache/read-through-swr.ts +178 -0
- package/src/cache/segment-codec.ts +248 -0
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +156 -211
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1102 -0
- package/src/client.rsc.tsx +43 -21
- package/src/client.tsx +131 -347
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +23 -4
- package/src/components/DefaultDocument.tsx +13 -3
- package/src/context-var.ts +168 -0
- package/src/debug.ts +19 -9
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +106 -10
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +110 -35
- package/src/handles/MetaTags.tsx +83 -59
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +93 -0
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +44 -53
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +20 -65
- package/src/host/errors.ts +21 -30
- package/src/host/index.ts +13 -9
- package/src/host/pattern-matcher.ts +50 -79
- package/src/host/router.ts +151 -121
- package/src/host/testing.ts +45 -32
- package/src/host/types.ts +52 -11
- package/src/host/utils.ts +2 -2
- package/src/href-client.ts +192 -57
- package/src/index.rsc.ts +177 -35
- package/src/index.ts +255 -71
- package/src/internal-debug.ts +9 -2
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +31 -99
- package/src/loader.ts +30 -12
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +41 -0
- package/src/prerender/param-hash.ts +16 -14
- package/src/prerender/store.ts +121 -21
- package/src/prerender.ts +460 -26
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +198 -128
- package/src/root-error-boundary.tsx +42 -48
- package/src/route-content-wrapper.tsx +22 -77
- package/src/route-definition/dsl-helpers.ts +1116 -0
- package/src/route-definition/helper-factories.ts +88 -0
- package/src/route-definition/helpers-types.ts +505 -0
- package/src/route-definition/index.ts +54 -0
- package/src/route-definition/redirect.ts +134 -0
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-definition.ts +1 -1481
- package/src/route-map-builder.ts +82 -144
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +71 -45
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +263 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +54 -27
- package/src/router/find-match.ts +245 -0
- package/src/router/handler-context.ts +377 -125
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +59 -28
- package/src/router/lazy-includes.ts +254 -0
- package/src/router/loader-resolution.ts +421 -157
- package/src/router/logging.ts +106 -6
- package/src/router/manifest.ts +131 -57
- package/src/router/match-api.ts +167 -246
- package/src/router/match-context.ts +4 -24
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +117 -93
- package/src/router/match-middleware/cache-lookup.ts +297 -150
- package/src/router/match-middleware/cache-store.ts +123 -51
- package/src/router/match-middleware/intercept-resolution.ts +44 -43
- package/src/router/match-middleware/segment-resolution.ts +64 -22
- package/src/router/match-pipelines.ts +11 -87
- package/src/router/match-result.ts +121 -50
- package/src/router/metrics.ts +219 -28
- package/src/router/middleware-types.ts +93 -0
- package/src/router/middleware.ts +505 -441
- package/src/router/navigation-snapshot.ts +133 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +311 -142
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +547 -0
- package/src/router/preview-match.ts +102 -0
- package/src/router/request-classification.ts +278 -0
- package/src/router/revalidation.ts +203 -62
- package/src/router/route-snapshot.ts +246 -0
- package/src/router/router-context.ts +45 -48
- package/src/router/router-interfaces.ts +554 -0
- package/src/router/router-options.ts +779 -0
- package/src/router/router-registry.ts +21 -0
- package/src/router/segment-resolution/fresh.ts +772 -0
- package/src/router/segment-resolution/helpers.ts +348 -0
- package/src/router/segment-resolution/loader-cache.ts +250 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/segment-resolution/revalidation.ts +1331 -0
- package/src/router/segment-resolution/static-store.ts +81 -0
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +25 -1354
- package/src/router/segment-wrappers.ts +292 -0
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +261 -0
- package/src/router/telemetry.ts +377 -0
- package/src/router/timeout.ts +128 -0
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +240 -61
- package/src/router/types.ts +23 -70
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +781 -2378
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +46 -0
- package/src/rsc/handler.ts +905 -1142
- package/src/rsc/helpers.ts +275 -19
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +305 -0
- package/src/rsc/manifest-init.ts +77 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +155 -0
- package/src/rsc/progressive-enhancement.ts +502 -0
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +104 -0
- package/src/rsc/response-route-handler.ts +257 -0
- package/src/rsc/rsc-rendering.ts +337 -0
- package/src/rsc/runtime-warnings.ts +55 -0
- package/src/rsc/server-action.ts +522 -0
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +95 -12
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +99 -82
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +349 -134
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +452 -85
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +310 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +123 -42
- package/src/server/live.ts +130 -0
- package/src/server/loader-registry.ts +51 -100
- package/src/server/request-context.ts +842 -157
- package/src/server.ts +15 -8
- package/src/ssr/index.tsx +412 -136
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +45 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +199 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +40 -72
- package/src/theme/ThemeScript.tsx +12 -14
- package/src/theme/constants.ts +57 -15
- package/src/theme/index.ts +3 -20
- package/src/theme/theme-context.ts +5 -35
- package/src/theme/theme-script.ts +43 -39
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +123 -0
- package/src/types/cache-types.ts +207 -0
- package/src/types/error-types.ts +132 -0
- package/src/types/global-namespace.ts +113 -0
- package/src/types/handler-context.ts +839 -0
- package/src/types/index.ts +81 -0
- package/src/types/loader-types.ts +212 -0
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +138 -0
- package/src/types/route-entry.ts +114 -0
- package/src/types/segments.ts +271 -0
- package/src/types.ts +1 -1795
- package/src/urls/include-helper.ts +162 -0
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +43 -0
- package/src/urls/path-helper-types.ts +413 -0
- package/src/urls/path-helper.ts +275 -0
- package/src/urls/pattern-types.ts +124 -0
- package/src/urls/response-types.ts +109 -0
- package/src/urls/type-extraction.ts +316 -0
- package/src/urls/urls-function.ts +80 -0
- package/src/urls.ts +1 -1341
- package/src/use-loader.tsx +406 -141
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +182 -0
- package/src/vite/discovery/discover-routers.ts +389 -0
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +467 -0
- package/src/vite/discovery/route-types-writer.ts +214 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +161 -0
- package/src/vite/discovery/virtual-module-codegen.ts +183 -0
- package/src/vite/index.ts +23 -2255
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +303 -0
- package/src/vite/plugins/cjs-to-esm.ts +90 -0
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- package/src/vite/plugins/client-ref-hashing.ts +118 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
- package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +805 -0
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +313 -0
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +94 -0
- package/src/vite/plugins/version-plugin.ts +263 -0
- package/src/vite/plugins/virtual-entries.ts +234 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +560 -0
- package/src/vite/router-discovery.ts +1638 -0
- package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +132 -0
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +15 -0
- package/src/vite/utils/package-resolution.ts +89 -0
- package/src/vite/utils/prerender-utils.ts +249 -0
- package/src/vite/utils/shared-utils.ts +269 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/react/use-client-cache.ts +0 -56
- package/src/browser/request-controller.ts +0 -164
- package/src/browser/shallow.ts +0 -35
- package/src/cache/memory-store.ts +0 -253
- package/src/handles/index.ts +0 -6
- package/src/href-context.ts +0 -33
- package/src/network-error-thrower.tsx +0 -21
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- package/src/vite/package-resolution.ts +0 -125
- package/src/vite/virtual-entries.ts +0 -114
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PPR shell capture orchestration (Axis 2, see docs/design/ppr-shell-resume.md).
|
|
3
|
+
*
|
|
4
|
+
* Capture does NOT flow through the HTTP middleware pipeline. The shell-cache
|
|
5
|
+
* middleware sets a `_shellCapture` DESCRIPTOR before its single foreground
|
|
6
|
+
* next(); the render layer (rsc-rendering.ts) reads it after building the served
|
|
7
|
+
* response and calls scheduleShellCapture. The capture then runs as a background
|
|
8
|
+
* task that re-derives the shell via `ctx.router.match()` under its OWN derived
|
|
9
|
+
* request context — fresh handle store, `_shellCaptureRun: true` so loaders mask
|
|
10
|
+
* (loader-mask.ts) and every loader-consuming subtree postpones. It drives the
|
|
11
|
+
* static prerender to a quiescent shell, aborts to freeze the prelude + postponed
|
|
12
|
+
* state, and stores the pair via putShell. Because it uses match() rather than a
|
|
13
|
+
* second next(), the middleware chain (auth, logging, the single-use next() latch)
|
|
14
|
+
* never re-runs — and the capture inherits the foreground's post-middleware
|
|
15
|
+
* context state (variables, cache store) it delegates to.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { bufferToBase64 } from "../cache/cf/cf-base64.js";
|
|
20
|
+
import { reportCacheError } from "../cache/cache-error.js";
|
|
21
|
+
import { runBackground } from "../cache/background-task.js";
|
|
22
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
23
|
+
import {
|
|
24
|
+
runWithRequestContext,
|
|
25
|
+
setRequestContextParams,
|
|
26
|
+
type RequestContext,
|
|
27
|
+
} from "../server/request-context.js";
|
|
28
|
+
import { createHandleStore, type HandleStore } from "../server/handle-store.js";
|
|
29
|
+
import type { ShellCacheEntry } from "../cache/types.js";
|
|
30
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
31
|
+
import type { RscPayload, SSRModule } from "./types.js";
|
|
32
|
+
import { buildFullPayload } from "./full-payload.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Task-quantized quiesce: the number of consecutive macrotask hops with zero new
|
|
36
|
+
* Flight bytes that marks the shell "quiet". This replaces the old 50ms
|
|
37
|
+
* wall-clock debounce.
|
|
38
|
+
*
|
|
39
|
+
* The capture Flight render is a REGULAR renderToReadableStream (not a static
|
|
40
|
+
* prerender), so React schedules both its retries and its byte-flush on
|
|
41
|
+
* setTimeout(0) MACROTASKS (verified against the vendored edge production
|
|
42
|
+
* react-server-dom build: pingTask uses scheduleMicrotask only when
|
|
43
|
+
* request.type === PRERENDER, otherwise setTimeout; enqueueFlush is always
|
|
44
|
+
* setTimeout). Masked loaders are the live lane — their rows never emit — so once
|
|
45
|
+
* the shell rows finish flushing the stream goes permanently byte-silent, and K
|
|
46
|
+
* consecutive quiet macrotask hops after the last observed byte declare quiesce.
|
|
47
|
+
*
|
|
48
|
+
* K=2 gives a race window of ~two event-loop turns: shell work still producing
|
|
49
|
+
* bytes keeps resetting the counter; anything not producing bytes within the
|
|
50
|
+
* window (the masked loaders, and any genuinely pending I/O) becomes a hole. The
|
|
51
|
+
* only residual is raw per-request I/O rendered directly in shell (not via a
|
|
52
|
+
* loader) that resolves inside the window — a documented shell anti-pattern; put
|
|
53
|
+
* per-request data in loaders or behind live(). See docs/design/ppr-shell-resume.md.
|
|
54
|
+
*/
|
|
55
|
+
const FLIGHT_QUIET_HOPS = 2;
|
|
56
|
+
|
|
57
|
+
/** Default upper bound on the capture prerender wait before forcing the abort. */
|
|
58
|
+
const SHELL_CAPTURE_MAX_WAIT_MS = 5000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Module-level in-flight key set: the stampede guard for background captures, and
|
|
62
|
+
* its single owner. One capture runs per key per isolate; concurrent MISS/stale
|
|
63
|
+
* requests for the same key coalesce onto the first (the rest see the key present
|
|
64
|
+
* in scheduleShellCapture and skip). Added when a capture is scheduled and cleared
|
|
65
|
+
* in the task's finally once it settles, so a later request can recapture when TTL
|
|
66
|
+
* rolls. Living here (not split across the middleware) keeps the add/clear
|
|
67
|
+
* lifecycle in one layer.
|
|
68
|
+
*/
|
|
69
|
+
const inFlightCaptures = new Set<string>();
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Keys already warned about a refused (null) capture, so the eternal-MISS shape
|
|
73
|
+
* logs once per key per isolate instead of on every request.
|
|
74
|
+
*/
|
|
75
|
+
const warnedNullCaptures = new Set<string>();
|
|
76
|
+
|
|
77
|
+
function warnNullCaptureOnce(key: string): void {
|
|
78
|
+
if (warnedNullCaptures.has(key)) return;
|
|
79
|
+
warnedNullCaptures.add(key);
|
|
80
|
+
console.warn(
|
|
81
|
+
`[rango] Shell capture for "${key}" produced no usable shell (empty or ` +
|
|
82
|
+
"not-ready prelude); nothing was stored, so this request stays on MISS. A later " +
|
|
83
|
+
"request re-captures - if the route NEVER flips to HIT, the most common cause is " +
|
|
84
|
+
"a loader route without a route-level loading() boundary: its loader data is " +
|
|
85
|
+
"awaited at tree-build, so under capture's masked loaders no shell exists above " +
|
|
86
|
+
"<body>. Add loading() to the loader route (and keep shell material in a layout) " +
|
|
87
|
+
"to make it PPR-capturable. See docs/design/ppr-shell-resume.md.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface FlightCaptureGate {
|
|
92
|
+
/** Identity passthrough of the source stream; feed this to captureShellHTML. */
|
|
93
|
+
stream: ReadableStream<Uint8Array>;
|
|
94
|
+
/**
|
|
95
|
+
* Resolves once the source has been byte-quiet for FLIGHT_QUIET_HOPS macrotask
|
|
96
|
+
* hops (or has closed — the DATA variant). At that instant the gate FREEZES:
|
|
97
|
+
* no further source byte reaches the fizz side, and the readable is left open
|
|
98
|
+
* (never closed / errored) so fizz postpones the still-pending references
|
|
99
|
+
* instead of seeing "Connection closed".
|
|
100
|
+
*/
|
|
101
|
+
quiesce: Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Stop the internal macrotask-hop loop. captureShellHTML's maxWaitMs bounds the
|
|
104
|
+
* overall wait; dispose() is the clean shutdown for the pathological case where
|
|
105
|
+
* the source never goes byte-quiet (quiesce never fires), so the hop loop would
|
|
106
|
+
* otherwise keep rescheduling after captureShellHTML has already aborted and
|
|
107
|
+
* returned.
|
|
108
|
+
*/
|
|
109
|
+
dispose(): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Wrap the capture Flight stream so the fizz shell prerender reads a stream that
|
|
114
|
+
* (a) forwards the shell rows unchanged, (b) resolves `quiesce` after the rows go
|
|
115
|
+
* byte-silent for FLIGHT_QUIET_HOPS macrotask hops, and (c) FREEZES at that
|
|
116
|
+
* instant — dropping any later byte without closing or erroring the readable, so
|
|
117
|
+
* the pending masked-loader references stay pending and fizz postpones them (the
|
|
118
|
+
* "unclosing stream" property, here for free because the masked rows never emit).
|
|
119
|
+
* Freezing also guarantees no post-quiesce byte — including an error row from any
|
|
120
|
+
* later abort/cancel of the underlying render — can corrupt the frozen prelude.
|
|
121
|
+
*
|
|
122
|
+
* Quiet is measured in TASKS, not wall-clock: after the first byte a macrotask
|
|
123
|
+
* hop loop compares a byte counter each turn and fires after K quiet turns. The
|
|
124
|
+
* hop timers are unref'd so they never keep a Node process alive, and the source
|
|
125
|
+
* closing (no holes) fires quiesce immediately for the DATA variant — the
|
|
126
|
+
* TransformStream then closes the readable, so fizz completes with postponed null.
|
|
127
|
+
*/
|
|
128
|
+
export function gateFlightForCapture(
|
|
129
|
+
source: ReadableStream<Uint8Array>,
|
|
130
|
+
quietHops: number = FLIGHT_QUIET_HOPS,
|
|
131
|
+
): FlightCaptureGate {
|
|
132
|
+
let resolveQuiet!: () => void;
|
|
133
|
+
const quiesce = new Promise<void>((resolve) => {
|
|
134
|
+
resolveQuiet = resolve;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
let bytesSeen = 0;
|
|
138
|
+
let armed = false;
|
|
139
|
+
let settled = false;
|
|
140
|
+
let disposed = false;
|
|
141
|
+
let frozen = false;
|
|
142
|
+
|
|
143
|
+
const fire = (): void => {
|
|
144
|
+
if (settled) return;
|
|
145
|
+
settled = true;
|
|
146
|
+
frozen = true;
|
|
147
|
+
resolveQuiet();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const scheduleHop = (fn: () => void): void => {
|
|
151
|
+
const t = setTimeout(fn, 0);
|
|
152
|
+
// Never let the quiet-detection hop alone keep a Node process alive
|
|
153
|
+
// (no-op on workerd).
|
|
154
|
+
(t as { unref?: () => void }).unref?.();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// The hop loop starts only after the first byte, so it can never declare
|
|
158
|
+
// quiesce before fizz has begun pulling rows through the transform.
|
|
159
|
+
const arm = (): void => {
|
|
160
|
+
if (armed || settled || disposed) return;
|
|
161
|
+
armed = true;
|
|
162
|
+
let lastSeen = bytesSeen;
|
|
163
|
+
let quiet = 0;
|
|
164
|
+
const hop = (): void => {
|
|
165
|
+
if (settled || disposed) return;
|
|
166
|
+
if (bytesSeen === lastSeen) {
|
|
167
|
+
quiet += 1;
|
|
168
|
+
if (quiet >= quietHops) {
|
|
169
|
+
fire();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
lastSeen = bytesSeen;
|
|
174
|
+
quiet = 0;
|
|
175
|
+
}
|
|
176
|
+
scheduleHop(hop);
|
|
177
|
+
};
|
|
178
|
+
scheduleHop(hop);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const monitor = new TransformStream<Uint8Array, Uint8Array>({
|
|
182
|
+
transform(chunk, controller) {
|
|
183
|
+
// Post-quiesce: drop the byte. Do NOT enqueue and do NOT close/error — the
|
|
184
|
+
// frozen fizz input must stay a fixed byte set behind an open (unclosing)
|
|
185
|
+
// readable so still-pending references postpone.
|
|
186
|
+
if (frozen) return;
|
|
187
|
+
bytesSeen += chunk.length;
|
|
188
|
+
arm();
|
|
189
|
+
controller.enqueue(chunk);
|
|
190
|
+
},
|
|
191
|
+
flush() {
|
|
192
|
+
// Source closed with no freeze => DATA variant (no holes): quiet
|
|
193
|
+
// immediately. The TransformStream then closes the readable, so fizz
|
|
194
|
+
// completes and postponed comes back null.
|
|
195
|
+
fire();
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
stream: source.pipeThrough(monitor),
|
|
201
|
+
quiesce,
|
|
202
|
+
dispose(): void {
|
|
203
|
+
disposed = true;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Schedule the background shell capture for a served document. Stampede-guarded:
|
|
210
|
+
* one capture per key per isolate. Runs via runBackground (waitUntil on workerd,
|
|
211
|
+
* fire-and-forget in Node dev), so the served response is never blocked on it. Any
|
|
212
|
+
* error is routed through reportCacheError — capture is best-effort; a failure just
|
|
213
|
+
* means the next request recaptures.
|
|
214
|
+
*
|
|
215
|
+
* Eligibility (nonce/allReady/partial/status/strategy) is decided by the caller
|
|
216
|
+
* (rsc-rendering.ts maybeScheduleShellCapture); this function only owns the
|
|
217
|
+
* stampede guard and the background dispatch.
|
|
218
|
+
*/
|
|
219
|
+
export function scheduleShellCapture(
|
|
220
|
+
ctx: HandlerContext<any>,
|
|
221
|
+
request: Request,
|
|
222
|
+
env: any,
|
|
223
|
+
url: URL,
|
|
224
|
+
reqCtx: RequestContext<any>,
|
|
225
|
+
ssrModule: SSRModule,
|
|
226
|
+
descriptor: NonNullable<RequestContext["_shellCapture"]>,
|
|
227
|
+
): void {
|
|
228
|
+
const key = descriptor.key;
|
|
229
|
+
if (inFlightCaptures.has(key)) return;
|
|
230
|
+
inFlightCaptures.add(key);
|
|
231
|
+
runBackground(reqCtx, async () => {
|
|
232
|
+
try {
|
|
233
|
+
await runShellCapture(
|
|
234
|
+
ctx,
|
|
235
|
+
request,
|
|
236
|
+
env,
|
|
237
|
+
url,
|
|
238
|
+
reqCtx,
|
|
239
|
+
ssrModule,
|
|
240
|
+
descriptor,
|
|
241
|
+
);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// Detached background task — pass reqCtx so onError still fires when the ALS
|
|
244
|
+
// context is gone. Best-effort: a failure just means the next request
|
|
245
|
+
// recaptures.
|
|
246
|
+
reportCacheError(error, "cache-write", "[ShellCache] capture", reqCtx);
|
|
247
|
+
} finally {
|
|
248
|
+
inFlightCaptures.delete(key);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Run the shell capture in a DERIVED request context, then store the result.
|
|
255
|
+
*
|
|
256
|
+
* The derived context is `Object.create(reqCtx)` so it inherits the foreground's
|
|
257
|
+
* post-middleware state (variables, cache store, env/request/url, waitUntil) while
|
|
258
|
+
* overriding the render-scoped accumulators as own properties:
|
|
259
|
+
* - _handleStore: a fresh store. The foreground store is already drained to
|
|
260
|
+
* completion (its stream() flipped `completed` on settle) and would throw
|
|
261
|
+
* LateHandlePushError on any re-push. Every downstream reader resolves the
|
|
262
|
+
* store off the ambient context (setupLoaderAccess captures
|
|
263
|
+
* _getRequestContext()._handleStore; trackHandler reads it), so the fresh
|
|
264
|
+
* store on the derived context is what the capture match() writes handles to.
|
|
265
|
+
* - _requestTags: a fresh Set. The capture collects its OWN shell tags here —
|
|
266
|
+
* non-loader tags only, since loaders are masked — which is exactly the tag
|
|
267
|
+
* set a shell entry should be invalidatable by (loader tags belong to holes).
|
|
268
|
+
* - _transitionWhen: a fresh [] so the capture's transition gating is its own.
|
|
269
|
+
* - _shellCaptureRun: true — the switch loaders/cookies/headers guards read.
|
|
270
|
+
* - _shellCapture: the descriptor (informational; putShell target/ttl/swr).
|
|
271
|
+
* - _metricsStore: undefined so the capture never appends to the foreground's
|
|
272
|
+
* (already-finalized) metrics.
|
|
273
|
+
*/
|
|
274
|
+
async function runShellCapture(
|
|
275
|
+
ctx: HandlerContext<any>,
|
|
276
|
+
request: Request,
|
|
277
|
+
env: any,
|
|
278
|
+
url: URL,
|
|
279
|
+
reqCtx: RequestContext<any>,
|
|
280
|
+
ssrModule: SSRModule,
|
|
281
|
+
descriptor: NonNullable<RequestContext["_shellCapture"]>,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
const freshHandleStore = createHandleStore();
|
|
284
|
+
freshHandleStore.onError = reqCtx._handleStore.onError;
|
|
285
|
+
|
|
286
|
+
const derivedCtx: RequestContext = Object.create(reqCtx);
|
|
287
|
+
derivedCtx._handleStore = freshHandleStore;
|
|
288
|
+
derivedCtx._requestTags = new Set<string>();
|
|
289
|
+
derivedCtx._transitionWhen = [];
|
|
290
|
+
derivedCtx._shellCaptureRun = true;
|
|
291
|
+
derivedCtx._shellCapture = descriptor;
|
|
292
|
+
derivedCtx._metricsStore = undefined;
|
|
293
|
+
|
|
294
|
+
await runWithRequestContext(derivedCtx, async () => {
|
|
295
|
+
const match = await ctx.router.match(request, { env });
|
|
296
|
+
// A route that redirects has no shell to capture — bail (no store write).
|
|
297
|
+
if (match.redirect) return;
|
|
298
|
+
setRequestContextParams(match.params, match.routeName);
|
|
299
|
+
|
|
300
|
+
const payload = buildFullPayload(
|
|
301
|
+
match,
|
|
302
|
+
ctx,
|
|
303
|
+
url,
|
|
304
|
+
derivedCtx,
|
|
305
|
+
freshHandleStore,
|
|
306
|
+
);
|
|
307
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
308
|
+
onError: (error: unknown) => {
|
|
309
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Shell tags = the non-loader request tags the capture render recorded on its
|
|
314
|
+
// own fresh _requestTags. Loaders are masked, so loader cache tags (which
|
|
315
|
+
// belong to the holes, not the shell) are correctly excluded.
|
|
316
|
+
const tags =
|
|
317
|
+
derivedCtx._requestTags.size > 0
|
|
318
|
+
? [...derivedCtx._requestTags]
|
|
319
|
+
: undefined;
|
|
320
|
+
|
|
321
|
+
await captureAndStoreShell(
|
|
322
|
+
ssrModule,
|
|
323
|
+
rscStream,
|
|
324
|
+
freshHandleStore,
|
|
325
|
+
derivedCtx,
|
|
326
|
+
{
|
|
327
|
+
...descriptor,
|
|
328
|
+
tags,
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Seal handles, derive the quiesce signal, prerender + abort via the SSR module's
|
|
336
|
+
* captureShellHTML, and store the result. Never throws out of the store write: a
|
|
337
|
+
* failed putShell is routed through reportCacheError so the background task stays
|
|
338
|
+
* best-effort. `ssrModule.captureShellHTML` MUST be present (eligibility is
|
|
339
|
+
* checked before scheduling).
|
|
340
|
+
*/
|
|
341
|
+
async function captureAndStoreShell(
|
|
342
|
+
ssrModule: SSRModule,
|
|
343
|
+
rscStream: ReadableStream<Uint8Array>,
|
|
344
|
+
handleStore: HandleStore,
|
|
345
|
+
reqCtx: RequestContext<any>,
|
|
346
|
+
capture: NonNullable<RequestContext["_shellCapture"]>,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
const captureShellHTML = ssrModule.captureShellHTML!;
|
|
349
|
+
|
|
350
|
+
// Seal the handle store so the payload's handles generator (resolvedHandleStream
|
|
351
|
+
// -> handleStore.stream()) converges and completes even though masked loaders
|
|
352
|
+
// never resolve. handleStore.settled gates ONLY on tracked HANDLER promises
|
|
353
|
+
// (handleStore.track, via trackHandler) — NOT on deferred handle VALUES pushed
|
|
354
|
+
// through ctx.use(Handle).defer(), which are plain pushed promises. So seal()
|
|
355
|
+
// does not reject or hang on outstanding defers: settled resolves once the
|
|
356
|
+
// handlers settle, and each deferred slot resolves on its own createDeferred
|
|
357
|
+
// timeout (defer.ts, default 10s) or when its resolver fires. A defer whose
|
|
358
|
+
// resolver depends on a masked loader can never fire, so it stays pending until
|
|
359
|
+
// that 10s timeout — longer than maxWaitMs (5s). At the abort the handles
|
|
360
|
+
// generator has not yielded, SsrRoot suspends at the root (consumeAsyncGenerator
|
|
361
|
+
// sits above every boundary), the prelude comes back trivial, and
|
|
362
|
+
// captureShellHTML's sanity gate returns null: the designed fail-safe no-op, not
|
|
363
|
+
// an error. This mirrors the __prerender_collect seal+settled regime, which also
|
|
364
|
+
// excludes loaders. See docs/design/ppr-shell-resume.md ("Loaders and handles").
|
|
365
|
+
handleStore.seal();
|
|
366
|
+
|
|
367
|
+
const gate = gateFlightForCapture(rscStream);
|
|
368
|
+
// Quiesce = handles settled AND the Flight shell rows went task-quiet. Either
|
|
369
|
+
// half stalling is bounded by captureShellHTML's maxWaitMs.
|
|
370
|
+
const quiesce = Promise.all([handleStore.settled, gate.quiesce]).then(
|
|
371
|
+
() => {},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
// captureShellHTML CONSUMES the (gated) stream — it is not also SSR'd.
|
|
376
|
+
const result = await observePhase(PHASES.ssr, () =>
|
|
377
|
+
captureShellHTML(gate.stream, {
|
|
378
|
+
quiesce,
|
|
379
|
+
maxWaitMs: SHELL_CAPTURE_MAX_WAIT_MS,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// null = sanity gate refused (trivial/empty prelude, no <body>). Store
|
|
384
|
+
// nothing; the route stays on axis 1 and every future request re-captures to
|
|
385
|
+
// the same refusal, so surface it once per key: the dominant cause is a
|
|
386
|
+
// route shape with no capturable shell — a loader route WITHOUT a route-level
|
|
387
|
+
// loading() boundary awaits its loader data at tree-build (renderSegments'
|
|
388
|
+
// loading-less branch), so the masked loader pins the whole tree above
|
|
389
|
+
// <body>. Silent refusal made that shape an undiagnosable eternal MISS.
|
|
390
|
+
if (result === null) {
|
|
391
|
+
warnNullCaptureOnce(capture.key);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Store per the flag's key/ttl/swr/tags, into the flag's store: the middleware
|
|
396
|
+
// threads the SAME store it resolved for its getShell read (options.store ??
|
|
397
|
+
// _cacheStore), so a store-attached middleware writes captures where it reads
|
|
398
|
+
// them. The _cacheStore fallback covers a flag armed without a store (tests).
|
|
399
|
+
// reactVersion is read from the same React.version import the middleware
|
|
400
|
+
// validates reads against, so capture and serve always agree.
|
|
401
|
+
const store = capture.store ?? reqCtx._cacheStore;
|
|
402
|
+
if (store?.putShell) {
|
|
403
|
+
try {
|
|
404
|
+
const entry: ShellCacheEntry = {
|
|
405
|
+
// slice() copies just this view's bytes into a fresh ArrayBuffer, so a
|
|
406
|
+
// prelude that is a subarray of a larger backing buffer encodes only its
|
|
407
|
+
// own region — bufferToBase64 reads the whole ArrayBuffer it is handed.
|
|
408
|
+
prelude: bufferToBase64(result.prelude.slice().buffer as ArrayBuffer),
|
|
409
|
+
postponed: result.postponed,
|
|
410
|
+
reactVersion: React.version,
|
|
411
|
+
createdAt: Date.now(),
|
|
412
|
+
};
|
|
413
|
+
await store.putShell(
|
|
414
|
+
capture.key,
|
|
415
|
+
entry,
|
|
416
|
+
capture.ttl,
|
|
417
|
+
capture.swr,
|
|
418
|
+
capture.tags,
|
|
419
|
+
);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
// Best-effort: a failed put must never throw out of the background task.
|
|
422
|
+
reportCacheError(
|
|
423
|
+
error,
|
|
424
|
+
"cache-write",
|
|
425
|
+
"[ShellCache] capture put",
|
|
426
|
+
reqCtx,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} finally {
|
|
431
|
+
// Stop the hop loop for the pathological never-quiets path (quiesce never
|
|
432
|
+
// fired, capture returned via maxWaitMs). On the normal path the loop already
|
|
433
|
+
// stopped when it fired quiesce; dispose() is then a no-op.
|
|
434
|
+
gate.dispose();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Exported for unit tests that drive the capture core directly.
|
|
439
|
+
export { runShellCapture, captureAndStoreShell };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR Setup Utilities
|
|
3
|
+
*
|
|
4
|
+
* Manages early kickoff and retrieval of SSR module loading and stream mode
|
|
5
|
+
* resolution. Both operations are request-scoped but independent of route
|
|
6
|
+
* matching, so they can run in parallel with segment resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
10
|
+
import type { SSRModule } from "./types.js";
|
|
11
|
+
import type { SSRStreamMode } from "../router/router-options.js";
|
|
12
|
+
import type { MetricsStore } from "../server/context.js";
|
|
13
|
+
import { appendMetric } from "../router/metrics.js";
|
|
14
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
15
|
+
|
|
16
|
+
export type SSRSetup = readonly [SSRModule, SSRStreamMode];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Key used to stash the early SSR setup promise on request variables.
|
|
20
|
+
* Read back via `getSSRSetup`.
|
|
21
|
+
*/
|
|
22
|
+
export const SSR_SETUP_VAR = "__ssrSetup";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start loading the SSR module and resolving the stream mode in parallel.
|
|
26
|
+
* When a `getMetricsStore` getter is provided, records individual
|
|
27
|
+
* `ssr:module-load` and `ssr:stream-mode` metrics (the getter is called
|
|
28
|
+
* lazily so stores created after kickoff are still captured). Without a
|
|
29
|
+
* getter the promises run bare — no `.then()` microtasks, no
|
|
30
|
+
* `performance.now()` calls — keeping the non-debug hot path lean.
|
|
31
|
+
*/
|
|
32
|
+
export function startSSRSetup<TEnv>(
|
|
33
|
+
ctx: HandlerContext<TEnv>,
|
|
34
|
+
request: Request,
|
|
35
|
+
env: TEnv,
|
|
36
|
+
url: URL,
|
|
37
|
+
getMetricsStore?: () => MetricsStore | undefined,
|
|
38
|
+
): Promise<SSRSetup> {
|
|
39
|
+
if (!getMetricsStore) {
|
|
40
|
+
return Promise.all([
|
|
41
|
+
ctx.loadSSRModule(),
|
|
42
|
+
ctx.resolveStreamMode(request, env, url),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
const start = performance.now();
|
|
46
|
+
return Promise.all([
|
|
47
|
+
ctx.loadSSRModule().then((mod) => {
|
|
48
|
+
appendMetric(
|
|
49
|
+
getMetricsStore(),
|
|
50
|
+
"ssr:module-load",
|
|
51
|
+
start,
|
|
52
|
+
performance.now() - start,
|
|
53
|
+
);
|
|
54
|
+
return mod;
|
|
55
|
+
}),
|
|
56
|
+
ctx.resolveStreamMode(request, env, url).then((mode) => {
|
|
57
|
+
appendMetric(
|
|
58
|
+
getMetricsStore(),
|
|
59
|
+
"ssr:stream-mode",
|
|
60
|
+
start,
|
|
61
|
+
performance.now() - start,
|
|
62
|
+
);
|
|
63
|
+
return mode;
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve the SSR setup result. Returns the early-kicked-off promise
|
|
70
|
+
* when available (stashed on request variables), otherwise starts a
|
|
71
|
+
* fresh setup.
|
|
72
|
+
*/
|
|
73
|
+
export function getSSRSetup<TEnv>(
|
|
74
|
+
ctx: HandlerContext<TEnv>,
|
|
75
|
+
request: Request,
|
|
76
|
+
env: TEnv,
|
|
77
|
+
url: URL,
|
|
78
|
+
metricsStore: MetricsStore | undefined,
|
|
79
|
+
): Promise<SSRSetup> {
|
|
80
|
+
const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
|
|
81
|
+
| Promise<SSRSetup>
|
|
82
|
+
| undefined;
|
|
83
|
+
if (early) return early;
|
|
84
|
+
return startSSRSetup(
|
|
85
|
+
ctx,
|
|
86
|
+
request,
|
|
87
|
+
env,
|
|
88
|
+
url,
|
|
89
|
+
metricsStore ? () => metricsStore : undefined,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify whether a request may require SSR (HTML rendering).
|
|
95
|
+
*
|
|
96
|
+
* Returns false for requests that are definitively RSC-only, loader fetches,
|
|
97
|
+
* prerender collection, or Accept-based RSC (no text/html). This mirrors
|
|
98
|
+
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
|
+
*
|
|
100
|
+
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
+
* runs after classifyRequest() determines the request mode.
|
|
102
|
+
*/
|
|
103
|
+
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
|
+
if (
|
|
105
|
+
url.searchParams.has("_rsc_partial") ||
|
|
106
|
+
url.searchParams.has("_rsc_action") ||
|
|
107
|
+
request.headers.has("rsc-action") ||
|
|
108
|
+
url.searchParams.has("_rsc_loader") ||
|
|
109
|
+
url.searchParams.has("__rsc") ||
|
|
110
|
+
url.searchParams.has("__prerender_collect")
|
|
111
|
+
) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Mirror the Accept-based RSC decision from rsc-rendering.ts:
|
|
116
|
+
// if Accept is present and does not include text/html (and no __html override),
|
|
117
|
+
// the response will be RSC, not HTML.
|
|
118
|
+
const accept = request.headers.get("accept");
|
|
119
|
+
if (
|
|
120
|
+
accept &&
|
|
121
|
+
!accept.includes("text/html") &&
|
|
122
|
+
!url.searchParams.has("__html")
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
|
|
131
|
+
// from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
|
|
132
|
+
// Accept header as needing SSR; this treats it as RSC).
|
|
133
|
+
export function isRscRequest(
|
|
134
|
+
request: Request,
|
|
135
|
+
url: URL,
|
|
136
|
+
isPartial: boolean,
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
isPartial ||
|
|
140
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
141
|
+
!url.searchParams.has("__html")) ||
|
|
142
|
+
url.searchParams.has("__rsc")
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { MatchResult } from "../types.js";
|
|
2
|
+
import type { TransitionWhenContext } from "../types/segments.js";
|
|
3
|
+
import type { getRequestContext } from "../server/request-context.js";
|
|
4
|
+
import { invokeOnError } from "../router/error-handling.js";
|
|
5
|
+
import type { OnErrorCallback } from "../types/error-types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply transition({ when }) gates to a payload's segments.
|
|
9
|
+
*
|
|
10
|
+
* The predicates were collected during resolution (keyed by segment id) and
|
|
11
|
+
* stripped from the serialized config; here — after handlers ran and outside any
|
|
12
|
+
* cache scope — we evaluate each and drop the segment's transition when the
|
|
13
|
+
* predicate does not hold, so the navigation streams its loading fallback
|
|
14
|
+
* instead of holding the previous content. A predicate that throws is reported
|
|
15
|
+
* to the router's onError (phase "rendering") and then treated as "do not hold"
|
|
16
|
+
* (conservative), so a buggy predicate degrades to no transition rather than
|
|
17
|
+
* failing the response.
|
|
18
|
+
*
|
|
19
|
+
* Mutating the segments here is safe: the segment cache stores a serialized copy
|
|
20
|
+
* (segment-codec), written during match() BEFORE this gate runs, so dropping a
|
|
21
|
+
* transition never corrupts a cache entry. The flip side is that a cache hit
|
|
22
|
+
* skips resolution, collects no predicate, and replays the cached transition
|
|
23
|
+
* as-is (it was serialized before the gate) — combining transition({ when })
|
|
24
|
+
* with cache() on the same segment freezes the gate to its cached state, so
|
|
25
|
+
* avoid caching a route whose transition decision is request-dependent.
|
|
26
|
+
*
|
|
27
|
+
* Returns the same array (mutated) for inline use at the payload's `segments`
|
|
28
|
+
* field.
|
|
29
|
+
*/
|
|
30
|
+
export function gateTransitions(
|
|
31
|
+
segments: MatchResult["segments"],
|
|
32
|
+
ctx: ReturnType<typeof getRequestContext>,
|
|
33
|
+
onError?: OnErrorCallback,
|
|
34
|
+
): MatchResult["segments"] {
|
|
35
|
+
const predicates = ctx._transitionWhen;
|
|
36
|
+
if (predicates && predicates.length) {
|
|
37
|
+
for (const { id, when } of predicates) {
|
|
38
|
+
let drop: boolean;
|
|
39
|
+
try {
|
|
40
|
+
// Assemble the ShouldRevalidateFn-shaped predicate context from the
|
|
41
|
+
// request context. Source fields (currentUrl/currentParams/fromRouteName)
|
|
42
|
+
// were stashed at match time from the navigation snapshot; action fields
|
|
43
|
+
// at the action-bearing gate call sites. nextUrl/nextParams/toRouteName/
|
|
44
|
+
// method/get/env come straight off ctx (setRequestContextParams ran
|
|
45
|
+
// before the gate). Source/action fields are undefined when absent —
|
|
46
|
+
// never fabricated (see TransitionWhenContext).
|
|
47
|
+
const whenCtx: TransitionWhenContext = {
|
|
48
|
+
currentUrl: ctx._gateCurrentUrl,
|
|
49
|
+
currentParams: ctx._gateCurrentParams,
|
|
50
|
+
fromRouteName:
|
|
51
|
+
ctx._prevRouteKey as TransitionWhenContext["fromRouteName"],
|
|
52
|
+
nextUrl: ctx.url,
|
|
53
|
+
nextParams: ctx.params,
|
|
54
|
+
toRouteName: ctx.routeName,
|
|
55
|
+
actionId: ctx._gateActionId,
|
|
56
|
+
actionUrl: ctx._gateActionUrl,
|
|
57
|
+
actionResult: ctx._gateActionResult,
|
|
58
|
+
formData: ctx._gateFormData,
|
|
59
|
+
method: ctx.request.method,
|
|
60
|
+
get: ctx.get,
|
|
61
|
+
env: ctx.env,
|
|
62
|
+
};
|
|
63
|
+
drop = when(whenCtx) === false;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// A throwing predicate must not fail the response: report it and treat
|
|
66
|
+
// the transition as gated off (do not hold). invokeOnError no-ops when
|
|
67
|
+
// onError is undefined.
|
|
68
|
+
drop = true;
|
|
69
|
+
invokeOnError(
|
|
70
|
+
onError,
|
|
71
|
+
error,
|
|
72
|
+
"rendering",
|
|
73
|
+
{
|
|
74
|
+
request: ctx.request,
|
|
75
|
+
url: ctx.url,
|
|
76
|
+
params: ctx.params,
|
|
77
|
+
segmentId: id,
|
|
78
|
+
},
|
|
79
|
+
"RSC",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (drop) {
|
|
83
|
+
const seg = segments.find((s) => s.id === id);
|
|
84
|
+
if (seg) seg.transition = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return segments;
|
|
89
|
+
}
|