@rangojs/router 0.22.1 → 0.23.0
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/bin/rango.js +8 -3
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +19 -9
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +16 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +14 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -3
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +38 -0
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +11 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +842 -308
- package/package.json +1 -1
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +37 -10
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache.ts +22 -1
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/handle-store.ts +104 -8
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +20 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +93 -32
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getParallelEntries,
|
|
15
15
|
getParallelSlotEntries,
|
|
16
16
|
type EntryData,
|
|
17
|
+
type LoaderEntry,
|
|
17
18
|
type ParallelEntryData,
|
|
18
19
|
} from "../../server/context";
|
|
19
20
|
import type {
|
|
@@ -34,6 +35,13 @@ import {
|
|
|
34
35
|
} from "../logging.js";
|
|
35
36
|
import { resolveLoaderData } from "./loader-cache.js";
|
|
36
37
|
import { entryLoadingMasksLoaders } from "./loader-mask.js";
|
|
38
|
+
import {
|
|
39
|
+
defersLoader,
|
|
40
|
+
deferUnit,
|
|
41
|
+
getPrefetchDeferral,
|
|
42
|
+
isFillRequest,
|
|
43
|
+
registerDeferredLoader,
|
|
44
|
+
} from "./prefetch-deferral.js";
|
|
37
45
|
import {
|
|
38
46
|
handleHandlerResult,
|
|
39
47
|
warnOnStreamedResponse,
|
|
@@ -102,6 +110,45 @@ function emitRevalidationDecision(
|
|
|
102
110
|
});
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// prefetch: false (prefetch-deferral.ts)
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* How to deliver a loader a prefetch deferred, kept so deliverStartedLoaders
|
|
119
|
+
* can deliver it after all. Keyed by the emitted segment object.
|
|
120
|
+
*/
|
|
121
|
+
const deferredLoaderRuns = new WeakMap<ResolvedSegment, () => unknown>();
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A deferred loader something started in this request (a handler that awaits
|
|
125
|
+
* it with ctx.use(), or another loader) ran in the prefetch whatever its flag
|
|
126
|
+
* says: deliver that run instead of having the fill run it a second time. A
|
|
127
|
+
* reader starts it through its registration (prefetch-deferral.ts
|
|
128
|
+
* startDeferredLoader), so the delivery's resolveLoaderData finds the same
|
|
129
|
+
* run or cache() binding. Called once resolution has awaited every handler it
|
|
130
|
+
* awaits; a streamed handler that reads the loader later is not seen, and
|
|
131
|
+
* that loader runs again in the fill.
|
|
132
|
+
*/
|
|
133
|
+
function deliverStartedLoaders<TEnv>(
|
|
134
|
+
ctx: HandlerContext<any, TEnv>,
|
|
135
|
+
segments: ResolvedSegment[],
|
|
136
|
+
): void {
|
|
137
|
+
const plan = getPrefetchDeferral(ctx);
|
|
138
|
+
if (plan?.mode !== "prefetch") return;
|
|
139
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
140
|
+
const started = (loaderId: string): boolean =>
|
|
141
|
+
plan.deferredLoaders?.get(loaderId)?.started === true ||
|
|
142
|
+
internal._loaderStarted?.(loaderId) === true;
|
|
143
|
+
for (const segment of segments) {
|
|
144
|
+
if (!segment.deferred || segment.type !== "loader") continue;
|
|
145
|
+
const run = deferredLoaderRuns.get(segment);
|
|
146
|
+
if (!run || !started(segment.loaderId!)) continue;
|
|
147
|
+
delete segment.deferred;
|
|
148
|
+
segment.loaderData = run();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
105
152
|
// ---------------------------------------------------------------------------
|
|
106
153
|
// Revalidation path (partial match)
|
|
107
154
|
// ---------------------------------------------------------------------------
|
|
@@ -147,6 +194,7 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
147
194
|
}));
|
|
148
195
|
|
|
149
196
|
const matchedIds = loaderMeta.map((m) => m.segmentId);
|
|
197
|
+
const fill = isFillRequest(ctx);
|
|
150
198
|
|
|
151
199
|
const revalidationChecks = await Promise.all(
|
|
152
200
|
loaderMeta.map(
|
|
@@ -173,6 +221,8 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
173
221
|
}
|
|
174
222
|
return true;
|
|
175
223
|
}
|
|
224
|
+
// Fill: a held segment is skipped outright, no predicate runs.
|
|
225
|
+
if (fill) return false;
|
|
176
226
|
|
|
177
227
|
const dummySegment: ResolvedSegment = {
|
|
178
228
|
id: segmentId,
|
|
@@ -226,35 +276,72 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
226
276
|
// (matchPartialWithPprReplay) seeds once its doc record hits.
|
|
227
277
|
const bakeLane = !entryLoadingMasksLoaders(entry.loading);
|
|
228
278
|
|
|
279
|
+
const kickoff = (
|
|
280
|
+
loaderEntry: LoaderEntry,
|
|
281
|
+
segmentId: string,
|
|
282
|
+
loaderId: string,
|
|
283
|
+
): Promise<unknown> =>
|
|
284
|
+
runInsideLoaderScope(
|
|
285
|
+
() =>
|
|
286
|
+
resolveLoaderData(
|
|
287
|
+
loaderEntry,
|
|
288
|
+
ctx,
|
|
289
|
+
ctx.pathname,
|
|
290
|
+
// `bake`, not awaitBeforeFlush: a navigation evaluation never
|
|
291
|
+
// carries awaitBeforeFlush, and an ssr: false loader must pin
|
|
292
|
+
// here exactly as on the document HIT.
|
|
293
|
+
bakeLane || loaderEntry.bake === true ? segmentId : null,
|
|
294
|
+
),
|
|
295
|
+
loaderId,
|
|
296
|
+
);
|
|
297
|
+
const start = (
|
|
298
|
+
loaderEntry: LoaderEntry,
|
|
299
|
+
segmentId: string,
|
|
300
|
+
loaderId: string,
|
|
301
|
+
) =>
|
|
302
|
+
deps.wrapLoaderPromise(
|
|
303
|
+
kickoff(loaderEntry, segmentId, loaderId),
|
|
304
|
+
entry,
|
|
305
|
+
segmentId,
|
|
306
|
+
ctx.pathname,
|
|
307
|
+
errorContext,
|
|
308
|
+
);
|
|
309
|
+
|
|
229
310
|
const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
|
|
230
311
|
const segments: ResolvedSegment[] = loadersToRun.map(
|
|
231
|
-
({ loaderEntry, loader, segmentId, index }) =>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
312
|
+
({ loaderEntry, loader, segmentId, index }) => {
|
|
313
|
+
const base = {
|
|
314
|
+
id: segmentId,
|
|
315
|
+
namespace: entry.id,
|
|
316
|
+
type: "loader" as const,
|
|
317
|
+
index,
|
|
318
|
+
component: null,
|
|
319
|
+
params: ctx.params,
|
|
320
|
+
loaderId: loader.$$id,
|
|
321
|
+
};
|
|
322
|
+
// Only a loader on a segment the client lacks (defersLoader). It
|
|
323
|
+
// would have run. Registered, not started: it runs, and reads its
|
|
324
|
+
// cache(), only if a reader starts it (startDeferredLoader).
|
|
325
|
+
if (defersLoader(ctx, loaderEntry, segmentId)) {
|
|
326
|
+
registerDeferredLoader(ctx, loader.$$id, shortCode, () =>
|
|
327
|
+
kickoff(loaderEntry, segmentId, loader.$$id),
|
|
328
|
+
);
|
|
329
|
+
const deferred: ResolvedSegment = {
|
|
330
|
+
...base,
|
|
331
|
+
belongsToRoute,
|
|
332
|
+
deferred: true,
|
|
333
|
+
};
|
|
334
|
+
deferredLoaderRuns.set(deferred, () =>
|
|
335
|
+
start(loaderEntry, segmentId, loader.$$id),
|
|
336
|
+
);
|
|
337
|
+
return deferred;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
...base,
|
|
341
|
+
loaderData: start(loaderEntry, segmentId, loader.$$id),
|
|
342
|
+
belongsToRoute,
|
|
343
|
+
};
|
|
344
|
+
},
|
|
258
345
|
);
|
|
259
346
|
|
|
260
347
|
return { segments, matchedIds };
|
|
@@ -372,6 +459,7 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
372
459
|
await collectEntryLoaders(entry, entry.type === "route");
|
|
373
460
|
}
|
|
374
461
|
|
|
462
|
+
deliverStartedLoaders(context, allLoaderSegments);
|
|
375
463
|
return { segments: allLoaderSegments, matchedIds: allMatchedIds };
|
|
376
464
|
}
|
|
377
465
|
|
|
@@ -612,8 +700,17 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
612
700
|
// on the next request, perpetuating the staleness).
|
|
613
701
|
matchedIds.push(parallelId);
|
|
614
702
|
|
|
703
|
+
// A slot below a unit a prefetch skipped goes with it, and no predicate
|
|
704
|
+
// runs: a prefetch sends what the slot declares instead of running its
|
|
705
|
+
// handler.
|
|
706
|
+
let deferred = deferUnit(context, parallelId, "below");
|
|
615
707
|
let shouldResolve: boolean;
|
|
616
|
-
if (
|
|
708
|
+
if (isFillRequest(context)) {
|
|
709
|
+
// Fill: only what the client does not hold renders, no predicate runs.
|
|
710
|
+
shouldResolve = !clientSegmentIds.has(parallelId);
|
|
711
|
+
} else if (deferred) {
|
|
712
|
+
shouldResolve = true;
|
|
713
|
+
} else if (isFullRefetch) {
|
|
617
714
|
// Client has nothing cached — slot MUST render. User revalidate fns are
|
|
618
715
|
// bypassed here because returning false would leave the segment blank
|
|
619
716
|
// with no client-side fallback.
|
|
@@ -687,18 +784,22 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
687
784
|
shouldResolve,
|
|
688
785
|
);
|
|
689
786
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
787
|
+
// A slot with its own flagged loading() is its own unit.
|
|
788
|
+
if (shouldResolve) deferred ??= deferUnit(context, parallelId, "slot");
|
|
789
|
+
const { component, handlerRan } = deferred
|
|
790
|
+
? { component: null, handlerRan: false }
|
|
791
|
+
: await resolveParallelSlotComponent({
|
|
792
|
+
shouldResolve,
|
|
793
|
+
parallelEntry,
|
|
794
|
+
slot,
|
|
795
|
+
parallelId,
|
|
796
|
+
handler,
|
|
797
|
+
context,
|
|
798
|
+
deps,
|
|
799
|
+
routeKey,
|
|
800
|
+
params,
|
|
801
|
+
boundaryEntry: options?.boundaryEntry ?? entry,
|
|
802
|
+
});
|
|
702
803
|
|
|
703
804
|
segments.push({
|
|
704
805
|
id: parallelId,
|
|
@@ -719,6 +820,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
719
820
|
...(parallelEntry.mountPath
|
|
720
821
|
? { mountPath: parallelEntry.mountPath }
|
|
721
822
|
: {}),
|
|
823
|
+
...deferred,
|
|
722
824
|
});
|
|
723
825
|
|
|
724
826
|
if (loaderOrder === "after") {
|
|
@@ -750,6 +852,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
750
852
|
const matchedId = entry.shortCode;
|
|
751
853
|
|
|
752
854
|
let handlerRan = false;
|
|
855
|
+
let deferred: { readonly deferred: true } | undefined;
|
|
753
856
|
const component = await revalidate(
|
|
754
857
|
async () => {
|
|
755
858
|
const hasSegment = clientSegmentIds.has(entry.shortCode);
|
|
@@ -777,6 +880,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
777
880
|
}
|
|
778
881
|
return true;
|
|
779
882
|
}
|
|
883
|
+
// Fill: a held segment is skipped outright, no predicate runs.
|
|
884
|
+
if (isFillRequest(context)) return false;
|
|
780
885
|
|
|
781
886
|
const dummySegment: ResolvedSegment = {
|
|
782
887
|
id: entry.shortCode,
|
|
@@ -826,6 +931,10 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
826
931
|
return shouldRevalidate;
|
|
827
932
|
},
|
|
828
933
|
async () => {
|
|
934
|
+
// Only an entry the client lacks (defersUnit). Its handler would
|
|
935
|
+
// have run, and a prefetch sends the entry's fallback in its place.
|
|
936
|
+
deferred = deferUnit(context, entry.shortCode, "chain");
|
|
937
|
+
if (deferred) return null;
|
|
829
938
|
handlerRan = true;
|
|
830
939
|
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
831
940
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
@@ -928,6 +1037,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
928
1037
|
: {}),
|
|
929
1038
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
930
1039
|
_handlerRan: handlerRan,
|
|
1040
|
+
...deferred,
|
|
931
1041
|
};
|
|
932
1042
|
|
|
933
1043
|
return { segment, matchedId };
|
|
@@ -1020,10 +1130,8 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1020
1130
|
}
|
|
1021
1131
|
}
|
|
1022
1132
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
// (handler data visible) then push handler segment last for tree order.
|
|
1026
|
-
const parallelResult = await resolveParallelSegmentsWithRevalidation(
|
|
1133
|
+
const resolveSlots = () =>
|
|
1134
|
+
resolveParallelSegmentsWithRevalidation(
|
|
1027
1135
|
entry,
|
|
1028
1136
|
params,
|
|
1029
1137
|
context,
|
|
@@ -1038,6 +1146,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1038
1146
|
actionContext,
|
|
1039
1147
|
stale,
|
|
1040
1148
|
);
|
|
1149
|
+
|
|
1150
|
+
if (routeHandlerResult) {
|
|
1151
|
+
// Route entry: handler already executed above; resolve parallels
|
|
1152
|
+
// (handler data visible) then push handler segment last for tree order.
|
|
1153
|
+
const parallelResult = await resolveSlots();
|
|
1041
1154
|
segments.push(...parallelResult.segments);
|
|
1042
1155
|
matchedIds.push(...parallelResult.matchedIds);
|
|
1043
1156
|
|
|
@@ -1064,21 +1177,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1064
1177
|
segments.push(handlerResult.segment);
|
|
1065
1178
|
matchedIds.push(handlerResult.matchedId);
|
|
1066
1179
|
|
|
1067
|
-
const parallelResult = await
|
|
1068
|
-
entry,
|
|
1069
|
-
params,
|
|
1070
|
-
context,
|
|
1071
|
-
belongsToRoute,
|
|
1072
|
-
clientSegmentIds,
|
|
1073
|
-
prevParams,
|
|
1074
|
-
request,
|
|
1075
|
-
prevUrl,
|
|
1076
|
-
nextUrl,
|
|
1077
|
-
routeKey,
|
|
1078
|
-
deps,
|
|
1079
|
-
actionContext,
|
|
1080
|
-
stale,
|
|
1081
|
-
);
|
|
1180
|
+
const parallelResult = await resolveSlots();
|
|
1082
1181
|
segments.push(...parallelResult.segments);
|
|
1083
1182
|
matchedIds.push(...parallelResult.matchedIds);
|
|
1084
1183
|
|
|
@@ -1192,6 +1291,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1192
1291
|
// so ctx.set() values are visible to parallel children.
|
|
1193
1292
|
matchedIds.push(orphan.shortCode);
|
|
1194
1293
|
|
|
1294
|
+
let deferred: { readonly deferred: true } | undefined;
|
|
1195
1295
|
const component = await revalidate(
|
|
1196
1296
|
async () => {
|
|
1197
1297
|
if (!clientSegmentIds.has(orphan.shortCode)) {
|
|
@@ -1208,6 +1308,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1208
1308
|
}
|
|
1209
1309
|
return true;
|
|
1210
1310
|
}
|
|
1311
|
+
// Fill: a held segment is skipped outright, no predicate runs.
|
|
1312
|
+
if (isFillRequest(context)) return false;
|
|
1211
1313
|
|
|
1212
1314
|
const dummySegment: ResolvedSegment = {
|
|
1213
1315
|
id: orphan.shortCode,
|
|
@@ -1246,7 +1348,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1246
1348
|
);
|
|
1247
1349
|
return shouldRevalidate;
|
|
1248
1350
|
},
|
|
1249
|
-
async () =>
|
|
1351
|
+
async () => {
|
|
1352
|
+
// Below a unit a prefetch skipped: the orphan goes with it.
|
|
1353
|
+
deferred = deferUnit(context, orphan.shortCode, "below");
|
|
1354
|
+
if (deferred) return null;
|
|
1355
|
+
return resolveLayoutComponent(orphan, context);
|
|
1356
|
+
},
|
|
1250
1357
|
() => null,
|
|
1251
1358
|
);
|
|
1252
1359
|
|
|
@@ -1265,6 +1372,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1265
1372
|
deps.viewTransitionDefault,
|
|
1266
1373
|
),
|
|
1267
1374
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
1375
|
+
...deferred,
|
|
1268
1376
|
});
|
|
1269
1377
|
|
|
1270
1378
|
// Resolve the orphan layout's parallel slots through the shared main-path
|
|
@@ -1425,5 +1533,6 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1425
1533
|
}
|
|
1426
1534
|
}
|
|
1427
1535
|
|
|
1536
|
+
deliverStartedLoaders(context, allSegments);
|
|
1428
1537
|
return { segments: allSegments, matchedIds };
|
|
1429
1538
|
}
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
type DevShellLookup,
|
|
95
95
|
} from "./shell-build-manifest.js";
|
|
96
96
|
import { contextGet } from "../context-var.js";
|
|
97
|
+
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
97
98
|
import {
|
|
98
99
|
isWarmReplace,
|
|
99
100
|
noteWarmShellEvent,
|
|
@@ -302,7 +303,10 @@ type PreparedRender =
|
|
|
302
303
|
| {
|
|
303
304
|
kind: "payload";
|
|
304
305
|
payload: RscPayload;
|
|
305
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* The response depends on the source page: an intercept targets the
|
|
308
|
+
* route, or it carries deferred units (`prefetch: false`).
|
|
309
|
+
*/
|
|
306
310
|
sourceScoped: boolean;
|
|
307
311
|
pprReplayStatus?: PprReplayStatus;
|
|
308
312
|
/** The key a navigation-only heal capture stores under, when one is needed. */
|
|
@@ -831,6 +835,12 @@ function* preparePayloadPlan<TEnv>(
|
|
|
831
835
|
};
|
|
832
836
|
}
|
|
833
837
|
|
|
838
|
+
// What a prefetch defers depends on what the source page holds: a page
|
|
839
|
+
// that holds the segment gets it rendered. Adopted from another page, a
|
|
840
|
+
// deferring payload held the click on a fill for content a plain click
|
|
841
|
+
// showed at once, and ran held work again.
|
|
842
|
+
const deferring = result.segments.some((s) => s.deferred);
|
|
843
|
+
if (deferring) reqCtx._payloadDeferred = true;
|
|
834
844
|
return {
|
|
835
845
|
kind: "payload",
|
|
836
846
|
payload: {
|
|
@@ -860,7 +870,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
860
870
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
861
871
|
},
|
|
862
872
|
},
|
|
863
|
-
sourceScoped: result.interceptTargeted === true,
|
|
873
|
+
sourceScoped: result.interceptTargeted === true || deferring,
|
|
864
874
|
pprReplayStatus,
|
|
865
875
|
healKey,
|
|
866
876
|
};
|
|
@@ -1200,16 +1210,34 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1200
1210
|
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
1201
1211
|
}
|
|
1202
1212
|
// Enable browser HTTP caching for prefetch responses only.
|
|
1203
|
-
// Requires X-Rango-Prefetch
|
|
1204
|
-
// a source-agnostic response (see sourceScoped above), and a
|
|
1205
|
-
// cache-control value (false disables caching).
|
|
1206
|
-
|
|
1207
|
-
if (isPrefetch && isPartial && !sourceScoped) {
|
|
1213
|
+
// Requires a prefetch request (X-Rango-Prefetch, sent by Link prefetch
|
|
1214
|
+
// fetch), a source-agnostic response (see sourceScoped above), and a
|
|
1215
|
+
// configured cache-control value (false disables caching).
|
|
1216
|
+
if (reqCtx._requestKind === "prefetch" && isPartial && !sourceScoped) {
|
|
1208
1217
|
const cc = ctx.router.prefetchCacheControl;
|
|
1209
1218
|
if (cc) {
|
|
1210
1219
|
rscHeaders["cache-control"] = cc;
|
|
1211
1220
|
}
|
|
1212
1221
|
}
|
|
1222
|
+
// prefetch: false (docs/design/prefetch-false.md, R7). A response that
|
|
1223
|
+
// carries deferred units answers one page's prefetch, and a fill is the
|
|
1224
|
+
// second half of one adoption: neither is reused by a shared cache or the
|
|
1225
|
+
// document cache (which also refuses a payload marked `_payloadDeferred`),
|
|
1226
|
+
// whatever Cache-Control the route set. A route with no flag keeps its
|
|
1227
|
+
// headers.
|
|
1228
|
+
if (
|
|
1229
|
+
reqCtx._payloadDeferred ||
|
|
1230
|
+
(isPartial && reqCtx._requestKind === "fill")
|
|
1231
|
+
) {
|
|
1232
|
+
rscHeaders["cache-control"] = NOT_REUSED;
|
|
1233
|
+
}
|
|
1234
|
+
// The browser may still keep the body, and answers from it without asking
|
|
1235
|
+
// while it reloads a document for back/forward. A navigation is the same
|
|
1236
|
+
// URL as its prefetch: the header that tells them apart keeps the deferring
|
|
1237
|
+
// body from answering one.
|
|
1238
|
+
if (reqCtx._payloadDeferred) {
|
|
1239
|
+
rscHeaders.vary += `, ${PREFETCH_HEADER}`;
|
|
1240
|
+
}
|
|
1213
1241
|
|
|
1214
1242
|
const isFlightResponse = isRscRequest(request, url, isPartial);
|
|
1215
1243
|
const stageTracking = {
|
|
@@ -1694,6 +1722,21 @@ function publishTailTiming(
|
|
|
1694
1722
|
|
|
1695
1723
|
const PENDING = Symbol("pending");
|
|
1696
1724
|
|
|
1725
|
+
/**
|
|
1726
|
+
* Cache-Control of a response that is not to be reused: one that carries
|
|
1727
|
+
* deferred units (`prefetch: false`), and a fill. `private` keeps it out of
|
|
1728
|
+
* shared caches; `no-cache` makes the browser ask again, and with no
|
|
1729
|
+
* validator on the response that is a full request. The browser may keep the
|
|
1730
|
+
* body all the same (Chromium does) and answers from it, without asking,
|
|
1731
|
+
* while it reloads a document for back/forward. That is harmless: the client
|
|
1732
|
+
* completes any payload that carries deferred work, wherever it came from.
|
|
1733
|
+
* Not `no-store`:
|
|
1734
|
+
* Chromium reports a `no-store` fetch whose body is read through a stream
|
|
1735
|
+
* as canceled (net::ERR_ABORTED in DevTools, and Playwright's
|
|
1736
|
+
* response.finished() never settles), though every byte arrived.
|
|
1737
|
+
*/
|
|
1738
|
+
const NOT_REUSED = "private, no-cache";
|
|
1739
|
+
|
|
1697
1740
|
/**
|
|
1698
1741
|
* Whether `value` is not a promise, or a promise that has already settled:
|
|
1699
1742
|
* racing it against an already-resolved promise, a settled one's reaction is
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from "../server/context.js";
|
|
74
74
|
import { isThenable } from "../handles/is-thenable.js";
|
|
75
75
|
import { requestHeaders } from "../server/request-headers.js";
|
|
76
|
+
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
76
77
|
import type {
|
|
77
78
|
CachedEntryData,
|
|
78
79
|
ShellCacheEntry,
|
|
@@ -1448,7 +1449,7 @@ function createNavigationCaptureRequest(request: Request, url: URL): Request {
|
|
|
1448
1449
|
headers.set("accept", "text/html");
|
|
1449
1450
|
for (const name of [
|
|
1450
1451
|
"rsc-action",
|
|
1451
|
-
|
|
1452
|
+
PREFETCH_HEADER,
|
|
1452
1453
|
"x-rango-state",
|
|
1453
1454
|
"x-rsc-hmr",
|
|
1454
1455
|
"x-rsc-router-client-path",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
2
|
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
3
|
+
import { asStream } from "./browser/defer.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Browser: a boundary is handed the settled value. React treats a promise as
|
|
@@ -7,7 +8,13 @@ import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
|
7
8
|
* promise it has not read, in a render that cannot wait, shows its fallback.
|
|
8
9
|
* So a cache entry records its array once the aggregate fulfils and later
|
|
9
10
|
* calls return that same array; until then (and when it rejects) they return
|
|
10
|
-
* the promise. A boundary with no loaders gets one shared empty array.
|
|
11
|
+
* the promise. A boundary with no loaders gets one shared empty array. An
|
|
12
|
+
* aggregate whose sources are all streams that have fulfilled (React's
|
|
13
|
+
* `status` on the thenable, which Flight sets itself; a plain value is not a
|
|
14
|
+
* stream, whatever fields it carries) is known on its first
|
|
15
|
+
* call and is the array from the start: the sources of a page whose entry was
|
|
16
|
+
* rewritten without a render (browser/partial-update.ts fillHoles) are
|
|
17
|
+
* first aggregated by the next navigation that keeps them.
|
|
11
18
|
*
|
|
12
19
|
* Server: a fresh promise per call, so Suspense emits the fallback in the
|
|
13
20
|
* streamed HTML. A shared resolved promise would carry React's `.status`
|
|
@@ -22,10 +29,10 @@ const IS_BROWSER = typeof window !== "undefined";
|
|
|
22
29
|
|
|
23
30
|
interface LoaderCacheEntry {
|
|
24
31
|
sources: any[];
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
// The aggregate while it is pending (a rejection passes through untouched),
|
|
33
|
+
// then the array it fulfilled with. The array from the start for sources
|
|
34
|
+
// that have all fulfilled.
|
|
35
|
+
result: Promise<any[]> | any[];
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
// Cap the per-key entries array. A stable first-ref (e.g. a layout loader whose
|
|
@@ -91,13 +98,7 @@ export function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
return Promise.all(
|
|
95
|
-
loaders.map((loader) =>
|
|
96
|
-
loader.loaderData instanceof Promise
|
|
97
|
-
? loader.loaderData
|
|
98
|
-
: Promise.resolve(loader.loaderData),
|
|
99
|
-
),
|
|
100
|
-
);
|
|
101
|
+
return Promise.all(loaders.map((loader) => asStream(loader.loaderData)));
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function isObjectLike(value: unknown): value is object {
|
|
@@ -131,17 +132,19 @@ export function getMemoizedLoaderPromise(
|
|
|
131
132
|
if (entries) {
|
|
132
133
|
for (const entry of entries) {
|
|
133
134
|
if (hasSameReferences(entry.sources, sources)) {
|
|
134
|
-
return entry.
|
|
135
|
+
return entry.result;
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
const newEntry = { sources
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const newEntry: LoaderCacheEntry = { sources, result: [] };
|
|
141
|
+
newEntry.result = sources.every(
|
|
142
|
+
(source) =>
|
|
143
|
+
source instanceof Promise &&
|
|
144
|
+
(source as { status?: string }).status === "fulfilled",
|
|
145
|
+
)
|
|
146
|
+
? sources.map((source) => source.value)
|
|
147
|
+
: buildLoaderPromise(loaders).then((values) => (newEntry.result = values));
|
|
145
148
|
if (entries) {
|
|
146
149
|
// Bound the array: drop the oldest entry before appending when at the cap.
|
|
147
150
|
if (entries.length >= MAX_ENTRIES_PER_KEY) {
|
|
@@ -153,5 +156,5 @@ export function getMemoizedLoaderPromise(
|
|
|
153
156
|
} else {
|
|
154
157
|
primitiveLoaderCache.set(first, [newEntry]);
|
|
155
158
|
}
|
|
156
|
-
return
|
|
159
|
+
return newEntry.result;
|
|
157
160
|
}
|