@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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { LOADER_SEGMENT_ID } from "../browser/react/filter-segment-order.js";
|
|
2
|
+
import type { HandleData } from "../server/handle-store.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which run pushed each handle value, so a partial response replaces only the
|
|
6
|
+
* pushes of what it re-ran (#1103).
|
|
7
|
+
*
|
|
8
|
+
* A segment's handle bucket holds its handler's pushes and those of the
|
|
9
|
+
* loaders registered on it: a loader pushes under its owning segment's id
|
|
10
|
+
* (fresh.ts, revalidation.ts pin `_currentSegmentId`). A navigation or an
|
|
11
|
+
* action response decides each of them on its own: the handler re-runs and a
|
|
12
|
+
* loader whose revalidate() answers false keeps its data on the client. The
|
|
13
|
+
* response carries only what re-ran, so replacing the whole bucket with it
|
|
14
|
+
* dropped the pushes of the loader whose data the page still shows (the
|
|
15
|
+
* document title, a breadcrumb), and a loader re-run under a handler that did
|
|
16
|
+
* not re-run took the handler's pushes with it.
|
|
17
|
+
*
|
|
18
|
+
* The record rides in the snapshot itself, beside each handle, under a name
|
|
19
|
+
* no handle has: `data[pushedByKey(h)][segmentId][i]` is the $$id of the
|
|
20
|
+
* loader whose run pushed `data[h][segmentId][i]`, or null for the segment's
|
|
21
|
+
* handler. A bucket without a loader push has no entry. Inside the snapshot it
|
|
22
|
+
* crosses Flight and every clone and history-cache copy with the values;
|
|
23
|
+
* collectHandleData reads a handle by its $$id, so no collect sees it.
|
|
24
|
+
* resolveDeferredHandleValues keeps it aligned when it drops a value.
|
|
25
|
+
*/
|
|
26
|
+
const PUSHED_BY_SUFFIX = "\u0000pushedBy";
|
|
27
|
+
|
|
28
|
+
/** The snapshot key holding `handleName`'s pushedBy record. */
|
|
29
|
+
export function pushedByKey(handleName: string): string {
|
|
30
|
+
return handleName + PUSHED_BY_SUFFIX;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True for a pushedBy record's key, false for a handle's. */
|
|
34
|
+
export function isPushedByKey(name: string): boolean {
|
|
35
|
+
return name.endsWith(PUSHED_BY_SUFFIX);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `data` without its pushedBy records, as a reader outside the router sees it. */
|
|
39
|
+
export function withoutPushedBy(data: HandleData): HandleData {
|
|
40
|
+
const out: HandleData = {};
|
|
41
|
+
for (const name in data) {
|
|
42
|
+
if (!isPushedByKey(name)) out[name] = data[name];
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type PushedBy = readonly (string | null)[] | undefined;
|
|
48
|
+
|
|
49
|
+
const keysOf = (a: object | undefined, b: object | undefined): Set<string> =>
|
|
50
|
+
new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The handle data after a partial response's snapshot `incoming` (each stream
|
|
54
|
+
* yield holds everything the response pushed so far). A segment outside
|
|
55
|
+
* `segmentOrder` loses its buckets. In the others a value stands for the run
|
|
56
|
+
* that pushed it: the segment's handler, or a loader `matched` registers on
|
|
57
|
+
* the segment (`${shortCode}D${index}.${$$id}`, a loader's pushes being filed
|
|
58
|
+
* under its shortCode; an id it does not register, a dependency or a loader a
|
|
59
|
+
* handler read, counts as the handler's). A run that has values in the
|
|
60
|
+
* response, or ran for it (`resolvedIds`: the handlers and loaders that ran),
|
|
61
|
+
* replaces its values with the ones it delivered, nothing when it delivered
|
|
62
|
+
* none. The other runs keep theirs in place, and the response's values take
|
|
63
|
+
* the position of the first value they replace (the end when they replace
|
|
64
|
+
* none). New containers throughout: an object a reader was handed (a history
|
|
65
|
+
* entry's data) is never written to.
|
|
66
|
+
*/
|
|
67
|
+
export function mergePartialHandleData(
|
|
68
|
+
current: HandleData,
|
|
69
|
+
incoming: HandleData,
|
|
70
|
+
segmentOrder: readonly string[],
|
|
71
|
+
matched: readonly string[],
|
|
72
|
+
resolvedIds: readonly string[] | undefined,
|
|
73
|
+
): HandleData {
|
|
74
|
+
// A run's key: a segment's id for its handler, `${segmentId}\0${$$id}` for
|
|
75
|
+
// a loader (its segment id with the `D${index}.` marker replaced).
|
|
76
|
+
const runKey = (id: string): string =>
|
|
77
|
+
id.replace(LOADER_SEGMENT_ID, "\u0000");
|
|
78
|
+
const inOrder = new Set(segmentOrder);
|
|
79
|
+
const registered = new Set(matched.map(runKey));
|
|
80
|
+
const ran = new Set(resolvedIds?.map(runKey));
|
|
81
|
+
const out: HandleData = {};
|
|
82
|
+
for (const name of keysOf(current, incoming)) {
|
|
83
|
+
if (isPushedByKey(name)) continue;
|
|
84
|
+
const byName = pushedByKey(name);
|
|
85
|
+
const buckets: Record<string, unknown[]> = (out[name] = {});
|
|
86
|
+
for (const segmentId of keysOf(current[name], incoming[name])) {
|
|
87
|
+
if (!inOrder.has(segmentId)) continue;
|
|
88
|
+
const run = (by: PushedBy, i: number): string => {
|
|
89
|
+
const key = `${segmentId}\u0000${by?.[i]}`;
|
|
90
|
+
return registered.has(key) ? key : segmentId;
|
|
91
|
+
};
|
|
92
|
+
const currentBy = current[byName]?.[segmentId] as PushedBy;
|
|
93
|
+
const incomingValues = incoming[name]?.[segmentId] ?? [];
|
|
94
|
+
const incomingBy = incoming[byName]?.[segmentId] as PushedBy;
|
|
95
|
+
const delivered = new Set(
|
|
96
|
+
incomingValues.map((_, i) => run(incomingBy, i)),
|
|
97
|
+
);
|
|
98
|
+
const values: unknown[] = [];
|
|
99
|
+
const by: (string | null)[] = [];
|
|
100
|
+
let at = -1;
|
|
101
|
+
(current[name]?.[segmentId] ?? []).forEach((value, i) => {
|
|
102
|
+
const key = run(currentBy, i);
|
|
103
|
+
if (delivered.has(key) || ran.has(key)) {
|
|
104
|
+
if (at < 0) at = values.length;
|
|
105
|
+
} else {
|
|
106
|
+
values.push(value);
|
|
107
|
+
by.push(currentBy?.[i] ?? null);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (at < 0) at = values.length;
|
|
111
|
+
values.splice(at, 0, ...incomingValues);
|
|
112
|
+
by.splice(
|
|
113
|
+
at,
|
|
114
|
+
0,
|
|
115
|
+
...incomingValues.map((_, i) => incomingBy?.[i] ?? null),
|
|
116
|
+
);
|
|
117
|
+
if (values.length > 0) buckets[segmentId] = values;
|
|
118
|
+
if (by.some((run) => run !== null)) (out[byName] ??= {})[segmentId] = by;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
package/src/loader-store.ts
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { unwrapsSynchronously } from "./thenable-status.js";
|
|
39
|
+
import { asStream } from "./browser/defer.js";
|
|
39
40
|
|
|
40
41
|
/** The loader-segment shape announcePendingStreams reads (ResolvedSegment). */
|
|
41
42
|
export interface PendingStreamSource {
|
|
@@ -538,14 +539,23 @@ export class LoaderStore {
|
|
|
538
539
|
this.pendingStreams.set(loaderId, set);
|
|
539
540
|
}
|
|
540
541
|
set.add(stream);
|
|
541
|
-
const release = () =>
|
|
542
|
-
const current = this.pendingStreams.get(loaderId);
|
|
543
|
-
if (!current || !current.delete(stream)) return;
|
|
544
|
-
if (current.size === 0) this.pendingStreams.delete(loaderId);
|
|
545
|
-
};
|
|
542
|
+
const release = () => this.releasePendingStream(loaderId, stream);
|
|
546
543
|
stream.then(release, release);
|
|
547
544
|
}
|
|
548
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Stop tracking a stream. trackPendingStream does it as the stream settles;
|
|
548
|
+
* an abandoned fill's hole (browser/partial-update.ts releaseHoles) never
|
|
549
|
+
* settles, and
|
|
550
|
+
* left tracked it would pin the family's held readers on every later
|
|
551
|
+
* transition.
|
|
552
|
+
*/
|
|
553
|
+
releasePendingStream(loaderId: string, stream: Promise<unknown>): void {
|
|
554
|
+
const current = this.pendingStreams.get(loaderId);
|
|
555
|
+
if (!current || !current.delete(stream)) return;
|
|
556
|
+
if (current.size === 0) this.pendingStreams.delete(loaderId);
|
|
557
|
+
}
|
|
558
|
+
|
|
549
559
|
/** True while any navigation stream for `loaderId` is still pending. */
|
|
550
560
|
isStreamPending(loaderId: string): boolean {
|
|
551
561
|
return this.pendingStreams.has(loaderId);
|
|
@@ -566,12 +576,13 @@ export class LoaderStore {
|
|
|
566
576
|
*/
|
|
567
577
|
announcePendingStreams(segments: readonly PendingStreamSource[]): void {
|
|
568
578
|
for (const segment of segments) {
|
|
579
|
+
const data = asStream(segment.loaderData);
|
|
569
580
|
if (
|
|
570
581
|
segment.type === "loader" &&
|
|
571
582
|
segment.loaderId &&
|
|
572
|
-
|
|
583
|
+
data instanceof Promise
|
|
573
584
|
) {
|
|
574
|
-
this.trackPendingStream(segment.loaderId,
|
|
585
|
+
this.trackPendingStream(segment.loaderId, data);
|
|
575
586
|
}
|
|
576
587
|
}
|
|
577
588
|
for (const loaderId of this.pendingStreams.keys()) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire markers of `prefetch: false` (docs/design/prefetch-false.md):
|
|
3
|
+
* shared by the browser that sends them and the server that reads them
|
|
4
|
+
* (router/segment-resolution/prefetch-deferral.ts requestKind). Dependency
|
|
5
|
+
* free, like segment-fragments.ts: the browser must not import the server
|
|
6
|
+
* module that reads them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
|
|
10
|
+
export const PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
11
|
+
|
|
12
|
+
/** Query param of a fill request (browser/navigation-client.ts). */
|
|
13
|
+
export const FILL_PARAM = "_rsc_fill";
|
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
LOADER_REDIRECT,
|
|
11
11
|
} from "./decode-loader-results.js";
|
|
12
12
|
import { LoaderRedirect } from "./loader-redirect.js";
|
|
13
|
+
import { Defer, readDefer } from "./browser/defer.js";
|
|
13
14
|
import type { BoundaryAudit } from "./suspense-audit.js";
|
|
14
15
|
import {
|
|
15
16
|
AuditedFallback,
|
|
16
17
|
useBoundaryAudit,
|
|
17
18
|
useRevealAudit,
|
|
18
19
|
} from "./suspense-audit-react.js";
|
|
20
|
+
import { SETTLED_THENABLE } from "./thenable-status.js";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Router-owned error boundary for read-site loader errors. segment-system
|
|
@@ -141,7 +143,7 @@ export function AuditedRouteContent({
|
|
|
141
143
|
const audit = useBoundaryAudit(
|
|
142
144
|
`content:${segmentId}`,
|
|
143
145
|
segmentId ?? "",
|
|
144
|
-
content,
|
|
146
|
+
content instanceof Defer ? content.promise : content,
|
|
145
147
|
);
|
|
146
148
|
return (
|
|
147
149
|
<Suspense
|
|
@@ -174,8 +176,16 @@ const Suspender = ({
|
|
|
174
176
|
// A pending promise suspends here and the wrapping Suspense shows the
|
|
175
177
|
// loading() fallback; a node renders without suspending. The wrapper tree
|
|
176
178
|
// (RouteContentWrapper > Suspense > Suspender) is identical either way (see
|
|
177
|
-
// docs/tree-structure.md), like LoaderResolver's resolved-data branch.
|
|
178
|
-
|
|
179
|
+
// docs/tree-structure.md), like LoaderResolver's resolved-data branch. A
|
|
180
|
+
// `prefetch: false` hole is read here, by the read rule, so the filled
|
|
181
|
+
// page's node later takes the same place. The content can be a pending
|
|
182
|
+
// promise in one tree and its node in the next: use() at this place on
|
|
183
|
+
// every render, as LoaderResolver does.
|
|
184
|
+
return content instanceof Defer
|
|
185
|
+
? (readDefer(content) as ReactNode)
|
|
186
|
+
: content instanceof Promise
|
|
187
|
+
? use(content)
|
|
188
|
+
: (use(SETTLED_THENABLE), content);
|
|
179
189
|
};
|
|
180
190
|
|
|
181
191
|
/**
|
|
@@ -329,10 +339,14 @@ function LoaderResolver({
|
|
|
329
339
|
);
|
|
330
340
|
}
|
|
331
341
|
|
|
342
|
+
// A slot's aggregate can be a pending promise on one render and its arrived
|
|
343
|
+
// array on the next (segment-loader-promise.ts): use() at this place on
|
|
344
|
+
// every render, or React reports a component that suspended in use() and
|
|
345
|
+
// then finished without it (the useLoader pattern, use-loader.tsx).
|
|
332
346
|
const resolvedData =
|
|
333
347
|
loaderDataPromise instanceof Promise
|
|
334
348
|
? use(loaderDataPromise)
|
|
335
|
-
: loaderDataPromise;
|
|
349
|
+
: (use(SETTLED_THENABLE), loaderDataPromise);
|
|
336
350
|
|
|
337
351
|
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
338
352
|
resolvedData,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type InterceptConfig,
|
|
23
23
|
type LoaderEntry,
|
|
24
24
|
} from "../server/context";
|
|
25
|
-
import { invariant } from "../errors";
|
|
25
|
+
import { assertBooleanOption, invariant } from "../errors";
|
|
26
26
|
import { validateUserRouteName } from "../route-name.js";
|
|
27
27
|
import { isCachedFunction } from "../cache/taint.js";
|
|
28
28
|
import { RangoContext } from "../server/context";
|
|
@@ -44,6 +44,7 @@ import type {
|
|
|
44
44
|
TransitionItem,
|
|
45
45
|
UseItems,
|
|
46
46
|
} from "../route-types.js";
|
|
47
|
+
import { isRenderableLoading } from "../types/boundaries.js";
|
|
47
48
|
import type { RouteHelpers } from "./helpers-types.js";
|
|
48
49
|
import {
|
|
49
50
|
resolveHandlerUse,
|
|
@@ -899,9 +900,15 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
899
900
|
(optionsGiven as { stream?: unknown } | undefined)?.stream === undefined,
|
|
900
901
|
"loader() stream was replaced: use loader(Def, { ssr: false }) — the same knob as loading(fallback, { ssr: false }) — to await the loader before first flush on document requests.",
|
|
901
902
|
);
|
|
902
|
-
|
|
903
|
-
optionsGiven?.ssr
|
|
904
|
-
|
|
903
|
+
assertBooleanOption(
|
|
904
|
+
optionsGiven?.ssr,
|
|
905
|
+
"loader() ssr",
|
|
906
|
+
"stream on every render",
|
|
907
|
+
);
|
|
908
|
+
assertBooleanOption(
|
|
909
|
+
optionsGiven?.prefetch,
|
|
910
|
+
"loader() prefetch",
|
|
911
|
+
"run the loader in prefetches",
|
|
905
912
|
);
|
|
906
913
|
|
|
907
914
|
const name = `${ctx.namespace}.$${store.getNextIndex("loader")}`;
|
|
@@ -917,6 +924,7 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
917
924
|
...(optionsGiven?.ssr === false && { bake: true as const }),
|
|
918
925
|
...(optionsGiven?.ssr === false &&
|
|
919
926
|
ctx.isSSR && { awaitBeforeFlush: true as const }),
|
|
927
|
+
...(optionsGiven?.prefetch === false && { prefetch: false as const }),
|
|
920
928
|
};
|
|
921
929
|
|
|
922
930
|
// Merge handler.use defaults (attached to the loader definition) with explicit use
|
|
@@ -969,6 +977,12 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
969
977
|
invariant(false, "No parent entry available for loading()");
|
|
970
978
|
}
|
|
971
979
|
|
|
980
|
+
assertBooleanOption(
|
|
981
|
+
options?.prefetch,
|
|
982
|
+
"loading() prefetch",
|
|
983
|
+
"render what the fallback covers in prefetches",
|
|
984
|
+
);
|
|
985
|
+
|
|
972
986
|
// Unwrap function form: loading(() => <Skeleton />) → loading(<Skeleton />)
|
|
973
987
|
const resolved =
|
|
974
988
|
typeof component === "function" ? (component as () => any)() : component;
|
|
@@ -980,6 +994,15 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
980
994
|
parent.loading = resolved;
|
|
981
995
|
}
|
|
982
996
|
|
|
997
|
+
// The flag means "what this fallback covers": without a fallback to show
|
|
998
|
+
// there is nothing to defer behind. Recorded from `resolved`, not
|
|
999
|
+
// `parent.loading`, so the document evaluation (ssr: false) agrees with
|
|
1000
|
+
// the navigation one. Last loading() wins, like `loading` itself.
|
|
1001
|
+
parent.loadingPrefetch =
|
|
1002
|
+
options?.prefetch === false && isRenderableLoading(resolved)
|
|
1003
|
+
? false
|
|
1004
|
+
: undefined;
|
|
1005
|
+
|
|
983
1006
|
const name = `$${store.getNextIndex("loading")}`;
|
|
984
1007
|
return { name, type: "loading" } as LoadingItem;
|
|
985
1008
|
};
|
|
@@ -328,6 +328,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
328
328
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
329
329
|
* ```
|
|
330
330
|
*
|
|
331
|
+
* Pass `{ prefetch: false }` to keep the loader out of prefetches: a
|
|
332
|
+
* prefetch skips it and the click that adopts the prefetch fetches it in
|
|
333
|
+
* one follow-up request (see {@link LoaderOptions}). Only on a segment the
|
|
334
|
+
* client does not have yet: a loader the client already holds runs in the
|
|
335
|
+
* prefetch when it revalidates.
|
|
336
|
+
*
|
|
337
|
+
* ```typescript
|
|
338
|
+
* loader(ReviewsLoader, { prefetch: false }),
|
|
339
|
+
* ```
|
|
340
|
+
*
|
|
331
341
|
* @param loaderDef - Loader created with createLoader()
|
|
332
342
|
* @param optionsOrUse - Delivery options, or the use() callback when passing none
|
|
333
343
|
* @param use - Optional callback for loader-specific revalidation rules
|
|
@@ -345,14 +355,30 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
345
355
|
*
|
|
346
356
|
* // Skip loading on SSR, only show on client navigation
|
|
347
357
|
* loading(<Skeleton />, { ssr: false })
|
|
358
|
+
*
|
|
359
|
+
* // Keep everything behind this fallback out of prefetches
|
|
360
|
+
* loading(<Skeleton />, { prefetch: false })
|
|
348
361
|
* ```
|
|
349
362
|
* @param component - The loading UI to show during navigation
|
|
350
363
|
* @param options - Configuration options
|
|
351
364
|
* @param options.ssr - If false, skip showing loading on document requests (SSR)
|
|
365
|
+
* @param options.prefetch - If false, the flag applies when this entry's
|
|
366
|
+
* segment is new to the client. A prefetch then skips the whole unit: the
|
|
367
|
+
* handler, its loaders, its orphan layouts and parallel slots and, for a
|
|
368
|
+
* layout, every deeper entry; the click that adopts the prefetch shows
|
|
369
|
+
* this fallback and fetches them in one follow-up request, which reads
|
|
370
|
+
* cache() and "use cache" as any request does (a prefetch reads neither
|
|
371
|
+
* for skipped work). A segment the client already holds is never
|
|
372
|
+
* deferred: a prefetch renders it as usual when it revalidates, and what
|
|
373
|
+
* is below it follows its own flags. To keep a child out of prefetches
|
|
374
|
+
* while moving inside a section, flag the child's own loading(). An entry
|
|
375
|
+
* whose handler output is stored (cache(), ppr, Prerender/Static) is
|
|
376
|
+
* served as usual and only its loaders are skipped. Loaders behind the
|
|
377
|
+
* fallback cannot call ctx.rendered().
|
|
352
378
|
*/
|
|
353
379
|
loading: (
|
|
354
380
|
component: ReactNode | (() => ReactNode),
|
|
355
|
-
options?: { ssr?: boolean },
|
|
381
|
+
options?: { ssr?: boolean; prefetch?: boolean },
|
|
356
382
|
) => LoadingItem;
|
|
357
383
|
/**
|
|
358
384
|
* Attach an error boundary to catch errors in this segment and children
|
|
@@ -331,8 +331,9 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
331
331
|
deps.wrapLoaderPromise(
|
|
332
332
|
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
333
333
|
// cache() applies here too.
|
|
334
|
-
runInsideLoaderScope(
|
|
335
|
-
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
334
|
+
runInsideLoaderScope(
|
|
335
|
+
() => resolveLoaderData(loaderEntry, context, context.pathname),
|
|
336
|
+
loader.$$id,
|
|
336
337
|
),
|
|
337
338
|
parentEntry,
|
|
338
339
|
segmentId,
|
|
@@ -564,8 +565,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
564
565
|
deps.wrapLoaderPromise(
|
|
565
566
|
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
566
567
|
// cache() applies here too.
|
|
567
|
-
runInsideLoaderScope(
|
|
568
|
-
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
568
|
+
runInsideLoaderScope(
|
|
569
|
+
() => resolveLoaderData(loaderEntry, context, context.pathname),
|
|
570
|
+
loader.$$id,
|
|
569
571
|
),
|
|
570
572
|
parentEntry,
|
|
571
573
|
segmentId,
|
package/src/router/is-action.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared `isAction()` matcher for revalidate predicates (server
|
|
2
|
+
* Shared `isAction()` matcher for revalidate() predicates (server) and
|
|
3
|
+
* transition({ when }) (browser).
|
|
3
4
|
*
|
|
4
5
|
* Action identity is `actionId`. The helper resolves an imported reference the
|
|
5
6
|
* same way the action boundary derives that id (`$id ?? $$id`), so a
|
|
@@ -55,6 +55,12 @@ import {
|
|
|
55
55
|
} from "../server/handle-store.js";
|
|
56
56
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
57
57
|
import { _getRequestContext } from "../server/request-context.js";
|
|
58
|
+
import {
|
|
59
|
+
firstUnitCandidate,
|
|
60
|
+
loaderOwner,
|
|
61
|
+
startDeferredLoader,
|
|
62
|
+
type PrefetchDeferral,
|
|
63
|
+
} from "./segment-resolution/prefetch-deferral.js";
|
|
58
64
|
import {
|
|
59
65
|
assertNonCacheableReadAllowed,
|
|
60
66
|
isInsideLoaderScope,
|
|
@@ -372,6 +378,29 @@ function detectLoaderCycle(
|
|
|
372
378
|
return dfs(to);
|
|
373
379
|
}
|
|
374
380
|
|
|
381
|
+
const warnedRenderedBesideUnit = new Set<string>();
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Dev only, once per loader: a loader that is not deferrable awaits
|
|
385
|
+
* ctx.rendered() in a tree where a prefetch can skip a handler. The prefetch
|
|
386
|
+
* rejects that call (see rendered() below); saying so on a document or a
|
|
387
|
+
* navigation shows it before production, where prefetch is automatic.
|
|
388
|
+
*/
|
|
389
|
+
function warnRenderedBesideUnit(
|
|
390
|
+
deferral: PrefetchDeferral,
|
|
391
|
+
loaderId: string,
|
|
392
|
+
): void {
|
|
393
|
+
if (warnedRenderedBesideUnit.has(loaderId)) return;
|
|
394
|
+
const unit = firstUnitCandidate(deferral);
|
|
395
|
+
if (!unit) return;
|
|
396
|
+
warnedRenderedBesideUnit.add(loaderId);
|
|
397
|
+
console.warn(
|
|
398
|
+
`[rango] loader "${loaderId}" awaits ctx.rendered() on a route where "${unit.shortCode}" (${unit.id}) has loading(fallback, { prefetch: false }). ` +
|
|
399
|
+
`A prefetch of this route skips that handler, so ctx.rendered() throws in the prefetch and this loader is an error on the click that adopts it. ` +
|
|
400
|
+
`Remove { prefetch: false } from that loading(), or stop calling ctx.rendered() in this loader.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
375
404
|
/**
|
|
376
405
|
* Creates a memoizing loader executor with cycle detection.
|
|
377
406
|
* Shared by setupLoaderAccess and setupLoaderAccessSilent; only the handle
|
|
@@ -397,7 +426,9 @@ function createLoaderExecutor<TEnv>(
|
|
|
397
426
|
/**
|
|
398
427
|
* A stale refresh's executor (setupLoaderAccess _runLoaderIsolated): its
|
|
399
428
|
* runs' pushes are diverted from the page's store, so a run's end says
|
|
400
|
-
* nothing about the page's replayed values (HandleStore.settleLoaderRun)
|
|
429
|
+
* nothing about the page's replayed values (HandleStore.settleLoaderRun),
|
|
430
|
+
* and it never starts a loader the page's prefetch deferred
|
|
431
|
+
* (startDeferredLoader): a refresh reads each loader fresh.
|
|
401
432
|
*/
|
|
402
433
|
detached?: boolean;
|
|
403
434
|
/**
|
|
@@ -472,7 +503,14 @@ function createLoaderExecutor<TEnv>(
|
|
|
472
503
|
const bound =
|
|
473
504
|
callerLoaderId !== null &&
|
|
474
505
|
internal._loaderCacheOverrides?.get(loader.$$id);
|
|
475
|
-
|
|
506
|
+
// A loader the prefetch deferred starts through its registration, cache()
|
|
507
|
+
// binding included, as a navigation's run of it does.
|
|
508
|
+
const memo =
|
|
509
|
+
bound ||
|
|
510
|
+
loaderPromises.get(loader.$$id) ||
|
|
511
|
+
(executorOptions?.detached
|
|
512
|
+
? undefined
|
|
513
|
+
: startDeferredLoader(ctx, loader.$$id));
|
|
476
514
|
if (memo) {
|
|
477
515
|
if (recordTags) readValueTags(memo);
|
|
478
516
|
// A "use cache" body reading it refuses an identity read the run made
|
|
@@ -511,8 +549,9 @@ function createLoaderExecutor<TEnv>(
|
|
|
511
549
|
// resolution continues — by the time the async body pushes, it would be
|
|
512
550
|
// stale. Attributing to the owning route/layout segment gives handle
|
|
513
551
|
// pushes the same granularity handler pushes have (segmentOrder-correct
|
|
514
|
-
// for breadcrumb accumulation).
|
|
515
|
-
|
|
552
|
+
// for breadcrumb accumulation). A loader a prefetch deferred and a reader
|
|
553
|
+
// starts anyway keeps the segment that registers it (loaderOwner).
|
|
554
|
+
const owningSegmentId = loaderOwner(ctx, loader.$$id);
|
|
516
555
|
|
|
517
556
|
let renderedResolved = false;
|
|
518
557
|
let renderedPromise: Promise<void> | null = null;
|
|
@@ -633,6 +672,32 @@ function createLoaderExecutor<TEnv>(
|
|
|
633
672
|
);
|
|
634
673
|
}
|
|
635
674
|
|
|
675
|
+
// prefetch: false (segment-resolution/prefetch-deferral.ts). A
|
|
676
|
+
// deferrable loader runs in a fill request, where its entry is held
|
|
677
|
+
// and its handler does not run: there is no render to wait for.
|
|
678
|
+
// Refused on every request kind, so it is seen on a document load.
|
|
679
|
+
const deferral = internal._prefetchDeferral;
|
|
680
|
+
if (deferral?.scope.loaderIds.has(currentLoaderId)) {
|
|
681
|
+
throw new Error(
|
|
682
|
+
`ctx.rendered() is not available to loader "${currentLoaderId}": it is deferred in prefetches ` +
|
|
683
|
+
`(registered with loader(Def, { prefetch: false }), or behind a loading(fallback, { prefetch: false })), ` +
|
|
684
|
+
`and the fill request that runs it afterwards does not re-run its entry's handler, so there is no render to wait for and no handle data to read. ` +
|
|
685
|
+
`Remove { prefetch: false } from the loader (or from the loading() above it), or stop calling ctx.rendered() in this loader.`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (deferral?.mode === "fill") {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`ctx.rendered() is not available to loader "${currentLoaderId}" in a fill request: the handlers the client already holds do not run, so their handle data is missing.`,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
if (
|
|
694
|
+
process.env.NODE_ENV !== "production" &&
|
|
695
|
+
deferral &&
|
|
696
|
+
!deferral.mode
|
|
697
|
+
) {
|
|
698
|
+
warnRenderedBesideUnit(deferral, currentLoaderId);
|
|
699
|
+
}
|
|
700
|
+
|
|
636
701
|
// awaitBeforeFlush cycle: segment resolution awaits this loader
|
|
637
702
|
// (loader(Def, { ssr: false })), the barrier awaits segment
|
|
638
703
|
// resolution, and rendered() awaits the barrier — waiting here can
|
|
@@ -684,6 +749,17 @@ function createLoaderExecutor<TEnv>(
|
|
|
684
749
|
// keeps a handler from depending on this loader.
|
|
685
750
|
const streaming = reqCtx._treeHasStreaming === true;
|
|
686
751
|
renderedPromise = reqCtx._renderBarrier.then(async () => {
|
|
752
|
+
// A prefetch that skipped a handler unit has none of that handler's
|
|
753
|
+
// pushes, and the fill never re-runs this loader: an error, never
|
|
754
|
+
// a handle list with a hole in it. Known only once resolution has
|
|
755
|
+
// decided the unit, which is what the barrier waits for.
|
|
756
|
+
if (deferral?.mode === "prefetch" && deferral.deferredUnit) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`ctx.rendered() in loader "${currentLoaderId}" cannot settle in a prefetch of this route: segment "${deferral.deferredUnit}" has loading(fallback, { prefetch: false }), ` +
|
|
759
|
+
`so the prefetch skipped its handler and its handle data does not exist. ` +
|
|
760
|
+
`Remove { prefetch: false } from that loading(), or stop calling ctx.rendered() in this loader.`,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
687
763
|
if (streaming) {
|
|
688
764
|
reqCtx._handleStore.seal();
|
|
689
765
|
await reqCtx._handleStore.settled;
|
|
@@ -807,6 +883,9 @@ export function setupLoaderAccess<TEnv>(
|
|
|
807
883
|
// pushes, and a later run here replaces them (HandleStore.pushReplayed).
|
|
808
884
|
const claimed = new Set<string>();
|
|
809
885
|
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
886
|
+
internal._loaderStarted = (loaderId) =>
|
|
887
|
+
loaderPromises.has(loaderId) ||
|
|
888
|
+
internal._loaderCacheOverrides?.has(loaderId) === true;
|
|
810
889
|
internal._claimLoaderPushes = (loaderId) => {
|
|
811
890
|
if (loaderPromises.has(loaderId) || claimed.has(loaderId)) return false;
|
|
812
891
|
claimed.add(loaderId);
|
package/src/router/match-api.ts
CHANGED
|
@@ -32,7 +32,12 @@ import {
|
|
|
32
32
|
getContext,
|
|
33
33
|
type InterceptSelectorContext,
|
|
34
34
|
} from "../server/context";
|
|
35
|
-
import type {
|
|
35
|
+
import type {
|
|
36
|
+
ErrorBoundaryHandler,
|
|
37
|
+
ErrorInfo,
|
|
38
|
+
InternalHandlerContext,
|
|
39
|
+
MatchResult,
|
|
40
|
+
} from "../types";
|
|
36
41
|
import type { ReactNode } from "react";
|
|
37
42
|
import type { MatchContext } from "./match-context.js";
|
|
38
43
|
import type { MatchApiDeps, ActionContext } from "./types.js";
|
|
@@ -49,6 +54,11 @@ import {
|
|
|
49
54
|
type RouteSnapshot,
|
|
50
55
|
} from "./route-snapshot.js";
|
|
51
56
|
import { resolveNavigation } from "./navigation-snapshot.js";
|
|
57
|
+
import {
|
|
58
|
+
partialDeferralMode,
|
|
59
|
+
planPrefetchDeferral,
|
|
60
|
+
requestKind,
|
|
61
|
+
} from "./segment-resolution/prefetch-deferral.js";
|
|
52
62
|
|
|
53
63
|
/**
|
|
54
64
|
* Create match context for full requests (document/SSR).
|
|
@@ -174,6 +184,9 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
174
184
|
if (reqCtx?._shellCaptureRun) {
|
|
175
185
|
reqCtx._shellCaptureLoaderLanes = routeLoaderLanes(snapshot.entries);
|
|
176
186
|
}
|
|
187
|
+
// A document never defers; the plan feeds the ctx.rendered() guards.
|
|
188
|
+
(handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
|
|
189
|
+
planPrefetchDeferral(snapshot.entries, undefined, matched, cacheScope);
|
|
177
190
|
|
|
178
191
|
return {
|
|
179
192
|
request,
|
|
@@ -453,6 +466,28 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
453
466
|
if (cacheScope) armRecordTagOwners();
|
|
454
467
|
if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
|
|
455
468
|
|
|
469
|
+
// `prefetch: false` (segment-resolution/prefetch-deferral.ts). With no
|
|
470
|
+
// request context (a match run on its own) the request is asked directly.
|
|
471
|
+
const kind = reqCtx ? reqCtx._requestKind : requestKind(request, rawUrl);
|
|
472
|
+
const deferral = planPrefetchDeferral(
|
|
473
|
+
snapshot.entries,
|
|
474
|
+
partialDeferralMode({
|
|
475
|
+
fill: kind === "fill",
|
|
476
|
+
prefetch: kind === "prefetch",
|
|
477
|
+
method: request.method,
|
|
478
|
+
isAction,
|
|
479
|
+
isIntercept,
|
|
480
|
+
isShellCapture: reqCtx?._shellCaptureRun === true,
|
|
481
|
+
}),
|
|
482
|
+
matched,
|
|
483
|
+
cacheScope,
|
|
484
|
+
// As the request listed them, not the copy forced above: see
|
|
485
|
+
// prefetch-deferral.ts.
|
|
486
|
+
nav.clientSegmentSet,
|
|
487
|
+
);
|
|
488
|
+
(handlerContext as InternalHandlerContext<any, TEnv>)._prefetchDeferral =
|
|
489
|
+
deferral;
|
|
490
|
+
|
|
456
491
|
return {
|
|
457
492
|
request,
|
|
458
493
|
url: cleanUrl,
|
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
runWithRequestContext,
|
|
114
114
|
type RequestContext,
|
|
115
115
|
} from "../../server/request-context.js";
|
|
116
|
+
import { isFillRequest } from "../segment-resolution/prefetch-deferral.js";
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
119
|
* Re-render a cached route in the background and write it with
|
|
@@ -281,7 +282,16 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
281
282
|
// Only trigger background revalidation if:
|
|
282
283
|
// 1. Cache was hit and stale
|
|
283
284
|
// 2. Cache scope exists
|
|
284
|
-
|
|
285
|
+
// 3. The request is not a fill (prefetch: false). A fill runs no handler
|
|
286
|
+
// of a segment the client holds, and the re-render runs every handler
|
|
287
|
+
// of the route. The record stays stale until a request that reads it
|
|
288
|
+
// arrives: the prefetch before the fill already was one.
|
|
289
|
+
if (
|
|
290
|
+
!state.shouldRevalidate ||
|
|
291
|
+
!state.cacheHit ||
|
|
292
|
+
!ctx.cacheScope ||
|
|
293
|
+
isFillRequest(ctx.handlerContext)
|
|
294
|
+
) {
|
|
285
295
|
return;
|
|
286
296
|
}
|
|
287
297
|
|