@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical inbound-Cookie-header parser.
|
|
3
|
+
*
|
|
4
|
+
* Kept as a dependency-free leaf so any consumer (request-context, the host
|
|
5
|
+
* dispatcher, tests) can share one implementation without pulling a heavier
|
|
6
|
+
* module's graph. A duplicate copy in middleware-cookies.ts was removed; the
|
|
7
|
+
* host copy in cookie-handler.ts was collapsed onto this one. Not part of the
|
|
8
|
+
* public export surface.
|
|
9
|
+
*/
|
|
10
|
+
export function parseCookiesFromHeader(
|
|
11
|
+
cookieHeader: string | null,
|
|
12
|
+
): Record<string, string> {
|
|
13
|
+
if (!cookieHeader) return {};
|
|
14
|
+
|
|
15
|
+
const cookies: Record<string, string> = {};
|
|
16
|
+
const pairs = cookieHeader.split(";");
|
|
17
|
+
|
|
18
|
+
for (const pair of pairs) {
|
|
19
|
+
const [name, ...rest] = pair.trim().split("=");
|
|
20
|
+
if (name) {
|
|
21
|
+
const raw = rest.join("=");
|
|
22
|
+
try {
|
|
23
|
+
cookies[name] = decodeURIComponent(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
// Malformed percent-encoding: fall back to raw value
|
|
26
|
+
cookies[name] = raw;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return cookies;
|
|
32
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie Store — Next.js-style cookie facade backed by the response-derived model.
|
|
3
|
+
*
|
|
4
|
+
* `cookies()` returns a CookieStore scoped to the current request.
|
|
5
|
+
* Reads merge the original Cookie header with Set-Cookie mutations
|
|
6
|
+
* already queued on the response stub (last-write-wins).
|
|
7
|
+
* Writes append Set-Cookie to the response stub.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CookieOptions } from "../router/middleware-types.js";
|
|
11
|
+
import { getRequestContext, _getRequestContext } from "./request-context.js";
|
|
12
|
+
import { isInsideCacheScope } from "./context.js";
|
|
13
|
+
import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A single cookie entry returned by get() and getAll().
|
|
17
|
+
*/
|
|
18
|
+
export interface Cookie {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Request-scoped cookie store.
|
|
25
|
+
*
|
|
26
|
+
* Reads see the effective merged view (original request + same-request mutations).
|
|
27
|
+
* Writes append Set-Cookie headers to the shared response stub.
|
|
28
|
+
*/
|
|
29
|
+
export interface CookieStore {
|
|
30
|
+
/** Get a single cookie by name. Returns undefined if not set or deleted. */
|
|
31
|
+
get(name: string): Cookie | undefined;
|
|
32
|
+
|
|
33
|
+
/** Get all effective cookies, or all cookies with a given name. */
|
|
34
|
+
getAll(name?: string): Cookie[];
|
|
35
|
+
|
|
36
|
+
/** Check whether a cookie exists in the effective view. */
|
|
37
|
+
has(name: string): boolean;
|
|
38
|
+
|
|
39
|
+
/** Set a cookie (appends Set-Cookie to the response stub). */
|
|
40
|
+
set(name: string, value: string, options?: CookieOptions): void;
|
|
41
|
+
|
|
42
|
+
/** Delete a cookie (appends Set-Cookie with maxAge=0 to the response stub). */
|
|
43
|
+
delete(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the request-scoped cookie store.
|
|
48
|
+
*
|
|
49
|
+
* Must be called inside a request context (middleware, handler, loader, action).
|
|
50
|
+
* Throws if called outside request scope.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* import { cookies } from "@rangojs/router";
|
|
55
|
+
*
|
|
56
|
+
* // In a handler, loader, or action:
|
|
57
|
+
* const session = cookies().get("session")?.value;
|
|
58
|
+
* cookies().set("session", "new-token", { httpOnly: true });
|
|
59
|
+
* cookies().delete("session");
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function cookies(): CookieStore {
|
|
63
|
+
const ctx = getRequestContext();
|
|
64
|
+
assertNotInsideCacheContext(ctx, "cookies");
|
|
65
|
+
assertNotInsideShellCapture(ctx, "cookies");
|
|
66
|
+
return createCookieStore(ctx);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read-only view of HTTP headers.
|
|
71
|
+
* Exposes only the read methods of the Headers API.
|
|
72
|
+
*/
|
|
73
|
+
export interface ReadonlyHeaders {
|
|
74
|
+
get(name: string): string | null;
|
|
75
|
+
has(name: string): boolean;
|
|
76
|
+
entries(): HeadersIterator<[string, string]>;
|
|
77
|
+
keys(): HeadersIterator<string>;
|
|
78
|
+
values(): HeadersIterator<string>;
|
|
79
|
+
forEach(
|
|
80
|
+
callback: (value: string, name: string, parent: ReadonlyHeaders) => void,
|
|
81
|
+
): void;
|
|
82
|
+
[Symbol.iterator](): HeadersIterator<[string, string]>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Minimal iterator interface (avoids pulling IterableIterator from lib.dom)
|
|
86
|
+
type HeadersIterator<T> = IterableIterator<T>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Throw if called inside a cache boundary — either a "use cache" function
|
|
90
|
+
* (`INSIDE_CACHE_EXEC` stamped on ctx by the cache runtime) or a `cache()`
|
|
91
|
+
* DSL boundary (`isInsideCacheScope()` — the render-store flag set while
|
|
92
|
+
* resolving a `type: "cache"` route entry).
|
|
93
|
+
*
|
|
94
|
+
* Reading request-scoped data (cookies, headers) inside a cached scope
|
|
95
|
+
* produces per-request values that are NOT reflected in the cache key, so
|
|
96
|
+
* they would be frozen into the shared cache entry and served to the wrong
|
|
97
|
+
* users. This is the same hazard for both scopes: a `cache()` boundary caches
|
|
98
|
+
* everything except loaders (it is the document-level "PPR shell"), so a read
|
|
99
|
+
* here is baked into the shell exactly like a `"use cache"` return value is
|
|
100
|
+
* baked into its cache entry.
|
|
101
|
+
*
|
|
102
|
+
* `isInsideCacheScope()` returns false inside loaders (loaders always run
|
|
103
|
+
* fresh on every request, even on a cache hit), so reading cookies()/headers()
|
|
104
|
+
* from a loader is allowed — loaders are the dynamic "holes" of a cached
|
|
105
|
+
* document.
|
|
106
|
+
*/
|
|
107
|
+
function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
|
|
108
|
+
if (
|
|
109
|
+
ctx !== null &&
|
|
110
|
+
ctx !== undefined &&
|
|
111
|
+
typeof ctx === "object" &&
|
|
112
|
+
(INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
|
|
113
|
+
) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${fnName}() cannot be called inside a "use cache" function. ` +
|
|
116
|
+
`Request-scoped data (cookies, headers) varies per request but is not ` +
|
|
117
|
+
`reflected in the cache key, so cached results would be served to the ` +
|
|
118
|
+
`wrong users. Extract the value before the cached function and pass it ` +
|
|
119
|
+
`as an argument:\n\n` +
|
|
120
|
+
` const locale = cookies().get("locale")?.value ?? "en";\n` +
|
|
121
|
+
` const data = await getCachedData(locale); // locale is now in the cache key`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (isInsideCacheScope()) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`${fnName}() cannot be called inside a cache() boundary. ` +
|
|
127
|
+
`A cache() scope caches everything except loaders, so request-scoped ` +
|
|
128
|
+
`data (cookies, headers) read here would be frozen into the shared ` +
|
|
129
|
+
`cached shell and served to other users. Read it inside a loader ` +
|
|
130
|
+
`instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
|
|
131
|
+
` loader("user", () => getUser(cookies().get("session")?.value));`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Throw if called during the ACTIVE background shell-capture render
|
|
138
|
+
* (`_shellCaptureRun` true on the derived request context built by
|
|
139
|
+
* shell-capture.ts). The captured shell prelude is shared across every user
|
|
140
|
+
* hitting the URL, so a request-scoped read here would bake one user's
|
|
141
|
+
* cookies/headers into markup served to others — same hazard as the cache
|
|
142
|
+
* scopes above, at the document tier. Loaders need no exemption: they are
|
|
143
|
+
* masked (never executed) during capture and remain the per-request holes of
|
|
144
|
+
* the shell.
|
|
145
|
+
*
|
|
146
|
+
* Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
|
|
147
|
+
* is also present during the FOREGROUND render (it means "a capture is wanted"),
|
|
148
|
+
* and the foreground must read cookies/headers normally to serve the real user.
|
|
149
|
+
* Only the derived capture context sets `_shellCaptureRun`.
|
|
150
|
+
*
|
|
151
|
+
* Applies only to the READ surfaces (cookies(), headers()) whose values
|
|
152
|
+
* become markup. Response directives (invalidateClientCache(),
|
|
153
|
+
* keepClientCache()) stay callable: during capture they are header effects on
|
|
154
|
+
* a discarded response, and on the live HIT path the full pipeline runs so their
|
|
155
|
+
* headers flow to the client normally.
|
|
156
|
+
*
|
|
157
|
+
* The throw makes such a route PPR-ineligible by construction: the capture
|
|
158
|
+
* render errors, nothing is stored, and every request keeps getting the
|
|
159
|
+
* normal axis-1 render.
|
|
160
|
+
*/
|
|
161
|
+
function assertNotInsideShellCapture(ctx: unknown, fnName: string): void {
|
|
162
|
+
if (
|
|
163
|
+
ctx !== null &&
|
|
164
|
+
typeof ctx === "object" &&
|
|
165
|
+
(ctx as { _shellCaptureRun?: unknown })._shellCaptureRun === true
|
|
166
|
+
) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`${fnName}() cannot be called while capturing a shared shell ` +
|
|
169
|
+
`(shell-cache middleware). The captured shell is served to every user ` +
|
|
170
|
+
`of this URL, so request-scoped data read here would leak one user's ` +
|
|
171
|
+
`${fnName === "cookies" ? "cookies" : "headers"} to others. Read it ` +
|
|
172
|
+
`inside a loader instead — loaders are never captured and always run ` +
|
|
173
|
+
`fresh per request:\n\n` +
|
|
174
|
+
` loader("user", () => getUser(cookies().get("session")?.value));`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get the original request headers (read-only).
|
|
183
|
+
*
|
|
184
|
+
* Must be called inside a request context.
|
|
185
|
+
* Returns a read-only view of the incoming request's headers.
|
|
186
|
+
* Mutation methods (set, append, delete) throw at runtime.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* import { headers } from "@rangojs/router";
|
|
191
|
+
*
|
|
192
|
+
* const auth = headers().get("authorization");
|
|
193
|
+
* const contentType = headers().get("content-type");
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export function headers(): ReadonlyHeaders {
|
|
197
|
+
const ctx = getRequestContext();
|
|
198
|
+
assertNotInsideCacheContext(ctx, "headers");
|
|
199
|
+
assertNotInsideShellCapture(ctx, "headers");
|
|
200
|
+
return new Proxy(ctx.request.headers, {
|
|
201
|
+
get(target, prop, receiver) {
|
|
202
|
+
if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
|
|
203
|
+
return () => {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`headers().${prop}() is not allowed. headers() returns a read-only view of request headers. ` +
|
|
206
|
+
`Use ctx.header() to set response headers.`,
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const value = Reflect.get(target, prop, receiver);
|
|
211
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
212
|
+
},
|
|
213
|
+
}) as unknown as ReadonlyHeaders;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Force the calling client's caches to miss from now on, from the server seat:
|
|
218
|
+
* write a rotated `Set-Cookie` for the rango state. The responding client
|
|
219
|
+
* applies it on receipt, and its history cache is marked stale by the
|
|
220
|
+
* jar-divergence observer at its next read. Per-client and lazy — it rotates
|
|
221
|
+
* only the client that receives this response, not every client.
|
|
222
|
+
*
|
|
223
|
+
* Idempotent within a request (one `Set-Cookie`). Inert (a dev warning) when
|
|
224
|
+
* called outside a request context. Like `cookies()`, it throws inside a
|
|
225
|
+
* `"use cache"` / `cache()` boundary, but is allowed from a loader (loaders are
|
|
226
|
+
* the dynamic holes of a cached document).
|
|
227
|
+
*/
|
|
228
|
+
export function invalidateClientCache(): void {
|
|
229
|
+
const ctx = _getRequestContext();
|
|
230
|
+
if (!ctx) {
|
|
231
|
+
if (process.env.NODE_ENV !== "production") {
|
|
232
|
+
console.warn(
|
|
233
|
+
"[rango] invalidateClientCache() was called outside a request context; ignored.",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
assertNotInsideCacheContext(ctx, "invalidateClientCache");
|
|
239
|
+
ctx._rotateStateCookie();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Suppress a server action's automatic client-cache invalidation: tell the
|
|
244
|
+
* action bridge this action changed nothing a route renders, so it should leave
|
|
245
|
+
* the client's state and caches alone (no rotation, no prefetch wipe, no
|
|
246
|
+
* broadcast, no revalidation refetch). Per-response, not per-action-definition —
|
|
247
|
+
* only the execution knows whether anything changed.
|
|
248
|
+
*
|
|
249
|
+
* Sets an internal response header the bridge reads. Idempotent within a
|
|
250
|
+
* request. Inert (a dev warning) outside a request context — there is no
|
|
251
|
+
* automatic invalidation to suppress.
|
|
252
|
+
*/
|
|
253
|
+
export function keepClientCache(): void {
|
|
254
|
+
const ctx = _getRequestContext();
|
|
255
|
+
if (!ctx) {
|
|
256
|
+
if (process.env.NODE_ENV !== "production") {
|
|
257
|
+
console.warn(
|
|
258
|
+
"[rango] keepClientCache() was called outside a request context; ignored.",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
assertNotInsideCacheContext(ctx, "keepClientCache");
|
|
264
|
+
ctx._setKeepCacheDirective();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create a CookieStore backed by a RequestContext.
|
|
269
|
+
* @internal Shared between cookies() shorthand and context methods.
|
|
270
|
+
*/
|
|
271
|
+
function createCookieStore(ctx: {
|
|
272
|
+
cookie(name: string): string | undefined;
|
|
273
|
+
cookies(): Record<string, string>;
|
|
274
|
+
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
275
|
+
deleteCookie(
|
|
276
|
+
name: string,
|
|
277
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
278
|
+
): void;
|
|
279
|
+
}): CookieStore {
|
|
280
|
+
return {
|
|
281
|
+
get(name: string): Cookie | undefined {
|
|
282
|
+
const value = ctx.cookie(name);
|
|
283
|
+
return value !== undefined ? { name, value } : undefined;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
getAll(name?: string): Cookie[] {
|
|
287
|
+
const all = ctx.cookies();
|
|
288
|
+
if (name !== undefined) {
|
|
289
|
+
const value = all[name];
|
|
290
|
+
return value !== undefined ? [{ name, value }] : [];
|
|
291
|
+
}
|
|
292
|
+
return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
has(name: string): boolean {
|
|
296
|
+
return ctx.cookie(name) !== undefined;
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
set(name: string, value: string, options?: CookieOptions): void {
|
|
300
|
+
ctx.setCookie(name, value, options);
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
delete(
|
|
304
|
+
name: string,
|
|
305
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
306
|
+
): void {
|
|
307
|
+
ctx.deleteCookie(name, options);
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -12,21 +12,26 @@
|
|
|
12
12
|
import type { LoaderFn } from "../types.js";
|
|
13
13
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
export interface LoaderRegistryEntry {
|
|
16
|
+
fn: LoaderFn<any, any, any>;
|
|
17
|
+
middleware: MiddlewareFn[];
|
|
18
|
+
/** Whether this loader is fetchable via the _rsc_loader endpoint. */
|
|
19
|
+
fetchable: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
|
|
19
23
|
|
|
20
24
|
export function registerFetchableLoader(
|
|
21
25
|
id: string,
|
|
22
26
|
fn: LoaderFn<any, any, any>,
|
|
23
27
|
middleware: MiddlewareFn[],
|
|
28
|
+
fetchable: boolean,
|
|
24
29
|
): void {
|
|
25
|
-
fetchableLoaderRegistry.set(id, { fn, middleware });
|
|
30
|
+
fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function getFetchableLoader(
|
|
29
34
|
id: string,
|
|
30
|
-
):
|
|
35
|
+
): LoaderRegistryEntry | undefined {
|
|
31
36
|
return fetchableLoaderRegistry.get(id);
|
|
32
37
|
}
|
|
@@ -14,9 +14,37 @@
|
|
|
14
14
|
export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Build a HandleData snapshot from a HandleStore using segment ordering.
|
|
18
|
+
* Reads data directly from the store for each segment in order.
|
|
19
19
|
*/
|
|
20
|
+
export function buildHandleSnapshot(
|
|
21
|
+
handleStore: HandleStore,
|
|
22
|
+
segmentOrder: string[],
|
|
23
|
+
): HandleData {
|
|
24
|
+
const data: HandleData = {};
|
|
25
|
+
for (const segmentId of segmentOrder) {
|
|
26
|
+
const segData = handleStore.getDataForSegment(segmentId);
|
|
27
|
+
for (const handleName in segData) {
|
|
28
|
+
if (!data[handleName]) data[handleName] = {};
|
|
29
|
+
data[handleName][segmentId] = segData[handleName];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createLateHandlePushError(
|
|
36
|
+
handleName: string,
|
|
37
|
+
segmentId: string,
|
|
38
|
+
): Error {
|
|
39
|
+
const error = new Error(
|
|
40
|
+
`Handle "${handleName}" for segment "${segmentId}" was pushed after handle collection completed. ` +
|
|
41
|
+
`This usually means an async JSX subtree suspended and later tried to push a handle during streaming. ` +
|
|
42
|
+
`Push handles from the route/layout handler or during the initial synchronous JSX render instead.`,
|
|
43
|
+
);
|
|
44
|
+
error.name = "LateHandlePushError";
|
|
45
|
+
return error;
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
function cloneHandleData(data: HandleData): HandleData {
|
|
21
49
|
const clone: HandleData = {};
|
|
22
50
|
for (const handleName in data) {
|
|
@@ -44,11 +72,26 @@ export interface HandleStore {
|
|
|
44
72
|
track<T>(promise: Promise<T>): Promise<T>;
|
|
45
73
|
|
|
46
74
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
75
|
+
* Signal that no more track() calls will be made.
|
|
76
|
+
* settled will not resolve until seal() is called AND all tracked
|
|
77
|
+
* promises have settled. Calling stream() or getData() auto-seals.
|
|
78
|
+
*/
|
|
79
|
+
seal(): void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Promise that resolves when the store is sealed AND all tracked
|
|
83
|
+
* handlers have settled.
|
|
49
84
|
*/
|
|
50
85
|
readonly settled: Promise<void>;
|
|
51
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Optional error callback for late streaming-handle failures.
|
|
89
|
+
* Called when push() throws LateHandlePushError (handle pushed after
|
|
90
|
+
* stream completion). Allows the router to surface these errors
|
|
91
|
+
* to onError and telemetry.
|
|
92
|
+
*/
|
|
93
|
+
onError?: (error: Error) => void;
|
|
94
|
+
|
|
52
95
|
/**
|
|
53
96
|
* Push handle data for a specific handle and segment.
|
|
54
97
|
* Multiple pushes to the same handle/segment accumulate in an array.
|
|
@@ -58,9 +101,7 @@ export interface HandleStore {
|
|
|
58
101
|
|
|
59
102
|
/**
|
|
60
103
|
* Get all collected handle data after all handlers have settled.
|
|
61
|
-
*
|
|
62
|
-
* The data may contain unresolved promises which RSC will stream.
|
|
63
|
-
* @deprecated Use stream() for progressive updates
|
|
104
|
+
* Waits for `settled`, then returns the finalized data.
|
|
64
105
|
*/
|
|
65
106
|
getData(): Promise<HandleData>;
|
|
66
107
|
|
|
@@ -81,7 +122,10 @@ export interface HandleStore {
|
|
|
81
122
|
* Replay cached handle data back into the store (for cache hits).
|
|
82
123
|
* Used to restore handle data when serving cached segments.
|
|
83
124
|
*/
|
|
84
|
-
replaySegmentData(
|
|
125
|
+
replaySegmentData(
|
|
126
|
+
segmentId: string,
|
|
127
|
+
segmentHandles: Record<string, unknown[]>,
|
|
128
|
+
): void;
|
|
85
129
|
}
|
|
86
130
|
|
|
87
131
|
/**
|
|
@@ -105,11 +149,35 @@ export interface HandleStore {
|
|
|
105
149
|
* ```
|
|
106
150
|
*/
|
|
107
151
|
export function createHandleStore(): HandleStore {
|
|
108
|
-
const pending: Promise<unknown>[] = [];
|
|
109
152
|
const data: HandleData = {};
|
|
110
153
|
|
|
111
|
-
//
|
|
112
|
-
|
|
154
|
+
// Settlement barrier: resolved only when sealed AND inflight === 0.
|
|
155
|
+
// seal() signals "no more track() calls". Each track() increments
|
|
156
|
+
// inflightCount, each promise.finally() decrements. settled resolves
|
|
157
|
+
// once both conditions are met — even if tracks are added while
|
|
158
|
+
// earlier ones are still in flight.
|
|
159
|
+
let sealed = false;
|
|
160
|
+
let inflightCount = 0;
|
|
161
|
+
let drainWaiters: (() => void)[] = [];
|
|
162
|
+
|
|
163
|
+
function notifyDrain() {
|
|
164
|
+
if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
|
|
165
|
+
const waiters = drainWaiters;
|
|
166
|
+
drainWaiters = [];
|
|
167
|
+
for (const resolve of waiters) resolve();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sealInternal() {
|
|
172
|
+
if (sealed) return;
|
|
173
|
+
sealed = true;
|
|
174
|
+
notifyDrain();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Dirty flag for pending emissions and resolver for waiting consumer.
|
|
178
|
+
// stream() only ever yields the latest full state, so we track a single
|
|
179
|
+
// dirty bit and clone `data` once at yield time instead of per push.
|
|
180
|
+
let hasPendingEmission = false;
|
|
113
181
|
let emissionResolver: (() => void) | null = null;
|
|
114
182
|
let completed = false;
|
|
115
183
|
|
|
@@ -124,7 +192,7 @@ export function createHandleStore(): HandleStore {
|
|
|
124
192
|
|
|
125
193
|
// Wait for the next emission or completion
|
|
126
194
|
function waitForEmission(): Promise<void> {
|
|
127
|
-
if (
|
|
195
|
+
if (hasPendingEmission || completed) {
|
|
128
196
|
return Promise.resolve();
|
|
129
197
|
}
|
|
130
198
|
return new Promise((resolve) => {
|
|
@@ -134,18 +202,36 @@ export function createHandleStore(): HandleStore {
|
|
|
134
202
|
|
|
135
203
|
return {
|
|
136
204
|
track<T>(promise: Promise<T>): Promise<T> {
|
|
137
|
-
|
|
205
|
+
inflightCount++;
|
|
206
|
+
// Use .then() instead of .finally() to avoid creating an unhandled rejection
|
|
207
|
+
// branch when the promise rejects. .finally() re-throws on a new branch that
|
|
208
|
+
// can crash the process if not caught.
|
|
209
|
+
const onSettle = () => {
|
|
210
|
+
inflightCount--;
|
|
211
|
+
notifyDrain();
|
|
212
|
+
};
|
|
213
|
+
promise.then(onSettle, onSettle);
|
|
138
214
|
return promise;
|
|
139
215
|
},
|
|
140
216
|
|
|
217
|
+
seal() {
|
|
218
|
+
sealInternal();
|
|
219
|
+
},
|
|
220
|
+
|
|
141
221
|
get settled(): Promise<void> {
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
222
|
+
if (sealed && inflightCount === 0) return Promise.resolve();
|
|
223
|
+
return new Promise<void>((resolve) => {
|
|
224
|
+
drainWaiters.push(resolve);
|
|
225
|
+
});
|
|
146
226
|
},
|
|
147
227
|
|
|
148
228
|
push(handleName: string, segmentId: string, value: unknown): void {
|
|
229
|
+
if (completed) {
|
|
230
|
+
const error = createLateHandlePushError(handleName, segmentId);
|
|
231
|
+
if (this.onError) this.onError(error);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
|
|
149
235
|
if (!data[handleName]) {
|
|
150
236
|
data[handleName] = {};
|
|
151
237
|
}
|
|
@@ -154,49 +240,42 @@ export function createHandleStore(): HandleStore {
|
|
|
154
240
|
}
|
|
155
241
|
data[handleName][segmentId].push(value);
|
|
156
242
|
|
|
157
|
-
//
|
|
158
|
-
|
|
243
|
+
// Mark dirty; the actual snapshot is cloned once at yield time.
|
|
244
|
+
hasPendingEmission = true;
|
|
159
245
|
signalEmission();
|
|
160
246
|
},
|
|
161
247
|
|
|
162
248
|
getData(): Promise<HandleData> {
|
|
163
|
-
|
|
249
|
+
sealInternal();
|
|
250
|
+
return this.settled.then(() => cloneHandleData(data));
|
|
164
251
|
},
|
|
165
252
|
|
|
166
253
|
async *stream(): AsyncGenerator<HandleData, void, unknown> {
|
|
167
|
-
|
|
254
|
+
sealInternal();
|
|
255
|
+
|
|
168
256
|
this.settled.then(() => {
|
|
169
257
|
completed = true;
|
|
170
258
|
signalEmission();
|
|
171
259
|
});
|
|
172
260
|
|
|
173
|
-
//
|
|
174
|
-
// This allows multiple handles pushing in quick succession to be batched
|
|
261
|
+
// Batch rapid synchronous pushes with initial delay
|
|
175
262
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
176
263
|
|
|
177
|
-
// If we already have data, yield the accumulated state
|
|
178
264
|
if (Object.keys(data).length > 0) {
|
|
179
|
-
|
|
180
|
-
pendingEmissions = [];
|
|
265
|
+
hasPendingEmission = false;
|
|
181
266
|
yield cloneHandleData(data);
|
|
182
267
|
}
|
|
183
268
|
|
|
184
|
-
// Continue streaming on each push
|
|
185
269
|
while (!completed) {
|
|
186
270
|
await waitForEmission();
|
|
187
271
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const latest = pendingEmissions[pendingEmissions.length - 1];
|
|
192
|
-
pendingEmissions = [];
|
|
193
|
-
yield latest;
|
|
272
|
+
if (hasPendingEmission) {
|
|
273
|
+
hasPendingEmission = false;
|
|
274
|
+
yield cloneHandleData(data);
|
|
194
275
|
}
|
|
195
276
|
}
|
|
196
277
|
|
|
197
|
-
|
|
198
|
-
// (handles that pushed after our last yield but before completion)
|
|
199
|
-
if (pendingEmissions.length > 0) {
|
|
278
|
+
if (hasPendingEmission) {
|
|
200
279
|
yield cloneHandleData(data);
|
|
201
280
|
}
|
|
202
281
|
},
|
|
@@ -211,18 +290,20 @@ export function createHandleStore(): HandleStore {
|
|
|
211
290
|
return result;
|
|
212
291
|
},
|
|
213
292
|
|
|
214
|
-
replaySegmentData(
|
|
293
|
+
replaySegmentData(
|
|
294
|
+
segmentId: string,
|
|
295
|
+
segmentHandles: Record<string, unknown[]>,
|
|
296
|
+
): void {
|
|
215
297
|
for (const handleName in segmentHandles) {
|
|
216
298
|
if (!data[handleName]) {
|
|
217
299
|
data[handleName] = {};
|
|
218
300
|
}
|
|
219
|
-
// Replace
|
|
220
|
-
//
|
|
221
|
-
//
|
|
301
|
+
// Replace (not append) to avoid handle bleeding between routes.
|
|
302
|
+
// Cached segment restoration should replace existing data for that
|
|
303
|
+
// segment, not accumulate on top of data from a different route.
|
|
222
304
|
data[handleName][segmentId] = [...segmentHandles[handleName]];
|
|
223
305
|
}
|
|
224
|
-
|
|
225
|
-
pendingEmissions.push(cloneHandleData(data));
|
|
306
|
+
hasPendingEmission = true;
|
|
226
307
|
signalEmission();
|
|
227
308
|
},
|
|
228
309
|
};
|