@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,426 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ppr
|
|
3
|
+
description: PPR shell caching — serve a cached HTML shell instantly and resume live loader holes (createShellCacheMiddleware)
|
|
4
|
+
argument-hint: "[setup]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# PPR Shell Caching
|
|
8
|
+
|
|
9
|
+
Caches the rendered HTML **shell** of a route (React `prerender` prelude bytes
|
|
10
|
+
plus `postponed` state) and, on a later request, flushes those bytes before any
|
|
11
|
+
render work happens, then resumes fizz for just the live loader holes. The
|
|
12
|
+
browser sees one ordinary streamed document; loaders stay fresh on every
|
|
13
|
+
request. This is the second render axis — the default axis-1 path is untouched,
|
|
14
|
+
and every ineligible request falls open to it.
|
|
15
|
+
|
|
16
|
+
Compare `/document-cache`, which freezes the WHOLE response including loader
|
|
17
|
+
output. Shell caching is for pages that mix a stable shell with live data: the
|
|
18
|
+
shell is shared per host+URL, the holes are per request.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
The middleware needs a store that implements the shell family
|
|
23
|
+
(`getShell`/`putShell`): `MemorySegmentCacheStore` (dev/tests), `CFCacheStore`
|
|
24
|
+
(Cloudflare KV), or `VercelCacheStore` (runtime cache). It defaults to the
|
|
25
|
+
app-level store from `createRouter({ cache })`; a store without the family
|
|
26
|
+
disables the middleware (fail-open to axis 1).
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createRouter } from "@rangojs/router";
|
|
30
|
+
import {
|
|
31
|
+
createShellCacheMiddleware,
|
|
32
|
+
CFCacheStore,
|
|
33
|
+
} from "@rangojs/router/cache";
|
|
34
|
+
import { urlpatterns } from "./urls";
|
|
35
|
+
|
|
36
|
+
const router = createRouter<AppBindings>({
|
|
37
|
+
document: Document,
|
|
38
|
+
urls: urlpatterns,
|
|
39
|
+
cache: (env, ctx) => ({
|
|
40
|
+
store: new CFCacheStore({ kv: env.CACHE_KV, ctx: ctx! }),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Path-scoped: only the routes that fit the shell/hole shape below.
|
|
45
|
+
router.use(
|
|
46
|
+
"/products",
|
|
47
|
+
createShellCacheMiddleware({ ttlSeconds: 600, swrSeconds: 120 }),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export default router;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Where PPR sits: the cache onion
|
|
54
|
+
|
|
55
|
+
Rango's caches layer like an onion — each ring stores a progressively more
|
|
56
|
+
"cooked" representation of the same page, and PPR is a new ring, not a
|
|
57
|
+
replacement for any existing one. From innermost (raw values) to outermost
|
|
58
|
+
(final bytes):
|
|
59
|
+
|
|
60
|
+
| Ring | Primitive | What is stored | What stays live on a hit |
|
|
61
|
+
| ----------------------- | ---------------------------------------- | -------------------------------------------------- | -------------------------------------- |
|
|
62
|
+
| 1. Function values | `"use cache"` | a function's return value | everything around the call |
|
|
63
|
+
| 2. Loader values | `loader(Fn, () => [cache({...})])` | one loader's result (opt-in; loaders default live) | all other loaders, handlers, rendering |
|
|
64
|
+
| 3. Segments (Flight) | `cache()` route / build-time `prerender` | serialized rendered segments + replayed handles | loaders, HTML render |
|
|
65
|
+
| 4. **HTML shell (PPR)** | `createShellCacheMiddleware` | rendered prelude bytes + React postponed state | loaders (the holes), hydration payload |
|
|
66
|
+
| 5. Whole response | `/document-cache` | final response bytes, headers included | nothing — all-or-nothing |
|
|
67
|
+
|
|
68
|
+
Each ring is derived from the ones inside it, and PPR makes that literal:
|
|
69
|
+
the captured shell is the fizz render of ring 3's replayed segments, which is
|
|
70
|
+
why shell/payload consistency holds by construction on `cache()` routes. The
|
|
71
|
+
rings compose in one request: a HIT serves ring 4's bytes instantly, the
|
|
72
|
+
resume pass replays ring 3's segments for the hydration payload, a hole's
|
|
73
|
+
loader may consult ring 2, and a component inside it may consult ring 1.
|
|
74
|
+
|
|
75
|
+
The onion also explains the boundary with ring 5: the document cache freezes
|
|
76
|
+
loaders too (no holes, coarser but simpler), which is why stacking both on
|
|
77
|
+
one route is pointless — pick the outermost ring whose "stays live" column
|
|
78
|
+
matches the route (see Pitfalls).
|
|
79
|
+
|
|
80
|
+
Invalidation crosses rings: `invalidateTags` reaches segment, shell, loader,
|
|
81
|
+
and item entries in the same store, and shell entries additionally
|
|
82
|
+
self-invalidate on `React.version` change. TTL/SWR are per-ring — an inner
|
|
83
|
+
ring's shorter TTL shows through a hole immediately (loaders are live), but
|
|
84
|
+
shell-embedded content refreshes only on the shell's own recapture.
|
|
85
|
+
|
|
86
|
+
## Creating holes: I want X → do Y
|
|
87
|
+
|
|
88
|
+
A shell-cached route is a stable shell with live **holes** punched through it.
|
|
89
|
+
Everything hinges on where the hole is, and the hole is always a route-level
|
|
90
|
+
`loading()` boundary. Start here:
|
|
91
|
+
|
|
92
|
+
| I want… | Do this |
|
|
93
|
+
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| Live per-request data in the page | a route `loader()` **+ route-level `loading()`** — the loader is the hole, `loading()` is the boundary the capture postpones at and the resume stitches into |
|
|
95
|
+
| A slow nested value streamed **inside** the hole | return `{ outer, pendingData: Promise }` from the loader; `use(pendingData)` under the consumer's OWN inner `<Suspense>` (see "Loader-carried promises") |
|
|
96
|
+
| A hole for data that is **already resolved** (in-memory, `Promise.resolve`, a cached read) | wrap it in **`live(() => …)`** under your own `<Suspense>` — masked at capture exactly like a loader, so it postpones instead of settling into the shared shell (see "live()") |
|
|
97
|
+
| Shell-safe, deterministic data | `await` it in a **handler** — it is shell material, frozen into the prelude |
|
|
98
|
+
| **Per-user** data | a `loader` (masked at capture, always fresh), or **`live()`** for a boundary that is not a route loader. NEVER a handler or middleware-derived `ctx` state — those run during capture and bake into the **shared** shell (see Security) |
|
|
99
|
+
| A slow nested value on a route with **no** `loading()` | still fine on axis 1: the tree-build await is SHALLOW, so `{ outer, pendingData }` streams the inner under the consumer's `<Suspense>` — but the route is not shell-cacheable |
|
|
100
|
+
|
|
101
|
+
The last row is the common trap: "no `loading()` blocks" does NOT mean "nested
|
|
102
|
+
promises can't stream". They stream today, unchanged by PPR — the route just
|
|
103
|
+
has no hole, so shell caching stays off for it (eternal MISS, below).
|
|
104
|
+
|
|
105
|
+
## The hole contract (read this before wiring a route)
|
|
106
|
+
|
|
107
|
+
A hole exists ONLY where the route-level `loading()` boundary separates loader
|
|
108
|
+
consumption from the shell. `loading()` becomes `LoaderBoundary`
|
|
109
|
+
(`src/route-content-wrapper.tsx`) — a `<Suspense>` whose resolver `use()`es the
|
|
110
|
+
loader promise INSIDE it — so the masked loader postpones exactly there and the
|
|
111
|
+
prelude freezes the layouts plus the fallback. Two consequences:
|
|
112
|
+
|
|
113
|
+
1. Shell material (static content, handle reads, interactive client islands)
|
|
114
|
+
lives in a **layout** above the loader route.
|
|
115
|
+
2. The loader-consuming route below carries **`loading()`**.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
119
|
+
// Shell: header, nav, islands, handle pushes. Frozen into the prelude.
|
|
120
|
+
layout(ProductShellLayout, () => [
|
|
121
|
+
// Hole: the live price. Masked at capture, fresh on every serve.
|
|
122
|
+
path("/products/:id", PricePage, { name: "product" }, () => [
|
|
123
|
+
loader(LivePriceLoader),
|
|
124
|
+
loading(<PriceSkeleton />), // the boundary capture postpones at
|
|
125
|
+
]),
|
|
126
|
+
]),
|
|
127
|
+
]);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Why a route without `loading()` can never be a hole — even with a fast
|
|
131
|
+
loader.** The loading-less branch in `renderSegments` (`src/segment-system.tsx`)
|
|
132
|
+
awaits loader data at TREE-BUILD (`await buildLoaderPromise(...)`), above every
|
|
133
|
+
Suspense boundary. That await is SHALLOW — it settles only the loader's OUTER
|
|
134
|
+
value — but during capture the loader is masked WHOLE: `createMaskedLoaderPromise`
|
|
135
|
+
(`src/router/segment-resolution/loader-mask.ts`) hands back a never-resolving
|
|
136
|
+
promise for the entire value, so even the outer never settles. The tree-build
|
|
137
|
+
await pins the whole tree above `<body>`, the prelude comes back trivial, and
|
|
138
|
+
the sanity gate refuses to store. Observable symptom: `x-rango-shell: MISS` on
|
|
139
|
+
every request forever, plus a **once-per-key** worker warning you can grep for —
|
|
140
|
+
`produced no usable shell … without a route-level loading() boundary`
|
|
141
|
+
(`src/rsc/shell-capture.ts`).
|
|
142
|
+
|
|
143
|
+
Whole-loader masking is deliberate scar tissue, not a limitation to route
|
|
144
|
+
around. Loaders are the ONE lane exempt from the `cookies()`/`headers()` capture
|
|
145
|
+
guard — they always run fresh on serve — so running a loader even _partially_
|
|
146
|
+
during capture could bake a per-user outer field into the shared shell, breaking
|
|
147
|
+
freshness and the security model at once. Finer-grained masking is intentionally
|
|
148
|
+
not offered. A hand-rolled `<Suspense>` around a `useLoader()` reader does not
|
|
149
|
+
help either: the pin is at the tree-build await, which is _above_ it.
|
|
150
|
+
|
|
151
|
+
## Loader-carried promises: streaming inside a hole
|
|
152
|
+
|
|
153
|
+
A hole is not limited to one value. A loader can return its outer value fast and
|
|
154
|
+
carry a **nested promise** that settles later; `FlightSerialize` preserves the
|
|
155
|
+
`Promise` across the RSC boundary (`src/serialize.ts`), so the client `use()`es
|
|
156
|
+
it under its OWN inner `<Suspense>` — a second streaming layer _inside_ the hole.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// loader — outer resolves fast; the nested promise settles later
|
|
160
|
+
export const StreamLoader = createLoader(async () => {
|
|
161
|
+
const pendingData = new Promise<string>((r) =>
|
|
162
|
+
setTimeout(() => r("slow inner value"), 300),
|
|
163
|
+
);
|
|
164
|
+
return { label: "fast outer value", pendingData };
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// consumer (client): use() the nested promise under an INNER Suspense
|
|
170
|
+
"use client";
|
|
171
|
+
import { Suspense, use } from "react";
|
|
172
|
+
import { useLoader } from "@rangojs/router/client";
|
|
173
|
+
|
|
174
|
+
function Inner({ promise }: { promise: Promise<string> }) {
|
|
175
|
+
return <span>{use(promise)}</span>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function StreamView({ loader }: { loader: LoaderDefinition<Data> }) {
|
|
179
|
+
const { data } = useLoader(loader); // resolves the OUTER value
|
|
180
|
+
return (
|
|
181
|
+
<>
|
|
182
|
+
<div>{data.label}</div>
|
|
183
|
+
<Suspense fallback={<div>loading inner…</div>}>
|
|
184
|
+
<Inner promise={data.pendingData} /> {/* streams the nested value */}
|
|
185
|
+
</Suspense>
|
|
186
|
+
</>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Route shape is unchanged: `loader(StreamLoader)` + `loading(<Skeleton />)`. On a
|
|
192
|
+
**HIT** the resume streams three progressive layers in one response body:
|
|
193
|
+
|
|
194
|
+
1. the cached shell prelude (layout + the `loading()` fallback) — flushed
|
|
195
|
+
instantly, before any render work;
|
|
196
|
+
2. the outer loader value fills the hole, carrying the inner `<Suspense>`
|
|
197
|
+
fallback;
|
|
198
|
+
3. the nested-promise inner value + React's `$RC` boundary stitch.
|
|
199
|
+
|
|
200
|
+
Capture never sees any of this: the loader is masked, so the whole subtree
|
|
201
|
+
postpones at `loading()` and the nested promise costs nothing at capture time.
|
|
202
|
+
That is what makes loader-carried promises DETERMINISTIC — contrast the
|
|
203
|
+
handler-passed promise below, which races the capture's quiet window. The
|
|
204
|
+
three-layer timeline is pinned in dev + production e2e
|
|
205
|
+
(`tests/cloudflare-basic/e2e/ppr-shell.test.ts`, `e2e/shell-cache.test.ts`).
|
|
206
|
+
|
|
207
|
+
One nuance: a loader with a `cache(...)` config deep-settles on write, so a
|
|
208
|
+
loader-cache HIT delivers the inner promise already resolved.
|
|
209
|
+
|
|
210
|
+
## live(): a deterministic hole for any boundary
|
|
211
|
+
|
|
212
|
+
`loading()` makes a route LOADER a hole. `live()` makes ANY boundary a hole —
|
|
213
|
+
including one whose data is already resolved. During the background capture
|
|
214
|
+
`live()` behaves exactly like the loader mask: it returns a never-settling
|
|
215
|
+
promise, so the consuming `<Suspense>` postpones and the prelude freezes only the
|
|
216
|
+
fallback. On the serve pass (and on the client) it is a passthrough — the thunk
|
|
217
|
+
runs, or the promise passes through unchanged.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import { Suspense } from "react";
|
|
221
|
+
import { live } from "@rangojs/router";
|
|
222
|
+
|
|
223
|
+
async function Greeting() {
|
|
224
|
+
// Promise.resolve(...) would normally SETTLE during capture and bake into the
|
|
225
|
+
// shared shell. live() holds it out, so this boundary postpones instead.
|
|
226
|
+
const name = await live(() => Promise.resolve(currentUserName()));
|
|
227
|
+
return <span>Hi {name}</span>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// under the frozen shell:
|
|
231
|
+
// <Suspense fallback={<span>…</span>}>
|
|
232
|
+
// <Greeting />
|
|
233
|
+
// </Suspense>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Two forms:
|
|
237
|
+
|
|
238
|
+
- **Thunk (preferred): `live(() => value)`** — during capture the thunk NEVER
|
|
239
|
+
runs (no fetch, no cost); the boundary is a pure hole.
|
|
240
|
+
- **Value: `live(promise)`** — the work already fired before `live()` saw it, so
|
|
241
|
+
during capture the real promise is DISCARDED and a hole returned in its place.
|
|
242
|
+
Use it only when you already hold the promise; prefer the thunk otherwise.
|
|
243
|
+
|
|
244
|
+
`live()` is what makes a resolved value a hole at all: a bare `Promise.resolve(x)`
|
|
245
|
+
under `<Suspense>` settles inside the capture's quiet window and freezes into the
|
|
246
|
+
shell. It is also the escape hatch for the passed-promise trap below. The
|
|
247
|
+
capture/serve split is pinned in dev + production e2e (the "live() makes a
|
|
248
|
+
resolved promise a HOLE" case in `tests/cloudflare-basic/e2e/ppr-shell.test.ts`
|
|
249
|
+
and `e2e/shell-cache.test.ts`).
|
|
250
|
+
|
|
251
|
+
## Passed promises are not holes
|
|
252
|
+
|
|
253
|
+
The pattern that looks like a hole but is not: a **handler** creates a promise
|
|
254
|
+
and passes it as a prop to a client component that `use()`s it inside its own
|
|
255
|
+
`<Suspense>`. Only route **loaders** (and `live()`) are masked at capture — a
|
|
256
|
+
handler and any promise it creates EXECUTE during the background capture render.
|
|
257
|
+
What happens next is decided by the promise's LATENCY CLASS against the
|
|
258
|
+
capture's quiet window (task-quantized: it closes a couple of macrotask hops
|
|
259
|
+
after the last Flight byte, not on a wall clock). Both sides are reliable —
|
|
260
|
+
just in opposite directions:
|
|
261
|
+
|
|
262
|
+
- Resolved or microtask-resolvable (`Promise.resolve`, a warm in-memory read):
|
|
263
|
+
reliably SHELL, every capture — it settles in the same window as plain JSX.
|
|
264
|
+
If the value is per-request, that is a deterministic bug: frozen into the
|
|
265
|
+
shared shell until TTL (hydration repairs it from the fresh payload —
|
|
266
|
+
degraded, not corrupt, but a drift you shipped).
|
|
267
|
+
- Genuinely pending real I/O: reliably a HOLE — it cannot win a task-quantized
|
|
268
|
+
window. Resume fills it at serve. The capture still paid the promise's
|
|
269
|
+
execution cost and side effects, though. The only nondeterministic sliver
|
|
270
|
+
left is I/O completing within ~2 event-loop turns of the shell going quiet
|
|
271
|
+
— freakishly fast, self-healing via TTL/recapture, and only reachable by
|
|
272
|
+
code that declared no intent.
|
|
273
|
+
|
|
274
|
+
An async HANDLER (a streamed `loading()` handler returning a promise) is the
|
|
275
|
+
deliberate opposite: it is tracked in the handle store, and capture WAITS for
|
|
276
|
+
handlers to settle before aborting — handler output is shell material by
|
|
277
|
+
design, never a hole.
|
|
278
|
+
|
|
279
|
+
Verdict: a promise's latency class picks its side — you can safely assume a
|
|
280
|
+
genuinely pending, unresolved promise becomes a hole. But that decision was
|
|
281
|
+
made by latency, not by you. Wherever intent and latency could disagree —
|
|
282
|
+
per-request data that might get cache-fast, a value that must never appear in
|
|
283
|
+
the shared shell — say it in code: **`live()`** for a guaranteed hole (masked
|
|
284
|
+
at capture like a loader; prefer the thunk form so nothing runs during
|
|
285
|
+
capture), a loader behind `loading()` for route-level live data (zero capture
|
|
286
|
+
cost, and its nested promises stream too, per above), a plain `await` for
|
|
287
|
+
shell-safe deterministic data. Unwrapped promises are for the cases where
|
|
288
|
+
either outcome is acceptable.
|
|
289
|
+
|
|
290
|
+
Note on `useLoader()`: it never observes pending data. Inside a `loading()`
|
|
291
|
+
route, `LoaderBoundary` resolves the loader promise INSIDE its own Suspense
|
|
292
|
+
before children render, so `useLoader().data` (the OUTER value) is always
|
|
293
|
+
resolved; `isLoading` is client-side refetch state, not a server pending signal.
|
|
294
|
+
A nested promise on that data is separate — it streams under the consumer's own
|
|
295
|
+
inner `<Suspense>` (above). Multiple holes per page (several `loading()`
|
|
296
|
+
routes/parallels) are fine: resume fills every postponed boundary.
|
|
297
|
+
|
|
298
|
+
## Execution matrix
|
|
299
|
+
|
|
300
|
+
Three passes, three different cost profiles. The foreground request is never
|
|
301
|
+
blocked on the background capture.
|
|
302
|
+
|
|
303
|
+
| Phase | MISS (foreground) | Background capture | HIT (foreground) |
|
|
304
|
+
| ---------------- | ---------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
305
|
+
| Middleware chain | runs (full) | **NOT re-run** — inherits the request's post-middleware context | runs (full) |
|
|
306
|
+
| `router.match` | runs | re-runs under a derived context | runs |
|
|
307
|
+
| Handlers | run | run | run |
|
|
308
|
+
| Loaders | run **fresh** | **MASKED** (never execute) | run **fresh** |
|
|
309
|
+
| Flight render | full | full | full (hydration needs the whole payload — no Flight resume) |
|
|
310
|
+
| HTML production | full fizz | `prerender` + abort → prelude + postponed | `resume` only the holes — O(paths to holes) |
|
|
311
|
+
| Shell store | schedules a bg capture | `putShell(key, …)` | `getShell(key)`; a stale/SWR hit also schedules a recapture |
|
|
312
|
+
| Prelude bytes | — | — | prepended by the middleware before the resumed body |
|
|
313
|
+
|
|
314
|
+
Loader freshness under PPR is **identical to axis 1**: loaders — the outer value
|
|
315
|
+
AND any nested promise — run fresh on every request, including HITs. Only the
|
|
316
|
+
HTML _around_ the hole came from cache. Background capture is scheduled via
|
|
317
|
+
`runBackground` (`waitUntil` on workerd, fire-and-forget in Node dev), so it
|
|
318
|
+
never delays the served response. Re-deriving through `router.match()` rather
|
|
319
|
+
than a second `next()` is what keeps middleware from running twice
|
|
320
|
+
(`src/rsc/shell-capture.ts`).
|
|
321
|
+
|
|
322
|
+
## Security
|
|
323
|
+
|
|
324
|
+
Shell caching shares one shell per host+URL across all users, so its safety
|
|
325
|
+
rests on three things — the first two are enforced, the third is on you.
|
|
326
|
+
|
|
327
|
+
**(a) Access control is sound.** The middleware runs on every request, including
|
|
328
|
+
HITs, and composition is **marker-gated**: the middleware prepends the cached
|
|
329
|
+
prelude ONLY when the live response carries the internal `x-rango-shell-resumed`
|
|
330
|
+
marker (`src/cache/shell-cache.ts`). Any middleware short-circuit — a 401, a
|
|
331
|
+
redirect, a 404 — never resumes, so it never carries the marker and passes
|
|
332
|
+
through **untouched**, never composed with a cached shell. Put auth middleware
|
|
333
|
+
upstream of the shell middleware and unauthorized users get their 401/redirect,
|
|
334
|
+
not someone else's cached page.
|
|
335
|
+
|
|
336
|
+
**(b) Identity can't leak into a shared shell.** `cookies()` and `headers()`
|
|
337
|
+
THROW during the background capture render (`assertNotInsideShellCapture`,
|
|
338
|
+
`src/server/cookie-store.ts`), the same guard family as `"use cache"` and
|
|
339
|
+
`cache()`. A shell that reads cookies is PPR-ineligible by construction.
|
|
340
|
+
|
|
341
|
+
**(c) Residual hazard — state it plainly.** Middleware-derived per-user state is
|
|
342
|
+
NOT guarded: a `ctx` variable set by an upstream auth middleware and read by a
|
|
343
|
+
handler WITHOUT `cookies()`/`headers()` is invisible to guard (b). The background
|
|
344
|
+
capture inherits the triggering request's post-middleware context and bakes that
|
|
345
|
+
state into the shared shell. Mitigations, in order of preference:
|
|
346
|
+
|
|
347
|
+
- shell-cache only **public/shared** pages;
|
|
348
|
+
- put all per-user content in **loaders** (the enforced, masked lane);
|
|
349
|
+
- `isEnabled` to disable the middleware for authenticated sessions;
|
|
350
|
+
- `keyGenerator` to add a per-variant dimension (it owns the FULL key identity,
|
|
351
|
+
including host — see Options).
|
|
352
|
+
|
|
353
|
+
Shell content that still varies per request degrades to a hydration repair
|
|
354
|
+
(bounded by TTL/SWR), not corruption — but it is a smell. `cache()` the route so
|
|
355
|
+
the same replayed segments feed the captured shell and every resumed render.
|
|
356
|
+
|
|
357
|
+
## What always stays on axis 1
|
|
358
|
+
|
|
359
|
+
Non-GET, RSC/partial/action/loader fetches, per-request CSP nonce,
|
|
360
|
+
`streamMode: "allReady"`, redirects, 404s, error renders, and any store without
|
|
361
|
+
the shell family. A stored shell is also invalidated when `React.version`
|
|
362
|
+
changes (postponed state is build-coupled), so deploys self-heal via recapture.
|
|
363
|
+
|
|
364
|
+
First byte on a HIT does not wait on the shell render or the loader. Hydration
|
|
365
|
+
uses the fresh per-request Flight payload, so interactivity is unaffected.
|
|
366
|
+
|
|
367
|
+
## Partial navigations
|
|
368
|
+
|
|
369
|
+
Soft navigations (`_rsc_partial`) bypass this middleware, and that is by
|
|
370
|
+
design, not a gap: a partial response has no HTML tier — no fizz render to
|
|
371
|
+
skip, which is the entire cost document-PPR eliminates. On a `cache()` route a
|
|
372
|
+
partial navigation already delivers the PPR contract at the data tier:
|
|
373
|
+
replayed cached segments flush immediately (in-memory after the store read),
|
|
374
|
+
loaders run fresh and stream their rows into the same response, and the
|
|
375
|
+
client shows `loading()` fallbacks until they arrive. Shell instantly, live
|
|
376
|
+
holes revived — same semantics, different wire format.
|
|
377
|
+
|
|
378
|
+
For warm-navigation latency, combine `cache()` with prefetching: a prefetched
|
|
379
|
+
partial payload is the client-side analogue of the shell cache, and loaders
|
|
380
|
+
still stream fresh on arrival. Document-PPR covers the cold full-document
|
|
381
|
+
load; prefetch + `cache()` covers navigation.
|
|
382
|
+
|
|
383
|
+
What partial navigations do NOT get is the byte-level shortcut: the worker
|
|
384
|
+
still deserializes stored segments and re-encodes the Flight payload per
|
|
385
|
+
request. Serving a stored Flight byte-prefix and appending fresh loader rows
|
|
386
|
+
would require hand-managed row-ID alignment — React has no Flight-side
|
|
387
|
+
resume (no postponed-state equivalent exists for Flight) — and is a deferred
|
|
388
|
+
optimization, tracked in the design doc's out-of-scope list.
|
|
389
|
+
|
|
390
|
+
## Options
|
|
391
|
+
|
|
392
|
+
| Option | Default | Notes |
|
|
393
|
+
| -------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
394
|
+
| `store` | app-level `_cacheStore` | must implement `getShell`/`putShell`; the capture writes to the SAME store the middleware reads |
|
|
395
|
+
| `ttlSeconds` | `300` | shell freshness window |
|
|
396
|
+
| `swrSeconds` | — | stale window: serve stale + background recapture |
|
|
397
|
+
| `keyGenerator` | `${host}${pathname}${sortedSearch}` | custom keys own the FULL identity — include the host unless the store is provably single-host (multi-tenant shells must never collide) |
|
|
398
|
+
| `isEnabled` | — | per-request opt-out predicate (e.g. disable for authed sessions) |
|
|
399
|
+
| `skipPaths` | `[]` | path-prefix opt-out |
|
|
400
|
+
| `debug` | `false` | HIT/MISS/CAPTURED logging |
|
|
401
|
+
|
|
402
|
+
## Pitfalls
|
|
403
|
+
|
|
404
|
+
- **Loader route without `loading()`**: eternal MISS plus a once-per-key
|
|
405
|
+
console warning. Move shell material to a layout and add `loading()` (see "The
|
|
406
|
+
hole contract").
|
|
407
|
+
- **Handler-passed promise for live data**: nondeterministic race, drift into
|
|
408
|
+
the shared shell. Use a loader behind `loading()`, or wrap it in `live()`.
|
|
409
|
+
- **`live()` value form (`live(promise)`)**: the work already fired before
|
|
410
|
+
`live()` saw it, so during capture the promise still runs and its side effects
|
|
411
|
+
still happen — only its result is held out of the shell. Prefer the thunk form
|
|
412
|
+
`live(() => …)` so nothing executes during capture.
|
|
413
|
+
- **Per-user state via `ctx` variables**: not guarded — see Security (c).
|
|
414
|
+
- **Stacking with `/document-cache`**: pick one per route. The document cache
|
|
415
|
+
would cache the composite — correct output, but it makes shell caching
|
|
416
|
+
redundant there.
|
|
417
|
+
- **Dev + HMR**: works, but edits produce stale shells until TTL/recapture.
|
|
418
|
+
- A cold-worker capture can occasionally abort mid-render; it is logged as
|
|
419
|
+
retryable and the next request recaptures — self-healing, not an error.
|
|
420
|
+
|
|
421
|
+
## Related
|
|
422
|
+
|
|
423
|
+
- `/document-cache` — whole-response edge caching (no live holes)
|
|
424
|
+
- `/caching` and `/cache-guide` — segment/function caching (axis 1 data)
|
|
425
|
+
- `/shell-manifest` — replayed handles as cache metadata read by live loaders
|
|
426
|
+
- Design doc: `docs/design/ppr-shell-resume.md` in the package
|