@rangojs/router 0.0.0-experimental.48 → 0.0.0-experimental.4ffa0f9b

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.
Files changed (108) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +421 -121
  4. package/package.json +2 -2
  5. package/skills/cache-guide/SKILL.md +32 -0
  6. package/skills/caching/SKILL.md +8 -0
  7. package/skills/links/SKILL.md +3 -1
  8. package/skills/loader/SKILL.md +53 -43
  9. package/skills/middleware/SKILL.md +2 -0
  10. package/skills/parallel/SKILL.md +67 -0
  11. package/skills/prerender/SKILL.md +110 -68
  12. package/skills/route/SKILL.md +31 -0
  13. package/skills/router-setup/SKILL.md +87 -2
  14. package/skills/typesafety/SKILL.md +10 -0
  15. package/src/__internal.ts +1 -1
  16. package/src/browser/app-version.ts +14 -0
  17. package/src/browser/navigation-bridge.ts +16 -3
  18. package/src/browser/navigation-client.ts +64 -40
  19. package/src/browser/navigation-store.ts +43 -8
  20. package/src/browser/partial-update.ts +27 -5
  21. package/src/browser/prefetch/fetch.ts +8 -2
  22. package/src/browser/prefetch/queue.ts +61 -29
  23. package/src/browser/prefetch/resource-ready.ts +77 -0
  24. package/src/browser/react/Link.tsx +44 -8
  25. package/src/browser/react/NavigationProvider.tsx +13 -4
  26. package/src/browser/react/context.ts +7 -2
  27. package/src/browser/react/use-handle.ts +9 -58
  28. package/src/browser/react/use-router.ts +21 -8
  29. package/src/browser/rsc-router.tsx +26 -3
  30. package/src/browser/scroll-restoration.ts +10 -8
  31. package/src/browser/server-action-bridge.ts +8 -6
  32. package/src/browser/types.ts +27 -5
  33. package/src/build/generate-manifest.ts +6 -6
  34. package/src/build/generate-route-types.ts +3 -0
  35. package/src/build/route-types/include-resolution.ts +8 -1
  36. package/src/build/route-types/router-processing.ts +211 -72
  37. package/src/build/route-types/scan-filter.ts +8 -1
  38. package/src/cache/cache-scope.ts +46 -5
  39. package/src/cache/taint.ts +55 -0
  40. package/src/client.tsx +2 -56
  41. package/src/context-var.ts +72 -2
  42. package/src/handle.ts +40 -0
  43. package/src/index.rsc.ts +3 -1
  44. package/src/index.ts +8 -0
  45. package/src/prerender/store.ts +5 -4
  46. package/src/prerender.ts +138 -77
  47. package/src/reverse.ts +22 -1
  48. package/src/route-definition/dsl-helpers.ts +42 -19
  49. package/src/route-definition/helpers-types.ts +10 -6
  50. package/src/route-definition/index.ts +3 -0
  51. package/src/route-definition/redirect.ts +9 -1
  52. package/src/route-definition/resolve-handler-use.ts +149 -0
  53. package/src/route-types.ts +11 -0
  54. package/src/router/content-negotiation.ts +100 -1
  55. package/src/router/handler-context.ts +79 -23
  56. package/src/router/intercept-resolution.ts +9 -4
  57. package/src/router/loader-resolution.ts +121 -10
  58. package/src/router/match-api.ts +124 -189
  59. package/src/router/match-middleware/cache-lookup.ts +22 -7
  60. package/src/router/match-middleware/cache-store.ts +5 -0
  61. package/src/router/match-middleware/segment-resolution.ts +53 -0
  62. package/src/router/middleware-types.ts +6 -8
  63. package/src/router/middleware.ts +2 -5
  64. package/src/router/navigation-snapshot.ts +182 -0
  65. package/src/router/prerender-match.ts +110 -10
  66. package/src/router/preview-match.ts +30 -102
  67. package/src/router/request-classification.ts +310 -0
  68. package/src/router/route-snapshot.ts +245 -0
  69. package/src/router/router-interfaces.ts +36 -4
  70. package/src/router/router-options.ts +37 -11
  71. package/src/router/segment-resolution/fresh.ts +65 -9
  72. package/src/router/segment-resolution/helpers.ts +29 -24
  73. package/src/router/segment-resolution/revalidation.ts +65 -7
  74. package/src/router/types.ts +1 -0
  75. package/src/router.ts +54 -5
  76. package/src/rsc/handler.ts +460 -368
  77. package/src/rsc/manifest-init.ts +5 -1
  78. package/src/rsc/progressive-enhancement.ts +4 -0
  79. package/src/rsc/rsc-rendering.ts +5 -0
  80. package/src/rsc/server-action.ts +2 -0
  81. package/src/rsc/ssr-setup.ts +2 -2
  82. package/src/rsc/types.ts +8 -1
  83. package/src/server/context.ts +50 -1
  84. package/src/server/loader-registry.ts +9 -8
  85. package/src/server/request-context.ts +134 -14
  86. package/src/ssr/index.tsx +3 -0
  87. package/src/static-handler.ts +18 -6
  88. package/src/types/cache-types.ts +4 -4
  89. package/src/types/handler-context.ts +37 -19
  90. package/src/types/loader-types.ts +36 -9
  91. package/src/types/route-entry.ts +1 -1
  92. package/src/urls/path-helper-types.ts +9 -2
  93. package/src/urls/path-helper.ts +47 -12
  94. package/src/urls/pattern-types.ts +12 -0
  95. package/src/urls/response-types.ts +16 -6
  96. package/src/use-loader.tsx +73 -4
  97. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  98. package/src/vite/discovery/discover-routers.ts +5 -1
  99. package/src/vite/discovery/prerender-collection.ts +14 -1
  100. package/src/vite/discovery/state.ts +13 -4
  101. package/src/vite/index.ts +4 -0
  102. package/src/vite/plugin-types.ts +60 -5
  103. package/src/vite/plugins/performance-tracks.ts +88 -0
  104. package/src/vite/plugins/refresh-cmd.ts +88 -26
  105. package/src/vite/rango.ts +19 -2
  106. package/src/vite/router-discovery.ts +153 -34
  107. package/src/vite/utils/prerender-utils.ts +18 -0
  108. 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 { contextGet, contextSet } from "../context-var.js";
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 declared with " +
112
- "{ passthrough: true }. Remove the passthrough() call or add " +
113
- "{ passthrough: true } to the Prerender options.",
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_]*)(\([^)]*\))?\??/g,
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?.var ?? {};
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" functions.
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
- var: variables,
248
- get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as HandlerContext<
249
- any,
250
- TEnv
251
- >["get"],
252
- set: ((keyOrVar: any, value: any) => {
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
- contextSet(variables, keyOrVar, value);
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, var, get, set, res) throw with a clear error.
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
- return throwUnavailable("env");
333
- },
334
- get var(): any {
335
- return throwUnavailable("var");
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
- return throwUnavailable("env");
417
- },
418
- get var(): any {
419
- return throwUnavailable("var");
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 { HandlerContext, ResolvedSegment } from "../types";
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.var as Record<string, any>,
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 type { HandleStore } from "../server/handle-store.js";
23
+ import { isHandle, collectHandleData, type Handle } from "../handle.js";
24
+ import type { HandleStore, HandleData } 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,76 @@ function createLoaderExecutor<TEnv>(
250
267
  pathname: ctx.pathname,
251
268
  url: ctx.url,
252
269
  env: ctx.env,
253
- var: ctx.var,
254
- get: ctx.get,
255
- use: <TDep, TDepParams = any>(
256
- dep: LoaderDefinition<TDep, TDepParams>,
257
- ): Promise<TDep> => {
258
- return useLoader(dep, currentLoaderId);
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 = buildHandleSnapshot(
288
+ reqCtx._handleStore,
289
+ segmentOrder,
290
+ );
291
+ return collectHandleData(item, snapshot, segmentOrder);
292
+ }
293
+
294
+ // Loader case
295
+ return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
296
+ }) as LoaderContext["use"],
260
297
  method: "GET",
261
298
  body: undefined,
262
299
  reverse: ctx.reverse as LoaderContext["reverse"],
300
+ rendered: (): Promise<void> => {
301
+ // Guard: only DSL loaders may use rendered()
302
+ if (!isDslLoader) {
303
+ throw new Error(
304
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
305
+ `Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
306
+ );
307
+ }
308
+
309
+ // Guard: reject streaming trees
310
+ const reqCtx = reqCtxRef ?? _getRequestContext();
311
+ if (reqCtx?._treeHasStreaming) {
312
+ throw new Error(
313
+ `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
314
+ `Streaming handlers may not have settled when rendered() resolves. ` +
315
+ `Remove loading() from the route tree or restructure to avoid rendered().`,
316
+ );
317
+ }
318
+
319
+ if (renderedPromise) return renderedPromise;
320
+
321
+ if (!reqCtx) {
322
+ throw new Error(
323
+ `ctx.rendered() failed: request context not available.`,
324
+ );
325
+ }
326
+
327
+ // Register this loader as waiting for the barrier so that
328
+ // setupLoaderAccess can detect deadlocks when a handler
329
+ // tries to await the same loader via ctx.use().
330
+ if (!reqCtx._renderBarrierWaiters) {
331
+ reqCtx._renderBarrierWaiters = new Set();
332
+ }
333
+ reqCtx._renderBarrierWaiters.add(currentLoaderId);
334
+
335
+ renderedPromise = reqCtx._renderBarrier.then(() => {
336
+ renderedResolved = true;
337
+ });
338
+ return renderedPromise;
339
+ },
263
340
  };
264
341
 
265
342
  const doneLoader = track(`loader:${loader.$$id}`, 2);
@@ -277,6 +354,25 @@ function createLoaderExecutor<TEnv>(
277
354
  return useLoader;
278
355
  }
279
356
 
357
+ /**
358
+ * Build a HandleData snapshot from the HandleStore using segment ordering.
359
+ * Reads data directly from the store for each segment in order.
360
+ */
361
+ function buildHandleSnapshot(
362
+ handleStore: HandleStore,
363
+ segmentOrder: string[],
364
+ ): HandleData {
365
+ const data: HandleData = {};
366
+ for (const segmentId of segmentOrder) {
367
+ const segData = handleStore.getDataForSegment(segmentId);
368
+ for (const handleName in segData) {
369
+ if (!data[handleName]) data[handleName] = {};
370
+ data[handleName][segmentId] = segData[handleName];
371
+ }
372
+ }
373
+ return data;
374
+ }
375
+
280
376
  /**
281
377
  * Set up the use() method on handler context to access loaders and handles.
282
378
  *
@@ -327,7 +423,22 @@ export function setupLoaderAccess<TEnv>(
327
423
  };
328
424
  }
329
425
 
330
- return useLoader(item as LoaderDefinition<any, any>, null);
426
+ // Deadlock guard: if the loader has called rendered(), it is waiting
427
+ // for segment resolution to complete. A handler awaiting that loader
428
+ // blocks segment resolution, creating a deadlock.
429
+ const loader = item as LoaderDefinition<any, any>;
430
+ if (loaderPromises.has(loader.$$id)) {
431
+ const reqCtx = _getRequestContext();
432
+ if (reqCtx?._renderBarrierWaiters?.has(loader.$$id)) {
433
+ throw new Error(
434
+ `Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
435
+ `The loader is waiting for segment resolution, but the handler blocks resolution. ` +
436
+ `Move the data dependency to a loader-to-loader pattern instead.`,
437
+ );
438
+ }
439
+ }
440
+
441
+ return useLoader(loader, null);
331
442
  }) as typeof ctx.use;
332
443
  }
333
444