@rangojs/router 0.0.0-experimental.142 → 0.0.0-experimental.144
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 +25 -6
- package/package.json +4 -2
- package/skills/cache-guide/SKILL.md +3 -1
- package/skills/caching/SKILL.md +41 -2
- package/skills/catalog.json +6 -0
- package/skills/composability/SKILL.md +32 -0
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/loader/SKILL.md +5 -0
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/observability/SKILL.md +8 -0
- package/skills/parallel/SKILL.md +4 -0
- package/skills/ppr/SKILL.md +110 -20
- package/skills/rango/SKILL.md +10 -0
- package/skills/route/SKILL.md +8 -0
- package/skills/typesafety/SKILL.md +1 -0
- package/skills/typesafety/generated-files-and-cli.md +30 -0
- package/skills/use-cache/SKILL.md +12 -2
- package/src/browser/partial-update.ts +7 -0
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +54 -20
- package/src/cache/document-cache.ts +17 -11
- package/src/cache/vercel/vercel-cache-store.ts +9 -19
- package/src/cloudflare/tracing.ts +7 -8
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -8
- package/src/redirect-origin.ts +14 -0
- package/src/route-definition/helpers-types.ts +5 -4
- package/src/route-map-builder.ts +41 -4
- package/src/router/find-match.ts +15 -1
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +8 -2
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/match-handlers.ts +175 -133
- package/src/router/middleware.ts +40 -30
- package/src/router/router-interfaces.ts +9 -0
- package/src/router/segment-resolution/loader-snapshot.ts +98 -17
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router.ts +22 -14
- package/src/rsc/handler.ts +55 -32
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +35 -2
- package/src/rsc/shell-capture.ts +98 -20
- package/src/server/context.ts +47 -9
- package/src/server/cookie-store.ts +26 -5
- package/src/server/request-context.ts +22 -0
- package/src/ssr/index.tsx +145 -107
- package/src/testing/dispatch.ts +149 -37
- package/src/urls/path-helper-types.ts +9 -4
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/inject-client-debug.ts +64 -12
- package/src/vite/router-discovery.ts +9 -1
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type RequestContext,
|
|
20
20
|
} from "../server/request-context.js";
|
|
21
21
|
import { mayNeedSSR } from "../rsc/ssr-setup.js";
|
|
22
|
-
import {
|
|
22
|
+
import { cacheKeyBase } from "./cache-key-utils.js";
|
|
23
23
|
import { runBackground } from "./background-task.js";
|
|
24
24
|
import { reportCacheError } from "./cache-error.js";
|
|
25
25
|
|
|
@@ -188,7 +188,13 @@ export interface DocumentCacheOptions<TEnv = any> {
|
|
|
188
188
|
skipPaths?: string[];
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
* Custom cache key generator
|
|
191
|
+
* Custom cache key generator.
|
|
192
|
+
*
|
|
193
|
+
* Replaces the default `host + pathname + search` key entirely. On a
|
|
194
|
+
* multi-domain deployment served by one function you MUST include `url.host`
|
|
195
|
+
* (or an equivalent tenant discriminator) yourself — the default key is
|
|
196
|
+
* host-namespaced, but a custom generator's output is used verbatim, so
|
|
197
|
+
* omitting host bleeds one hostname's cached response to another.
|
|
192
198
|
*/
|
|
193
199
|
keyGenerator?: (url: URL) => string;
|
|
194
200
|
|
|
@@ -311,17 +317,17 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
311
317
|
isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
|
|
312
318
|
const typeSuffix = isRscRequest ? ":rsc" : ":html";
|
|
313
319
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
320
|
+
// Default key rides the shared host-namespaced base (cacheKeyBase) so the
|
|
321
|
+
// segment tier (cache-scope.ts) and this document tier cannot drift on the
|
|
322
|
+
// host-namespacing rule -- see the contract on cacheKeyBase.
|
|
323
|
+
// The keyGenerator branch is left untouched: a consumer-supplied generator
|
|
324
|
+
// owns its own namespacing (auto-prefixing host would silently change their
|
|
325
|
+
// existing keys and double any host they already include).
|
|
322
326
|
const cacheKey = keyGenerator
|
|
323
327
|
? keyGenerator(url) + segmentHash + typeSuffix
|
|
324
|
-
:
|
|
328
|
+
: cacheKeyBase(url.host, url.pathname, url.searchParams) +
|
|
329
|
+
segmentHash +
|
|
330
|
+
typeSuffix;
|
|
325
331
|
// 1. Check cache
|
|
326
332
|
const cached = await store.getResponse(cacheKey);
|
|
327
333
|
|
|
@@ -51,6 +51,15 @@ import {
|
|
|
51
51
|
} from "../cache-policy.js";
|
|
52
52
|
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
53
53
|
import type { CacheErrorCategory } from "../cache-error.js";
|
|
54
|
+
// Reuse the CF store's binary-safe base64 helpers. bufferToBase64 caps each
|
|
55
|
+
// String.fromCharCode batch at 8192 and uses .apply (never a spread), so a large
|
|
56
|
+
// Response/PPR-shell body cannot blow the JS argument-count ceiling (~65k) and
|
|
57
|
+
// throw RangeError inside putResponse/putShell - which the outer try/catch would
|
|
58
|
+
// swallow as a cache-write degrade, silently never caching the entry. Output is
|
|
59
|
+
// byte-identical to a per-byte encoder (chunk size does not affect base64), so
|
|
60
|
+
// this is a robustness fix, not a format change. Do NOT reintroduce a local
|
|
61
|
+
// spread-based encoder or raise the chunk here; cf-base64.ts is import-pure.
|
|
62
|
+
import { bufferToBase64, base64ToBuffer } from "../cf/cf-base64.js";
|
|
54
63
|
|
|
55
64
|
/**
|
|
56
65
|
* Minimal structural shape of the Vercel Runtime Cache returned by `getCache()`
|
|
@@ -277,25 +286,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
277
286
|
return typeof value === "object" && value !== null;
|
|
278
287
|
}
|
|
279
288
|
|
|
280
|
-
/** Encode binary body bytes to base64 in chunks (avoids call-stack blowups). */
|
|
281
|
-
function bufferToBase64(buffer: ArrayBuffer): string {
|
|
282
|
-
const bytes = new Uint8Array(buffer);
|
|
283
|
-
let binary = "";
|
|
284
|
-
const CHUNK = 0x8000;
|
|
285
|
-
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
286
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
287
|
-
}
|
|
288
|
-
return btoa(binary);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Decode a base64 body back into bytes. */
|
|
292
|
-
function base64ToBuffer(b64: string): ArrayBuffer {
|
|
293
|
-
const binary = atob(b64);
|
|
294
|
-
const bytes = new Uint8Array(binary.length);
|
|
295
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
296
|
-
return bytes.buffer;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
289
|
/**
|
|
300
290
|
* Vercel Runtime Cache-backed segment cache store.
|
|
301
291
|
*
|
|
@@ -39,7 +39,7 @@ import { _getRequestContext } from "../server/request-context.js";
|
|
|
39
39
|
import {
|
|
40
40
|
type RouterTracingConfig,
|
|
41
41
|
type SpanRunner,
|
|
42
|
-
type
|
|
42
|
+
type TracingToggleOptions,
|
|
43
43
|
NOOP_TRACE_SPAN,
|
|
44
44
|
} from "../router/tracing.js";
|
|
45
45
|
|
|
@@ -57,13 +57,12 @@ interface CloudflareTracing {
|
|
|
57
57
|
enterSpan<T>(name: string, callback: (span: CloudflareSpan) => T): T;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
60
|
+
/**
|
|
61
|
+
* Options for createCloudflareTracing. Alias of the shared
|
|
62
|
+
* {@link TracingToggleOptions} (`enabled` master switch + per-phase `spans`
|
|
63
|
+
* toggles); the name is public API.
|
|
64
|
+
*/
|
|
65
|
+
export type CloudflareTracingOptions = TracingToggleOptions;
|
|
67
66
|
|
|
68
67
|
/**
|
|
69
68
|
* Resolve the per-request Cloudflare tracer from the active execution context.
|
package/src/index.rsc.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -343,14 +343,17 @@ export {
|
|
|
343
343
|
// Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
|
|
344
344
|
// `Rango.PathResponse`, declared in href-client.ts) — no import needed.
|
|
345
345
|
|
|
346
|
-
// Telemetry types only — the createConsoleSink/createOTelSink
|
|
347
|
-
// server-only and live in index.rsc.ts (the
|
|
348
|
-
// bare `@rangojs/router` import). Re-exporting
|
|
349
|
-
// (default/client) entry would pull telemetry.ts and
|
|
350
|
-
// the client module graph; both tree-shake to zero bytes
|
|
351
|
-
// bundle analysis output and slow build-time module
|
|
352
|
-
//
|
|
353
|
-
//
|
|
346
|
+
// Telemetry types only — the createConsoleSink / createOTelSink /
|
|
347
|
+
// createOTelTracing VALUES are server-only and live in index.rsc.ts (the
|
|
348
|
+
// `react-server` condition of the bare `@rangojs/router` import). Re-exporting
|
|
349
|
+
// them as values from this (default/client) entry would pull telemetry.ts and
|
|
350
|
+
// telemetry-otel.ts into the client module graph; both tree-shake to zero bytes
|
|
351
|
+
// but still appear in bundle analysis output and slow build-time module
|
|
352
|
+
// resolution. The factory values are NOT re-exported from `@rangojs/router/server`
|
|
353
|
+
// either — that subpath is internal, not user-facing (see server.ts header).
|
|
354
|
+
// Non-RSC server code imports these TYPES from the root and obtains the factory
|
|
355
|
+
// VALUES from its own router definition module, which resolves to index.rsc.ts
|
|
356
|
+
// under the `react-server` condition.
|
|
354
357
|
export type {
|
|
355
358
|
OTelTracer,
|
|
356
359
|
OTelActiveSpanTracer,
|
|
@@ -386,6 +389,7 @@ export type {
|
|
|
386
389
|
RouterTracingConfig,
|
|
387
390
|
TracePhase,
|
|
388
391
|
TracePhaseToggles,
|
|
392
|
+
TracingToggleOptions,
|
|
389
393
|
} from "./router/tracing.js";
|
|
390
394
|
|
|
391
395
|
// Timeout types and error class
|
package/src/redirect-origin.ts
CHANGED
|
@@ -60,6 +60,20 @@ export function resolveExternalRedirect(
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The safe same-origin landing for a blocked redirect.
|
|
65
|
+
*
|
|
66
|
+
* Every guard that neutralizes a cross-origin/unsafe redirect target sends the
|
|
67
|
+
* browser here instead: the app's basename root, or `"/"` when unset. Kept
|
|
68
|
+
* beside the resolvers so the "where does a blocked redirect go" answer lives
|
|
69
|
+
* in ONE place -- the server 3xx guard (`rsc/redirect-guard.ts`) and the
|
|
70
|
+
* shell-HIT degradation path (`rsc/rsc-rendering.ts`) must agree, or a blocked
|
|
71
|
+
* redirect lands differently depending on which exit it took.
|
|
72
|
+
*/
|
|
73
|
+
export function safeSameOriginLanding(basename: string | undefined): string {
|
|
74
|
+
return basename && basename !== "/" ? basename : "/";
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
/**
|
|
64
78
|
* Out-of-band brand for `redirect(url, { external: true })`.
|
|
65
79
|
*
|
|
@@ -149,8 +149,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
149
149
|
* so they take precedence on `loading()` and other last-write-wins
|
|
150
150
|
* fields.
|
|
151
151
|
*/
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
// Not generic over the slots record: an inferred type parameter makes the
|
|
153
|
+
// object literal an inference site, which suppresses contextual typing of
|
|
154
|
+
// arrow slot handlers (`(ctx) => ...` was implicit any).
|
|
155
|
+
parallel: (
|
|
156
|
+
slots: Record<
|
|
154
157
|
`@${string}`,
|
|
155
158
|
| Handler<any, any, TEnv>
|
|
156
159
|
| ReactNode
|
|
@@ -159,8 +162,6 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
159
162
|
use?: () => UseItems<ParallelUseItem>;
|
|
160
163
|
}
|
|
161
164
|
>,
|
|
162
|
-
>(
|
|
163
|
-
slots: TSlots,
|
|
164
165
|
use?: () => UseItems<ParallelUseItem>,
|
|
165
166
|
) => ParallelItem;
|
|
166
167
|
/**
|
package/src/route-map-builder.ts
CHANGED
|
@@ -20,11 +20,25 @@ let cachedPrecomputedEntries: Array<{
|
|
|
20
20
|
/**
|
|
21
21
|
* Register routes into the global route map.
|
|
22
22
|
* Routes are merged with any existing registered routes.
|
|
23
|
-
* Called by createRouter() during module evaluation
|
|
23
|
+
* Called by createRouter() during module evaluation, and by lazy-include
|
|
24
|
+
* expansion (src/router/lazy-includes.ts) with each expansion's route delta.
|
|
25
|
+
*
|
|
26
|
+
* Merges IN PLACE — O(|map|), not O(total routes). The previous
|
|
27
|
+
* `globalRouteMap = { ...globalRouteMap, ...map }` copy made every
|
|
28
|
+
* lazy-include first hit O(total routes) on the request path: with a 26k-route
|
|
29
|
+
* manifest the spread measured 8.9ms/call (M4, node), paid once per level of a
|
|
30
|
+
* nested async-include chain (3 calls on a 3-level chain — the 464ms edge
|
|
31
|
+
* cold-hit in issue #666).
|
|
32
|
+
*
|
|
33
|
+
* In-place mutation is safe because every getGlobalRouteMap() consumer reads
|
|
34
|
+
* it fresh per call (server/request-context.ts, rsc/loader-fetch.ts,
|
|
35
|
+
* router/intercept-resolution.ts, testing/generated-routes.ts,
|
|
36
|
+
* rsc/manifest-init.ts) — none memoizes the returned reference. If you add a
|
|
37
|
+
* consumer that caches the map object, it will now observe later
|
|
38
|
+
* registrations; snapshot it yourself if you need frozen contents.
|
|
24
39
|
*/
|
|
25
40
|
export function registerRouteMap(map: Record<string, string>): void {
|
|
26
|
-
|
|
27
|
-
globalRouteMap = { ...globalRouteMap, ...map };
|
|
41
|
+
Object.assign(globalRouteMap, map);
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
/**
|
|
@@ -139,6 +153,7 @@ export function clearAllRouterData(): void {
|
|
|
139
153
|
perRouterManifestMap.clear();
|
|
140
154
|
perRouterTrieMap.clear();
|
|
141
155
|
perRouterPrecomputedEntriesMap.clear();
|
|
156
|
+
authoritativeTrieRouters.clear();
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
export function setRouterManifest(
|
|
@@ -162,6 +177,23 @@ export function setRouterTrie(
|
|
|
162
177
|
perRouterTrieMap.set(routerId, trie);
|
|
163
178
|
}
|
|
164
179
|
|
|
180
|
+
// Routers whose trie came from the COMPLETE build manifest (deserialized via
|
|
181
|
+
// ensureRouterManifest). For these, a trie miss is a real 404 and findMatch
|
|
182
|
+
// skips the regex fallback scan — the only remaining route-count-proportional
|
|
183
|
+
// match path (#664). Dev rebuilds (manifest-init.ts, router-discovery HMR
|
|
184
|
+
// pushes) deliberately never mark authoritative: the dev-only trie-gap warning
|
|
185
|
+
// in find-match.ts depends on the fallback running on misses, and dev route
|
|
186
|
+
// churn (HMR, dev-time routes) makes a stale-trie 404 unacceptable there.
|
|
187
|
+
const authoritativeTrieRouters: Set<string> = new Set();
|
|
188
|
+
|
|
189
|
+
export function markRouterTrieAuthoritative(routerId: string): void {
|
|
190
|
+
authoritativeTrieRouters.add(routerId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function isRouterTrieAuthoritative(routerId: string): boolean {
|
|
194
|
+
return authoritativeTrieRouters.has(routerId);
|
|
195
|
+
}
|
|
196
|
+
|
|
165
197
|
export function getRouterTrie(
|
|
166
198
|
routerId: string,
|
|
167
199
|
): import("./build/route-trie.js").TrieNode | undefined {
|
|
@@ -204,7 +236,12 @@ export async function ensureRouterManifest(routerId: string): Promise<void> {
|
|
|
204
236
|
if (loader) {
|
|
205
237
|
const mod = await loader();
|
|
206
238
|
if (mod.manifest) perRouterManifestMap.set(routerId, mod.manifest);
|
|
207
|
-
if (mod.trie)
|
|
239
|
+
if (mod.trie) {
|
|
240
|
+
perRouterTrieMap.set(routerId, mod.trie);
|
|
241
|
+
// A trie serialized into the build manifest comes from complete
|
|
242
|
+
// discovery — misses are authoritative 404s (see find-match.ts).
|
|
243
|
+
markRouterTrieAuthoritative(routerId);
|
|
244
|
+
}
|
|
208
245
|
if (mod.precomputedEntries)
|
|
209
246
|
perRouterPrecomputedEntriesMap.set(routerId, mod.precomputedEntries);
|
|
210
247
|
routerManifestLoaders.delete(routerId);
|
package/src/router/find-match.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { tryTrieMatch } from "./trie-matching.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getRouterTrie,
|
|
4
|
+
isRouterTrieAuthoritative,
|
|
5
|
+
} from "../route-map-builder.js";
|
|
3
6
|
import {
|
|
4
7
|
findMatch as findRouteMatch,
|
|
5
8
|
isLazyEvaluationNeeded,
|
|
@@ -166,6 +169,17 @@ export function createFindMatch<TEnv = any>(
|
|
|
166
169
|
};
|
|
167
170
|
return cloneMatchResult(lastFindMatchResult);
|
|
168
171
|
}
|
|
172
|
+
} else if (isRouterTrieAuthoritative(deps.routerId)) {
|
|
173
|
+
// Authoritative miss (#664): this trie was deserialized from the
|
|
174
|
+
// COMPLETE build manifest, so trailing-slash redirects are already
|
|
175
|
+
// trie-native hits and a miss means no route exists. Skip the regex
|
|
176
|
+
// fallback — the only route-count-proportional match path — and do
|
|
177
|
+
// not evaluate lazy includes for unmatched (bot-probe) traffic.
|
|
178
|
+
// Trie hits that need lazy splicing keep the fallback loop below
|
|
179
|
+
// (trieMatched === true never reaches this branch).
|
|
180
|
+
lastFindMatchPathname = pathname;
|
|
181
|
+
lastFindMatchResult = null;
|
|
182
|
+
return null;
|
|
169
183
|
}
|
|
170
184
|
}
|
|
171
185
|
|
package/src/router/instrument.ts
CHANGED
|
@@ -318,10 +318,10 @@ export function observeHandler<C, R>(
|
|
|
318
318
|
* sink is configured.
|
|
319
319
|
*
|
|
320
320
|
* This is the canonical emitter for SYNCHRONOUS facts that fire inside the
|
|
321
|
-
* request's ALS scope (
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
321
|
+
* request's ALS scope (revalidation decisions, cache-lookup decisions). A few
|
|
322
|
+
* emitters deliberately stay on the lower-level resolveSink + safeEmit because
|
|
323
|
+
* observeEvent's lazy, per-call getRouterContext() read does not fit them — keep
|
|
324
|
+
* this the complete list:
|
|
325
325
|
* - router.ts wrapLoaderPromise (loader.start/end/error) and
|
|
326
326
|
* segment-resolution/streamed-handler-telemetry.ts (streamed handler.error)
|
|
327
327
|
* capture the sink + request id EAGERLY and emit from a fire-and-forget
|
|
@@ -330,6 +330,11 @@ export function observeHandler<C, R>(
|
|
|
330
330
|
* loop (request.start/end/error, cache.decision, ...).
|
|
331
331
|
* - segment-resolution/helpers.ts emits via a caller-provided report.telemetry
|
|
332
332
|
* sink rather than the ALS router context.
|
|
333
|
+
* - rsc/handler.ts handleTimeoutResponse (request.timeout), the origin guard
|
|
334
|
+
* (request.origin-rejected), and handleStore.onError (late-handle
|
|
335
|
+
* handler.error) emit via router.telemetry directly — they run outside the
|
|
336
|
+
* RouterContext ALS (only match()/matchPartial() enter it), so a
|
|
337
|
+
* getRouterContext() read there throws and the event would vanish.
|
|
333
338
|
*/
|
|
334
339
|
export function observeEvent(event: TelemetryEvent): void {
|
|
335
340
|
// getRouterContext() either throws (real impl, outside a router context — e.g.
|
|
@@ -91,7 +91,11 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
91
91
|
for (const [name, pattern] of Object.entries(routes)) {
|
|
92
92
|
deps.mergedRouteMap[name] = pattern;
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
// Register only this entry's routes (the delta): the full
|
|
95
|
+
// mergedRouteMap is seeded from the generated manifest at
|
|
96
|
+
// createRouter() time and already registered there — re-passing it
|
|
97
|
+
// made this request-path call O(total routes) (issue #666).
|
|
98
|
+
registerRouteMap(routes);
|
|
95
99
|
return;
|
|
96
100
|
}
|
|
97
101
|
}
|
|
@@ -245,7 +249,9 @@ function runExpansion<TEnv = any>(
|
|
|
245
249
|
deps.routesEntries.splice(insertIndex, 0, nestedEntry);
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
|
|
252
|
+
// Delta only — see the matching comment on the precomputed branch above and
|
|
253
|
+
// the WHY block on registerRouteMap (issue #666).
|
|
254
|
+
registerRouteMap(routesObject);
|
|
249
255
|
|
|
250
256
|
// Expansion fully succeeded (handler ran, routes + nested includes spliced) —
|
|
251
257
|
// mark done now so a mid-expansion throw above leaves lazyEvaluated=false and
|
|
@@ -420,10 +420,22 @@ function createLoaderExecutor<TEnv>(
|
|
|
420
420
|
// throw. rendered() gating uses the captured isDslLoader (above), so this
|
|
421
421
|
// does not grant rendered() to handler-invoked loaders. Uses a body-only
|
|
422
422
|
// scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
|
|
423
|
+
//
|
|
424
|
+
// `handlerInvoked` (!isDslLoader) rides on the scope for the CONSUMPTION-
|
|
425
|
+
// LANE RULE: a handler-consumed loader's value is a BAKED copy in every
|
|
426
|
+
// shared artifact (cache(), "use cache", the PPR shell), so its identity
|
|
427
|
+
// reads are exempt from the shell-capture guard — same allowance the
|
|
428
|
+
// cache-purity guards give it. DSL segment loaders keep their lane
|
|
429
|
+
// machinery (live = masked at capture, bake = guarded). A DSL loader's
|
|
430
|
+
// nested deps inherit isDslLoader=false only when the CHAIN started in a
|
|
431
|
+
// handler; a chain started by the segment funnel stays DSL (the loader
|
|
432
|
+
// scope ALS survives the body's awaits).
|
|
423
433
|
const promise = observePhase(PHASES.loader(loader.$$id), () =>
|
|
424
434
|
Promise.resolve(
|
|
425
|
-
runInsideLoaderBodyScope(
|
|
426
|
-
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
435
|
+
runInsideLoaderBodyScope(
|
|
436
|
+
() => loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
437
|
+
loader.$$id,
|
|
438
|
+
!isDslLoader,
|
|
427
439
|
),
|
|
428
440
|
).finally(() => {
|
|
429
441
|
pendingLoaders.delete(loader.$$id);
|