@rangojs/router 0.0.0-experimental.debug-cache-fix → 0.0.0-experimental.dfdb0387
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 +76 -18
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +702 -231
- package/package.json +2 -2
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +16 -3
- package/src/browser/navigation-client.ts +98 -46
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +32 -5
- package/src/browser/prefetch/cache.ts +16 -6
- package/src/browser/prefetch/fetch.ts +52 -6
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +67 -8
- package/src/browser/react/NavigationProvider.tsx +13 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +26 -3
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/segment-reconciler.ts +26 -0
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-scope.ts +12 -14
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +2 -56
- package/src/context-var.ts +72 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +12 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +42 -19
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +79 -23
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +156 -21
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +26 -7
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +82 -4
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +80 -9
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +91 -8
- package/src/router/types.ts +1 -0
- package/src/router.ts +54 -5
- package/src/rsc/handler.ts +472 -372
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -2
- package/src/rsc/rsc-rendering.ts +10 -1
- package/src/rsc/server-action.ts +8 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/server/context.ts +50 -1
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +175 -15
- package/src/ssr/index.tsx +3 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +37 -19
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +1 -1
- package/src/types/segments.ts +1 -0
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/rango.ts +19 -2
- package/src/vite/router-discovery.ts +178 -37
- package/src/vite/utils/prerender-utils.ts +18 -0
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -8,7 +8,13 @@ import type { HandlerContext, InternalHandlerContext } from "../types";
|
|
|
8
8
|
import { _getRequestContext } from "../server/request-context.js";
|
|
9
9
|
import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
|
|
10
10
|
import { parseSearchParams, serializeSearchParams } from "../search-params.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
contextGet,
|
|
13
|
+
contextSet,
|
|
14
|
+
isNonCacheable,
|
|
15
|
+
type ContextSetOptions,
|
|
16
|
+
} from "../context-var.js";
|
|
17
|
+
import { isInsideCacheScope } from "../server/context.js";
|
|
12
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
13
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
14
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
@@ -108,9 +114,9 @@ function createPrerenderPassthroughFn(
|
|
|
108
114
|
}
|
|
109
115
|
if (!isPassthroughRoute) {
|
|
110
116
|
throw new Error(
|
|
111
|
-
"ctx.passthrough() is only available on routes
|
|
112
|
-
"
|
|
113
|
-
"
|
|
117
|
+
"ctx.passthrough() is only available on routes wrapped with " +
|
|
118
|
+
"Passthrough(). Remove the passthrough() call or wrap the " +
|
|
119
|
+
"Prerender definition with Passthrough(prerenderDef, liveHandler).",
|
|
114
120
|
);
|
|
115
121
|
}
|
|
116
122
|
return PRERENDER_PASSTHROUGH;
|
|
@@ -160,9 +166,24 @@ export function createReverseFunction(
|
|
|
160
166
|
: hrefParams;
|
|
161
167
|
|
|
162
168
|
// Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
|
|
169
|
+
// Optional params (:param?) are omitted when not provided
|
|
163
170
|
if (effectiveParams) {
|
|
171
|
+
let hadOmittedOptional = false;
|
|
172
|
+
// First pass: optional params (trailing ?)
|
|
164
173
|
result = result.replace(
|
|
165
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))
|
|
174
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
175
|
+
(_, key) => {
|
|
176
|
+
const value = effectiveParams[key];
|
|
177
|
+
if (value === undefined) {
|
|
178
|
+
hadOmittedOptional = true;
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
return encodeURIComponent(value);
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
// Second pass: required params (no trailing ?)
|
|
185
|
+
result = result.replace(
|
|
186
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
|
|
166
187
|
(_, key) => {
|
|
167
188
|
const value = effectiveParams[key];
|
|
168
189
|
if (value === undefined) {
|
|
@@ -171,6 +192,13 @@ export function createReverseFunction(
|
|
|
171
192
|
return encodeURIComponent(value);
|
|
172
193
|
},
|
|
173
194
|
);
|
|
195
|
+
// Clean up slashes only when an optional param was actually omitted,
|
|
196
|
+
// so intentional trailing-slash patterns like "/blog/" are preserved.
|
|
197
|
+
if (hadOmittedOptional) {
|
|
198
|
+
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
199
|
+
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
200
|
+
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
201
|
+
}
|
|
174
202
|
}
|
|
175
203
|
|
|
176
204
|
// Append search params as query string
|
|
@@ -201,7 +229,7 @@ export function createHandlerContext<TEnv>(
|
|
|
201
229
|
// Get variables from request context - this is the unified context
|
|
202
230
|
// shared between middleware and route handlers
|
|
203
231
|
const requestContext = _getRequestContext();
|
|
204
|
-
const variables: any = requestContext?.
|
|
232
|
+
const variables: any = requestContext?._variables ?? {};
|
|
205
233
|
|
|
206
234
|
// If route has a search schema, parse URLSearchParams into typed object
|
|
207
235
|
const searchSchema = routeName ? getSearchSchema(routeName) : undefined;
|
|
@@ -213,7 +241,7 @@ export function createHandlerContext<TEnv>(
|
|
|
213
241
|
const stubResponse =
|
|
214
242
|
requestContext?.res ?? new Response(null, { status: 200 });
|
|
215
243
|
|
|
216
|
-
// Guard mutating Headers methods so they throw inside "use cache"
|
|
244
|
+
// Guard mutating Headers methods so they throw inside "use cache" or cache() scope.
|
|
217
245
|
// Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
|
|
218
246
|
// is stamped by cache-runtime, not the shared request context.
|
|
219
247
|
const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
|
|
@@ -225,6 +253,13 @@ export function createHandlerContext<TEnv>(
|
|
|
225
253
|
if (MUTATING_HEADERS_METHODS.has(prop as string)) {
|
|
226
254
|
return (...args: any[]) => {
|
|
227
255
|
assertNotInsideCacheExec(ctx, "headers");
|
|
256
|
+
if (isInsideCacheScope()) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`ctx.headers.${String(prop)}() cannot be called inside a cache() boundary. ` +
|
|
259
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
260
|
+
`Move header mutations to a middleware or layout outside the cache() scope.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
228
263
|
return value.apply(target, args);
|
|
229
264
|
};
|
|
230
265
|
}
|
|
@@ -237,6 +272,7 @@ export function createHandlerContext<TEnv>(
|
|
|
237
272
|
ctx = {
|
|
238
273
|
params,
|
|
239
274
|
build: false,
|
|
275
|
+
dev: false,
|
|
240
276
|
request,
|
|
241
277
|
searchParams,
|
|
242
278
|
search: searchSchema ? resolvedSearchParams : {},
|
|
@@ -244,14 +280,24 @@ export function createHandlerContext<TEnv>(
|
|
|
244
280
|
url,
|
|
245
281
|
originalUrl: new URL(request.url),
|
|
246
282
|
env: bindings,
|
|
247
|
-
|
|
248
|
-
get: ((keyOrVar: any) =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
283
|
+
_variables: variables,
|
|
284
|
+
get: ((keyOrVar: any) => {
|
|
285
|
+
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
286
|
+
// Works for both ContextVar tokens and string keys.
|
|
287
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
290
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
291
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return contextGet(variables, keyOrVar);
|
|
295
|
+
}) as HandlerContext<any, TEnv>["get"],
|
|
296
|
+
set: ((keyOrVar: any, value: any, options?: ContextSetOptions) => {
|
|
253
297
|
assertNotInsideCacheExec(ctx, "set");
|
|
254
|
-
|
|
298
|
+
// Write is dumb: store value + non-cacheable metadata.
|
|
299
|
+
// Enforcement happens at read time via ctx.get().
|
|
300
|
+
contextSet(variables, keyOrVar, value, options);
|
|
255
301
|
}) as HandlerContext<any, TEnv>["set"],
|
|
256
302
|
res: stubResponse, // Stub response for setting headers
|
|
257
303
|
headers: guardedHeaders, // Guarded shorthand for res.headers
|
|
@@ -297,7 +343,7 @@ export function createHandlerContext<TEnv>(
|
|
|
297
343
|
*
|
|
298
344
|
* Returns an InternalHandlerContext where params, pathname, url, searchParams,
|
|
299
345
|
* search, reverse, and use(handle) work. Request-time properties
|
|
300
|
-
* (request, env, headers, cookies,
|
|
346
|
+
* (request, env, headers, cookies, get, set, res) throw with a clear error.
|
|
301
347
|
*/
|
|
302
348
|
export function createPrerenderContext<TEnv>(
|
|
303
349
|
params: Record<string, string>,
|
|
@@ -306,6 +352,8 @@ export function createPrerenderContext<TEnv>(
|
|
|
306
352
|
routeName?: string,
|
|
307
353
|
buildVars?: Record<string, any>,
|
|
308
354
|
isPassthroughRoute?: boolean,
|
|
355
|
+
buildEnv?: TEnv,
|
|
356
|
+
devMode?: boolean,
|
|
309
357
|
): InternalHandlerContext<any, TEnv> {
|
|
310
358
|
const syntheticUrl = new URL(`http://prerender${pathname}`);
|
|
311
359
|
const variables = buildVars ?? {};
|
|
@@ -320,6 +368,7 @@ export function createPrerenderContext<TEnv>(
|
|
|
320
368
|
return {
|
|
321
369
|
params,
|
|
322
370
|
build: true,
|
|
371
|
+
dev: devMode ?? false,
|
|
323
372
|
get request(): Request {
|
|
324
373
|
return throwUnavailable("request");
|
|
325
374
|
},
|
|
@@ -329,11 +378,13 @@ export function createPrerenderContext<TEnv>(
|
|
|
329
378
|
url: syntheticUrl,
|
|
330
379
|
originalUrl: syntheticUrl,
|
|
331
380
|
get env(): TEnv {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
381
|
+
if (buildEnv !== undefined) return buildEnv;
|
|
382
|
+
throw new Error(
|
|
383
|
+
"ctx.env is not available during pre-rendering. " +
|
|
384
|
+
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
385
|
+
);
|
|
336
386
|
},
|
|
387
|
+
_variables: variables,
|
|
337
388
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
338
389
|
set: ((keyOrVar: any, value: any) => {
|
|
339
390
|
contextSet(variables, keyOrVar, value);
|
|
@@ -379,6 +430,8 @@ export function createPrerenderContext<TEnv>(
|
|
|
379
430
|
export function createStaticContext<TEnv>(
|
|
380
431
|
routeMap: Record<string, string>,
|
|
381
432
|
routeName?: string,
|
|
433
|
+
buildEnv?: TEnv,
|
|
434
|
+
devMode?: boolean,
|
|
382
435
|
): InternalHandlerContext<any, TEnv> {
|
|
383
436
|
const variables: Record<string, any> = {};
|
|
384
437
|
|
|
@@ -394,6 +447,7 @@ export function createStaticContext<TEnv>(
|
|
|
394
447
|
return throwUnavailable("params");
|
|
395
448
|
},
|
|
396
449
|
build: true,
|
|
450
|
+
dev: devMode ?? false,
|
|
397
451
|
get request(): Request {
|
|
398
452
|
return throwUnavailable("request");
|
|
399
453
|
},
|
|
@@ -413,11 +467,13 @@ export function createStaticContext<TEnv>(
|
|
|
413
467
|
return throwUnavailable("originalUrl");
|
|
414
468
|
},
|
|
415
469
|
get env(): TEnv {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
470
|
+
if (buildEnv !== undefined) return buildEnv;
|
|
471
|
+
throw new Error(
|
|
472
|
+
"ctx.env is not available in Static() handlers. " +
|
|
473
|
+
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
474
|
+
);
|
|
420
475
|
},
|
|
476
|
+
_variables: variables,
|
|
421
477
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
422
478
|
set: ((keyOrVar: any, value: any) => {
|
|
423
479
|
contextSet(variables, keyOrVar, value);
|
|
@@ -11,7 +11,11 @@ import type {
|
|
|
11
11
|
InterceptEntry,
|
|
12
12
|
InterceptSelectorContext,
|
|
13
13
|
} from "../server/context";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
HandlerContext,
|
|
16
|
+
InternalHandlerContext,
|
|
17
|
+
ResolvedSegment,
|
|
18
|
+
} from "../types";
|
|
15
19
|
import { evaluateRevalidation } from "./revalidation.js";
|
|
16
20
|
import { getRequestContext } from "../server/request-context.js";
|
|
17
21
|
import { executeInterceptMiddleware } from "./middleware.js";
|
|
@@ -20,6 +24,7 @@ import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
|
20
24
|
import { handleHandlerResult } from "./segment-resolution.js";
|
|
21
25
|
import type { SegmentResolutionDeps } from "./types.js";
|
|
22
26
|
import { debugLog } from "./logging.js";
|
|
27
|
+
import { runInsideLoaderScope } from "../server/context.js";
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Check if an intercept's when conditions are satisfied.
|
|
@@ -133,7 +138,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
133
138
|
context.request,
|
|
134
139
|
context.env,
|
|
135
140
|
params,
|
|
136
|
-
context
|
|
141
|
+
(context as InternalHandlerContext<any, TEnv>)._variables,
|
|
137
142
|
requestCtx.res,
|
|
138
143
|
createReverseFunction(getGlobalRouteMap()),
|
|
139
144
|
);
|
|
@@ -207,7 +212,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
207
212
|
loaderIds.push(loader.$$id);
|
|
208
213
|
loaderPromises.push(
|
|
209
214
|
deps.wrapLoaderPromise(
|
|
210
|
-
context.use(loader),
|
|
215
|
+
runInsideLoaderScope(() => context.use(loader)),
|
|
211
216
|
parentEntry,
|
|
212
217
|
segmentId,
|
|
213
218
|
context.pathname,
|
|
@@ -374,7 +379,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
374
379
|
loaderIds.push(loader.$$id);
|
|
375
380
|
loaderPromises.push(
|
|
376
381
|
deps.wrapLoaderPromise(
|
|
377
|
-
context.use(loader),
|
|
382
|
+
runInsideLoaderScope(() => context.use(loader)),
|
|
378
383
|
parentEntry,
|
|
379
384
|
segmentId,
|
|
380
385
|
context.pathname,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
import { track } from "../server/context";
|
|
9
9
|
import type { EntryData } from "../server/context";
|
|
10
|
+
import { contextGet } from "../context-var.js";
|
|
10
11
|
import type {
|
|
11
12
|
ResolvedSegment,
|
|
12
13
|
HandlerContext,
|
|
@@ -19,10 +20,11 @@ import type {
|
|
|
19
20
|
ErrorInfo,
|
|
20
21
|
} from "../types";
|
|
21
22
|
import type { LoaderRevalidationResult, ActionContext } from "./types";
|
|
22
|
-
import { isHandle, type Handle } from "../handle.js";
|
|
23
|
-
import
|
|
23
|
+
import { isHandle, collectHandleData, type Handle } from "../handle.js";
|
|
24
|
+
import { buildHandleSnapshot } from "../server/handle-store.js";
|
|
24
25
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
25
26
|
import { _getRequestContext } from "../server/request-context.js";
|
|
27
|
+
import { isInsideLoaderScope } from "../server/context.js";
|
|
26
28
|
import { debugLog } from "./logging.js";
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -241,6 +243,21 @@ function createLoaderExecutor<TEnv>(
|
|
|
241
243
|
pendingLoaders.add(loader.$$id);
|
|
242
244
|
|
|
243
245
|
const currentLoaderId = loader.$$id;
|
|
246
|
+
const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
|
|
247
|
+
|
|
248
|
+
// Capture whether this loader is being started from a DSL loader scope
|
|
249
|
+
// (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
|
|
250
|
+
// inside loader scope. This determines whether rendered() is allowed.
|
|
251
|
+
const isDslLoader = isInsideLoaderScope();
|
|
252
|
+
|
|
253
|
+
let renderedResolved = false;
|
|
254
|
+
let renderedPromise: Promise<void> | null = null;
|
|
255
|
+
|
|
256
|
+
// Loader functions are always fresh (never cached), so they get an
|
|
257
|
+
// unguarded get that bypasses non-cacheable read guards. This applies
|
|
258
|
+
// to ALL loaders — DSL and handler-called — because the loader
|
|
259
|
+
// function itself always re-executes. Also handles nested deps
|
|
260
|
+
// (loaderA → use(loaderB)) since all share this unguarded get.
|
|
244
261
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
245
262
|
params: ctx.params,
|
|
246
263
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
@@ -250,16 +267,86 @@ function createLoaderExecutor<TEnv>(
|
|
|
250
267
|
pathname: ctx.pathname,
|
|
251
268
|
url: ctx.url,
|
|
252
269
|
env: ctx.env,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
use: <
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
get: ((keyOrVar: any) =>
|
|
271
|
+
contextGet(variables, keyOrVar)) as typeof ctx.get,
|
|
272
|
+
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
273
|
+
if (isHandle(item)) {
|
|
274
|
+
if (!renderedResolved) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
|
|
277
|
+
`Handle "${item.$$id}" cannot be read until the render tree has settled.`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
281
|
+
if (!reqCtx) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`ctx.use(handle) failed: request context not available.`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
|
|
287
|
+
const snapshot =
|
|
288
|
+
reqCtx._renderBarrierHandleSnapshot ??
|
|
289
|
+
buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
|
|
290
|
+
return collectHandleData(item, snapshot, segmentOrder);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Loader case
|
|
294
|
+
return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
|
|
295
|
+
}) as LoaderContext["use"],
|
|
260
296
|
method: "GET",
|
|
261
297
|
body: undefined,
|
|
262
298
|
reverse: ctx.reverse as LoaderContext["reverse"],
|
|
299
|
+
rendered: (): Promise<void> => {
|
|
300
|
+
// Guard: only DSL loaders may use rendered()
|
|
301
|
+
if (!isDslLoader) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
304
|
+
`Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Guard: reject streaming trees
|
|
309
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
310
|
+
if (reqCtx?._treeHasStreaming) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`ctx.rendered() is not supported when the matched route tree uses loading(). ` +
|
|
313
|
+
`Streaming handlers may not have settled when rendered() resolves. ` +
|
|
314
|
+
`Remove loading() from the route tree or restructure to avoid rendered().`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (renderedPromise) return renderedPromise;
|
|
319
|
+
|
|
320
|
+
if (!reqCtx) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`ctx.rendered() failed: request context not available.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Bidirectional deadlock check: if a handler already started
|
|
327
|
+
// awaiting this loader, calling rendered() would deadlock.
|
|
328
|
+
if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
|
|
331
|
+
`is already awaiting this loader via ctx.use(). The handler blocks ` +
|
|
332
|
+
`segment resolution, which blocks the barrier, which blocks this loader. ` +
|
|
333
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Register this loader as waiting for the barrier so that
|
|
338
|
+
// setupLoaderAccess can detect deadlocks when a handler
|
|
339
|
+
// tries to await the same loader via ctx.use().
|
|
340
|
+
if (!reqCtx._renderBarrierWaiters) {
|
|
341
|
+
reqCtx._renderBarrierWaiters = new Set();
|
|
342
|
+
}
|
|
343
|
+
reqCtx._renderBarrierWaiters.add(currentLoaderId);
|
|
344
|
+
|
|
345
|
+
renderedPromise = reqCtx._renderBarrier.then(() => {
|
|
346
|
+
renderedResolved = true;
|
|
347
|
+
});
|
|
348
|
+
return renderedPromise;
|
|
349
|
+
},
|
|
263
350
|
};
|
|
264
351
|
|
|
265
352
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
@@ -290,15 +377,22 @@ export function setupLoaderAccess<TEnv>(
|
|
|
290
377
|
ctx: HandlerContext<any, TEnv>,
|
|
291
378
|
loaderPromises: Map<string, Promise<any>>,
|
|
292
379
|
): void {
|
|
293
|
-
// Eagerly capture the HandleStore at setup time
|
|
294
|
-
// In workerd/Cloudflare, dynamic imports and
|
|
295
|
-
// can disrupt AsyncLocalStorage, causing
|
|
296
|
-
// undefined when handlers later call
|
|
297
|
-
//
|
|
298
|
-
const
|
|
380
|
+
// Eagerly capture the request context and HandleStore at setup time
|
|
381
|
+
// (before pipeline async ops). In workerd/Cloudflare, dynamic imports and
|
|
382
|
+
// fetch() in the match pipeline can disrupt AsyncLocalStorage, causing
|
|
383
|
+
// getRequestContext() to return undefined when handlers later call
|
|
384
|
+
// ctx.use(handle). Capturing early ensures references survive ALS disruption.
|
|
385
|
+
const reqCtxRef = _getRequestContext();
|
|
386
|
+
const handleStoreRef = reqCtxRef?._handleStore;
|
|
299
387
|
|
|
300
388
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
301
389
|
|
|
390
|
+
// Track whether we're inside a handle push callback. Loaders started
|
|
391
|
+
// from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
|
|
392
|
+
// block segment resolution, so they must not be registered as handler
|
|
393
|
+
// dependencies for deadlock detection.
|
|
394
|
+
let insideHandlePush = false;
|
|
395
|
+
|
|
302
396
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
303
397
|
if (isHandle(item)) {
|
|
304
398
|
const handle = item;
|
|
@@ -318,16 +412,57 @@ export function setupLoaderAccess<TEnv>(
|
|
|
318
412
|
) => {
|
|
319
413
|
if (!store) return;
|
|
320
414
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
415
|
+
if (typeof dataOrFn === "function") {
|
|
416
|
+
// Mark scope so ctx.use(loader) calls inside the callback
|
|
417
|
+
// are not registered as handler-to-loader deps.
|
|
418
|
+
insideHandlePush = true;
|
|
419
|
+
try {
|
|
420
|
+
const result = (dataOrFn as () => Promise<unknown>)();
|
|
421
|
+
store.push(handle.$$id, segmentId, result);
|
|
422
|
+
} finally {
|
|
423
|
+
insideHandlePush = false;
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
325
427
|
|
|
326
|
-
store.push(handle.$$id, segmentId,
|
|
428
|
+
store.push(handle.$$id, segmentId, dataOrFn);
|
|
327
429
|
};
|
|
328
430
|
}
|
|
329
431
|
|
|
330
|
-
|
|
432
|
+
// Deadlock guard and handler-to-loader dependency tracking.
|
|
433
|
+
// Skip when inside a DSL loader scope (resolveLoaderData also calls
|
|
434
|
+
// ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
|
|
435
|
+
// inside a handle push callback (push callbacks don't block segment
|
|
436
|
+
// resolution so they can't cause rendered() deadlocks).
|
|
437
|
+
const loader = item as LoaderDefinition<any, any>;
|
|
438
|
+
if (!isInsideLoaderScope() && !insideHandlePush) {
|
|
439
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
440
|
+
if (reqCtx) {
|
|
441
|
+
// Direction 1: handler awaits loader that already called rendered()
|
|
442
|
+
if (
|
|
443
|
+
loaderPromises.has(loader.$$id) &&
|
|
444
|
+
reqCtx._renderBarrierWaiters?.has(loader.$$id)
|
|
445
|
+
) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
|
|
448
|
+
`The loader is waiting for segment resolution, but the handler blocks resolution. ` +
|
|
449
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
// Direction 2: track dep so rendered() can detect the deadlock
|
|
453
|
+
// if the loader calls it later. Skip when the barrier has already
|
|
454
|
+
// resolved — no deadlock is possible (rendered() resolves immediately).
|
|
455
|
+
// _renderBarrierSegmentOrder is undefined before resolution, string[]
|
|
456
|
+
// after. This also prevents false positives from handle push callbacks
|
|
457
|
+
// that resume after their first await (post-barrier-resolution).
|
|
458
|
+
if (reqCtx._renderBarrierSegmentOrder === undefined) {
|
|
459
|
+
if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
|
|
460
|
+
reqCtx._handlerLoaderDeps.add(loader.$$id);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return useLoader(loader, null);
|
|
331
466
|
}) as typeof ctx.use;
|
|
332
467
|
}
|
|
333
468
|
|