@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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/README.md +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- 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 +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -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/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- 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/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
|
@@ -22,9 +22,21 @@ import {
|
|
|
22
22
|
matchError as _matchError,
|
|
23
23
|
} from "./match-api.js";
|
|
24
24
|
import { previewMatch as _previewMatch } from "./preview-match.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
runWithRouterLogContext,
|
|
27
|
+
withRouterLogScope,
|
|
28
|
+
isRouterDebugEnabled,
|
|
29
|
+
startRevalidationTrace,
|
|
30
|
+
flushRevalidationTrace,
|
|
31
|
+
} from "./logging.js";
|
|
26
32
|
import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
|
|
27
33
|
import type { MiddlewareFn } from "./middleware.js";
|
|
34
|
+
import {
|
|
35
|
+
type TelemetrySink,
|
|
36
|
+
safeEmit,
|
|
37
|
+
resolveSink,
|
|
38
|
+
getRequestId,
|
|
39
|
+
} from "./telemetry.js";
|
|
28
40
|
|
|
29
41
|
export interface MatchHandlerDeps<TEnv = any> {
|
|
30
42
|
buildRouterContext: () => RouterContext<TEnv>;
|
|
@@ -38,6 +50,7 @@ export interface MatchHandlerDeps<TEnv = any> {
|
|
|
38
50
|
selectorContext: InterceptSelectorContext | null,
|
|
39
51
|
isAction: boolean,
|
|
40
52
|
) => { intercept: InterceptEntry; entry: EntryData } | null;
|
|
53
|
+
telemetry?: TelemetrySink;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
export interface MatchHandlers<TEnv = any> {
|
|
@@ -98,6 +111,8 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
98
111
|
defaultErrorBoundary,
|
|
99
112
|
findInterceptForRoute,
|
|
100
113
|
} = deps;
|
|
114
|
+
const hasTelemetry = !!deps.telemetry;
|
|
115
|
+
const telemetry = resolveSink(deps.telemetry);
|
|
101
116
|
|
|
102
117
|
async function createMatchContextForFull(
|
|
103
118
|
request: Request,
|
|
@@ -140,13 +155,43 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
140
155
|
* - background-revalidation: SWR revalidation
|
|
141
156
|
*/
|
|
142
157
|
async function match(request: Request, env: TEnv): Promise<MatchResult> {
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
const requestId = hasTelemetry ? getRequestId(request) : undefined;
|
|
159
|
+
return runWithRouterLogContext({ request, transaction: "match" }, () => {
|
|
160
|
+
const routerCtx = buildRouterContext();
|
|
161
|
+
routerCtx.requestId = requestId;
|
|
162
|
+
return runWithRouterContext(routerCtx, async () =>
|
|
145
163
|
withRouterLogScope("match", async () => {
|
|
164
|
+
const matchStart = performance.now();
|
|
165
|
+
const pathname = new URL(request.url).pathname;
|
|
166
|
+
if (hasTelemetry) {
|
|
167
|
+
safeEmit(telemetry, {
|
|
168
|
+
type: "request.start",
|
|
169
|
+
timestamp: matchStart,
|
|
170
|
+
requestId,
|
|
171
|
+
method: request.method,
|
|
172
|
+
pathname,
|
|
173
|
+
transaction: "match",
|
|
174
|
+
isPartial: false,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
146
178
|
const result = await createMatchContextForFull(request, env);
|
|
147
179
|
|
|
148
180
|
// Handle redirect case
|
|
149
181
|
if ("type" in result && result.type === "redirect") {
|
|
182
|
+
if (hasTelemetry) {
|
|
183
|
+
safeEmit(telemetry, {
|
|
184
|
+
type: "request.end",
|
|
185
|
+
timestamp: performance.now(),
|
|
186
|
+
requestId,
|
|
187
|
+
method: request.method,
|
|
188
|
+
pathname,
|
|
189
|
+
transaction: "match",
|
|
190
|
+
durationMs: performance.now() - matchStart,
|
|
191
|
+
segmentCount: 0,
|
|
192
|
+
cacheHit: false,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
150
195
|
return {
|
|
151
196
|
segments: [],
|
|
152
197
|
matched: [],
|
|
@@ -161,8 +206,47 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
161
206
|
try {
|
|
162
207
|
const state = createPipelineState();
|
|
163
208
|
const pipeline = createMatchPartialPipeline(ctx, state);
|
|
164
|
-
|
|
209
|
+
const matchResult = await collectMatchResult(pipeline, ctx, state);
|
|
210
|
+
if (hasTelemetry) {
|
|
211
|
+
safeEmit(telemetry, {
|
|
212
|
+
type: "cache.decision",
|
|
213
|
+
timestamp: performance.now(),
|
|
214
|
+
requestId,
|
|
215
|
+
pathname,
|
|
216
|
+
routeKey: ctx.routeKey,
|
|
217
|
+
hit: state.cacheHit,
|
|
218
|
+
shouldRevalidate: !!state.shouldRevalidate,
|
|
219
|
+
source: state.cacheSource,
|
|
220
|
+
});
|
|
221
|
+
safeEmit(telemetry, {
|
|
222
|
+
type: "request.end",
|
|
223
|
+
timestamp: performance.now(),
|
|
224
|
+
requestId,
|
|
225
|
+
method: request.method,
|
|
226
|
+
pathname,
|
|
227
|
+
transaction: "match",
|
|
228
|
+
durationMs: performance.now() - matchStart,
|
|
229
|
+
segmentCount: matchResult.segments.length,
|
|
230
|
+
cacheHit: state.cacheHit,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return matchResult;
|
|
165
234
|
} catch (error) {
|
|
235
|
+
if (hasTelemetry) {
|
|
236
|
+
const errorObj =
|
|
237
|
+
error instanceof Error ? error : new Error(String(error));
|
|
238
|
+
safeEmit(telemetry, {
|
|
239
|
+
type: "request.error",
|
|
240
|
+
timestamp: performance.now(),
|
|
241
|
+
requestId,
|
|
242
|
+
method: request.method,
|
|
243
|
+
pathname,
|
|
244
|
+
transaction: "match",
|
|
245
|
+
error: errorObj,
|
|
246
|
+
phase: error instanceof Response ? "redirect" : "routing",
|
|
247
|
+
durationMs: performance.now() - matchStart,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
166
250
|
if (error instanceof Response) throw error;
|
|
167
251
|
// Report unhandled errors during full match pipeline
|
|
168
252
|
callOnError(error, "routing", {
|
|
@@ -175,8 +259,8 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
175
259
|
throw sanitizeError(error);
|
|
176
260
|
}
|
|
177
261
|
}),
|
|
178
|
-
)
|
|
179
|
-
);
|
|
262
|
+
);
|
|
263
|
+
});
|
|
180
264
|
}
|
|
181
265
|
|
|
182
266
|
async function matchError(
|
|
@@ -214,23 +298,112 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
214
298
|
context: TEnv,
|
|
215
299
|
actionContext?: ActionContext,
|
|
216
300
|
): Promise<MatchResult | null> {
|
|
301
|
+
const partialRequestId = hasTelemetry ? getRequestId(request) : undefined;
|
|
217
302
|
return runWithRouterLogContext(
|
|
218
303
|
{ request, transaction: "matchPartial" },
|
|
219
|
-
() =>
|
|
220
|
-
|
|
304
|
+
() => {
|
|
305
|
+
const routerCtx = buildRouterContext();
|
|
306
|
+
routerCtx.requestId = partialRequestId;
|
|
307
|
+
return runWithRouterContext(routerCtx, async () =>
|
|
221
308
|
withRouterLogScope("matchPartial", async () => {
|
|
309
|
+
const matchStart = performance.now();
|
|
310
|
+
const pathname = new URL(request.url).pathname;
|
|
311
|
+
if (hasTelemetry) {
|
|
312
|
+
safeEmit(telemetry, {
|
|
313
|
+
type: "request.start",
|
|
314
|
+
timestamp: matchStart,
|
|
315
|
+
requestId: partialRequestId,
|
|
316
|
+
method: request.method,
|
|
317
|
+
pathname,
|
|
318
|
+
transaction: "matchPartial",
|
|
319
|
+
isPartial: true,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
222
323
|
const ctx = await createMatchContextForPartial(
|
|
223
324
|
request,
|
|
224
325
|
context,
|
|
225
326
|
actionContext,
|
|
226
327
|
);
|
|
227
|
-
if (!ctx)
|
|
328
|
+
if (!ctx) {
|
|
329
|
+
if (hasTelemetry) {
|
|
330
|
+
safeEmit(telemetry, {
|
|
331
|
+
type: "request.end",
|
|
332
|
+
timestamp: performance.now(),
|
|
333
|
+
requestId: partialRequestId,
|
|
334
|
+
method: request.method,
|
|
335
|
+
pathname,
|
|
336
|
+
transaction: "matchPartial",
|
|
337
|
+
durationMs: performance.now() - matchStart,
|
|
338
|
+
segmentCount: 0,
|
|
339
|
+
cacheHit: false,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isRouterDebugEnabled()) {
|
|
346
|
+
startRevalidationTrace({
|
|
347
|
+
method: request.method,
|
|
348
|
+
prevUrl: ctx.prevUrl.href,
|
|
349
|
+
nextUrl: ctx.url.href,
|
|
350
|
+
routeKey: ctx.routeKey,
|
|
351
|
+
isAction: !!actionContext,
|
|
352
|
+
stale: ctx.stale || undefined,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
228
355
|
|
|
229
356
|
try {
|
|
230
357
|
const state = createPipelineState();
|
|
231
358
|
const pipeline = createMatchPartialPipeline(ctx, state);
|
|
232
|
-
|
|
359
|
+
const matchResult = await collectMatchResult(
|
|
360
|
+
pipeline,
|
|
361
|
+
ctx,
|
|
362
|
+
state,
|
|
363
|
+
);
|
|
364
|
+
flushRevalidationTrace();
|
|
365
|
+
if (hasTelemetry) {
|
|
366
|
+
safeEmit(telemetry, {
|
|
367
|
+
type: "cache.decision",
|
|
368
|
+
timestamp: performance.now(),
|
|
369
|
+
requestId: partialRequestId,
|
|
370
|
+
pathname,
|
|
371
|
+
routeKey: ctx.routeKey,
|
|
372
|
+
hit: state.cacheHit,
|
|
373
|
+
shouldRevalidate: !!state.shouldRevalidate,
|
|
374
|
+
source: state.cacheSource,
|
|
375
|
+
});
|
|
376
|
+
safeEmit(telemetry, {
|
|
377
|
+
type: "request.end",
|
|
378
|
+
timestamp: performance.now(),
|
|
379
|
+
requestId: partialRequestId,
|
|
380
|
+
method: request.method,
|
|
381
|
+
pathname,
|
|
382
|
+
transaction: "matchPartial",
|
|
383
|
+
durationMs: performance.now() - matchStart,
|
|
384
|
+
segmentCount: matchResult.segments.length,
|
|
385
|
+
cacheHit: state.cacheHit,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return matchResult;
|
|
233
389
|
} catch (error) {
|
|
390
|
+
flushRevalidationTrace();
|
|
391
|
+
if (hasTelemetry) {
|
|
392
|
+
const errorObj =
|
|
393
|
+
error instanceof Error ? error : new Error(String(error));
|
|
394
|
+
const phase = actionContext ? "action" : "revalidation";
|
|
395
|
+
safeEmit(telemetry, {
|
|
396
|
+
type: "request.error",
|
|
397
|
+
timestamp: performance.now(),
|
|
398
|
+
requestId: partialRequestId,
|
|
399
|
+
method: request.method,
|
|
400
|
+
pathname,
|
|
401
|
+
transaction: "matchPartial",
|
|
402
|
+
error: errorObj,
|
|
403
|
+
phase: error instanceof Response ? "redirect" : phase,
|
|
404
|
+
durationMs: performance.now() - matchStart,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
234
407
|
if (error instanceof Response) throw error;
|
|
235
408
|
// Report unhandled errors during partial match pipeline
|
|
236
409
|
callOnError(error, actionContext ? "action" : "revalidation", {
|
|
@@ -244,7 +417,8 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
244
417
|
throw sanitizeError(error);
|
|
245
418
|
}
|
|
246
419
|
}),
|
|
247
|
-
)
|
|
420
|
+
);
|
|
421
|
+
},
|
|
248
422
|
);
|
|
249
423
|
}
|
|
250
424
|
|
|
@@ -30,23 +30,15 @@
|
|
|
30
30
|
* |
|
|
31
31
|
* v (async, doesn't block response)
|
|
32
32
|
* +---------------------------+
|
|
33
|
-
* | Create fresh
|
|
33
|
+
* | Create fresh context | Fresh handleStore, handlerContext,
|
|
34
|
+
* | (full isolation) | and loaderPromises map
|
|
34
35
|
* +---------------------------+
|
|
35
36
|
* |
|
|
36
37
|
* v
|
|
37
|
-
*
|
|
38
|
-
* |
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* +-----+-----+
|
|
42
|
-
* | |
|
|
43
|
-
* yes no
|
|
44
|
-
* | |
|
|
45
|
-
* v v
|
|
46
|
-
* resolveAll resolveWithRevalidation
|
|
47
|
-
* Segments + resolveIntercepts
|
|
48
|
-
* | |
|
|
49
|
-
* +-----------+
|
|
38
|
+
* +---------------------------+
|
|
39
|
+
* | resolveAllSegments() | Fresh resolution (no revalidation)
|
|
40
|
+
* | + resolveIntercepts() | Ensures complete components
|
|
41
|
+
* +---------------------------+
|
|
50
42
|
* |
|
|
51
43
|
* v
|
|
52
44
|
* +---------------------------+
|
|
@@ -90,27 +82,22 @@
|
|
|
90
82
|
* ISOLATION FROM RESPONSE
|
|
91
83
|
* =======================
|
|
92
84
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
85
|
+
* Background revalidation creates fully isolated context:
|
|
86
|
+
* - Fresh handleStore (prevents polluting the response stream)
|
|
87
|
+
* - Fresh handlerContext + loaderPromises (prevents reusing memoized
|
|
88
|
+
* loader results from the foreground pass)
|
|
89
|
+
* - handleStore is saved/restored in try/finally
|
|
96
90
|
*
|
|
97
|
-
* This
|
|
98
|
-
* - Polluting the current response stream
|
|
99
|
-
* - Causing duplicate data in the client
|
|
100
|
-
* - Creating race conditions
|
|
91
|
+
* This matches the proactive caching pattern in cache-store.ts.
|
|
101
92
|
*
|
|
102
93
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
94
|
+
* FRESH RESOLUTION (NO REVALIDATION)
|
|
95
|
+
* ===================================
|
|
105
96
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* Partial Match (navigation):
|
|
111
|
-
* - resolveAllSegmentsWithRevalidation()
|
|
112
|
-
* - Also resolves intercept segments if applicable
|
|
113
|
-
* - More complex but handles all scenarios
|
|
97
|
+
* Both full and partial requests use resolveAllSegments() (without
|
|
98
|
+
* revalidation logic) to ensure all segments have complete components.
|
|
99
|
+
* Using revalidation-aware resolution would produce null components
|
|
100
|
+
* for skipped segments, which would corrupt the cache entry.
|
|
114
101
|
*/
|
|
115
102
|
import type { ResolvedSegment } from "../../types.js";
|
|
116
103
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
@@ -148,7 +135,8 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
148
135
|
const {
|
|
149
136
|
getRequestContext,
|
|
150
137
|
createHandleStore,
|
|
151
|
-
|
|
138
|
+
createHandlerContext,
|
|
139
|
+
setupLoaderAccess,
|
|
152
140
|
resolveAllSegments,
|
|
153
141
|
resolveInterceptEntry,
|
|
154
142
|
} = getRouterContext<TEnv>();
|
|
@@ -161,72 +149,62 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
161
149
|
pathname: ctx.pathname,
|
|
162
150
|
fullMatch: ctx.isFullMatch,
|
|
163
151
|
});
|
|
164
|
-
try {
|
|
165
|
-
// Create a fresh handleStore for background revalidation
|
|
166
|
-
// to avoid polluting the current response's handle stream
|
|
167
|
-
if (requestCtx) {
|
|
168
|
-
requestCtx._handleStore = createHandleStore();
|
|
169
|
-
}
|
|
170
152
|
|
|
171
|
-
|
|
153
|
+
// Save and replace handleStore to avoid polluting the response stream.
|
|
154
|
+
// Restore in finally (same pattern as proactive caching in cache-store).
|
|
155
|
+
const originalHandleStore = requestCtx._handleStore;
|
|
156
|
+
requestCtx._handleStore = createHandleStore();
|
|
172
157
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
158
|
+
try {
|
|
159
|
+
// Create fresh handler context and loader promises to avoid
|
|
160
|
+
// reusing memoized results from the foreground pass
|
|
161
|
+
const freshHandlerContext = createHandlerContext(
|
|
162
|
+
ctx.matched.params,
|
|
163
|
+
ctx.request,
|
|
164
|
+
ctx.url.searchParams,
|
|
165
|
+
ctx.pathname,
|
|
166
|
+
ctx.url,
|
|
167
|
+
ctx.env,
|
|
168
|
+
ctx.routeMap,
|
|
169
|
+
ctx.matched.routeKey,
|
|
170
|
+
ctx.matched.responseType,
|
|
171
|
+
ctx.matched.pt === true,
|
|
172
|
+
);
|
|
173
|
+
const freshLoaderPromises = new Map<string, Promise<any>>();
|
|
174
|
+
setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
|
|
175
|
+
|
|
176
|
+
// Resolve all segments fresh (without revalidation logic)
|
|
177
|
+
// to ensure complete components for caching
|
|
178
|
+
const freshSegments = await ctx.Store.run(() =>
|
|
179
|
+
resolveAllSegments(
|
|
185
180
|
ctx.entries,
|
|
186
181
|
ctx.routeKey,
|
|
187
182
|
ctx.matched.params,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ctx.prevUrl,
|
|
193
|
-
ctx.url,
|
|
194
|
-
ctx.loaderPromises,
|
|
195
|
-
ctx.actionContext,
|
|
196
|
-
ctx.interceptResult,
|
|
197
|
-
ctx.localRouteName,
|
|
198
|
-
ctx.pathname,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
freshSegments = freshResult.segments;
|
|
183
|
+
freshHandlerContext,
|
|
184
|
+
freshLoaderPromises,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
202
187
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
188
|
+
// Also resolve intercept segments fresh if applicable
|
|
189
|
+
let freshInterceptSegments: ResolvedSegment[] = [];
|
|
190
|
+
if (ctx.interceptResult) {
|
|
191
|
+
freshInterceptSegments = await ctx.Store.run(() =>
|
|
192
|
+
resolveInterceptEntry(
|
|
193
|
+
ctx.interceptResult!.intercept,
|
|
194
|
+
ctx.interceptResult!.entry,
|
|
208
195
|
ctx.matched.params,
|
|
209
|
-
|
|
196
|
+
freshHandlerContext,
|
|
210
197
|
true,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
prevParams: ctx.prevParams,
|
|
214
|
-
request: ctx.request,
|
|
215
|
-
prevUrl: ctx.prevUrl,
|
|
216
|
-
nextUrl: ctx.url,
|
|
217
|
-
routeKey: ctx.routeKey,
|
|
218
|
-
actionContext: ctx.actionContext,
|
|
219
|
-
stale: false,
|
|
220
|
-
},
|
|
221
|
-
);
|
|
222
|
-
freshSegments = [...freshSegments, ...freshInterceptSegments];
|
|
223
|
-
}
|
|
198
|
+
),
|
|
199
|
+
);
|
|
224
200
|
}
|
|
225
201
|
|
|
202
|
+
const completeSegments = [...freshSegments, ...freshInterceptSegments];
|
|
203
|
+
requestCtx._handleStore.seal();
|
|
226
204
|
await cacheScope.cacheRoute(
|
|
227
205
|
ctx.pathname,
|
|
228
206
|
ctx.matched.params,
|
|
229
|
-
|
|
207
|
+
completeSegments,
|
|
230
208
|
ctx.isIntercept,
|
|
231
209
|
);
|
|
232
210
|
debugLog("backgroundRevalidation", "revalidation complete", {
|
|
@@ -237,6 +215,8 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
237
215
|
pathname: ctx.pathname,
|
|
238
216
|
error: String(error),
|
|
239
217
|
});
|
|
218
|
+
} finally {
|
|
219
|
+
requestCtx._handleStore = originalHandleStore;
|
|
240
220
|
}
|
|
241
221
|
});
|
|
242
222
|
};
|
|
@@ -92,6 +92,8 @@
|
|
|
92
92
|
import type { ResolvedSegment } from "../../types.js";
|
|
93
93
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
94
94
|
import { getRouterContext } from "../router-context.js";
|
|
95
|
+
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
96
|
+
import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
|
|
95
97
|
import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
|
|
96
98
|
import type { HandleStore } from "../../server/handle-store.js";
|
|
97
99
|
import {
|
|
@@ -185,6 +187,7 @@ async function* yieldFromStore<TEnv>(
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
state.cacheHit = true;
|
|
190
|
+
state.cacheSource = "prerender";
|
|
188
191
|
state.cachedSegments = segments;
|
|
189
192
|
state.cachedMatchedIds = segments.map((s) => s.id);
|
|
190
193
|
|
|
@@ -306,13 +309,21 @@ export function withCacheLookup<TEnv>(
|
|
|
306
309
|
await ensurePrerenderDeps();
|
|
307
310
|
if (prerenderStoreInstance) {
|
|
308
311
|
const paramHash = _hashParams!(ctx.matched.params);
|
|
312
|
+
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
313
|
+
(entry) =>
|
|
314
|
+
entry.type === "route" &&
|
|
315
|
+
entry.prerenderDef?.options?.passthrough === true,
|
|
316
|
+
);
|
|
309
317
|
|
|
310
318
|
if (ctx.isIntercept) {
|
|
311
319
|
// Intercept navigation: try intercept-specific prerender entry
|
|
312
320
|
const entry = await prerenderStoreInstance.get(
|
|
313
321
|
ctx.matched.routeKey,
|
|
314
322
|
paramHash + "/i",
|
|
315
|
-
{
|
|
323
|
+
{
|
|
324
|
+
pathname: ctx.pathname,
|
|
325
|
+
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
326
|
+
},
|
|
316
327
|
);
|
|
317
328
|
if (entry) {
|
|
318
329
|
yield* yieldFromStore(
|
|
@@ -331,7 +342,10 @@ export function withCacheLookup<TEnv>(
|
|
|
331
342
|
const entry = await prerenderStoreInstance.get(
|
|
332
343
|
ctx.matched.routeKey,
|
|
333
344
|
paramHash,
|
|
334
|
-
{
|
|
345
|
+
{
|
|
346
|
+
pathname: ctx.pathname,
|
|
347
|
+
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
348
|
+
},
|
|
335
349
|
);
|
|
336
350
|
if (entry) {
|
|
337
351
|
yield* yieldFromStore(
|
|
@@ -367,12 +381,20 @@ export function withCacheLookup<TEnv>(
|
|
|
367
381
|
await ensurePrerenderDeps();
|
|
368
382
|
if (prerenderStoreInstance) {
|
|
369
383
|
const paramHash = _hashParams!(ctx.matched.params);
|
|
384
|
+
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
385
|
+
(entry) =>
|
|
386
|
+
entry.type === "route" &&
|
|
387
|
+
entry.prerenderDef?.options?.passthrough === true,
|
|
388
|
+
);
|
|
370
389
|
|
|
371
390
|
if (ctx.isIntercept) {
|
|
372
391
|
const entry = await prerenderStoreInstance.get(
|
|
373
392
|
ctx.matched.routeKey,
|
|
374
393
|
paramHash + "/i",
|
|
375
|
-
{
|
|
394
|
+
{
|
|
395
|
+
pathname: ctx.pathname,
|
|
396
|
+
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
397
|
+
},
|
|
376
398
|
);
|
|
377
399
|
if (entry) {
|
|
378
400
|
yield* yieldFromStore(
|
|
@@ -389,7 +411,10 @@ export function withCacheLookup<TEnv>(
|
|
|
389
411
|
const entry = await prerenderStoreInstance.get(
|
|
390
412
|
ctx.matched.routeKey,
|
|
391
413
|
paramHash,
|
|
392
|
-
{
|
|
414
|
+
{
|
|
415
|
+
pathname: ctx.pathname,
|
|
416
|
+
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
417
|
+
},
|
|
393
418
|
);
|
|
394
419
|
if (entry) {
|
|
395
420
|
yield* yieldFromStore(
|
|
@@ -442,6 +467,7 @@ export function withCacheLookup<TEnv>(
|
|
|
442
467
|
|
|
443
468
|
// Cache HIT
|
|
444
469
|
state.cacheHit = true;
|
|
470
|
+
state.cacheSource = "runtime";
|
|
445
471
|
state.shouldRevalidate = cacheResult.shouldRevalidate;
|
|
446
472
|
state.cachedSegments = cacheResult.segments;
|
|
447
473
|
state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
|
|
@@ -460,6 +486,17 @@ export function withCacheLookup<TEnv>(
|
|
|
460
486
|
for (const segment of cacheResult.segments) {
|
|
461
487
|
// Skip segments client doesn't have - they need their component
|
|
462
488
|
if (!ctx.clientSegmentSet.has(segment.id)) {
|
|
489
|
+
if (isTraceActive()) {
|
|
490
|
+
pushRevalidationTraceEntry({
|
|
491
|
+
segmentId: segment.id,
|
|
492
|
+
segmentType: segment.type,
|
|
493
|
+
belongsToRoute: segment.belongsToRoute ?? false,
|
|
494
|
+
source: "cache-hit",
|
|
495
|
+
defaultShouldRevalidate: true,
|
|
496
|
+
finalShouldRevalidate: true,
|
|
497
|
+
reason: "new-segment",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
463
500
|
yield segment;
|
|
464
501
|
continue;
|
|
465
502
|
}
|
|
@@ -474,6 +511,17 @@ export function withCacheLookup<TEnv>(
|
|
|
474
511
|
const entryInfo = entryRevalidateMap?.get(segment.id);
|
|
475
512
|
if (!entryInfo || entryInfo.revalidate.length === 0) {
|
|
476
513
|
// No revalidation rules, use default behavior (skip if client has)
|
|
514
|
+
if (isTraceActive()) {
|
|
515
|
+
pushRevalidationTraceEntry({
|
|
516
|
+
segmentId: segment.id,
|
|
517
|
+
segmentType: segment.type,
|
|
518
|
+
belongsToRoute: segment.belongsToRoute ?? false,
|
|
519
|
+
source: "cache-hit",
|
|
520
|
+
defaultShouldRevalidate: false,
|
|
521
|
+
finalShouldRevalidate: false,
|
|
522
|
+
reason: "cached-no-rules",
|
|
523
|
+
});
|
|
524
|
+
}
|
|
477
525
|
segment.component = null;
|
|
478
526
|
segment.loading = undefined;
|
|
479
527
|
yield segment;
|
|
@@ -495,8 +543,24 @@ export function withCacheLookup<TEnv>(
|
|
|
495
543
|
routeKey: ctx.routeKey,
|
|
496
544
|
context: ctx.handlerContext,
|
|
497
545
|
actionContext: ctx.actionContext,
|
|
546
|
+
stale: cacheResult.shouldRevalidate || undefined,
|
|
547
|
+
traceSource: "cache-hit",
|
|
498
548
|
});
|
|
499
549
|
|
|
550
|
+
const routerCtx = getRouterContext<TEnv>();
|
|
551
|
+
if (routerCtx.telemetry) {
|
|
552
|
+
const tSink = resolveSink(routerCtx.telemetry);
|
|
553
|
+
safeEmit(tSink, {
|
|
554
|
+
type: "revalidation.decision",
|
|
555
|
+
timestamp: performance.now(),
|
|
556
|
+
requestId: routerCtx.requestId,
|
|
557
|
+
segmentId: segment.id,
|
|
558
|
+
pathname: ctx.pathname,
|
|
559
|
+
routeKey: ctx.routeKey,
|
|
560
|
+
shouldRevalidate,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
500
564
|
if (!shouldRevalidate) {
|
|
501
565
|
// Client has it, no revalidation needed
|
|
502
566
|
segment.component = null;
|
|
@@ -541,6 +605,7 @@ export function withCacheLookup<TEnv>(
|
|
|
541
605
|
ctx.url,
|
|
542
606
|
ctx.routeKey,
|
|
543
607
|
ctx.actionContext,
|
|
608
|
+
cacheResult.shouldRevalidate || undefined,
|
|
544
609
|
),
|
|
545
610
|
);
|
|
546
611
|
|
|
@@ -211,6 +211,7 @@ export function withCacheStore<TEnv>(
|
|
|
211
211
|
ctx.routeMap,
|
|
212
212
|
ctx.matched.routeKey,
|
|
213
213
|
ctx.matched.responseType,
|
|
214
|
+
ctx.matched.pt === true,
|
|
214
215
|
);
|
|
215
216
|
const proactiveLoaderPromises = new Map<string, Promise<any>>();
|
|
216
217
|
|
|
@@ -248,6 +249,7 @@ export function withCacheStore<TEnv>(
|
|
|
248
249
|
...freshSegments,
|
|
249
250
|
...freshInterceptSegments,
|
|
250
251
|
];
|
|
252
|
+
requestCtx._handleStore.seal();
|
|
251
253
|
await cacheScope.cacheRoute(
|
|
252
254
|
ctx.pathname,
|
|
253
255
|
ctx.matched.params,
|