@rangojs/router 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/action-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +8 -2
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/react/use-action.d.ts +7 -2
- package/dist/types/browser/react/use-handle.d.ts +19 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/segment-reconciler.d.ts +32 -4
- package/dist/types/browser/types.d.ts +24 -5
- package/dist/types/browser/wire-protocol.d.ts +26 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +11 -5
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +6 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +11 -1
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +53 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/testing/render-route.d.ts +43 -8
- package/dist/types/testing/serve-shell-request.d.ts +8 -1
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +30 -3
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +39 -0
- package/dist/vite/index.js +3822 -3727
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/client-urls/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +33 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +16 -5
- package/skills/intercept/SKILL.md +23 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +15 -1
- package/skills/testing/client-components.md +25 -18
- package/skills/testing/response-routes.md +6 -4
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/use-cache/SKILL.md +46 -8
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/__internal.ts +7 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +37 -13
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +59 -26
- package/src/browser/navigation-client.ts +20 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/react/use-action.ts +24 -48
- package/src/browser/react/use-handle.ts +22 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +92 -41
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +109 -53
- package/src/browser/server-action-bridge.ts +81 -61
- package/src/browser/types.ts +25 -5
- package/src/browser/wire-protocol.ts +43 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +132 -75
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +64 -46
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +6 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +22 -10
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +82 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +5 -5
- package/src/router/match-context.ts +23 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +46 -57
- package/src/router/match-middleware/cache-lookup.ts +677 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +14 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +15 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +139 -0
- package/src/router/segment-resolution/revalidation.ts +939 -789
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +11 -1
- package/src/router.ts +19 -17
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +759 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +275 -227
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +75 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +226 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/render-route.tsx +199 -13
- package/src/testing/serve-shell-request.ts +36 -40
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +30 -3
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +40 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -38,6 +38,7 @@ import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
|
|
|
38
38
|
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
39
39
|
import type { RscPayload } from "../types.js";
|
|
40
40
|
import { getRegisteredStore } from "../navigation-store-handle.js";
|
|
41
|
+
import { writePartialParams } from "../wire-protocol.js";
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Decoder injected at app startup (see setPrefetchDecoder). This is
|
|
@@ -153,24 +154,15 @@ function buildPrefetchUrl(
|
|
|
153
154
|
if (targetUrl.origin !== window.location.origin) {
|
|
154
155
|
return null;
|
|
155
156
|
}
|
|
156
|
-
targetUrl.searchParams.set("_rsc_partial", "true");
|
|
157
|
-
if (segmentIds.length > 0) {
|
|
158
|
-
targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
|
|
159
|
-
}
|
|
160
|
-
if (version) {
|
|
161
|
-
targetUrl.searchParams.set("_rsc_v", version);
|
|
162
|
-
}
|
|
163
|
-
if (routerId) {
|
|
164
|
-
targetUrl.searchParams.set("_rsc_rid", routerId);
|
|
165
|
-
}
|
|
166
157
|
// The page's rewrite target, so the server diffs against the route that
|
|
167
158
|
// rendered it, as for the navigation.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
159
|
+
return writePartialParams(targetUrl, {
|
|
160
|
+
segmentIds,
|
|
161
|
+
emptySegments: "omit",
|
|
162
|
+
version,
|
|
163
|
+
routerId,
|
|
164
|
+
rewriteTarget: getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget,
|
|
165
|
+
});
|
|
174
166
|
}
|
|
175
167
|
|
|
176
168
|
/**
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { wait, waitForIdle, waitForViewportImages } from "./resource-ready.js";
|
|
18
|
+
import { WIRE_PREFIX } from "../../wire-params.js";
|
|
18
19
|
|
|
19
20
|
// Max prefetches executing at once. Mirrors DEFAULT_PREFETCH_CONCURRENCY
|
|
20
21
|
// (router/prefetch-limits.ts); kept as a local literal so the client bundle
|
|
@@ -128,16 +129,15 @@ export function enqueuePrefetch(
|
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
131
|
* Normalize a URL-like string for keep-alive matching: parse against a
|
|
131
|
-
* placeholder origin and strip
|
|
132
|
+
* placeholder origin and strip every `WIRE_PREFIX` query param. Returns
|
|
132
133
|
* `pathname + search` so comparisons ignore hash and the internal params
|
|
133
|
-
* that prefetch appends to targets
|
|
134
|
-
* `_rsc_v`, `_rsc_rid`, `_rsc_stale`).
|
|
134
|
+
* that prefetch appends to targets.
|
|
135
135
|
*/
|
|
136
136
|
function normalizeForMatch(urlish: string): string {
|
|
137
137
|
try {
|
|
138
138
|
const u = new URL(urlish, "http://placeholder");
|
|
139
139
|
for (const k of [...u.searchParams.keys()]) {
|
|
140
|
-
if (k.startsWith(
|
|
140
|
+
if (k.startsWith(WIRE_PREFIX)) u.searchParams.delete(k);
|
|
141
141
|
}
|
|
142
142
|
return u.pathname + u.search;
|
|
143
143
|
} catch {
|
|
@@ -78,7 +78,8 @@ async function processHandles(
|
|
|
78
78
|
|
|
79
79
|
// This nav's instance token, captured before any await — processHandles runs
|
|
80
80
|
// right after its own commit, so this is that commit's token. generateHistoryKey
|
|
81
|
-
// is URL
|
|
81
|
+
// is the URL (plus the source page for an intercept), so an A->B->A revisit
|
|
82
|
+
// reuses the key; the token lets a late
|
|
82
83
|
// resolution tell its own visit apart from a newer same-URL visit, so a stale
|
|
83
84
|
// nav can never clobber a fresher one's live state or cache (P1).
|
|
84
85
|
const myInstance = store.getNavInstance();
|
|
@@ -33,8 +33,8 @@ export function loaderParentId(id: string): string {
|
|
|
33
33
|
* 2. Place each parallel slot id (contains ".@") immediately after its
|
|
34
34
|
* parent layout/route id. Raw segment-resolution emission order does NOT
|
|
35
35
|
* guarantee this: route-mounted parallels are resolved/pushed BEFORE the
|
|
36
|
-
* route handler's segment is appended (see
|
|
37
|
-
*
|
|
36
|
+
* route handler's segment is appended (see resolveSegment in
|
|
37
|
+
* segment-resolution/revalidation.ts), so matched can read
|
|
38
38
|
* `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
|
|
39
39
|
* with later-wins semantics, so without normalization the route handler's
|
|
40
40
|
* Meta would override the slot's more-specific Meta — backwards.
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
useContext,
|
|
5
|
-
useState,
|
|
6
|
-
useEffect,
|
|
7
|
-
useRef,
|
|
8
|
-
useOptimistic,
|
|
9
|
-
startTransition,
|
|
10
|
-
} from "react";
|
|
3
|
+
import { useContext, useState, useEffect, useRef } from "react";
|
|
11
4
|
import { NavigationStoreContext } from "./context.js";
|
|
12
5
|
import { shallowEqual } from "./shallow-equal.js";
|
|
13
6
|
import type { TrackedActionState } from "../types.js";
|
|
@@ -83,14 +76,19 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
|
|
|
83
76
|
* Features:
|
|
84
77
|
* - Tracks action lifecycle: idle → loading → streaming → idle
|
|
85
78
|
* - Captures result/error locally (React handles cleanup)
|
|
86
|
-
* - If
|
|
79
|
+
* - If the same action fires several times, reports the latest call to
|
|
80
|
+
* start. An earlier call that settles later changes nothing it reports;
|
|
81
|
+
* a latest call that was aborted reports idle with nothing. One call at a
|
|
82
|
+
* time, not one per argument: a reader filtering on `payload` cannot track
|
|
83
|
+
* an earlier call once a later one starts.
|
|
87
84
|
* - Supports selector pattern like useNavigation
|
|
88
85
|
*
|
|
89
86
|
* Matching behavior:
|
|
90
87
|
* - **Function reference**: Uses full $$id for exact matching. This is precise
|
|
91
88
|
* and distinguishes between actions with the same name in different files.
|
|
92
89
|
* - **String**: Matches by suffix (action name after #). This is convenient
|
|
93
|
-
* but may be ambiguous if multiple files export the same action name
|
|
90
|
+
* but may be ambiguous if multiple files export the same action name: it
|
|
91
|
+
* follows the latest call of any action with that export name.
|
|
94
92
|
*
|
|
95
93
|
* @param action - Either a server action function or a string action name.
|
|
96
94
|
* - **Function**: Must be directly imported in the client component.
|
|
@@ -138,8 +136,10 @@ export function useAction<T>(
|
|
|
138
136
|
? getActionId(action)
|
|
139
137
|
: "";
|
|
140
138
|
|
|
141
|
-
//
|
|
142
|
-
|
|
139
|
+
// Set urgently from notifyAction's task, also while React holds a page: a
|
|
140
|
+
// transition of its own would take the held page's transition types (see
|
|
141
|
+
// useNavigation).
|
|
142
|
+
const [value, setValue] = useState<T | TrackedActionState>(() => {
|
|
143
143
|
if (!ctx) {
|
|
144
144
|
return selector ? selector(DEFAULT_ACTION_STATE) : DEFAULT_ACTION_STATE;
|
|
145
145
|
}
|
|
@@ -149,11 +149,8 @@ export function useAction<T>(
|
|
|
149
149
|
: ctx.eventController.getActionState(actionId);
|
|
150
150
|
return selector ? selector(state) : state;
|
|
151
151
|
});
|
|
152
|
-
const prevSelected = useRef(
|
|
153
|
-
prevSelected.current =
|
|
154
|
-
const [optimisticState, setOptimisticState] = useOptimistic<
|
|
155
|
-
T | TrackedActionState
|
|
156
|
-
>(null!);
|
|
152
|
+
const prevSelected = useRef(value);
|
|
153
|
+
prevSelected.current = value;
|
|
157
154
|
|
|
158
155
|
// Ref keeps the latest selector for subscription callbacks without
|
|
159
156
|
// re-subscribing on every render. Selector changes themselves are not
|
|
@@ -166,41 +163,20 @@ export function useAction<T>(
|
|
|
166
163
|
useEffect(() => {
|
|
167
164
|
if (!ctx) return;
|
|
168
165
|
|
|
166
|
+
const sync = (state: TrackedActionState) => {
|
|
167
|
+
const selected = selectorRef.current ? selectorRef.current(state) : state;
|
|
168
|
+
if (!shallowEqual(selected, prevSelected.current)) {
|
|
169
|
+
prevSelected.current = selected;
|
|
170
|
+
setValue(selected);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
169
173
|
// Sync current state for the (possibly new) actionId so that switching
|
|
170
174
|
// actions on an idle page doesn't leave stale data from the old action.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
? selectorRef.current(currentState)
|
|
174
|
-
: currentState;
|
|
175
|
-
if (!shallowEqual(currentSelected, prevSelected.current)) {
|
|
176
|
-
prevSelected.current = currentSelected;
|
|
177
|
-
setBaseState(currentSelected);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Subscribe to action-specific updates
|
|
181
|
-
const unsubscribe = ctx.eventController.subscribeToAction(
|
|
182
|
-
actionId,
|
|
183
|
-
(state) => {
|
|
184
|
-
const selectedState = selectorRef.current
|
|
185
|
-
? selectorRef.current(state)
|
|
186
|
-
: state;
|
|
187
|
-
|
|
188
|
-
if (!shallowEqual(selectedState, prevSelected.current)) {
|
|
189
|
-
prevSelected.current = selectedState;
|
|
190
|
-
setBaseState(selectedState);
|
|
191
|
-
startTransition(() => {
|
|
192
|
-
setOptimisticState(selectedState);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
return () => {
|
|
199
|
-
unsubscribe();
|
|
200
|
-
};
|
|
175
|
+
sync(ctx.eventController.getActionState(actionId));
|
|
176
|
+
return ctx.eventController.subscribeToAction(actionId, sync);
|
|
201
177
|
}, [actionId]);
|
|
202
178
|
|
|
203
|
-
return
|
|
179
|
+
return value;
|
|
204
180
|
}
|
|
205
181
|
|
|
206
182
|
export type { TrackedActionState };
|
|
@@ -53,7 +53,7 @@ export function useHandle<T, A, S>(
|
|
|
53
53
|
return selectorRef.current ? selectorRef.current(collected) : collected;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
const [value, setValue] = useCommitAnchoredState(collect);
|
|
56
|
+
const [value, setValue] = useCommitAnchoredState(collect, true);
|
|
57
57
|
|
|
58
58
|
useEffect(() => {
|
|
59
59
|
if (!ctx) return;
|
|
@@ -68,25 +68,29 @@ export function useHandle<T, A, S>(
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Route state the controller's handle notifications drive
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* priority, it renders in the page's lane, and the page's effects read it.
|
|
77
|
-
* A page in a transition (signal "anchor") does not: until it commits
|
|
71
|
+
* Route state the controller's handle notifications drive commits with the
|
|
72
|
+
* page it belongs to. NavigationProvider flushes the controller inside every
|
|
73
|
+
* payload update, with signal "anchor" for a page in a transition and
|
|
74
|
+
* "release" for an urgent one, so the page's own change is set in the page's
|
|
75
|
+
* lane and the page's effects read it. From the anchor until the page commits
|
|
78
76
|
* (releaseRouteState() from the provider's layout effect, signal "release"),
|
|
79
|
-
* a
|
|
80
|
-
* page's own commit
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
77
|
+
* a later change waits and is applied in the commit's task, right after the
|
|
78
|
+
* page's own commit: set in a lane of its own, it would commit over the page
|
|
79
|
+
* being left (#1165).
|
|
80
|
+
*
|
|
81
|
+
* `waitForRelease`, set by useHandle only: the anchor's own change waits too,
|
|
82
|
+
* so a handle value lands right after its page's commit, never in its lane. A
|
|
83
|
+
* handle value can carry a promise that suspends, and in the page's lane one
|
|
84
|
+
* that suspends in a revealed <Suspense> would hold the page. Nothing
|
|
85
|
+
* useSegments reads can suspend, so it leaves it off and its path and
|
|
86
|
+
* location commit with their page.
|
|
87
|
+
*
|
|
88
|
+
* A value shallow-equal to the last one sets nothing, so a reader renders only
|
|
89
|
+
* when its value moves.
|
|
87
90
|
*/
|
|
88
91
|
export function useCommitAnchoredState<T>(
|
|
89
92
|
init: () => T,
|
|
93
|
+
waitForRelease?: boolean,
|
|
90
94
|
): [T, (value: T, signal?: RouteStateSignal) => void] {
|
|
91
95
|
// An app that enables the React Compiler compiles the router too; its memo
|
|
92
96
|
// cache gains nothing here and cost 107 B gzip in the router chunk.
|
|
@@ -102,8 +106,8 @@ export function useCommitAnchoredState<T>(
|
|
|
102
106
|
r.last = next;
|
|
103
107
|
r.held = true;
|
|
104
108
|
}
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
if (!r.held || (r.waiting && (waitForRelease || signal !== "anchor")))
|
|
110
|
+
return;
|
|
107
111
|
r.held = false;
|
|
108
112
|
setValue(() => r.last);
|
|
109
113
|
});
|
|
@@ -5,12 +5,10 @@ import {
|
|
|
5
5
|
useContext,
|
|
6
6
|
useState,
|
|
7
7
|
useEffect,
|
|
8
|
-
useRef,
|
|
9
|
-
useOptimistic,
|
|
10
|
-
startTransition,
|
|
11
8
|
type Context,
|
|
12
9
|
} from "react";
|
|
13
10
|
import { NavigationStoreContext } from "./context.js";
|
|
11
|
+
import type { EventController, NavigationEntry } from "../event-controller.js";
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* Context for Link component to provide its destination URL
|
|
@@ -43,24 +41,45 @@ function normalizeUrl(url: string, origin: string): string {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
/**
|
|
46
|
-
*
|
|
44
|
+
* The navigation in flight a link can be pending for. One that shows no
|
|
45
|
+
* loading state (a background revalidation, an action's refetch) is none: it
|
|
46
|
+
* goes to no link and ends none whose page React still holds.
|
|
47
|
+
*/
|
|
48
|
+
function visibleNavigation(ec: EventController): NavigationEntry | null {
|
|
49
|
+
const navigation = ec.getCurrentNavigation();
|
|
50
|
+
return navigation && !navigation.options?.skipLoadingState
|
|
51
|
+
? navigation
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether `navigation` goes to `linkTo`: its URL or, after a server redirect,
|
|
57
|
+
* the URL its chain started from.
|
|
47
58
|
*/
|
|
48
59
|
function isPendingFor(
|
|
49
60
|
linkTo: string | null,
|
|
50
|
-
|
|
61
|
+
navigation: NavigationEntry | null,
|
|
51
62
|
origin: string,
|
|
52
63
|
): boolean {
|
|
53
|
-
if (linkTo === null ||
|
|
64
|
+
if (linkTo === null || !navigation) {
|
|
54
65
|
return false;
|
|
55
66
|
}
|
|
56
|
-
|
|
67
|
+
const to = normalizeUrl(linkTo, origin);
|
|
68
|
+
const from = navigation.options?._redirectFrom;
|
|
69
|
+
return (
|
|
70
|
+
normalizeUrl(navigation.url, origin) === to ||
|
|
71
|
+
(from !== undefined && normalizeUrl(from, origin) === to)
|
|
72
|
+
);
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
/**
|
|
60
76
|
* Hook to track the pending state of a Link component
|
|
61
77
|
*
|
|
62
|
-
* Must be used inside a Link component. Returns `{ pending: true }`
|
|
63
|
-
*
|
|
78
|
+
* Must be used inside a Link component. Returns `{ pending: true }` while a
|
|
79
|
+
* navigation to the link's destination is in flight, until the page it
|
|
80
|
+
* brings commits: under transition() while React holds that page, and
|
|
81
|
+
* through a server redirect until the target page commits. A newer
|
|
82
|
+
* navigation ends it at once.
|
|
64
83
|
*
|
|
65
84
|
* Useful for showing inline loading indicators on individual links.
|
|
66
85
|
*
|
|
@@ -84,63 +103,32 @@ export function useLinkStatus(): LinkStatus {
|
|
|
84
103
|
|
|
85
104
|
const origin = ctx?.origin ?? "http://localhost";
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
ctx.eventController
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// to `true` if a parent transition (e.g. the <Link> click / view transition
|
|
103
|
-
// commit) is still pending.
|
|
104
|
-
const optimisticPinnedRef = useRef(false);
|
|
105
|
-
|
|
106
|
-
const [pending, setOptimisticPending] = useOptimistic(basePending);
|
|
107
|
-
|
|
106
|
+
// The document renders no navigation in flight.
|
|
107
|
+
const [pending, setPending] = useState<boolean>(
|
|
108
|
+
() =>
|
|
109
|
+
!!ctx &&
|
|
110
|
+
!ctx.eventController.getHydrationSnapshot() &&
|
|
111
|
+
isPendingFor(linkTo, visibleNavigation(ctx.eventController), origin),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// The value commits in the lane of the notification that carries it, as
|
|
115
|
+
// useNavigation's does: at once from the click's task, and with the page
|
|
116
|
+
// when its commit clears the navigation (NavigationProvider flushes that
|
|
117
|
+
// notification inside the page's update). Set once per navigation: a
|
|
118
|
+
// repeated urgent set would commit ahead of a held page's settle. A
|
|
119
|
+
// background navigation (an action's refetch) is not a newer one, so a
|
|
120
|
+
// held link stays pending.
|
|
108
121
|
useEffect(() => {
|
|
109
122
|
if (!ctx || linkTo === null) {
|
|
110
123
|
return;
|
|
111
124
|
}
|
|
112
125
|
|
|
126
|
+
let last: NavigationEntry | null | undefined;
|
|
113
127
|
const update = () => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
prevPending.current = isPending;
|
|
119
|
-
|
|
120
|
-
const shouldPin = isPending && state.state !== "idle";
|
|
121
|
-
|
|
122
|
-
if (shouldPin) {
|
|
123
|
-
// Pin the optimistic value so the spinner shows immediately even if
|
|
124
|
-
// a parent transition (e.g. <Link> click) defers the urgent
|
|
125
|
-
// setBasePending commit.
|
|
126
|
-
startTransition(() => {
|
|
127
|
-
setOptimisticPending(isPending);
|
|
128
|
-
});
|
|
129
|
-
optimisticPinnedRef.current = true;
|
|
130
|
-
} else if (optimisticPinnedRef.current) {
|
|
131
|
-
// Release a previously-pinned optimistic value. Without this,
|
|
132
|
-
// useOptimistic keeps returning the stale `true` while any parent
|
|
133
|
-
// transition is still pending, even after basePending flipped to
|
|
134
|
-
// false at navigation completion — leaving the link spinner stuck.
|
|
135
|
-
startTransition(() => {
|
|
136
|
-
setOptimisticPending(isPending);
|
|
137
|
-
});
|
|
138
|
-
optimisticPinnedRef.current = false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Always update base state
|
|
142
|
-
setBasePending(isPending);
|
|
143
|
-
}
|
|
128
|
+
const navigation = visibleNavigation(ctx.eventController);
|
|
129
|
+
if (navigation === last) return;
|
|
130
|
+
last = navigation;
|
|
131
|
+
setPending(isPendingFor(linkTo, navigation, origin));
|
|
144
132
|
};
|
|
145
133
|
|
|
146
134
|
// Catch-up: re-read state synchronously on mount before subscribing, so a
|
|
@@ -5,13 +5,15 @@ import {
|
|
|
5
5
|
useState,
|
|
6
6
|
useEffect,
|
|
7
7
|
useLayoutEffect,
|
|
8
|
-
startTransition,
|
|
9
8
|
useRef,
|
|
10
9
|
} from "react";
|
|
11
10
|
import { NavigationStoreContext } from "./context.js";
|
|
12
11
|
import { shallowEqual } from "./shallow-equal.js";
|
|
13
12
|
import type { PublicNavigationState } from "../types.js";
|
|
14
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
DerivedNavigationState,
|
|
15
|
+
RouteStateSignal,
|
|
16
|
+
} from "../event-controller.js";
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Convert derived state to public version (strips inflightActions)
|
|
@@ -21,6 +23,32 @@ function toPublicState(state: DerivedNavigationState): PublicNavigationState {
|
|
|
21
23
|
return publicState;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/** The state a value was selected from, and the value. */
|
|
27
|
+
interface Snapshot<T> {
|
|
28
|
+
state: PublicNavigationState;
|
|
29
|
+
value: T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A notification while React holds a page: work that starts shows at once
|
|
34
|
+
* (loading, streaming, a newer click's URL), what settles waits for the
|
|
35
|
+
* page's commit (rango-d152), and location moves only with the notification
|
|
36
|
+
* that moves it (the page on screen until the next one shows).
|
|
37
|
+
*/
|
|
38
|
+
function hold(
|
|
39
|
+
base: PublicNavigationState,
|
|
40
|
+
live: PublicNavigationState,
|
|
41
|
+
moved: boolean,
|
|
42
|
+
): PublicNavigationState {
|
|
43
|
+
return {
|
|
44
|
+
state: live.state === "loading" ? live.state : base.state,
|
|
45
|
+
isStreaming: live.isStreaming || base.isStreaming,
|
|
46
|
+
isNavigating: live.isNavigating || base.isNavigating,
|
|
47
|
+
pendingUrl: live.pendingUrl ?? base.pendingUrl,
|
|
48
|
+
location: moved ? live.location : base.location,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
24
52
|
/**
|
|
25
53
|
* Hook to access reactive navigation state with optional selector for performance.
|
|
26
54
|
*
|
|
@@ -46,18 +74,20 @@ export function useNavigation<T>(
|
|
|
46
74
|
throw new Error("useNavigation must be used within NavigationProvider");
|
|
47
75
|
}
|
|
48
76
|
|
|
49
|
-
const [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
ctx.eventController.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
const [snapshot, setSnapshot] = useState<Snapshot<T | PublicNavigationState>>(
|
|
78
|
+
() => {
|
|
79
|
+
const state = toPublicState(
|
|
80
|
+
ctx.eventController.getHydrationSnapshot()?.state ??
|
|
81
|
+
ctx.eventController.getState(),
|
|
82
|
+
);
|
|
83
|
+
return { state, value: selector ? selector(state) : state };
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
// What React ends on once every set so far commits, and what it committed.
|
|
87
|
+
const latest = useRef(snapshot);
|
|
88
|
+
const committed = useRef(snapshot);
|
|
59
89
|
useLayoutEffect(() => {
|
|
60
|
-
|
|
90
|
+
committed.current = snapshot;
|
|
61
91
|
});
|
|
62
92
|
|
|
63
93
|
// Store selector in a ref so the subscription callback always uses the
|
|
@@ -74,34 +104,48 @@ export function useNavigation<T>(
|
|
|
74
104
|
// the location hooks' values do: NavigationProvider flushes a navigation's
|
|
75
105
|
// notification into the transition that commits its tree, so idle commits
|
|
76
106
|
// with the destination's location (useSearchParams, usePathname,
|
|
77
|
-
// useParams).
|
|
78
|
-
//
|
|
107
|
+
// useParams). From that page's anchor to its release every other
|
|
108
|
+
// notification goes through hold(), urgently, so React rebases the same
|
|
109
|
+
// update onto the page when it commits. Never a transition of its own.
|
|
79
110
|
useEffect(() => {
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
let pageHeld = false;
|
|
112
|
+
let location: PublicNavigationState["location"] | undefined;
|
|
113
|
+
const select = (state: PublicNavigationState) =>
|
|
114
|
+
selectorRef.current ? selectorRef.current(state) : state;
|
|
115
|
+
const update = (signal?: RouteStateSignal) => {
|
|
116
|
+
if (signal) pageHeld = signal === "anchor";
|
|
117
|
+
const live = toPublicState(ctx.eventController.getState());
|
|
118
|
+
const moved = live.location !== location;
|
|
119
|
+
location = live.location;
|
|
120
|
+
const held = pageHeld && !signal;
|
|
121
|
+
const next = (
|
|
122
|
+
base: Snapshot<T | PublicNavigationState>,
|
|
123
|
+
): Snapshot<T | PublicNavigationState> => {
|
|
124
|
+
const state = held ? hold(base.state, live, moved) : live;
|
|
125
|
+
return { state, value: select(state) };
|
|
126
|
+
};
|
|
127
|
+
const target = next(latest.current);
|
|
128
|
+
// A release is compared with the state React renders it on, not
|
|
129
|
+
// `latest`: an urgent page can end the hold before React commits the
|
|
130
|
+
// anchor's set. An anchor sets inside its page's transition, which
|
|
131
|
+
// renders every set so far, so `latest` is that state. A release that
|
|
132
|
+
// finds every set committed with its value sets nothing: the updater
|
|
133
|
+
// would render the reader again only to bail out.
|
|
134
|
+
if (signal === "release") {
|
|
135
|
+
if (
|
|
136
|
+
latest.current === committed.current &&
|
|
137
|
+
shallowEqual(target.value, latest.current.value)
|
|
138
|
+
)
|
|
139
|
+
return;
|
|
140
|
+
latest.current = target;
|
|
141
|
+
setSnapshot((prev) =>
|
|
142
|
+
shallowEqual(prev.value, target.value) ? prev : target,
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
104
145
|
}
|
|
146
|
+
if (shallowEqual(target.value, latest.current.value)) return;
|
|
147
|
+
latest.current = target;
|
|
148
|
+
setSnapshot(held ? next : target);
|
|
105
149
|
};
|
|
106
150
|
|
|
107
151
|
// Catch-up: re-read state synchronously on mount before subscribing, so a
|
|
@@ -110,8 +154,15 @@ export function useNavigation<T>(
|
|
|
110
154
|
// useSearchParams.
|
|
111
155
|
update();
|
|
112
156
|
|
|
113
|
-
|
|
157
|
+
const unsubscribe = ctx.eventController.subscribe(update);
|
|
158
|
+
// The anchor and release signals ride the handle channel. One without a
|
|
159
|
+
// signal re-reads the same state, as the state channel's own would.
|
|
160
|
+
const unsubscribeSignals = ctx.eventController.subscribeToHandles(update);
|
|
161
|
+
return () => {
|
|
162
|
+
unsubscribe();
|
|
163
|
+
unsubscribeSignals();
|
|
164
|
+
};
|
|
114
165
|
}, []);
|
|
115
166
|
|
|
116
|
-
return value as T | PublicNavigationState;
|
|
167
|
+
return snapshot.value as T | PublicNavigationState;
|
|
117
168
|
}
|
|
@@ -614,6 +614,7 @@ export async function initBrowserApp(
|
|
|
614
614
|
|
|
615
615
|
const historyKey = generateHistoryKey(window.location.href, {
|
|
616
616
|
intercept: responseIsIntercept,
|
|
617
|
+
interceptSource: interceptSourceUrl ?? undefined,
|
|
617
618
|
});
|
|
618
619
|
store.setHistoryKey(historyKey);
|
|
619
620
|
const currentHandleData = liveHandleDataForCache(
|