@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136
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 +8 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +440 -133
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3386 -1111
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +78 -25
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +281 -11
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +87 -18
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +289 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +914 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +144 -7
- package/skills/prerender/SKILL.md +172 -125
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +101 -5
- package/skills/router-setup/SKILL.md +117 -10
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- 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 +129 -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 +84 -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/typesafety/SKILL.md +332 -29
- package/skills/use-cache/SKILL.md +57 -14
- 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 +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- 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 +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +131 -30
- package/src/browser/navigation-client.ts +199 -86
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +157 -74
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +174 -109
- package/src/browser/prefetch/cache.ts +222 -69
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +113 -32
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +300 -122
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +125 -28
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +115 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +260 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +239 -52
- package/src/cache/cache-scope.ts +234 -87
- 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 +1989 -378
- 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 +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +99 -13
- package/src/index.ts +139 -19
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- 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 +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +149 -74
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +124 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +54 -25
- package/src/router/match-middleware/cache-lookup.ts +205 -271
- package/src/router/match-middleware/cache-store.ts +81 -50
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +93 -39
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +268 -171
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +3 -27
- package/src/router/router-interfaces.ts +129 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +197 -92
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +392 -338
- package/src/router/segment-resolution/static-store.ts +19 -5
- 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 +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +174 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +660 -518
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +133 -13
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +115 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +36 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +269 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +235 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +442 -173
- package/src/ssr/index.tsx +24 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -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 +183 -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 +581 -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 +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +234 -82
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +68 -18
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +178 -5
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- 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/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +119 -111
- package/src/vite/router-discovery.ts +941 -142
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- 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 +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
+
import { parseCookiesFromHeader } from "./cookie-parse.js";
|
|
15
|
+
import type { CacheErrorCategory } from "../cache/cache-error.js";
|
|
14
16
|
import type { CookieOptions } from "../router/middleware.js";
|
|
17
|
+
import {
|
|
18
|
+
KEEP_CACHE_HEADER,
|
|
19
|
+
getRawCookieValue,
|
|
20
|
+
mintStateValue,
|
|
21
|
+
serializeStateCookie,
|
|
22
|
+
} from "../browser/cookie-name.js";
|
|
15
23
|
import type { LoaderDefinition, LoaderContext } from "../types.js";
|
|
16
24
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
17
25
|
import type {
|
|
@@ -20,21 +28,47 @@ import type {
|
|
|
20
28
|
DefaultRouteName,
|
|
21
29
|
} from "../types/global-namespace.js";
|
|
22
30
|
import type { Handle } from "../handle.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
31
|
+
import {
|
|
32
|
+
type ContextVar,
|
|
33
|
+
contextGet,
|
|
34
|
+
contextSet,
|
|
35
|
+
isNonCacheable,
|
|
36
|
+
} from "../context-var.js";
|
|
37
|
+
import {
|
|
38
|
+
createHandleStore,
|
|
39
|
+
buildHandleSnapshot,
|
|
40
|
+
type HandleStore,
|
|
41
|
+
type HandleData,
|
|
42
|
+
} from "./handle-store.js";
|
|
25
43
|
import { isHandle } from "../handle.js";
|
|
26
|
-
import {
|
|
44
|
+
import { withDefer } from "../defer.js";
|
|
45
|
+
import { type MetricsStore } from "./context.js";
|
|
46
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
27
47
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
28
48
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
29
49
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
30
|
-
import {
|
|
50
|
+
import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
|
|
51
|
+
import type { TransitionWhenFn } from "../types/segments.js";
|
|
52
|
+
import type { ResolvedTracing } from "../router/tracing.js";
|
|
53
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
54
|
+
import {
|
|
55
|
+
THEME_COOKIE,
|
|
56
|
+
isValidTheme,
|
|
57
|
+
warnInvalidTheme,
|
|
58
|
+
} from "../theme/constants.js";
|
|
31
59
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
60
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
61
|
+
import { isInsideCacheScope } from "./context.js";
|
|
33
62
|
import {
|
|
34
63
|
createReverseFunction,
|
|
35
64
|
stripInternalParams,
|
|
36
65
|
} from "../router/handler-context.js";
|
|
37
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
getGlobalRouteMap,
|
|
68
|
+
isRouteRootScoped,
|
|
69
|
+
getSearchSchema,
|
|
70
|
+
} from "../route-map-builder.js";
|
|
71
|
+
import { parseSearchParams } from "../search-params.js";
|
|
38
72
|
import { invariant } from "../errors.js";
|
|
39
73
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
40
74
|
|
|
@@ -47,24 +81,9 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
|
47
81
|
export interface RequestContext<
|
|
48
82
|
TEnv = DefaultEnv,
|
|
49
83
|
TParams = Record<string, string>,
|
|
50
|
-
> {
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
/** Original HTTP request */
|
|
54
|
-
request: Request;
|
|
55
|
-
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
56
|
-
url: URL;
|
|
57
|
-
/**
|
|
58
|
-
* The original request URL with all parameters intact, including
|
|
59
|
-
* internal `_rsc*` transport params.
|
|
60
|
-
*/
|
|
61
|
-
originalUrl: URL;
|
|
62
|
-
/** URL pathname */
|
|
63
|
-
pathname: string;
|
|
64
|
-
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
65
|
-
searchParams: URLSearchParams;
|
|
66
|
-
/** Variables set by middleware (same as ctx.var) */
|
|
67
|
-
var: Record<string, any>;
|
|
84
|
+
> extends RequestScope<TEnv> {
|
|
85
|
+
/** @internal Shared variable backing store for ctx.get()/ctx.set(). */
|
|
86
|
+
_variables: Record<string, any>;
|
|
68
87
|
/** Get a variable set by middleware */
|
|
69
88
|
get: {
|
|
70
89
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
@@ -72,8 +91,12 @@ export interface RequestContext<
|
|
|
72
91
|
};
|
|
73
92
|
/** Set a variable (shared with middleware and handlers) */
|
|
74
93
|
set: {
|
|
75
|
-
<T>(
|
|
76
|
-
|
|
94
|
+
<T>(
|
|
95
|
+
contextVar: ContextVar<T>,
|
|
96
|
+
value: T,
|
|
97
|
+
options?: { cache?: boolean },
|
|
98
|
+
): void;
|
|
99
|
+
<K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
|
|
77
100
|
};
|
|
78
101
|
/**
|
|
79
102
|
* Route params (populated after route matching)
|
|
@@ -100,6 +123,10 @@ export interface RequestContext<
|
|
|
100
123
|
setStatus(status: number): void;
|
|
101
124
|
/** @internal Set status bypassing cache-exec guard (for framework error handling) */
|
|
102
125
|
_setStatus(status: number): void;
|
|
126
|
+
/** @internal Rotate the rango state cookie (server seat of invalidateClientCache). */
|
|
127
|
+
_rotateStateCookie(): void;
|
|
128
|
+
/** @internal Set the keepClientCache() directive header on the response. */
|
|
129
|
+
_setKeepCacheDirective(): void;
|
|
103
130
|
|
|
104
131
|
/**
|
|
105
132
|
* Access loader data or push handle data.
|
|
@@ -135,29 +162,43 @@ export interface RequestContext<
|
|
|
135
162
|
/** @internal Handle store for tracking handle data across segments */
|
|
136
163
|
_handleStore: HandleStore;
|
|
137
164
|
|
|
165
|
+
/**
|
|
166
|
+
* @internal transition({ when }) predicates for segments matched this request,
|
|
167
|
+
* keyed by segment id. Collected during resolution (the function is stripped
|
|
168
|
+
* from the serialized segment config), then evaluated post-handler in
|
|
169
|
+
* rsc-rendering — outside any cache scope — to drop the transition of any
|
|
170
|
+
* segment whose predicate returns false.
|
|
171
|
+
*/
|
|
172
|
+
_transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
|
|
173
|
+
|
|
138
174
|
/** @internal Cache store for segment caching (optional, used by CacheScope) */
|
|
139
175
|
_cacheStore?: SegmentCacheStore;
|
|
140
176
|
|
|
177
|
+
/**
|
|
178
|
+
* @internal Handler-owned registry of explicit per-scope stores from
|
|
179
|
+
* cache({ store }). Created once per createRSCHandler() and threaded into
|
|
180
|
+
* every request context, so it accumulates every explicit store the handler
|
|
181
|
+
* resolves. updateTag()/revalidateTag() iterate this set plus _cacheStore to
|
|
182
|
+
* reach every store that may hold tagged entries. The app-level store is not
|
|
183
|
+
* added here (it is always reachable via _cacheStore).
|
|
184
|
+
*/
|
|
185
|
+
_explicitTaggedStores?: Set<SegmentCacheStore>;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @internal Union of every cache tag resolved while producing this request's
|
|
189
|
+
* response (from cache({ tags }), runtime cacheTag(), and loader cache tags).
|
|
190
|
+
* Populated at the tag-resolution sites via recordRequestTags(). Read by the
|
|
191
|
+
* document cache middleware so a full-page entry is tagged with everything its
|
|
192
|
+
* content used and can therefore be invalidated by updateTag()/revalidateTag().
|
|
193
|
+
*/
|
|
194
|
+
_requestTags: Set<string>;
|
|
195
|
+
|
|
141
196
|
/** @internal Cache profiles for "use cache" profile resolution (per-router) */
|
|
142
197
|
_cacheProfiles?: Record<
|
|
143
198
|
string,
|
|
144
199
|
import("../cache/profile-registry.js").CacheProfile
|
|
145
200
|
>;
|
|
146
201
|
|
|
147
|
-
/**
|
|
148
|
-
* Schedule work to run after the response is sent.
|
|
149
|
-
* On Cloudflare Workers, uses ctx.waitUntil().
|
|
150
|
-
* On Node.js, runs as fire-and-forget.
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* ```typescript
|
|
154
|
-
* ctx.waitUntil(async () => {
|
|
155
|
-
* await cacheStore.set(key, data, ttl);
|
|
156
|
-
* });
|
|
157
|
-
* ```
|
|
158
|
-
*/
|
|
159
|
-
waitUntil(fn: () => Promise<void>): void;
|
|
160
|
-
|
|
161
202
|
/**
|
|
162
203
|
* Register a callback to run when the response is created.
|
|
163
204
|
* Callbacks are sync and receive the response. They can:
|
|
@@ -261,6 +302,92 @@ export interface RequestContext<
|
|
|
261
302
|
/** @internal Previous route key (from the navigation source), used for revalidation */
|
|
262
303
|
_prevRouteKey?: string;
|
|
263
304
|
|
|
305
|
+
/**
|
|
306
|
+
* @internal Navigation/action source data the transition({ when }) gate reads
|
|
307
|
+
* to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
|
|
308
|
+
* come from the navigation snapshot (set at match time); action* are stashed
|
|
309
|
+
* at the action-bearing gate call sites. All undefined when there is no source
|
|
310
|
+
* (initial full load) or no action (plain navigation).
|
|
311
|
+
*/
|
|
312
|
+
_gateCurrentUrl?: URL;
|
|
313
|
+
_gateCurrentParams?: Record<string, string>;
|
|
314
|
+
_gateActionId?: string;
|
|
315
|
+
_gateActionUrl?: URL;
|
|
316
|
+
_gateActionResult?: unknown;
|
|
317
|
+
_gateFormData?: FormData;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @internal True while the post-action revalidation render is running (set by
|
|
321
|
+
* revalidateAfterAction). The "use cache" runtime reads this to prefer
|
|
322
|
+
* freshness over a fast stale response during an action: a stale entry
|
|
323
|
+
* re-executes in the foreground (so the action response reflects the refreshed
|
|
324
|
+
* value) with only the store write deferred, instead of serving stale and
|
|
325
|
+
* revalidating in the background. A plain navigation (flag unset) keeps SWR.
|
|
326
|
+
*/
|
|
327
|
+
_inActionRevalidation?: boolean;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @internal Render barrier for experimental `rendered()` API.
|
|
331
|
+
* Resolves when all non-loader segments have settled and handle data
|
|
332
|
+
* is available. Used by DSL loaders that call `ctx.rendered()`.
|
|
333
|
+
*/
|
|
334
|
+
_renderBarrier: Promise<void>;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @internal Resolve the render barrier. Accepts resolved segments, filters
|
|
338
|
+
* out loaders, and captures non-loader segment IDs as the handle ordering.
|
|
339
|
+
* Called after segment resolution (fresh) or handle replay (cache/prerender).
|
|
340
|
+
*/
|
|
341
|
+
_resolveRenderBarrier: (
|
|
342
|
+
segments: Array<{ type: string; id: string }>,
|
|
343
|
+
) => void;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @internal Segment order at barrier resolution time, used by loader
|
|
347
|
+
* ctx.use(handle) to collect handle data in correct order.
|
|
348
|
+
*/
|
|
349
|
+
_renderBarrierSegmentOrder?: string[];
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @internal Set to true when the matched entry tree contains any `loading()`
|
|
353
|
+
* entries (streaming). On a streaming tree rendered() waits for the streaming
|
|
354
|
+
* handlers to settle (via handleStore.settled) before resolving, and the
|
|
355
|
+
* deadlock guard state is kept live until that wait completes.
|
|
356
|
+
*/
|
|
357
|
+
_treeHasStreaming?: boolean;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @internal Loader IDs that have called rendered() and are waiting for the
|
|
361
|
+
* barrier. Used to detect deadlocks when a handler tries to await the same
|
|
362
|
+
* loader via ctx.use(Loader).
|
|
363
|
+
*/
|
|
364
|
+
_renderBarrierWaiters?: Set<string>;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @internal Loader IDs that handlers have started awaiting via ctx.use().
|
|
368
|
+
* Used for bidirectional deadlock detection: if a loader later calls
|
|
369
|
+
* rendered() and a handler already awaits it, we can detect the deadlock.
|
|
370
|
+
*/
|
|
371
|
+
_handlerLoaderDeps?: Set<string>;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @internal Cached HandleData snapshot built at barrier resolution time.
|
|
375
|
+
* Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
|
|
376
|
+
*/
|
|
377
|
+
_renderBarrierHandleSnapshot?: HandleData;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @internal The deadlock guard window is closed (no further handler-awaits-
|
|
381
|
+
* loader cycle is possible). For non-streaming trees this is set when the
|
|
382
|
+
* barrier resolves. For streaming trees the window stays open until
|
|
383
|
+
* handleStore.settled — rendered() keeps waiting past the barrier and a
|
|
384
|
+
* loading() handler can still resume and await a still-waiting loader — so it
|
|
385
|
+
* is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
|
|
386
|
+
* reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
|
|
387
|
+
* during the streaming settle wait.
|
|
388
|
+
*/
|
|
389
|
+
_renderBarrierGuardClosed?: boolean;
|
|
390
|
+
|
|
264
391
|
/** @internal Per-request error dedup set for onError reporting */
|
|
265
392
|
_reportedErrors: WeakSet<object>;
|
|
266
393
|
|
|
@@ -268,15 +395,40 @@ export interface RequestContext<
|
|
|
268
395
|
* @internal Report a non-fatal background error through the router's
|
|
269
396
|
* onError callback. Wired by the RSC handler / router during request
|
|
270
397
|
* creation. Cache-runtime and other subsystems call this to surface
|
|
271
|
-
* errors without failing the response.
|
|
398
|
+
* errors without failing the response. `category` is surfaced to consumers as
|
|
399
|
+
* `metadata.category` on the onError context (phase `cache`).
|
|
272
400
|
*/
|
|
273
|
-
_reportBackgroundError?: (
|
|
401
|
+
_reportBackgroundError?: (
|
|
402
|
+
error: unknown,
|
|
403
|
+
category: CacheErrorCategory,
|
|
404
|
+
) => void;
|
|
274
405
|
|
|
275
406
|
/** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
|
|
276
407
|
_debugPerformance?: boolean;
|
|
277
408
|
|
|
278
409
|
/** @internal Request-scoped performance metrics store */
|
|
279
410
|
_metricsStore?: MetricsStore;
|
|
411
|
+
|
|
412
|
+
/** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
|
|
413
|
+
_tracing?: ResolvedTracing;
|
|
414
|
+
|
|
415
|
+
/** @internal Router basename for this request (used by redirect()) */
|
|
416
|
+
_basename?: string;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
420
|
+
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
421
|
+
*/
|
|
422
|
+
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @internal Coarse route-level cache signal for the X-Rango-Cache debug
|
|
426
|
+
* header. Populated by match/matchPartial only when the debug cache signal
|
|
427
|
+
* gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
|
|
428
|
+
* the response-finalization path (createResponseWithMergedHeaders). Undefined
|
|
429
|
+
* when the gate is off, so no header is emitted.
|
|
430
|
+
*/
|
|
431
|
+
_cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
|
|
280
432
|
}
|
|
281
433
|
|
|
282
434
|
/**
|
|
@@ -295,18 +447,42 @@ export type PublicRequestContext<
|
|
|
295
447
|
| "setCookie"
|
|
296
448
|
| "deleteCookie"
|
|
297
449
|
| "_handleStore"
|
|
450
|
+
| "_transitionWhen"
|
|
298
451
|
| "_cacheStore"
|
|
452
|
+
| "_explicitTaggedStores"
|
|
453
|
+
| "_requestTags"
|
|
299
454
|
| "_cacheProfiles"
|
|
300
455
|
| "_onResponseCallbacks"
|
|
301
456
|
| "_themeConfig"
|
|
302
457
|
| "_locationState"
|
|
303
458
|
| "_routeName"
|
|
304
459
|
| "_prevRouteKey"
|
|
460
|
+
| "_gateCurrentUrl"
|
|
461
|
+
| "_gateCurrentParams"
|
|
462
|
+
| "_gateActionId"
|
|
463
|
+
| "_gateActionUrl"
|
|
464
|
+
| "_gateActionResult"
|
|
465
|
+
| "_gateFormData"
|
|
466
|
+
| "_inActionRevalidation"
|
|
305
467
|
| "_reportedErrors"
|
|
468
|
+
| "_renderBarrier"
|
|
469
|
+
| "_resolveRenderBarrier"
|
|
470
|
+
| "_renderBarrierSegmentOrder"
|
|
471
|
+
| "_treeHasStreaming"
|
|
472
|
+
| "_renderBarrierWaiters"
|
|
473
|
+
| "_handlerLoaderDeps"
|
|
474
|
+
| "_renderBarrierHandleSnapshot"
|
|
475
|
+
| "_renderBarrierGuardClosed"
|
|
306
476
|
| "_reportBackgroundError"
|
|
307
477
|
| "_debugPerformance"
|
|
308
478
|
| "_metricsStore"
|
|
479
|
+
| "_basename"
|
|
309
480
|
| "_setStatus"
|
|
481
|
+
| "_rotateStateCookie"
|
|
482
|
+
| "_setKeepCacheDirective"
|
|
483
|
+
| "_variables"
|
|
484
|
+
| "_classifiedRoute"
|
|
485
|
+
| "_cacheSignal"
|
|
310
486
|
| "res"
|
|
311
487
|
>;
|
|
312
488
|
|
|
@@ -358,6 +534,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
|
|
|
358
534
|
export function setRequestContextParams(
|
|
359
535
|
params: Record<string, string>,
|
|
360
536
|
routeName?: string,
|
|
537
|
+
routeMap?: Record<string, string>,
|
|
361
538
|
): void {
|
|
362
539
|
const ctx = requestContextStorage.getStore();
|
|
363
540
|
if (ctx) {
|
|
@@ -370,9 +547,13 @@ export function setRequestContextParams(
|
|
|
370
547
|
: undefined
|
|
371
548
|
) as DefaultRouteName | undefined;
|
|
372
549
|
}
|
|
373
|
-
// Update reverse with scoped resolution now that route is known
|
|
550
|
+
// Update reverse with scoped resolution now that route is known. Production
|
|
551
|
+
// omits routeMap and uses the global map (routes are registered globally);
|
|
552
|
+
// the testing primitives (renderToFlightString/renderServerTree) pass a
|
|
553
|
+
// scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
|
|
554
|
+
// registered last.
|
|
374
555
|
ctx.reverse = createReverseFunction(
|
|
375
|
-
getGlobalRouteMap(),
|
|
556
|
+
routeMap ?? getGlobalRouteMap(),
|
|
376
557
|
routeName,
|
|
377
558
|
params,
|
|
378
559
|
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
@@ -388,11 +569,17 @@ export function setRequestContextParams(
|
|
|
388
569
|
*/
|
|
389
570
|
export function setRequestContextPrevRouteKey(
|
|
390
571
|
prevRouteKey: string | undefined,
|
|
572
|
+
currentUrl?: URL,
|
|
573
|
+
currentParams?: Record<string, string>,
|
|
391
574
|
): void {
|
|
392
575
|
const ctx = requestContextStorage.getStore();
|
|
393
|
-
if (ctx
|
|
394
|
-
|
|
395
|
-
}
|
|
576
|
+
if (!ctx) return;
|
|
577
|
+
if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
|
|
578
|
+
// Source URL/params for the transition({ when }) gate (effectiveFromUrl /
|
|
579
|
+
// effectiveFromMatch.params from the navigation snapshot). Same write point as
|
|
580
|
+
// _prevRouteKey, which doubles as fromRouteName.
|
|
581
|
+
if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
|
|
582
|
+
if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
|
|
396
583
|
}
|
|
397
584
|
|
|
398
585
|
/**
|
|
@@ -406,23 +593,7 @@ export function getLocationState(): LocationStateEntry[] | undefined {
|
|
|
406
593
|
return ctx?._locationState;
|
|
407
594
|
}
|
|
408
595
|
|
|
409
|
-
|
|
410
|
-
* Get the current request context, throwing if not available
|
|
411
|
-
* @deprecated Use getRequestContext() directly — it now throws if outside context
|
|
412
|
-
*/
|
|
413
|
-
export function requireRequestContext<
|
|
414
|
-
TEnv = DefaultEnv,
|
|
415
|
-
>(): RequestContext<TEnv> {
|
|
416
|
-
return getRequestContext<TEnv>();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Cloudflare Workers ExecutionContext (subset we need)
|
|
421
|
-
*/
|
|
422
|
-
export interface ExecutionContext {
|
|
423
|
-
waitUntil(promise: Promise<any>): void;
|
|
424
|
-
passThroughOnException(): void;
|
|
425
|
-
}
|
|
596
|
+
export type { ExecutionContext };
|
|
426
597
|
|
|
427
598
|
/**
|
|
428
599
|
* Options for creating a request context
|
|
@@ -436,6 +607,11 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
436
607
|
initialResponse?: Response;
|
|
437
608
|
/** Optional cache store for segment caching (used by CacheScope) */
|
|
438
609
|
cacheStore?: SegmentCacheStore;
|
|
610
|
+
/**
|
|
611
|
+
* Handler-owned registry of explicit per-scope stores for cross-store tag
|
|
612
|
+
* invalidation. Created once per handler, reused across requests.
|
|
613
|
+
*/
|
|
614
|
+
explicitTaggedStores?: Set<SegmentCacheStore>;
|
|
439
615
|
/** Optional cache profiles for "use cache" resolution (per-router) */
|
|
440
616
|
cacheProfiles?: Record<
|
|
441
617
|
string,
|
|
@@ -445,6 +621,10 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
445
621
|
executionContext?: ExecutionContext;
|
|
446
622
|
/** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
|
|
447
623
|
themeConfig?: ResolvedThemeConfig | null;
|
|
624
|
+
/** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
|
|
625
|
+
stateCookieName?: string;
|
|
626
|
+
/** Build version, used as the prefix of a server-rotated rango state value. */
|
|
627
|
+
version?: string;
|
|
448
628
|
}
|
|
449
629
|
|
|
450
630
|
/**
|
|
@@ -465,15 +645,17 @@ export function createRequestContext<TEnv>(
|
|
|
465
645
|
variables,
|
|
466
646
|
initialResponse,
|
|
467
647
|
cacheStore,
|
|
648
|
+
explicitTaggedStores,
|
|
468
649
|
cacheProfiles,
|
|
469
650
|
executionContext,
|
|
470
651
|
themeConfig,
|
|
652
|
+
stateCookieName,
|
|
653
|
+
version: stateVersion,
|
|
471
654
|
} = options;
|
|
472
655
|
const cookieHeader = request.headers.get("Cookie");
|
|
656
|
+
let rangoStateRotated = false;
|
|
473
657
|
let parsedCookies: Record<string, string> | null = null;
|
|
474
658
|
|
|
475
|
-
// Create stub response for collecting headers/cookies.
|
|
476
|
-
// All cookie/header mutations go here; cookie reads derive from it.
|
|
477
659
|
let stubResponse = initialResponse
|
|
478
660
|
? new Response(null, {
|
|
479
661
|
status: initialResponse.status,
|
|
@@ -482,11 +664,9 @@ export function createRequestContext<TEnv>(
|
|
|
482
664
|
})
|
|
483
665
|
: new Response(null, { status: 200 });
|
|
484
666
|
|
|
485
|
-
// Create handle store and loader memoization for this request
|
|
486
667
|
const handleStore = createHandleStore();
|
|
487
668
|
const loaderPromises = new Map<string, Promise<any>>();
|
|
488
669
|
|
|
489
|
-
// Lazy parse cookies from the original Cookie header
|
|
490
670
|
const getParsedCookies = (): Record<string, string> => {
|
|
491
671
|
if (!parsedCookies) {
|
|
492
672
|
parsedCookies = parseCookiesFromHeader(cookieHeader);
|
|
@@ -494,7 +674,6 @@ export function createRequestContext<TEnv>(
|
|
|
494
674
|
return parsedCookies;
|
|
495
675
|
};
|
|
496
676
|
|
|
497
|
-
// Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
|
|
498
677
|
let responseCookieCache: Map<string, string | null> | null = null;
|
|
499
678
|
const getResponseCookies = (): Map<string, string | null> => {
|
|
500
679
|
if (!responseCookieCache) {
|
|
@@ -506,8 +685,17 @@ export function createRequestContext<TEnv>(
|
|
|
506
685
|
responseCookieCache = null;
|
|
507
686
|
};
|
|
508
687
|
|
|
509
|
-
|
|
510
|
-
|
|
688
|
+
function assertNotInsideCacheScopeALS(methodName: string): void {
|
|
689
|
+
if (isInsideCacheScope()) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`ctx.${methodName}() cannot be called inside a cache() boundary. ` +
|
|
692
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
693
|
+
`Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Response stub Set-Cookie wins, then original header (source of truth for mutations).
|
|
511
699
|
const effectiveCookie = (name: string): string | undefined => {
|
|
512
700
|
const mutations = getResponseCookies();
|
|
513
701
|
if (mutations.has(name)) {
|
|
@@ -517,14 +705,11 @@ export function createRequestContext<TEnv>(
|
|
|
517
705
|
return getParsedCookies()[name];
|
|
518
706
|
};
|
|
519
707
|
|
|
520
|
-
// Theme helpers (only used when themeConfig is provided)
|
|
521
708
|
const getTheme = (): Theme | undefined => {
|
|
522
709
|
if (!themeConfig) return undefined;
|
|
523
710
|
|
|
524
|
-
// Use overlay-aware read so setTheme() in the same request is reflected
|
|
525
711
|
const stored = effectiveCookie(themeConfig.storageKey);
|
|
526
712
|
if (stored) {
|
|
527
|
-
// Validate stored value
|
|
528
713
|
if (stored === "system" && themeConfig.enableSystem) {
|
|
529
714
|
return "system";
|
|
530
715
|
}
|
|
@@ -538,15 +723,15 @@ export function createRequestContext<TEnv>(
|
|
|
538
723
|
const setTheme = (theme: Theme): void => {
|
|
539
724
|
if (!themeConfig) return;
|
|
540
725
|
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
726
|
+
// Shared guard (isValidTheme): reject any value not in the configured theme
|
|
727
|
+
// set, AND reject "system" when system detection is off — a cookie of
|
|
728
|
+
// theme=system with enableSystem:false would re-apply a bogus class="system"
|
|
729
|
+
// on the next SSR.
|
|
730
|
+
if (!isValidTheme(theme, themeConfig)) {
|
|
731
|
+
warnInvalidTheme(theme, themeConfig);
|
|
546
732
|
return;
|
|
547
733
|
}
|
|
548
734
|
|
|
549
|
-
// Write to stub — effectiveCookie() will pick it up on next read
|
|
550
735
|
stubResponse.headers.append(
|
|
551
736
|
"Set-Cookie",
|
|
552
737
|
serializeCookieValue(themeConfig.storageKey, theme, {
|
|
@@ -558,10 +743,8 @@ export function createRequestContext<TEnv>(
|
|
|
558
743
|
invalidateResponseCookieCache();
|
|
559
744
|
};
|
|
560
745
|
|
|
561
|
-
// Strip internal _rsc* params so userland sees a clean URL.
|
|
562
746
|
const cleanUrl = stripInternalParams(url);
|
|
563
747
|
|
|
564
|
-
// Build the context object first (without use), then add use
|
|
565
748
|
const ctx: RequestContext<TEnv> = {
|
|
566
749
|
env,
|
|
567
750
|
request,
|
|
@@ -569,12 +752,20 @@ export function createRequestContext<TEnv>(
|
|
|
569
752
|
originalUrl: new URL(request.url),
|
|
570
753
|
pathname: url.pathname,
|
|
571
754
|
searchParams: cleanUrl.searchParams,
|
|
572
|
-
|
|
573
|
-
get: ((keyOrVar: any) =>
|
|
574
|
-
|
|
575
|
-
|
|
755
|
+
_variables: variables,
|
|
756
|
+
get: ((keyOrVar: any) => {
|
|
757
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
758
|
+
throw new Error(
|
|
759
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
760
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
761
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return contextGet(variables, keyOrVar);
|
|
765
|
+
}) as RequestContext<TEnv>["get"],
|
|
766
|
+
set: ((keyOrVar: any, value: any, options?: any) => {
|
|
576
767
|
assertNotInsideCacheExec(ctx, "set");
|
|
577
|
-
contextSet(variables, keyOrVar, value);
|
|
768
|
+
contextSet(variables, keyOrVar, value, options);
|
|
578
769
|
}) as RequestContext<TEnv>["set"],
|
|
579
770
|
params: {} as Record<string, string>,
|
|
580
771
|
|
|
@@ -612,6 +803,7 @@ export function createRequestContext<TEnv>(
|
|
|
612
803
|
|
|
613
804
|
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
614
805
|
assertNotInsideCacheExec(ctx, "setCookie");
|
|
806
|
+
assertNotInsideCacheScopeALS("setCookie");
|
|
615
807
|
stubResponse.headers.append(
|
|
616
808
|
"Set-Cookie",
|
|
617
809
|
serializeCookieValue(name, value, options),
|
|
@@ -624,6 +816,7 @@ export function createRequestContext<TEnv>(
|
|
|
624
816
|
options?: Pick<CookieOptions, "domain" | "path">,
|
|
625
817
|
): void {
|
|
626
818
|
assertNotInsideCacheExec(ctx, "deleteCookie");
|
|
819
|
+
assertNotInsideCacheScopeALS("deleteCookie");
|
|
627
820
|
stubResponse.headers.append(
|
|
628
821
|
"Set-Cookie",
|
|
629
822
|
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
@@ -633,11 +826,52 @@ export function createRequestContext<TEnv>(
|
|
|
633
826
|
|
|
634
827
|
header(name: string, value: string): void {
|
|
635
828
|
assertNotInsideCacheExec(ctx, "header");
|
|
829
|
+
assertNotInsideCacheScopeALS("header");
|
|
636
830
|
stubResponse.headers.set(name, value);
|
|
637
831
|
},
|
|
638
832
|
|
|
833
|
+
// Rotate the rango state cookie for the responding client (the server seat
|
|
834
|
+
// of invalidateClientCache). Writes ONE Set-Cookie per request with the
|
|
835
|
+
// value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
|
|
836
|
+
// serializer), not the URL-encoded form serializeCookieValue would produce.
|
|
837
|
+
// The timestamp is strictly greater than the client's current one (inbound
|
|
838
|
+
// X-Rango-State), so a same-millisecond server rotation still differs from
|
|
839
|
+
// the client value and the divergence observer fires.
|
|
840
|
+
_rotateStateCookie(): void {
|
|
841
|
+
if (rangoStateRotated) return;
|
|
842
|
+
rangoStateRotated = true;
|
|
843
|
+
if (!stateCookieName) return;
|
|
844
|
+
// The client's current value, for the monotonic guard: prefer the
|
|
845
|
+
// X-Rango-State header (router navigation/prefetch fetches send it), but
|
|
846
|
+
// fall back to the request's rango state cookie — action POSTs / plain
|
|
847
|
+
// app fetch()s carry no router header yet DO send the cookie. Without the
|
|
848
|
+
// fallback, prevTs stays 0 and a same-ms mint can equal the client value,
|
|
849
|
+
// leaving the divergence observer silent. `|| null` so an empty header
|
|
850
|
+
// ('' from proxy normalization) falls through instead of short-circuiting.
|
|
851
|
+
// getRawCookieValue reads the cookie undecoded (the wire value
|
|
852
|
+
// decodeStateValue decodes exactly once) AND is the same parser the client
|
|
853
|
+
// mirror uses, so both seats read the same jar entry.
|
|
854
|
+
const prevRaw =
|
|
855
|
+
(request.headers.get("x-rango-state") || null) ??
|
|
856
|
+
getRawCookieValue(cookieHeader, stateCookieName);
|
|
857
|
+
const value = mintStateValue(stateVersion ?? "0", prevRaw);
|
|
858
|
+
stubResponse.headers.append(
|
|
859
|
+
"Set-Cookie",
|
|
860
|
+
serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
|
|
861
|
+
);
|
|
862
|
+
invalidateResponseCookieCache();
|
|
863
|
+
},
|
|
864
|
+
|
|
865
|
+
// Set the keepClientCache() directive header. The action bridge reads it on
|
|
866
|
+
// the response and suppresses its automatic invalidation. `.set` makes this
|
|
867
|
+
// idempotent (one header regardless of call count).
|
|
868
|
+
_setKeepCacheDirective(): void {
|
|
869
|
+
stubResponse.headers.set(KEEP_CACHE_HEADER, "1");
|
|
870
|
+
},
|
|
871
|
+
|
|
639
872
|
setStatus(status: number): void {
|
|
640
873
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
874
|
+
assertNotInsideCacheScopeALS("setStatus");
|
|
641
875
|
stubResponse = new Response(null, {
|
|
642
876
|
status,
|
|
643
877
|
headers: stubResponse.headers,
|
|
@@ -651,35 +885,39 @@ export function createRequestContext<TEnv>(
|
|
|
651
885
|
});
|
|
652
886
|
},
|
|
653
887
|
|
|
654
|
-
// Placeholder - will be replaced below
|
|
655
888
|
use: null as any,
|
|
656
889
|
|
|
657
890
|
method: request.method,
|
|
658
891
|
|
|
659
892
|
_handleStore: handleStore,
|
|
893
|
+
_transitionWhen: [],
|
|
660
894
|
_cacheStore: cacheStore,
|
|
895
|
+
_explicitTaggedStores: explicitTaggedStores,
|
|
896
|
+
_requestTags: new Set<string>(),
|
|
661
897
|
_cacheProfiles: cacheProfiles,
|
|
662
898
|
|
|
663
899
|
waitUntil(fn: () => Promise<void>): void {
|
|
664
900
|
if (executionContext?.waitUntil) {
|
|
665
|
-
//
|
|
666
|
-
|
|
901
|
+
// Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
|
|
902
|
+
// non-async callback becomes a rejected promise handed to the host's
|
|
903
|
+
// waitUntil (logged as a background failure), instead of escaping into
|
|
904
|
+
// the request flow. Mirrors fireAndForgetWaitUntil's deferral.
|
|
905
|
+
executionContext.waitUntil(Promise.resolve().then(fn));
|
|
667
906
|
} else {
|
|
668
|
-
|
|
669
|
-
fn().catch((err) =>
|
|
670
|
-
console.error("[waitUntil] Background task failed:", err),
|
|
671
|
-
);
|
|
907
|
+
fireAndForgetWaitUntil(fn);
|
|
672
908
|
}
|
|
673
909
|
},
|
|
674
910
|
|
|
911
|
+
executionContext,
|
|
912
|
+
|
|
675
913
|
_onResponseCallbacks: [],
|
|
676
914
|
|
|
677
915
|
onResponse(callback: (response: Response) => Response): void {
|
|
678
916
|
assertNotInsideCacheExec(ctx, "onResponse");
|
|
917
|
+
assertNotInsideCacheScopeALS("onResponse");
|
|
679
918
|
this._onResponseCallbacks.push(callback);
|
|
680
919
|
},
|
|
681
920
|
|
|
682
|
-
// Theme properties (only set when themeConfig is provided)
|
|
683
921
|
get theme() {
|
|
684
922
|
return themeConfig ? getTheme() : undefined;
|
|
685
923
|
},
|
|
@@ -703,35 +941,88 @@ export function createRequestContext<TEnv>(
|
|
|
703
941
|
_reportedErrors: new WeakSet<object>(),
|
|
704
942
|
_metricsStore: undefined,
|
|
705
943
|
|
|
944
|
+
_renderBarrier: null as any,
|
|
945
|
+
_resolveRenderBarrier: null as any,
|
|
946
|
+
_renderBarrierSegmentOrder: undefined,
|
|
947
|
+
|
|
706
948
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
707
949
|
};
|
|
708
950
|
|
|
709
|
-
//
|
|
951
|
+
// Lazy allocation: only create Promise when a loader calls rendered().
|
|
952
|
+
let barrierResolved = false;
|
|
953
|
+
let resolveBarrier: (() => void) | undefined;
|
|
954
|
+
ctx._renderBarrier = null as any;
|
|
955
|
+
ctx._resolveRenderBarrier = (
|
|
956
|
+
segments: Array<{ type: string; id: string }>,
|
|
957
|
+
) => {
|
|
958
|
+
if (barrierResolved) return;
|
|
959
|
+
barrierResolved = true;
|
|
960
|
+
const segOrder = segments
|
|
961
|
+
.filter((s) => s.type !== "loader")
|
|
962
|
+
.map((s) => s.id);
|
|
963
|
+
ctx._renderBarrierSegmentOrder = segOrder;
|
|
964
|
+
|
|
965
|
+
const closeGuard = () => {
|
|
966
|
+
ctx._renderBarrierWaiters = undefined;
|
|
967
|
+
ctx._handlerLoaderDeps = undefined;
|
|
968
|
+
ctx._renderBarrierGuardClosed = true;
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
if (ctx._treeHasStreaming) {
|
|
972
|
+
handleStore.settled.then(closeGuard);
|
|
973
|
+
} else {
|
|
974
|
+
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
975
|
+
handleStore,
|
|
976
|
+
segOrder,
|
|
977
|
+
);
|
|
978
|
+
closeGuard();
|
|
979
|
+
}
|
|
980
|
+
if (resolveBarrier) resolveBarrier();
|
|
981
|
+
};
|
|
982
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
983
|
+
get() {
|
|
984
|
+
const p = barrierResolved
|
|
985
|
+
? Promise.resolve()
|
|
986
|
+
: new Promise<void>((resolve) => {
|
|
987
|
+
resolveBarrier = resolve;
|
|
988
|
+
});
|
|
989
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
990
|
+
value: p,
|
|
991
|
+
writable: false,
|
|
992
|
+
configurable: false,
|
|
993
|
+
});
|
|
994
|
+
return p;
|
|
995
|
+
},
|
|
996
|
+
configurable: true,
|
|
997
|
+
});
|
|
998
|
+
|
|
710
999
|
ctx.use = createUseFunction({
|
|
711
1000
|
handleStore,
|
|
712
1001
|
loaderPromises,
|
|
713
1002
|
getContext: () => ctx,
|
|
714
1003
|
});
|
|
715
1004
|
|
|
716
|
-
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
717
1005
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
718
1006
|
return ctx;
|
|
719
1007
|
}
|
|
720
1008
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
*
|
|
726
|
-
|
|
727
|
-
|
|
1009
|
+
// Capture the Max-Age value so it can be parsed numerically. A leading zero
|
|
1010
|
+
// (Max-Age=05) is a non-zero lifetime, not a deletion; only a value that parses
|
|
1011
|
+
// to <= 0 marks a cookie for deletion. Pattern-matching a leading "0" misread
|
|
1012
|
+
// zero-prefixed values like 05 / 010 as deletions.
|
|
1013
|
+
const MAX_AGE_RE = /;\s*Max-Age\s*=\s*(-?\d+)/i;
|
|
1014
|
+
|
|
1015
|
+
function isCookieDeletion(header: string): boolean {
|
|
1016
|
+
const m = MAX_AGE_RE.exec(header);
|
|
1017
|
+
if (!m) return false;
|
|
1018
|
+
return Number(m[1]) <= 0;
|
|
1019
|
+
}
|
|
728
1020
|
|
|
729
1021
|
function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
730
1022
|
const result = new Map<string, string | null>();
|
|
731
1023
|
const setCookies = response.headers.getSetCookie();
|
|
732
1024
|
|
|
733
1025
|
for (const header of setCookies) {
|
|
734
|
-
// First segment before ';' is the name=value pair
|
|
735
1026
|
const semiIdx = header.indexOf(";");
|
|
736
1027
|
const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
|
|
737
1028
|
const eqIdx = pair.indexOf("=");
|
|
@@ -743,49 +1034,22 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
|
743
1034
|
name = decodeURIComponent(pair.substring(0, eqIdx).trim());
|
|
744
1035
|
value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
|
|
745
1036
|
} catch {
|
|
746
|
-
// Malformed encoding — skip this entry
|
|
747
1037
|
continue;
|
|
748
1038
|
}
|
|
749
1039
|
|
|
750
|
-
|
|
751
|
-
const isDeleted = MAX_AGE_ZERO_RE.test(header);
|
|
1040
|
+
const isDeleted = isCookieDeletion(header);
|
|
752
1041
|
result.set(name, isDeleted ? null : value);
|
|
753
1042
|
}
|
|
754
1043
|
|
|
755
1044
|
return result;
|
|
756
1045
|
}
|
|
757
1046
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
cookieHeader: string | null,
|
|
763
|
-
): Record<string, string> {
|
|
764
|
-
if (!cookieHeader) return {};
|
|
765
|
-
|
|
766
|
-
const cookies: Record<string, string> = {};
|
|
767
|
-
const pairs = cookieHeader.split(";");
|
|
768
|
-
|
|
769
|
-
for (const pair of pairs) {
|
|
770
|
-
const [name, ...rest] = pair.trim().split("=");
|
|
771
|
-
if (name) {
|
|
772
|
-
const raw = rest.join("=");
|
|
773
|
-
try {
|
|
774
|
-
cookies[name] = decodeURIComponent(raw);
|
|
775
|
-
} catch {
|
|
776
|
-
// Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
|
|
777
|
-
cookies[name] = raw;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
return cookies;
|
|
783
|
-
}
|
|
1047
|
+
// Re-exported for unit tests and the existing import path. The implementation
|
|
1048
|
+
// lives in the dependency-free ./cookie-parse leaf so consumers (e.g. the host
|
|
1049
|
+
// dispatcher) can share it without pulling this module's request-context graph.
|
|
1050
|
+
export { parseCookiesFromHeader };
|
|
784
1051
|
|
|
785
|
-
|
|
786
|
-
* Serialize a cookie for Set-Cookie header
|
|
787
|
-
*/
|
|
788
|
-
function serializeCookieValue(
|
|
1052
|
+
export function serializeCookieValue(
|
|
789
1053
|
name: string,
|
|
790
1054
|
value: string,
|
|
791
1055
|
options: CookieOptions = {},
|
|
@@ -812,20 +1076,12 @@ export interface CreateUseFunctionOptions<TEnv> {
|
|
|
812
1076
|
getContext: () => RequestContext<TEnv>;
|
|
813
1077
|
}
|
|
814
1078
|
|
|
815
|
-
/**
|
|
816
|
-
* Create the use() function for loader and handle composition.
|
|
817
|
-
*
|
|
818
|
-
* This is the unified implementation used by both RequestContext and HandlerContext.
|
|
819
|
-
* - For loaders: executes and memoizes loader functions
|
|
820
|
-
* - For handles: returns a push function to add handle data
|
|
821
|
-
*/
|
|
822
1079
|
export function createUseFunction<TEnv>(
|
|
823
1080
|
options: CreateUseFunctionOptions<TEnv>,
|
|
824
1081
|
): RequestContext["use"] {
|
|
825
1082
|
const { handleStore, loaderPromises, getContext } = options;
|
|
826
1083
|
|
|
827
1084
|
return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
828
|
-
// Handle case: return a push function
|
|
829
1085
|
if (isHandle(item)) {
|
|
830
1086
|
const handle = item;
|
|
831
1087
|
const ctx = getContext();
|
|
@@ -838,30 +1094,24 @@ export function createUseFunction<TEnv>(
|
|
|
838
1094
|
);
|
|
839
1095
|
}
|
|
840
1096
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
// Push directly - promises will be serialized by RSC and streamed
|
|
852
|
-
handleStore.push(handle.$$id, segmentId, valueOrPromise);
|
|
853
|
-
};
|
|
1097
|
+
return withDefer(
|
|
1098
|
+
(dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
|
|
1099
|
+
const valueOrPromise =
|
|
1100
|
+
typeof dataOrFn === "function"
|
|
1101
|
+
? (dataOrFn as () => Promise<unknown>)()
|
|
1102
|
+
: dataOrFn;
|
|
1103
|
+
|
|
1104
|
+
handleStore.push(handle.$$id, segmentId, valueOrPromise);
|
|
1105
|
+
},
|
|
1106
|
+
);
|
|
854
1107
|
}
|
|
855
1108
|
|
|
856
|
-
// Loader case
|
|
857
1109
|
const loader = item as LoaderDefinition<any, any>;
|
|
858
1110
|
|
|
859
|
-
// Return cached promise if already started
|
|
860
1111
|
if (loaderPromises.has(loader.$$id)) {
|
|
861
1112
|
return loaderPromises.get(loader.$$id);
|
|
862
1113
|
}
|
|
863
1114
|
|
|
864
|
-
// Get loader function - either from loader object or fetchable registry
|
|
865
1115
|
let loaderFn = loader.fn;
|
|
866
1116
|
if (!loaderFn) {
|
|
867
1117
|
const fetchable = getFetchableLoader(loader.$$id);
|
|
@@ -878,24 +1128,36 @@ export function createUseFunction<TEnv>(
|
|
|
878
1128
|
|
|
879
1129
|
const ctx = getContext();
|
|
880
1130
|
|
|
881
|
-
//
|
|
1131
|
+
// Build the typed ctx.search the same way the render path
|
|
1132
|
+
// (createHandlerContext) and the fetchable-loader path (loader-fetch.ts) do:
|
|
1133
|
+
// parse the route's search schema over the cleaned searchParams. The base
|
|
1134
|
+
// RequestContext carries no `search` field, so reading `(ctx as any).search`
|
|
1135
|
+
// here always yielded {} — dropping typed search for action/dispatch loaders.
|
|
1136
|
+
const searchSchema = ctx._routeName
|
|
1137
|
+
? getSearchSchema(ctx._routeName)
|
|
1138
|
+
: undefined;
|
|
1139
|
+
const loaderSearch = searchSchema
|
|
1140
|
+
? parseSearchParams(ctx.searchParams, searchSchema)
|
|
1141
|
+
: {};
|
|
1142
|
+
|
|
882
1143
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
883
1144
|
params: ctx.params,
|
|
884
1145
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
885
1146
|
request: ctx.request,
|
|
886
1147
|
searchParams: ctx.searchParams,
|
|
887
|
-
search:
|
|
1148
|
+
search: loaderSearch,
|
|
888
1149
|
pathname: ctx.pathname,
|
|
889
1150
|
url: ctx.url,
|
|
1151
|
+
originalUrl: ctx.originalUrl,
|
|
890
1152
|
env: ctx.env as any,
|
|
891
|
-
|
|
1153
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
1154
|
+
executionContext: ctx.executionContext,
|
|
892
1155
|
get: ctx.get as any,
|
|
893
|
-
use: <TDep, TDepParams = any>(
|
|
1156
|
+
use: (<TDep, TDepParams = any>(
|
|
894
1157
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
895
1158
|
): Promise<TDep> => {
|
|
896
|
-
// Recursive call - will start dep loader if not already started
|
|
897
1159
|
return ctx.use(dep);
|
|
898
|
-
},
|
|
1160
|
+
}) as LoaderContext["use"],
|
|
899
1161
|
method: "GET",
|
|
900
1162
|
body: undefined,
|
|
901
1163
|
reverse: createReverseFunction(
|
|
@@ -904,15 +1166,22 @@ export function createUseFunction<TEnv>(
|
|
|
904
1166
|
ctx.params as Record<string, string>,
|
|
905
1167
|
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
906
1168
|
),
|
|
1169
|
+
rendered: () => {
|
|
1170
|
+
throw new Error(
|
|
1171
|
+
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
1172
|
+
`It cannot be used from request-context loaders or server actions.`,
|
|
1173
|
+
);
|
|
1174
|
+
},
|
|
907
1175
|
};
|
|
908
1176
|
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1177
|
+
// Meter through the same unified phase API as the loader-resolution funnel
|
|
1178
|
+
// (observePhase), so a loader resolved via this base request-context ctx.use
|
|
1179
|
+
// co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
|
|
1180
|
+
// drift between the two ctx.use implementations.
|
|
1181
|
+
const promise = observePhase(PHASES.loader(loader.$$id), () =>
|
|
1182
|
+
Promise.resolve(loaderFn(loaderCtx)),
|
|
1183
|
+
);
|
|
914
1184
|
|
|
915
|
-
// Memoize for subsequent calls
|
|
916
1185
|
loaderPromises.set(loader.$$id, promise);
|
|
917
1186
|
|
|
918
1187
|
return promise;
|