@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30
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/AGENTS.md +5 -0
- package/README.md +883 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4655 -747
- package/package.json +78 -50
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +54 -25
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +23 -21
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +390 -63
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +133 -10
- package/skills/layout/SKILL.md +102 -5
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +366 -29
- package/skills/middleware/SKILL.md +173 -36
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +80 -3
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +86 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +227 -14
- package/skills/router-setup/SKILL.md +225 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +12 -11
- package/skills/typesafety/SKILL.md +401 -75
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +10 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +20 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +201 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +267 -317
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +173 -73
- package/src/browser/react/NavigationProvider.tsx +138 -27
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +49 -65
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +27 -78
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +111 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +504 -584
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +92 -57
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +10 -15
- package/src/client.tsx +114 -135
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +34 -19
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +135 -49
- package/src/index.rsc.ts +182 -17
- package/src/index.ts +238 -24
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1388
- package/src/route-map-builder.ts +241 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +371 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +155 -32
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -29
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +321 -30
- package/src/router/prerender-match.ts +400 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +77 -3
- package/src/router.ts +688 -3656
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +786 -760
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +40 -14
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +57 -61
- package/src/server/context.ts +202 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +422 -70
- package/src/server.ts +36 -120
- package/src/ssr/index.tsx +157 -26
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1577
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -726
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -3
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyActionResponse,
|
|
3
|
+
type ActionScenario,
|
|
4
|
+
} from "./action-response-classifier.js";
|
|
5
|
+
import type { ActionEntry } from "./event-controller.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Plain data inputs for classifying a post-reconciliation action outcome.
|
|
9
|
+
* No browser objects or controller references — all values are snapshots.
|
|
10
|
+
*/
|
|
11
|
+
export interface ActionOutcomeInput {
|
|
12
|
+
/** This action's unique instance ID */
|
|
13
|
+
handleId: string;
|
|
14
|
+
/** All in-flight action entries (snapshot from event controller) */
|
|
15
|
+
inflightActions: Map<string, ActionEntry>;
|
|
16
|
+
/** Whether any concurrent actions occurred (controller-level shared flag) */
|
|
17
|
+
hadAnyConcurrentActions: boolean;
|
|
18
|
+
/** Segments revalidated by concurrent actions (from tracking set) */
|
|
19
|
+
revalidatedSegments: Set<string>;
|
|
20
|
+
/** window.location.pathname captured at action start */
|
|
21
|
+
actionStartPathname: string;
|
|
22
|
+
/** window.location.pathname at classification time */
|
|
23
|
+
currentPathname: string;
|
|
24
|
+
/** window.history.state?.key captured at action start */
|
|
25
|
+
actionStartLocationKey: string | undefined;
|
|
26
|
+
/** window.history.state?.key at classification time */
|
|
27
|
+
currentLocationKey: string | undefined;
|
|
28
|
+
/** Number of segments after reconciliation */
|
|
29
|
+
reconciledSegmentCount: number;
|
|
30
|
+
/** Number of matched segment IDs from server */
|
|
31
|
+
matchedCount: number;
|
|
32
|
+
/** Current intercept source URL (null when not on intercept route) */
|
|
33
|
+
currentInterceptSource: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute consolidation segments from concurrent action state.
|
|
38
|
+
*
|
|
39
|
+
* Returns segment IDs that need re-fetching when concurrent actions
|
|
40
|
+
* have each revalidated different parts of the tree, or null if
|
|
41
|
+
* consolidation is not needed.
|
|
42
|
+
*/
|
|
43
|
+
function computeConsolidationSegments(
|
|
44
|
+
input: ActionOutcomeInput,
|
|
45
|
+
): string[] | null {
|
|
46
|
+
if (!input.hadAnyConcurrentActions) return null;
|
|
47
|
+
if (input.revalidatedSegments.size === 0) return null;
|
|
48
|
+
|
|
49
|
+
// Can't consolidate while any action is still waiting for a server response
|
|
50
|
+
const stillFetchingCount = [...input.inflightActions.values()].filter(
|
|
51
|
+
(a) => a.phase === "fetching",
|
|
52
|
+
).length;
|
|
53
|
+
if (stillFetchingCount > 0) return null;
|
|
54
|
+
|
|
55
|
+
return Array.from(input.revalidatedSegments);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Count other actions still in "fetching" phase (excluding this handle).
|
|
60
|
+
*/
|
|
61
|
+
function countOtherFetchingActions(input: ActionOutcomeInput): number {
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const [, a] of input.inflightActions) {
|
|
64
|
+
if (a.phase === "fetching" && a.id !== input.handleId) {
|
|
65
|
+
count++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Classify a post-reconciliation action outcome into one of 5 scenarios.
|
|
73
|
+
*
|
|
74
|
+
* This is the single entry point for post-action decision logic.
|
|
75
|
+
* It gathers consolidation and concurrency data from the plain inputs,
|
|
76
|
+
* then delegates to the pure classifyActionResponse function.
|
|
77
|
+
*
|
|
78
|
+
* The server-action-bridge calls this after reconciliation to decide
|
|
79
|
+
* whether to render, skip, consolidate, or refetch.
|
|
80
|
+
*/
|
|
81
|
+
export function classifyActionOutcome(
|
|
82
|
+
input: ActionOutcomeInput,
|
|
83
|
+
): ActionScenario {
|
|
84
|
+
return classifyActionResponse({
|
|
85
|
+
actionStartPathname: input.actionStartPathname,
|
|
86
|
+
currentPathname: input.currentPathname,
|
|
87
|
+
actionStartLocationKey: input.actionStartLocationKey,
|
|
88
|
+
currentLocationKey: input.currentLocationKey,
|
|
89
|
+
reconciledSegmentCount: input.reconciledSegmentCount,
|
|
90
|
+
matchedCount: input.matchedCount,
|
|
91
|
+
currentInterceptSource: input.currentInterceptSource,
|
|
92
|
+
consolidationSegments: computeConsolidationSegments(input),
|
|
93
|
+
otherFetchingActionCount: countOtherFetchingActions(input),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type { ActionScenario };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discriminated union of post-reconciliation action response scenarios.
|
|
3
|
+
*
|
|
4
|
+
* Error and full-update-unsupported are handled inline in the bridge
|
|
5
|
+
* before reconciliation. This classifier only runs for partial responses
|
|
6
|
+
* that have been successfully reconciled.
|
|
7
|
+
*/
|
|
8
|
+
export type ActionScenario =
|
|
9
|
+
| {
|
|
10
|
+
type: "navigated-away";
|
|
11
|
+
historyKeyChanged: boolean;
|
|
12
|
+
onInterceptRoute: boolean;
|
|
13
|
+
}
|
|
14
|
+
| { type: "hmr-missing" }
|
|
15
|
+
| { type: "consolidation-needed"; segmentIds: string[] }
|
|
16
|
+
| { type: "concurrent-skip"; otherFetchingCount: number }
|
|
17
|
+
| { type: "normal" };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pure data inputs for classifying a partial action response.
|
|
21
|
+
* All values come from the bridge but no browser APIs or side effects.
|
|
22
|
+
*/
|
|
23
|
+
export interface ClassifierInput {
|
|
24
|
+
/** window.location.pathname captured at action start */
|
|
25
|
+
actionStartPathname: string;
|
|
26
|
+
/** window.location.pathname at classification time */
|
|
27
|
+
currentPathname: string;
|
|
28
|
+
/** window.history.state?.key captured at action start */
|
|
29
|
+
actionStartLocationKey: string | undefined;
|
|
30
|
+
/** window.history.state?.key at classification time */
|
|
31
|
+
currentLocationKey: string | undefined;
|
|
32
|
+
/** Number of segments after reconciliation */
|
|
33
|
+
reconciledSegmentCount: number;
|
|
34
|
+
/** Number of matched segment IDs from server */
|
|
35
|
+
matchedCount: number;
|
|
36
|
+
/** Segment IDs needing consolidation (from concurrent action tracking) */
|
|
37
|
+
consolidationSegments: string[] | null;
|
|
38
|
+
/** Number of other actions still in "fetching" phase */
|
|
39
|
+
otherFetchingActionCount: number;
|
|
40
|
+
/** Current intercept source URL (null when not on intercept route) */
|
|
41
|
+
currentInterceptSource: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify a partial action response into one of 5 post-reconciliation
|
|
46
|
+
* scenarios.
|
|
47
|
+
*
|
|
48
|
+
* Called after error and full-update cases are handled inline by the bridge.
|
|
49
|
+
* The classification order matches the priority chain:
|
|
50
|
+
* 1. User navigated away during action
|
|
51
|
+
* 2. HMR missing segments (fewer reconciled than matched)
|
|
52
|
+
* 3. Consolidation needed (concurrent actions finished)
|
|
53
|
+
* 4. Concurrent skip (other actions still fetching)
|
|
54
|
+
* 5. Normal (single action, no issues)
|
|
55
|
+
*
|
|
56
|
+
* This is a pure function with no side effects - the bridge handles
|
|
57
|
+
* all UI updates, store mutations, and network requests based on the
|
|
58
|
+
* returned scenario.
|
|
59
|
+
*/
|
|
60
|
+
export function classifyActionResponse(input: ClassifierInput): ActionScenario {
|
|
61
|
+
// Check if user navigated away during the action
|
|
62
|
+
const userNavigatedAway =
|
|
63
|
+
input.currentPathname !== input.actionStartPathname ||
|
|
64
|
+
input.currentLocationKey !== input.actionStartLocationKey;
|
|
65
|
+
|
|
66
|
+
if (userNavigatedAway) {
|
|
67
|
+
const historyKeyChanged =
|
|
68
|
+
input.currentLocationKey !== input.actionStartLocationKey;
|
|
69
|
+
return {
|
|
70
|
+
type: "navigated-away",
|
|
71
|
+
historyKeyChanged,
|
|
72
|
+
onInterceptRoute: input.currentInterceptSource !== null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// HMR resilience: segments missing after reconciliation
|
|
77
|
+
if (input.reconciledSegmentCount < input.matchedCount) {
|
|
78
|
+
return { type: "hmr-missing" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Consolidation needed for concurrent actions
|
|
82
|
+
if (input.consolidationSegments && input.consolidationSegments.length > 0) {
|
|
83
|
+
return {
|
|
84
|
+
type: "consolidation-needed",
|
|
85
|
+
segmentIds: input.consolidationSegments,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Other actions still fetching - skip UI update
|
|
90
|
+
if (input.otherFetchingActionCount > 0) {
|
|
91
|
+
return {
|
|
92
|
+
type: "concurrent-skip",
|
|
93
|
+
otherFetchingCount: input.otherFetchingActionCount,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Normal single-action completion
|
|
98
|
+
return { type: "normal" };
|
|
99
|
+
}
|
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
ResolvedSegment,
|
|
9
9
|
RscMetadata,
|
|
10
10
|
HandleData,
|
|
11
|
+
StreamingToken,
|
|
11
12
|
} from "./types.js";
|
|
13
|
+
import { filterSegmentOrder } from "./react/filter-segment-order.js";
|
|
12
14
|
|
|
13
15
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
14
16
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -40,7 +42,7 @@ export interface NavigationEntry {
|
|
|
40
42
|
abort: AbortController;
|
|
41
43
|
phase: NavigationPhase;
|
|
42
44
|
startedAt: number;
|
|
43
|
-
options?: NavigateOptions;
|
|
45
|
+
options?: NavigateOptions & { skipLoadingState?: boolean };
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -116,15 +118,6 @@ export interface HandleState {
|
|
|
116
118
|
segmentOrder: string[];
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
/**
|
|
120
|
-
* Token for tracking an active stream
|
|
121
|
-
* Call end() when the stream completes
|
|
122
|
-
*/
|
|
123
|
-
export interface StreamingToken {
|
|
124
|
-
/** End this streaming operation */
|
|
125
|
-
end(): void;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
121
|
/**
|
|
129
122
|
* Result from starting a navigation
|
|
130
123
|
* Implements Disposable for use with `using` keyword
|
|
@@ -165,8 +158,8 @@ export interface ActionHandle extends Disposable {
|
|
|
165
158
|
readonly settled: boolean;
|
|
166
159
|
/** Check if any concurrent actions were started */
|
|
167
160
|
hadConcurrentActions: boolean;
|
|
168
|
-
/** Get
|
|
169
|
-
|
|
161
|
+
/** Get raw set of segments revalidated by concurrent actions */
|
|
162
|
+
getRevalidatedSegments(): Set<string>;
|
|
170
163
|
/** Clear consolidation tracking */
|
|
171
164
|
clearConsolidation(): void;
|
|
172
165
|
}
|
|
@@ -176,7 +169,10 @@ export interface ActionHandle extends Disposable {
|
|
|
176
169
|
*/
|
|
177
170
|
export interface EventController {
|
|
178
171
|
// Navigation operations
|
|
179
|
-
startNavigation(
|
|
172
|
+
startNavigation(
|
|
173
|
+
url: string,
|
|
174
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
175
|
+
): NavigationHandle;
|
|
180
176
|
abortNavigation(): void;
|
|
181
177
|
|
|
182
178
|
// Action operations
|
|
@@ -186,6 +182,7 @@ export interface EventController {
|
|
|
186
182
|
// State access
|
|
187
183
|
getState(): DerivedNavigationState;
|
|
188
184
|
getActionState(actionId: string): TrackedActionState;
|
|
185
|
+
getLocation(): NavigationLocation;
|
|
189
186
|
|
|
190
187
|
// Location updates (for popstate where navigation doesn't go through startNavigation)
|
|
191
188
|
setLocation(location: NavigationLocation): void;
|
|
@@ -194,7 +191,7 @@ export interface EventController {
|
|
|
194
191
|
subscribe(listener: StateListener): () => void;
|
|
195
192
|
subscribeToAction(
|
|
196
193
|
actionId: string,
|
|
197
|
-
listener: ActionStateListener
|
|
194
|
+
listener: ActionStateListener,
|
|
198
195
|
): () => void;
|
|
199
196
|
subscribeToHandles(listener: HandleListener): () => void;
|
|
200
197
|
|
|
@@ -202,13 +199,19 @@ export interface EventController {
|
|
|
202
199
|
setHandleData(
|
|
203
200
|
data: HandleData,
|
|
204
201
|
matched?: string[],
|
|
205
|
-
isPartial?: boolean
|
|
202
|
+
isPartial?: boolean,
|
|
206
203
|
): void;
|
|
207
204
|
getHandleState(): HandleState;
|
|
208
205
|
|
|
206
|
+
// Params operations
|
|
207
|
+
setParams(params: Record<string, string>): void;
|
|
208
|
+
getParams(): Record<string, string>;
|
|
209
|
+
|
|
209
210
|
// Direct state access for advanced use
|
|
210
211
|
getCurrentNavigation(): NavigationEntry | null;
|
|
211
212
|
getInflightActions(): Map<string, ActionEntry>;
|
|
213
|
+
/** Whether any concurrent actions have occurred (shared across all handles) */
|
|
214
|
+
hadAnyConcurrentActions(): boolean;
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
// ============================================================================
|
|
@@ -230,7 +233,10 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
230
233
|
* When subscriptionId has no '#', it's just an action name and matches by suffix.
|
|
231
234
|
* This allows useAction("addToCart") to match "hash#addToCart" or "src/file.ts#addToCart".
|
|
232
235
|
*/
|
|
233
|
-
function matchesActionId(
|
|
236
|
+
function matchesActionId(
|
|
237
|
+
subscriptionId: string,
|
|
238
|
+
entryActionId: string,
|
|
239
|
+
): boolean {
|
|
234
240
|
if (subscriptionId.includes("#")) {
|
|
235
241
|
// Full ID: exact match
|
|
236
242
|
return subscriptionId === entryActionId;
|
|
@@ -261,7 +267,7 @@ export interface EventControllerConfig {
|
|
|
261
267
|
* Actions use mergeMap semantics (all run concurrently, consolidate at end).
|
|
262
268
|
*/
|
|
263
269
|
export function createEventController(
|
|
264
|
-
config?: EventControllerConfig
|
|
270
|
+
config?: EventControllerConfig,
|
|
265
271
|
): EventController {
|
|
266
272
|
// ========================================================================
|
|
267
273
|
// Source of Truth
|
|
@@ -293,6 +299,9 @@ export function createEventController(
|
|
|
293
299
|
let handleData: HandleData = {};
|
|
294
300
|
let handleSegmentOrder: string[] = [];
|
|
295
301
|
|
|
302
|
+
// Merged route params from current match
|
|
303
|
+
let routeParams: Record<string, string> = {};
|
|
304
|
+
|
|
296
305
|
// ========================================================================
|
|
297
306
|
// Listeners
|
|
298
307
|
// ========================================================================
|
|
@@ -334,7 +343,7 @@ export function createEventController(
|
|
|
334
343
|
listeners.forEach((listener) => listener(state));
|
|
335
344
|
}
|
|
336
345
|
}
|
|
337
|
-
}, 0)
|
|
346
|
+
}, 0),
|
|
338
347
|
);
|
|
339
348
|
}
|
|
340
349
|
|
|
@@ -367,9 +376,12 @@ export function createEventController(
|
|
|
367
376
|
}));
|
|
368
377
|
|
|
369
378
|
// State: loading if navigation OR actions are in progress
|
|
379
|
+
// Background revalidations (skipLoadingState) don't affect visible state
|
|
370
380
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
371
|
-
const
|
|
372
|
-
currentNavigation !== null
|
|
381
|
+
const isVisibleNavigation =
|
|
382
|
+
currentNavigation !== null &&
|
|
383
|
+
!currentNavigation.options?.skipLoadingState;
|
|
384
|
+
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
373
385
|
|
|
374
386
|
// Streaming: true if any active streams (navigation or action) or loading
|
|
375
387
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
@@ -378,8 +390,13 @@ export function createEventController(
|
|
|
378
390
|
state,
|
|
379
391
|
isStreaming,
|
|
380
392
|
location,
|
|
381
|
-
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
|
|
382
|
-
|
|
393
|
+
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
394
|
+
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
395
|
+
pendingUrl:
|
|
396
|
+
currentNavigation?.phase === "fetching" &&
|
|
397
|
+
!currentNavigation.options?.skipLoadingState
|
|
398
|
+
? currentNavigation.url
|
|
399
|
+
: null,
|
|
383
400
|
inflightActions: inflightActionsList,
|
|
384
401
|
};
|
|
385
402
|
}
|
|
@@ -388,12 +405,16 @@ export function createEventController(
|
|
|
388
405
|
// Find the most recent action with this ID that's not settling
|
|
389
406
|
// Uses suffix matching when actionId is just a name (no #)
|
|
390
407
|
const activeEntry = [...inflightActions.values()]
|
|
391
|
-
.filter(
|
|
408
|
+
.filter(
|
|
409
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling",
|
|
410
|
+
)
|
|
392
411
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
393
412
|
|
|
394
413
|
// Also check for settling entries to get result/error
|
|
395
414
|
const settlingEntry = [...inflightActions.values()]
|
|
396
|
-
.filter(
|
|
415
|
+
.filter(
|
|
416
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
417
|
+
)
|
|
397
418
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
398
419
|
|
|
399
420
|
const entry = activeEntry || settlingEntry;
|
|
@@ -431,7 +452,7 @@ export function createEventController(
|
|
|
431
452
|
|
|
432
453
|
function startNavigation(
|
|
433
454
|
url: string,
|
|
434
|
-
options?: NavigateOptions
|
|
455
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
435
456
|
): NavigationHandle {
|
|
436
457
|
// Cancel existing navigation (switchMap semantics)
|
|
437
458
|
if (currentNavigation) {
|
|
@@ -463,6 +484,7 @@ export function createEventController(
|
|
|
463
484
|
|
|
464
485
|
startStreaming(): StreamingToken {
|
|
465
486
|
let ended = false;
|
|
487
|
+
entry.phase = "streaming";
|
|
466
488
|
activeStreamCount++;
|
|
467
489
|
notify();
|
|
468
490
|
|
|
@@ -650,24 +672,8 @@ export function createEventController(
|
|
|
650
672
|
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
651
673
|
},
|
|
652
674
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// We don't need to wait for streaming to complete since we're refetching anyway
|
|
656
|
-
// Count actions that are still fetching (waiting for server response)
|
|
657
|
-
const stillFetchingCount = [...inflightActions.values()].filter(
|
|
658
|
-
(a) => a.phase === "fetching"
|
|
659
|
-
).length;
|
|
660
|
-
|
|
661
|
-
if (stillFetchingCount > 0) {
|
|
662
|
-
return null; // Some actions still waiting for server response
|
|
663
|
-
}
|
|
664
|
-
if (!hadAnyConcurrentActions) {
|
|
665
|
-
return null; // No concurrent actions occurred
|
|
666
|
-
}
|
|
667
|
-
if (concurrentRevalidatedSegments.size === 0) {
|
|
668
|
-
return null; // No segments to consolidate
|
|
669
|
-
}
|
|
670
|
-
return Array.from(concurrentRevalidatedSegments);
|
|
675
|
+
getRevalidatedSegments(): Set<string> {
|
|
676
|
+
return concurrentRevalidatedSegments;
|
|
671
677
|
},
|
|
672
678
|
|
|
673
679
|
clearConsolidation() {
|
|
@@ -702,16 +708,26 @@ export function createEventController(
|
|
|
702
708
|
}
|
|
703
709
|
|
|
704
710
|
function abortAllActions() {
|
|
705
|
-
for (const entry of inflightActions
|
|
711
|
+
for (const [id, entry] of inflightActions) {
|
|
712
|
+
// Preserve settling entries — they have already been handled by
|
|
713
|
+
// fail()/complete() and will self-cleanup via the settlement timeout.
|
|
714
|
+
// Clearing them here would prevent debounced notifications from
|
|
715
|
+
// delivering the error/result state to subscribers.
|
|
716
|
+
if (entry.phase === "settling") continue;
|
|
706
717
|
entry.abort.abort();
|
|
718
|
+
inflightActions.delete(id);
|
|
707
719
|
}
|
|
708
|
-
inflightActions.clear();
|
|
709
720
|
hadAnyConcurrentActions = false;
|
|
710
721
|
concurrentRevalidatedSegments.clear();
|
|
711
722
|
notify();
|
|
712
|
-
// Notify all action listeners
|
|
713
|
-
|
|
714
|
-
|
|
723
|
+
// Notify all action listeners directly by subscription ID.
|
|
724
|
+
// actionListeners keys are subscription IDs (possibly short names like
|
|
725
|
+
// "addToCart"), not full entry actionIds. Passing them to notifyAction
|
|
726
|
+
// would fail the suffix matcher — instead, notify each subscriber with
|
|
727
|
+
// its own state.
|
|
728
|
+
for (const [subscriptionId, listeners] of actionListeners) {
|
|
729
|
+
const state = getActionState(subscriptionId);
|
|
730
|
+
listeners.forEach((listener) => listener(state));
|
|
715
731
|
}
|
|
716
732
|
}
|
|
717
733
|
|
|
@@ -719,22 +735,10 @@ export function createEventController(
|
|
|
719
735
|
// Handle Operations
|
|
720
736
|
// ========================================================================
|
|
721
737
|
|
|
722
|
-
/**
|
|
723
|
-
* Filter segment IDs to only include routes and layouts.
|
|
724
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
725
|
-
*/
|
|
726
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
727
|
-
return matched.filter((id) => {
|
|
728
|
-
if (id.includes(".@")) return false;
|
|
729
|
-
if (/D\d+\./.test(id)) return false;
|
|
730
|
-
return true;
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
738
|
function setHandleData(
|
|
735
739
|
data: HandleData,
|
|
736
740
|
matched?: string[],
|
|
737
|
-
isPartial?: boolean
|
|
741
|
+
isPartial?: boolean,
|
|
738
742
|
): void {
|
|
739
743
|
const newSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
740
744
|
|
|
@@ -783,7 +787,7 @@ export function createEventController(
|
|
|
783
787
|
|
|
784
788
|
function subscribeToAction(
|
|
785
789
|
actionId: string,
|
|
786
|
-
listener: ActionStateListener
|
|
790
|
+
listener: ActionStateListener,
|
|
787
791
|
): () => void {
|
|
788
792
|
let listeners = actionListeners.get(actionId);
|
|
789
793
|
if (!listeners) {
|
|
@@ -805,6 +809,19 @@ export function createEventController(
|
|
|
805
809
|
return () => handleListeners.delete(listener);
|
|
806
810
|
}
|
|
807
811
|
|
|
812
|
+
// ========================================================================
|
|
813
|
+
// Params Operations
|
|
814
|
+
// ========================================================================
|
|
815
|
+
|
|
816
|
+
function setParams(params: Record<string, string>): void {
|
|
817
|
+
routeParams = params;
|
|
818
|
+
notify();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function getParams(): Record<string, string> {
|
|
822
|
+
return routeParams;
|
|
823
|
+
}
|
|
824
|
+
|
|
808
825
|
// ========================================================================
|
|
809
826
|
// Return Controller
|
|
810
827
|
// ========================================================================
|
|
@@ -821,12 +838,17 @@ export function createEventController(
|
|
|
821
838
|
// State
|
|
822
839
|
getState,
|
|
823
840
|
getActionState,
|
|
841
|
+
getLocation: () => location,
|
|
824
842
|
setLocation,
|
|
825
843
|
|
|
826
844
|
// Handles
|
|
827
845
|
setHandleData,
|
|
828
846
|
getHandleState,
|
|
829
847
|
|
|
848
|
+
// Params
|
|
849
|
+
setParams,
|
|
850
|
+
getParams,
|
|
851
|
+
|
|
830
852
|
// Subscriptions
|
|
831
853
|
subscribe,
|
|
832
854
|
subscribeToAction,
|
|
@@ -835,6 +857,7 @@ export function createEventController(
|
|
|
835
857
|
// Direct access
|
|
836
858
|
getCurrentNavigation: () => currentNavigation,
|
|
837
859
|
getInflightActions: () => inflightActions,
|
|
860
|
+
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
838
861
|
};
|
|
839
862
|
}
|
|
840
863
|
|
|
@@ -848,7 +871,7 @@ let controllerInstance: EventController | null = null;
|
|
|
848
871
|
* Initialize the global event controller
|
|
849
872
|
*/
|
|
850
873
|
export function initEventController(
|
|
851
|
-
config?: EventControllerConfig
|
|
874
|
+
config?: EventControllerConfig,
|
|
852
875
|
): EventController {
|
|
853
876
|
if (!controllerInstance) {
|
|
854
877
|
controllerInstance = createEventController(config);
|
|
@@ -862,7 +885,7 @@ export function initEventController(
|
|
|
862
885
|
export function getEventController(): EventController {
|
|
863
886
|
if (!controllerInstance) {
|
|
864
887
|
throw new Error(
|
|
865
|
-
"Event controller not initialized. Call initEventController first."
|
|
888
|
+
"Event controller not initialized. Call initEventController first.",
|
|
866
889
|
);
|
|
867
890
|
}
|
|
868
891
|
return controllerInstance;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLocationStateEntry,
|
|
3
|
+
resolveLocationStateEntries,
|
|
4
|
+
} from "./react/location-state-shared.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
|
|
8
|
+
*/
|
|
9
|
+
function isTypedLocationState(
|
|
10
|
+
state: unknown,
|
|
11
|
+
): state is Record<string, unknown> {
|
|
12
|
+
if (state === null || typeof state !== "object") return false;
|
|
13
|
+
return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve navigation state - handles both LocationStateEntry[] and plain formats
|
|
18
|
+
*/
|
|
19
|
+
export function resolveNavigationState(state: unknown): unknown {
|
|
20
|
+
if (
|
|
21
|
+
Array.isArray(state) &&
|
|
22
|
+
state.length > 0 &&
|
|
23
|
+
isLocationStateEntry(state[0])
|
|
24
|
+
) {
|
|
25
|
+
return resolveLocationStateEntries(state);
|
|
26
|
+
}
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build history state object from user state
|
|
32
|
+
* - Typed state: spread directly into history.state
|
|
33
|
+
* - Plain state: store in history.state.state
|
|
34
|
+
*/
|
|
35
|
+
export function buildHistoryState(
|
|
36
|
+
userState: unknown,
|
|
37
|
+
routerState?: { intercept?: boolean; sourceUrl?: string },
|
|
38
|
+
serverState?: Record<string, unknown>,
|
|
39
|
+
): Record<string, unknown> | null {
|
|
40
|
+
const result: Record<string, unknown> = {};
|
|
41
|
+
|
|
42
|
+
if (routerState?.intercept) {
|
|
43
|
+
result.intercept = true;
|
|
44
|
+
if (routerState.sourceUrl) {
|
|
45
|
+
result.sourceUrl = routerState.sourceUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (userState !== undefined) {
|
|
50
|
+
if (isTypedLocationState(userState)) {
|
|
51
|
+
Object.assign(result, userState);
|
|
52
|
+
} else {
|
|
53
|
+
result.state = userState;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (serverState) {
|
|
58
|
+
Object.assign(result, serverState);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Merge server-set location state into the current history entry.
|
|
66
|
+
* Replaces the current history state and dispatches notification event
|
|
67
|
+
* so useLocationState hooks re-read from history.state.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeLocationState(
|
|
70
|
+
locationState: Record<string, unknown>,
|
|
71
|
+
): void {
|
|
72
|
+
const merged = {
|
|
73
|
+
...window.history.state,
|
|
74
|
+
...locationState,
|
|
75
|
+
};
|
|
76
|
+
window.history.replaceState(merged, "", window.location.href);
|
|
77
|
+
if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
|
|
78
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
import type { SlotState } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a segment is an intercept segment.
|
|
6
|
+
* Intercept segments have namespace starting with "intercept:" — both the
|
|
7
|
+
* parallel container (@modal) and its content children receive this namespace
|
|
8
|
+
* from intercept-resolution.ts. Regular parallel segments like @sidebar do not.
|
|
9
|
+
*/
|
|
10
|
+
export function isInterceptSegment(s: ResolvedSegment): boolean {
|
|
11
|
+
return s.namespace?.startsWith("intercept:") === true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split an array of segments into main and intercept groups.
|
|
16
|
+
* Intercept segments are separated for explicit injection into the render tree
|
|
17
|
+
* via the interceptSegments render option.
|
|
18
|
+
*/
|
|
19
|
+
export function splitInterceptSegments(segments: ResolvedSegment[]): {
|
|
20
|
+
main: ResolvedSegment[];
|
|
21
|
+
intercept: ResolvedSegment[];
|
|
22
|
+
} {
|
|
23
|
+
const main: ResolvedSegment[] = [];
|
|
24
|
+
const intercept: ResolvedSegment[] = [];
|
|
25
|
+
for (const s of segments) {
|
|
26
|
+
if (isInterceptSegment(s)) {
|
|
27
|
+
intercept.push(s);
|
|
28
|
+
} else {
|
|
29
|
+
main.push(s);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { main, intercept };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if any slot is currently active (has content to render).
|
|
37
|
+
* Active slots indicate an intercept response where a parallel segment
|
|
38
|
+
* (e.g., @modal) has matched and should be rendered.
|
|
39
|
+
*/
|
|
40
|
+
export function hasActiveIntercept(slots?: Record<string, SlotState>): boolean {
|
|
41
|
+
if (!slots) return false;
|
|
42
|
+
return Object.values(slots).some((slot) => slot.active);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if cached segments contain any intercept segments.
|
|
47
|
+
* Intercept caches shouldn't be used for cached SWR rendering since
|
|
48
|
+
* whether interception happens depends on the current page context.
|
|
49
|
+
*/
|
|
50
|
+
export function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
|
|
51
|
+
return segments.some(isInterceptSegment);
|
|
52
|
+
}
|