@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- 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 +224 -32
- package/skills/caching/SKILL.md +279 -17
- 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 +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -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 +322 -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 +0 -65
- 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/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 +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- 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 +17 -14
- 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 +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- 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 +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- 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 +117 -23
- 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 +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- 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 +224 -41
- package/src/cache/cache-scope.ts +188 -82
- 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/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- 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 +18 -6
- 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 +31 -23
- 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 +96 -12
- package/src/index.ts +94 -14
- 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 +32 -37
- package/src/prerender.ts +61 -6
- 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 +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- 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 +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -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 +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- 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 +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -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 +96 -68
- 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 +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- 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-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- 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 +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- 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 +109 -118
- package/src/vite/router-discovery.ts +718 -119
- 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 +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- 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
|
@@ -19,66 +19,25 @@ import type {
|
|
|
19
19
|
} from "../../types";
|
|
20
20
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
21
21
|
import { resolveLoaderData } from "./loader-cache.js";
|
|
22
|
-
import { _getRequestContext } from "../../server/request-context.js";
|
|
23
|
-
import { appendMetric } from "../metrics.js";
|
|
24
22
|
import {
|
|
25
23
|
handleHandlerResult,
|
|
26
24
|
tryStaticHandler,
|
|
27
25
|
tryStaticSlot,
|
|
28
26
|
resolveLayoutComponent,
|
|
29
27
|
resolveWithErrorBoundary,
|
|
28
|
+
warnOnStreamedResponse,
|
|
29
|
+
buildLoaderErrorContext,
|
|
30
30
|
} from "./helpers.js";
|
|
31
|
+
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
31
32
|
import { getRouterContext } from "../router-context.js";
|
|
32
|
-
import {
|
|
33
|
+
import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
|
|
34
|
+
import { observeHandler } from "../instrument.js";
|
|
33
35
|
import {
|
|
34
36
|
track,
|
|
35
|
-
|
|
37
|
+
RangoContext,
|
|
36
38
|
runInsideLoaderScope,
|
|
37
39
|
} from "../../server/context.js";
|
|
38
40
|
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Streamed handler telemetry
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Attach a fire-and-forget rejection observer to a streamed handler promise.
|
|
45
|
-
* React catches the actual error via its error boundary; this only emits
|
|
46
|
-
* the handler.error telemetry event.
|
|
47
|
-
*/
|
|
48
|
-
function observeStreamedHandler(
|
|
49
|
-
promise: Promise<ReactNode>,
|
|
50
|
-
segmentId: string,
|
|
51
|
-
segmentType: string,
|
|
52
|
-
pathname?: string,
|
|
53
|
-
routeKey?: string,
|
|
54
|
-
params?: Record<string, string>,
|
|
55
|
-
): void {
|
|
56
|
-
let routerCtx;
|
|
57
|
-
try {
|
|
58
|
-
routerCtx = getRouterContext();
|
|
59
|
-
} catch {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (!routerCtx?.telemetry) return;
|
|
63
|
-
const sink = resolveSink(routerCtx.telemetry);
|
|
64
|
-
const reqId = routerCtx.requestId;
|
|
65
|
-
promise.catch((err: unknown) => {
|
|
66
|
-
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
67
|
-
safeEmit(sink, {
|
|
68
|
-
type: "handler.error",
|
|
69
|
-
timestamp: performance.now(),
|
|
70
|
-
requestId: reqId,
|
|
71
|
-
segmentId,
|
|
72
|
-
segmentType,
|
|
73
|
-
error: errorObj,
|
|
74
|
-
handledByBoundary: true,
|
|
75
|
-
pathname,
|
|
76
|
-
routeKey,
|
|
77
|
-
params,
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
41
|
// ---------------------------------------------------------------------------
|
|
83
42
|
// Fresh path (full match, no revalidation)
|
|
84
43
|
// ---------------------------------------------------------------------------
|
|
@@ -100,7 +59,13 @@ export async function resolveLoaders<TEnv>(
|
|
|
100
59
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
101
60
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
102
61
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
103
|
-
|
|
62
|
+
|
|
63
|
+
// Error context for wrapLoaderPromise: without it, a throwing DSL loader never
|
|
64
|
+
// fires createRouter({ onError }) (phase "loader") nor emits the loader.error
|
|
65
|
+
// telemetry event — wrapLoaderPromise only builds the onError/telemetry path
|
|
66
|
+
// when errorContext is supplied. Built from ctx so the live render path reports
|
|
67
|
+
// loader failures the same way handlers/actions/routing/fetchable-loaders do.
|
|
68
|
+
const errorContext = buildLoaderErrorContext(ctx);
|
|
104
69
|
|
|
105
70
|
if (!loadingDisabled) {
|
|
106
71
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
@@ -122,6 +87,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
122
87
|
entry,
|
|
123
88
|
segmentId,
|
|
124
89
|
ctx.pathname,
|
|
90
|
+
errorContext,
|
|
125
91
|
),
|
|
126
92
|
belongsToRoute,
|
|
127
93
|
};
|
|
@@ -132,46 +98,46 @@ export async function resolveLoaders<TEnv>(
|
|
|
132
98
|
|
|
133
99
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
134
100
|
// settled promises so handlers don't stream loading placeholders.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
101
|
+
//
|
|
102
|
+
// Wrap each loader promise with wrapLoaderPromise BEFORE awaiting. The wrapped
|
|
103
|
+
// promise resolves to a LoaderDataResult and never rejects, routing a failed
|
|
104
|
+
// loader to its own per-loader error boundary. Awaiting the RAW promises here
|
|
105
|
+
// instead would (1) propagate a rejection to the segment-level boundary,
|
|
106
|
+
// collapsing the whole entry and discarding successful sibling data, and
|
|
107
|
+
// (2) leave the other in-flight raw promises without a .catch, producing
|
|
108
|
+
// unhandled rejections. Mirrors the loading path and intercept-resolution.
|
|
109
|
+
const pendingLoaderData = loaderEntries.map((loaderEntry, i) => {
|
|
110
|
+
const { loader } = loaderEntry;
|
|
111
|
+
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
112
|
+
const wrapped = deps.wrapLoaderPromise(
|
|
113
|
+
runInsideLoaderScope(() =>
|
|
114
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
115
|
+
),
|
|
116
|
+
entry,
|
|
117
|
+
segmentId,
|
|
118
|
+
ctx.pathname,
|
|
119
|
+
errorContext,
|
|
139
120
|
);
|
|
140
|
-
return {
|
|
121
|
+
return { wrapped, segmentId };
|
|
141
122
|
});
|
|
142
|
-
await Promise.all(pendingLoaderData.map((p) => p.
|
|
123
|
+
await Promise.all(pendingLoaderData.map((p) => p.wrapped));
|
|
143
124
|
|
|
144
125
|
return loaderEntries.map((loaderEntry, i) => {
|
|
145
126
|
const { loader } = loaderEntry;
|
|
146
|
-
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
147
127
|
const pending = pendingLoaderData[i]!;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const batchEnd = performance.now();
|
|
153
|
-
appendMetric(
|
|
154
|
-
ms,
|
|
155
|
-
`loader:${loader.$$id}`,
|
|
156
|
-
pending.start,
|
|
157
|
-
batchEnd - pending.start,
|
|
158
|
-
2,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
128
|
+
// The "loader:<id>" perf metric is recorded by observePhase at the single
|
|
129
|
+
// loader-metering site (useLoader, reached via ctx.use during
|
|
130
|
+
// resolveLoaderData), with the real per-loader duration rather than a
|
|
131
|
+
// Promise.all batch ceiling.
|
|
161
132
|
return {
|
|
162
|
-
id: segmentId,
|
|
133
|
+
id: pending.segmentId,
|
|
163
134
|
namespace: entry.id,
|
|
164
135
|
type: "loader" as const,
|
|
165
136
|
index: i,
|
|
166
137
|
component: null,
|
|
167
138
|
params: ctx.params,
|
|
168
139
|
loaderId: loader.$$id,
|
|
169
|
-
loaderData:
|
|
170
|
-
pending.promise,
|
|
171
|
-
entry,
|
|
172
|
-
segmentId,
|
|
173
|
-
ctx.pathname,
|
|
174
|
-
),
|
|
140
|
+
loaderData: pending.wrapped,
|
|
175
141
|
belongsToRoute,
|
|
176
142
|
};
|
|
177
143
|
});
|
|
@@ -183,6 +149,15 @@ export async function resolveLoaders<TEnv>(
|
|
|
183
149
|
export interface ResolveSegmentOptions {
|
|
184
150
|
/** When true, skip resolveLoaders() calls (used for pre-rendering) */
|
|
185
151
|
skipLoaders?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* When true, a thrown render error is re-thrown instead of being converted
|
|
154
|
+
* into an error-boundary segment. Set only by the pre-render path so a
|
|
155
|
+
* build-time render failure (and a `throw new Skip()` inside a render fn)
|
|
156
|
+
* surfaces to the build instead of being silently baked into a frozen error
|
|
157
|
+
* page served as a 200 (issue #587). The live request path leaves this unset,
|
|
158
|
+
* so error boundaries keep catching at request time.
|
|
159
|
+
*/
|
|
160
|
+
throwOnError?: boolean;
|
|
186
161
|
}
|
|
187
162
|
|
|
188
163
|
/**
|
|
@@ -224,7 +199,11 @@ export async function resolveSegment<TEnv>(
|
|
|
224
199
|
index: 0,
|
|
225
200
|
component,
|
|
226
201
|
loading: entry.loading === false ? null : entry.loading,
|
|
227
|
-
transition:
|
|
202
|
+
transition: applyViewTransitionDefault(
|
|
203
|
+
entry.transition,
|
|
204
|
+
deps.viewTransitionDefault,
|
|
205
|
+
entry.shortCode,
|
|
206
|
+
),
|
|
228
207
|
params,
|
|
229
208
|
belongsToRoute: false,
|
|
230
209
|
layoutName: entry.id,
|
|
@@ -291,8 +270,11 @@ export async function resolveSegment<TEnv>(
|
|
|
291
270
|
!context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
|
|
292
271
|
const doneRouteHandler = track(`handler:${entry.id}`, 2);
|
|
293
272
|
if (entry.loading) {
|
|
294
|
-
const result = handleHandlerResult(
|
|
273
|
+
const result = handleHandlerResult(
|
|
274
|
+
observeHandler(entry.id, handler, context),
|
|
275
|
+
);
|
|
295
276
|
if (result instanceof Promise) {
|
|
277
|
+
warnOnStreamedResponse(result, entry.id);
|
|
296
278
|
result.finally(doneRouteHandler).catch(() => {});
|
|
297
279
|
const tracked = deps.trackHandler(result, {
|
|
298
280
|
segmentId: entry.shortCode,
|
|
@@ -312,7 +294,9 @@ export async function resolveSegment<TEnv>(
|
|
|
312
294
|
component = result;
|
|
313
295
|
}
|
|
314
296
|
} else {
|
|
315
|
-
component = handleHandlerResult(
|
|
297
|
+
component = handleHandlerResult(
|
|
298
|
+
await observeHandler(entry.id, handler, context),
|
|
299
|
+
);
|
|
316
300
|
doneRouteHandler();
|
|
317
301
|
}
|
|
318
302
|
}
|
|
@@ -359,7 +343,11 @@ export async function resolveSegment<TEnv>(
|
|
|
359
343
|
index: 0,
|
|
360
344
|
component: component ?? null,
|
|
361
345
|
loading: entry.loading === false ? null : entry.loading,
|
|
362
|
-
transition:
|
|
346
|
+
transition: applyViewTransitionDefault(
|
|
347
|
+
entry.transition,
|
|
348
|
+
deps.viewTransitionDefault,
|
|
349
|
+
entry.shortCode,
|
|
350
|
+
),
|
|
363
351
|
params,
|
|
364
352
|
belongsToRoute: true,
|
|
365
353
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
@@ -443,7 +431,11 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
443
431
|
belongsToRoute,
|
|
444
432
|
layoutName: orphan.id,
|
|
445
433
|
loading: orphan.loading === false ? null : orphan.loading,
|
|
446
|
-
transition:
|
|
434
|
+
transition: applyViewTransitionDefault(
|
|
435
|
+
orphan.transition,
|
|
436
|
+
deps.viewTransitionDefault,
|
|
437
|
+
orphan.shortCode,
|
|
438
|
+
),
|
|
447
439
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
448
440
|
});
|
|
449
441
|
|
|
@@ -515,6 +507,14 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
515
507
|
if (handler === undefined) {
|
|
516
508
|
continue;
|
|
517
509
|
}
|
|
510
|
+
// Pin `_currentSegmentId` to the slot's own id so handle pushes from
|
|
511
|
+
// inside the slot handler get their own bucket in the HandleStore.
|
|
512
|
+
// Parent-keying would collapse them into the parent layout's bucket;
|
|
513
|
+
// the partial-update merge then replaces the parent's bucket on a
|
|
514
|
+
// slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
|
|
515
|
+
// filterSegmentOrder() retains slot ids so the client preserves them.
|
|
516
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
517
|
+
`${parentShortCode}.${slot}`;
|
|
518
518
|
const doneParallelHandler = track(
|
|
519
519
|
`handler:${parallelEntry.id}.${slot}`,
|
|
520
520
|
2,
|
|
@@ -523,7 +523,9 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
523
523
|
parallelEntry.loading !== undefined && parallelEntry.loading !== false;
|
|
524
524
|
if (hasLoadingFallback) {
|
|
525
525
|
const result =
|
|
526
|
-
typeof handler === "function"
|
|
526
|
+
typeof handler === "function"
|
|
527
|
+
? observeHandler(`${parallelEntry.id}.${slot}`, handler, context)
|
|
528
|
+
: handler;
|
|
527
529
|
if (result instanceof Promise) {
|
|
528
530
|
result.finally(doneParallelHandler).catch(() => {});
|
|
529
531
|
const tracked = deps.trackHandler(result, {
|
|
@@ -545,7 +547,13 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
545
547
|
}
|
|
546
548
|
} else {
|
|
547
549
|
component =
|
|
548
|
-
typeof handler === "function"
|
|
550
|
+
typeof handler === "function"
|
|
551
|
+
? await observeHandler(
|
|
552
|
+
`${parallelEntry.id}.${slot}`,
|
|
553
|
+
handler,
|
|
554
|
+
context,
|
|
555
|
+
)
|
|
556
|
+
: handler;
|
|
549
557
|
doneParallelHandler();
|
|
550
558
|
}
|
|
551
559
|
}
|
|
@@ -557,7 +565,11 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
557
565
|
index: 0,
|
|
558
566
|
component,
|
|
559
567
|
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
560
|
-
transition:
|
|
568
|
+
transition: applyViewTransitionDefault(
|
|
569
|
+
parallelEntry.transition,
|
|
570
|
+
deps.viewTransitionDefault,
|
|
571
|
+
`${parentShortCode}.${slot}`,
|
|
572
|
+
),
|
|
561
573
|
params,
|
|
562
574
|
slot,
|
|
563
575
|
belongsToRoute,
|
|
@@ -624,7 +636,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
624
636
|
// can guard non-cacheable variable reads. Also guards response-level
|
|
625
637
|
// side effects (headers.set). Persists for all descendant entries.
|
|
626
638
|
if (entry.type === "cache") {
|
|
627
|
-
const store =
|
|
639
|
+
const store = RangoContext.getStore();
|
|
628
640
|
if (store) store.insideCacheScope = true;
|
|
629
641
|
}
|
|
630
642
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
@@ -646,6 +658,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
646
658
|
deps,
|
|
647
659
|
{ request: safeRequest, url: context.url, routeKey, telemetry },
|
|
648
660
|
context.pathname,
|
|
661
|
+
options?.throwOnError,
|
|
649
662
|
);
|
|
650
663
|
doneEntry();
|
|
651
664
|
// Deduplicate by segment ID. include() scopes can produce entries that
|
|
@@ -19,13 +19,48 @@ import {
|
|
|
19
19
|
import { getRequestContext } from "../../server/request-context.js";
|
|
20
20
|
import { DefaultErrorFallback } from "../../default-error-boundary.js";
|
|
21
21
|
import type { EntryData } from "../../server/context";
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
ResolvedSegment,
|
|
24
|
+
ErrorInfo,
|
|
25
|
+
HandlerContext,
|
|
26
|
+
InternalHandlerContext,
|
|
27
|
+
} from "../../types";
|
|
23
28
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
24
29
|
import { debugLog } from "../logging.js";
|
|
25
30
|
import { tryStaticLookup } from "./static-store.js";
|
|
31
|
+
import { observeHandler } from "../instrument.js";
|
|
26
32
|
import type { TelemetrySink } from "../telemetry.js";
|
|
27
33
|
import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
|
|
28
34
|
|
|
35
|
+
/** The errorContext shape wrapLoaderPromise expects as its 5th argument. */
|
|
36
|
+
type LoaderErrorContext<TEnv> = NonNullable<
|
|
37
|
+
Parameters<SegmentResolutionDeps<TEnv>["wrapLoaderPromise"]>[4]
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the errorContext passed to wrapLoaderPromise so a throwing DSL loader
|
|
42
|
+
* fires createRouter({ onError }) (phase "loader") and emits the loader.error
|
|
43
|
+
* telemetry event. wrapLoaderPromise only wires the onError/telemetry path when
|
|
44
|
+
* this 5th argument is present; every real call site previously omitted it, so
|
|
45
|
+
* loaders were the one phase whose failures were silently dropped (handlers,
|
|
46
|
+
* actions, routing, rendering, and fetchable loaders all reported correctly).
|
|
47
|
+
*
|
|
48
|
+
* The fields come off the handler context, which already carries the request,
|
|
49
|
+
* url, params, env, and (on the internal shape) the matched route name.
|
|
50
|
+
*/
|
|
51
|
+
export function buildLoaderErrorContext<TEnv>(
|
|
52
|
+
ctx: HandlerContext<any, TEnv>,
|
|
53
|
+
): LoaderErrorContext<TEnv> {
|
|
54
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
55
|
+
return {
|
|
56
|
+
request: ctx.request,
|
|
57
|
+
url: ctx.url,
|
|
58
|
+
routeKey: internal._routeName,
|
|
59
|
+
params: ctx.params as Record<string, string>,
|
|
60
|
+
env: ctx.env,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
29
64
|
// ---------------------------------------------------------------------------
|
|
30
65
|
// Handler result processing
|
|
31
66
|
// ---------------------------------------------------------------------------
|
|
@@ -52,6 +87,40 @@ export function handleHandlerResult(
|
|
|
52
87
|
return result;
|
|
53
88
|
}
|
|
54
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Dev-only: warn when a handler on a route that declares loading() resolves or
|
|
92
|
+
* rejects with a Response (e.g. redirect()).
|
|
93
|
+
*
|
|
94
|
+
* On a non-loading route a returned/thrown Response short-circuits to an HTTP
|
|
95
|
+
* redirect. But when the route declares loading(), the handler result is
|
|
96
|
+
* streamed (not awaited at the resolution boundary), so the Response surfaces
|
|
97
|
+
* only during RSC serialization and is rendered into the stream instead of
|
|
98
|
+
* becoming a 302/308 — a silent failure mode. Issue redirects from middleware,
|
|
99
|
+
* a loader, or a synchronous handler return instead. Compiled out in production.
|
|
100
|
+
*/
|
|
101
|
+
export function warnOnStreamedResponse(
|
|
102
|
+
result: Promise<unknown>,
|
|
103
|
+
entryId: string,
|
|
104
|
+
): void {
|
|
105
|
+
if (process.env.NODE_ENV === "production") return;
|
|
106
|
+
// A Response can surface either as a rejection (handleHandlerResult rethrows a
|
|
107
|
+
// resolved Response) or as a resolved value (the raw parallel-slot handler is
|
|
108
|
+
// not run through handleHandlerResult). Check both so every streamed path is
|
|
109
|
+
// covered. Each handler is an independent observer; it does not consume the
|
|
110
|
+
// rejection for the trackHandler/observeStreamedHandler chains.
|
|
111
|
+
const check = (value: unknown) => {
|
|
112
|
+
if (value instanceof Response) {
|
|
113
|
+
console.warn(
|
|
114
|
+
`[rango] Handler for "${entryId}" returned a Response (e.g. ` +
|
|
115
|
+
`redirect()), but it declares loading(): the Response is rendered ` +
|
|
116
|
+
`into the RSC stream, NOT sent as an HTTP redirect. Issue redirects ` +
|
|
117
|
+
`from middleware, a loader, or a synchronous handler return.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
result.then(check, check);
|
|
122
|
+
}
|
|
123
|
+
|
|
55
124
|
// ---------------------------------------------------------------------------
|
|
56
125
|
// Static handler interception
|
|
57
126
|
// ---------------------------------------------------------------------------
|
|
@@ -96,11 +165,16 @@ export async function resolveLayoutComponent<TEnv>(
|
|
|
96
165
|
entry: EntryData,
|
|
97
166
|
context: HandlerContext<any, TEnv>,
|
|
98
167
|
): Promise<ReactNode> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
168
|
+
// Static/prerender hit: no handler runs, so emit no rango.handler span.
|
|
169
|
+
const staticComponent = await tryStaticHandler(entry, entry.shortCode);
|
|
170
|
+
if (staticComponent !== undefined) return staticComponent;
|
|
171
|
+
const handler = entry.handler;
|
|
172
|
+
if (typeof handler !== "function") return handler as ReactNode;
|
|
173
|
+
// Wrap ONLY the handler call in the rango.handler span (the perf metric is owned
|
|
174
|
+
// by track("handler:<id>") at the call site). handleHandlerResult stays OUTSIDE
|
|
175
|
+
// the span so a handler that returns a Response (redirect control flow, which it
|
|
176
|
+
// rethrows) is not recorded as a span error — mirrors the route-handler sites.
|
|
177
|
+
return handleHandlerResult(await observeHandler(entry.id, handler, context));
|
|
104
178
|
}
|
|
105
179
|
|
|
106
180
|
// ---------------------------------------------------------------------------
|
|
@@ -250,11 +324,17 @@ export async function resolveWithErrorBoundary<TEnv, TResult>(
|
|
|
250
324
|
deps: SegmentResolutionDeps<TEnv>,
|
|
251
325
|
report?: ErrorReportContext,
|
|
252
326
|
pathname?: string,
|
|
327
|
+
throwOnError?: boolean,
|
|
253
328
|
): Promise<TResult> {
|
|
254
329
|
try {
|
|
255
330
|
return await resolveFn();
|
|
256
331
|
} catch (error) {
|
|
257
332
|
if (error instanceof Response) throw error;
|
|
333
|
+
// Pre-render surfaces render failures to the build instead of baking the
|
|
334
|
+
// error boundary as a frozen 200 (issue #587). A `throw new Skip()` in a
|
|
335
|
+
// render fn also propagates here so the build can skip that URL rather than
|
|
336
|
+
// bake its error page. The live request path leaves throwOnError unset.
|
|
337
|
+
if (throwOnError) throw error;
|
|
258
338
|
const segment = catchSegmentError(
|
|
259
339
|
error,
|
|
260
340
|
entry,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Cache key resolution (3-tier, matching CacheScope.resolveKey):
|
|
9
9
|
* 1. options.key(requestCtx) — full override
|
|
10
10
|
* 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
|
|
11
|
-
* 3. loader:{loaderId}:{pathname}:{sortedParams} — default
|
|
11
|
+
* 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
|
|
12
12
|
*
|
|
13
13
|
* Values are serialized via RSC Flight (serializeResult/deserializeResult),
|
|
14
14
|
* supporting ReactNode, Promises, null, and all RSC-serializable types.
|
|
@@ -19,18 +19,23 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { LoaderEntry } from "../../server/context.js";
|
|
22
|
-
import type { HandlerContext } from "../../types.js";
|
|
22
|
+
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
23
23
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getRequestContext,
|
|
26
|
+
runWithRequestContext,
|
|
27
|
+
} from "../../server/request-context.js";
|
|
25
28
|
import { sortedRouteParams } from "../../cache/cache-key-utils.js";
|
|
26
29
|
import {
|
|
27
30
|
resolveTtl,
|
|
28
31
|
resolveSwrWindow,
|
|
29
32
|
resolveCacheKey,
|
|
30
33
|
resolveCacheStore,
|
|
34
|
+
resolveTagsOption,
|
|
31
35
|
DEFAULT_ROUTE_TTL,
|
|
32
36
|
} from "../../cache/cache-policy.js";
|
|
33
37
|
import { readThroughItem } from "../../cache/read-through-swr.js";
|
|
38
|
+
import { recordRequestTags } from "../../cache/cache-tag.js";
|
|
34
39
|
// Lazy-loaded to avoid pulling @vitejs/plugin-rsc/rsc into modules that
|
|
35
40
|
// import segment-resolution but never use loader caching.
|
|
36
41
|
let _serializeResult: typeof import("../../cache/segment-codec.js").serializeResult;
|
|
@@ -55,12 +60,13 @@ function debugLoaderCacheLog(message: string): void {
|
|
|
55
60
|
|
|
56
61
|
function getDefaultLoaderCacheKey(
|
|
57
62
|
loaderId: string,
|
|
63
|
+
host: string,
|
|
58
64
|
pathname: string,
|
|
59
65
|
params: Record<string, string>,
|
|
60
66
|
): string {
|
|
61
67
|
const paramStr = sortedRouteParams(params);
|
|
62
68
|
const base = paramStr ? `${pathname}:${paramStr}` : pathname;
|
|
63
|
-
return `loader:${loaderId}:${base}`;
|
|
69
|
+
return `loader:${loaderId}:${host}${base}`;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
/**
|
|
@@ -74,7 +80,13 @@ async function resolveLoaderKey(
|
|
|
74
80
|
params: Record<string, string>,
|
|
75
81
|
): Promise<string> {
|
|
76
82
|
const options = loaderEntry.cache!.options;
|
|
77
|
-
|
|
83
|
+
// The host is part of the loader cache identity, matching the route-level
|
|
84
|
+
// cache (cache-scope getCacheKeyBase: `${host}${pathname}`) and "use cache"
|
|
85
|
+
// (cache-runtime pushes ctx.url.host). Without it, a multi-tenant host router
|
|
86
|
+
// serving the same pathname for different hosts would leak one host's cached
|
|
87
|
+
// loader data to another.
|
|
88
|
+
const host = getRequestContext()?.url?.host ?? "localhost";
|
|
89
|
+
const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
|
|
78
90
|
if (options === false) return defaultKey;
|
|
79
91
|
return resolveCacheKey(options.key, store, defaultKey, "LoaderCache");
|
|
80
92
|
}
|
|
@@ -87,23 +99,8 @@ async function resolveLoaderKey(
|
|
|
87
99
|
*/
|
|
88
100
|
function resolveTags(loaderEntry: LoaderEntry): string[] | undefined {
|
|
89
101
|
const options = loaderEntry.cache?.options;
|
|
90
|
-
if (!options
|
|
91
|
-
|
|
92
|
-
if (typeof options.tags === "function") {
|
|
93
|
-
const requestCtx = getRequestContext();
|
|
94
|
-
if (!requestCtx) return undefined;
|
|
95
|
-
try {
|
|
96
|
-
return options.tags(requestCtx);
|
|
97
|
-
} catch (error) {
|
|
98
|
-
console.error(
|
|
99
|
-
`[LoaderCache] Tags function failed, caching without tags:`,
|
|
100
|
-
error,
|
|
101
|
-
);
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return options.tags;
|
|
102
|
+
if (!options) return undefined;
|
|
103
|
+
return resolveTagsOption(options.tags, getRequestContext(), "LoaderCache");
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
function getLoaderStore(
|
|
@@ -119,6 +116,11 @@ function getLoaderStore(
|
|
|
119
116
|
*
|
|
120
117
|
* When the LoaderEntry has no cache config, delegates directly to ctx.use(loader).
|
|
121
118
|
* When cached, checks store first and stores on miss via waitUntil.
|
|
119
|
+
*
|
|
120
|
+
* Loader metering is NOT done here — it lives at the ctx.use execution funnel
|
|
121
|
+
* (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
|
|
122
|
+
* so it emits no loader phase (the loader did not execute; the hit is only a
|
|
123
|
+
* LoaderCache debug log).
|
|
122
124
|
*/
|
|
123
125
|
export function resolveLoaderData<TEnv>(
|
|
124
126
|
loaderEntry: LoaderEntry,
|
|
@@ -148,15 +150,51 @@ export function resolveLoaderData<TEnv>(
|
|
|
148
150
|
|
|
149
151
|
const loaderId = loaderEntry.loader.$$id;
|
|
150
152
|
|
|
153
|
+
// A handler that later awaits this same loader via ctx.use(loader) must get
|
|
154
|
+
// THIS memoized promise, not a fresh execution. Rather than rebind ctx.use
|
|
155
|
+
// once per cached loader (O(N) chained wrappers + a synchronous
|
|
156
|
+
// capture-before-overwrite invariant), install a single stable interceptor on
|
|
157
|
+
// the first cached loader that consults a per-ctx override table, then just
|
|
158
|
+
// prime the table for each subsequent cached loader. The captured pre-
|
|
159
|
+
// interceptor `originalUse` (whatever setup mode installed it) runs the
|
|
160
|
+
// cache-miss execute, so a loader never awaits its own in-flight promise.
|
|
161
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
162
|
+
let overrides = internal._loaderCacheOverrides;
|
|
163
|
+
if (!overrides) {
|
|
164
|
+
overrides = internal._loaderCacheOverrides = new Map();
|
|
165
|
+
const originalUse = ctx.use;
|
|
166
|
+
internal._loaderCacheOriginalUse = originalUse;
|
|
167
|
+
ctx.use = ((item: any) => {
|
|
168
|
+
const cached = overrides!.get(item?.$$id);
|
|
169
|
+
if (cached) return cached;
|
|
170
|
+
return originalUse(item);
|
|
171
|
+
}) as typeof ctx.use;
|
|
172
|
+
}
|
|
173
|
+
const runMiss = internal._loaderCacheOriginalUse!;
|
|
174
|
+
|
|
175
|
+
// Dedup the cache read-through across repeated resolutions of the SAME
|
|
176
|
+
// loaderId in one request. An orphan layout with parallel slots inherits its
|
|
177
|
+
// parent route's loaders, so resolveOrphanLayout (fresh.ts) re-resolves the
|
|
178
|
+
// parent's loaders under a different shortCode — calling resolveLoaderData
|
|
179
|
+
// again for the same loaderId. The cache key (loader:{loaderId}:{host}
|
|
180
|
+
// {pathname}:{sortedParams}) does not include the shortCode and ctx/params
|
|
181
|
+
// are identical, so both resolutions produce the same data. Reuse the already
|
|
182
|
+
// in-flight dataPromise instead of issuing a second getItem/setItem (e.g. a
|
|
183
|
+
// second KV round-trip) for one logical cached loader. The shortCode only
|
|
184
|
+
// affects the emitted segmentId in resolveLoaders, not the cached value.
|
|
185
|
+
const existing = overrides.get(loaderId);
|
|
186
|
+
if (existing) return existing;
|
|
187
|
+
|
|
188
|
+
// Compute ttl/swr/tags only AFTER the dedup short-circuit: a deduped second
|
|
189
|
+
// resolution of the same loaderId (the orphan-layout inheritance path) must
|
|
190
|
+
// not re-run the user tags() callback. These values are only consumed inside
|
|
191
|
+
// the read-through below, so they belong here, past the dedup gate.
|
|
151
192
|
const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
|
|
152
193
|
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
153
194
|
const swr = swrWindow || undefined;
|
|
154
195
|
const tags = resolveTags(loaderEntry);
|
|
196
|
+
recordRequestTags(tags);
|
|
155
197
|
|
|
156
|
-
// Wrap ctx.use() so cache HIT primes the handler's memoization map.
|
|
157
|
-
// ctx.use() closes over the match context's loaderPromises (not request context's).
|
|
158
|
-
// By intercepting ctx.use(), we inject cached data into the correct map.
|
|
159
|
-
const originalUse = ctx.use;
|
|
160
198
|
const dataPromise = (async () => {
|
|
161
199
|
const codec = await getCodec();
|
|
162
200
|
const key = await resolveLoaderKey(
|
|
@@ -167,11 +205,20 @@ export function resolveLoaderData<TEnv>(
|
|
|
167
205
|
ctx.params,
|
|
168
206
|
);
|
|
169
207
|
|
|
208
|
+
// Capture the request context up front (foreground, ALS present) so the
|
|
209
|
+
// background stale revalidation can re-establish it. On workerd a waitUntil
|
|
210
|
+
// task runs detached from the request's I/O context, so a loader body that
|
|
211
|
+
// reads the ambient getRequestContext() would otherwise throw "called
|
|
212
|
+
// outside of a request context" and the revalidation would fail silently.
|
|
213
|
+
// The wrap is applied via wrapBackground (background path only); the
|
|
214
|
+
// foreground miss runs execute() directly since its context is present.
|
|
215
|
+
const requestCtxForExecute = getRequestContext();
|
|
170
216
|
return readThroughItem({
|
|
171
217
|
getItem: (k) => store.getItem!(k),
|
|
172
218
|
setItem: (k, v, o) => store.setItem!(k, v, o),
|
|
173
219
|
key,
|
|
174
|
-
execute: () =>
|
|
220
|
+
execute: () => runMiss(loaderEntry.loader),
|
|
221
|
+
wrapBackground: (run) => runWithRequestContext(requestCtxForExecute, run),
|
|
175
222
|
serialize: (d) => codec.serializeResult(d),
|
|
176
223
|
deserialize: (v) => codec.deserializeResult(v),
|
|
177
224
|
storeOptions: { ttl, swr, tags },
|
|
@@ -179,21 +226,11 @@ export function resolveLoaderData<TEnv>(
|
|
|
179
226
|
onStale: () => debugLoaderCacheLog(`[LoaderCache] STALE: ${key}`),
|
|
180
227
|
onMiss: () => debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`),
|
|
181
228
|
onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
|
|
182
|
-
host:
|
|
229
|
+
host: requestCtxForExecute,
|
|
183
230
|
});
|
|
184
231
|
})();
|
|
185
232
|
|
|
186
|
-
|
|
187
|
-
// This is needed because ctx.use() closes over the match context's loaderPromises
|
|
188
|
-
// map which is separate from the request context. By wrapping use(), we intercept
|
|
189
|
-
// the handler's call and return the shared dataPromise.
|
|
190
|
-
const wrappedUse = ((item: any) => {
|
|
191
|
-
if (item === loaderEntry.loader || item?.$$id === loaderId) {
|
|
192
|
-
return dataPromise;
|
|
193
|
-
}
|
|
194
|
-
return originalUse(item);
|
|
195
|
-
}) as typeof ctx.use;
|
|
196
|
-
ctx.use = wrappedUse;
|
|
233
|
+
overrides.set(loaderId, dataPromise);
|
|
197
234
|
|
|
198
235
|
return dataPromise;
|
|
199
236
|
}
|