@rangojs/router 0.18.0 → 0.19.1
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/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +37 -0
- package/dist/types/cache/cache-key-utils.d.ts +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +48 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +126 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +962 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +67 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +4 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +54 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +202 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +209 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +42 -5
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +152 -26
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +30 -10
- package/src/rsc/handler.ts +12 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +263 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +134 -87
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -223,6 +223,8 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
223
223
|
res: stubRes,
|
|
224
224
|
cookie: () => undefined,
|
|
225
225
|
cookies: () => ({}),
|
|
226
|
+
_readCookie: () => undefined,
|
|
227
|
+
_readCookies: () => ({}),
|
|
226
228
|
setCookie: () => {},
|
|
227
229
|
deleteCookie: () => {},
|
|
228
230
|
header: () => {},
|
|
@@ -526,6 +528,8 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
526
528
|
res: stubRes,
|
|
527
529
|
cookie: () => undefined,
|
|
528
530
|
cookies: () => ({}),
|
|
531
|
+
_readCookie: () => undefined,
|
|
532
|
+
_readCookies: () => ({}),
|
|
529
533
|
setCookie: () => {},
|
|
530
534
|
deleteCookie: () => {},
|
|
531
535
|
header: () => {},
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Builds on RouteSnapshot from route-snapshot.ts.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { requestHeaders } from "../server/request-headers.js";
|
|
18
19
|
import { RouteNotFoundError } from "../errors.js";
|
|
19
20
|
import type { EntryData } from "../server/context.js";
|
|
20
21
|
import type { CollectedMiddleware } from "./middleware-types.js";
|
|
@@ -149,10 +150,11 @@ export async function classifyRequest<TEnv = any>(
|
|
|
149
150
|
): Promise<RequestPlan<TEnv>> {
|
|
150
151
|
const pathname = url.pathname;
|
|
151
152
|
const isAction =
|
|
152
|
-
request.
|
|
153
|
+
requestHeaders(request).has("rsc-action") ||
|
|
154
|
+
url.searchParams.has("_rsc_action");
|
|
153
155
|
const isLoaderFetch = url.searchParams.has("_rsc_loader");
|
|
154
156
|
const isPartialReq = url.searchParams.has("_rsc_partial");
|
|
155
|
-
const contentType = request.
|
|
157
|
+
const contentType = requestHeaders(request).get("content-type") || "";
|
|
156
158
|
const isFormSubmission =
|
|
157
159
|
contentType.includes("multipart/form-data") ||
|
|
158
160
|
contentType.includes("application/x-www-form-urlencoded");
|
|
@@ -176,7 +178,7 @@ export async function classifyRequest<TEnv = any>(
|
|
|
176
178
|
) {
|
|
177
179
|
let reloadUrl = stripInternalParams(url).toString();
|
|
178
180
|
if (isAction) {
|
|
179
|
-
const referer = request.
|
|
181
|
+
const referer = requestHeaders(request).get("referer");
|
|
180
182
|
if (referer) {
|
|
181
183
|
try {
|
|
182
184
|
const refererUrl = new URL(referer);
|
|
@@ -248,7 +250,8 @@ export async function classifyRequest<TEnv = any>(
|
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
const actionId =
|
|
251
|
-
request.
|
|
253
|
+
requestHeaders(request).get("rsc-action") ||
|
|
254
|
+
url.searchParams.get("_rsc_action");
|
|
252
255
|
|
|
253
256
|
const hasVariants =
|
|
254
257
|
snapshot.matched.negotiateVariants &&
|
|
@@ -518,7 +518,10 @@ export interface RangoOptions<TEnv = any> {
|
|
|
518
518
|
*
|
|
519
519
|
* Controls how long prefetch responses are kept in the client-side
|
|
520
520
|
* in-memory cache and sets `Cache-Control: private, max-age=<ttl>`
|
|
521
|
-
* on
|
|
521
|
+
* on prefetch responses, so the browser's HTTP cache may reuse them
|
|
522
|
+
* (`private`: shared caches such as a CDN do not store them). A route an
|
|
523
|
+
* intercept targets gets no prefetch `Cache-Control`: whether its
|
|
524
|
+
* response is the modal or the full page depends on the source page.
|
|
522
525
|
*
|
|
523
526
|
* The cache is automatically invalidated on server actions regardless
|
|
524
527
|
* of TTL, so this is primarily a staleness safety net.
|
|
@@ -324,7 +324,6 @@ export async function resolveSegment<TEnv>(
|
|
|
324
324
|
transition: applyViewTransitionDefault(
|
|
325
325
|
entry.transition,
|
|
326
326
|
deps.viewTransitionDefault,
|
|
327
|
-
entry.shortCode,
|
|
328
327
|
),
|
|
329
328
|
params,
|
|
330
329
|
belongsToRoute: false,
|
|
@@ -474,7 +473,6 @@ export async function resolveSegment<TEnv>(
|
|
|
474
473
|
transition: applyViewTransitionDefault(
|
|
475
474
|
entry.transition,
|
|
476
475
|
deps.viewTransitionDefault,
|
|
477
|
-
entry.shortCode,
|
|
478
476
|
),
|
|
479
477
|
params,
|
|
480
478
|
belongsToRoute: true,
|
|
@@ -563,7 +561,6 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
563
561
|
transition: applyViewTransitionDefault(
|
|
564
562
|
orphan.transition,
|
|
565
563
|
deps.viewTransitionDefault,
|
|
566
|
-
orphan.shortCode,
|
|
567
564
|
),
|
|
568
565
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
569
566
|
});
|
|
@@ -723,7 +720,6 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
723
720
|
transition: applyViewTransitionDefault(
|
|
724
721
|
parallelEntry.transition,
|
|
725
722
|
deps.viewTransitionDefault,
|
|
726
|
-
`${parentShortCode}.${slot}`,
|
|
727
723
|
),
|
|
728
724
|
params,
|
|
729
725
|
slot,
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* getItem()/setItem() methods on SegmentCacheStore.
|
|
7
7
|
*
|
|
8
8
|
* Cache key resolution (3-tier, matching CacheScope.resolveKey):
|
|
9
|
-
* 1. options.key(requestCtx) — full override
|
|
9
|
+
* 1. options.key(requestCtx) — full override, stored namespaced as
|
|
10
|
+
* loader:{loaderId}:key:{encodeURIComponent(result)} (#1009)
|
|
10
11
|
* 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
|
|
11
12
|
* 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
|
|
12
13
|
*
|
|
@@ -57,13 +58,17 @@ import {
|
|
|
57
58
|
} from "../../cache/handle-capture.js";
|
|
58
59
|
import { encodeHandles, decodeHandles } from "../../cache/handle-snapshot.js";
|
|
59
60
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
61
|
+
import { runIdentityExempt } from "../../cache/cache-exec-scope.js";
|
|
60
62
|
import {
|
|
61
63
|
getRequestContext,
|
|
62
64
|
_getRequestContext,
|
|
63
65
|
runWithRequestContext,
|
|
64
66
|
} from "../../server/request-context.js";
|
|
65
67
|
import { observePhase, PHASES } from "../instrument.js";
|
|
66
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
KEY_PART_PREFIX,
|
|
70
|
+
sortedRouteParams,
|
|
71
|
+
} from "../../cache/cache-key-utils.js";
|
|
67
72
|
import {
|
|
68
73
|
resolveTtl,
|
|
69
74
|
resolveSwrWindow,
|
|
@@ -73,19 +78,25 @@ import {
|
|
|
73
78
|
DEFAULT_ROUTE_TTL,
|
|
74
79
|
} from "../../cache/cache-policy.js";
|
|
75
80
|
import { readThroughItem } from "../../cache/read-through-swr.js";
|
|
81
|
+
import {
|
|
82
|
+
executionStart,
|
|
83
|
+
predatesInvalidation,
|
|
84
|
+
} from "../../cache/tag-invalidation.js";
|
|
76
85
|
import {
|
|
77
86
|
maskNestedContainerThenables,
|
|
78
87
|
overlayLoaderContainer,
|
|
79
88
|
} from "./loader-snapshot.js";
|
|
80
89
|
import {
|
|
81
90
|
SHELL_BAKE_TAG_OWNER,
|
|
82
|
-
armLoaderTagSets,
|
|
83
91
|
captureRecordedTags,
|
|
92
|
+
earliestRecordedStart,
|
|
84
93
|
flattenRecordedTags,
|
|
85
94
|
linkLoaderTags,
|
|
86
95
|
linkLoaderTagsTo,
|
|
87
96
|
linkRecordedTags,
|
|
88
97
|
markIdentityRead,
|
|
98
|
+
markTagSetStart,
|
|
99
|
+
readStartedLoaderValue,
|
|
89
100
|
readValueTags,
|
|
90
101
|
recordedIdentityRead,
|
|
91
102
|
recordLoaderTags,
|
|
@@ -128,6 +139,28 @@ function getDefaultLoaderCacheKey(
|
|
|
128
139
|
return `loader:${loaderId}:${host}${base}`;
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
/**
|
|
143
|
+
* A loader's own `key()` result as its entry key (#1009). The result is
|
|
144
|
+
* often request input, and the item family (`getItem`/`setItem`) has no
|
|
145
|
+
* other discriminator, so stored raw it could name another loader's entry
|
|
146
|
+
* or a `"use cache"` entry (`use-cache:<id>:...`, cache-runtime.ts), which
|
|
147
|
+
* shares that family; route records use `get`/`set`. Namespaced, it can't:
|
|
148
|
+
* - it starts with `loader:`, which no `"use cache"` key does;
|
|
149
|
+
* - an encoded result holds no `:`, so the key reads unambiguously from
|
|
150
|
+
* the right (result, `key:`, loader id), and two namespaced keys are
|
|
151
|
+
* equal only for the same loader and result;
|
|
152
|
+
* - a default key `loader:<id>:<host><pathname>...` holds a `/` past its
|
|
153
|
+
* id, and an encoded result holds none, so the two are equal only if one
|
|
154
|
+
* loader's id is literally `<other id>:<host>/<path>...`. Loader ids can
|
|
155
|
+
* hold `:` (a dev id is a root-relative path, absolute `D:/...` on
|
|
156
|
+
* Windows), so this is the guarantee, not "ids hold no `:`".
|
|
157
|
+
* A store keyGenerator result stays the store's own namespace, as on routes
|
|
158
|
+
* (composeCacheKeys).
|
|
159
|
+
*/
|
|
160
|
+
function loaderKeyFromResult(loaderId: string, result: string): string {
|
|
161
|
+
return `loader:${loaderId}:${KEY_PART_PREFIX}${encodeURIComponent(result)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
131
164
|
/**
|
|
132
165
|
* Resolve cache key using the shared 3-tier priority. `declared`: a key() or
|
|
133
166
|
* store keyGenerator produced it, so it can carry request identity (#972).
|
|
@@ -148,9 +181,14 @@ async function resolveLoaderKey(
|
|
|
148
181
|
const host = getRequestContext()?.url?.host ?? "localhost";
|
|
149
182
|
const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
|
|
150
183
|
if (options === false) return { key: defaultKey, declared: false };
|
|
184
|
+
const keyFn = options.key;
|
|
151
185
|
return {
|
|
152
|
-
key: await resolveCacheKey(
|
|
153
|
-
|
|
186
|
+
key: await resolveCacheKey(
|
|
187
|
+
keyFn && (async (ctx) => loaderKeyFromResult(loaderId, await keyFn(ctx))),
|
|
188
|
+
store,
|
|
189
|
+
defaultKey,
|
|
190
|
+
),
|
|
191
|
+
declared: Boolean(keyFn || store.keyGenerator),
|
|
154
192
|
};
|
|
155
193
|
}
|
|
156
194
|
|
|
@@ -199,6 +237,64 @@ export function bindsLoaderCache(entries: readonly EntryData[]): boolean {
|
|
|
199
237
|
return entries.some(entryBindsLoaderCache);
|
|
200
238
|
}
|
|
201
239
|
|
|
240
|
+
const chainLoaderEntriesMemo = new WeakMap<EntryData, readonly LoaderEntry[]>();
|
|
241
|
+
|
|
242
|
+
// The loaders an entry registers, its orphan layouts' and its parallel
|
|
243
|
+
// slots' included.
|
|
244
|
+
function chainLoaderEntries(entry: EntryData): readonly LoaderEntry[] {
|
|
245
|
+
let loaders = chainLoaderEntriesMemo.get(entry);
|
|
246
|
+
if (loaders === undefined) {
|
|
247
|
+
loaders = [
|
|
248
|
+
...(entry.loader ?? []),
|
|
249
|
+
...(entry.layout ?? []).flatMap(chainLoaderEntries),
|
|
250
|
+
...Object.values(entry.parallel ?? {}).flatMap((p) =>
|
|
251
|
+
p ? chainLoaderEntries(p) : [],
|
|
252
|
+
),
|
|
253
|
+
];
|
|
254
|
+
chainLoaderEntriesMemo.set(entry, loaders);
|
|
255
|
+
}
|
|
256
|
+
return loaders;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The ids of the loaders a request over the matched chain `entries` runs on
|
|
261
|
+
* the live lane (no `ssr: false`), on a chain entry, its orphan layouts or
|
|
262
|
+
* its parallel slots. A document HIT tail restores the record's pushes of
|
|
263
|
+
* every other owner as authoritative (withCacheLookup, HandleStore
|
|
264
|
+
* pushRestored); a live-lane loader is a hole, so its live run replaces its
|
|
265
|
+
* recorded pushes instead. Memoized per entry.
|
|
266
|
+
*/
|
|
267
|
+
export function liveLaneLoaderIds(
|
|
268
|
+
entries: readonly EntryData[],
|
|
269
|
+
): ReadonlySet<string> {
|
|
270
|
+
const ids = new Set<string>();
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
for (const l of chainLoaderEntries(entry)) {
|
|
273
|
+
if (!l.bake) ids.add(l.loader.$$id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return ids;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The lane of each loader the matched chain `entries` registers: "live" (no
|
|
281
|
+
* `ssr: false`) or "bake". A loader registered on both is "live". A PPR
|
|
282
|
+
* capture credits a push to the first registered loader around it when that
|
|
283
|
+
* one is live (shell-capture.ts deriveShellCaptureContext).
|
|
284
|
+
*/
|
|
285
|
+
export function routeLoaderLanes(
|
|
286
|
+
entries: readonly EntryData[],
|
|
287
|
+
): ReadonlyMap<string, "live" | "bake"> {
|
|
288
|
+
const lanes = new Map<string, "live" | "bake">();
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
for (const l of chainLoaderEntries(entry)) {
|
|
291
|
+
if (!l.bake) lanes.set(l.loader.$$id, "live");
|
|
292
|
+
else if (!lanes.has(l.loader.$$id)) lanes.set(l.loader.$$id, "bake");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return lanes;
|
|
296
|
+
}
|
|
297
|
+
|
|
202
298
|
function getLoaderStore(
|
|
203
299
|
loaderEntry: LoaderEntry,
|
|
204
300
|
): import("../../cache/types.js").SegmentCacheStore | null {
|
|
@@ -228,31 +324,47 @@ function getLoaderStore(
|
|
|
228
324
|
* replayed one.
|
|
229
325
|
* A PPR shell capture records the replayed pushes under this loader (its
|
|
230
326
|
* push funnel reads the pushReplayed owner, shell-capture.ts), so a shell
|
|
231
|
-
* HIT restores them owned
|
|
232
|
-
*
|
|
327
|
+
* HIT restores them owned (HandleStore.pushRestored). This replay still
|
|
328
|
+
* runs on that HIT: the store drops its settled values, which the record
|
|
329
|
+
* already restored, and keeps the thenable ones the record could not keep
|
|
330
|
+
* (a deferred push, the reason the loader's record carries `runs`).
|
|
233
331
|
*/
|
|
234
332
|
async function replayLoaderHandles(
|
|
235
|
-
encoded: string,
|
|
333
|
+
encoded: string | undefined,
|
|
236
334
|
handleStore: HandleStore,
|
|
237
335
|
segmentId: string,
|
|
336
|
+
cachedLoaderId: string,
|
|
238
337
|
claim: ((loaderId: string) => boolean) | undefined,
|
|
239
338
|
): Promise<void> {
|
|
240
|
-
const recorded = await decodeHandles(encoded);
|
|
339
|
+
const recorded = encoded ? await decodeHandles(encoded) : {};
|
|
241
340
|
if (!recorded) return;
|
|
242
341
|
const delivers = new Map<string, boolean>();
|
|
243
|
-
|
|
244
|
-
const owner = key.slice(key.indexOf(":") + 1);
|
|
342
|
+
const claims = (owner: string): boolean => {
|
|
245
343
|
let deliver = delivers.get(owner);
|
|
246
344
|
if (deliver === undefined) {
|
|
247
345
|
deliver = claim ? claim(owner) : true;
|
|
248
346
|
delivers.set(owner, deliver);
|
|
249
347
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
348
|
+
return deliver;
|
|
349
|
+
};
|
|
350
|
+
const replay = (): void => {
|
|
351
|
+
for (const key in recorded) {
|
|
352
|
+
const owner = key.slice(key.indexOf(":") + 1);
|
|
353
|
+
if (!claims(owner)) continue;
|
|
354
|
+
for (const [handleName, values] of Object.entries(recorded[key])) {
|
|
355
|
+
for (const value of values) {
|
|
356
|
+
handleStore.pushReplayed(handleName, segmentId, value, owner);
|
|
357
|
+
}
|
|
254
358
|
}
|
|
255
359
|
}
|
|
360
|
+
};
|
|
361
|
+
// A hole's entry, pushes or none, takes the place of the shell copies of
|
|
362
|
+
// its pushes (HandleStore.redeliverReplays). Not once the hole delivered
|
|
363
|
+
// in this request: a run a reader started first replaces them itself.
|
|
364
|
+
if (claims(cachedLoaderId)) {
|
|
365
|
+
handleStore.redeliverReplays(cachedLoaderId, replay);
|
|
366
|
+
} else {
|
|
367
|
+
replay();
|
|
256
368
|
}
|
|
257
369
|
}
|
|
258
370
|
|
|
@@ -449,9 +561,10 @@ function executeLoaderData<TEnv>(
|
|
|
449
561
|
|
|
450
562
|
// Evaluate runtime condition if provided
|
|
451
563
|
const options = cacheConfig.options;
|
|
452
|
-
|
|
564
|
+
const condition = options.condition;
|
|
565
|
+
if (condition) {
|
|
453
566
|
const requestCtx = getRequestContext();
|
|
454
|
-
if (requestCtx && !
|
|
567
|
+
if (requestCtx && !runIdentityExempt(() => condition(requestCtx))) {
|
|
455
568
|
return ctx.use(loaderEntry.loader);
|
|
456
569
|
}
|
|
457
570
|
}
|
|
@@ -479,7 +592,7 @@ function executeLoaderData<TEnv>(
|
|
|
479
592
|
// useLoader.
|
|
480
593
|
linkLoaderTags(item.$$id);
|
|
481
594
|
readValueTags(cached);
|
|
482
|
-
return cached;
|
|
595
|
+
return readStartedLoaderValue(cached, item.$$id);
|
|
483
596
|
}
|
|
484
597
|
return originalUse(item);
|
|
485
598
|
}) as typeof ctx.use;
|
|
@@ -508,11 +621,8 @@ function executeLoaderData<TEnv>(
|
|
|
508
621
|
const swr = swrWindow || undefined;
|
|
509
622
|
const tags = resolveTags(loaderEntry);
|
|
510
623
|
recordLoaderTags(loaderId, tags);
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
// resolveLoaders call outside a match from running unarmed after this
|
|
514
|
-
// point (loaders it started earlier stay unrecorded).
|
|
515
|
-
armLoaderTagSets();
|
|
624
|
+
// The per-execution loader tag sets were armed before any loader ran: by
|
|
625
|
+
// the match (match-api.ts, bindsLoaderCache), or by runLoader.
|
|
516
626
|
// An execution of this loader outside the binding (a reader started it
|
|
517
627
|
// first, a stale refresh) answers for the cache() tags too.
|
|
518
628
|
if (tags && tags.length > 0) {
|
|
@@ -530,8 +640,10 @@ function executeLoaderData<TEnv>(
|
|
|
530
640
|
const isOwnBodyPush = () => isInsideLoaderBody(loaderId);
|
|
531
641
|
// What a reader of this value takes on (tagLoaderValue below,
|
|
532
642
|
// readValueTags): the cache() tags, plus the entry's tags on a HIT or the
|
|
533
|
-
// execution's on a MISS.
|
|
643
|
+
// execution's on a MISS. A HIT's value is as old as its lookup: a cached
|
|
644
|
+
// reader gates its own write from here at the latest (#977).
|
|
534
645
|
const valueTags = new Set(tags);
|
|
646
|
+
markTagSetStart(valueTags, executionStart());
|
|
535
647
|
|
|
536
648
|
const dataPromise = (async () => {
|
|
537
649
|
const codec = await getCodec();
|
|
@@ -560,7 +672,9 @@ function executeLoaderData<TEnv>(
|
|
|
560
672
|
const bodyTags = new Set(tags);
|
|
561
673
|
let revalidating = false;
|
|
562
674
|
let hitHandles: string | undefined;
|
|
675
|
+
let cachedRead = false;
|
|
563
676
|
const onCachedRead = (label: string, cached: CacheItemResult) => {
|
|
677
|
+
cachedRead = true;
|
|
564
678
|
hitHandles = cached.handles;
|
|
565
679
|
const { read } = unmarkIdentity(cached.value);
|
|
566
680
|
if (read) markIdentityRead(valueTags, read);
|
|
@@ -634,6 +748,15 @@ function executeLoaderData<TEnv>(
|
|
|
634
748
|
// After the handle encode: it settles pending pushes, which can read.
|
|
635
749
|
const read = identityRead();
|
|
636
750
|
const entryTags = [...flattenRecordedTags(bodyTags)];
|
|
751
|
+
// One of the entry's tags was invalidated after the value's oldest
|
|
752
|
+
// part started (#977): this execution, or the run of this loader or
|
|
753
|
+
// of a dependency another reader started earlier. Written now, a
|
|
754
|
+
// value that may predate it would outlive it. Skip the write; the
|
|
755
|
+
// next read runs the body.
|
|
756
|
+
const start = earliestRecordedStart(bodyTags);
|
|
757
|
+
if (start && (await predatesInvalidation(store, entryTags, start))) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
637
760
|
await store.setItem!(k, markIdentity(v, read), {
|
|
638
761
|
...o,
|
|
639
762
|
tags: entryTags.length > 0 ? entryTags : undefined,
|
|
@@ -642,6 +765,7 @@ function executeLoaderData<TEnv>(
|
|
|
642
765
|
}),
|
|
643
766
|
key,
|
|
644
767
|
execute: async () => {
|
|
768
|
+
markTagSetStart(bodyTags, executionStart());
|
|
645
769
|
const value = await captureRecordedTags(bodyTags, runBody);
|
|
646
770
|
identityRead();
|
|
647
771
|
return value;
|
|
@@ -675,12 +799,14 @@ function executeLoaderData<TEnv>(
|
|
|
675
799
|
host: requestCtxForExecute,
|
|
676
800
|
});
|
|
677
801
|
|
|
678
|
-
// An entry
|
|
679
|
-
|
|
802
|
+
// An entry without handles (none recorded, or dropped by an encode
|
|
803
|
+
// timeout) replays none, and still stands for a hole's shell copies.
|
|
804
|
+
if (cachedRead && handleStore && owningSegmentId) {
|
|
680
805
|
await replayLoaderHandles(
|
|
681
806
|
hitHandles,
|
|
682
807
|
handleStore,
|
|
683
808
|
owningSegmentId,
|
|
809
|
+
loaderId,
|
|
684
810
|
internal._claimLoaderPushes,
|
|
685
811
|
);
|
|
686
812
|
}
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* fresh into it.
|
|
9
9
|
*
|
|
10
10
|
* Capture mode is signalled by `requestCtx._shellCaptureRun`, set to true ONLY on
|
|
11
|
-
* the derived request context of the background capture task (shell-capture.ts)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* for the mask so every loader execution site gates the same way (loader-cache.ts
|
|
11
|
+
* the derived request context of the background capture task (shell-capture.ts);
|
|
12
|
+
* the foreground render never sets it. This module is the single home for the
|
|
13
|
+
* mask so every loader execution site gates the same way (loader-cache.ts
|
|
15
14
|
* `resolveLoaderData`, fresh.ts `resolveLoaders`).
|
|
16
15
|
*/
|
|
17
16
|
|
|
@@ -10,24 +10,18 @@
|
|
|
10
10
|
* server-component element holding a promise, which is a hole as a whole (see
|
|
11
11
|
* elideLoaderContainer):
|
|
12
12
|
*
|
|
13
|
-
* - elide: deep-walk the settled container; a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* elide anyway, and the overlay keeps decoding SETTLED markers
|
|
20
|
-
* from pre-mask snapshots. The result is promise-free and
|
|
21
|
-
* Flight-serializable.
|
|
13
|
+
* - elide: deep-walk the settled container; a nested promise is a hole,
|
|
14
|
+
* replaced by {@link LOADER_HOLE_KEY}. loader-cache masks every
|
|
15
|
+
* nested thenable at capture ({@link maskNestedContainerThenables},
|
|
16
|
+
* a never-settling promise), so the promise shape alone decides:
|
|
17
|
+
* a value that settled in time does not bake (#692). The result
|
|
18
|
+
* is promise-free and Flight-serializable.
|
|
22
19
|
* - overlay: on a HIT the loader runs fresh (only the loader body can mint
|
|
23
20
|
* the live nested promises), then the recorded container is laid
|
|
24
21
|
* over it: recorded paths win (they are what the prelude froze),
|
|
25
22
|
* hole-marker paths take the fresh run's value (the live hole),
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* handing them the raw value throws React #438 on every HIT (the
|
|
29
|
-
* storefront PDP regression) — and fresh-only paths pass through
|
|
30
|
-
* (they cannot contradict prelude bytes that never rendered them).
|
|
23
|
+
* and fresh-only paths pass through (they cannot contradict
|
|
24
|
+
* prelude bytes that never rendered them).
|
|
31
25
|
*/
|
|
32
26
|
|
|
33
27
|
import { isValidElement, type ReactElement } from "react";
|
|
@@ -64,37 +58,6 @@ export function isLoaderHoleMarker(value: unknown): value is LoaderHoleMarker {
|
|
|
64
58
|
);
|
|
65
59
|
}
|
|
66
60
|
|
|
67
|
-
/**
|
|
68
|
-
* Marker wrapping the inlined value of a NESTED promise that settled during
|
|
69
|
-
* capture. The value baked (physics), but the container key was a promise —
|
|
70
|
-
* the overlay must hand consumers a Promise.resolve(value), not the raw value,
|
|
71
|
-
* or an unconditional use(data.x) throws React #438 on every HIT. The ROOT
|
|
72
|
-
* container is never wrapped: loader-cache overlays against the awaited fresh
|
|
73
|
-
* container value.
|
|
74
|
-
*/
|
|
75
|
-
export const LOADER_SETTLED_KEY = "$rangoLoaderSettled" as const;
|
|
76
|
-
|
|
77
|
-
export interface LoaderSettledMarker {
|
|
78
|
-
[LOADER_SETTLED_KEY]: 1;
|
|
79
|
-
value: unknown;
|
|
80
|
-
/**
|
|
81
|
-
* Present when the pinned subtree contains hole markers. Computed once at
|
|
82
|
-
* capture (elide already visits every node) so the per-HIT overlay never
|
|
83
|
-
* rescans the pinned structure to pick its rehydration path.
|
|
84
|
-
*/
|
|
85
|
-
holes?: 1;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function isLoaderSettledMarker(
|
|
89
|
-
value: unknown,
|
|
90
|
-
): value is LoaderSettledMarker {
|
|
91
|
-
return (
|
|
92
|
-
typeof value === "object" &&
|
|
93
|
-
value !== null &&
|
|
94
|
-
(value as Record<string, unknown>)[LOADER_SETTLED_KEY] === 1
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
61
|
/**
|
|
99
62
|
* Probe whether a promise is already settled without waiting for it: races it
|
|
100
63
|
* against an immediately-resolved sentinel across two microtask hops (then
|
|
@@ -128,12 +91,12 @@ export type ElideResult =
|
|
|
128
91
|
| { state: "rejected" };
|
|
129
92
|
|
|
130
93
|
/**
|
|
131
|
-
* Deep-elide a settled bake-lane container for recording.
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
94
|
+
* Deep-elide a settled bake-lane container for recording. Nested promises
|
|
95
|
+
* become hole markers: the capture masked each of them (never settles), so
|
|
96
|
+
* it postponed as a hole. A REJECTED container poisons the record (error UI
|
|
97
|
+
* must never bake into a shared shell) — the caller refuses the capture.
|
|
98
|
+
* Plain data objects, arrays and the props of host, Suspense/Fragment and
|
|
99
|
+
* client-reference elements are
|
|
137
100
|
* traversed copy-on-write (an element through cloneElementWithProps); a shared
|
|
138
101
|
* subtree or a cycle resolves to the same elided result. An element Flight
|
|
139
102
|
* renders by calling user code (see rendersOnServer) is a leaf, or ONE hole
|
|
@@ -165,17 +128,7 @@ async function elideNested(
|
|
|
165
128
|
seen: Map<object, ElideResult>,
|
|
166
129
|
): Promise<ElideResult> {
|
|
167
130
|
if (isThenable(value)) {
|
|
168
|
-
|
|
169
|
-
if (probed.state === "pending") {
|
|
170
|
-
return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
|
|
171
|
-
}
|
|
172
|
-
if (probed.state === "rejected") return { state: "rejected" };
|
|
173
|
-
const inner = await elideNested(probed.value, seen);
|
|
174
|
-
if (inner.state === "rejected") return inner;
|
|
175
|
-
const marker: LoaderSettledMarker = inner.hasHole
|
|
176
|
-
? { [LOADER_SETTLED_KEY]: 1, value: inner.value, holes: 1 }
|
|
177
|
-
: { [LOADER_SETTLED_KEY]: 1, value: inner.value };
|
|
178
|
-
return { state: "ok", value: marker, hasHole: inner.hasHole };
|
|
131
|
+
return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
|
|
179
132
|
}
|
|
180
133
|
if (typeof value !== "object" || value === null) {
|
|
181
134
|
return { state: "ok", value, hasHole: false };
|
|
@@ -324,22 +277,6 @@ export function overlayLoaderContainer(
|
|
|
324
277
|
): unknown {
|
|
325
278
|
if (isLoaderHoleMarker(recorded)) return fresh;
|
|
326
279
|
|
|
327
|
-
if (isLoaderSettledMarker(recorded)) {
|
|
328
|
-
const pinned = recorded.value;
|
|
329
|
-
// Deep holes inside a settled container (capture-computed `holes` bit)
|
|
330
|
-
// need the fresh promise's resolved value to fill them; a fully-pinned
|
|
331
|
-
// container resolves immediately (the prelude already shows it — never
|
|
332
|
-
// gate it on fresh latency). A rejecting fresh run degrades holes to
|
|
333
|
-
// undefined instead of poisoning the pin.
|
|
334
|
-
if (recorded.holes === 1) {
|
|
335
|
-
return Promise.resolve(fresh).then(
|
|
336
|
-
(freshValue) => overlayLoaderContainer(freshValue, pinned),
|
|
337
|
-
() => overlayLoaderContainer(undefined, pinned),
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
return Promise.resolve(overlayLoaderContainer(undefined, pinned));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
280
|
if (Array.isArray(recorded)) {
|
|
344
281
|
const freshArr = Array.isArray(fresh) ? fresh : [];
|
|
345
282
|
let out: unknown[] | undefined;
|
|
@@ -81,30 +81,55 @@ export function cloneElementWithProps(
|
|
|
81
81
|
*/
|
|
82
82
|
export function settleNestedThenables(value: unknown): Promise<void> {
|
|
83
83
|
const pending: Promise<void>[] = [];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (isValidElement<Record<string, unknown>>(v)) {
|
|
93
|
-
walk(v.props);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (Array.isArray(v) || isPlainDataObject(v)) {
|
|
97
|
-
if (seen.has(v)) return;
|
|
98
|
-
seen.add(v);
|
|
99
|
-
for (const item of Array.isArray(v) ? v : Object.values(v)) walk(item);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
walk(value);
|
|
84
|
+
someNestedThenable(value, (thenable) => {
|
|
85
|
+
pending.push(
|
|
86
|
+
Promise.resolve(thenable).then(settleNestedThenables, () => undefined),
|
|
87
|
+
);
|
|
88
|
+
return false;
|
|
89
|
+
});
|
|
103
90
|
return pending.length === 0
|
|
104
91
|
? Promise.resolve()
|
|
105
92
|
: Promise.all(pending).then(() => undefined);
|
|
106
93
|
}
|
|
107
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The walk settleNestedThenables and holdsThenable share: visit each thenable
|
|
97
|
+
* reachable from `value` through plain data objects, arrays and React element
|
|
98
|
+
* props (never an element's owner or type), the shapes
|
|
99
|
+
* maskNestedContainerThenables masks; every other object is a leaf. Stops at
|
|
100
|
+
* the first visit that returns true and reports whether one did.
|
|
101
|
+
*/
|
|
102
|
+
function someNestedThenable(
|
|
103
|
+
value: unknown,
|
|
104
|
+
visit: (thenable: PromiseLike<unknown>) => boolean,
|
|
105
|
+
): boolean {
|
|
106
|
+
let seen: Set<object> | undefined;
|
|
107
|
+
const walk = (v: unknown): boolean => {
|
|
108
|
+
if (isThenable(v)) return visit(v);
|
|
109
|
+
if (typeof v !== "object" || v === null) return false;
|
|
110
|
+
if (isValidElement<Record<string, unknown>>(v)) return walk(v.props);
|
|
111
|
+
if (!Array.isArray(v) && !isPlainDataObject(v)) return false;
|
|
112
|
+
seen ??= new Set();
|
|
113
|
+
if (seen.has(v)) return false;
|
|
114
|
+
seen.add(v);
|
|
115
|
+
// Own enumerable values, as maskNestedContainerThenables walks
|
|
116
|
+
// Object.keys: an array's non-index properties are walked too.
|
|
117
|
+
return Object.values(v).some(walk);
|
|
118
|
+
};
|
|
119
|
+
return walk(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* True when `value` is a thenable or holds one where the capture's loader
|
|
124
|
+
* push funnel masks it (maskNestedContainerThenables): through plain data
|
|
125
|
+
* objects, arrays and React element props. Such a push stays out of a shell's
|
|
126
|
+
* record and reaches a HIT only from a run of its loader there, so a HIT
|
|
127
|
+
* keeps it; a push without one is the record's (HandleStore.pushRestored).
|
|
128
|
+
*/
|
|
129
|
+
export function holdsThenable(value: unknown): boolean {
|
|
130
|
+
return someNestedThenable(value, () => true);
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
/**
|
|
109
134
|
* Optional single-walk report for maskNestedContainerThenables: `thenable`
|
|
110
135
|
* flips true when the walk masked at least one thenable. The capture push
|
|
@@ -139,9 +164,9 @@ export interface MaskReport {
|
|
|
139
164
|
* nodes included, are leaves. Copy-on-write: a subtree without a thenable
|
|
140
165
|
* comes back by identity, so an element is rebuilt only when its props hold
|
|
141
166
|
* one, and then through cloneElementWithProps, which keeps its type, key,
|
|
142
|
-
* owner, dev fields and key validation. The INPUT IS NEVER MUTATED
|
|
143
|
-
*
|
|
144
|
-
*
|
|
167
|
+
* owner, dev fields and key validation. The INPUT IS NEVER MUTATED: a handler
|
|
168
|
+
* that reads the loader (`ctx.use`) reads the raw container and must keep
|
|
169
|
+
* real values. Cycles are preserved as cycles in the copy.
|
|
145
170
|
*/
|
|
146
171
|
export function maskNestedContainerThenables(
|
|
147
172
|
value: unknown,
|