@rangojs/router 0.0.0-experimental.cb54cbba → 0.0.0-experimental.debug-cache-2383ca26
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/AGENTS.md +4 -0
- package/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +139 -200
- package/package.json +15 -14
- package/skills/caching/SKILL.md +37 -4
- package/skills/parallel/SKILL.md +126 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -3
- package/src/browser/navigation-client.ts +60 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +50 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +53 -13
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +27 -0
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +31 -34
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/types.ts +9 -0
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +43 -3
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/debug.ts +2 -2
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/redirect.ts +2 -2
- package/src/route-map-builder.ts +7 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +66 -9
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +8 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware.ts +2 -1
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +122 -15
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +347 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router.ts +5 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +90 -13
- package/src/server/request-context.ts +10 -4
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +103 -17
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +144 -209
- package/src/vite/router-discovery.ts +0 -8
- package/src/vite/utils/banner.ts +3 -3
|
@@ -123,7 +123,6 @@ export function withInterceptResolution<TEnv>(
|
|
|
123
123
|
return async function* (
|
|
124
124
|
source: AsyncGenerator<ResolvedSegment>,
|
|
125
125
|
): AsyncGenerator<ResolvedSegment> {
|
|
126
|
-
const pipelineStart = performance.now();
|
|
127
126
|
const ms = ctx.metricsStore;
|
|
128
127
|
|
|
129
128
|
// First, yield all segments from the source (main segment resolution or cache)
|
|
@@ -133,13 +132,16 @@ export function withInterceptResolution<TEnv>(
|
|
|
133
132
|
yield segment;
|
|
134
133
|
}
|
|
135
134
|
|
|
135
|
+
// Measure own work only (after source iteration completes)
|
|
136
|
+
const ownStart = performance.now();
|
|
137
|
+
|
|
136
138
|
// Skip intercept resolution for full match (document requests don't have intercepts)
|
|
137
139
|
if (ctx.isFullMatch) {
|
|
138
140
|
if (ms) {
|
|
139
141
|
ms.metrics.push({
|
|
140
142
|
label: "pipeline:intercept",
|
|
141
|
-
duration: performance.now() -
|
|
142
|
-
startTime:
|
|
143
|
+
duration: performance.now() - ownStart,
|
|
144
|
+
startTime: ownStart - ms.requestStart,
|
|
143
145
|
});
|
|
144
146
|
}
|
|
145
147
|
return;
|
|
@@ -163,8 +165,8 @@ export function withInterceptResolution<TEnv>(
|
|
|
163
165
|
if (ms) {
|
|
164
166
|
ms.metrics.push({
|
|
165
167
|
label: "pipeline:intercept",
|
|
166
|
-
duration: performance.now() -
|
|
167
|
-
startTime:
|
|
168
|
+
duration: performance.now() - ownStart,
|
|
169
|
+
startTime: ownStart - ms.requestStart,
|
|
168
170
|
});
|
|
169
171
|
}
|
|
170
172
|
return;
|
|
@@ -216,8 +218,8 @@ export function withInterceptResolution<TEnv>(
|
|
|
216
218
|
if (ms) {
|
|
217
219
|
ms.metrics.push({
|
|
218
220
|
label: "pipeline:intercept",
|
|
219
|
-
duration: performance.now() -
|
|
220
|
-
startTime:
|
|
221
|
+
duration: performance.now() - ownStart,
|
|
222
|
+
startTime: ownStart - ms.requestStart,
|
|
221
223
|
});
|
|
222
224
|
}
|
|
223
225
|
};
|
|
@@ -104,7 +104,6 @@ export function withSegmentResolution<TEnv>(
|
|
|
104
104
|
return async function* (
|
|
105
105
|
source: AsyncGenerator<ResolvedSegment>,
|
|
106
106
|
): AsyncGenerator<ResolvedSegment> {
|
|
107
|
-
const pipelineStart = performance.now();
|
|
108
107
|
const ms = ctx.metricsStore;
|
|
109
108
|
|
|
110
109
|
// IMPORTANT: Always iterate source first to give cache-lookup a chance
|
|
@@ -113,13 +112,16 @@ export function withSegmentResolution<TEnv>(
|
|
|
113
112
|
yield segment;
|
|
114
113
|
}
|
|
115
114
|
|
|
115
|
+
// Measure own work only (after source iteration completes)
|
|
116
|
+
const ownStart = performance.now();
|
|
117
|
+
|
|
116
118
|
// If cache hit, segments were already yielded by cache lookup
|
|
117
119
|
if (state.cacheHit) {
|
|
118
120
|
if (ms) {
|
|
119
121
|
ms.metrics.push({
|
|
120
122
|
label: "pipeline:segment-resolve",
|
|
121
|
-
duration: performance.now() -
|
|
122
|
-
startTime:
|
|
123
|
+
duration: performance.now() - ownStart,
|
|
124
|
+
startTime: ownStart - ms.requestStart,
|
|
123
125
|
});
|
|
124
126
|
}
|
|
125
127
|
return;
|
|
@@ -168,6 +170,7 @@ export function withSegmentResolution<TEnv>(
|
|
|
168
170
|
ctx.interceptResult,
|
|
169
171
|
ctx.localRouteName,
|
|
170
172
|
ctx.pathname,
|
|
173
|
+
ctx.stale,
|
|
171
174
|
),
|
|
172
175
|
);
|
|
173
176
|
|
|
@@ -184,8 +187,8 @@ export function withSegmentResolution<TEnv>(
|
|
|
184
187
|
if (ms) {
|
|
185
188
|
ms.metrics.push({
|
|
186
189
|
label: "pipeline:segment-resolve",
|
|
187
|
-
duration: performance.now() -
|
|
188
|
-
startTime:
|
|
190
|
+
duration: performance.now() - ownStart,
|
|
191
|
+
startTime: ownStart - ms.requestStart,
|
|
189
192
|
});
|
|
190
193
|
}
|
|
191
194
|
};
|
|
@@ -67,10 +67,11 @@
|
|
|
67
67
|
* Keep if:
|
|
68
68
|
* - component !== null (needs rendering)
|
|
69
69
|
* - type === "loader" (carries data even with null component)
|
|
70
|
+
* - client doesn't have the segment (structurally required parent node)
|
|
70
71
|
*
|
|
71
72
|
* Skip if:
|
|
72
|
-
* - component === null AND type !== "loader"
|
|
73
|
-
* - (
|
|
73
|
+
* - component === null AND type !== "loader" AND client has it cached
|
|
74
|
+
* - (Revalidation skip — client already has this segment's UI)
|
|
74
75
|
*
|
|
75
76
|
*
|
|
76
77
|
* INTERCEPT HANDLING
|
|
@@ -109,6 +110,7 @@
|
|
|
109
110
|
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
111
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
112
|
import { debugLog } from "./logging.js";
|
|
113
|
+
import { appendMetric } from "./metrics.js";
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* Collect all segments from an async generator
|
|
@@ -167,10 +169,15 @@ export function buildMatchResult<TEnv>(
|
|
|
167
169
|
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
168
170
|
allIds = [...new Set(allIds)];
|
|
169
171
|
|
|
170
|
-
// Filter out segments
|
|
171
|
-
//
|
|
172
|
+
// Filter out null-component segments only when the client already has
|
|
173
|
+
// them cached (revalidation skip). If the client doesn't have the segment,
|
|
174
|
+
// it must be included even with null component — it's structurally required
|
|
175
|
+
// as a parent node for child layouts/parallels to reconcile against.
|
|
176
|
+
// Loader segments are always included as they carry data.
|
|
177
|
+
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
172
178
|
segmentsToRender = allSegments.filter(
|
|
173
|
-
(s) =>
|
|
179
|
+
(s) =>
|
|
180
|
+
s.component !== null || s.type === "loader" || !clientIdSet.has(s.id),
|
|
174
181
|
);
|
|
175
182
|
}
|
|
176
183
|
|
|
@@ -210,10 +217,19 @@ export async function collectMatchResult<TEnv>(
|
|
|
210
217
|
): Promise<MatchResult> {
|
|
211
218
|
const allSegments = await collectSegments(pipeline);
|
|
212
219
|
|
|
220
|
+
const buildStart = performance.now();
|
|
221
|
+
|
|
213
222
|
// Update state with collected segments if not already set
|
|
214
223
|
if (state.segments.length === 0) {
|
|
215
224
|
state.segments = allSegments;
|
|
216
225
|
}
|
|
217
226
|
|
|
218
|
-
|
|
227
|
+
const result = buildMatchResult(allSegments, ctx, state);
|
|
228
|
+
appendMetric(
|
|
229
|
+
ctx.metricsStore,
|
|
230
|
+
"collect-result",
|
|
231
|
+
buildStart,
|
|
232
|
+
performance.now() - buildStart,
|
|
233
|
+
);
|
|
234
|
+
return result;
|
|
219
235
|
}
|
package/src/router/metrics.ts
CHANGED
|
@@ -15,7 +15,12 @@ function formatMs(value: number): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
|
|
18
|
-
return [...metrics].sort((a, b) =>
|
|
18
|
+
return [...metrics].sort((a, b) => {
|
|
19
|
+
// handler:total always goes last (it wraps everything)
|
|
20
|
+
if (a.label === "handler:total") return 1;
|
|
21
|
+
if (b.label === "handler:total") return -1;
|
|
22
|
+
return a.startTime - b.startTime;
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface Span {
|
package/src/router/middleware.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
import { _getRequestContext } from "../server/request-context.js";
|
|
22
22
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
23
|
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
24
|
+
import { stripInternalParams } from "./handler-context.js";
|
|
24
25
|
|
|
25
26
|
// Re-export types and cookie utilities for backward compatibility
|
|
26
27
|
export type {
|
|
@@ -147,7 +148,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
147
148
|
search?: Record<string, unknown>,
|
|
148
149
|
) => string,
|
|
149
150
|
): MiddlewareContext<TEnv> {
|
|
150
|
-
const url = new URL(request.url);
|
|
151
|
+
const url = stripInternalParams(new URL(request.url));
|
|
151
152
|
|
|
152
153
|
// Track the initial response to detect pre/post-next() phase.
|
|
153
154
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -138,6 +138,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
138
138
|
interceptResult: InterceptResult | null,
|
|
139
139
|
localRouteName: string,
|
|
140
140
|
pathname: string,
|
|
141
|
+
stale?: boolean,
|
|
141
142
|
) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
|
|
142
143
|
|
|
143
144
|
// Generator-based segment resolution (for pipeline)
|
|
@@ -188,7 +189,10 @@ export interface RouterContext<TEnv = any> {
|
|
|
188
189
|
| "cache-hit"
|
|
189
190
|
| "loader"
|
|
190
191
|
| "parallel"
|
|
191
|
-
| "orphan-layout"
|
|
192
|
+
| "orphan-layout"
|
|
193
|
+
| "route-handler"
|
|
194
|
+
| "layout-handler"
|
|
195
|
+
| "intercept-loader";
|
|
192
196
|
}) => Promise<boolean>;
|
|
193
197
|
|
|
194
198
|
// Request context
|
|
@@ -206,6 +210,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
206
210
|
params: Record<string, string>,
|
|
207
211
|
handlerContext: HandlerContext<any, TEnv>,
|
|
208
212
|
loaderPromises: Map<string, Promise<any>>,
|
|
213
|
+
options?: { skipLoaders?: boolean },
|
|
209
214
|
) => Promise<ResolvedSegment[]>;
|
|
210
215
|
|
|
211
216
|
// Generator-based simple resolution
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ReactNode } from "react";
|
|
9
9
|
import { invariant } from "../../errors";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
getParallelEntries,
|
|
12
|
+
getParallelSlotEntries,
|
|
13
|
+
type EntryData,
|
|
14
|
+
} from "../../server/context";
|
|
11
15
|
import type {
|
|
12
16
|
HandlerContext,
|
|
13
17
|
InternalHandlerContext,
|
|
@@ -15,6 +19,8 @@ import type {
|
|
|
15
19
|
} from "../../types";
|
|
16
20
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
17
21
|
import { resolveLoaderData } from "./loader-cache.js";
|
|
22
|
+
import { _getRequestContext } from "../../server/request-context.js";
|
|
23
|
+
import { appendMetric } from "../metrics.js";
|
|
18
24
|
import {
|
|
19
25
|
handleHandlerResult,
|
|
20
26
|
tryStaticHandler,
|
|
@@ -90,8 +96,12 @@ export async function resolveLoaders<TEnv>(
|
|
|
90
96
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
91
97
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
92
98
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
99
|
+
const ms = _getRequestContext()?._metricsStore;
|
|
93
100
|
|
|
94
101
|
if (!loadingDisabled) {
|
|
102
|
+
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
103
|
+
// No per-loader timing here — settlement happens asynchronously during
|
|
104
|
+
// RSC/SSR stream consumption, after the perf timeline is logged.
|
|
95
105
|
return loaderEntries.map((loaderEntry, i) => {
|
|
96
106
|
const { loader } = loaderEntry;
|
|
97
107
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
@@ -116,14 +126,31 @@ export async function resolveLoaders<TEnv>(
|
|
|
116
126
|
|
|
117
127
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
118
128
|
// settled promises so handlers don't stream loading placeholders.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
// We can measure actual execution time here since we await all loaders.
|
|
130
|
+
const pendingLoaderData = loaderEntries.map((loaderEntry) => {
|
|
131
|
+
const start = performance.now();
|
|
132
|
+
const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
|
|
133
|
+
return { promise, start, loaderId: loaderEntry.loader.$$id };
|
|
134
|
+
});
|
|
135
|
+
await Promise.all(pendingLoaderData.map((p) => p.promise));
|
|
123
136
|
|
|
124
137
|
return loaderEntries.map((loaderEntry, i) => {
|
|
125
138
|
const { loader } = loaderEntry;
|
|
126
139
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
140
|
+
const pending = pendingLoaderData[i]!;
|
|
141
|
+
if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
|
|
142
|
+
// All loaders ran in parallel via Promise.all — each span covers
|
|
143
|
+
// from its own kickoff to the batch settlement, giving a ceiling
|
|
144
|
+
// on that loader's contribution to the overall wait.
|
|
145
|
+
const batchEnd = performance.now();
|
|
146
|
+
appendMetric(
|
|
147
|
+
ms,
|
|
148
|
+
`loader:${loader.$$id}`,
|
|
149
|
+
pending.start,
|
|
150
|
+
batchEnd - pending.start,
|
|
151
|
+
2,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
127
154
|
return {
|
|
128
155
|
id: segmentId,
|
|
129
156
|
namespace: entry.id,
|
|
@@ -133,7 +160,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
133
160
|
params: ctx.params,
|
|
134
161
|
loaderId: loader.$$id,
|
|
135
162
|
loaderData: deps.wrapLoaderPromise(
|
|
136
|
-
|
|
163
|
+
pending.promise,
|
|
137
164
|
entry,
|
|
138
165
|
segmentId,
|
|
139
166
|
ctx.pathname,
|
|
@@ -197,7 +224,10 @@ export async function resolveSegment<TEnv>(
|
|
|
197
224
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
198
225
|
});
|
|
199
226
|
|
|
200
|
-
|
|
227
|
+
const resolvedParallelEntries = new Set<string>();
|
|
228
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
229
|
+
entry.parallel,
|
|
230
|
+
)) {
|
|
201
231
|
const parallelSegments = await resolveParallelEntry(
|
|
202
232
|
parallelEntry,
|
|
203
233
|
params,
|
|
@@ -207,8 +237,11 @@ export async function resolveSegment<TEnv>(
|
|
|
207
237
|
deps,
|
|
208
238
|
options,
|
|
209
239
|
routeKey,
|
|
240
|
+
[slot],
|
|
241
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
210
242
|
);
|
|
211
243
|
segments.push(...parallelSegments);
|
|
244
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
212
245
|
}
|
|
213
246
|
|
|
214
247
|
for (const orphan of entry.layout) {
|
|
@@ -286,7 +319,10 @@ export async function resolveSegment<TEnv>(
|
|
|
286
319
|
segments.push(...orphanSegments);
|
|
287
320
|
}
|
|
288
321
|
|
|
289
|
-
|
|
322
|
+
const resolvedParallelEntries = new Set<string>();
|
|
323
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
324
|
+
entry.parallel,
|
|
325
|
+
)) {
|
|
290
326
|
const parallelSegments = await resolveParallelEntry(
|
|
291
327
|
parallelEntry,
|
|
292
328
|
params,
|
|
@@ -296,8 +332,11 @@ export async function resolveSegment<TEnv>(
|
|
|
296
332
|
deps,
|
|
297
333
|
options,
|
|
298
334
|
routeKey,
|
|
335
|
+
[slot],
|
|
336
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
299
337
|
);
|
|
300
338
|
segments.push(...parallelSegments);
|
|
339
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
301
340
|
}
|
|
302
341
|
|
|
303
342
|
segments.push({
|
|
@@ -305,7 +344,7 @@ export async function resolveSegment<TEnv>(
|
|
|
305
344
|
namespace: entry.id,
|
|
306
345
|
type: "route",
|
|
307
346
|
index: 0,
|
|
308
|
-
component,
|
|
347
|
+
component: component ?? null,
|
|
309
348
|
loading: entry.loading === false ? null : entry.loading,
|
|
310
349
|
transition: entry.transition,
|
|
311
350
|
params,
|
|
@@ -368,7 +407,10 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
368
407
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
369
408
|
});
|
|
370
409
|
|
|
371
|
-
|
|
410
|
+
const resolvedParallelEntries = new Set<string>();
|
|
411
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
412
|
+
orphan.parallel,
|
|
413
|
+
)) {
|
|
372
414
|
const parallelSegments = await resolveParallelEntry(
|
|
373
415
|
parallelEntry,
|
|
374
416
|
params,
|
|
@@ -378,8 +420,11 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
378
420
|
deps,
|
|
379
421
|
options,
|
|
380
422
|
routeKey,
|
|
423
|
+
[slot],
|
|
424
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
381
425
|
);
|
|
382
426
|
segments.push(...parallelSegments);
|
|
427
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
383
428
|
}
|
|
384
429
|
|
|
385
430
|
return segments;
|
|
@@ -397,6 +442,8 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
397
442
|
deps: SegmentResolutionDeps<TEnv>,
|
|
398
443
|
options?: ResolveSegmentOptions,
|
|
399
444
|
routeKey?: string,
|
|
445
|
+
slotNames?: `@${string}`[],
|
|
446
|
+
includeLoaders: boolean = true,
|
|
400
447
|
): Promise<ResolvedSegment[]> {
|
|
401
448
|
invariant(
|
|
402
449
|
parallelEntry.type === "parallel",
|
|
@@ -411,7 +458,12 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
411
458
|
| ReactNode
|
|
412
459
|
>;
|
|
413
460
|
|
|
414
|
-
|
|
461
|
+
const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
|
|
462
|
+
|
|
463
|
+
for (const slot of slotsToResolve) {
|
|
464
|
+
// Try static lookup first — in production, handler bodies are evicted
|
|
465
|
+
// and replaced with stubs that have no .handler property (undefined).
|
|
466
|
+
// The static store holds the pre-rendered component for these slots.
|
|
415
467
|
let component: ReactNode | undefined = await tryStaticSlot(
|
|
416
468
|
parallelEntry,
|
|
417
469
|
slot,
|
|
@@ -419,6 +471,10 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
419
471
|
);
|
|
420
472
|
|
|
421
473
|
if (component === undefined) {
|
|
474
|
+
const handler = slots[slot];
|
|
475
|
+
if (handler === undefined) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
422
478
|
const doneParallelHandler = track(
|
|
423
479
|
`handler:${parallelEntry.id}.${slot}`,
|
|
424
480
|
2,
|
|
@@ -472,7 +528,7 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
472
528
|
});
|
|
473
529
|
}
|
|
474
530
|
|
|
475
|
-
if (!
|
|
531
|
+
if (!options?.skipLoaders && includeLoaders) {
|
|
476
532
|
const loaderSegments = await resolveLoaders(
|
|
477
533
|
parallelEntry,
|
|
478
534
|
context,
|
|
@@ -480,6 +536,15 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
480
536
|
deps,
|
|
481
537
|
parentShortCode,
|
|
482
538
|
);
|
|
539
|
+
// Tag parallel-owned loaders so renderSegments can stream them
|
|
540
|
+
// using the parallel's loading() instead of awaiting on the layout
|
|
541
|
+
const parallelLoading =
|
|
542
|
+
parallelEntry.loading === false ? undefined : parallelEntry.loading;
|
|
543
|
+
if (parallelLoading) {
|
|
544
|
+
for (const seg of loaderSegments) {
|
|
545
|
+
seg.parallelLoading = parallelLoading;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
483
548
|
segments.push(...loaderSegments);
|
|
484
549
|
}
|
|
485
550
|
|
|
@@ -559,11 +624,53 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
559
624
|
deps: SegmentResolutionDeps<TEnv>,
|
|
560
625
|
): Promise<ResolvedSegment[]> {
|
|
561
626
|
const loaderSegments: ResolvedSegment[] = [];
|
|
627
|
+
const seenIds = new Set<string>();
|
|
628
|
+
|
|
629
|
+
async function collectEntryLoaders(
|
|
630
|
+
entry: EntryData,
|
|
631
|
+
belongsToRoute: boolean,
|
|
632
|
+
shortCodeOverride?: string,
|
|
633
|
+
): Promise<void> {
|
|
634
|
+
// Skip if all loaders from this entry have already been resolved
|
|
635
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
636
|
+
const entryLoaders = entry.loader ?? [];
|
|
637
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
638
|
+
const allAlreadySeen =
|
|
639
|
+
entryLoaders.length > 0 &&
|
|
640
|
+
entryLoaders.every((le, i) =>
|
|
641
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
642
|
+
);
|
|
643
|
+
if (!allAlreadySeen) {
|
|
644
|
+
const segments = await resolveLoaders(
|
|
645
|
+
entry,
|
|
646
|
+
context,
|
|
647
|
+
belongsToRoute,
|
|
648
|
+
deps,
|
|
649
|
+
shortCodeOverride,
|
|
650
|
+
);
|
|
651
|
+
for (const seg of segments) {
|
|
652
|
+
if (!seenIds.has(seg.id)) {
|
|
653
|
+
seenIds.add(seg.id);
|
|
654
|
+
loaderSegments.push(seg);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const seenParallelEntryIds = new Set<string>();
|
|
660
|
+
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
661
|
+
if (seenParallelEntryIds.has(parallelEntry.id)) continue;
|
|
662
|
+
seenParallelEntryIds.add(parallelEntry.id);
|
|
663
|
+
await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
667
|
+
for (const layoutEntry of entry.layout) {
|
|
668
|
+
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
562
671
|
|
|
563
672
|
for (const entry of entries) {
|
|
564
|
-
|
|
565
|
-
const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
|
|
566
|
-
loaderSegments.push(...segments);
|
|
673
|
+
await collectEntryLoaders(entry, entry.type === "route");
|
|
567
674
|
}
|
|
568
675
|
|
|
569
676
|
return loaderSegments;
|
|
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
const loaderId = loaderEntry.loader.$$id;
|
|
150
|
+
|
|
150
151
|
const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
|
|
151
152
|
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
152
153
|
const swr = swrWindow || undefined;
|