@rangojs/router 0.32.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 -10173
- 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/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 +15 -6
- 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/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/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 +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -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 +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- 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 +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- 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 +26 -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 +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -18
- 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/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 +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- 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/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 +27 -0
- package/src/cache/document-cache.ts +5 -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/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 +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- 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 +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- 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 +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -239,6 +239,11 @@ export interface MatchPipelineState {
|
|
|
239
239
|
// Whether cache should be revalidated (SWR)
|
|
240
240
|
shouldRevalidate?: boolean;
|
|
241
241
|
|
|
242
|
+
// Whether the prerender entry that answered is past its soft staleAt: it
|
|
243
|
+
// serves while a refresh is due, so it is not what the page will hold once
|
|
244
|
+
// that lands (MatchResult.fromStorage counts it live)
|
|
245
|
+
storedStale?: boolean;
|
|
246
|
+
|
|
242
247
|
// Source of cache hit ("runtime" or "prerender")
|
|
243
248
|
cacheSource?: "runtime" | "prerender";
|
|
244
249
|
|
|
@@ -155,10 +155,11 @@ export interface MatchHandlerDeps<TEnv = any> {
|
|
|
155
155
|
defaultErrorBoundary: ReactNode | ErrorBoundaryHandler | undefined;
|
|
156
156
|
findMatch: (pathname: string, ms?: any) => any;
|
|
157
157
|
findInterceptForRoute: (
|
|
158
|
-
|
|
158
|
+
routeKeys: string | readonly string[],
|
|
159
159
|
parentEntry: EntryData | null,
|
|
160
160
|
selectorContext: InterceptSelectorContext | null,
|
|
161
161
|
isAction: boolean,
|
|
162
|
+
held?: ReadonlySet<string>,
|
|
162
163
|
) => { intercept: InterceptEntry; entry: EntryData } | null;
|
|
163
164
|
telemetry?: TelemetrySink;
|
|
164
165
|
/**
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
import { ShellRecordUnavailableError } from "../../cache/shell-snapshot.js";
|
|
84
84
|
import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
|
|
85
85
|
import { paramsEqual } from "../params-util.js";
|
|
86
|
+
import { markSourceHeld, readsOfAllParams } from "../source-reads.js";
|
|
86
87
|
import { requestHeaders } from "../../server/request-headers.js";
|
|
87
88
|
import { isInterceptSegment } from "../../browser/intercept-utils.js";
|
|
88
89
|
import {
|
|
@@ -144,7 +145,8 @@ async function ensurePrerenderDeps(): Promise<void> {
|
|
|
144
145
|
* - `prerender-build`: the bundled build manifest (immutable).
|
|
145
146
|
* - `prerender-overlay`: the writable on-demand overlay. Its entry may be
|
|
146
147
|
* newer than the client's copy: one that a refresh (possibly inside this
|
|
147
|
-
* very action) replaced.
|
|
148
|
+
* very action) replaced. `stale`: it is past its soft `staleAt` (serves
|
|
149
|
+
* while a refresh is due).
|
|
148
150
|
*/
|
|
149
151
|
type StoredSegmentSource =
|
|
150
152
|
| {
|
|
@@ -154,7 +156,8 @@ type StoredSegmentSource =
|
|
|
154
156
|
scope: CacheScope;
|
|
155
157
|
restoreHandles?: (sentIds: ReadonlySet<string>) => Promise<void>;
|
|
156
158
|
}
|
|
157
|
-
| { kind: "prerender-build"
|
|
159
|
+
| { kind: "prerender-build"; entry: PrerenderEntry }
|
|
160
|
+
| { kind: "prerender-overlay"; entry: PrerenderEntry; stale: boolean };
|
|
158
161
|
|
|
159
162
|
/**
|
|
160
163
|
* Decode a prerender entry's segments and replay its handle data into the
|
|
@@ -339,12 +342,15 @@ function storedEntrySends<TEnv>(
|
|
|
339
342
|
ctx.clientSegmentSet.size > 0
|
|
340
343
|
? router.buildEntryRevalidateMap?.(ctx.entries)
|
|
341
344
|
: undefined;
|
|
342
|
-
// A keep that hangs on the source's params is right for
|
|
343
|
-
// scope a prefetch of the answer to
|
|
344
|
-
//
|
|
345
|
+
// A keep that hangs on the source's params is right for the pages with
|
|
346
|
+
// the same params only: scope a prefetch of the answer to them, as a
|
|
347
|
+
// predicate that compared every param would (#1331).
|
|
345
348
|
const heldForSource = (): false => {
|
|
346
|
-
(
|
|
347
|
-
|
|
349
|
+
markSourceHeld(
|
|
350
|
+
ctx.handlerContext as InternalHandlerContext<any, TEnv>,
|
|
351
|
+
readsOfAllParams(ctx.prevParams),
|
|
352
|
+
{ params: ctx.prevParams, routeName: undefined },
|
|
353
|
+
);
|
|
348
354
|
return false;
|
|
349
355
|
};
|
|
350
356
|
const decide = (segment: ResolvedSegment): boolean | Promise<boolean> => {
|
|
@@ -563,6 +569,11 @@ async function* serveStoredSegments<TEnv>(
|
|
|
563
569
|
state.cacheHit = true;
|
|
564
570
|
state.cacheSource = fromCache ? "runtime" : "prerender";
|
|
565
571
|
if (fromCache) state.shouldRevalidate = source.shouldRevalidate;
|
|
572
|
+
// Not shouldRevalidate: that one starts the cache() scope's background
|
|
573
|
+
// revalidation.
|
|
574
|
+
if (source.kind === "prerender-overlay" && source.stale) {
|
|
575
|
+
state.storedStale = true;
|
|
576
|
+
}
|
|
566
577
|
state.cachedSegments = segments;
|
|
567
578
|
const cachedMatchedIds = [...liveMatchedIds, ...segments.map((s) => s.id)];
|
|
568
579
|
state.cachedMatchedIds = cachedMatchedIds;
|
|
@@ -807,11 +818,8 @@ async function readPrerenderSource<TEnv>(
|
|
|
807
818
|
// A stale "removed" marker is rechecked like a stale page: only a
|
|
808
819
|
// refresh that hit notFound() (or declined) stamps one with a ttl or
|
|
809
820
|
// tags. A prerender.remove() marker has neither and never gets here.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
reqCtx &&
|
|
813
|
-
isStoredEntryStale(stored, Date.now())
|
|
814
|
-
) {
|
|
821
|
+
const stale = isStoredEntryStale(stored, Date.now());
|
|
822
|
+
if (overlay.config.onRevalidate && reqCtx && stale) {
|
|
815
823
|
scheduleOverlayRevalidation(
|
|
816
824
|
serializePrerenderKey(key),
|
|
817
825
|
overlay.config.onRevalidate,
|
|
@@ -833,7 +841,7 @@ async function readPrerenderSource<TEnv>(
|
|
|
833
841
|
)._prerenderRemoved = true;
|
|
834
842
|
return null;
|
|
835
843
|
}
|
|
836
|
-
return { kind: "prerender-overlay", entry: stored.entry };
|
|
844
|
+
return { kind: "prerender-overlay", entry: stored.entry, stale };
|
|
837
845
|
}
|
|
838
846
|
}
|
|
839
847
|
|
|
@@ -115,7 +115,6 @@ import type {
|
|
|
115
115
|
import { isPlaceholder } from "../types/segments.js";
|
|
116
116
|
import { interceptMatched } from "../browser/intercept-utils.js";
|
|
117
117
|
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
118
|
-
import { debugLog } from "./logging.js";
|
|
119
118
|
import { appendMetric } from "./metrics.js";
|
|
120
119
|
import {
|
|
121
120
|
collectInterceptTargetNames,
|
|
@@ -132,83 +131,11 @@ export async function collectSegments(
|
|
|
132
131
|
return segments;
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
/**
|
|
136
|
-
* Deduplicate inherited loader segments by loaderId.
|
|
137
|
-
*
|
|
138
|
-
* When a route has loaders and a child layout has parallel slots, the same
|
|
139
|
-
* loader is resolved twice: once for the route and once inherited into the
|
|
140
|
-
* layout (tagged with `_inherited`). The inherited copy is only needed when
|
|
141
|
-
* the route uses `loading()` — in that case, the loader data is inside a
|
|
142
|
-
* LoaderBoundary/Suspense that parallel slots can't reach through. Without
|
|
143
|
-
* loading(), useLoader() traverses parent contexts and finds the data.
|
|
144
|
-
*/
|
|
145
|
-
function deduplicateLoaderSegments(
|
|
146
|
-
segments: ResolvedSegment[],
|
|
147
|
-
logPrefix: string,
|
|
148
|
-
): { segments: ResolvedSegment[]; removedIds: Set<string> } {
|
|
149
|
-
// Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
|
|
150
|
-
// namespace, and namespaces of segments that declare loading().
|
|
151
|
-
const originalLoaders = new Set<string>();
|
|
152
|
-
const loaderIdsByNamespace = new Map<string, string[]>();
|
|
153
|
-
const namespacesWithLoading = new Set<string>();
|
|
154
|
-
for (const s of segments) {
|
|
155
|
-
if (s.type === "loader" && s.loaderId) {
|
|
156
|
-
if (!s._inherited) originalLoaders.add(s.loaderId);
|
|
157
|
-
const ids = loaderIdsByNamespace.get(s.namespace);
|
|
158
|
-
if (ids) ids.push(s.loaderId);
|
|
159
|
-
else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
|
|
160
|
-
} else if (
|
|
161
|
-
s.type !== "loader" &&
|
|
162
|
-
s.loading !== undefined &&
|
|
163
|
-
s.loading !== false
|
|
164
|
-
) {
|
|
165
|
-
namespacesWithLoading.add(s.namespace);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const loadersWithLoading = new Set<string>();
|
|
170
|
-
for (const ns of namespacesWithLoading) {
|
|
171
|
-
for (const id of loaderIdsByNamespace.get(ns) ?? []) {
|
|
172
|
-
loadersWithLoading.add(id);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const result: ResolvedSegment[] = [];
|
|
177
|
-
const removedIds = new Set<string>();
|
|
178
|
-
|
|
179
|
-
for (const s of segments) {
|
|
180
|
-
if (
|
|
181
|
-
s.type === "loader" &&
|
|
182
|
-
s.loaderId &&
|
|
183
|
-
s._inherited &&
|
|
184
|
-
originalLoaders.has(s.loaderId) &&
|
|
185
|
-
!loadersWithLoading.has(s.loaderId)
|
|
186
|
-
) {
|
|
187
|
-
removedIds.add(s.id);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
result.push(s);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (removedIds.size > 0) {
|
|
194
|
-
debugLog(
|
|
195
|
-
logPrefix,
|
|
196
|
-
`deduped ${removedIds.size} inherited loader segment(s)`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return { segments: result, removedIds };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
134
|
export function buildMatchResult<TEnv>(
|
|
204
135
|
allSegments: ResolvedSegment[],
|
|
205
136
|
ctx: MatchContext<TEnv>,
|
|
206
137
|
state: MatchPipelineState,
|
|
207
138
|
): MatchResult {
|
|
208
|
-
const logPrefix = ctx.isFullMatch
|
|
209
|
-
? "[Router.match]"
|
|
210
|
-
: "[Router.matchPartial]";
|
|
211
|
-
|
|
212
139
|
let allIds: string[];
|
|
213
140
|
let segmentsToRender: ResolvedSegment[];
|
|
214
141
|
let resolvedIds: string[];
|
|
@@ -271,19 +198,11 @@ export function buildMatchResult<TEnv>(
|
|
|
271
198
|
}
|
|
272
199
|
}
|
|
273
200
|
|
|
274
|
-
|
|
275
|
-
segmentsToRender,
|
|
276
|
-
logPrefix,
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const matchedIds =
|
|
280
|
-
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
281
|
-
|
|
282
|
-
// One pass over dedupedSegments: strip the internal _handlerRan marker and
|
|
201
|
+
// One pass over segmentsToRender: strip the internal _handlerRan marker and
|
|
283
202
|
// collect the diff ids (id is unchanged by the strip) together.
|
|
284
203
|
const cleanedSegments: ResolvedSegment[] = [];
|
|
285
204
|
const diff: string[] = [];
|
|
286
|
-
for (const s of
|
|
205
|
+
for (const s of segmentsToRender) {
|
|
287
206
|
if (s._handlerRan === undefined) {
|
|
288
207
|
cleanedSegments.push(s);
|
|
289
208
|
} else {
|
|
@@ -297,10 +216,13 @@ export function buildMatchResult<TEnv>(
|
|
|
297
216
|
// origin — the browser-local clientUrls matcher declines its optimistic
|
|
298
217
|
// presentation for these (see collectInterceptTargetNames).
|
|
299
218
|
const interceptTargets = collectInterceptTargetNames(ctx.manifestEntry);
|
|
219
|
+
const sourceHeld = (
|
|
220
|
+
ctx.handlerContext as InternalHandlerContext<any, TEnv> | undefined
|
|
221
|
+
)?._sourceHeld;
|
|
300
222
|
|
|
301
223
|
return {
|
|
302
224
|
segments: cleanedSegments,
|
|
303
|
-
matched:
|
|
225
|
+
matched: allIds,
|
|
304
226
|
diff,
|
|
305
227
|
resolvedIds,
|
|
306
228
|
params: ctx.matched.params,
|
|
@@ -308,15 +230,45 @@ export function buildMatchResult<TEnv>(
|
|
|
308
230
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
309
231
|
...(interceptTargets.length > 0 ? { interceptTargets } : {}),
|
|
310
232
|
...(ctx.interceptTargeted ? { interceptTargeted: true } : {}),
|
|
311
|
-
...(
|
|
312
|
-
|
|
313
|
-
? {
|
|
233
|
+
...(sourceHeld ? { sourceHeld } : {}),
|
|
234
|
+
...(!ctx.isFullMatch && sendsOnlyStored(segmentsToRender, state)
|
|
235
|
+
? { fromStorage: true as const }
|
|
314
236
|
: {}),
|
|
315
237
|
routeMiddleware:
|
|
316
238
|
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
317
239
|
};
|
|
318
240
|
}
|
|
319
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Whether a partial answer sends only what the cache lookup decoded from
|
|
244
|
+
* storage (MatchResult.fromStorage). By object, not by id: a
|
|
245
|
+
* segment the live walk resolved under a stored one's id (a live layout above
|
|
246
|
+
* a cache() boundary, an intercept resolved live) is not the stored copy.
|
|
247
|
+
* Any loader segment ran in this request, so it is live. Defaults to live:
|
|
248
|
+
* counted stored by mistake, a segment brings back the cached failures #1170
|
|
249
|
+
* removed. A stale cache() record (state.shouldRevalidate, refreshed in the
|
|
250
|
+
* background), a stale on-demand Prerender overlay entry (state.storedStale,
|
|
251
|
+
* which only the overlay can be: the build manifest never goes stale) and an
|
|
252
|
+
* answer that sends nothing count as live too: the browser would keep the
|
|
253
|
+
* first two past their refresh, and the third is a source page's answer.
|
|
254
|
+
*/
|
|
255
|
+
function sendsOnlyStored(
|
|
256
|
+
sent: readonly ResolvedSegment[],
|
|
257
|
+
state: MatchPipelineState,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (
|
|
260
|
+
!state.cacheHit ||
|
|
261
|
+
!state.cachedSegments ||
|
|
262
|
+
state.shouldRevalidate ||
|
|
263
|
+
state.storedStale ||
|
|
264
|
+
sent.length === 0
|
|
265
|
+
) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const stored = new Set(state.cachedSegments);
|
|
269
|
+
return sent.every((s) => s.type !== "loader" && stored.has(s));
|
|
270
|
+
}
|
|
271
|
+
|
|
320
272
|
export async function collectMatchResult<TEnv>(
|
|
321
273
|
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
322
274
|
ctx: MatchContext<TEnv>,
|
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,8 @@ import { _getRequestContext } from "../server/request-context.js";
|
|
|
21
21
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
22
22
|
import { paramsEqual } from "./params-util.js";
|
|
23
23
|
import { makeIsAction } from "./is-action.js";
|
|
24
|
+
import { createSourceReads, hasSourceReads } from "../prefetch-source-scope.js";
|
|
25
|
+
import { markSourceHeld, recordingParams } from "./source-reads.js";
|
|
24
26
|
|
|
25
27
|
const REVALIDATE_ALWAYS: ShouldRevalidateFn<any, any> = () => true;
|
|
26
28
|
const REVALIDATE_NEVER: ShouldRevalidateFn<any, any> = () => false;
|
|
@@ -155,18 +157,45 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
155
157
|
// second, contradicting entry would let flushRevalidationTrace count the
|
|
156
158
|
// segment as both revalidated and skipped.
|
|
157
159
|
const floorTrue = defaultOverride?.floor === true && defaultOverride.value;
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 {
|
|
164
182
|
if (floorTrue && !finalVal) {
|
|
165
183
|
pushTrace(defaultShouldRevalidate, true, `${reason}:floored`);
|
|
166
184
|
return true;
|
|
167
185
|
}
|
|
168
|
-
if (!finalVal &&
|
|
169
|
-
|
|
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
|
+
});
|
|
170
199
|
}
|
|
171
200
|
pushTrace(defaultShouldRevalidate, finalVal, reason);
|
|
172
201
|
return finalVal;
|
|
@@ -174,6 +203,8 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
174
203
|
|
|
175
204
|
let defaultShouldRevalidate: boolean;
|
|
176
205
|
let defaultReason: string;
|
|
206
|
+
// The default hangs on paramsChanged / searchChanged (the nav lane below).
|
|
207
|
+
let defaultComparesSource = false;
|
|
177
208
|
|
|
178
209
|
if (defaultOverride) {
|
|
179
210
|
defaultShouldRevalidate = defaultOverride.value;
|
|
@@ -197,6 +228,8 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
197
228
|
defaultReason = reasons.parentChain;
|
|
198
229
|
}
|
|
199
230
|
} else {
|
|
231
|
+
defaultComparesSource =
|
|
232
|
+
segment.type === "route" || segment.belongsToRoute === true;
|
|
200
233
|
if (segment.type === "route") {
|
|
201
234
|
const routeChanged = paramsChanged || searchChanged;
|
|
202
235
|
defaultShouldRevalidate = routeChanged;
|
|
@@ -245,40 +278,36 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
245
278
|
segmentId: segment.id,
|
|
246
279
|
});
|
|
247
280
|
}
|
|
248
|
-
return decide(defaultShouldRevalidate, defaultReason);
|
|
281
|
+
return decide(defaultShouldRevalidate, defaultReason, true);
|
|
249
282
|
}
|
|
250
283
|
|
|
251
284
|
const prevSegment = getPrevSegment ? await getPrevSegment() : null;
|
|
285
|
+
currentParams = prevSegment?.params || prevParams;
|
|
252
286
|
|
|
253
287
|
let currentSuggestion = defaultShouldRevalidate;
|
|
254
288
|
|
|
255
289
|
const toRouteName =
|
|
256
290
|
routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
|
|
257
|
-
const prevRouteKey = reqCtx?._prevRouteKey;
|
|
258
|
-
const fromRouteName =
|
|
259
|
-
prevRouteKey && !isAutoGeneratedRouteName(prevRouteKey)
|
|
260
|
-
? prevRouteKey
|
|
261
|
-
: undefined;
|
|
262
291
|
|
|
263
292
|
const isActionFn = makeIsAction(actionContext?.actionId);
|
|
264
293
|
|
|
265
|
-
const currentParams = prevSegment?.params || prevParams;
|
|
266
|
-
|
|
267
294
|
for (const { name, fn } of revalidations) {
|
|
268
295
|
let result: any;
|
|
269
296
|
try {
|
|
270
297
|
result = fn({
|
|
271
298
|
get currentParams() {
|
|
272
|
-
|
|
273
|
-
return currentParams;
|
|
299
|
+
return (recordedParams ??= recordingParams(currentParams, reads));
|
|
274
300
|
},
|
|
275
301
|
get currentUrl() {
|
|
276
|
-
|
|
302
|
+
reads.url = true;
|
|
277
303
|
return prevUrl;
|
|
278
304
|
},
|
|
279
305
|
nextParams,
|
|
280
306
|
nextUrl,
|
|
281
|
-
defaultShouldRevalidate
|
|
307
|
+
get defaultShouldRevalidate() {
|
|
308
|
+
readDefault = true;
|
|
309
|
+
return currentSuggestion;
|
|
310
|
+
},
|
|
282
311
|
context,
|
|
283
312
|
// Segment metadata (which segment is being evaluated)
|
|
284
313
|
segmentType: segment.type,
|
|
@@ -293,7 +322,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
293
322
|
method: request.method,
|
|
294
323
|
routeName: toRouteName,
|
|
295
324
|
get fromRouteName() {
|
|
296
|
-
|
|
325
|
+
reads.route = true;
|
|
297
326
|
return fromRouteName;
|
|
298
327
|
},
|
|
299
328
|
toRouteName,
|
|
@@ -346,7 +375,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
346
375
|
revalidator: name,
|
|
347
376
|
revalidate: result,
|
|
348
377
|
});
|
|
349
|
-
return decide(result, `hard:${name}
|
|
378
|
+
return decide(result, `hard:${name}`, readDefault);
|
|
350
379
|
} else if (
|
|
351
380
|
result &&
|
|
352
381
|
typeof result === "object" &&
|
|
@@ -372,5 +401,6 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
372
401
|
revalidate: currentSuggestion,
|
|
373
402
|
});
|
|
374
403
|
const softNames = revalidations.map((r) => r.name).join(",");
|
|
375
|
-
|
|
404
|
+
// A soft result may carry the default it read on: counted as the default's.
|
|
405
|
+
return decide(currentSuggestion, `soft-chain:${softNames}`, true);
|
|
376
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
|