@rangojs/router 0.17.0 → 0.19.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/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -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 +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- 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/prerender/build-shell-capture.d.ts +11 -4
- 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/instrument.d.ts +8 -0
- 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/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- 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 +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- 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 +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- 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 +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -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 +9 -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 +48 -1
- package/src/cache/cache-key-utils.ts +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +1094 -376
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- 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 +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- 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 +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- 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 +47 -3
- package/src/rsc/handler.ts +24 -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 +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -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 +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- 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 +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -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 +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -4,37 +4,40 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A bake-lane loader (lane rule: see resolveLoaderData, loader-cache.ts)
|
|
6
6
|
* executes during shell capture; its settled container bakes into the prelude
|
|
7
|
-
* while nested promises postpone as holes. To keep a HIT's fresh
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* while nested promises postpone as holes. To keep a HIT's fresh payload in
|
|
8
|
+
* agreement with that frozen prelude, the capture pins the container in the
|
|
9
|
+
* shell snapshot. Everything outside a hole is pinned; the one exception is a
|
|
10
|
+
* server-component element holding a promise, which is a hole as a whole (see
|
|
11
|
+
* elideLoaderContainer):
|
|
10
12
|
*
|
|
11
|
-
* - elide: deep-walk the settled container; a
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* elide anyway, and the overlay keeps decoding SETTLED markers
|
|
18
|
-
* from pre-mask snapshots. The result is promise-free and
|
|
19
|
-
* 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.
|
|
20
19
|
* - overlay: on a HIT the loader runs fresh (only the loader body can mint
|
|
21
20
|
* the live nested promises), then the recorded container is laid
|
|
22
21
|
* over it: recorded paths win (they are what the prelude froze),
|
|
23
22
|
* hole-marker paths take the fresh run's value (the live hole),
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* handing them the raw value throws React #438 on every HIT (the
|
|
27
|
-
* storefront PDP regression) — and fresh-only paths pass through
|
|
28
|
-
* (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).
|
|
29
25
|
*/
|
|
30
26
|
|
|
27
|
+
import { isValidElement, type ReactElement } from "react";
|
|
31
28
|
import { isThenable } from "../../handles/is-thenable.js";
|
|
29
|
+
import {
|
|
30
|
+
cloneElementWithProps,
|
|
31
|
+
isPlainDataObject,
|
|
32
|
+
maskNestedContainerThenables,
|
|
33
|
+
type MaskReport,
|
|
34
|
+
} from "./mask-nested.js";
|
|
32
35
|
|
|
33
36
|
// Capture-side nested-thenable masking lives in the LEAF module mask-nested.ts
|
|
34
37
|
// (shared with the capture handle-store push wrap in rsc/shell-capture.ts).
|
|
35
38
|
// Re-exported here so loader-cache and the unit tests keep one import site for
|
|
36
39
|
// the snapshot family.
|
|
37
|
-
export { maskNestedContainerThenables }
|
|
40
|
+
export { maskNestedContainerThenables };
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* Marker object standing in for a pending nested promise in a recorded loader
|
|
@@ -55,43 +58,6 @@ export function isLoaderHoleMarker(value: unknown): value is LoaderHoleMarker {
|
|
|
55
58
|
);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
/**
|
|
59
|
-
* Marker wrapping the inlined value of a NESTED promise that settled during
|
|
60
|
-
* capture. The value baked (physics), but the container key was a promise —
|
|
61
|
-
* the overlay must hand consumers a Promise.resolve(value), not the raw value,
|
|
62
|
-
* or an unconditional use(data.x) throws React #438 on every HIT. The ROOT
|
|
63
|
-
* container is never wrapped: loader-cache overlays against the awaited fresh
|
|
64
|
-
* container value.
|
|
65
|
-
*/
|
|
66
|
-
export const LOADER_SETTLED_KEY = "$rangoLoaderSettled" as const;
|
|
67
|
-
|
|
68
|
-
export interface LoaderSettledMarker {
|
|
69
|
-
[LOADER_SETTLED_KEY]: 1;
|
|
70
|
-
value: unknown;
|
|
71
|
-
/**
|
|
72
|
-
* Present when the pinned subtree contains hole markers. Computed once at
|
|
73
|
-
* capture (elide already visits every node) so the per-HIT overlay never
|
|
74
|
-
* rescans the pinned structure to pick its rehydration path.
|
|
75
|
-
*/
|
|
76
|
-
holes?: 1;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function isLoaderSettledMarker(
|
|
80
|
-
value: unknown,
|
|
81
|
-
): value is LoaderSettledMarker {
|
|
82
|
-
return (
|
|
83
|
-
typeof value === "object" &&
|
|
84
|
-
value !== null &&
|
|
85
|
-
(value as Record<string, unknown>)[LOADER_SETTLED_KEY] === 1
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
90
|
-
if (typeof value !== "object" || value === null) return false;
|
|
91
|
-
const proto = Object.getPrototypeOf(value);
|
|
92
|
-
return proto === Object.prototype || proto === null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
61
|
/**
|
|
96
62
|
* Probe whether a promise is already settled without waiting for it: races it
|
|
97
63
|
* against an immediately-resolved sentinel across two microtask hops (then
|
|
@@ -125,14 +91,18 @@ export type ElideResult =
|
|
|
125
91
|
| { state: "rejected" };
|
|
126
92
|
|
|
127
93
|
/**
|
|
128
|
-
* Deep-elide a settled bake-lane container for recording.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
|
100
|
+
* traversed copy-on-write (an element through cloneElementWithProps); a shared
|
|
101
|
+
* subtree or a cycle resolves to the same elided result. An element Flight
|
|
102
|
+
* renders by calling user code (see rendersOnServer) is a leaf, or ONE hole
|
|
103
|
+
* marker when a thenable sits anywhere in its props: the pin encode calls that
|
|
104
|
+
* component, so a marker inside its props would reach it. Anything else
|
|
105
|
+
* (Date, Map, class instance, lazy node) is a pinned leaf.
|
|
136
106
|
*
|
|
137
107
|
* The ROOT container promise-chain unwraps with NO marker: loader-cache
|
|
138
108
|
* overlays against the AWAITED fresh container, so the recorded root must be
|
|
@@ -140,7 +110,7 @@ export type ElideResult =
|
|
|
140
110
|
*/
|
|
141
111
|
export async function elideLoaderContainer(
|
|
142
112
|
value: unknown,
|
|
143
|
-
seen:
|
|
113
|
+
seen: Map<object, ElideResult> = new Map(),
|
|
144
114
|
): Promise<ElideResult> {
|
|
145
115
|
if (isThenable(value)) {
|
|
146
116
|
const probed = await probeSettled(value);
|
|
@@ -155,59 +125,151 @@ export async function elideLoaderContainer(
|
|
|
155
125
|
|
|
156
126
|
async function elideNested(
|
|
157
127
|
value: unknown,
|
|
158
|
-
seen:
|
|
128
|
+
seen: Map<object, ElideResult>,
|
|
159
129
|
): Promise<ElideResult> {
|
|
160
130
|
if (isThenable(value)) {
|
|
161
|
-
|
|
162
|
-
if (probed.state === "pending") {
|
|
163
|
-
return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
|
|
164
|
-
}
|
|
165
|
-
if (probed.state === "rejected") return { state: "rejected" };
|
|
166
|
-
const inner = await elideNested(probed.value, seen);
|
|
167
|
-
if (inner.state === "rejected") return inner;
|
|
168
|
-
const marker: LoaderSettledMarker = inner.hasHole
|
|
169
|
-
? { [LOADER_SETTLED_KEY]: 1, value: inner.value, holes: 1 }
|
|
170
|
-
: { [LOADER_SETTLED_KEY]: 1, value: inner.value };
|
|
171
|
-
return { state: "ok", value: marker, hasHole: inner.hasHole };
|
|
131
|
+
return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
|
|
172
132
|
}
|
|
133
|
+
if (typeof value !== "object" || value === null) {
|
|
134
|
+
return { state: "ok", value, hasHole: false };
|
|
135
|
+
}
|
|
136
|
+
const cached = seen.get(value);
|
|
137
|
+
if (cached) return cached;
|
|
173
138
|
|
|
174
|
-
if (
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
139
|
+
if (isValidElement<Record<string, unknown>>(value)) {
|
|
140
|
+
if (rendersOnServer(value.type)) {
|
|
141
|
+
return elementHoldsThenable(value.props)
|
|
142
|
+
? { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true }
|
|
143
|
+
: { state: "ok", value, hasHole: false };
|
|
144
|
+
}
|
|
145
|
+
// Cached before the walk: a cycle back here pins the original.
|
|
146
|
+
const result: ElideResult & { state: "ok" } = {
|
|
147
|
+
state: "ok",
|
|
148
|
+
value,
|
|
149
|
+
hasHole: false,
|
|
150
|
+
};
|
|
151
|
+
seen.set(value, result);
|
|
152
|
+
const props = value.props;
|
|
153
|
+
let changed: Record<string, unknown> | undefined;
|
|
154
|
+
for (const key of Object.keys(props)) {
|
|
155
|
+
const r = await elideNested(props[key], seen);
|
|
181
156
|
if (r.state === "rejected") return r;
|
|
182
|
-
|
|
183
|
-
hasHole ||= r.hasHole;
|
|
157
|
+
if (r.value !== props[key]) (changed ??= {})[key] = r.value;
|
|
158
|
+
result.hasHole ||= r.hasHole;
|
|
184
159
|
}
|
|
185
|
-
|
|
160
|
+
if (changed) result.value = cloneElementWithProps(value, changed);
|
|
161
|
+
return result;
|
|
186
162
|
}
|
|
187
163
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
164
|
+
if (Array.isArray(value) || isPlainDataObject(value)) {
|
|
165
|
+
const source = value as Record<string, unknown>;
|
|
166
|
+
const out = (Array.isArray(value) ? new Array(value.length) : {}) as Record<
|
|
167
|
+
string,
|
|
168
|
+
unknown
|
|
169
|
+
>;
|
|
170
|
+
// Cached before the walk: a cycle back here pins the copy.
|
|
171
|
+
const result: ElideResult & { state: "ok" } = {
|
|
172
|
+
state: "ok",
|
|
173
|
+
value: out,
|
|
174
|
+
hasHole: false,
|
|
175
|
+
};
|
|
176
|
+
seen.set(value, result);
|
|
177
|
+
let changed = false;
|
|
178
|
+
for (const key of Object.keys(source)) {
|
|
179
|
+
const r = await elideNested(source[key], seen);
|
|
195
180
|
if (r.state === "rejected") return r;
|
|
196
181
|
out[key] = r.value;
|
|
197
|
-
|
|
182
|
+
if (r.value !== source[key]) changed = true;
|
|
183
|
+
result.hasHole ||= r.hasHole;
|
|
198
184
|
}
|
|
199
|
-
|
|
185
|
+
if (!changed) result.value = value;
|
|
186
|
+
return result;
|
|
200
187
|
}
|
|
201
188
|
|
|
202
189
|
return { state: "ok", value, hasHole: false };
|
|
203
190
|
}
|
|
204
191
|
|
|
192
|
+
const CLIENT_REFERENCE = Symbol.for("react.client.reference");
|
|
193
|
+
const TEMPORARY_REFERENCE = Symbol.for("react.temporary.reference");
|
|
194
|
+
const REACT_MEMO = Symbol.for("react.memo");
|
|
195
|
+
const REACT_FORWARD_REF = Symbol.for("react.forward_ref");
|
|
196
|
+
const REACT_LAZY = Symbol.for("react.lazy");
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether Flight calls user code to encode an element of this type (its
|
|
200
|
+
* renderElement): a function component, or a memo/forwardRef/lazy wrapper
|
|
201
|
+
* around one. Host (string), Suspense/Fragment (symbol) and client-reference
|
|
202
|
+
* types are encoded with their props as data, so a hole marker in those props
|
|
203
|
+
* reaches no component. A lazy type is resolved the way Flight resolves it:
|
|
204
|
+
* the Flight decode (a cache() hit, a "use cache" result) hands every client
|
|
205
|
+
* component back as a lazy around its client reference, and the capture render
|
|
206
|
+
* already resolved it. One that cannot resolve yet counts as a server
|
|
207
|
+
* component.
|
|
208
|
+
*/
|
|
209
|
+
function rendersOnServer(type: unknown): boolean {
|
|
210
|
+
if (typeof type !== "function" && (typeof type !== "object" || !type)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const tag = (type as { $$typeof?: unknown }).$$typeof;
|
|
214
|
+
if (tag === CLIENT_REFERENCE || tag === TEMPORARY_REFERENCE) return false;
|
|
215
|
+
if (typeof type === "function") return true;
|
|
216
|
+
if (tag === REACT_MEMO) {
|
|
217
|
+
return rendersOnServer((type as { type?: unknown }).type);
|
|
218
|
+
}
|
|
219
|
+
if (tag === REACT_LAZY) {
|
|
220
|
+
const resolved = readLazyNode(type);
|
|
221
|
+
return resolved === UNRESOLVED || rendersOnServer(resolved);
|
|
222
|
+
}
|
|
223
|
+
return tag === REACT_FORWARD_REF;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Whether the mask found a thenable in an element's props: the mask's own
|
|
228
|
+
* walk, so elide and the mask share one traversal rule.
|
|
229
|
+
*/
|
|
230
|
+
function elementHoldsThenable(props: unknown): boolean {
|
|
231
|
+
const report: MaskReport = { thenable: false };
|
|
232
|
+
maskNestedContainerThenables(props, undefined, report);
|
|
233
|
+
return report.thenable;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const UNRESOLVED: unique symbol = Symbol("unresolved");
|
|
237
|
+
|
|
238
|
+
function isLazyNode(value: unknown): boolean {
|
|
239
|
+
return (
|
|
240
|
+
typeof value === "object" &&
|
|
241
|
+
value !== null &&
|
|
242
|
+
(value as { $$typeof?: unknown }).$$typeof === REACT_LAZY
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The value behind a lazy node, or UNRESOLVED. Flight moves an element past
|
|
248
|
+
* ~3.2 KB of its row into a row of its own (`$L`), which the decode wraps in a
|
|
249
|
+
* lazy node, so a hole marker inside that element sits behind one. The pinned
|
|
250
|
+
* record is fully decoded when a HIT overlays it, so the read is synchronous;
|
|
251
|
+
* a node that is still pending throws and stays a leaf.
|
|
252
|
+
*/
|
|
253
|
+
function readLazyNode(value: unknown): unknown {
|
|
254
|
+
if (!isLazyNode(value)) return UNRESOLVED;
|
|
255
|
+
const lazy = value as { _payload: unknown; _init: (p: unknown) => unknown };
|
|
256
|
+
try {
|
|
257
|
+
return lazy._init(lazy._payload);
|
|
258
|
+
} catch {
|
|
259
|
+
return UNRESOLVED;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
205
263
|
/**
|
|
206
264
|
* Overlay a recorded (elided) container onto the fresh run's container for a
|
|
207
265
|
* shell HIT. Recorded wins per path; hole markers take the fresh value (the
|
|
208
266
|
* live nested promise); fresh-only object keys pass through. Where the shapes
|
|
209
267
|
* disagree structurally, recorded wins wholesale — it is what the prelude
|
|
210
|
-
* froze, and parity beats freshness inside the shell.
|
|
268
|
+
* froze, and parity beats freshness inside the shell. Copy-on-write: a
|
|
269
|
+
* recorded subtree without markers comes back by identity. A recorded element
|
|
270
|
+
* is rebuilt through cloneElementWithProps only at its marker paths, so its
|
|
271
|
+
* other props stay pinned; props the fresh element adds do not pass through
|
|
272
|
+
* (the prelude rendered the element without them).
|
|
211
273
|
*/
|
|
212
274
|
export function overlayLoaderContainer(
|
|
213
275
|
fresh: unknown,
|
|
@@ -215,45 +277,72 @@ export function overlayLoaderContainer(
|
|
|
215
277
|
): unknown {
|
|
216
278
|
if (isLoaderHoleMarker(recorded)) return fresh;
|
|
217
279
|
|
|
218
|
-
if (
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// undefined instead of poisoning the pin.
|
|
225
|
-
if (recorded.holes === 1) {
|
|
226
|
-
return Promise.resolve(fresh).then(
|
|
227
|
-
(freshValue) => overlayLoaderContainer(freshValue, pinned),
|
|
228
|
-
() => overlayLoaderContainer(undefined, pinned),
|
|
229
|
-
);
|
|
280
|
+
if (Array.isArray(recorded)) {
|
|
281
|
+
const freshArr = Array.isArray(fresh) ? fresh : [];
|
|
282
|
+
let out: unknown[] | undefined;
|
|
283
|
+
for (let i = 0; i < recorded.length; i++) {
|
|
284
|
+
const next = overlayLoaderContainer(freshArr[i], recorded[i]);
|
|
285
|
+
if (next !== recorded[i]) (out ??= recorded.slice())[i] = next;
|
|
230
286
|
}
|
|
231
|
-
return
|
|
287
|
+
return out ?? recorded;
|
|
232
288
|
}
|
|
233
289
|
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
return recorded.map((item, i) => overlayLoaderContainer(freshArr[i], item));
|
|
290
|
+
if (isValidElement<Record<string, unknown>>(recorded)) {
|
|
291
|
+
return overlayElement(fresh, recorded);
|
|
237
292
|
}
|
|
238
293
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
294
|
+
const lazyValue = readLazyNode(recorded);
|
|
295
|
+
if (lazyValue !== UNRESOLVED) {
|
|
296
|
+
const next = overlayLoaderContainer(fresh, lazyValue);
|
|
297
|
+
return next === lazyValue ? recorded : next;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isPlainDataObject(recorded)) {
|
|
301
|
+
// Shape drift (fresh not a plain object): markers fall back to undefined.
|
|
302
|
+
const freshObj = isPlainDataObject(fresh) ? fresh : undefined;
|
|
303
|
+
let out: Record<string, unknown> | undefined;
|
|
249
304
|
for (const key of Object.keys(recorded)) {
|
|
250
|
-
|
|
305
|
+
const next = overlayLoaderContainer(freshObj?.[key], recorded[key]);
|
|
306
|
+
if (next !== recorded[key]) (out ??= { ...recorded })[key] = next;
|
|
251
307
|
}
|
|
252
|
-
|
|
253
|
-
|
|
308
|
+
if (freshObj) {
|
|
309
|
+
for (const key of Object.keys(freshObj)) {
|
|
310
|
+
if (!(key in recorded)) (out ??= { ...recorded })[key] = freshObj[key];
|
|
311
|
+
}
|
|
254
312
|
}
|
|
255
|
-
return out;
|
|
313
|
+
return out ?? recorded;
|
|
256
314
|
}
|
|
257
315
|
|
|
258
316
|
return recorded;
|
|
259
317
|
}
|
|
318
|
+
|
|
319
|
+
function overlayElement(
|
|
320
|
+
fresh: unknown,
|
|
321
|
+
recorded: ReactElement<Record<string, unknown>>,
|
|
322
|
+
): unknown {
|
|
323
|
+
if (isLazyNode(fresh)) {
|
|
324
|
+
// A fresh lazy node is read only when a recorded path needs the fresh
|
|
325
|
+
// value: reading it parses a decoded row or calls a React.lazy factory
|
|
326
|
+
// the HIT render never asks for otherwise.
|
|
327
|
+
if (overlayProps(undefined, recorded) === recorded) return recorded;
|
|
328
|
+
const read = readLazyNode(fresh);
|
|
329
|
+
fresh = read === UNRESOLVED ? undefined : read;
|
|
330
|
+
}
|
|
331
|
+
return overlayProps(
|
|
332
|
+
isValidElement<Record<string, unknown>>(fresh) ? fresh.props : undefined,
|
|
333
|
+
recorded,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function overlayProps(
|
|
338
|
+
freshProps: Record<string, unknown> | undefined,
|
|
339
|
+
recorded: ReactElement<Record<string, unknown>>,
|
|
340
|
+
): unknown {
|
|
341
|
+
const props = recorded.props;
|
|
342
|
+
let changed: Record<string, unknown> | undefined;
|
|
343
|
+
for (const key of Object.keys(props)) {
|
|
344
|
+
const next = overlayLoaderContainer(freshProps?.[key], props[key]);
|
|
345
|
+
if (next !== props[key]) (changed ??= {})[key] = next;
|
|
346
|
+
}
|
|
347
|
+
return changed ? cloneElementWithProps(recorded, changed) : recorded;
|
|
348
|
+
}
|