@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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side transition({ when }) decisions.
|
|
3
|
+
*
|
|
4
|
+
* A navigation is decided ONCE, at the first commit that presents its
|
|
5
|
+
* destination: the canonical commit for a server route, the optimistic swap
|
|
6
|
+
* for a clientUrls() cross-route navigation (the canonical commit then reuses
|
|
7
|
+
* the decision carried on its transaction), the history-cache restore or
|
|
8
|
+
* refetch commit for back/forward, the action commit (error lane included).
|
|
9
|
+
*
|
|
10
|
+
* The decision is navigation-wide: every distinct predicate (by function
|
|
11
|
+
* identity) declared by a committed segment, kept or re-sent (#989), runs, and
|
|
12
|
+
* any false gates the whole navigation off. It never changes the rendered
|
|
13
|
+
* tree's shape: every segment keeps its key and its <ViewTransition> element
|
|
14
|
+
* (segment-system.tsx renders each class "none"), and the commit is urgent
|
|
15
|
+
* instead of held (partial-update.ts). That is what keeps component state
|
|
16
|
+
* across when true -> false -> true (#995). A response carries no decision,
|
|
17
|
+
* so a reused prefetch is decided against the real source.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
ResolvedSegment,
|
|
22
|
+
RouteLocation,
|
|
23
|
+
TransitionWhenContext,
|
|
24
|
+
TransitionWhenFn,
|
|
25
|
+
TransitionWhenKind,
|
|
26
|
+
} from "../types/segments.js";
|
|
27
|
+
import { resolveTransitionWhen } from "../transition-when-ref.js";
|
|
28
|
+
import { makeIsAction } from "../router/is-action.js";
|
|
29
|
+
import { isInterceptSegment } from "./intercept-utils.js";
|
|
30
|
+
import type { NavigationStore } from "./types.js";
|
|
31
|
+
|
|
32
|
+
/** One side of the navigation before it is frozen into a RouteLocation. */
|
|
33
|
+
export interface RouteLocationInput {
|
|
34
|
+
url: string | URL;
|
|
35
|
+
params?: Readonly<Record<string, string>>;
|
|
36
|
+
routeName?: string;
|
|
37
|
+
state?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The action fields of a `kind: "action"` context. */
|
|
41
|
+
export interface TransitionWhenActionInput {
|
|
42
|
+
id?: string;
|
|
43
|
+
formData?: FormData;
|
|
44
|
+
result?: unknown;
|
|
45
|
+
error?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TransitionDecisionInput {
|
|
49
|
+
kind: TransitionWhenKind;
|
|
50
|
+
from: RouteLocationInput;
|
|
51
|
+
/** The destination; ignored for "action" and "revalidate" (`to` is `from`). */
|
|
52
|
+
to?: RouteLocationInput;
|
|
53
|
+
action?: TransitionWhenActionInput;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const EMPTY_PARAMS: Readonly<Record<string, string>> = Object.freeze({});
|
|
57
|
+
|
|
58
|
+
function createRouteLocation(input: RouteLocationInput): RouteLocation {
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
// A fresh URL: a predicate may not mutate the store's or the caller's.
|
|
61
|
+
url: new URL(
|
|
62
|
+
String(input.url),
|
|
63
|
+
typeof window !== "undefined"
|
|
64
|
+
? window.location.href
|
|
65
|
+
: "http://localhost/",
|
|
66
|
+
),
|
|
67
|
+
params: input.params ? Object.freeze({ ...input.params }) : EMPTY_PARAMS,
|
|
68
|
+
routeName: input.routeName,
|
|
69
|
+
state: input.state,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createTransitionWhenContext(
|
|
74
|
+
input: TransitionDecisionInput,
|
|
75
|
+
): TransitionWhenContext {
|
|
76
|
+
const from = createRouteLocation(input.from);
|
|
77
|
+
const inAction = input.kind === "action";
|
|
78
|
+
const { id, formData, result, error } = input.action ?? {};
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
kind: input.kind,
|
|
81
|
+
from,
|
|
82
|
+
to:
|
|
83
|
+
inAction || input.kind === "revalidate" || !input.to
|
|
84
|
+
? from
|
|
85
|
+
: createRouteLocation(input.to),
|
|
86
|
+
isAction: makeIsAction(id, inAction),
|
|
87
|
+
...(inAction && { action: Object.freeze({ id, formData, result, error }) }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run one predicate. A throw counts as false (the navigation does not hold)
|
|
93
|
+
* and is logged; it never fails the navigation.
|
|
94
|
+
*/
|
|
95
|
+
export function evaluateTransitionWhen(
|
|
96
|
+
fn: TransitionWhenFn,
|
|
97
|
+
ctx: TransitionWhenContext,
|
|
98
|
+
): boolean {
|
|
99
|
+
try {
|
|
100
|
+
return fn(ctx) !== false;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(
|
|
103
|
+
"[rango] transition({ when }) threw; the navigation does not hold.",
|
|
104
|
+
error,
|
|
105
|
+
);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Distinct predicates of the segments that declare one, in segment order. */
|
|
111
|
+
export function collectTransitionWhens(
|
|
112
|
+
segments: readonly ResolvedSegment[],
|
|
113
|
+
): Set<TransitionWhenFn> {
|
|
114
|
+
const fns = new Set<TransitionWhenFn>();
|
|
115
|
+
for (const segment of segments) {
|
|
116
|
+
const fn = resolveTransitionWhen(segment.transition?.when);
|
|
117
|
+
if (fn) fns.add(fn);
|
|
118
|
+
}
|
|
119
|
+
return fns;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Decide a commit: true when the navigation is gated off (some predicate
|
|
124
|
+
* returned false or threw). The context is built only when a predicate runs.
|
|
125
|
+
* An intercept commit is not evaluated: it never holds or animates
|
|
126
|
+
* (partial-update.ts shouldStartViewTransition). `extra` is a predicate not on
|
|
127
|
+
* a segment yet (the optimistic clientUrls() destination).
|
|
128
|
+
*/
|
|
129
|
+
export function decideTransitionGatedOff(
|
|
130
|
+
segments: readonly ResolvedSegment[],
|
|
131
|
+
input: () => TransitionDecisionInput,
|
|
132
|
+
extra?: TransitionWhenFn,
|
|
133
|
+
): boolean {
|
|
134
|
+
if (segments.some(isInterceptSegment)) return false;
|
|
135
|
+
const fns = collectTransitionWhens(segments);
|
|
136
|
+
if (extra) fns.add(extra);
|
|
137
|
+
if (fns.size === 0) return false;
|
|
138
|
+
const ctx = createTransitionWhenContext(input());
|
|
139
|
+
for (const fn of fns) if (!evaluateTransitionWhen(fn, ctx)) return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Merge route params the way popstate restores them (per-segment params). */
|
|
144
|
+
export function mergeSegmentParams(
|
|
145
|
+
segments: readonly ResolvedSegment[],
|
|
146
|
+
): Record<string, string> {
|
|
147
|
+
const params: Record<string, string> = {};
|
|
148
|
+
for (const s of segments) if (s.params) Object.assign(params, s.params);
|
|
149
|
+
return params;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface CommitDecision {
|
|
153
|
+
/** Newer params / route name for the committed location (an action response's). */
|
|
154
|
+
from?: { params?: Record<string, string>; routeName?: string };
|
|
155
|
+
/** The destination, from the committed location; omit for "action" / "revalidate". */
|
|
156
|
+
to?: (from: RouteLocationInput) => RouteLocationInput;
|
|
157
|
+
action?: TransitionWhenActionInput;
|
|
158
|
+
extra?: TransitionWhenFn;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* decideTransitionGatedOff with `from` read from the store: call it before
|
|
163
|
+
* the commit moves the store. `state` is the live history.state, except for
|
|
164
|
+
* back/forward: at popstate history.state already belongs to the destination,
|
|
165
|
+
* so the entry being left comes from the store's per-entry memory (undefined
|
|
166
|
+
* after a reload, never guessed).
|
|
167
|
+
*/
|
|
168
|
+
export function decideCommitGatedOff(
|
|
169
|
+
store: NavigationStore,
|
|
170
|
+
segments: readonly ResolvedSegment[],
|
|
171
|
+
kind: TransitionWhenKind,
|
|
172
|
+
decision: CommitDecision = {},
|
|
173
|
+
): boolean {
|
|
174
|
+
return decideTransitionGatedOff(
|
|
175
|
+
segments,
|
|
176
|
+
() => {
|
|
177
|
+
const memory = store.getHistoryEntryMemory();
|
|
178
|
+
const from: RouteLocationInput = {
|
|
179
|
+
url: store.getSegmentState().currentUrl || window.location.href,
|
|
180
|
+
params:
|
|
181
|
+
decision.from?.params ??
|
|
182
|
+
mergeSegmentParams(
|
|
183
|
+
store.getCachedSegments(store.getHistoryKey())?.segments ?? [],
|
|
184
|
+
),
|
|
185
|
+
routeName: decision.from?.routeName ?? memory?.routeName,
|
|
186
|
+
state: kind === "pop" ? memory?.state : window.history.state,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
kind,
|
|
190
|
+
from,
|
|
191
|
+
to: decision.to?.(from),
|
|
192
|
+
action: decision.action,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
decision.extra,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The FormData argument a server action was called with, if any. */
|
|
200
|
+
export function findActionFormData(
|
|
201
|
+
args: readonly unknown[],
|
|
202
|
+
): FormData | undefined {
|
|
203
|
+
if (typeof FormData === "undefined") return undefined;
|
|
204
|
+
return args.find((arg): arg is FormData => arg instanceof FormData);
|
|
205
|
+
}
|
package/src/browser/types.ts
CHANGED
|
@@ -48,6 +48,12 @@ export interface RscMetadata {
|
|
|
48
48
|
resolvedIds?: string[];
|
|
49
49
|
/** Merged route params from the matched route */
|
|
50
50
|
params?: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* The matched route's name (include name prefix applied), when the route is
|
|
53
|
+
* named. The browser keeps it per history entry for transition({ when })'s
|
|
54
|
+
* `from.routeName` / `to.routeName` (browser/transition-when.ts).
|
|
55
|
+
*/
|
|
56
|
+
routeName?: string;
|
|
51
57
|
/**
|
|
52
58
|
* State of named slots for this route match
|
|
53
59
|
* Key is slot name (e.g., "@modal"), value is slot state
|
|
@@ -276,6 +282,18 @@ export interface SegmentState {
|
|
|
276
282
|
currentSegmentIds: string[];
|
|
277
283
|
}
|
|
278
284
|
|
|
285
|
+
/**
|
|
286
|
+
* What the router remembers about one history entry for transition({ when })
|
|
287
|
+
* (NavigationStore.rememberDisplayedEntry).
|
|
288
|
+
*
|
|
289
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
290
|
+
*/
|
|
291
|
+
export interface HistoryEntryMemory {
|
|
292
|
+
readonly routeName: string | undefined;
|
|
293
|
+
/** The entry's `history.state` as last recorded. */
|
|
294
|
+
readonly state: unknown;
|
|
295
|
+
}
|
|
296
|
+
|
|
279
297
|
/**
|
|
280
298
|
* Navigation update emitted when UI should re-render
|
|
281
299
|
*
|
|
@@ -296,13 +314,67 @@ export interface NavigationUpdate {
|
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
/**
|
|
299
|
-
*
|
|
300
|
-
* -
|
|
301
|
-
*
|
|
317
|
+
* Plain object accepted as {@link PlainHistoryState}. The `any` string index
|
|
318
|
+
* admits interface-typed values (an `unknown` index would reject interfaces,
|
|
319
|
+
* which carry no implicit index signature). The `never` members reject:
|
|
320
|
+
* - a single typed entry (`GridState(value)` without the array) — its
|
|
321
|
+
* `__rsc_ls_*` fields would be spread onto history.state and
|
|
322
|
+
* `useLocationState(GridState)` would read `undefined`;
|
|
323
|
+
* - functions, including a location-state definition passed uncalled
|
|
324
|
+
* (`Symbol.hasInstance` comes from `Function.prototype`), which throw
|
|
325
|
+
* `DataCloneError`;
|
|
326
|
+
* - arrays and other iterables (`Symbol.iterator`), so an array must match the
|
|
327
|
+
* typed-entry or plain-array member instead;
|
|
328
|
+
* - built-ins tagged with `Symbol.toStringTag` (`Promise`, `WeakMap`,
|
|
329
|
+
* `WeakSet`), which throw `DataCloneError`. The cloneable tagged ones
|
|
330
|
+
* (`Map`, `Set`, `ArrayBuffer`, typed arrays) have their own members.
|
|
331
|
+
*
|
|
332
|
+
* Members are not checked: a function, symbol, or React element inside a
|
|
333
|
+
* plain object still compiles, as does a top-level `ReactElement` or DOM node
|
|
334
|
+
* (an object with none of the keys above).
|
|
335
|
+
*/
|
|
336
|
+
export interface PlainHistoryObject {
|
|
337
|
+
readonly [key: string]: any;
|
|
338
|
+
readonly __rsc_ls_key?: never;
|
|
339
|
+
readonly __rsc_ls_value?: never;
|
|
340
|
+
readonly __rsc_ls_lazy?: never;
|
|
341
|
+
readonly [Symbol.iterator]?: never;
|
|
342
|
+
readonly [Symbol.hasInstance]?: never;
|
|
343
|
+
readonly [Symbol.toStringTag]?: never;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Plain (untyped) navigation state, stored under `history.state.state` and
|
|
348
|
+
* read with `useLocationState<T>()`. A structured-clone-safe value: primitives,
|
|
349
|
+
* arrays, `Map`/`Set`, `ArrayBuffer`, typed arrays, and plain objects. Excludes
|
|
350
|
+
* functions, symbols, `Promise`/`WeakMap`/`WeakSet`, and objects carrying
|
|
351
|
+
* `__rsc_ls_*` keys (a typed entry passed without its array). Arrays, `Map`,
|
|
352
|
+
* and `Set` are checked element by element; plain object members are not
|
|
353
|
+
* checked (see {@link PlainHistoryObject}).
|
|
354
|
+
*/
|
|
355
|
+
export type PlainHistoryState =
|
|
356
|
+
| string
|
|
357
|
+
| number
|
|
358
|
+
| boolean
|
|
359
|
+
| bigint
|
|
360
|
+
| null
|
|
361
|
+
| undefined
|
|
362
|
+
| readonly PlainHistoryState[]
|
|
363
|
+
| ReadonlyMap<PlainHistoryState, PlainHistoryState>
|
|
364
|
+
| ReadonlySet<PlainHistoryState>
|
|
365
|
+
| ArrayBuffer
|
|
366
|
+
| ArrayBufferView
|
|
367
|
+
| PlainHistoryObject;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* State value for `router.push()` / `router.replace()` / `<Link state>`.
|
|
371
|
+
* - `readonly LocationStateEntry[]`: typed entries from `createLocationState()`
|
|
372
|
+
* definitions, e.g. `[GridState({ count: 3 })]` (recommended)
|
|
373
|
+
* - {@link PlainHistoryState}: plain structured-clone-safe state
|
|
302
374
|
*/
|
|
303
375
|
export type HistoryState =
|
|
304
|
-
| import("./react/location-state-shared.js").LocationStateEntry[]
|
|
305
|
-
|
|
|
376
|
+
| readonly import("./react/location-state-shared.js").LocationStateEntry[]
|
|
377
|
+
| PlainHistoryState;
|
|
306
378
|
|
|
307
379
|
/**
|
|
308
380
|
* Options for navigation operations
|
|
@@ -329,6 +401,22 @@ export interface NavigateOptions {
|
|
|
329
401
|
* ```
|
|
330
402
|
*/
|
|
331
403
|
revalidate?: boolean;
|
|
404
|
+
/**
|
|
405
|
+
* Set to `false` to present this navigation without a transition: the
|
|
406
|
+
* commit is urgent and every `<ViewTransition>` class resolves to `"none"`,
|
|
407
|
+
* the same result as a `transition({ when })` predicate returning `false`.
|
|
408
|
+
* No `when` predicate is called. Back/forward, action and revalidation
|
|
409
|
+
* commits are unaffected (they are not started by this call).
|
|
410
|
+
*
|
|
411
|
+
* @default true (the route's transition config applies)
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```tsx
|
|
415
|
+
* router.push("/photos/2", { transition: false });
|
|
416
|
+
* <Link to="/photos/2" transition={false}>Next</Link>
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
transition?: boolean;
|
|
332
420
|
/**
|
|
333
421
|
* State to pass to history.pushState/replaceState
|
|
334
422
|
* Accessible via useLocationState() hook.
|
|
@@ -350,15 +438,25 @@ export interface NavigateOptions {
|
|
|
350
438
|
* // Plain static state
|
|
351
439
|
* navigate("/product", { state: { from: "list" } });
|
|
352
440
|
*
|
|
353
|
-
* //
|
|
354
|
-
* navigate("/product", { state: ()
|
|
441
|
+
* // Compile errors (and a dev-mode runtime error for untyped callers):
|
|
442
|
+
* navigate("/product", { state: ProductState(p) }); // entry without the array
|
|
443
|
+
* navigate("/product", { state: [ProductState] }); // definition not called
|
|
355
444
|
* ```
|
|
356
445
|
*/
|
|
357
446
|
state?: HistoryState;
|
|
358
447
|
}
|
|
359
448
|
|
|
360
|
-
/**
|
|
361
|
-
|
|
449
|
+
/**
|
|
450
|
+
* @internal Extended options used only within the navigation bridge. `state`
|
|
451
|
+
* is widened to `unknown`: the redirect lanes pass state already resolved to
|
|
452
|
+
* a flat `history.state` record, and in dev Link passes an uncalled
|
|
453
|
+
* definition through to resolveNavigationState's check.
|
|
454
|
+
*/
|
|
455
|
+
export interface NavigateOptionsInternal extends Omit<
|
|
456
|
+
NavigateOptions,
|
|
457
|
+
"state"
|
|
458
|
+
> {
|
|
459
|
+
state?: unknown;
|
|
362
460
|
/** Skip segment cache (used by redirect-with-state to force re-render) */
|
|
363
461
|
_skipCache?: boolean;
|
|
364
462
|
}
|
|
@@ -465,6 +563,22 @@ export interface NavigationStore {
|
|
|
465
563
|
// History-based segment cache (for back/forward navigation and partial merging)
|
|
466
564
|
getHistoryKey(): string;
|
|
467
565
|
setHistoryKey(key: string): void;
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Per-history-entry memory for transition({ when }) sources: record the
|
|
569
|
+
* entry on screen (keyed by its `history.state.key`) with its current
|
|
570
|
+
* `history.state` and, when given, its route name (kept from the last
|
|
571
|
+
* record otherwise; every push/replace creates a new key, so a stale name
|
|
572
|
+
* never carries to another entry). Called after every commit, restore and
|
|
573
|
+
* state merge.
|
|
574
|
+
*/
|
|
575
|
+
rememberDisplayedEntry(routeName?: string): void;
|
|
576
|
+
/**
|
|
577
|
+
* The memory of the entry on screen, or of `entryKey`. At popstate
|
|
578
|
+
* history.state already belongs to the destination, so back/forward reads
|
|
579
|
+
* the entry being LEFT from here. In-memory only: empty after a reload.
|
|
580
|
+
*/
|
|
581
|
+
getHistoryEntryMemory(entryKey?: string): HistoryEntryMemory | undefined;
|
|
468
582
|
/** Monotonic token of the most recently committed navigation. */
|
|
469
583
|
getNavInstance(): number;
|
|
470
584
|
cacheSegmentsForHistory(
|
|
@@ -631,7 +745,7 @@ export interface ServerActionBridgeConfig {
|
|
|
631
745
|
* Navigation bridge for handling client-side navigation
|
|
632
746
|
*/
|
|
633
747
|
export interface NavigationBridge {
|
|
634
|
-
navigate(url: string, options?:
|
|
748
|
+
navigate(url: string, options?: NavigateOptionsInternal): Promise<void>;
|
|
635
749
|
refresh(): Promise<void>;
|
|
636
750
|
handlePopstate(): Promise<void>;
|
|
637
751
|
registerLinkInterception(): () => void;
|
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
import type { UrlPatterns } from "../urls.js";
|
|
12
12
|
import type { AllUseItems } from "../route-types.js";
|
|
13
13
|
import { extractStaticPrefix } from "../router/pattern-matching.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getParallelSlotEntries,
|
|
16
|
+
RangoContext,
|
|
17
|
+
runWithPrefixes,
|
|
18
|
+
} from "../server/context.js";
|
|
15
19
|
import type { EntryData, TrackedInclude } from "../server/context.js";
|
|
16
20
|
import type { TrailingSlashMode } from "../types.js";
|
|
17
21
|
import { createRouteHelpers } from "../route-definition.js";
|
|
18
22
|
import MapRootLayout from "../server/root-layout.js";
|
|
19
23
|
import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
|
|
24
|
+
import {
|
|
25
|
+
createTransitionWhenError,
|
|
26
|
+
transitionWhenProblem,
|
|
27
|
+
} from "../transition-when-ref.js";
|
|
20
28
|
import {
|
|
21
29
|
isIncludeProvider,
|
|
22
30
|
resolveIncludeModule,
|
|
@@ -59,6 +67,57 @@ export interface GeneratedManifest {
|
|
|
59
67
|
routeSearchSchemas?: Record<string, Record<string, string>>;
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Fail discovery on an invalid transition({ when }) anywhere in the entry
|
|
72
|
+
* chains of the evaluated routes: the route itself, its ancestors, their
|
|
73
|
+
* orphan layouts, parallel slots and intercepts. Strict: a plain function is
|
|
74
|
+
* rejected, because discovery evaluates the tree in the react-server graph
|
|
75
|
+
* where a "use client" import is a client reference (transition-when-ref.ts).
|
|
76
|
+
* The error names the first route found through the entry.
|
|
77
|
+
*/
|
|
78
|
+
function validateTransitionWhens(
|
|
79
|
+
manifest: Map<string, EntryData>,
|
|
80
|
+
patterns: Map<string, string>,
|
|
81
|
+
): void {
|
|
82
|
+
const seen = new Set<object>();
|
|
83
|
+
for (const [routeName, routeEntry] of manifest) {
|
|
84
|
+
const route = {
|
|
85
|
+
routeName,
|
|
86
|
+
pattern:
|
|
87
|
+
patterns.get(routeName) ??
|
|
88
|
+
(routeEntry.type === "route" ? routeEntry.pattern : undefined),
|
|
89
|
+
};
|
|
90
|
+
const check = (when: unknown, entryType: string): void => {
|
|
91
|
+
const problem = transitionWhenProblem(when, true);
|
|
92
|
+
if (problem) {
|
|
93
|
+
throw createTransitionWhenError(when, problem, {
|
|
94
|
+
...route,
|
|
95
|
+
entryType,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const visit = (entry: EntryData): void => {
|
|
100
|
+
if (seen.has(entry)) return;
|
|
101
|
+
seen.add(entry);
|
|
102
|
+
check(entry.transition?.when, entry.type);
|
|
103
|
+
for (const orphan of entry.layout) visit(orphan);
|
|
104
|
+
for (const { entry: slot } of getParallelSlotEntries(entry.parallel)) {
|
|
105
|
+
visit(slot);
|
|
106
|
+
}
|
|
107
|
+
for (const intercept of entry.intercept) {
|
|
108
|
+
check(intercept.transition?.when, "intercept");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
for (
|
|
112
|
+
let entry: EntryData | null | undefined = routeEntry;
|
|
113
|
+
entry;
|
|
114
|
+
entry = entry.parent ?? entry.orphanOwner
|
|
115
|
+
) {
|
|
116
|
+
visit(entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
62
121
|
/**
|
|
63
122
|
* Build prefix tree node by running the patterns with proper context.
|
|
64
123
|
* Uses a visited set to detect circular includes and prevent infinite recursion.
|
|
@@ -116,6 +175,7 @@ async function buildPrefixTreeNode(
|
|
|
116
175
|
responseTypeRoutes?: Record<string, string>,
|
|
117
176
|
routeSearchSchemas?: Record<string, Record<string, string>>,
|
|
118
177
|
routerId?: string,
|
|
178
|
+
validateTransitionWhen?: boolean,
|
|
119
179
|
): Promise<PrefixTreeNode> {
|
|
120
180
|
// Resolve an async include provider (`() => import("./routes")`) so its routes
|
|
121
181
|
// are walked into the build-time manifest/types/href. Runtime matching still
|
|
@@ -174,6 +234,8 @@ async function buildPrefixTreeNode(
|
|
|
174
234
|
},
|
|
175
235
|
);
|
|
176
236
|
|
|
237
|
+
if (validateTransitionWhen) validateTransitionWhens(manifest, patternsMap);
|
|
238
|
+
|
|
177
239
|
// Collect route names defined in this include (routes have prefixes applied)
|
|
178
240
|
const routes = [...patternsMap.keys()];
|
|
179
241
|
Object.assign(routeManifest, Object.fromEntries(patternsMap));
|
|
@@ -230,6 +292,7 @@ async function buildPrefixTreeNode(
|
|
|
230
292
|
responseTypeRoutes,
|
|
231
293
|
routeSearchSchemas,
|
|
232
294
|
routerId,
|
|
295
|
+
validateTransitionWhen,
|
|
233
296
|
),
|
|
234
297
|
);
|
|
235
298
|
|
|
@@ -313,6 +376,14 @@ export async function generateManifestFull<TEnv>(
|
|
|
313
376
|
* EntryData map built below is local; this is the only seam that surfaces it.
|
|
314
377
|
*/
|
|
315
378
|
collectClientFallbackRef?: (refKey: string) => void;
|
|
379
|
+
/**
|
|
380
|
+
* Fail on an invalid transition({ when }): a non-function, or a plain
|
|
381
|
+
* (server) function where a client reference is required. Only the Vite
|
|
382
|
+
* plugin's discovery passes it: it evaluates the tree in the react-server
|
|
383
|
+
* graph, the one place a "use client" import is told apart from a server
|
|
384
|
+
* function (transition-when-ref.ts).
|
|
385
|
+
*/
|
|
386
|
+
validateTransitionWhen?: boolean;
|
|
316
387
|
},
|
|
317
388
|
): Promise<FullManifest> {
|
|
318
389
|
const routeManifest: Record<string, string> = {};
|
|
@@ -351,6 +422,10 @@ export async function generateManifestFull<TEnv>(
|
|
|
351
422
|
},
|
|
352
423
|
);
|
|
353
424
|
|
|
425
|
+
if (options?.validateTransitionWhen) {
|
|
426
|
+
validateTransitionWhens(manifest, patternsMap);
|
|
427
|
+
}
|
|
428
|
+
|
|
354
429
|
// Surface the "use client" components registered as error/notFound fallbacks
|
|
355
430
|
// (route-tree errorBoundary()/notFoundBoundary() helpers, stored on EntryData).
|
|
356
431
|
// The boundary may be a handler function and/or wrap the client boundary in
|
|
@@ -413,6 +488,7 @@ export async function generateManifestFull<TEnv>(
|
|
|
413
488
|
responseTypeRoutes,
|
|
414
489
|
routeSearchSchemas,
|
|
415
490
|
options?.routerId,
|
|
491
|
+
options?.validateTransitionWhen,
|
|
416
492
|
),
|
|
417
493
|
);
|
|
418
494
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Background Task Runner
|
|
3
3
|
*
|
|
4
|
-
* Unified helper for scheduling async work via waitUntil
|
|
5
|
-
*
|
|
4
|
+
* Unified helper for scheduling async work via waitUntil, and the two bounded
|
|
5
|
+
* waits the PPR shell capture puts on background work (raceDeadline,
|
|
6
|
+
* settleGrowing). When waitUntil is unavailable, falls back to blocking or
|
|
7
|
+
* skipping.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
interface WaitUntilHost {
|
|
@@ -32,3 +34,54 @@ export function runBackground(
|
|
|
32
34
|
return task();
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Race `work` against an absolute deadline (epoch ms), keeping its value. The
|
|
40
|
+
* losing work is not cancelled (a router match has no abort signal) and its
|
|
41
|
+
* late rejection is swallowed. The timer is unref'd, so a pending deadline
|
|
42
|
+
* never keeps a Node dev process alive.
|
|
43
|
+
*/
|
|
44
|
+
export async function raceDeadline<T>(
|
|
45
|
+
work: Promise<T>,
|
|
46
|
+
deadline: number,
|
|
47
|
+
): Promise<{ done: true; value: T } | { done: false }> {
|
|
48
|
+
work.catch(() => {});
|
|
49
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
const expired = new Promise<{ done: false }>((resolve) => {
|
|
51
|
+
timer = setTimeout(
|
|
52
|
+
() => resolve({ done: false }),
|
|
53
|
+
Math.max(0, deadline - Date.now()),
|
|
54
|
+
);
|
|
55
|
+
(timer as { unref?: () => void }).unref?.();
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
return await Promise.race([
|
|
59
|
+
work.then((value) => ({ done: true as const, value })),
|
|
60
|
+
expired,
|
|
61
|
+
]);
|
|
62
|
+
} finally {
|
|
63
|
+
if (timer) clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wait until every promise in `list` has settled, including ones appended
|
|
69
|
+
* while waiting (a settled write can schedule a nested one: cacheRoute
|
|
70
|
+
* schedules its store.set in a second waitUntil), or until `deadline` (epoch
|
|
71
|
+
* ms). Resolves true when the list drained, false at the deadline. Never
|
|
72
|
+
* mutates `list`: a later call waits for what is still pending.
|
|
73
|
+
*/
|
|
74
|
+
export async function settleGrowing(
|
|
75
|
+
list: readonly Promise<unknown>[],
|
|
76
|
+
deadline: number,
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
let seen = 0;
|
|
79
|
+
while (list.length > seen) {
|
|
80
|
+
if (Date.now() >= deadline) return false;
|
|
81
|
+
const batch = list.slice(seen);
|
|
82
|
+
seen = list.length;
|
|
83
|
+
const settled = await raceDeadline(Promise.allSettled(batch), deadline);
|
|
84
|
+
if (!settled.done) return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
@@ -38,6 +38,53 @@ export interface CacheExecScope {
|
|
|
38
38
|
|
|
39
39
|
const cacheExecStorage = new AsyncLocalStorage<CacheExecScope>();
|
|
40
40
|
|
|
41
|
+
// Global key: the value must survive HMR re-evaluation of this module, like
|
|
42
|
+
// the other request-scoped ALS instances (server/context.ts RangoContext).
|
|
43
|
+
const IDENTITY_EXEMPT_KEY = Symbol.for("rangojs-router:identity-exempt");
|
|
44
|
+
const identityExemptStorage: AsyncLocalStorage<boolean> = ((globalThis as any)[
|
|
45
|
+
IDENTITY_EXEMPT_KEY
|
|
46
|
+
] ??= new AsyncLocalStorage<boolean>());
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run a callback whose identity reads never reach a shared entry, so
|
|
50
|
+
* guardIdentityRead (server/context.ts) lets them through and records
|
|
51
|
+
* nothing (#976):
|
|
52
|
+
*
|
|
53
|
+
* - a cache's own decisions and metadata: a `key()`, a store `keyGenerator`
|
|
54
|
+
* (cache-policy.ts resolveCacheKey), a `condition()` (cache-scope.ts
|
|
55
|
+
* conditionAllows, loader-cache.ts) and a `tags()` function
|
|
56
|
+
* (cache-policy.ts resolveTagsOption). The value picks the entry, whether
|
|
57
|
+
* one is used, or how it is invalidated; it is never rendered. Reading the
|
|
58
|
+
* request there is the documented way to partition an entry;
|
|
59
|
+
* - the router's `onError` (router/error-handling.ts invokeOnError): it
|
|
60
|
+
* observes, and a render error can reach it inside a capture or a cached
|
|
61
|
+
* scope.
|
|
62
|
+
*
|
|
63
|
+
* Scar: a ppr capture resolves its doc record key, and the record's tags,
|
|
64
|
+
* again under the capture context; once `ctx.request.headers` was guarded, a
|
|
65
|
+
* keyGenerator or tags() reading it refused every capture of the route.
|
|
66
|
+
*
|
|
67
|
+
* The exemption is async-local, so it would follow everything the callback
|
|
68
|
+
* starts. A "use cache" body (runWithCacheExecScope), a loader body
|
|
69
|
+
* (server/context.ts runInsideLoaderBodyScope) and a segment funnel
|
|
70
|
+
* (getContext().runWithStore) end it on entry (endIdentityExempt). Scar: a
|
|
71
|
+
* key() that awaited a "use cache" function reading cookies() stored the
|
|
72
|
+
* first visitor's cookie and served it to the next.
|
|
73
|
+
*/
|
|
74
|
+
export function runIdentityExempt<T>(fn: () => T): T {
|
|
75
|
+
return identityExemptStorage.run(true, fn);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** True inside runIdentityExempt, and not in a scope started from it. */
|
|
79
|
+
export function isInsideIdentityExempt(): boolean {
|
|
80
|
+
return identityExemptStorage.getStore() === true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Run `fn` with the identity exemption ended (see runIdentityExempt). */
|
|
84
|
+
export function endIdentityExempt<T>(fn: () => T): T {
|
|
85
|
+
return isInsideIdentityExempt() ? identityExemptStorage.run(false, fn) : fn();
|
|
86
|
+
}
|
|
87
|
+
|
|
41
88
|
/**
|
|
42
89
|
* Run fn with the "use cache" execution scope active. Continuations spawned
|
|
43
90
|
* from fn's synchronous kickoff inherit the scope; parallel chains do not.
|
|
@@ -48,7 +95,7 @@ export function runWithCacheExecScope<T>(
|
|
|
48
95
|
fn: () => T,
|
|
49
96
|
scope: CacheExecScope = createCacheExecScope(),
|
|
50
97
|
): T {
|
|
51
|
-
return cacheExecStorage.run(scope, fn);
|
|
98
|
+
return endIdentityExempt(() => cacheExecStorage.run(scope, fn));
|
|
52
99
|
}
|
|
53
100
|
|
|
54
101
|
/** A scope nested in the calling chain's current one, if any. */
|