@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
|
@@ -101,7 +101,10 @@
|
|
|
101
101
|
* - Non-GET request (only GET requests are cacheable)
|
|
102
102
|
*/
|
|
103
103
|
import type { ResolvedSegment } from "../../types.js";
|
|
104
|
-
import {
|
|
104
|
+
import {
|
|
105
|
+
getRequestContext,
|
|
106
|
+
runWithRequestContext,
|
|
107
|
+
} from "../../server/request-context.js";
|
|
105
108
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
109
|
import { getRouterContext } from "../router-context.js";
|
|
107
110
|
import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
|
|
@@ -123,21 +126,14 @@ export function withCacheStore<TEnv>(
|
|
|
123
126
|
): AsyncGenerator<ResolvedSegment> {
|
|
124
127
|
const ms = ctx.metricsStore;
|
|
125
128
|
|
|
126
|
-
// Collect all segments while passing them through
|
|
127
129
|
const allSegments: ResolvedSegment[] = [];
|
|
128
130
|
for await (const segment of source) {
|
|
129
131
|
allSegments.push(segment);
|
|
130
132
|
yield segment;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
// Measure own work only (after source iteration completes)
|
|
134
135
|
const ownStart = performance.now();
|
|
135
136
|
|
|
136
|
-
// Skip caching if:
|
|
137
|
-
// 1. Cache miss but cache scope is disabled
|
|
138
|
-
// 2. This is an action (actions don't cache)
|
|
139
|
-
// 3. Cache was already hit (no need to re-cache)
|
|
140
|
-
// 4. Non-GET request (only cache GET requests)
|
|
141
137
|
if (
|
|
142
138
|
!ctx.cacheScope?.enabled ||
|
|
143
139
|
ctx.isAction ||
|
|
@@ -162,31 +158,45 @@ export function withCacheStore<TEnv>(
|
|
|
162
158
|
createHandleStore,
|
|
163
159
|
} = getRouterContext<TEnv>();
|
|
164
160
|
|
|
165
|
-
//
|
|
166
|
-
|
|
161
|
+
// On a fresh intercept miss the intercept slot segments flow through
|
|
162
|
+
// `source` into allSegments AND are also recorded on state.interceptSegments.
|
|
163
|
+
// Dedup by id so each segment is cached once — cacheRoute serializes the
|
|
164
|
+
// array verbatim (no id-dedup), so an un-deduped append doubles the intercept
|
|
165
|
+
// segment in the cached entry, re-rendering the slot twice on the next hit.
|
|
166
|
+
const seenSegmentIds = new Set(allSegments.map((s) => s.id));
|
|
167
|
+
const allSegmentsToCache = [
|
|
168
|
+
...allSegments,
|
|
169
|
+
...state.interceptSegments.filter((s) => !seenSegmentIds.has(s.id)),
|
|
170
|
+
];
|
|
167
171
|
|
|
168
|
-
// Check if any non-loader segments have null components from revalidation
|
|
169
|
-
// skip (client already had them). Segments where the handler intentionally
|
|
170
|
-
// returned null are not revalidation skips — re-rendering them will still
|
|
171
|
-
// produce null, so proactive caching would be wasted work.
|
|
172
|
-
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
173
172
|
const hasNullComponents = allSegmentsToCache.some(
|
|
174
173
|
(s) =>
|
|
175
|
-
s.component === null &&
|
|
174
|
+
s.component === null &&
|
|
175
|
+
s.type !== "loader" &&
|
|
176
|
+
ctx.clientSegmentSet.has(s.id),
|
|
176
177
|
);
|
|
177
178
|
|
|
178
179
|
const requestCtx = getRequestContext();
|
|
179
180
|
if (!requestCtx) return;
|
|
180
181
|
|
|
181
182
|
const cacheScope = ctx.cacheScope;
|
|
183
|
+
|
|
184
|
+
// Record the route's segment-DSL cache tags into the request tag union NOW,
|
|
185
|
+
// synchronously in the pipeline. The actual store write (cacheRoute) runs in
|
|
186
|
+
// requestCtx.waitUntil() below — and the proactive path re-resolves the whole
|
|
187
|
+
// tree first — so cacheRoute's own tag recording races the document cache's
|
|
188
|
+
// post-body-drain snapshot of _requestTags. On a first-write miss the document
|
|
189
|
+
// tag union could miss these tags and revalidateTag()/updateTag() would not
|
|
190
|
+
// invalidate the cached document. Recording here (before the snapshot) closes
|
|
191
|
+
// the window for both the direct and proactive write paths; the duplicate
|
|
192
|
+
// record inside cacheRoute is idempotent.
|
|
193
|
+
cacheScope.recordTags(requestCtx);
|
|
194
|
+
|
|
182
195
|
const reqId = INTERNAL_RANGO_DEBUG
|
|
183
196
|
? getOrCreateRequestId(ctx.request)
|
|
184
197
|
: undefined;
|
|
185
198
|
|
|
186
|
-
// Register onResponse callback to skip caching for non-200 responses
|
|
187
|
-
// Note: error/notFound status codes are set elsewhere (not caching-specific)
|
|
188
199
|
requestCtx.onResponse((response) => {
|
|
189
|
-
// Only cache successful responses
|
|
190
200
|
if (response.status !== 200) {
|
|
191
201
|
debugLog("cacheStore", "skipping cache for non-200 response", {
|
|
192
202
|
status: response.status,
|
|
@@ -196,10 +206,7 @@ export function withCacheStore<TEnv>(
|
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
if (hasNullComponents) {
|
|
199
|
-
// Proactive caching: render all segments fresh in background
|
|
200
|
-
// This ensures cache has complete components for future requests
|
|
201
209
|
requestCtx.waitUntil(async () => {
|
|
202
|
-
// Prevent background metrics from polluting foreground timeline.
|
|
203
210
|
const savedMetrics = ctx.Store.metrics;
|
|
204
211
|
ctx.Store.metrics = undefined;
|
|
205
212
|
|
|
@@ -207,15 +214,9 @@ export function withCacheStore<TEnv>(
|
|
|
207
214
|
debugLog("cacheStore", "proactive caching started", {
|
|
208
215
|
pathname: ctx.pathname,
|
|
209
216
|
});
|
|
210
|
-
// Swap to a fresh HandleStore so handle.push() calls from
|
|
211
|
-
// proactive resolution are captured (not silenced). The original
|
|
212
|
-
// store's stream is already sent by waitUntil time.
|
|
213
|
-
// cacheRoute reads from requestCtx._handleStore, so this ensures
|
|
214
|
-
// complete handle data (e.g. breadcrumbs) is cached.
|
|
215
217
|
const originalHandleStore = requestCtx._handleStore;
|
|
216
218
|
requestCtx._handleStore = createHandleStore();
|
|
217
219
|
try {
|
|
218
|
-
// Create fresh context for proactive caching
|
|
219
220
|
const proactiveHandlerContext = createHandlerContext(
|
|
220
221
|
ctx.matched.params,
|
|
221
222
|
ctx.request,
|
|
@@ -230,36 +231,49 @@ export function withCacheStore<TEnv>(
|
|
|
230
231
|
);
|
|
231
232
|
const proactiveLoaderPromises = new Map<string, Promise<any>>();
|
|
232
233
|
|
|
233
|
-
// Use normal loader access so handle data is captured
|
|
234
234
|
setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
|
|
235
235
|
|
|
236
|
-
// Re-resolve ALL segments without revalidation.
|
|
237
|
-
// Skip DSL loaders — they are never cached (cacheRoute filters them)
|
|
238
|
-
// and are always resolved fresh on each request.
|
|
239
236
|
const Store = ctx.Store;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
237
|
+
// Re-establish the request-context ALS around the re-render. Store
|
|
238
|
+
// is a different ALS (DSL build context); on workerd a waitUntil
|
|
239
|
+
// task runs detached from the request's I/O context, so a handler/
|
|
240
|
+
// component that reads the ambient getRequestContext() during this
|
|
241
|
+
// background re-render would otherwise throw "called outside of a
|
|
242
|
+
// request context".
|
|
243
|
+
const freshSegments = await runWithRequestContext(requestCtx, () =>
|
|
244
|
+
Store.run(() =>
|
|
245
|
+
resolveAllSegments(
|
|
246
|
+
ctx.entries,
|
|
247
|
+
ctx.routeKey,
|
|
248
|
+
ctx.matched.params,
|
|
249
|
+
proactiveHandlerContext,
|
|
250
|
+
proactiveLoaderPromises,
|
|
251
|
+
{ skipLoaders: true },
|
|
252
|
+
),
|
|
248
253
|
),
|
|
249
254
|
);
|
|
250
255
|
|
|
251
|
-
// Also resolve intercept segments fresh if applicable
|
|
252
256
|
let freshInterceptSegments: ResolvedSegment[] = [];
|
|
253
257
|
if (ctx.interceptResult) {
|
|
254
|
-
freshInterceptSegments = await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
freshInterceptSegments = await runWithRequestContext(
|
|
259
|
+
requestCtx,
|
|
260
|
+
() =>
|
|
261
|
+
Store.run(() =>
|
|
262
|
+
resolveInterceptEntry(
|
|
263
|
+
ctx.interceptResult!.intercept,
|
|
264
|
+
ctx.interceptResult!.entry,
|
|
265
|
+
ctx.matched.params,
|
|
266
|
+
proactiveHandlerContext,
|
|
267
|
+
true, // belongsToRoute
|
|
268
|
+
// No revalidationContext = render fresh
|
|
269
|
+
undefined,
|
|
270
|
+
// Skip intercept middleware: the foreground already ran it
|
|
271
|
+
// before the response was sent. Re-running here (post-
|
|
272
|
+
// response, background) would fire side effects twice and a
|
|
273
|
+
// short-circuit Response would silently abort this write.
|
|
274
|
+
{ skipMiddleware: true },
|
|
275
|
+
),
|
|
276
|
+
),
|
|
263
277
|
);
|
|
264
278
|
}
|
|
265
279
|
|
|
@@ -300,8 +314,6 @@ export function withCacheStore<TEnv>(
|
|
|
300
314
|
}
|
|
301
315
|
});
|
|
302
316
|
} else {
|
|
303
|
-
// All segments have components - cache directly
|
|
304
|
-
// Schedule caching in waitUntil since cacheRoute is now async (key resolution)
|
|
305
317
|
if (INTERNAL_RANGO_DEBUG) {
|
|
306
318
|
console.log(
|
|
307
319
|
`[RSC CacheStore][req:${reqId}] Direct cache path: scheduling cacheRoute for ${ctx.pathname} (${allSegmentsToCache.length} segments, hasNullComponents=${hasNullComponents})`,
|
|
@@ -125,17 +125,14 @@ export function withInterceptResolution<TEnv>(
|
|
|
125
125
|
): AsyncGenerator<ResolvedSegment> {
|
|
126
126
|
const ms = ctx.metricsStore;
|
|
127
127
|
|
|
128
|
-
// First, yield all segments from the source (main segment resolution or cache)
|
|
129
128
|
const segments: ResolvedSegment[] = [];
|
|
130
129
|
for await (const segment of source) {
|
|
131
130
|
segments.push(segment);
|
|
132
131
|
yield segment;
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
// Measure own work only (after source iteration completes)
|
|
136
134
|
const ownStart = performance.now();
|
|
137
135
|
|
|
138
|
-
// Skip intercept resolution for full match (document requests don't have intercepts)
|
|
139
136
|
if (ctx.isFullMatch) {
|
|
140
137
|
if (ms) {
|
|
141
138
|
ms.metrics.push({
|
|
@@ -147,18 +144,12 @@ export function withInterceptResolution<TEnv>(
|
|
|
147
144
|
return;
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
// Skip intercept resolution if:
|
|
151
|
-
// 1. No intercept result
|
|
152
|
-
// 2. Already have intercept segments (from cache hit with intercept key)
|
|
153
|
-
// 3. Cache hit with intercept key
|
|
154
147
|
const skipInterceptResolution =
|
|
155
148
|
!ctx.interceptResult ||
|
|
156
149
|
state.interceptSegments.length > 0 ||
|
|
157
150
|
(state.cacheHit && ctx.isIntercept);
|
|
158
151
|
|
|
159
152
|
if (skipInterceptResolution) {
|
|
160
|
-
// For cache hit with intercept, extract intercept segments from cached data for slots
|
|
161
|
-
// and re-resolve loaders for fresh data
|
|
162
153
|
if (ctx.interceptResult && state.cacheHit && ctx.isIntercept) {
|
|
163
154
|
await handleCacheHitIntercept(ctx, state, segments);
|
|
164
155
|
}
|
|
@@ -172,7 +163,6 @@ export function withInterceptResolution<TEnv>(
|
|
|
172
163
|
return;
|
|
173
164
|
}
|
|
174
165
|
|
|
175
|
-
// Resolve intercept segments
|
|
176
166
|
const { resolveInterceptEntry } = getRouterContext<TEnv>();
|
|
177
167
|
|
|
178
168
|
const slotName = ctx.interceptResult!.intercept.slotName;
|
|
@@ -181,7 +171,6 @@ export function withInterceptResolution<TEnv>(
|
|
|
181
171
|
slotName,
|
|
182
172
|
});
|
|
183
173
|
|
|
184
|
-
// Resolve intercept entry (middleware, loaders, handler)
|
|
185
174
|
const Store = ctx.Store;
|
|
186
175
|
const interceptSegments = await Store.run(() =>
|
|
187
176
|
resolveInterceptEntry(
|
|
@@ -203,14 +192,12 @@ export function withInterceptResolution<TEnv>(
|
|
|
203
192
|
),
|
|
204
193
|
);
|
|
205
194
|
|
|
206
|
-
// Update state
|
|
207
195
|
state.interceptSegments = interceptSegments;
|
|
208
196
|
state.slots[slotName] = {
|
|
209
197
|
active: true,
|
|
210
198
|
segments: interceptSegments,
|
|
211
199
|
};
|
|
212
200
|
|
|
213
|
-
// Yield intercept segments
|
|
214
201
|
for (const segment of interceptSegments) {
|
|
215
202
|
yield segment;
|
|
216
203
|
}
|
|
@@ -225,11 +212,6 @@ export function withInterceptResolution<TEnv>(
|
|
|
225
212
|
};
|
|
226
213
|
}
|
|
227
214
|
|
|
228
|
-
/**
|
|
229
|
-
* Handle cache hit with intercept scenario
|
|
230
|
-
*
|
|
231
|
-
* Extract intercept segments from cached data and re-resolve loaders for fresh data.
|
|
232
|
-
*/
|
|
233
215
|
async function handleCacheHitIntercept<TEnv>(
|
|
234
216
|
ctx: MatchContext<TEnv>,
|
|
235
217
|
state: MatchPipelineState,
|
|
@@ -241,14 +223,11 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
241
223
|
|
|
242
224
|
const slotName = ctx.interceptResult.intercept.slotName;
|
|
243
225
|
|
|
244
|
-
// Find intercept segments from cached segments (namespace starts with "intercept:")
|
|
245
226
|
const interceptSegments = segments.filter((s) =>
|
|
246
227
|
s.namespace?.startsWith("intercept:"),
|
|
247
228
|
);
|
|
248
229
|
state.interceptSegments = interceptSegments;
|
|
249
230
|
|
|
250
|
-
// Re-resolve intercept loaders for fresh data on cache hit
|
|
251
|
-
// This keeps cached component/layout but fetches fresh loader data
|
|
252
231
|
if (resolveInterceptLoadersOnly) {
|
|
253
232
|
const Store = ctx.Store;
|
|
254
233
|
const freshLoaderResult = await Store.run(() =>
|
|
@@ -271,7 +250,6 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
271
250
|
),
|
|
272
251
|
);
|
|
273
252
|
|
|
274
|
-
// Update intercept segment's loaderDataPromise with fresh data
|
|
275
253
|
if (freshLoaderResult) {
|
|
276
254
|
const interceptMainSegment = interceptSegments.find(
|
|
277
255
|
(s) => s.type === "parallel" && s.slot,
|
|
@@ -93,12 +93,6 @@ import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
|
93
93
|
import { getRouterContext } from "../router-context.js";
|
|
94
94
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
* Check whether any entry in the tree uses loading() (streaming).
|
|
98
|
-
* Matches the router's streaming semantics in fresh.ts: streaming is
|
|
99
|
-
* enabled when `loading` is defined AND not `false`. `loading: false`
|
|
100
|
-
* explicitly disables streaming; `undefined` means no loading at all.
|
|
101
|
-
*/
|
|
102
96
|
export function treeHasStreaming(entries: EntryData[]): boolean {
|
|
103
97
|
for (const entry of entries) {
|
|
104
98
|
if (
|
|
@@ -130,12 +124,6 @@ export function treeHasStreaming(entries: EntryData[]): boolean {
|
|
|
130
124
|
return false;
|
|
131
125
|
}
|
|
132
126
|
|
|
133
|
-
/**
|
|
134
|
-
* Creates segment resolution middleware
|
|
135
|
-
*
|
|
136
|
-
* Only runs on cache miss (state.cacheHit === false).
|
|
137
|
-
* Uses resolveAllSegmentsWithRevalidation from RouterContext to resolve segments.
|
|
138
|
-
*/
|
|
139
127
|
export function withSegmentResolution<TEnv>(
|
|
140
128
|
ctx: MatchContext<TEnv>,
|
|
141
129
|
state: MatchPipelineState,
|
|
@@ -145,17 +133,12 @@ export function withSegmentResolution<TEnv>(
|
|
|
145
133
|
): AsyncGenerator<ResolvedSegment> {
|
|
146
134
|
const ms = ctx.metricsStore;
|
|
147
135
|
|
|
148
|
-
// IMPORTANT: Always iterate source first to give cache-lookup a chance
|
|
149
|
-
// to run and set state.cacheHit. Without this, cache-lookup never executes!
|
|
150
136
|
for await (const segment of source) {
|
|
151
137
|
yield segment;
|
|
152
138
|
}
|
|
153
139
|
|
|
154
|
-
// Measure own work only (after source iteration completes)
|
|
155
140
|
const ownStart = performance.now();
|
|
156
141
|
|
|
157
|
-
// If cache hit, segments were already yielded by cache lookup
|
|
158
|
-
// (render barrier is resolved on the cache-hit path)
|
|
159
142
|
if (state.cacheHit) {
|
|
160
143
|
if (ms) {
|
|
161
144
|
ms.metrics.push({
|
|
@@ -178,7 +161,6 @@ export function withSegmentResolution<TEnv>(
|
|
|
178
161
|
const Store = ctx.Store;
|
|
179
162
|
|
|
180
163
|
if (ctx.isFullMatch) {
|
|
181
|
-
// Full match (document request) - simple resolution without revalidation
|
|
182
164
|
const segments = await Store.run(() =>
|
|
183
165
|
resolveAllSegments(
|
|
184
166
|
ctx.entries,
|
|
@@ -189,15 +171,12 @@ export function withSegmentResolution<TEnv>(
|
|
|
189
171
|
),
|
|
190
172
|
);
|
|
191
173
|
|
|
192
|
-
// Update state with resolved segments
|
|
193
174
|
state.segments = segments;
|
|
194
175
|
state.matchedIds = segments.map((s: { id: string }) => s.id);
|
|
195
176
|
|
|
196
177
|
if (reqCtx) {
|
|
197
178
|
reqCtx._resolveRenderBarrier(segments);
|
|
198
179
|
}
|
|
199
|
-
|
|
200
|
-
// Yield all resolved segments
|
|
201
180
|
for (const segment of segments) {
|
|
202
181
|
yield segment;
|
|
203
182
|
}
|
|
@@ -214,7 +193,6 @@ export function withSegmentResolution<TEnv>(
|
|
|
214
193
|
ctx.request,
|
|
215
194
|
ctx.prevUrl,
|
|
216
195
|
ctx.url,
|
|
217
|
-
ctx.loaderPromises,
|
|
218
196
|
ctx.actionContext,
|
|
219
197
|
ctx.interceptResult,
|
|
220
198
|
ctx.localRouteName,
|
|
@@ -106,16 +106,6 @@ import {
|
|
|
106
106
|
withSegmentResolution,
|
|
107
107
|
} from "./match-middleware/index.js";
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
* Compose multiple async generator middleware into a single middleware
|
|
111
|
-
*
|
|
112
|
-
* Middleware are applied in reverse order (rightmost runs first, innermost).
|
|
113
|
-
* For the pipeline:
|
|
114
|
-
* compose(A, B, C)(source)
|
|
115
|
-
*
|
|
116
|
-
* The flow is: source -> C -> B -> A -> output
|
|
117
|
-
* Where C is the innermost (runs first on input) and A is outermost (runs last).
|
|
118
|
-
*/
|
|
119
109
|
export function compose<T>(
|
|
120
110
|
...middleware: GeneratorMiddleware<T>[]
|
|
121
111
|
): GeneratorMiddleware<T> {
|
|
@@ -126,54 +116,23 @@ export function compose<T>(
|
|
|
126
116
|
return middleware[0];
|
|
127
117
|
}
|
|
128
118
|
return (source) => {
|
|
129
|
-
// Apply middleware in reverse order (rightmost first)
|
|
130
119
|
return middleware.reduceRight((prev, fn) => fn(prev), source);
|
|
131
120
|
};
|
|
132
121
|
}
|
|
133
122
|
|
|
134
|
-
|
|
135
|
-
* Create an empty async generator (source for pipeline)
|
|
136
|
-
*/
|
|
137
|
-
export async function* empty<T>(): AsyncGenerator<T> {
|
|
138
|
-
// Yields nothing - used as the initial source for the pipeline
|
|
139
|
-
}
|
|
123
|
+
export async function* empty<T>(): AsyncGenerator<T> {}
|
|
140
124
|
|
|
141
|
-
/**
|
|
142
|
-
* Create the match partial pipeline
|
|
143
|
-
*
|
|
144
|
-
* Pipeline order (innermost to outermost):
|
|
145
|
-
* 1. cache-lookup - Check cache first, yield cached segments if hit
|
|
146
|
-
* 2. segment-resolution - Resolve segments if cache miss
|
|
147
|
-
* 3. intercept-resolution - Resolve intercept segments
|
|
148
|
-
* 4. cache-store - Store segments in cache
|
|
149
|
-
* 5. background-revalidation - Trigger SWR if cache was stale
|
|
150
|
-
*
|
|
151
|
-
* Data flow:
|
|
152
|
-
* - empty() produces no segments
|
|
153
|
-
* - cache-lookup either yields cached segments OR passes through to segment-resolution
|
|
154
|
-
* - segment-resolution resolves fresh segments on cache miss
|
|
155
|
-
* - intercept-resolution adds intercept segments
|
|
156
|
-
* - cache-store observes and caches segments
|
|
157
|
-
* - background-revalidation triggers SWR revalidation if needed
|
|
158
|
-
*/
|
|
159
125
|
export function createMatchPartialPipeline<TEnv>(
|
|
160
126
|
ctx: MatchContext<TEnv>,
|
|
161
127
|
state: MatchPipelineState,
|
|
162
128
|
): AsyncGenerator<ResolvedSegment> {
|
|
163
|
-
// Build the middleware chain
|
|
164
129
|
const pipeline = compose<ResolvedSegment>(
|
|
165
|
-
// Outermost - observes segments and triggers background revalidation
|
|
166
130
|
withBackgroundRevalidation(ctx, state),
|
|
167
|
-
// Observes and stores segments in cache
|
|
168
131
|
withCacheStore(ctx, state),
|
|
169
|
-
// Adds intercept segments after main segments
|
|
170
132
|
withInterceptResolution(ctx, state),
|
|
171
|
-
// Resolves segments on cache miss
|
|
172
133
|
withSegmentResolution(ctx, state),
|
|
173
|
-
// Innermost - checks cache first
|
|
174
134
|
withCacheLookup(ctx, state),
|
|
175
135
|
);
|
|
176
136
|
|
|
177
|
-
// Start with empty source - cache lookup or segment resolution will produce segments
|
|
178
137
|
return pipeline(empty());
|
|
179
138
|
}
|
|
@@ -112,9 +112,6 @@ import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
|
112
112
|
import { debugLog } from "./logging.js";
|
|
113
113
|
import { appendMetric } from "./metrics.js";
|
|
114
114
|
|
|
115
|
-
/**
|
|
116
|
-
* Collect all segments from an async generator
|
|
117
|
-
*/
|
|
118
115
|
export async function collectSegments(
|
|
119
116
|
generator: AsyncGenerator<ResolvedSegment>,
|
|
120
117
|
): Promise<ResolvedSegment[]> {
|
|
@@ -138,34 +135,36 @@ export async function collectSegments(
|
|
|
138
135
|
function deduplicateLoaderSegments(
|
|
139
136
|
segments: ResolvedSegment[],
|
|
140
137
|
logPrefix: string,
|
|
141
|
-
): ResolvedSegment[] {
|
|
142
|
-
//
|
|
143
|
-
// and
|
|
138
|
+
): { segments: ResolvedSegment[]; removedIds: Set<string> } {
|
|
139
|
+
// Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
|
|
140
|
+
// namespace, and namespaces of segments that declare loading().
|
|
144
141
|
const originalLoaders = new Set<string>();
|
|
145
|
-
const
|
|
142
|
+
const loaderIdsByNamespace = new Map<string, string[]>();
|
|
143
|
+
const namespacesWithLoading = new Set<string>();
|
|
146
144
|
for (const s of segments) {
|
|
147
|
-
if (s.type === "loader" && s.loaderId
|
|
148
|
-
originalLoaders.add(s.loaderId);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
if (s.type === "loader" && s.loaderId) {
|
|
146
|
+
if (!s._inherited) originalLoaders.add(s.loaderId);
|
|
147
|
+
const ids = loaderIdsByNamespace.get(s.namespace);
|
|
148
|
+
if (ids) ids.push(s.loaderId);
|
|
149
|
+
else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
|
|
150
|
+
} else if (
|
|
151
|
+
s.type !== "loader" &&
|
|
152
|
+
s.loading !== undefined &&
|
|
153
|
+
s.loading !== false
|
|
154
|
+
) {
|
|
155
|
+
namespacesWithLoading.add(s.namespace);
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for (const l of segments) {
|
|
160
|
-
if (l.type === "loader" && l.namespace === s.namespace && l.loaderId) {
|
|
161
|
-
loadersWithLoading.add(l.loaderId);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
158
|
+
|
|
159
|
+
const loadersWithLoading = new Set<string>();
|
|
160
|
+
for (const ns of namespacesWithLoading) {
|
|
161
|
+
for (const id of loaderIdsByNamespace.get(ns) ?? []) {
|
|
162
|
+
loadersWithLoading.add(id);
|
|
164
163
|
}
|
|
165
164
|
}
|
|
166
165
|
|
|
167
166
|
const result: ResolvedSegment[] = [];
|
|
168
|
-
|
|
167
|
+
const removedIds = new Set<string>();
|
|
169
168
|
|
|
170
169
|
for (const s of segments) {
|
|
171
170
|
if (
|
|
@@ -175,22 +174,22 @@ function deduplicateLoaderSegments(
|
|
|
175
174
|
originalLoaders.has(s.loaderId) &&
|
|
176
175
|
!loadersWithLoading.has(s.loaderId)
|
|
177
176
|
) {
|
|
178
|
-
|
|
177
|
+
removedIds.add(s.id);
|
|
179
178
|
continue;
|
|
180
179
|
}
|
|
181
180
|
result.push(s);
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
if (
|
|
185
|
-
debugLog(
|
|
183
|
+
if (removedIds.size > 0) {
|
|
184
|
+
debugLog(
|
|
185
|
+
logPrefix,
|
|
186
|
+
`deduped ${removedIds.size} inherited loader segment(s)`,
|
|
187
|
+
);
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
return result;
|
|
190
|
+
return { segments: result, removedIds };
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
/**
|
|
192
|
-
* Build the final MatchResult from collected segments and context
|
|
193
|
-
*/
|
|
194
193
|
export function buildMatchResult<TEnv>(
|
|
195
194
|
allSegments: ResolvedSegment[],
|
|
196
195
|
ctx: MatchContext<TEnv>,
|
|
@@ -204,11 +203,6 @@ export function buildMatchResult<TEnv>(
|
|
|
204
203
|
let segmentsToRender: ResolvedSegment[];
|
|
205
204
|
|
|
206
205
|
if (ctx.isFullMatch) {
|
|
207
|
-
// Full match (document request) - all segments are rendered
|
|
208
|
-
// Deduplicate by segment ID (defense-in-depth). The primary dedup is in
|
|
209
|
-
// resolveAllSegments, but this guards against any path that bypasses it.
|
|
210
|
-
// include() scopes can produce entries that resolve the same shared layout,
|
|
211
|
-
// and duplicate IDs change the client's React tree depth causing remounts.
|
|
212
206
|
const seen = new Set<string>();
|
|
213
207
|
segmentsToRender = [];
|
|
214
208
|
for (const s of allSegments) {
|
|
@@ -219,24 +213,14 @@ export function buildMatchResult<TEnv>(
|
|
|
219
213
|
}
|
|
220
214
|
allIds = segmentsToRender.map((s) => s.id);
|
|
221
215
|
} else {
|
|
222
|
-
// Partial match (navigation) - filter and handle intercepts
|
|
223
|
-
// When intercepting, tell browser to keep its current segments + add modal
|
|
224
|
-
// This prevents the browser from discarding the current page content
|
|
225
|
-
// If client sent empty segments (HMR recovery), use segment IDs from allSegments
|
|
226
216
|
allIds = ctx.interceptResult
|
|
227
217
|
? ctx.clientSegmentIds.length > 0
|
|
228
218
|
? [...ctx.clientSegmentIds, ...state.interceptSegments.map((s) => s.id)]
|
|
229
|
-
: allSegments.map((s) => s.id)
|
|
219
|
+
: allSegments.map((s) => s.id)
|
|
230
220
|
: [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
|
|
231
221
|
|
|
232
|
-
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
233
222
|
allIds = [...new Set(allIds)];
|
|
234
223
|
|
|
235
|
-
// Filter out null-component segments only when the client already has
|
|
236
|
-
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
237
|
-
// it must be included even with null component — it's structurally required
|
|
238
|
-
// as a parent node for child layouts/parallels to reconcile against.
|
|
239
|
-
// Loader segments are always included as they carry data.
|
|
240
224
|
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
241
225
|
segmentsToRender = allSegments.filter(
|
|
242
226
|
(s) =>
|
|
@@ -244,36 +228,29 @@ export function buildMatchResult<TEnv>(
|
|
|
244
228
|
);
|
|
245
229
|
}
|
|
246
230
|
|
|
247
|
-
const dedupedSegments = deduplicateLoaderSegments(
|
|
231
|
+
const { segments: dedupedSegments, removedIds } = deduplicateLoaderSegments(
|
|
248
232
|
segmentsToRender,
|
|
249
233
|
logPrefix,
|
|
250
234
|
);
|
|
251
235
|
|
|
252
|
-
debugLog(logPrefix, "all segments", {
|
|
253
|
-
segments: allSegments.map((s) => ({
|
|
254
|
-
id: s.id,
|
|
255
|
-
type: s.type,
|
|
256
|
-
hasComponent: s.component !== null,
|
|
257
|
-
})),
|
|
258
|
-
});
|
|
259
|
-
debugLog(logPrefix, "segments to render", {
|
|
260
|
-
segmentIds: dedupedSegments.map((s) => s.id),
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Remove deduped loader IDs from matched so the client doesn't treat
|
|
264
|
-
// them as missing segments and trigger a fallback refetch.
|
|
265
|
-
const removedIds = new Set(
|
|
266
|
-
segmentsToRender
|
|
267
|
-
.filter((s) => !dedupedSegments.includes(s))
|
|
268
|
-
.map((s) => s.id),
|
|
269
|
-
);
|
|
270
236
|
const matchedIds =
|
|
271
237
|
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
272
238
|
|
|
239
|
+
const resolvedIds = ctx.isFullMatch
|
|
240
|
+
? allSegments.map((s) => s.id)
|
|
241
|
+
: allSegments.filter((s) => s._handlerRan).map((s) => s.id);
|
|
242
|
+
|
|
243
|
+
const cleanedSegments = dedupedSegments.map((s) => {
|
|
244
|
+
if (s._handlerRan === undefined) return s;
|
|
245
|
+
const { _handlerRan: _drop, ...rest } = s;
|
|
246
|
+
return rest as ResolvedSegment;
|
|
247
|
+
});
|
|
248
|
+
|
|
273
249
|
return {
|
|
274
|
-
segments:
|
|
250
|
+
segments: cleanedSegments,
|
|
275
251
|
matched: matchedIds,
|
|
276
|
-
diff:
|
|
252
|
+
diff: cleanedSegments.map((s) => s.id),
|
|
253
|
+
resolvedIds,
|
|
277
254
|
params: ctx.matched.params,
|
|
278
255
|
routeName: ctx.routeKey,
|
|
279
256
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
@@ -282,12 +259,6 @@ export function buildMatchResult<TEnv>(
|
|
|
282
259
|
};
|
|
283
260
|
}
|
|
284
261
|
|
|
285
|
-
/**
|
|
286
|
-
* Collect segments from pipeline and build MatchResult
|
|
287
|
-
*
|
|
288
|
-
* This is the main entry point for building the final result after
|
|
289
|
-
* the pipeline has processed all segments.
|
|
290
|
-
*/
|
|
291
262
|
export async function collectMatchResult<TEnv>(
|
|
292
263
|
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
293
264
|
ctx: MatchContext<TEnv>,
|
|
@@ -297,7 +268,6 @@ export async function collectMatchResult<TEnv>(
|
|
|
297
268
|
|
|
298
269
|
const buildStart = performance.now();
|
|
299
270
|
|
|
300
|
-
// Update state with collected segments if not already set
|
|
301
271
|
if (state.segments.length === 0) {
|
|
302
272
|
state.segments = allSegments;
|
|
303
273
|
}
|