@rangojs/router 0.0.0-experimental.fa8a383a → 0.0.0-experimental.ffbe1b7f
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/dist/vite/index.js +17 -2
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/loader/SKILL.md +52 -42
- package/skills/parallel/SKILL.md +67 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/partial-update.ts +11 -0
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/NavigationProvider.tsx +5 -3
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/router/handler-context.ts +31 -8
- package/src/router/loader-resolution.ts +7 -1
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +46 -6
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-result.ts +11 -5
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -2
- package/src/router/middleware.ts +2 -2
- package/src/router/router-context.ts +1 -0
- package/src/router/segment-resolution/fresh.ts +37 -14
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +43 -19
- package/src/router/types.ts +1 -0
- package/src/router.ts +1 -0
- package/src/rsc/handler.ts +0 -9
- package/src/server/context.ts +12 -0
- package/src/server/request-context.ts +42 -8
- package/src/types/handler-context.ts +120 -22
- package/src/types/loader-types.ts +4 -4
package/src/router/metrics.ts
CHANGED
|
@@ -15,7 +15,12 @@ function formatMs(value: number): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
|
|
18
|
-
return [...metrics].sort((a, b) =>
|
|
18
|
+
return [...metrics].sort((a, b) => {
|
|
19
|
+
// handler:total always goes last (it wraps everything)
|
|
20
|
+
if (a.label === "handler:total") return 1;
|
|
21
|
+
if (b.label === "handler:total") return -1;
|
|
22
|
+
return a.startTime - b.startTime;
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface Span {
|
|
@@ -27,8 +27,12 @@ type GetVariableFn = {
|
|
|
27
27
|
* Set variable function type
|
|
28
28
|
*/
|
|
29
29
|
type SetVariableFn = {
|
|
30
|
-
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
31
|
-
<K extends keyof DefaultVars>(
|
|
30
|
+
<T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
|
|
31
|
+
<K extends keyof DefaultVars>(
|
|
32
|
+
key: K,
|
|
33
|
+
value: DefaultVars[K],
|
|
34
|
+
options?: { cache?: boolean },
|
|
35
|
+
): void;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
/**
|
package/src/router/middleware.ts
CHANGED
|
@@ -204,8 +204,8 @@ export function createMiddlewareContext<TEnv>(
|
|
|
204
204
|
get: ((keyOrVar: any) =>
|
|
205
205
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
206
206
|
|
|
207
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
208
|
-
contextSet(variables, keyOrVar, value);
|
|
207
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
208
|
+
contextSet(variables, keyOrVar, value, options);
|
|
209
209
|
}) as MiddlewareContext<TEnv>["set"],
|
|
210
210
|
|
|
211
211
|
var: variables as MiddlewareContext<TEnv>["var"],
|
|
@@ -210,6 +210,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
210
210
|
params: Record<string, string>,
|
|
211
211
|
handlerContext: HandlerContext<any, TEnv>,
|
|
212
212
|
loaderPromises: Map<string, Promise<any>>,
|
|
213
|
+
options?: { skipLoaders?: boolean },
|
|
213
214
|
) => Promise<ResolvedSegment[]>;
|
|
214
215
|
|
|
215
216
|
// Generator-based simple resolution
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "./helpers.js";
|
|
31
31
|
import { getRouterContext } from "../router-context.js";
|
|
32
32
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
33
|
-
import { track } from "../../server/context.js";
|
|
33
|
+
import { track, RSCRouterContext } from "../../server/context.js";
|
|
34
34
|
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
// Streamed handler telemetry
|
|
@@ -100,9 +100,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
100
100
|
|
|
101
101
|
if (!loadingDisabled) {
|
|
102
102
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
103
|
-
|
|
104
|
-
// RSC/SSR stream consumption, after the perf timeline is logged.
|
|
105
|
-
return loaderEntries.map((loaderEntry, i) => {
|
|
103
|
+
const segments = loaderEntries.map((loaderEntry, i) => {
|
|
106
104
|
const { loader } = loaderEntry;
|
|
107
105
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
108
106
|
return {
|
|
@@ -122,11 +120,12 @@ export async function resolveLoaders<TEnv>(
|
|
|
122
120
|
belongsToRoute,
|
|
123
121
|
};
|
|
124
122
|
});
|
|
123
|
+
|
|
124
|
+
return segments;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
128
128
|
// settled promises so handlers don't stream loading placeholders.
|
|
129
|
-
// We can measure actual execution time here since we await all loaders.
|
|
130
129
|
const pendingLoaderData = loaderEntries.map((loaderEntry) => {
|
|
131
130
|
const start = performance.now();
|
|
132
131
|
const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
|
|
@@ -344,7 +343,7 @@ export async function resolveSegment<TEnv>(
|
|
|
344
343
|
namespace: entry.id,
|
|
345
344
|
type: "route",
|
|
346
345
|
index: 0,
|
|
347
|
-
component,
|
|
346
|
+
component: component ?? null,
|
|
348
347
|
loading: entry.loading === false ? null : entry.loading,
|
|
349
348
|
transition: entry.transition,
|
|
350
349
|
params,
|
|
@@ -580,6 +579,13 @@ export async function resolveAllSegments<TEnv>(
|
|
|
580
579
|
} catch {}
|
|
581
580
|
|
|
582
581
|
for (const entry of entries) {
|
|
582
|
+
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
583
|
+
// can guard non-cacheable variable reads. Also guards response-level
|
|
584
|
+
// side effects (headers.set). Persists for all descendant entries.
|
|
585
|
+
if (entry.type === "cache") {
|
|
586
|
+
const store = RSCRouterContext.getStore();
|
|
587
|
+
if (store) store.insideCacheScope = true;
|
|
588
|
+
}
|
|
583
589
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
584
590
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
585
591
|
entry,
|
|
@@ -624,20 +630,37 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
624
630
|
deps: SegmentResolutionDeps<TEnv>,
|
|
625
631
|
): Promise<ResolvedSegment[]> {
|
|
626
632
|
const loaderSegments: ResolvedSegment[] = [];
|
|
633
|
+
const seenIds = new Set<string>();
|
|
627
634
|
|
|
628
635
|
async function collectEntryLoaders(
|
|
629
636
|
entry: EntryData,
|
|
630
637
|
belongsToRoute: boolean,
|
|
631
638
|
shortCodeOverride?: string,
|
|
632
639
|
): Promise<void> {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
640
|
+
// Skip if all loaders from this entry have already been resolved
|
|
641
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
642
|
+
const entryLoaders = entry.loader ?? [];
|
|
643
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
644
|
+
const allAlreadySeen =
|
|
645
|
+
entryLoaders.length > 0 &&
|
|
646
|
+
entryLoaders.every((le, i) =>
|
|
647
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
648
|
+
);
|
|
649
|
+
if (!allAlreadySeen) {
|
|
650
|
+
const segments = await resolveLoaders(
|
|
651
|
+
entry,
|
|
652
|
+
context,
|
|
653
|
+
belongsToRoute,
|
|
654
|
+
deps,
|
|
655
|
+
shortCodeOverride,
|
|
656
|
+
);
|
|
657
|
+
for (const seg of segments) {
|
|
658
|
+
if (!seenIds.has(seg.id)) {
|
|
659
|
+
seenIds.add(seg.id);
|
|
660
|
+
loaderSegments.push(seg);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
641
664
|
|
|
642
665
|
const seenParallelEntryIds = new Set<string>();
|
|
643
666
|
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Error boundary segment creation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import { createElement, type ReactNode } from "react";
|
|
12
12
|
import { DataNotFoundError } from "../../errors";
|
|
13
13
|
import {
|
|
14
14
|
createErrorInfo,
|
|
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
|
|
|
180
180
|
|
|
181
181
|
if (error instanceof DataNotFoundError) {
|
|
182
182
|
const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
|
|
183
|
+
// Fall back to router's notFound component, then a plain default
|
|
184
|
+
const notFoundOption = deps.notFoundComponent;
|
|
185
|
+
const defaultFallback =
|
|
186
|
+
typeof notFoundOption === "function"
|
|
187
|
+
? notFoundOption({ pathname: pathname ?? "" })
|
|
188
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
189
|
+
const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
+
const notFoundInfo = createNotFoundInfo(
|
|
192
|
+
error,
|
|
193
|
+
entry.shortCode,
|
|
194
|
+
entry.type,
|
|
195
|
+
pathname,
|
|
196
|
+
);
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
reportError(true, {
|
|
199
|
+
notFound: true,
|
|
200
|
+
message: notFoundInfo.message,
|
|
201
|
+
});
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
debugLog("segment", "notFound boundary handled error", {
|
|
204
|
+
segmentId: entry.shortCode,
|
|
205
|
+
message: notFoundInfo.message,
|
|
206
|
+
});
|
|
201
207
|
|
|
202
|
-
|
|
208
|
+
setResponseStatus(404);
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
210
|
+
return createNotFoundSegment(
|
|
211
|
+
notFoundInfo,
|
|
212
|
+
effectiveNotFoundFallback,
|
|
213
|
+
entry,
|
|
214
|
+
params,
|
|
215
|
+
);
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
const fallback = deps.findNearestErrorBoundary(entry);
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import { getRouterContext } from "../router-context.js";
|
|
43
43
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
44
44
|
import { track } from "../../server/context.js";
|
|
45
|
+
import { RSCRouterContext } from "../../server/context.js";
|
|
45
46
|
|
|
46
47
|
// ---------------------------------------------------------------------------
|
|
47
48
|
// Telemetry helpers
|
|
@@ -262,29 +263,46 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
262
263
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
263
264
|
const allLoaderSegments: ResolvedSegment[] = [];
|
|
264
265
|
const allMatchedIds: string[] = [];
|
|
266
|
+
const seenIds = new Set<string>();
|
|
265
267
|
|
|
266
268
|
async function collectEntryLoaders(
|
|
267
269
|
entry: EntryData,
|
|
268
270
|
belongsToRoute: boolean,
|
|
269
271
|
shortCodeOverride?: string,
|
|
270
272
|
): Promise<void> {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
273
|
+
// Skip if all loaders from this entry have already been resolved
|
|
274
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
275
|
+
const loaderEntries = entry.loader ?? [];
|
|
276
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
277
|
+
const allAlreadySeen =
|
|
278
|
+
loaderEntries.length > 0 &&
|
|
279
|
+
loaderEntries.every((le, i) =>
|
|
280
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
281
|
+
);
|
|
282
|
+
if (!allAlreadySeen) {
|
|
283
|
+
const { segments, matchedIds } = await resolveLoadersWithRevalidation(
|
|
284
|
+
entry,
|
|
285
|
+
context,
|
|
286
|
+
belongsToRoute,
|
|
287
|
+
clientSegmentIds,
|
|
288
|
+
prevParams,
|
|
289
|
+
request,
|
|
290
|
+
prevUrl,
|
|
291
|
+
nextUrl,
|
|
292
|
+
routeKey,
|
|
293
|
+
deps,
|
|
294
|
+
actionContext,
|
|
295
|
+
shortCodeOverride,
|
|
296
|
+
stale,
|
|
297
|
+
);
|
|
298
|
+
for (const seg of segments) {
|
|
299
|
+
if (!seenIds.has(seg.id)) {
|
|
300
|
+
seenIds.add(seg.id);
|
|
301
|
+
allLoaderSegments.push(seg);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
allMatchedIds.push(...matchedIds);
|
|
305
|
+
}
|
|
288
306
|
|
|
289
307
|
const seenParallelEntryIds = new Set<string>();
|
|
290
308
|
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
@@ -705,10 +723,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
705
723
|
() => null,
|
|
706
724
|
);
|
|
707
725
|
|
|
726
|
+
// Normalize void handlers (undefined) to null so the reconciler's
|
|
727
|
+
// component === null checks work consistently for both void and explicit null.
|
|
708
728
|
const resolvedComponent =
|
|
709
729
|
component && typeof component === "object" && "content" in component
|
|
710
|
-
? (component as { content: ReactNode }).content
|
|
711
|
-
: component;
|
|
730
|
+
? ((component as { content: ReactNode }).content ?? null)
|
|
731
|
+
: (component ?? null);
|
|
712
732
|
|
|
713
733
|
const segment: ResolvedSegment = {
|
|
714
734
|
id: entry.shortCode,
|
|
@@ -1229,6 +1249,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1229
1249
|
}
|
|
1230
1250
|
|
|
1231
1251
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1252
|
+
if (entry.type === "cache") {
|
|
1253
|
+
const store = RSCRouterContext.getStore();
|
|
1254
|
+
if (store) store.insideCacheScope = true;
|
|
1255
|
+
}
|
|
1232
1256
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
1233
1257
|
const resolved = await resolveWithErrorBoundary(
|
|
1234
1258
|
nonParallelEntry,
|
package/src/router/types.ts
CHANGED
|
@@ -96,6 +96,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
96
96
|
findNearestNotFoundBoundary: (
|
|
97
97
|
entry: EntryData | null,
|
|
98
98
|
) => ReactNode | NotFoundBoundaryHandler | null;
|
|
99
|
+
notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
|
|
99
100
|
callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
|
|
100
101
|
}
|
|
101
102
|
|
package/src/router.ts
CHANGED
package/src/rsc/handler.ts
CHANGED
|
@@ -490,7 +490,6 @@ export function createRSCHandler<
|
|
|
490
490
|
// has completed so :post spans are captured in the timeline.
|
|
491
491
|
// Handler timing parts are always emitted (even without debug metrics)
|
|
492
492
|
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
493
|
-
const finalizeStart = performance.now();
|
|
494
493
|
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
495
494
|
// Preserve any existing Server-Timing set by response routes or middleware
|
|
496
495
|
const existingTiming = response.headers.get("Server-Timing");
|
|
@@ -507,14 +506,6 @@ export function createRSCHandler<
|
|
|
507
506
|
const totalStart = earlyMetricsStore
|
|
508
507
|
? handlerStart
|
|
509
508
|
: metricsStore.requestStart;
|
|
510
|
-
// response-finalize measures the gap between render completion and
|
|
511
|
-
// handler return: header assembly, onResponse callbacks, etc.
|
|
512
|
-
appendMetric(
|
|
513
|
-
metricsStore,
|
|
514
|
-
"response-finalize",
|
|
515
|
-
finalizeStart,
|
|
516
|
-
performance.now() - finalizeStart,
|
|
517
|
-
);
|
|
518
509
|
appendMetric(
|
|
519
510
|
metricsStore,
|
|
520
511
|
"handler:total",
|
package/src/server/context.ts
CHANGED
|
@@ -273,6 +273,9 @@ interface HelperContext {
|
|
|
273
273
|
string,
|
|
274
274
|
import("../cache/profile-registry.js").CacheProfile
|
|
275
275
|
>;
|
|
276
|
+
/** True when resolving handlers inside a cache() DSL boundary.
|
|
277
|
+
* Read by ctx.get() to guard non-cacheable variable reads. */
|
|
278
|
+
insideCacheScope?: boolean;
|
|
276
279
|
}
|
|
277
280
|
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
278
281
|
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
@@ -666,3 +669,12 @@ export function track(label: string, depth?: number): () => void {
|
|
|
666
669
|
});
|
|
667
670
|
};
|
|
668
671
|
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Check if the current execution is inside a cache() DSL boundary.
|
|
675
|
+
* Returns false inside loader execution — loaders are always fresh
|
|
676
|
+
* (never cached), so non-cacheable reads are safe.
|
|
677
|
+
*/
|
|
678
|
+
export function isInsideCacheScope(): boolean {
|
|
679
|
+
return RSCRouterContext.getStore()?.insideCacheScope === true;
|
|
680
|
+
}
|
|
@@ -20,7 +20,12 @@ import type {
|
|
|
20
20
|
DefaultRouteName,
|
|
21
21
|
} from "../types/global-namespace.js";
|
|
22
22
|
import type { Handle } from "../handle.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
type ContextVar,
|
|
25
|
+
contextGet,
|
|
26
|
+
contextSet,
|
|
27
|
+
isNonCacheable,
|
|
28
|
+
} from "../context-var.js";
|
|
24
29
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
25
30
|
import { isHandle } from "../handle.js";
|
|
26
31
|
import { track, type MetricsStore } from "./context.js";
|
|
@@ -30,6 +35,7 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
|
30
35
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
31
36
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
37
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
38
|
+
import { isInsideCacheScope } from "./context.js";
|
|
33
39
|
import {
|
|
34
40
|
createReverseFunction,
|
|
35
41
|
stripInternalParams,
|
|
@@ -72,8 +78,12 @@ export interface RequestContext<
|
|
|
72
78
|
};
|
|
73
79
|
/** Set a variable (shared with middleware and handlers) */
|
|
74
80
|
set: {
|
|
75
|
-
<T>(
|
|
76
|
-
|
|
81
|
+
<T>(
|
|
82
|
+
contextVar: ContextVar<T>,
|
|
83
|
+
value: T,
|
|
84
|
+
options?: { cache?: boolean },
|
|
85
|
+
): void;
|
|
86
|
+
<K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
|
|
77
87
|
};
|
|
78
88
|
/**
|
|
79
89
|
* Route params (populated after route matching)
|
|
@@ -506,6 +516,18 @@ export function createRequestContext<TEnv>(
|
|
|
506
516
|
responseCookieCache = null;
|
|
507
517
|
};
|
|
508
518
|
|
|
519
|
+
// Guard: throw if a response-level side effect is called inside a cache() scope.
|
|
520
|
+
// Uses ALS to detect the scope (set during segment resolution).
|
|
521
|
+
function assertNotInsideCacheScopeALS(methodName: string): void {
|
|
522
|
+
if (isInsideCacheScope()) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`ctx.${methodName}() cannot be called inside a cache() boundary. ` +
|
|
525
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
526
|
+
`Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
509
531
|
// Effective cookie read: response stub Set-Cookie wins, then original header.
|
|
510
532
|
// The stub IS the source of truth for same-request mutations.
|
|
511
533
|
const effectiveCookie = (name: string): string | undefined => {
|
|
@@ -570,11 +592,19 @@ export function createRequestContext<TEnv>(
|
|
|
570
592
|
pathname: url.pathname,
|
|
571
593
|
searchParams: cleanUrl.searchParams,
|
|
572
594
|
var: variables,
|
|
573
|
-
get: ((keyOrVar: any) =>
|
|
574
|
-
|
|
575
|
-
|
|
595
|
+
get: ((keyOrVar: any) => {
|
|
596
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
599
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
600
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return contextGet(variables, keyOrVar);
|
|
604
|
+
}) as RequestContext<TEnv>["get"],
|
|
605
|
+
set: ((keyOrVar: any, value: any, options?: any) => {
|
|
576
606
|
assertNotInsideCacheExec(ctx, "set");
|
|
577
|
-
contextSet(variables, keyOrVar, value);
|
|
607
|
+
contextSet(variables, keyOrVar, value, options);
|
|
578
608
|
}) as RequestContext<TEnv>["set"],
|
|
579
609
|
params: {} as Record<string, string>,
|
|
580
610
|
|
|
@@ -612,6 +642,7 @@ export function createRequestContext<TEnv>(
|
|
|
612
642
|
|
|
613
643
|
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
614
644
|
assertNotInsideCacheExec(ctx, "setCookie");
|
|
645
|
+
assertNotInsideCacheScopeALS("setCookie");
|
|
615
646
|
stubResponse.headers.append(
|
|
616
647
|
"Set-Cookie",
|
|
617
648
|
serializeCookieValue(name, value, options),
|
|
@@ -624,6 +655,7 @@ export function createRequestContext<TEnv>(
|
|
|
624
655
|
options?: Pick<CookieOptions, "domain" | "path">,
|
|
625
656
|
): void {
|
|
626
657
|
assertNotInsideCacheExec(ctx, "deleteCookie");
|
|
658
|
+
assertNotInsideCacheScopeALS("deleteCookie");
|
|
627
659
|
stubResponse.headers.append(
|
|
628
660
|
"Set-Cookie",
|
|
629
661
|
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
@@ -633,11 +665,13 @@ export function createRequestContext<TEnv>(
|
|
|
633
665
|
|
|
634
666
|
header(name: string, value: string): void {
|
|
635
667
|
assertNotInsideCacheExec(ctx, "header");
|
|
668
|
+
assertNotInsideCacheScopeALS("header");
|
|
636
669
|
stubResponse.headers.set(name, value);
|
|
637
670
|
},
|
|
638
671
|
|
|
639
672
|
setStatus(status: number): void {
|
|
640
673
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
674
|
+
assertNotInsideCacheScopeALS("setStatus");
|
|
641
675
|
stubResponse = new Response(null, {
|
|
642
676
|
status,
|
|
643
677
|
headers: stubResponse.headers,
|
|
@@ -676,6 +710,7 @@ export function createRequestContext<TEnv>(
|
|
|
676
710
|
|
|
677
711
|
onResponse(callback: (response: Response) => Response): void {
|
|
678
712
|
assertNotInsideCacheExec(ctx, "onResponse");
|
|
713
|
+
assertNotInsideCacheScopeALS("onResponse");
|
|
679
714
|
this._onResponseCallbacks.push(callback);
|
|
680
715
|
},
|
|
681
716
|
|
|
@@ -906,7 +941,6 @@ export function createUseFunction<TEnv>(
|
|
|
906
941
|
),
|
|
907
942
|
};
|
|
908
943
|
|
|
909
|
-
// Start loader execution with tracking
|
|
910
944
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
911
945
|
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
912
946
|
doneLoader();
|