@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847
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 +9 -0
- package/README.md +884 -4
- package/dist/bin/rango.js +1531 -212
- package/dist/vite/index.js +3995 -2489
- package/package.json +57 -52
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +85 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/hooks/SKILL.md +328 -70
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +62 -15
- package/skills/loader/SKILL.md +368 -42
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +14 -10
- package/skills/parallel/SKILL.md +137 -1
- package/skills/prerender/SKILL.md +366 -28
- package/skills/rango/SKILL.md +85 -21
- package/skills/response-routes/SKILL.md +136 -83
- package/skills/route/SKILL.md +195 -21
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +240 -102
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +266 -558
- package/src/browser/navigation-client.ts +132 -75
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +303 -309
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +144 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +190 -70
- package/src/browser/react/NavigationProvider.tsx +78 -11
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +29 -70
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +188 -57
- package/src/browser/scroll-restoration.ts +117 -44
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +488 -606
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +116 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +63 -21
- package/src/build/generate-route-types.ts +36 -1038
- package/src/build/index.ts +2 -5
- package/src/build/route-trie.ts +38 -12
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +122 -303
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +84 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +12 -7
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +104 -40
- package/src/index.ts +122 -67
- package/src/internal-debug.ts +9 -3
- package/src/loader.rsc.ts +18 -93
- package/src/loader.ts +26 -9
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +121 -17
- package/src/prerender.ts +325 -20
- package/src/reverse.ts +144 -124
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +959 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1450
- package/src/route-map-builder.ts +87 -133
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +41 -6
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +324 -116
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +179 -133
- package/src/router/logging.ts +112 -6
- package/src/router/manifest.ts +58 -19
- package/src/router/match-api.ts +89 -88
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +86 -89
- package/src/router/match-middleware/cache-lookup.ts +295 -49
- package/src/router/match-middleware/cache-store.ts +56 -13
- package/src/router/match-middleware/intercept-resolution.ts +45 -22
- package/src/router/match-middleware/segment-resolution.ts +20 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +44 -21
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +327 -369
- package/src/router/pattern-matching.ts +169 -31
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +105 -14
- package/src/router/router-context.ts +40 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +677 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1296 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1354
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +96 -29
- package/src/router/types.ts +15 -9
- package/src/router.ts +642 -2366
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +639 -1027
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +38 -11
- package/src/search-params.ts +66 -54
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +237 -54
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +438 -71
- package/src/server.ts +26 -164
- package/src/ssr/index.tsx +101 -31
- package/src/static-handler.ts +22 -4
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +773 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1795
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1323
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -2259
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -104,8 +104,9 @@ import type { ResolvedSegment } from "../../types.js";
|
|
|
104
104
|
import { getRequestContext } from "../../server/request-context.js";
|
|
105
105
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
106
|
import { getRouterContext } from "../router-context.js";
|
|
107
|
+
import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
|
|
108
|
+
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
107
109
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
108
|
-
import { debugLog, debugWarn } from "../logging.js";
|
|
109
110
|
|
|
110
111
|
/**
|
|
111
112
|
* Creates cache store middleware
|
|
@@ -120,7 +121,6 @@ export function withCacheStore<TEnv>(
|
|
|
120
121
|
return async function* (
|
|
121
122
|
source: AsyncGenerator<ResolvedSegment>,
|
|
122
123
|
): AsyncGenerator<ResolvedSegment> {
|
|
123
|
-
const pipelineStart = performance.now();
|
|
124
124
|
const ms = ctx.metricsStore;
|
|
125
125
|
|
|
126
126
|
// Collect all segments while passing them through
|
|
@@ -130,6 +130,9 @@ export function withCacheStore<TEnv>(
|
|
|
130
130
|
yield segment;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// Measure own work only (after source iteration completes)
|
|
134
|
+
const ownStart = performance.now();
|
|
135
|
+
|
|
133
136
|
// Skip caching if:
|
|
134
137
|
// 1. Cache miss but cache scope is disabled
|
|
135
138
|
// 2. This is an action (actions don't cache)
|
|
@@ -142,16 +145,21 @@ export function withCacheStore<TEnv>(
|
|
|
142
145
|
ctx.request.method !== "GET"
|
|
143
146
|
) {
|
|
144
147
|
if (ms) {
|
|
145
|
-
ms.metrics.push({
|
|
148
|
+
ms.metrics.push({
|
|
149
|
+
label: "pipeline:cache-store",
|
|
150
|
+
duration: performance.now() - ownStart,
|
|
151
|
+
startTime: ownStart - ms.requestStart,
|
|
152
|
+
});
|
|
146
153
|
}
|
|
147
154
|
return;
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
const {
|
|
151
158
|
createHandlerContext,
|
|
152
|
-
|
|
159
|
+
setupLoaderAccess,
|
|
153
160
|
resolveAllSegments,
|
|
154
161
|
resolveInterceptEntry,
|
|
162
|
+
createHandleStore,
|
|
155
163
|
} = getRouterContext<TEnv>();
|
|
156
164
|
|
|
157
165
|
// Combine main segments with intercept segments
|
|
@@ -167,6 +175,9 @@ export function withCacheStore<TEnv>(
|
|
|
167
175
|
if (!requestCtx) return;
|
|
168
176
|
|
|
169
177
|
const cacheScope = ctx.cacheScope;
|
|
178
|
+
const reqId = INTERNAL_RANGO_DEBUG
|
|
179
|
+
? getOrCreateRequestId(ctx.request)
|
|
180
|
+
: undefined;
|
|
170
181
|
|
|
171
182
|
// Register onResponse callback to skip caching for non-200 responses
|
|
172
183
|
// Note: error/notFound status codes are set elsewhere (not caching-specific)
|
|
@@ -184,29 +195,35 @@ export function withCacheStore<TEnv>(
|
|
|
184
195
|
// Proactive caching: render all segments fresh in background
|
|
185
196
|
// This ensures cache has complete components for future requests
|
|
186
197
|
requestCtx.waitUntil(async () => {
|
|
198
|
+
const start = performance.now();
|
|
187
199
|
debugLog("cacheStore", "proactive caching started", {
|
|
188
200
|
pathname: ctx.pathname,
|
|
189
201
|
});
|
|
202
|
+
// Swap to a fresh HandleStore so handle.push() calls from
|
|
203
|
+
// proactive resolution are captured (not silenced). The original
|
|
204
|
+
// store's stream is already sent by waitUntil time.
|
|
205
|
+
// cacheRoute reads from requestCtx._handleStore, so this ensures
|
|
206
|
+
// complete handle data (e.g. breadcrumbs) is cached.
|
|
207
|
+
const originalHandleStore = requestCtx._handleStore;
|
|
208
|
+
requestCtx._handleStore = createHandleStore();
|
|
190
209
|
try {
|
|
191
210
|
// Create fresh context for proactive caching
|
|
192
|
-
// This prevents handle data from polluting the response stream
|
|
193
211
|
const proactiveHandlerContext = createHandlerContext(
|
|
194
212
|
ctx.matched.params,
|
|
195
213
|
ctx.request,
|
|
196
214
|
ctx.url.searchParams,
|
|
197
215
|
ctx.pathname,
|
|
198
216
|
ctx.url,
|
|
199
|
-
ctx.
|
|
217
|
+
ctx.env,
|
|
200
218
|
ctx.routeMap,
|
|
201
|
-
ctx.matched.routeKey
|
|
219
|
+
ctx.matched.routeKey,
|
|
220
|
+
ctx.matched.responseType,
|
|
221
|
+
ctx.matched.pt === true,
|
|
202
222
|
);
|
|
203
223
|
const proactiveLoaderPromises = new Map<string, Promise<any>>();
|
|
204
224
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
proactiveHandlerContext,
|
|
208
|
-
proactiveLoaderPromises,
|
|
209
|
-
);
|
|
225
|
+
// Use normal loader access so handle data is captured
|
|
226
|
+
setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
|
|
210
227
|
|
|
211
228
|
// Re-resolve ALL segments without revalidation
|
|
212
229
|
const Store = ctx.Store;
|
|
@@ -239,32 +256,54 @@ export function withCacheStore<TEnv>(
|
|
|
239
256
|
...freshSegments,
|
|
240
257
|
...freshInterceptSegments,
|
|
241
258
|
];
|
|
259
|
+
requestCtx._handleStore.seal();
|
|
242
260
|
await cacheScope.cacheRoute(
|
|
243
261
|
ctx.pathname,
|
|
244
262
|
ctx.matched.params,
|
|
245
263
|
completeSegments,
|
|
246
264
|
ctx.isIntercept,
|
|
247
265
|
);
|
|
266
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
267
|
+
const dur = performance.now() - start;
|
|
268
|
+
console.log(
|
|
269
|
+
`[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
248
272
|
debugLog("cacheStore", "proactive caching complete", {
|
|
249
273
|
pathname: ctx.pathname,
|
|
250
274
|
});
|
|
251
275
|
} catch (error) {
|
|
276
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
277
|
+
const dur = performance.now() - start;
|
|
278
|
+
console.log(
|
|
279
|
+
`[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} FAILED (${dur.toFixed(2)}ms) error=${String(error)}`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
252
282
|
debugWarn("cacheStore", "proactive caching failed", {
|
|
253
283
|
pathname: ctx.pathname,
|
|
254
284
|
error: String(error),
|
|
255
285
|
});
|
|
286
|
+
} finally {
|
|
287
|
+
requestCtx._handleStore = originalHandleStore;
|
|
256
288
|
}
|
|
257
289
|
});
|
|
258
290
|
} else {
|
|
259
291
|
// All segments have components - cache directly
|
|
260
292
|
// Schedule caching in waitUntil since cacheRoute is now async (key resolution)
|
|
261
293
|
requestCtx.waitUntil(async () => {
|
|
294
|
+
const start = performance.now();
|
|
262
295
|
await cacheScope.cacheRoute(
|
|
263
296
|
ctx.pathname,
|
|
264
297
|
ctx.matched.params,
|
|
265
298
|
allSegmentsToCache,
|
|
266
299
|
ctx.isIntercept,
|
|
267
300
|
);
|
|
301
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
302
|
+
const dur = performance.now() - start;
|
|
303
|
+
console.log(
|
|
304
|
+
`[RSC Background][req:${reqId}] Cache store ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${allSegmentsToCache.length}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
268
307
|
});
|
|
269
308
|
}
|
|
270
309
|
|
|
@@ -272,7 +311,11 @@ export function withCacheStore<TEnv>(
|
|
|
272
311
|
});
|
|
273
312
|
|
|
274
313
|
if (ms) {
|
|
275
|
-
ms.metrics.push({
|
|
314
|
+
ms.metrics.push({
|
|
315
|
+
label: "pipeline:cache-store",
|
|
316
|
+
duration: performance.now() - ownStart,
|
|
317
|
+
startTime: ownStart - ms.requestStart,
|
|
318
|
+
});
|
|
276
319
|
}
|
|
277
320
|
};
|
|
278
321
|
}
|
|
@@ -118,12 +118,11 @@ import { debugLog } from "../logging.js";
|
|
|
118
118
|
*/
|
|
119
119
|
export function withInterceptResolution<TEnv>(
|
|
120
120
|
ctx: MatchContext<TEnv>,
|
|
121
|
-
state: MatchPipelineState
|
|
121
|
+
state: MatchPipelineState,
|
|
122
122
|
): GeneratorMiddleware<ResolvedSegment> {
|
|
123
123
|
return async function* (
|
|
124
|
-
source: AsyncGenerator<ResolvedSegment
|
|
124
|
+
source: AsyncGenerator<ResolvedSegment>,
|
|
125
125
|
): AsyncGenerator<ResolvedSegment> {
|
|
126
|
-
const pipelineStart = performance.now();
|
|
127
126
|
const ms = ctx.metricsStore;
|
|
128
127
|
|
|
129
128
|
// First, yield all segments from the source (main segment resolution or cache)
|
|
@@ -133,10 +132,17 @@ export function withInterceptResolution<TEnv>(
|
|
|
133
132
|
yield segment;
|
|
134
133
|
}
|
|
135
134
|
|
|
135
|
+
// Measure own work only (after source iteration completes)
|
|
136
|
+
const ownStart = performance.now();
|
|
137
|
+
|
|
136
138
|
// Skip intercept resolution for full match (document requests don't have intercepts)
|
|
137
139
|
if (ctx.isFullMatch) {
|
|
138
140
|
if (ms) {
|
|
139
|
-
ms.metrics.push({
|
|
141
|
+
ms.metrics.push({
|
|
142
|
+
label: "pipeline:intercept",
|
|
143
|
+
duration: performance.now() - ownStart,
|
|
144
|
+
startTime: ownStart - ms.requestStart,
|
|
145
|
+
});
|
|
140
146
|
}
|
|
141
147
|
return;
|
|
142
148
|
}
|
|
@@ -157,7 +163,11 @@ export function withInterceptResolution<TEnv>(
|
|
|
157
163
|
await handleCacheHitIntercept(ctx, state, segments);
|
|
158
164
|
}
|
|
159
165
|
if (ms) {
|
|
160
|
-
ms.metrics.push({
|
|
166
|
+
ms.metrics.push({
|
|
167
|
+
label: "pipeline:intercept",
|
|
168
|
+
duration: performance.now() - ownStart,
|
|
169
|
+
startTime: ownStart - ms.requestStart,
|
|
170
|
+
});
|
|
161
171
|
}
|
|
162
172
|
return;
|
|
163
173
|
}
|
|
@@ -189,8 +199,8 @@ export function withInterceptResolution<TEnv>(
|
|
|
189
199
|
routeKey: ctx.routeKey,
|
|
190
200
|
actionContext: ctx.actionContext,
|
|
191
201
|
stale: ctx.stale,
|
|
192
|
-
}
|
|
193
|
-
)
|
|
202
|
+
},
|
|
203
|
+
),
|
|
194
204
|
);
|
|
195
205
|
|
|
196
206
|
// Update state
|
|
@@ -206,7 +216,11 @@ export function withInterceptResolution<TEnv>(
|
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
if (ms) {
|
|
209
|
-
ms.metrics.push({
|
|
219
|
+
ms.metrics.push({
|
|
220
|
+
label: "pipeline:intercept",
|
|
221
|
+
duration: performance.now() - ownStart,
|
|
222
|
+
startTime: ownStart - ms.requestStart,
|
|
223
|
+
});
|
|
210
224
|
}
|
|
211
225
|
};
|
|
212
226
|
}
|
|
@@ -219,7 +233,7 @@ export function withInterceptResolution<TEnv>(
|
|
|
219
233
|
async function handleCacheHitIntercept<TEnv>(
|
|
220
234
|
ctx: MatchContext<TEnv>,
|
|
221
235
|
state: MatchPipelineState,
|
|
222
|
-
segments: ResolvedSegment[]
|
|
236
|
+
segments: ResolvedSegment[],
|
|
223
237
|
): Promise<void> {
|
|
224
238
|
if (!ctx.interceptResult) return;
|
|
225
239
|
|
|
@@ -229,7 +243,7 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
229
243
|
|
|
230
244
|
// Find intercept segments from cached segments (namespace starts with "intercept:")
|
|
231
245
|
const interceptSegments = segments.filter((s) =>
|
|
232
|
-
s.namespace?.startsWith("intercept:")
|
|
246
|
+
s.namespace?.startsWith("intercept:"),
|
|
233
247
|
);
|
|
234
248
|
state.interceptSegments = interceptSegments;
|
|
235
249
|
|
|
@@ -253,28 +267,37 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
253
267
|
routeKey: ctx.routeKey,
|
|
254
268
|
actionContext: ctx.actionContext,
|
|
255
269
|
stale: ctx.stale,
|
|
256
|
-
}
|
|
257
|
-
)
|
|
270
|
+
},
|
|
271
|
+
),
|
|
258
272
|
);
|
|
259
273
|
|
|
260
274
|
// Update intercept segment's loaderDataPromise with fresh data
|
|
261
275
|
if (freshLoaderResult) {
|
|
262
276
|
const interceptMainSegment = interceptSegments.find(
|
|
263
|
-
(s) => s.type === "parallel" && s.slot
|
|
277
|
+
(s) => s.type === "parallel" && s.slot,
|
|
264
278
|
);
|
|
265
279
|
if (interceptMainSegment) {
|
|
266
|
-
interceptMainSegment.loaderDataPromise =
|
|
280
|
+
interceptMainSegment.loaderDataPromise =
|
|
281
|
+
freshLoaderResult.loaderDataPromise;
|
|
267
282
|
interceptMainSegment.loaderIds = freshLoaderResult.loaderIds;
|
|
268
|
-
debugLog(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
283
|
+
debugLog(
|
|
284
|
+
"matchPartial.intercept",
|
|
285
|
+
"cache hit with fresh intercept loaders",
|
|
286
|
+
{
|
|
287
|
+
routeName: ctx.localRouteName,
|
|
288
|
+
slotName,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
272
291
|
}
|
|
273
292
|
} else {
|
|
274
|
-
debugLog(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
293
|
+
debugLog(
|
|
294
|
+
"matchPartial.intercept",
|
|
295
|
+
"cache hit without intercept loader revalidation",
|
|
296
|
+
{
|
|
297
|
+
routeName: ctx.localRouteName,
|
|
298
|
+
slotName,
|
|
299
|
+
},
|
|
300
|
+
);
|
|
278
301
|
}
|
|
279
302
|
}
|
|
280
303
|
|
|
@@ -99,12 +99,11 @@ import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
|
99
99
|
*/
|
|
100
100
|
export function withSegmentResolution<TEnv>(
|
|
101
101
|
ctx: MatchContext<TEnv>,
|
|
102
|
-
state: MatchPipelineState
|
|
102
|
+
state: MatchPipelineState,
|
|
103
103
|
): GeneratorMiddleware<ResolvedSegment> {
|
|
104
104
|
return async function* (
|
|
105
|
-
source: AsyncGenerator<ResolvedSegment
|
|
105
|
+
source: AsyncGenerator<ResolvedSegment>,
|
|
106
106
|
): AsyncGenerator<ResolvedSegment> {
|
|
107
|
-
const pipelineStart = performance.now();
|
|
108
107
|
const ms = ctx.metricsStore;
|
|
109
108
|
|
|
110
109
|
// IMPORTANT: Always iterate source first to give cache-lookup a chance
|
|
@@ -113,10 +112,17 @@ export function withSegmentResolution<TEnv>(
|
|
|
113
112
|
yield segment;
|
|
114
113
|
}
|
|
115
114
|
|
|
115
|
+
// Measure own work only (after source iteration completes)
|
|
116
|
+
const ownStart = performance.now();
|
|
117
|
+
|
|
116
118
|
// If cache hit, segments were already yielded by cache lookup
|
|
117
119
|
if (state.cacheHit) {
|
|
118
120
|
if (ms) {
|
|
119
|
-
ms.metrics.push({
|
|
121
|
+
ms.metrics.push({
|
|
122
|
+
label: "pipeline:segment-resolve",
|
|
123
|
+
duration: performance.now() - ownStart,
|
|
124
|
+
startTime: ownStart - ms.requestStart,
|
|
125
|
+
});
|
|
120
126
|
}
|
|
121
127
|
return;
|
|
122
128
|
}
|
|
@@ -134,8 +140,8 @@ export function withSegmentResolution<TEnv>(
|
|
|
134
140
|
ctx.routeKey,
|
|
135
141
|
ctx.matched.params,
|
|
136
142
|
ctx.handlerContext,
|
|
137
|
-
ctx.loaderPromises
|
|
138
|
-
)
|
|
143
|
+
ctx.loaderPromises,
|
|
144
|
+
),
|
|
139
145
|
);
|
|
140
146
|
|
|
141
147
|
// Update state with resolved segments
|
|
@@ -163,8 +169,9 @@ export function withSegmentResolution<TEnv>(
|
|
|
163
169
|
ctx.actionContext,
|
|
164
170
|
ctx.interceptResult,
|
|
165
171
|
ctx.localRouteName,
|
|
166
|
-
ctx.pathname
|
|
167
|
-
|
|
172
|
+
ctx.pathname,
|
|
173
|
+
ctx.stale,
|
|
174
|
+
),
|
|
168
175
|
);
|
|
169
176
|
|
|
170
177
|
// Update state with resolved segments
|
|
@@ -178,7 +185,11 @@ export function withSegmentResolution<TEnv>(
|
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
if (ms) {
|
|
181
|
-
ms.metrics.push({
|
|
188
|
+
ms.metrics.push({
|
|
189
|
+
label: "pipeline:segment-resolve",
|
|
190
|
+
duration: performance.now() - ownStart,
|
|
191
|
+
startTime: ownStart - ms.requestStart,
|
|
192
|
+
});
|
|
182
193
|
}
|
|
183
194
|
};
|
|
184
195
|
}
|
|
@@ -86,19 +86,14 @@
|
|
|
86
86
|
* -> output: cached segments + fresh loader data
|
|
87
87
|
*
|
|
88
88
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* 2. createMatchPartialPipeline (Partial Match)
|
|
98
|
-
* - Used for client-side navigation
|
|
99
|
-
* - Includes revalidation for SWR
|
|
100
|
-
* - Compares with previous params/URL
|
|
101
|
-
* - Supports intercepts (soft navigation modals)
|
|
89
|
+
* PIPELINE VARIANT
|
|
90
|
+
* ================
|
|
91
|
+
*
|
|
92
|
+
* createMatchPartialPipeline handles both full (document) and partial
|
|
93
|
+
* (navigation) requests. The middleware steps adapt based on ctx.isFullMatch:
|
|
94
|
+
* - cache-lookup/store work for both
|
|
95
|
+
* - background-revalidation is a no-op for full matches (no stale state)
|
|
96
|
+
* - intercept-resolution is a no-op for full matches (no previous navigation)
|
|
102
97
|
*/
|
|
103
98
|
import type { ResolvedSegment } from "../types.js";
|
|
104
99
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
@@ -163,7 +158,7 @@ export async function* empty<T>(): AsyncGenerator<T> {
|
|
|
163
158
|
*/
|
|
164
159
|
export function createMatchPartialPipeline<TEnv>(
|
|
165
160
|
ctx: MatchContext<TEnv>,
|
|
166
|
-
state: MatchPipelineState
|
|
161
|
+
state: MatchPipelineState,
|
|
167
162
|
): AsyncGenerator<ResolvedSegment> {
|
|
168
163
|
// Build the middleware chain
|
|
169
164
|
const pipeline = compose<ResolvedSegment>(
|
|
@@ -176,39 +171,9 @@ export function createMatchPartialPipeline<TEnv>(
|
|
|
176
171
|
// Resolves segments on cache miss
|
|
177
172
|
withSegmentResolution(ctx, state),
|
|
178
173
|
// Innermost - checks cache first
|
|
179
|
-
withCacheLookup(ctx, state)
|
|
174
|
+
withCacheLookup(ctx, state),
|
|
180
175
|
);
|
|
181
176
|
|
|
182
177
|
// Start with empty source - cache lookup or segment resolution will produce segments
|
|
183
178
|
return pipeline(empty());
|
|
184
179
|
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Create the full match pipeline (simpler, no revalidation)
|
|
188
|
-
*
|
|
189
|
-
* Used for document requests (initial page load) where we don't need
|
|
190
|
-
* revalidation logic since there's no previous state to compare against.
|
|
191
|
-
*/
|
|
192
|
-
export function createMatchPipeline<TEnv>(
|
|
193
|
-
ctx: MatchContext<TEnv>,
|
|
194
|
-
state: MatchPipelineState
|
|
195
|
-
): AsyncGenerator<ResolvedSegment> {
|
|
196
|
-
// For full match, we only need:
|
|
197
|
-
// 1. Cache lookup
|
|
198
|
-
// 2. Segment resolution (without revalidation)
|
|
199
|
-
// 3. Intercept resolution
|
|
200
|
-
// 4. Cache store
|
|
201
|
-
|
|
202
|
-
// Note: Full match uses different resolution logic (resolveAllSegments instead of
|
|
203
|
-
// resolveAllSegmentsWithRevalidation). This will be handled by the segment resolution
|
|
204
|
-
// middleware checking ctx.isFullMatch or similar flag.
|
|
205
|
-
|
|
206
|
-
const pipeline = compose<ResolvedSegment>(
|
|
207
|
-
withCacheStore(ctx, state),
|
|
208
|
-
withInterceptResolution(ctx, state),
|
|
209
|
-
withSegmentResolution(ctx, state),
|
|
210
|
-
withCacheLookup(ctx, state)
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
return pipeline(empty());
|
|
214
|
-
}
|
|
@@ -67,10 +67,11 @@
|
|
|
67
67
|
* Keep if:
|
|
68
68
|
* - component !== null (needs rendering)
|
|
69
69
|
* - type === "loader" (carries data even with null component)
|
|
70
|
+
* - client doesn't have the segment (structurally required parent node)
|
|
70
71
|
*
|
|
71
72
|
* Skip if:
|
|
72
|
-
* - component === null AND type !== "loader"
|
|
73
|
-
* - (
|
|
73
|
+
* - component === null AND type !== "loader" AND client has it cached
|
|
74
|
+
* - (Revalidation skip — client already has this segment's UI)
|
|
74
75
|
*
|
|
75
76
|
*
|
|
76
77
|
* INTERCEPT HANDLING
|
|
@@ -108,14 +109,14 @@
|
|
|
108
109
|
*/
|
|
109
110
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
111
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
|
-
import { generateServerTiming, logMetrics } from "./metrics.js";
|
|
112
112
|
import { debugLog } from "./logging.js";
|
|
113
|
+
import { appendMetric } from "./metrics.js";
|
|
113
114
|
|
|
114
115
|
/**
|
|
115
116
|
* Collect all segments from an async generator
|
|
116
117
|
*/
|
|
117
118
|
export async function collectSegments(
|
|
118
|
-
generator: AsyncGenerator<ResolvedSegment
|
|
119
|
+
generator: AsyncGenerator<ResolvedSegment>,
|
|
119
120
|
): Promise<ResolvedSegment[]> {
|
|
120
121
|
const segments: ResolvedSegment[] = [];
|
|
121
122
|
for await (const segment of generator) {
|
|
@@ -130,17 +131,30 @@ export async function collectSegments(
|
|
|
130
131
|
export function buildMatchResult<TEnv>(
|
|
131
132
|
allSegments: ResolvedSegment[],
|
|
132
133
|
ctx: MatchContext<TEnv>,
|
|
133
|
-
state: MatchPipelineState
|
|
134
|
+
state: MatchPipelineState,
|
|
134
135
|
): MatchResult {
|
|
135
|
-
const logPrefix = ctx.isFullMatch
|
|
136
|
+
const logPrefix = ctx.isFullMatch
|
|
137
|
+
? "[Router.match]"
|
|
138
|
+
: "[Router.matchPartial]";
|
|
136
139
|
|
|
137
140
|
let allIds: string[];
|
|
138
141
|
let segmentsToRender: ResolvedSegment[];
|
|
139
142
|
|
|
140
143
|
if (ctx.isFullMatch) {
|
|
141
144
|
// Full match (document request) - all segments are rendered
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
// Deduplicate by segment ID (defense-in-depth). The primary dedup is in
|
|
146
|
+
// resolveAllSegments, but this guards against any path that bypasses it.
|
|
147
|
+
// include() scopes can produce entries that resolve the same shared layout,
|
|
148
|
+
// and duplicate IDs change the client's React tree depth causing remounts.
|
|
149
|
+
const seen = new Set<string>();
|
|
150
|
+
segmentsToRender = [];
|
|
151
|
+
for (const s of allSegments) {
|
|
152
|
+
if (!seen.has(s.id)) {
|
|
153
|
+
seen.add(s.id);
|
|
154
|
+
segmentsToRender.push(s);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
allIds = segmentsToRender.map((s) => s.id);
|
|
144
158
|
} else {
|
|
145
159
|
// Partial match (navigation) - filter and handle intercepts
|
|
146
160
|
// When intercepting, tell browser to keep its current segments + add modal
|
|
@@ -152,10 +166,18 @@ export function buildMatchResult<TEnv>(
|
|
|
152
166
|
: allSegments.map((s) => s.id) // Use actual segments, not matchedIds
|
|
153
167
|
: [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
|
|
154
168
|
|
|
155
|
-
//
|
|
156
|
-
|
|
169
|
+
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
170
|
+
allIds = [...new Set(allIds)];
|
|
171
|
+
|
|
172
|
+
// Filter out null-component segments only when the client already has
|
|
173
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
174
|
+
// it must be included even with null component — it's structurally required
|
|
175
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
176
|
+
// Loader segments are always included as they carry data.
|
|
177
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
157
178
|
segmentsToRender = allSegments.filter(
|
|
158
|
-
(s) =>
|
|
179
|
+
(s) =>
|
|
180
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
159
181
|
);
|
|
160
182
|
}
|
|
161
183
|
|
|
@@ -170,20 +192,12 @@ export function buildMatchResult<TEnv>(
|
|
|
170
192
|
segmentIds: segmentsToRender.map((s) => s.id),
|
|
171
193
|
});
|
|
172
194
|
|
|
173
|
-
// Output metrics if enabled
|
|
174
|
-
let serverTiming: string | undefined;
|
|
175
|
-
if (ctx.metricsStore) {
|
|
176
|
-
logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
|
|
177
|
-
serverTiming = generateServerTiming(ctx.metricsStore);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
195
|
return {
|
|
181
196
|
segments: segmentsToRender,
|
|
182
197
|
matched: allIds,
|
|
183
198
|
diff: segmentsToRender.map((s) => s.id),
|
|
184
199
|
params: ctx.matched.params,
|
|
185
200
|
routeName: ctx.routeKey,
|
|
186
|
-
serverTiming,
|
|
187
201
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
188
202
|
routeMiddleware:
|
|
189
203
|
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
@@ -199,14 +213,23 @@ export function buildMatchResult<TEnv>(
|
|
|
199
213
|
export async function collectMatchResult<TEnv>(
|
|
200
214
|
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
201
215
|
ctx: MatchContext<TEnv>,
|
|
202
|
-
state: MatchPipelineState
|
|
216
|
+
state: MatchPipelineState,
|
|
203
217
|
): Promise<MatchResult> {
|
|
204
218
|
const allSegments = await collectSegments(pipeline);
|
|
205
219
|
|
|
220
|
+
const buildStart = performance.now();
|
|
221
|
+
|
|
206
222
|
// Update state with collected segments if not already set
|
|
207
223
|
if (state.segments.length === 0) {
|
|
208
224
|
state.segments = allSegments;
|
|
209
225
|
}
|
|
210
226
|
|
|
211
|
-
|
|
227
|
+
const result = buildMatchResult(allSegments, ctx, state);
|
|
228
|
+
appendMetric(
|
|
229
|
+
ctx.metricsStore,
|
|
230
|
+
"collect-result",
|
|
231
|
+
buildStart,
|
|
232
|
+
performance.now() - buildStart,
|
|
233
|
+
);
|
|
234
|
+
return result;
|
|
212
235
|
}
|