@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2154 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +243 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +128 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +121 -0
- package/skills/testing/e2e-parity.md +124 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +127 -0
- package/skills/testing/loader.md +108 -0
- package/skills/testing/middleware.md +97 -0
- package/skills/testing/render-handler.md +102 -0
- package/skills/testing/response-routes.md +94 -0
- package/skills/testing/reverse-and-types.md +83 -0
- package/skills/testing/server-actions.md +89 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +104 -68
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +183 -44
- package/src/browser/prefetch/fetch.ts +228 -37
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +32 -1
- package/src/browser/rsc-router.tsx +69 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +95 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +32 -14
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +54 -17
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +25 -7
- package/src/loader.ts +16 -9
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +27 -6
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +52 -30
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +57 -61
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +67 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +326 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +51 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +323 -0
- package/src/testing/render-route.tsx +590 -0
- package/src/testing/run-loader.ts +363 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +285 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/router/find-match.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tryTrieMatch } from "./trie-matching.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getRouterTrie } from "../route-map-builder.js";
|
|
3
3
|
import {
|
|
4
4
|
findMatch as findRouteMatch,
|
|
5
5
|
isLazyEvaluationNeeded,
|
|
@@ -8,6 +8,19 @@ import {
|
|
|
8
8
|
import type { MetricsStore } from "../server/context";
|
|
9
9
|
import type { RouteEntry } from "../types";
|
|
10
10
|
|
|
11
|
+
// Return a shallow copy with an independent `params` object. The single-entry
|
|
12
|
+
// cache below is module-lifetime and keyed only on pathname, so the same result
|
|
13
|
+
// object is handed to every same-pathname request in the isolate. ctx.params
|
|
14
|
+
// aliases this `params` (see request-context), so without an own copy a handler
|
|
15
|
+
// that mutates ctx.params would corrupt the cached entry for later requests.
|
|
16
|
+
// `entry` and the flags are intentionally shared by reference: they are
|
|
17
|
+
// read-only, and entry identity is compared in match-api (prevMatch.entry).
|
|
18
|
+
function cloneMatchResult<TEnv>(
|
|
19
|
+
r: RouteMatchResult<TEnv> | null,
|
|
20
|
+
): RouteMatchResult<TEnv> | null {
|
|
21
|
+
return r ? { ...r, params: { ...r.params } } : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
export interface FindMatchDeps<TEnv = any> {
|
|
12
25
|
routesEntries: RouteEntry<TEnv>[];
|
|
13
26
|
evaluateLazyEntry: (entry: RouteEntry<TEnv>) => void;
|
|
@@ -35,9 +48,10 @@ export function createFindMatch<TEnv = any>(
|
|
|
35
48
|
pathname: string,
|
|
36
49
|
ms?: MetricsStore,
|
|
37
50
|
): RouteMatchResult<TEnv> | null {
|
|
38
|
-
// Return cached result if same pathname (avoids double-match per request)
|
|
51
|
+
// Return cached result if same pathname (avoids double-match per request).
|
|
52
|
+
// Clone so a caller mutating ctx.params cannot corrupt the shared cache.
|
|
39
53
|
if (lastFindMatchPathname === pathname) {
|
|
40
|
-
return lastFindMatchResult;
|
|
54
|
+
return cloneMatchResult(lastFindMatchResult);
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
// Helper to push sub-metrics
|
|
@@ -56,12 +70,19 @@ export function createFindMatch<TEnv = any>(
|
|
|
56
70
|
// routers and must not be used — in multi-router setups (host routing)
|
|
57
71
|
// overlapping paths like "/" would match the wrong app's route.
|
|
58
72
|
const routeTrie = getRouterTrie(deps.routerId);
|
|
73
|
+
// Whether the trie produced a match for this pathname (independent of
|
|
74
|
+
// whether the owning RouteEntry was resolvable yet). Used to suppress the
|
|
75
|
+
// R3 dev warning below: if the trie DID match but we fell through to the
|
|
76
|
+
// regex fallback only because a lazy entry was not spliced in yet, that is
|
|
77
|
+
// not a trie gap.
|
|
78
|
+
let trieMatched = false;
|
|
59
79
|
if (routeTrie) {
|
|
60
80
|
const trieStart = performance.now();
|
|
61
81
|
const trieResult = tryTrieMatch(routeTrie, pathname);
|
|
62
82
|
pushMetric?.("match:trie", trieStart);
|
|
63
83
|
|
|
64
84
|
if (trieResult) {
|
|
85
|
+
trieMatched = true;
|
|
65
86
|
// Find the RouteEntry that contains this route.
|
|
66
87
|
// Multiple entries can share the same staticPrefix (e.g., several
|
|
67
88
|
// include("/", patterns) calls all produce staticPrefix=""). Evaluate
|
|
@@ -114,7 +135,6 @@ export function createFindMatch<TEnv = any>(
|
|
|
114
135
|
params: trieResult.params,
|
|
115
136
|
optionalParams: new Set(trieResult.optionalParams || []),
|
|
116
137
|
redirectTo: trieResult.redirectTo,
|
|
117
|
-
ancestry: trieResult.ancestry,
|
|
118
138
|
...(trieResult.pr ? { pr: true } : {}),
|
|
119
139
|
...(trieResult.pt ? { pt: true } : {}),
|
|
120
140
|
...(trieResult.responseType
|
|
@@ -125,7 +145,7 @@ export function createFindMatch<TEnv = any>(
|
|
|
125
145
|
: {}),
|
|
126
146
|
...(trieResult.rscFirst ? { rscFirst: true } : {}),
|
|
127
147
|
};
|
|
128
|
-
return lastFindMatchResult;
|
|
148
|
+
return cloneMatchResult(lastFindMatchResult);
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
}
|
|
@@ -153,8 +173,36 @@ export function createFindMatch<TEnv = any>(
|
|
|
153
173
|
}
|
|
154
174
|
pushMetric?.("match:regex-fallback", regexStart);
|
|
155
175
|
|
|
176
|
+
// The trie is the single source of truth and is built before findMatch in
|
|
177
|
+
// both dev (handler rebuild) and production (ensureRouterManifest). If the
|
|
178
|
+
// trie was present yet the regex fallback resolved a real match, the trie
|
|
179
|
+
// has a gap (e.g. a route shape it cannot represent) and dev/prod could
|
|
180
|
+
// diverge if the trie were ever absent. Surface it in dev; folded out in
|
|
181
|
+
// production builds.
|
|
182
|
+
//
|
|
183
|
+
// Suppress when the trie DID match (`trieMatched`): that path falls through
|
|
184
|
+
// to the regex fallback only on the first request to a not-yet-spliced lazy
|
|
185
|
+
// entry (e.g. a 2+-level nested include whose deeper parent has not been
|
|
186
|
+
// evaluated). The trie knew the route; runtime lazy discovery simply lagged.
|
|
187
|
+
// That is the supported lazy-include flow, not a trie gap, so warning on it
|
|
188
|
+
// is a false positive (it manufactures bug reports and erodes the signal).
|
|
189
|
+
if (
|
|
190
|
+
process.env.NODE_ENV !== "production" &&
|
|
191
|
+
routeTrie &&
|
|
192
|
+
!trieMatched &&
|
|
193
|
+
result &&
|
|
194
|
+
!isLazyEvaluationNeeded(result)
|
|
195
|
+
) {
|
|
196
|
+
console.warn(
|
|
197
|
+
`[@rangojs/router] Route "${pathname}" resolved via the regex fallback ` +
|
|
198
|
+
`even though the route trie was present. The trie should be the single ` +
|
|
199
|
+
`matching source of truth; this indicates a trie gap. Please report this ` +
|
|
200
|
+
`with your route configuration.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
156
204
|
lastFindMatchPathname = pathname;
|
|
157
205
|
lastFindMatchResult = result;
|
|
158
|
-
return result;
|
|
206
|
+
return cloneMatchResult(result);
|
|
159
207
|
};
|
|
160
208
|
}
|
|
@@ -18,6 +18,8 @@ import { isInsideCacheScope } from "../server/context.js";
|
|
|
18
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
19
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
20
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
21
|
+
import { substitutePatternParams } from "./substitute-pattern-params.js";
|
|
22
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Strip internal _rsc* query params from a URL.
|
|
@@ -158,48 +160,14 @@ export function createReverseFunction(
|
|
|
158
160
|
);
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
let result = pattern;
|
|
162
|
-
|
|
163
163
|
// Merge current request params as defaults, explicit params override
|
|
164
164
|
const effectiveParams = currentParams
|
|
165
165
|
? { ...currentParams, ...hrefParams }
|
|
166
166
|
: hrefParams;
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let hadOmittedOptional = false;
|
|
172
|
-
// First pass: optional params (trailing ?)
|
|
173
|
-
result = result.replace(
|
|
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,
|
|
187
|
-
(_, key) => {
|
|
188
|
-
const value = effectiveParams[key];
|
|
189
|
-
if (value === undefined) {
|
|
190
|
-
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
191
|
-
}
|
|
192
|
-
return encodeURIComponent(value);
|
|
193
|
-
},
|
|
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
|
-
}
|
|
202
|
-
}
|
|
168
|
+
let result = effectiveParams
|
|
169
|
+
? substitutePatternParams(pattern, effectiveParams, name)
|
|
170
|
+
: pattern;
|
|
203
171
|
|
|
204
172
|
// Append search params as query string
|
|
205
173
|
if (search) {
|
|
@@ -278,8 +246,12 @@ export function createHandlerContext<TEnv>(
|
|
|
278
246
|
search: searchSchema ? resolvedSearchParams : {},
|
|
279
247
|
pathname,
|
|
280
248
|
url,
|
|
281
|
-
originalUrl: new URL(request.url),
|
|
249
|
+
originalUrl: requestContext?.originalUrl ?? new URL(request.url),
|
|
282
250
|
env: bindings,
|
|
251
|
+
waitUntil: requestContext
|
|
252
|
+
? requestContext.waitUntil.bind(requestContext)
|
|
253
|
+
: fireAndForgetWaitUntil,
|
|
254
|
+
executionContext: requestContext?.executionContext,
|
|
283
255
|
_variables: variables,
|
|
284
256
|
get: ((keyOrVar: any) => {
|
|
285
257
|
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
@@ -384,6 +356,12 @@ export function createPrerenderContext<TEnv>(
|
|
|
384
356
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
385
357
|
);
|
|
386
358
|
},
|
|
359
|
+
// Build-time prerender has no live request. waitUntil is a true no-op
|
|
360
|
+
// (running fn() here would fire side effects during build, which is
|
|
361
|
+
// incorrect — these are meant to outlive the live response).
|
|
362
|
+
// executionContext is absent for the same reason.
|
|
363
|
+
waitUntil: () => {},
|
|
364
|
+
executionContext: undefined,
|
|
387
365
|
_variables: variables,
|
|
388
366
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
389
367
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -473,6 +451,11 @@ export function createStaticContext<TEnv>(
|
|
|
473
451
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
474
452
|
);
|
|
475
453
|
},
|
|
454
|
+
// Static() handlers have no live request. waitUntil is a true no-op
|
|
455
|
+
// (running fn() here would fire side effects during build, which is
|
|
456
|
+
// incorrect). executionContext is absent for the same reason.
|
|
457
|
+
waitUntil: () => {},
|
|
458
|
+
executionContext: undefined,
|
|
476
459
|
_variables: variables,
|
|
477
460
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
478
461
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -66,28 +66,14 @@ export function findInterceptForRoute(
|
|
|
66
66
|
let current: EntryData | null = fromEntry;
|
|
67
67
|
|
|
68
68
|
while (current) {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// current first, then its sibling layouts — same order as before.
|
|
70
|
+
for (const source of [current, ...current.layout]) {
|
|
71
|
+
for (const intercept of source.intercept) {
|
|
71
72
|
if (
|
|
72
73
|
intercept.routeName === targetRouteKey &&
|
|
73
74
|
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
74
75
|
) {
|
|
75
|
-
return { intercept, entry:
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (current.layout && current.layout.length > 0) {
|
|
81
|
-
for (const siblingLayout of current.layout) {
|
|
82
|
-
if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
|
|
83
|
-
for (const intercept of siblingLayout.intercept) {
|
|
84
|
-
if (
|
|
85
|
-
intercept.routeName === targetRouteKey &&
|
|
86
|
-
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
87
|
-
) {
|
|
88
|
-
return { intercept, entry: siblingLayout };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
76
|
+
return { intercept, entry: source };
|
|
91
77
|
}
|
|
92
78
|
}
|
|
93
79
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { registerRouteMap } from "../route-map-builder.js";
|
|
2
|
-
import { extractStaticPrefix } from "./pattern-matching.js";
|
|
2
|
+
import { extractStaticPrefix, joinPrefix } from "./pattern-matching.js";
|
|
3
3
|
import {
|
|
4
|
-
EntryData,
|
|
5
|
-
|
|
4
|
+
type EntryData,
|
|
5
|
+
RangoContext,
|
|
6
6
|
runWithPrefixes,
|
|
7
7
|
getIsolatedLazyParent,
|
|
8
8
|
} from "../server/context";
|
|
@@ -81,11 +81,16 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
81
81
|
// Check for pre-computed routes from build-time data.
|
|
82
82
|
// Only leaf nodes (no nested includes) are precomputed, so entries with
|
|
83
83
|
// nested lazy includes fall through to the handler below.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
84
|
+
//
|
|
85
|
+
// The load-bearing protection against two includes sharing a staticPrefix
|
|
86
|
+
// lives UPSTREAM in buildPrecomputedByPrefix (build/prefix-tree-utils): a
|
|
87
|
+
// shared staticPrefix is omitted from the map entirely, so currentPrecomputed
|
|
88
|
+
// never returns routes for it and the shortcut is skipped. The live-count
|
|
89
|
+
// check below is a secondary guard only — it is TIMING-BLIND (it counts
|
|
90
|
+
// routesEntries, which cannot see a nested sibling that has not been spliced
|
|
91
|
+
// in yet), so it must NOT be relied on alone. Kept as defense-in-depth for the
|
|
92
|
+
// all-siblings-live case (e.g. several include("/", ...) placeholders created
|
|
93
|
+
// up front).
|
|
89
94
|
const currentPrecomputed = deps.getPrecomputedByPrefix();
|
|
90
95
|
if (currentPrecomputed) {
|
|
91
96
|
const routes = currentPrecomputed.get(entry.staticPrefix);
|
|
@@ -113,7 +118,15 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
113
118
|
const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
|
|
114
119
|
const lazyContext = entry.lazyContext;
|
|
115
120
|
|
|
116
|
-
// Create a new context for evaluating the lazy patterns
|
|
121
|
+
// Create a new context for evaluating the lazy patterns.
|
|
122
|
+
// KNOWN REDUNDANCY (LP3, docs/internal/matching-and-lazy-discovery.md): this
|
|
123
|
+
// runs lazyPatterns.handler() purely to extract `patterns` (route name ->
|
|
124
|
+
// pattern) for matching, and DISCARDS the EntryData `manifest` it builds.
|
|
125
|
+
// loadManifest() then runs the SAME handler again on the first request to
|
|
126
|
+
// build the EntryData tree for rendering. Unifying the two runs is deferred
|
|
127
|
+
// (the two run in different contexts — see the LP3 todo in
|
|
128
|
+
// lazy-include-perf.test.ts). The precomputed-entries shortcut above avoids
|
|
129
|
+
// THIS run entirely for leaf includes.
|
|
117
130
|
const manifest = new Map<string, EntryData>();
|
|
118
131
|
const patterns = new Map<string, string>();
|
|
119
132
|
const patternsByPrefix = new Map<string, Map<string, string>>();
|
|
@@ -125,14 +138,13 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
125
138
|
// Merge captured counters from include() to maintain consistent
|
|
126
139
|
// shortCode indices with sibling entries from pattern extraction
|
|
127
140
|
const lazyCounters: Record<string, number> = {};
|
|
128
|
-
if (lazyContext
|
|
129
|
-
const
|
|
130
|
-
for (const [key, value] of Object.entries(captured)) {
|
|
141
|
+
if (lazyContext?.counters) {
|
|
142
|
+
for (const [key, value] of Object.entries(lazyContext.counters)) {
|
|
131
143
|
lazyCounters[key] = value;
|
|
132
144
|
}
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
RangoContext.run(
|
|
136
148
|
{
|
|
137
149
|
manifest,
|
|
138
150
|
patterns,
|
|
@@ -141,14 +153,18 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
141
153
|
namespace: "lazy",
|
|
142
154
|
parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
|
|
143
155
|
counters: lazyCounters,
|
|
144
|
-
cacheProfiles:
|
|
145
|
-
rootScoped:
|
|
156
|
+
cacheProfiles: lazyContext?.cacheProfiles,
|
|
157
|
+
rootScoped: lazyContext?.rootScoped,
|
|
158
|
+
includeScope: lazyContext?.includeScope,
|
|
146
159
|
},
|
|
147
160
|
() => {
|
|
148
|
-
// Run the lazy patterns handler with the original context prefixes
|
|
149
|
-
// The prefix comes from the IncludeItem stored in lazyPatterns
|
|
161
|
+
// Run the lazy patterns handler with the original context prefixes.
|
|
162
|
+
// The prefix comes from the IncludeItem stored in lazyPatterns. Use the
|
|
163
|
+
// slash-collapsing join so a trailing-slash parent prefix does not bake a
|
|
164
|
+
// double slash into the registered route patterns (entry.routes,
|
|
165
|
+
// reverse(), EntryData.pattern, mountPath) when the handler runs.
|
|
150
166
|
const includePrefix = (entry as any)._lazyPrefix || "";
|
|
151
|
-
const fullPrefix = (lazyContext?.urlPrefix
|
|
167
|
+
const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
|
|
152
168
|
|
|
153
169
|
if (fullPrefix || lazyContext?.namePrefix) {
|
|
154
170
|
runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () => {
|
|
@@ -190,10 +206,13 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
190
206
|
// Detect nested lazy includes and register them as new entries
|
|
191
207
|
const nestedLazyIncludes = findLazyIncludes(handlerResult);
|
|
192
208
|
for (const lazyInclude of nestedLazyIncludes) {
|
|
193
|
-
// Compute the full URL prefix (combining parent prefix if any)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
// Compute the full URL prefix (combining parent prefix if any). Use the
|
|
210
|
+
// slash-collapsing join so a trailing-slash parent prefix does not produce
|
|
211
|
+
// a double-slash staticPrefix the trie's sp can never match.
|
|
212
|
+
const fullPrefix = joinPrefix(
|
|
213
|
+
lazyInclude.context.urlPrefix,
|
|
214
|
+
lazyInclude.prefix,
|
|
215
|
+
);
|
|
197
216
|
|
|
198
217
|
const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
|
|
199
218
|
prefix: "",
|
|
@@ -24,7 +24,12 @@ import { isHandle, collectHandleData, type Handle } from "../handle.js";
|
|
|
24
24
|
import { buildHandleSnapshot } from "../server/handle-store.js";
|
|
25
25
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
26
26
|
import { _getRequestContext } from "../server/request-context.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
isInsideLoaderScope,
|
|
29
|
+
runInsideLoaderBodyScope,
|
|
30
|
+
isInsidePushCallbackScope,
|
|
31
|
+
runInsidePushCallbackScope,
|
|
32
|
+
} from "../server/context.js";
|
|
28
33
|
import { debugLog } from "./logging.js";
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -266,7 +271,10 @@ function createLoaderExecutor<TEnv>(
|
|
|
266
271
|
search: (ctx as any).search,
|
|
267
272
|
pathname: ctx.pathname,
|
|
268
273
|
url: ctx.url,
|
|
274
|
+
originalUrl: ctx.originalUrl,
|
|
269
275
|
env: ctx.env,
|
|
276
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
277
|
+
executionContext: ctx.executionContext,
|
|
270
278
|
get: ((keyOrVar: any) =>
|
|
271
279
|
contextGet(variables, keyOrVar)) as typeof ctx.get,
|
|
272
280
|
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
@@ -284,6 +292,12 @@ function createLoaderExecutor<TEnv>(
|
|
|
284
292
|
);
|
|
285
293
|
}
|
|
286
294
|
const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
|
|
295
|
+
// The complete snapshot is cached at barrier resolution for
|
|
296
|
+
// non-streaming trees, and by rendered() after handleStore.settled for
|
|
297
|
+
// streaming trees (where the eager snapshot would have been incomplete
|
|
298
|
+
// because loading() handlers were still in flight). Either way it is
|
|
299
|
+
// present by the time a loader reads a handle; the fresh build is only
|
|
300
|
+
// a defensive fallback.
|
|
287
301
|
const snapshot =
|
|
288
302
|
reqCtx._renderBarrierHandleSnapshot ??
|
|
289
303
|
buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
|
|
@@ -305,15 +319,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
305
319
|
);
|
|
306
320
|
}
|
|
307
321
|
|
|
308
|
-
// Guard: reject streaming trees
|
|
309
322
|
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
323
|
|
|
318
324
|
if (renderedPromise) return renderedPromise;
|
|
319
325
|
|
|
@@ -324,7 +330,10 @@ function createLoaderExecutor<TEnv>(
|
|
|
324
330
|
}
|
|
325
331
|
|
|
326
332
|
// Bidirectional deadlock check: if a handler already started
|
|
327
|
-
// awaiting this loader, calling rendered() would deadlock.
|
|
333
|
+
// awaiting this loader, calling rendered() would deadlock. This is the
|
|
334
|
+
// real cycle guard (it holds for both streaming and non-streaming): the
|
|
335
|
+
// handler blocks segment resolution, which blocks the barrier, which
|
|
336
|
+
// blocks this loader.
|
|
328
337
|
if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
|
|
329
338
|
throw new Error(
|
|
330
339
|
`Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
|
|
@@ -342,7 +351,29 @@ function createLoaderExecutor<TEnv>(
|
|
|
342
351
|
}
|
|
343
352
|
reqCtx._renderBarrierWaiters.add(currentLoaderId);
|
|
344
353
|
|
|
345
|
-
|
|
354
|
+
// Streaming trees (loading()): the barrier resolves once the segment
|
|
355
|
+
// tree is resolved, but loading() handlers stream behind Suspense and
|
|
356
|
+
// their handle pushes are still in flight then. Their async execution
|
|
357
|
+
// IS tracked in the handle store (trackHandler -> store.track), so after
|
|
358
|
+
// the barrier we seal (no further handlers register once the tree is
|
|
359
|
+
// resolved) and wait for settled — every tracked handler, streaming
|
|
360
|
+
// included, has finished pushing. The loader's own segment streams in
|
|
361
|
+
// after, so this does not block the shell; the deadlock guard above
|
|
362
|
+
// keeps a handler from depending on this loader.
|
|
363
|
+
const streaming = reqCtx._treeHasStreaming === true;
|
|
364
|
+
renderedPromise = reqCtx._renderBarrier.then(async () => {
|
|
365
|
+
if (streaming) {
|
|
366
|
+
reqCtx._handleStore.seal();
|
|
367
|
+
await reqCtx._handleStore.settled;
|
|
368
|
+
// The eager snapshot was intentionally left unbuilt for streaming
|
|
369
|
+
// (it would have been incomplete). Build the complete one once, now
|
|
370
|
+
// that the store has settled, so every ctx.use(handle) reads the
|
|
371
|
+
// cached snapshot instead of rebuilding it per call.
|
|
372
|
+
reqCtx._renderBarrierHandleSnapshot ??= buildHandleSnapshot(
|
|
373
|
+
reqCtx._handleStore,
|
|
374
|
+
reqCtx._renderBarrierSegmentOrder ?? [],
|
|
375
|
+
);
|
|
376
|
+
}
|
|
346
377
|
renderedResolved = true;
|
|
347
378
|
});
|
|
348
379
|
return renderedPromise;
|
|
@@ -350,8 +381,19 @@ function createLoaderExecutor<TEnv>(
|
|
|
350
381
|
};
|
|
351
382
|
|
|
352
383
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
384
|
+
// Run the loader body inside loader scope so request-scoped reads
|
|
385
|
+
// (cookies()/headers() and non-cacheable ctx.get) are exempt from the
|
|
386
|
+
// cache-purity guards: loaders always run fresh, so their reads never leak
|
|
387
|
+
// into a cached segment. DSL loaders are already wrapped by fresh.ts; this
|
|
388
|
+
// also covers handler-invoked loaders (ctx.use(Loader) from a handler),
|
|
389
|
+
// which otherwise execute in the caller's cache scope and would wrongly
|
|
390
|
+
// throw. rendered() gating uses the captured isDslLoader (above), so this
|
|
391
|
+
// does not grant rendered() to handler-invoked loaders. Uses a body-only
|
|
392
|
+
// scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
|
|
353
393
|
const promise = Promise.resolve(
|
|
354
|
-
|
|
394
|
+
runInsideLoaderBodyScope(() =>
|
|
395
|
+
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
396
|
+
),
|
|
355
397
|
).finally(() => {
|
|
356
398
|
pendingLoaders.delete(loader.$$id);
|
|
357
399
|
doneLoader();
|
|
@@ -387,12 +429,6 @@ export function setupLoaderAccess<TEnv>(
|
|
|
387
429
|
|
|
388
430
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
389
431
|
|
|
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
|
-
|
|
396
432
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
397
433
|
if (isHandle(item)) {
|
|
398
434
|
const handle = item;
|
|
@@ -413,15 +449,17 @@ export function setupLoaderAccess<TEnv>(
|
|
|
413
449
|
if (!store) return;
|
|
414
450
|
|
|
415
451
|
if (typeof dataOrFn === "function") {
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
452
|
+
// Run the callback inside the push-callback scope so ctx.use(loader)
|
|
453
|
+
// calls it makes — including after its own awaits, for an async
|
|
454
|
+
// callback — are not registered as handler-to-loader deps and do not
|
|
455
|
+
// trip the deadlock guard. A pushed promise value is not tracked by
|
|
456
|
+
// handleStore.settled and does not block segment resolution, so it
|
|
457
|
+
// cannot form a rendered() deadlock. The ALS scope (not a plain
|
|
458
|
+
// boolean) is what survives the callback's awaits.
|
|
459
|
+
const result = runInsidePushCallbackScope(() =>
|
|
460
|
+
(dataOrFn as () => Promise<unknown>)(),
|
|
461
|
+
);
|
|
462
|
+
store.push(handle.$$id, segmentId, result);
|
|
425
463
|
return;
|
|
426
464
|
}
|
|
427
465
|
|
|
@@ -433,9 +471,12 @@ export function setupLoaderAccess<TEnv>(
|
|
|
433
471
|
// Skip when inside a DSL loader scope (resolveLoaderData also calls
|
|
434
472
|
// ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
|
|
435
473
|
// inside a handle push callback (push callbacks don't block segment
|
|
436
|
-
// resolution so they can't cause rendered() deadlocks).
|
|
474
|
+
// resolution so they can't cause rendered() deadlocks). The push-callback
|
|
475
|
+
// check is an ALS scope so it also exempts an ASYNC callback's continuation
|
|
476
|
+
// after its first await — relevant on streaming trees, where the guard
|
|
477
|
+
// state now stays live until handleStore.settled.
|
|
437
478
|
const loader = item as LoaderDefinition<any, any>;
|
|
438
|
-
if (!isInsideLoaderScope() && !
|
|
479
|
+
if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
|
|
439
480
|
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
440
481
|
if (reqCtx) {
|
|
441
482
|
// Direction 1: handler awaits loader that already called rendered()
|
|
@@ -449,13 +490,18 @@ export function setupLoaderAccess<TEnv>(
|
|
|
449
490
|
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
450
491
|
);
|
|
451
492
|
}
|
|
452
|
-
// Direction 2: track dep so rendered() can detect the deadlock
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
493
|
+
// Direction 2: track dep so rendered() can detect the deadlock if the
|
|
494
|
+
// loader calls it later. Skip once the guard window is CLOSED — for a
|
|
495
|
+
// non-streaming tree that is when the barrier resolves (rendered()
|
|
496
|
+
// resolves immediately), and for a streaming tree it is when
|
|
497
|
+
// handleStore.settled completes (rendered() keeps waiting until then, so
|
|
498
|
+
// a loading() handler resuming after the barrier can still form a
|
|
499
|
+
// cycle). Using the explicit guard-closed flag rather than
|
|
500
|
+
// _renderBarrierSegmentOrder keeps tracking live across the streaming
|
|
501
|
+
// settle wait. (Handle push callbacks are already excluded above via
|
|
502
|
+
// isInsidePushCallbackScope(), so they cannot produce false positives
|
|
503
|
+
// here.)
|
|
504
|
+
if (!reqCtx._renderBarrierGuardClosed) {
|
|
459
505
|
if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
|
|
460
506
|
reqCtx._handlerLoaderDeps.add(loader.$$id);
|
|
461
507
|
}
|