@rangojs/router 0.31.0 → 0.33.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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/history-state.d.ts +13 -2
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/route-definition/redirect.d.ts +5 -0
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/middleware-types.d.ts +6 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +38 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/history-state.ts +24 -2
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/build/runtime-discovery.ts +46 -8
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +37 -1
- package/src/cache/document-cache.ts +12 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/route-definition/redirect.ts +5 -0
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
|
@@ -117,7 +117,9 @@ function getShownRouteUrl(url: URL): URL | null {
|
|
|
117
117
|
if (!shown) return null;
|
|
118
118
|
try {
|
|
119
119
|
const parsed = new URL(shown, url.origin);
|
|
120
|
-
return parsed.origin === url.origin
|
|
120
|
+
return parsed.origin === url.origin
|
|
121
|
+
? withoutHash(formSearch(parsed))
|
|
122
|
+
: null;
|
|
121
123
|
} catch {
|
|
122
124
|
return null;
|
|
123
125
|
}
|
|
@@ -134,6 +136,19 @@ function formSearch(url: URL): URL {
|
|
|
134
136
|
return url;
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
/**
|
|
140
|
+
* `url` without its `#fragment`. The browser sends its whole address as the
|
|
141
|
+
* client path, anchor included, but never a fragment in a request URL, so the
|
|
142
|
+
* server's `nextUrl` has none. Left on the page-left URLs, `currentUrl` and
|
|
143
|
+
* `nextUrl` of one address differ (`/t/42#questions` vs `/t/42`) and a
|
|
144
|
+
* `revalidate()` rule comparing them reads two addresses. A fragment is never
|
|
145
|
+
* server state: it names no route, param or query.
|
|
146
|
+
*/
|
|
147
|
+
function withoutHash(url: URL): URL {
|
|
148
|
+
url.hash = "";
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
export async function resolveNavigation(
|
|
138
153
|
request: Request,
|
|
139
154
|
url: URL,
|
|
@@ -155,7 +170,7 @@ export async function resolveNavigation(
|
|
|
155
170
|
|
|
156
171
|
let visitor: URL;
|
|
157
172
|
try {
|
|
158
|
-
visitor = formSearch(new URL(previousUrl, url.origin));
|
|
173
|
+
visitor = withoutHash(formSearch(new URL(previousUrl, url.origin)));
|
|
159
174
|
} catch {
|
|
160
175
|
return null;
|
|
161
176
|
}
|
|
@@ -166,7 +181,7 @@ export async function resolveNavigation(
|
|
|
166
181
|
let interceptContextUrl: URL;
|
|
167
182
|
try {
|
|
168
183
|
interceptContextUrl = interceptSourceUrl
|
|
169
|
-
? new URL(interceptSourceUrl, url.origin)
|
|
184
|
+
? withoutHash(new URL(interceptSourceUrl, url.origin))
|
|
170
185
|
: prevUrl;
|
|
171
186
|
} catch {
|
|
172
187
|
interceptContextUrl = prevUrl;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve the prefetch cache TTL once, at router init, into the values the
|
|
3
3
|
* rest of the router consumes: seconds, and milliseconds for the client-side
|
|
4
|
-
* in-memory prefetch cache (shipped in payload metadata).
|
|
5
|
-
*
|
|
6
|
-
* render failed after its 200 went out (#1170).
|
|
4
|
+
* in-memory prefetch cache (shipped in payload metadata). A prefetch with a
|
|
5
|
+
* live part carries no Cache-Control: the browser HTTP cache would keep a
|
|
6
|
+
* prefetch whose render failed after its 200 went out (#1170). One answered
|
|
7
|
+
* from storage carries storedPrefetchCacheControl.
|
|
7
8
|
*
|
|
8
9
|
* `false` disables prefetch caching (seconds 0). A non-finite input
|
|
9
10
|
* (NaN/Infinity, e.g. from an env-derived number that failed to parse) is
|
|
@@ -41,3 +42,14 @@ export function resolvePrefetchCacheTTL(
|
|
|
41
42
|
|
|
42
43
|
return { seconds, ms: seconds * 1000 };
|
|
43
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The Cache-Control of a prefetch answer with nothing live in it
|
|
48
|
+
* (`_servedFromStorage`): the browser HTTP cache keeps it as long
|
|
49
|
+
* as the in-memory prefetch cache would. Undefined when prefetch caching is
|
|
50
|
+
* off.
|
|
51
|
+
*/
|
|
52
|
+
export function storedPrefetchCacheControl(ttlMs: number): string | undefined {
|
|
53
|
+
const seconds = Math.floor(ttlMs / 1000);
|
|
54
|
+
return seconds > 0 ? `private, max-age=${seconds}` : undefined;
|
|
55
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type {
|
|
8
8
|
ResolvedSegment,
|
|
9
9
|
HandlerContext,
|
|
10
|
+
InternalHandlerContext,
|
|
10
11
|
ShouldRevalidateFn,
|
|
11
12
|
} from "../types";
|
|
12
13
|
import type { ActionContext } from "./types";
|
|
@@ -20,6 +21,8 @@ import { _getRequestContext } from "../server/request-context.js";
|
|
|
20
21
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
21
22
|
import { paramsEqual } from "./params-util.js";
|
|
22
23
|
import { makeIsAction } from "./is-action.js";
|
|
24
|
+
import { createSourceReads, hasSourceReads } from "../prefetch-source-scope.js";
|
|
25
|
+
import { markSourceHeld, recordingParams } from "./source-reads.js";
|
|
23
26
|
|
|
24
27
|
const REVALIDATE_ALWAYS: ShouldRevalidateFn<any, any> = () => true;
|
|
25
28
|
const REVALIDATE_NEVER: ShouldRevalidateFn<any, any> = () => false;
|
|
@@ -154,17 +157,54 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
154
157
|
// second, contradicting entry would let flushRevalidationTrace count the
|
|
155
158
|
// segment as both revalidated and skipped.
|
|
156
159
|
const floorTrue = defaultOverride?.floor === true && defaultOverride.value;
|
|
157
|
-
|
|
160
|
+
// The source page the predicates see.
|
|
161
|
+
let currentParams: Record<string, string> = prevParams;
|
|
162
|
+
const prevRouteKey = reqCtx?._prevRouteKey;
|
|
163
|
+
const fromRouteName =
|
|
164
|
+
prevRouteKey && !isAutoGeneratedRouteName(prevRouteKey)
|
|
165
|
+
? prevRouteKey
|
|
166
|
+
: undefined;
|
|
167
|
+
// What the predicates read of the page navigated away from. A segment they
|
|
168
|
+
// keep then stays held for the pages with equal values of those reads only:
|
|
169
|
+
// a prefetch of the answer, adopted on a page with other values, would keep
|
|
170
|
+
// that page's copy (#1331). A re-render is source-safe, it sends the
|
|
171
|
+
// segment. Reads before a throw count: the fallback is decided after them.
|
|
172
|
+
const reads = createSourceReads();
|
|
173
|
+
let recordedParams: Record<string, string> | undefined;
|
|
174
|
+
// A rule read `defaultShouldRevalidate`.
|
|
175
|
+
let readDefault = false;
|
|
176
|
+
/** `usedDefault`: the decision is the default's, or a rule read it. */
|
|
177
|
+
function decide(
|
|
178
|
+
finalVal: boolean,
|
|
179
|
+
reason: string,
|
|
180
|
+
usedDefault: boolean,
|
|
181
|
+
): boolean {
|
|
158
182
|
if (floorTrue && !finalVal) {
|
|
159
183
|
pushTrace(defaultShouldRevalidate, true, `${reason}:floored`);
|
|
160
184
|
return true;
|
|
161
185
|
}
|
|
186
|
+
if (!finalVal && hasSourceReads(reads)) {
|
|
187
|
+
// The navigation default of a route segment, and of a segment the route
|
|
188
|
+
// owns, compares the target's params and search with the source's, and
|
|
189
|
+
// no read records that: a page grouped by the rules' reads but with
|
|
190
|
+
// other params would adopt the keep. Seen from a page whose params
|
|
191
|
+
// equal a rewritten target's (/en/p/1 prefetching /deals, which renders
|
|
192
|
+
// /en/p/1): grouped by locale, /en/p/2 adopted the answer and kept
|
|
193
|
+
// product 2 at /deals.
|
|
194
|
+
if (usedDefault && defaultComparesSource) reads.url = true;
|
|
195
|
+
markSourceHeld(context as InternalHandlerContext<any, TEnv>, reads, {
|
|
196
|
+
params: currentParams,
|
|
197
|
+
routeName: fromRouteName,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
162
200
|
pushTrace(defaultShouldRevalidate, finalVal, reason);
|
|
163
201
|
return finalVal;
|
|
164
202
|
}
|
|
165
203
|
|
|
166
204
|
let defaultShouldRevalidate: boolean;
|
|
167
205
|
let defaultReason: string;
|
|
206
|
+
// The default hangs on paramsChanged / searchChanged (the nav lane below).
|
|
207
|
+
let defaultComparesSource = false;
|
|
168
208
|
|
|
169
209
|
if (defaultOverride) {
|
|
170
210
|
defaultShouldRevalidate = defaultOverride.value;
|
|
@@ -188,6 +228,8 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
188
228
|
defaultReason = reasons.parentChain;
|
|
189
229
|
}
|
|
190
230
|
} else {
|
|
231
|
+
defaultComparesSource =
|
|
232
|
+
segment.type === "route" || segment.belongsToRoute === true;
|
|
191
233
|
if (segment.type === "route") {
|
|
192
234
|
const routeChanged = paramsChanged || searchChanged;
|
|
193
235
|
defaultShouldRevalidate = routeChanged;
|
|
@@ -236,20 +278,16 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
236
278
|
segmentId: segment.id,
|
|
237
279
|
});
|
|
238
280
|
}
|
|
239
|
-
return decide(defaultShouldRevalidate, defaultReason);
|
|
281
|
+
return decide(defaultShouldRevalidate, defaultReason, true);
|
|
240
282
|
}
|
|
241
283
|
|
|
242
284
|
const prevSegment = getPrevSegment ? await getPrevSegment() : null;
|
|
285
|
+
currentParams = prevSegment?.params || prevParams;
|
|
243
286
|
|
|
244
287
|
let currentSuggestion = defaultShouldRevalidate;
|
|
245
288
|
|
|
246
289
|
const toRouteName =
|
|
247
290
|
routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
|
|
248
|
-
const prevRouteKey = reqCtx?._prevRouteKey;
|
|
249
|
-
const fromRouteName =
|
|
250
|
-
prevRouteKey && !isAutoGeneratedRouteName(prevRouteKey)
|
|
251
|
-
? prevRouteKey
|
|
252
|
-
: undefined;
|
|
253
291
|
|
|
254
292
|
const isActionFn = makeIsAction(actionContext?.actionId);
|
|
255
293
|
|
|
@@ -257,11 +295,19 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
257
295
|
let result: any;
|
|
258
296
|
try {
|
|
259
297
|
result = fn({
|
|
260
|
-
currentParams
|
|
261
|
-
|
|
298
|
+
get currentParams() {
|
|
299
|
+
return (recordedParams ??= recordingParams(currentParams, reads));
|
|
300
|
+
},
|
|
301
|
+
get currentUrl() {
|
|
302
|
+
reads.url = true;
|
|
303
|
+
return prevUrl;
|
|
304
|
+
},
|
|
262
305
|
nextParams,
|
|
263
306
|
nextUrl,
|
|
264
|
-
defaultShouldRevalidate
|
|
307
|
+
get defaultShouldRevalidate() {
|
|
308
|
+
readDefault = true;
|
|
309
|
+
return currentSuggestion;
|
|
310
|
+
},
|
|
265
311
|
context,
|
|
266
312
|
// Segment metadata (which segment is being evaluated)
|
|
267
313
|
segmentType: segment.type,
|
|
@@ -275,7 +321,10 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
275
321
|
formData: actionContext?.formData,
|
|
276
322
|
method: request.method,
|
|
277
323
|
routeName: toRouteName,
|
|
278
|
-
fromRouteName
|
|
324
|
+
get fromRouteName() {
|
|
325
|
+
reads.route = true;
|
|
326
|
+
return fromRouteName;
|
|
327
|
+
},
|
|
279
328
|
toRouteName,
|
|
280
329
|
stale: cacheStale ? true : stale,
|
|
281
330
|
});
|
|
@@ -326,7 +375,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
326
375
|
revalidator: name,
|
|
327
376
|
revalidate: result,
|
|
328
377
|
});
|
|
329
|
-
return decide(result, `hard:${name}
|
|
378
|
+
return decide(result, `hard:${name}`, readDefault);
|
|
330
379
|
} else if (
|
|
331
380
|
result &&
|
|
332
381
|
typeof result === "object" &&
|
|
@@ -352,5 +401,6 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
352
401
|
revalidate: currentSuggestion,
|
|
353
402
|
});
|
|
354
403
|
const softNames = revalidations.map((r) => r.name).join(",");
|
|
355
|
-
|
|
404
|
+
// A soft result may carry the default it read on: counted as the default's.
|
|
405
|
+
return decide(currentSuggestion, `soft-chain:${softNames}`, true);
|
|
356
406
|
}
|
|
@@ -94,10 +94,11 @@ export interface RouterContext<TEnv = any> {
|
|
|
94
94
|
) => CacheScope | null;
|
|
95
95
|
|
|
96
96
|
findInterceptForRoute: (
|
|
97
|
-
|
|
97
|
+
routeKeys: string | readonly string[],
|
|
98
98
|
parentEntry: EntryData | null,
|
|
99
99
|
selectorContext: InterceptSelectorContext,
|
|
100
100
|
isAction: boolean,
|
|
101
|
+
held?: ReadonlySet<string>,
|
|
101
102
|
) => InterceptResult | null;
|
|
102
103
|
|
|
103
104
|
/** The segment walker; a full match passes freshPassInput (FRESH). */
|
|
@@ -535,9 +535,12 @@ export interface RangoOptions<TEnv = any> {
|
|
|
535
535
|
* TTL (in seconds) for the client-side in-memory prefetch cache.
|
|
536
536
|
*
|
|
537
537
|
* Controls how long prefetch responses are kept in the client-side
|
|
538
|
-
* in-memory cache.
|
|
539
|
-
*
|
|
540
|
-
*
|
|
538
|
+
* in-memory cache. A prefetch with nothing live in it (a document cache
|
|
539
|
+
* HIT, or every sent segment from a `cache()` record or a Prerender entry,
|
|
540
|
+
* with no loader) also carries `Cache-Control: private, max-age=<ttl>`,
|
|
541
|
+
* so the browser's HTTP cache keeps it as long. Any other prefetch carries
|
|
542
|
+
* none: a prefetch whose render failed after its 200 went out would
|
|
543
|
+
* otherwise answer later requests.
|
|
541
544
|
*
|
|
542
545
|
* The cache is automatically invalidated on server actions regardless
|
|
543
546
|
* of TTL, so this is primarily a staleness safety net.
|
|
@@ -285,9 +285,10 @@ export function routeLoaderLanes(
|
|
|
285
285
|
* records. A capture serves none: its bake-lane loaders execute.
|
|
286
286
|
*
|
|
287
287
|
* The one answer to "is this loader served from the shell record on this
|
|
288
|
-
* request", for its value (resolveLoaderData)
|
|
289
|
-
* its handle pushes (loaderPins)
|
|
290
|
-
*
|
|
288
|
+
* request", for its value (resolveLoaderData), for the record's copies of
|
|
289
|
+
* its handle pushes (loaderPins) and for whether a prefetch defers it
|
|
290
|
+
* (servesLoaderPin): all look the loader up here and nowhere else. A
|
|
291
|
+
* condition that makes pins, or one loader's pin, not apply belongs
|
|
291
292
|
* here or in the seed's decode (shell-snapshot.ts buildShellLoaderSeed),
|
|
292
293
|
* never after a lookup, or the two disagree: issues #1001 and #1003 were the
|
|
293
294
|
* pushes decided from the request's type while the value was decided from
|
|
@@ -299,6 +300,23 @@ function servedPins(
|
|
|
299
300
|
return isShellCaptureActive(reqCtx) ? undefined : reqCtx?._shellLoaderSeed;
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
/**
|
|
304
|
+
* This request serves `loaderEntry` from a pin (servedPins), so its body does
|
|
305
|
+
* not decide the value: resolveLoaderData serves a pin to an `ssr: false`
|
|
306
|
+
* registration only. A prefetch does not defer such a loader
|
|
307
|
+
* (prefetch-deferral.ts defersLoader): nothing would run, and the pin is the
|
|
308
|
+
* value the document shows.
|
|
309
|
+
*/
|
|
310
|
+
export function servesLoaderPin(
|
|
311
|
+
loaderEntry: LoaderEntry,
|
|
312
|
+
reqCtx: RequestContext<any> | undefined = _getRequestContext(),
|
|
313
|
+
): boolean {
|
|
314
|
+
return (
|
|
315
|
+
loaderEntry.bake === true &&
|
|
316
|
+
servedPins(reqCtx)?.has(loaderEntry.loader.$$id) === true
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
302
320
|
/**
|
|
303
321
|
* What a record restored on a request over the matched chain `entries` is to
|
|
304
322
|
* each loader's handle pushes (HandleStore RecordAuthority), from the pins
|
|
@@ -32,27 +32,41 @@ import {
|
|
|
32
32
|
type EntryData,
|
|
33
33
|
type InterceptEntry,
|
|
34
34
|
type LoaderEntry,
|
|
35
|
+
segmentPinOf,
|
|
35
36
|
} from "../../server/context.js";
|
|
36
37
|
import { requestHeaders } from "../../server/request-headers.js";
|
|
37
38
|
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
38
39
|
import type { CacheScope } from "../../cache/cache-scope.js";
|
|
39
|
-
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
40
|
+
import { PREFETCH_HEADER, PREFETCH_INTENT } from "../../prefetch-markers.js";
|
|
40
41
|
import { WIRE } from "../../wire-params.js";
|
|
41
42
|
import { isInterceptSegment } from "../../browser/intercept-utils.js";
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* What a request says it is, read once when its context is created
|
|
45
46
|
* (RequestContext._requestKind). A fill is marked on the raw URL and wins
|
|
46
|
-
* over the prefetch header.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* over the prefetch header. `"prefetch"` is a speculative prefetch (viewport,
|
|
48
|
+
* render) and `"intent"` a hover or an explicit `prefetch()` call
|
|
49
|
+
* (PREFETCH_INTENT): both are prefetches, and only the first defers. Whether
|
|
50
|
+
* a prefetch defers anything is decided per match, by partialDeferralMode: a
|
|
51
|
+
* shell capture inherits the context of the request that scheduled it and is
|
|
52
|
+
* never a prefetch.
|
|
49
53
|
*/
|
|
50
54
|
export function requestKind(
|
|
51
55
|
request: Request,
|
|
52
56
|
rawUrl: URL,
|
|
53
|
-
):
|
|
57
|
+
): RequestKind | undefined {
|
|
54
58
|
if (rawUrl.searchParams.has(WIRE.fill)) return "fill";
|
|
55
|
-
|
|
59
|
+
const prefetch = requestHeaders(request).get(PREFETCH_HEADER);
|
|
60
|
+
if (prefetch === null) return undefined;
|
|
61
|
+
return prefetch === PREFETCH_INTENT ? "intent" : "prefetch";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** requestKind's answer for a marked request. */
|
|
65
|
+
export type RequestKind = "prefetch" | "intent" | "fill";
|
|
66
|
+
|
|
67
|
+
/** A prefetch of either kind: speculative or intent. */
|
|
68
|
+
export function isPrefetchKind(kind: RequestKind | undefined): boolean {
|
|
69
|
+
return kind === "prefetch" || kind === "intent";
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
/** Static, per matched chain. */
|
|
@@ -173,7 +187,6 @@ const EMPTY_SCOPE: DeferralScope = {
|
|
|
173
187
|
};
|
|
174
188
|
|
|
175
189
|
const scopes = new WeakMap<EntryData, DeferralScope>();
|
|
176
|
-
const warnedBakeLoaders = new Set<string>();
|
|
177
190
|
|
|
178
191
|
/**
|
|
179
192
|
* The deferral scope of a matched chain, memoized on its leaf entry (entries
|
|
@@ -192,9 +205,6 @@ export function resolveDeferralScope(
|
|
|
192
205
|
const loaderIds = new Set<string>();
|
|
193
206
|
const units = new Map<EntryData, DeferralUnit>();
|
|
194
207
|
const chain = new Set(entries);
|
|
195
|
-
// ssr: false on a ppr route is the bake lane (loader-cache.ts
|
|
196
|
-
// resolveLoaderData): shell material, served from the record.
|
|
197
|
-
const pprRoute = isPprEntry(leaf);
|
|
198
208
|
// A clientUrls() group route renders loading() itself and its handler is
|
|
199
209
|
// ClientUrlsRoot (segment-system.tsx keys it by group): a unit would hand
|
|
200
210
|
// React a promise in place of the optimistic destination on screen. So
|
|
@@ -214,20 +224,8 @@ export function resolveDeferralScope(
|
|
|
214
224
|
for (const loaderEntry of entry.loader ?? []) {
|
|
215
225
|
const own = loaderEntry.prefetch === false;
|
|
216
226
|
if (!own && behind.length === 0) continue;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
process.env.NODE_ENV !== "production" &&
|
|
220
|
-
loaderEntry.prefetch === false &&
|
|
221
|
-
!warnedBakeLoaders.has(loaderEntry.loader.$$id)
|
|
222
|
-
) {
|
|
223
|
-
warnedBakeLoaders.add(loaderEntry.loader.$$id);
|
|
224
|
-
console.warn(
|
|
225
|
-
`[rango] loader "${loaderEntry.loader.$$id}" is registered with ssr: false and prefetch: false on a ppr route ("${leaf.id}"). ` +
|
|
226
|
-
`Under ppr an ssr: false loader is baked into the shell and served from it, so prefetch: false is ignored there. Drop one of the two options.`,
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
227
|
+
// An ssr: false loader on a ppr route is in scope too: whether this
|
|
228
|
+
// request serves it from a shell pin is per request (defersLoader).
|
|
231
229
|
loaders.set(loaderEntry, own ? true : behind);
|
|
232
230
|
loaderIds.add(loaderEntry.loader.$$id);
|
|
233
231
|
}
|
|
@@ -303,7 +301,9 @@ export function resolveInterceptDeferralScope(
|
|
|
303
301
|
|
|
304
302
|
/**
|
|
305
303
|
* Which half of the feature a partial request is, if either (`fill` and
|
|
306
|
-
* `prefetch` are requestKind's answer).
|
|
304
|
+
* `prefetch` are requestKind's answer). `prefetch` is a speculative prefetch
|
|
305
|
+
* only: an intent prefetch defers nothing, like the navigation it starts
|
|
306
|
+
* early (R16). Both halves are GETs: a request of
|
|
307
307
|
* another method that carries either marker is a normal request. A prefetch
|
|
308
308
|
* defers only on a plain GET navigation: an action re-renders what it
|
|
309
309
|
* changed, and a shell capture is never a prefetch whatever the request that
|
|
@@ -496,7 +496,7 @@ export function startDeferredLoader(
|
|
|
496
496
|
/**
|
|
497
497
|
* The segment a run of `loaderId` files its handle pushes under: the one
|
|
498
498
|
* that registers it when it is registered (registerDeferredLoader), else the
|
|
499
|
-
* segment
|
|
499
|
+
* segment whose code starts it (segmentPinOf).
|
|
500
500
|
*/
|
|
501
501
|
export function loaderOwner(
|
|
502
502
|
ctx: HandlerContext<any, any>,
|
|
@@ -504,8 +504,7 @@ export function loaderOwner(
|
|
|
504
504
|
): string | undefined {
|
|
505
505
|
const internal = ctx as InternalHandlerContext<any, any>;
|
|
506
506
|
return (
|
|
507
|
-
internal._deferredLoaders?.get(loaderId)?.owner ??
|
|
508
|
-
internal._currentSegmentId
|
|
507
|
+
internal._deferredLoaders?.get(loaderId)?.owner ?? segmentPinOf(internal)
|
|
509
508
|
);
|
|
510
509
|
}
|
|
511
510
|
|
|
@@ -514,18 +513,26 @@ export function loaderOwner(
|
|
|
514
513
|
* does not hold that segment, and the loader is flagged itself or sits behind
|
|
515
514
|
* the fallback of a flagged entry the client does not hold either. Under a
|
|
516
515
|
* flagged layout the client holds, a new route's loaders run.
|
|
516
|
+
*
|
|
517
|
+
* `servesPin` (loader-cache.ts servesLoaderPin) answers whether this request
|
|
518
|
+
* serves the loader from a pin of a document-captured shell. Such a loader
|
|
519
|
+
* is not deferred: its body does not decide the value, and the pin is what
|
|
520
|
+
* the document shows. On every other request it would run, so it is
|
|
521
|
+
* deferred as on a route without ppr. Asked when loaders resolve, after a
|
|
522
|
+
* replay HIT has armed the pins (rsc-rendering.ts matchPartialWithPprReplay).
|
|
517
523
|
*/
|
|
518
524
|
export function defersLoader(
|
|
519
525
|
ctx: HandlerContext<any, any>,
|
|
520
526
|
loaderEntry: LoaderEntry,
|
|
521
527
|
segmentId: string,
|
|
528
|
+
servesPin: (loaderEntry: LoaderEntry) => boolean,
|
|
522
529
|
): boolean {
|
|
523
530
|
const plan = getPrefetchDeferral(ctx);
|
|
524
531
|
if (plan?.mode !== "prefetch" || plan.held.has(segmentId)) return false;
|
|
525
532
|
const by = plan.scope.loaders.get(loaderEntry);
|
|
526
|
-
|
|
527
|
-
by === true || (by !== undefined && by.some((id) => !plan.held.has(id)))
|
|
528
|
-
);
|
|
533
|
+
const deferrable =
|
|
534
|
+
by === true || (by !== undefined && by.some((id) => !plan.held.has(id)));
|
|
535
|
+
return deferrable && !servesPin(loaderEntry);
|
|
529
536
|
}
|
|
530
537
|
|
|
531
538
|
/** A unit whose handler output is not stored: a prefetch can skip it. */
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
pushRevalidationTraceEntry,
|
|
41
41
|
isTraceActive,
|
|
42
42
|
} from "../logging.js";
|
|
43
|
-
import { resolveLoaderData } from "./loader-cache.js";
|
|
43
|
+
import { resolveLoaderData, servesLoaderPin } from "./loader-cache.js";
|
|
44
44
|
import {
|
|
45
45
|
entryLoadingMasksLoaders,
|
|
46
46
|
isShellCaptureActive,
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
noteThrower,
|
|
66
66
|
} from "./helpers.js";
|
|
67
67
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
68
|
+
import { isRenderableLoading } from "../../types/boundaries.js";
|
|
68
69
|
import { isLayoutLoaderCopy } from "../../client-urls/layout-loader-copies.js";
|
|
69
70
|
import { getRouterContext } from "../router-context.js";
|
|
70
71
|
import { observeEvent, observeSegmentHandler } from "../instrument.js";
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
track,
|
|
75
76
|
RangoContext,
|
|
76
77
|
runInsideLoaderScope,
|
|
78
|
+
runAsSegment,
|
|
77
79
|
latchCachedHeaderScope,
|
|
78
80
|
latchPprHeaderScopeForEntries,
|
|
79
81
|
} from "../../server/context.js";
|
|
@@ -267,6 +269,28 @@ export function ownerOf(
|
|
|
267
269
|
};
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
/**
|
|
273
|
+
* A route's loader copies inherited into a child layout with parallel slots
|
|
274
|
+
* are redundant beside the route's own loader segments: true when the route
|
|
275
|
+
* renders no loading() fallback on any request, so nothing hides the route's
|
|
276
|
+
* copy from useLoader() in the slots. A redundant copy is never resolved, on
|
|
277
|
+
* any request: the walkers check this before they inherit.
|
|
278
|
+
*
|
|
279
|
+
* Decided from the entry, never from the route segment's `loading`, which
|
|
280
|
+
* differs between requests: `loading(fallback, { ssr: false })` is `null` on
|
|
281
|
+
* a document (applyDocumentModeLoading) and the fallback on partials, and the
|
|
282
|
+
* walkers emit `loading(false)` as `null` too. A copy sent on one request and
|
|
283
|
+
* not on the next gives the layout a loader entry it did not have, so
|
|
284
|
+
* renderSegments wraps it in StreamedLoaderErrorBoundary and React remounts
|
|
285
|
+
* it with its Outlet (docs/tree-structure.md). Resolving a redundant copy and
|
|
286
|
+
* dropping it from the payload later is not enough: when the route's own
|
|
287
|
+
* loader is held (its revalidate() declines), the copy is new to the client,
|
|
288
|
+
* so it re-runs and is sent without the route's copy beside it.
|
|
289
|
+
*/
|
|
290
|
+
export function inheritedCopyRedundant(route: EntryData): boolean {
|
|
291
|
+
return !isRenderableLoading(route.loading) && route.loadingSsr !== false;
|
|
292
|
+
}
|
|
293
|
+
|
|
270
294
|
/**
|
|
271
295
|
* resolveOwnerLoaders has something to resolve for `entry`'s loaders. The
|
|
272
296
|
* walker checks before the call: most entries own none, and the async call
|
|
@@ -526,10 +550,14 @@ export async function resolveOwnerLoaders<TEnv>(
|
|
|
526
550
|
// Only a loader on a segment the client lacks (defersLoader). It
|
|
527
551
|
// would have run. Registered, not started: it runs, and reads its
|
|
528
552
|
// cache(), only if a reader starts it (startDeferredLoader).
|
|
529
|
-
if (defersLoader(ctx, loaderEntry, segmentId)) {
|
|
553
|
+
if (defersLoader(ctx, loaderEntry, segmentId, servesLoaderPin)) {
|
|
530
554
|
registerDeferredLoader(ctx, loader.$$id, shortCode, () =>
|
|
531
555
|
kickoff(loaderEntry, segmentId),
|
|
532
556
|
);
|
|
557
|
+
// A ppr shell record this request restored keeps an ssr: false
|
|
558
|
+
// loader's pushes as copies that wait for its run (loaderPins "hole").
|
|
559
|
+
// No run is made here: drop them, the fill sends the run's pushes.
|
|
560
|
+
_getRequestContext()?._handleStore?.settleLoaderRun(loader.$$id);
|
|
533
561
|
const deferred: ResolvedSegment = {
|
|
534
562
|
id: segmentId,
|
|
535
563
|
namespace: owner.namespace,
|
|
@@ -656,7 +684,8 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
656
684
|
entry.type === "route" &&
|
|
657
685
|
entry.loader &&
|
|
658
686
|
entry.loader.length > 0 &&
|
|
659
|
-
Object.keys(layoutEntry.parallel).length > 0
|
|
687
|
+
Object.keys(layoutEntry.parallel).length > 0 &&
|
|
688
|
+
!inheritedCopyRedundant(entry)
|
|
660
689
|
) {
|
|
661
690
|
const inherited = await resolveOwnerLoaders(
|
|
662
691
|
pass,
|
|
@@ -666,7 +695,6 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
666
695
|
for (const seg of inherited.segments) {
|
|
667
696
|
if (!seenIds.has(seg.id)) {
|
|
668
697
|
seenIds.add(seg.id);
|
|
669
|
-
seg._inherited = true;
|
|
670
698
|
allLoaderSegments.push(seg);
|
|
671
699
|
}
|
|
672
700
|
}
|
|
@@ -805,7 +833,7 @@ async function resolveParallelSlotComponent<TEnv>(
|
|
|
805
833
|
// A throw before the slot streams reaches the owning entry's boundary.
|
|
806
834
|
try {
|
|
807
835
|
if (hasLoadingFallback) {
|
|
808
|
-
const result = run
|
|
836
|
+
const result = runAsSegment(parallelId, run);
|
|
809
837
|
if (result instanceof Promise) {
|
|
810
838
|
warnOnStreamedResponse(result, parallelId);
|
|
811
839
|
result.finally(doneHandler).catch(() => {});
|
|
@@ -1174,7 +1202,9 @@ async function resolveRouteComponent<TEnv>(
|
|
|
1174
1202
|
}
|
|
1175
1203
|
if (!actionContext) {
|
|
1176
1204
|
const result = handleHandlerResult(
|
|
1177
|
-
|
|
1205
|
+
runAsSegment(entry.shortCode, () =>
|
|
1206
|
+
observeSegmentHandler(entry.shortCode, entry.id, handler, context),
|
|
1207
|
+
),
|
|
1178
1208
|
);
|
|
1179
1209
|
if (result instanceof Promise) {
|
|
1180
1210
|
warnOnStreamedResponse(result, entry.id);
|
|
@@ -1475,17 +1505,14 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
1475
1505
|
parentRouteEntry &&
|
|
1476
1506
|
parentRouteEntry.loader &&
|
|
1477
1507
|
parentRouteEntry.loader.length > 0 &&
|
|
1478
|
-
Object.keys(orphan.parallel).length > 0
|
|
1508
|
+
Object.keys(orphan.parallel).length > 0 &&
|
|
1509
|
+
!inheritedCopyRedundant(parentRouteEntry)
|
|
1479
1510
|
) {
|
|
1480
1511
|
const inheritedResult = await resolveOwnerLoaders(
|
|
1481
1512
|
pass,
|
|
1482
1513
|
ownerOf(parentRouteEntry, orphan.shortCode),
|
|
1483
1514
|
belongsToRoute,
|
|
1484
1515
|
);
|
|
1485
|
-
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
1486
|
-
for (const s of inheritedResult.segments) {
|
|
1487
|
-
s._inherited = true;
|
|
1488
|
-
}
|
|
1489
1516
|
segments.push(...inheritedResult.segments);
|
|
1490
1517
|
matchedIds.push(...inheritedResult.matchedIds);
|
|
1491
1518
|
}
|
|
@@ -1505,7 +1532,8 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
1505
1532
|
handlerRan = true;
|
|
1506
1533
|
// Its handler's pushes are its own. Pinned only while it runs: a route
|
|
1507
1534
|
// under loading() streams its handler, whose pushes after an await
|
|
1508
|
-
// read the pin after this returns
|
|
1535
|
+
// read the pin after this returns where a runtime lost its
|
|
1536
|
+
// runAsSegment scope (segmentPinOf).
|
|
1509
1537
|
const internal = context as InternalHandlerContext<any, TEnv>;
|
|
1510
1538
|
const pinned = internal._currentSegmentId;
|
|
1511
1539
|
internal._currentSegmentId = orphan.shortCode;
|