@rangojs/router 0.22.1 → 0.24.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 +10 -4
- 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/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- 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 +24 -10
- 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 +22 -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/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- 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/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- 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 +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- 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 +4 -4
- 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/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- 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 +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -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/request-scope.d.ts +7 -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 +14 -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 +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- 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 +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- 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 +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- 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 +45 -11
- 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-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- 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/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- 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/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -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/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- 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/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -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/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- 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/request-scope.ts +8 -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 +28 -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/expose-internal-ids.ts +2 -3
- 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 +116 -53
- 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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
_getRequestContext,
|
|
19
19
|
type RequestContext,
|
|
20
20
|
} from "../../server/request-context.js";
|
|
21
|
+
import { isRenderableLoading } from "../../types/boundaries.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* True when the current render is the active PPR shell capture and route loaders
|
|
@@ -38,12 +39,13 @@ export function isShellCaptureActive(
|
|
|
38
39
|
export { createMaskedLoaderPromise } from "./mask-nested.js";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* True when the entry's loading() is renderable (
|
|
42
|
-
* isRenderableLoading
|
|
42
|
+
* True when the entry's loading() is renderable (the shared
|
|
43
|
+
* isRenderableLoading, types/boundaries.ts, that segment-system gates on).
|
|
44
|
+
* Not the lane decision (lane rule: see
|
|
43
45
|
* resolveLoaderData, loader-cache.ts): callers only use it to decide whether
|
|
44
46
|
* an unflagged loader's segment key rides as the bake/seed key, which is inert
|
|
45
47
|
* at capture.
|
|
46
48
|
*/
|
|
47
49
|
export function entryLoadingMasksLoaders(loading: unknown): boolean {
|
|
48
|
-
return loading
|
|
50
|
+
return isRenderableLoading(loading);
|
|
49
51
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { isValidElement, type ReactElement } from "react";
|
|
28
28
|
import { isThenable } from "../../handles/is-thenable.js";
|
|
29
|
+
import { assignOwn } from "../../context-var.js";
|
|
29
30
|
import {
|
|
30
31
|
cloneElementWithProps,
|
|
31
32
|
isPlainDataObject,
|
|
@@ -178,7 +179,7 @@ async function elideNested(
|
|
|
178
179
|
for (const key of Object.keys(source)) {
|
|
179
180
|
const r = await elideNested(source[key], seen);
|
|
180
181
|
if (r.state === "rejected") return r;
|
|
181
|
-
out
|
|
182
|
+
assignOwn(out, key, r.value);
|
|
182
183
|
if (r.value !== source[key]) changed = true;
|
|
183
184
|
result.hasHole ||= r.hasHole;
|
|
184
185
|
}
|
|
@@ -301,13 +302,21 @@ export function overlayLoaderContainer(
|
|
|
301
302
|
// Shape drift (fresh not a plain object): markers fall back to undefined.
|
|
302
303
|
const freshObj = isPlainDataObject(fresh) ? fresh : undefined;
|
|
303
304
|
let out: Record<string, unknown> | undefined;
|
|
305
|
+
// Own keys only: a key named like an Object.prototype member is data.
|
|
306
|
+
// The spread copies an own `__proto__` as data, so assigning a recorded
|
|
307
|
+
// key writes it; a fresh-only key goes through assignOwn.
|
|
304
308
|
for (const key of Object.keys(recorded)) {
|
|
305
|
-
const next = overlayLoaderContainer(
|
|
309
|
+
const next = overlayLoaderContainer(
|
|
310
|
+
freshObj && Object.hasOwn(freshObj, key) ? freshObj[key] : undefined,
|
|
311
|
+
recorded[key],
|
|
312
|
+
);
|
|
306
313
|
if (next !== recorded[key]) (out ??= { ...recorded })[key] = next;
|
|
307
314
|
}
|
|
308
315
|
if (freshObj) {
|
|
309
316
|
for (const key of Object.keys(freshObj)) {
|
|
310
|
-
if (!(key
|
|
317
|
+
if (!Object.hasOwn(recorded, key)) {
|
|
318
|
+
assignOwn((out ??= { ...recorded }), key, freshObj[key]);
|
|
319
|
+
}
|
|
311
320
|
}
|
|
312
321
|
}
|
|
313
322
|
return out ?? recorded;
|
|
@@ -341,7 +350,12 @@ function overlayProps(
|
|
|
341
350
|
const props = recorded.props;
|
|
342
351
|
let changed: Record<string, unknown> | undefined;
|
|
343
352
|
for (const key of Object.keys(props)) {
|
|
344
|
-
const next = overlayLoaderContainer(
|
|
353
|
+
const next = overlayLoaderContainer(
|
|
354
|
+
freshProps && Object.hasOwn(freshProps, key)
|
|
355
|
+
? freshProps[key]
|
|
356
|
+
: undefined,
|
|
357
|
+
props[key],
|
|
358
|
+
);
|
|
345
359
|
if (next !== props[key]) (changed ??= {})[key] = next;
|
|
346
360
|
}
|
|
347
361
|
return changed ? cloneElementWithProps(recorded, changed) : recorded;
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Deliberately a LEAF module: loader-mask (the other natural home) imports
|
|
10
10
|
* request-context, so any funnel importing the mask from there inherits that
|
|
11
|
-
* graph. This module imports only is-thenable and
|
|
11
|
+
* graph. This module imports only react and two leaves, is-thenable and
|
|
12
|
+
* context-var.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { cloneElement, isValidElement, type ReactElement } from "react";
|
|
16
|
+
import { assignOwn } from "../../context-var.js";
|
|
15
17
|
import { isThenable } from "../../handles/is-thenable.js";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -52,7 +54,8 @@ type KeyValidated = { _store?: { validated?: unknown } };
|
|
|
52
54
|
* keyless child of a static children list (validated at creation) would warn
|
|
53
55
|
* "Each child in a list should have a unique key" once cloned. Pass only the
|
|
54
56
|
* props that change: cloneElement re-parents the element (owner) when its
|
|
55
|
-
* config carries `ref`.
|
|
57
|
+
* config carries `ref`. cloneElement copies props by assignment, so a prop
|
|
58
|
+
* key named `__proto__` can't survive a clone; prop copies don't use assignOwn.
|
|
56
59
|
*/
|
|
57
60
|
export function cloneElementWithProps(
|
|
58
61
|
element: ReactElement,
|
|
@@ -206,8 +209,9 @@ export function maskNestedContainerThenables(
|
|
|
206
209
|
seen.set(value, out);
|
|
207
210
|
let changed = false;
|
|
208
211
|
for (const key of Object.keys(source)) {
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
const masked = maskNestedContainerThenables(source[key], seen, report);
|
|
213
|
+
assignOwn(out, key, masked);
|
|
214
|
+
if (masked !== source[key]) changed = true;
|
|
211
215
|
}
|
|
212
216
|
if (changed) return out;
|
|
213
217
|
seen.set(value, value);
|
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `prefetch: false`: what a prefetch request defers and what its fill request
|
|
3
|
+
* runs (docs/design/prefetch-false.md).
|
|
4
|
+
*
|
|
5
|
+
* Two layers. The SCOPE is static per matched chain: which loader
|
|
6
|
+
* registrations are deferrable and which entries can be a handler unit. The
|
|
7
|
+
* PLAN is per match and rides the handler context
|
|
8
|
+
* (InternalHandlerContext._prefetchDeferral): the request's mode and where the
|
|
9
|
+
* chain's stored handler output starts. A request context cannot carry it: a
|
|
10
|
+
* shell capture derives its context from the request that scheduled it and
|
|
11
|
+
* would inherit a prefetch's mode.
|
|
12
|
+
*
|
|
13
|
+
* The decisions themselves are made in revalidation.ts, by asking this module.
|
|
14
|
+
*
|
|
15
|
+
* One rule runs through every decision: the flag applies only to a segment
|
|
16
|
+
* the client does not have yet. A segment the client holds is never deferred,
|
|
17
|
+
* whether or not it re-renders, and nothing is deferred because of it. A
|
|
18
|
+
* placeholder over content that is on screen would replace it with a fallback,
|
|
19
|
+
* or make the click wait where a plain navigation would not.
|
|
20
|
+
*
|
|
21
|
+
* "Holds" is what the request listed in `_rsc_segments` (PrefetchDeferral.held),
|
|
22
|
+
* the set buildMatchResult (match-result.ts) filters the response with. Not
|
|
23
|
+
* the set resolution works on: the match deletes a route's id from that one to
|
|
24
|
+
* force a same-route render from an intercept source (match-api.ts), and a
|
|
25
|
+
* unit deferred on that ground would be dropped from the response as held.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
getParallelEntries,
|
|
30
|
+
getParallelSlotEntries,
|
|
31
|
+
isPprEntry,
|
|
32
|
+
type EntryData,
|
|
33
|
+
type LoaderEntry,
|
|
34
|
+
} from "../../server/context.js";
|
|
35
|
+
import { requestHeaders } from "../../server/request-headers.js";
|
|
36
|
+
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
37
|
+
import type { CacheScope } from "../../cache/cache-scope.js";
|
|
38
|
+
import { FILL_PARAM, PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What a request says it is, read once when its context is created
|
|
42
|
+
* (RequestContext._requestKind). A fill is marked on the raw URL and wins
|
|
43
|
+
* over the prefetch header. Whether a prefetch defers anything is decided per
|
|
44
|
+
* match, by partialDeferralMode: a shell capture inherits the context of the
|
|
45
|
+
* request that scheduled it and is never a prefetch.
|
|
46
|
+
*/
|
|
47
|
+
export function requestKind(
|
|
48
|
+
request: Request,
|
|
49
|
+
rawUrl: URL,
|
|
50
|
+
): "prefetch" | "fill" | undefined {
|
|
51
|
+
if (rawUrl.searchParams.has(FILL_PARAM)) return "fill";
|
|
52
|
+
return requestHeaders(request).has(PREFETCH_HEADER) ? "prefetch" : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Static, per matched chain. */
|
|
56
|
+
interface DeferralScope {
|
|
57
|
+
/**
|
|
58
|
+
* Loader registrations a prefetch can defer, each with what makes it so:
|
|
59
|
+
* `true` for its own flag, else the segment ids of the flagged entries whose
|
|
60
|
+
* fallback covers it. A covered loader is deferred only while one of those
|
|
61
|
+
* segments is new to the client.
|
|
62
|
+
*/
|
|
63
|
+
readonly loaders: ReadonlyMap<LoaderEntry, true | readonly string[]>;
|
|
64
|
+
/** `$$id`s of `loaders`: the loaders that cannot call ctx.rendered(). */
|
|
65
|
+
readonly loaderIds: ReadonlySet<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Entries whose handler a prefetch can skip (a flagged loading() with a
|
|
68
|
+
* fallback to show). `index` is the chain index that owns the entry: its
|
|
69
|
+
* own for a chain entry, its parent's for a parallel entry. `ids` are its
|
|
70
|
+
* segment ids: the entry's for a chain entry, one per slot for a parallel
|
|
71
|
+
* entry.
|
|
72
|
+
*/
|
|
73
|
+
readonly units: ReadonlyMap<EntryData, DeferralUnit>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface DeferralUnit {
|
|
77
|
+
readonly index: number;
|
|
78
|
+
readonly ids: readonly string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Per match: InternalHandlerContext._prefetchDeferral. */
|
|
82
|
+
export interface PrefetchDeferral {
|
|
83
|
+
readonly scope: DeferralScope;
|
|
84
|
+
/**
|
|
85
|
+
* "prefetch": deferral replaces execution. "fill": the request that follows
|
|
86
|
+
* an adopted prefetch; a held segment is skipped outright. Unset on every
|
|
87
|
+
* other request, where the plan only feeds the ctx.rendered() guards.
|
|
88
|
+
*/
|
|
89
|
+
readonly mode?: "prefetch" | "fill";
|
|
90
|
+
/**
|
|
91
|
+
* First chain index whose handler output is stored or about to be (a
|
|
92
|
+
* cache() boundary, a ppr or Prerender route): no unit at or below it.
|
|
93
|
+
*/
|
|
94
|
+
readonly storedFrom: number;
|
|
95
|
+
/** The segment ids the request listed as held (`_rsc_segments`). */
|
|
96
|
+
readonly held: ReadonlySet<string>;
|
|
97
|
+
/**
|
|
98
|
+
* Segment ids of the handler units this prefetch skips: every unit that is
|
|
99
|
+
* not stored and not held. Empty outside a prefetch.
|
|
100
|
+
*/
|
|
101
|
+
readonly skipped: ReadonlySet<string>;
|
|
102
|
+
/** How this request may use the route's cache() record (recordUse). */
|
|
103
|
+
readonly record: RecordUse;
|
|
104
|
+
/** Segment id of the handler unit this prefetch skipped. */
|
|
105
|
+
deferredUnit?: string;
|
|
106
|
+
/**
|
|
107
|
+
* The walk is below a chain unit it skipped (resolution is in chain order,
|
|
108
|
+
* a route's orphan layouts and slots after its handler): every handler it
|
|
109
|
+
* reaches from here on is skipped too, and sends a placeholder with what
|
|
110
|
+
* the entry declares. The browser builds the page's tree once, from those
|
|
111
|
+
* (browser/partial-update.ts armHoles), so they have to list every segment
|
|
112
|
+
* a navigation to the page lists.
|
|
113
|
+
*/
|
|
114
|
+
below?: true;
|
|
115
|
+
/**
|
|
116
|
+
* The loaders this prefetch deferred, by `$$id` (registerDeferredLoader).
|
|
117
|
+
* A deferred loader runs only if something reads it: a handler's or another
|
|
118
|
+
* loader's ctx.use() starts it through its registration
|
|
119
|
+
* (startDeferredLoader), and revalidation.ts deliverStartedLoaders delivers
|
|
120
|
+
* what was started by the end of resolution.
|
|
121
|
+
*/
|
|
122
|
+
deferredLoaders?: Map<string, DeferredLoader>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* How a request may use the route's cache() record, decided once per match
|
|
127
|
+
* (planPrefetchDeferral) for the cache middleware.
|
|
128
|
+
*/
|
|
129
|
+
export interface RecordUse {
|
|
130
|
+
/**
|
|
131
|
+
* withCacheLookup reads it. False when this prefetch skips a chain unit
|
|
132
|
+
* (PrefetchDeferral.skipped): the unit takes everything below it, so the
|
|
133
|
+
* read would replay the record's handle pushes for segments the response
|
|
134
|
+
* does not carry. Decided from what the client holds, never per tree: a
|
|
135
|
+
* prefetch that holds the flagged layout reads and writes the record like
|
|
136
|
+
* any other request.
|
|
137
|
+
*/
|
|
138
|
+
readonly read: boolean;
|
|
139
|
+
/** withCacheStore writes it. False where `read` is: the record would be incomplete. */
|
|
140
|
+
readonly write: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* A background re-render of the whole route may write it: the stale-hit
|
|
143
|
+
* refresh (withBackgroundRevalidation) and the proactive write a partial
|
|
144
|
+
* response needs (withCacheStore). False for a fill: the re-render runs
|
|
145
|
+
* the handlers of segments the client holds, which a fill does not run.
|
|
146
|
+
*/
|
|
147
|
+
readonly refresh: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A loader this prefetch deferred: PrefetchDeferral.deferredLoaders. */
|
|
151
|
+
export interface DeferredLoader {
|
|
152
|
+
/**
|
|
153
|
+
* The segment that registers it. Its run files its handle pushes there,
|
|
154
|
+
* where a navigation's run of it files them, not under the reader
|
|
155
|
+
* (loaderOwner).
|
|
156
|
+
*/
|
|
157
|
+
readonly owner: string;
|
|
158
|
+
/**
|
|
159
|
+
* Starts it as resolution would have: resolveLoaderData with its cache()
|
|
160
|
+
* binding, so the reader and the delivered segment share one value.
|
|
161
|
+
*/
|
|
162
|
+
readonly start: () => Promise<unknown>;
|
|
163
|
+
/** A reader started it. */
|
|
164
|
+
started?: true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const EMPTY_SCOPE: DeferralScope = {
|
|
168
|
+
loaders: new Map(),
|
|
169
|
+
loaderIds: new Set(),
|
|
170
|
+
units: new Map(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const scopes = new WeakMap<EntryData, DeferralScope>();
|
|
174
|
+
const warnedBakeLoaders = new Set<string>();
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The deferral scope of a matched chain, memoized on its leaf entry (entries
|
|
178
|
+
* are per-isSSR manifest objects, replaced on HMR). EMPTY_SCOPE when nothing
|
|
179
|
+
* in the tree declares `prefetch: false`.
|
|
180
|
+
*/
|
|
181
|
+
export function resolveDeferralScope(
|
|
182
|
+
entries: readonly EntryData[],
|
|
183
|
+
): DeferralScope {
|
|
184
|
+
const leaf = entries[entries.length - 1];
|
|
185
|
+
if (!leaf) return EMPTY_SCOPE;
|
|
186
|
+
const cached = scopes.get(leaf);
|
|
187
|
+
if (cached) return cached;
|
|
188
|
+
|
|
189
|
+
const loaders = new Map<LoaderEntry, true | readonly string[]>();
|
|
190
|
+
const loaderIds = new Set<string>();
|
|
191
|
+
const units = new Map<EntryData, DeferralUnit>();
|
|
192
|
+
const chain = new Set(entries);
|
|
193
|
+
// ssr: false on a ppr route is the bake lane (loader-cache.ts
|
|
194
|
+
// resolveLoaderData): shell material, served from the record.
|
|
195
|
+
const pprRoute = isPprEntry(leaf);
|
|
196
|
+
// A clientUrls() group route renders loading() itself and its handler is
|
|
197
|
+
// ClientUrlsRoot (segment-system.tsx keys it by group): a unit would hand
|
|
198
|
+
// React a promise in place of the optimistic destination on screen. So
|
|
199
|
+
// in its chain only a loader's own flag counts, the group's and a server
|
|
200
|
+
// layout's above it; a flagged loading() defers nothing.
|
|
201
|
+
const clientGroupChain = leaf.clientGroup !== undefined;
|
|
202
|
+
const flagsLoading = (entry: EntryData): boolean =>
|
|
203
|
+
!clientGroupChain && entry.loadingPrefetch === false;
|
|
204
|
+
|
|
205
|
+
// `behind`: the segment ids of the flagged entries whose fallback covers
|
|
206
|
+
// `entry`'s loaders.
|
|
207
|
+
const collect = (
|
|
208
|
+
entry: EntryData,
|
|
209
|
+
behind: readonly string[],
|
|
210
|
+
index: number,
|
|
211
|
+
): void => {
|
|
212
|
+
for (const loaderEntry of entry.loader ?? []) {
|
|
213
|
+
const own = loaderEntry.prefetch === false;
|
|
214
|
+
if (!own && behind.length === 0) continue;
|
|
215
|
+
if (pprRoute && loaderEntry.bake) {
|
|
216
|
+
if (
|
|
217
|
+
process.env.NODE_ENV !== "production" &&
|
|
218
|
+
loaderEntry.prefetch === false &&
|
|
219
|
+
!warnedBakeLoaders.has(loaderEntry.loader.$$id)
|
|
220
|
+
) {
|
|
221
|
+
warnedBakeLoaders.add(loaderEntry.loader.$$id);
|
|
222
|
+
console.warn(
|
|
223
|
+
`[rango] loader "${loaderEntry.loader.$$id}" is registered with ssr: false and prefetch: false on a ppr route ("${leaf.id}"). ` +
|
|
224
|
+
`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.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
loaders.set(loaderEntry, own ? true : behind);
|
|
230
|
+
loaderIds.add(loaderEntry.loader.$$id);
|
|
231
|
+
}
|
|
232
|
+
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
233
|
+
// A slot's segment id, as resolveParallelSegmentsWithRevalidation
|
|
234
|
+
// (revalidation.ts) forms it.
|
|
235
|
+
const slotIds = flagsLoading(parallelEntry)
|
|
236
|
+
? getParallelSlotEntries(entry.parallel)
|
|
237
|
+
.filter((slot) => slot.entry === parallelEntry)
|
|
238
|
+
.map((slot) => `${entry.shortCode}.${slot.slot}`)
|
|
239
|
+
: [];
|
|
240
|
+
if (slotIds.length > 0) units.set(parallelEntry, { index, ids: slotIds });
|
|
241
|
+
collect(parallelEntry, [...behind, ...slotIds], index);
|
|
242
|
+
}
|
|
243
|
+
for (const orphan of entry.layout) {
|
|
244
|
+
if (chain.has(orphan)) continue;
|
|
245
|
+
// An orphan layout's handler is awaited after its owner's ran
|
|
246
|
+
// (revalidation.ts resolveOrphanLayoutWithRevalidation): never a unit,
|
|
247
|
+
// its loaders are still behind its fallback.
|
|
248
|
+
collect(
|
|
249
|
+
orphan,
|
|
250
|
+
flagsLoading(orphan) ? [...behind, orphan.shortCode] : behind,
|
|
251
|
+
index,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
let behind: readonly string[] = [];
|
|
257
|
+
entries.forEach((entry, index) => {
|
|
258
|
+
const flagged = flagsLoading(entry);
|
|
259
|
+
if (flagged) units.set(entry, { index, ids: [entry.shortCode] });
|
|
260
|
+
const own = flagged ? [...behind, entry.shortCode] : behind;
|
|
261
|
+
collect(entry, own, index);
|
|
262
|
+
// A layout's fallback covers its outlet: every deeper chain entry.
|
|
263
|
+
if (flagged && entry.type !== "route") behind = own;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const scope: DeferralScope =
|
|
267
|
+
loaders.size === 0 && units.size === 0
|
|
268
|
+
? EMPTY_SCOPE
|
|
269
|
+
: { loaders, loaderIds, units };
|
|
270
|
+
scopes.set(leaf, scope);
|
|
271
|
+
return scope;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Which half of the feature a partial request is, if either (`fill` and
|
|
276
|
+
* `prefetch` are requestKind's answer). Both halves are GETs: a request of
|
|
277
|
+
* another method that carries either marker is a normal request. A prefetch
|
|
278
|
+
* defers only on a plain GET navigation: an action re-renders what it
|
|
279
|
+
* changed, an intercept resolves its loaders outside the shared funnel, and a
|
|
280
|
+
* shell capture is never a prefetch whatever the request that scheduled it
|
|
281
|
+
* sent. The four conditions overlap today (an action is a POST, a capture
|
|
282
|
+
* runs the full match): each one states its own reason.
|
|
283
|
+
*/
|
|
284
|
+
export function partialDeferralMode(request: {
|
|
285
|
+
fill: boolean;
|
|
286
|
+
prefetch: boolean;
|
|
287
|
+
method: string;
|
|
288
|
+
isAction: boolean;
|
|
289
|
+
isIntercept: boolean;
|
|
290
|
+
isShellCapture: boolean;
|
|
291
|
+
}): PrefetchDeferral["mode"] {
|
|
292
|
+
if (request.method !== "GET") return undefined;
|
|
293
|
+
if (request.fill) return "fill";
|
|
294
|
+
return request.prefetch &&
|
|
295
|
+
!request.isAction &&
|
|
296
|
+
!request.isIntercept &&
|
|
297
|
+
!request.isShellCapture
|
|
298
|
+
? "prefetch"
|
|
299
|
+
: undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const NOTHING_HELD: ReadonlySet<string> = new Set();
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The plan for one match, or undefined when the tree declares no flag and the
|
|
306
|
+
* request is not a fill (the common case: nothing to carry). `held` is the
|
|
307
|
+
* client's segment ids as the request listed them.
|
|
308
|
+
*/
|
|
309
|
+
export function planPrefetchDeferral(
|
|
310
|
+
entries: readonly EntryData[],
|
|
311
|
+
mode: PrefetchDeferral["mode"],
|
|
312
|
+
matched: { pr?: boolean; od?: boolean },
|
|
313
|
+
cacheScope: CacheScope | null | undefined,
|
|
314
|
+
held: ReadonlySet<string> = NOTHING_HELD,
|
|
315
|
+
): PrefetchDeferral | undefined {
|
|
316
|
+
const scope = resolveDeferralScope(entries);
|
|
317
|
+
if (scope === EMPTY_SCOPE && mode !== "fill") return undefined;
|
|
318
|
+
|
|
319
|
+
const leaf = entries[entries.length - 1];
|
|
320
|
+
let storedFrom = Infinity;
|
|
321
|
+
if (
|
|
322
|
+
matched.pr ||
|
|
323
|
+
matched.od ||
|
|
324
|
+
(leaf &&
|
|
325
|
+
(isPprEntry(leaf) ||
|
|
326
|
+
// A Prerender route is stored whether or not this request found the
|
|
327
|
+
// artifact (dev renders it live): the rule is the declaration's.
|
|
328
|
+
(leaf.type === "route" && leaf.isPrerender)))
|
|
329
|
+
) {
|
|
330
|
+
storedFrom = 0;
|
|
331
|
+
} else if (cacheScope?.enabled) {
|
|
332
|
+
// The same index withCacheLookup resolves its live entries above.
|
|
333
|
+
storedFrom =
|
|
334
|
+
cacheScope.boundary === undefined
|
|
335
|
+
? 0
|
|
336
|
+
: Math.max(
|
|
337
|
+
0,
|
|
338
|
+
entries.findIndex((e) => e.shortCode === cacheScope.boundary),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const skipped = new Set<string>();
|
|
343
|
+
let skipsChain = false;
|
|
344
|
+
if (mode === "prefetch") {
|
|
345
|
+
for (const [entry, unit] of scope.units) {
|
|
346
|
+
if (!unitEligible(storedFrom, entry, unit)) continue;
|
|
347
|
+
for (const id of unit.ids) {
|
|
348
|
+
if (held.has(id)) continue;
|
|
349
|
+
skipped.add(id);
|
|
350
|
+
if (entry.type !== "parallel") skipsChain = true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const record: RecordUse = {
|
|
355
|
+
read: !skipsChain,
|
|
356
|
+
write: !skipsChain,
|
|
357
|
+
refresh: mode !== "fill",
|
|
358
|
+
};
|
|
359
|
+
return { scope, mode, storedFrom, held, skipped, record };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function getPrefetchDeferral(
|
|
363
|
+
ctx: HandlerContext<any, any> | undefined,
|
|
364
|
+
): PrefetchDeferral | undefined {
|
|
365
|
+
return (ctx as InternalHandlerContext<any, any> | undefined)
|
|
366
|
+
?._prefetchDeferral;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The request is a fill: a held segment is skipped without consulting anything. */
|
|
370
|
+
export function isFillRequest(ctx: HandlerContext<any, any>): boolean {
|
|
371
|
+
return getPrefetchDeferral(ctx)?.mode === "fill";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* A fill's whole revalidation rule (R5): send a segment the client does not
|
|
376
|
+
* hold, keep one it holds, and consult nothing else (no revalidate()
|
|
377
|
+
* predicate, no params or search comparison, no trace entry). Undefined on
|
|
378
|
+
* any other request, where revalidation decides. `held` is the set the
|
|
379
|
+
* deciding site resolves with.
|
|
380
|
+
*/
|
|
381
|
+
export function fillSends(
|
|
382
|
+
ctx: HandlerContext<any, any>,
|
|
383
|
+
segmentId: string,
|
|
384
|
+
held: ReadonlySet<string>,
|
|
385
|
+
): boolean | undefined {
|
|
386
|
+
return isFillRequest(ctx) ? !held.has(segmentId) : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const RECORD_OPEN: RecordUse = { read: true, write: true, refresh: true };
|
|
390
|
+
|
|
391
|
+
/** PrefetchDeferral.record, or all of it open when the match has no plan. */
|
|
392
|
+
export function recordUse(
|
|
393
|
+
ctx: HandlerContext<any, any> | undefined,
|
|
394
|
+
): RecordUse {
|
|
395
|
+
return getPrefetchDeferral(ctx)?.record ?? RECORD_OPEN;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Register a loader this prefetch deferred, with the segment that registers
|
|
400
|
+
* it and how to start it there (PrefetchDeferral.deferredLoaders). The first
|
|
401
|
+
* registration wins, as the loader's one run per request does.
|
|
402
|
+
*/
|
|
403
|
+
export function registerDeferredLoader(
|
|
404
|
+
ctx: HandlerContext<any, any>,
|
|
405
|
+
loaderId: string,
|
|
406
|
+
owner: string,
|
|
407
|
+
start: () => Promise<unknown>,
|
|
408
|
+
): void {
|
|
409
|
+
const plan = getPrefetchDeferral(ctx);
|
|
410
|
+
if (plan && !plan.deferredLoaders?.has(loaderId)) {
|
|
411
|
+
(plan.deferredLoaders ??= new Map()).set(loaderId, { owner, start });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* A reader asks for a loader this prefetch deferred and nothing has started:
|
|
417
|
+
* start it through its registration and return the run, or undefined when
|
|
418
|
+
* the read takes the ordinary path. Without this the reader ran the loader
|
|
419
|
+
* bare, without its cache() binding, and the segment delivered afterwards
|
|
420
|
+
* read the binding: two values on one page and an extra run on a HIT.
|
|
421
|
+
*
|
|
422
|
+
* A started cache() binding of the loader (`_loaderCacheOverrides`) is never
|
|
423
|
+
* redirected here: the read is that binding's own MISS execution, and
|
|
424
|
+
* starting the registration would hand the binding its own pending value.
|
|
425
|
+
*/
|
|
426
|
+
export function startDeferredLoader(
|
|
427
|
+
ctx: HandlerContext<any, any>,
|
|
428
|
+
loaderId: string,
|
|
429
|
+
): Promise<unknown> | undefined {
|
|
430
|
+
const internal = ctx as InternalHandlerContext<any, any>;
|
|
431
|
+
const deferred = internal._prefetchDeferral?.deferredLoaders?.get(loaderId);
|
|
432
|
+
if (
|
|
433
|
+
!deferred ||
|
|
434
|
+
deferred.started ||
|
|
435
|
+
internal._loaderCacheOverrides?.has(loaderId)
|
|
436
|
+
) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
deferred.started = true;
|
|
440
|
+
return deferred.start();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The segment a run of `loaderId` files its handle pushes under: the one
|
|
445
|
+
* that registers it when this prefetch deferred it, else the segment being
|
|
446
|
+
* resolved.
|
|
447
|
+
*/
|
|
448
|
+
export function loaderOwner(
|
|
449
|
+
ctx: HandlerContext<any, any>,
|
|
450
|
+
loaderId: string,
|
|
451
|
+
): string | undefined {
|
|
452
|
+
const internal = ctx as InternalHandlerContext<any, any>;
|
|
453
|
+
return (
|
|
454
|
+
internal._prefetchDeferral?.deferredLoaders?.get(loaderId)?.owner ??
|
|
455
|
+
internal._currentSegmentId
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* This prefetch defers `loaderEntry`, whose segment is `segmentId`: the client
|
|
461
|
+
* does not hold that segment, and the loader is flagged itself or sits behind
|
|
462
|
+
* the fallback of a flagged entry the client does not hold either. Under a
|
|
463
|
+
* flagged layout the client holds, a new route's loaders run.
|
|
464
|
+
*/
|
|
465
|
+
export function defersLoader(
|
|
466
|
+
ctx: HandlerContext<any, any>,
|
|
467
|
+
loaderEntry: LoaderEntry,
|
|
468
|
+
segmentId: string,
|
|
469
|
+
): boolean {
|
|
470
|
+
const plan = getPrefetchDeferral(ctx);
|
|
471
|
+
if (plan?.mode !== "prefetch" || plan.held.has(segmentId)) return false;
|
|
472
|
+
const by = plan.scope.loaders.get(loaderEntry);
|
|
473
|
+
return (
|
|
474
|
+
by === true || (by !== undefined && by.some((id) => !plan.held.has(id)))
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** A unit whose handler output is not stored: a prefetch can skip it. */
|
|
479
|
+
function unitEligible(
|
|
480
|
+
storedFrom: number,
|
|
481
|
+
entry: EntryData,
|
|
482
|
+
unit: DeferralUnit,
|
|
483
|
+
): boolean {
|
|
484
|
+
return (
|
|
485
|
+
unit.index < storedFrom &&
|
|
486
|
+
!("isStaticPrerender" in entry && entry.isStaticPrerender)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* This prefetch skips the handler of the entry whose segment is `segmentId`:
|
|
492
|
+
* a unit the client does not hold, or anything the client does not hold
|
|
493
|
+
* below one (belowDeferredUnit). A held segment that re-renders is rendered
|
|
494
|
+
* by the prefetch, as by a navigation.
|
|
495
|
+
*/
|
|
496
|
+
export function defersUnit(
|
|
497
|
+
ctx: HandlerContext<any, any>,
|
|
498
|
+
segmentId: string,
|
|
499
|
+
): boolean {
|
|
500
|
+
return (
|
|
501
|
+
getPrefetchDeferral(ctx)?.skipped.has(segmentId) === true ||
|
|
502
|
+
belowDeferredUnit(ctx, segmentId)
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* `segmentId` is new to the client and the walk is below a chain unit this
|
|
508
|
+
* prefetch skipped (PrefetchDeferral.below). Nothing of such a segment runs
|
|
509
|
+
* in the prefetch, a slot's revalidate() predicates included: on a new
|
|
510
|
+
* segment they cannot change the outcome, and what they would read from
|
|
511
|
+
* context was set by handlers that did not run.
|
|
512
|
+
*/
|
|
513
|
+
export function belowDeferredUnit(
|
|
514
|
+
ctx: HandlerContext<any, any>,
|
|
515
|
+
segmentId: string,
|
|
516
|
+
): boolean {
|
|
517
|
+
const plan = getPrefetchDeferral(ctx);
|
|
518
|
+
return plan?.below === true && !plan.held.has(segmentId);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* A unit candidate that is not stored: a prefetch of this tree can skip a
|
|
523
|
+
* handler. Returns its entry (the first in chain order), for the
|
|
524
|
+
* ctx.rendered() guards (loader-resolution.ts).
|
|
525
|
+
*/
|
|
526
|
+
export function firstUnitCandidate(
|
|
527
|
+
plan: PrefetchDeferral,
|
|
528
|
+
): EntryData | undefined {
|
|
529
|
+
for (const [entry, unit] of plan.scope.units) {
|
|
530
|
+
if (unitEligible(plan.storedFrom, entry, unit)) return entry;
|
|
531
|
+
}
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Record the handler unit this prefetch skipped. A `chain` entry takes what
|
|
537
|
+
* the walk reaches after it (PrefetchDeferral.below); a slot is skipped
|
|
538
|
+
* alone.
|
|
539
|
+
*/
|
|
540
|
+
export function markUnitDeferred(
|
|
541
|
+
ctx: HandlerContext<any, any>,
|
|
542
|
+
segmentId: string,
|
|
543
|
+
chain?: boolean,
|
|
544
|
+
): void {
|
|
545
|
+
const plan = getPrefetchDeferral(ctx);
|
|
546
|
+
if (!plan) return;
|
|
547
|
+
plan.deferredUnit ??= segmentId;
|
|
548
|
+
if (chain) plan.below = true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** What the segment of a unit a prefetch skipped carries. */
|
|
552
|
+
const DEFERRED = { deferred: true } as const;
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Whether this prefetch skips the handler of the unit whose segment is
|
|
556
|
+
* `segmentId`, recorded: the flag the segment carries
|
|
557
|
+
* (ResolvedSegment.deferred), or undefined when the handler runs.
|
|
558
|
+
* - `chain`: a chain entry, its own unit or below one (defersUnit). The walk
|
|
559
|
+
* takes what it reaches after it (markUnitDeferred).
|
|
560
|
+
* - `slot`: a slot that is its own unit, skipped alone.
|
|
561
|
+
* - `below`: what the walk reaches below a chain unit it skipped
|
|
562
|
+
* (belowDeferredUnit): a slot, before its predicates run, and an orphan
|
|
563
|
+
* layout, which is never a unit of its own (resolveDeferralScope). The unit
|
|
564
|
+
* above is recorded already.
|
|
565
|
+
*/
|
|
566
|
+
export function deferUnit(
|
|
567
|
+
ctx: HandlerContext<any, any>,
|
|
568
|
+
segmentId: string,
|
|
569
|
+
kind: "chain" | "slot" | "below",
|
|
570
|
+
): { readonly deferred: true } | undefined {
|
|
571
|
+
if (kind === "below") {
|
|
572
|
+
return belowDeferredUnit(ctx, segmentId) ? DEFERRED : undefined;
|
|
573
|
+
}
|
|
574
|
+
if (
|
|
575
|
+
kind === "chain"
|
|
576
|
+
? !defersUnit(ctx, segmentId)
|
|
577
|
+
: getPrefetchDeferral(ctx)?.skipped.has(segmentId) !== true
|
|
578
|
+
) {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
markUnitDeferred(ctx, segmentId, kind === "chain");
|
|
582
|
+
return DEFERRED;
|
|
583
|
+
}
|