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