@rangojs/router 0.28.0 → 0.30.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/browser/action-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +6 -1
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/use-action.d.ts +10 -4
- package/dist/types/browser/react/use-handle.d.ts +20 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/react/use-route-state.d.ts +45 -0
- package/dist/types/browser/segment-reconciler.d.ts +26 -0
- package/dist/types/browser/types.d.ts +30 -8
- package/dist/types/browser/wire-protocol.d.ts +1 -0
- package/dist/types/cache/tag-invalidation.d.ts +0 -10
- package/dist/types/router/error-handling.d.ts +7 -1
- package/dist/types/router/match-context.d.ts +2 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +3 -2
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +2 -0
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/server/request-context.d.ts +30 -1
- 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/error-types.d.ts +27 -8
- package/dist/types/wire-params.d.ts +5 -0
- package/dist/vite/index.js +62 -67
- package/package.json +1 -1
- package/skills/client-urls/SKILL.md +6 -8
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +40 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +17 -5
- package/skills/intercept/SKILL.md +6 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/server-actions/SKILL.md +8 -1
- package/skills/testing/client-components.md +25 -18
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/event-controller.ts +78 -19
- package/src/browser/navigation-bridge.ts +45 -13
- package/src/browser/navigation-client.ts +2 -0
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- package/src/browser/react/use-action.ts +27 -50
- package/src/browser/react/use-handle.ts +27 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +76 -51
- package/src/browser/react/use-params.ts +6 -35
- package/src/browser/react/use-pathname.ts +3 -32
- package/src/browser/react/use-route-state.ts +141 -0
- package/src/browser/react/use-search-params.ts +7 -40
- package/src/browser/segment-reconciler.ts +43 -0
- package/src/browser/server-action-bridge.ts +35 -8
- package/src/browser/types.ts +31 -8
- package/src/browser/wire-protocol.ts +2 -0
- package/src/cache/cache-runtime.ts +28 -31
- package/src/cache/document-cache.ts +34 -37
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/tag-invalidation.ts +3 -18
- package/src/router/error-handling.ts +18 -6
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/match-api.ts +1 -0
- package/src/router/match-context.ts +3 -0
- package/src/router/match-middleware/background-revalidation.ts +5 -4
- package/src/router/match-middleware/cache-lookup.ts +5 -3
- package/src/router/navigation-snapshot.ts +10 -0
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +4 -3
- package/src/router/segment-resolution/resolution-pass.ts +2 -0
- package/src/router/segment-resolution/revalidation.ts +57 -28
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- package/src/router/types.ts +1 -1
- package/src/router.ts +7 -4
- package/src/rsc/request-stages.ts +4 -2
- package/src/rsc/shell-capture.ts +63 -58
- package/src/server/request-context.ts +49 -0
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/render-route.tsx +204 -16
- package/src/testing/serve-shell-request.ts +19 -10
- package/src/types/error-types.ts +27 -8
- package/src/wire-params.ts +5 -0
|
@@ -833,6 +833,13 @@ export interface RequestContext<
|
|
|
833
833
|
/** @internal Per-request error dedup set for onError reporting */
|
|
834
834
|
_reportedErrors: WeakSet<object>;
|
|
835
835
|
|
|
836
|
+
/**
|
|
837
|
+
* @internal The segment a parallel slot's or orphan layout's error came
|
|
838
|
+
* from, for the owning entry's catch (segment-resolution/helpers.ts
|
|
839
|
+
* noteThrower). Per request: an error object can be thrown by several.
|
|
840
|
+
*/
|
|
841
|
+
_throwers?: WeakMap<object, { id: string; type: string }>;
|
|
842
|
+
|
|
836
843
|
/**
|
|
837
844
|
* @internal The errors this request printed to the server console, so a
|
|
838
845
|
* failure prints once where it originated (server/print-server-error.ts). A derived context shares it through the prototype.
|
|
@@ -1005,6 +1012,7 @@ export type PublicRequestContext<
|
|
|
1005
1012
|
| "_inActionRevalidation"
|
|
1006
1013
|
| "_actionRan"
|
|
1007
1014
|
| "_reportedErrors"
|
|
1015
|
+
| "_throwers"
|
|
1008
1016
|
| "_printedErrors"
|
|
1009
1017
|
| "_renderErrors"
|
|
1010
1018
|
| "_recoveredHandlerErrors"
|
|
@@ -1140,6 +1148,46 @@ export function runWithRequestContext<TEnv, T>(
|
|
|
1140
1148
|
return requestContextStorage.run(context, fn);
|
|
1141
1149
|
}
|
|
1142
1150
|
|
|
1151
|
+
/** Inside background work: the errors its onError reports named. */
|
|
1152
|
+
const backgroundWork = new AsyncLocalStorage<WeakSet<object>>();
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Run work whose output goes only to a cache, never to the response of the
|
|
1156
|
+
* request that started it, on `context`: a stale entry's refresh (a "use
|
|
1157
|
+
* cache" function, a cached loader, a cache() record, a document), a
|
|
1158
|
+
* navigation's proactive cache() write, a PPR shell capture. No visitor made
|
|
1159
|
+
* the invalidations in it, so they set no fresh-reads cookie
|
|
1160
|
+
* (cache/tag-invalidation.ts), and onError marks every error reported inside
|
|
1161
|
+
* it `background` (router/error-handling.ts invokeOnError). Its failure is
|
|
1162
|
+
* reported inside it too: a catch outside runs on the visitor's frame.
|
|
1163
|
+
*/
|
|
1164
|
+
export function runAsBackgroundWork<TEnv, T>(
|
|
1165
|
+
context: RequestContext<TEnv>,
|
|
1166
|
+
fn: () => T,
|
|
1167
|
+
): T {
|
|
1168
|
+
const reported = backgroundWork.getStore() ?? new WeakSet<object>();
|
|
1169
|
+
return requestContextStorage.run(context, () =>
|
|
1170
|
+
backgroundWork.run(reported, fn),
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Whether the caller runs inside runAsBackgroundWork. */
|
|
1175
|
+
export function isBackgroundWork(): boolean {
|
|
1176
|
+
return backgroundWork.getStore() !== undefined;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* The set onError's dedupe records a reported error in. Background work keeps
|
|
1181
|
+
* its own: an error object both the visitor's render and background work
|
|
1182
|
+
* reach (a memoized rejection, a thrown constant) is reported by each, so a
|
|
1183
|
+
* background report never takes the place of the visitor's.
|
|
1184
|
+
*/
|
|
1185
|
+
export function reportedErrorsOf(
|
|
1186
|
+
context: RequestContext<any>,
|
|
1187
|
+
): WeakSet<object> {
|
|
1188
|
+
return backgroundWork.getStore() ?? context._reportedErrors;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1143
1191
|
/**
|
|
1144
1192
|
* Get the current request context
|
|
1145
1193
|
* Throws if called outside of a request context
|
|
@@ -1745,6 +1793,7 @@ export function createRequestContext<TEnv>(
|
|
|
1745
1793
|
_locationState: undefined,
|
|
1746
1794
|
|
|
1747
1795
|
_reportedErrors: new WeakSet<object>(),
|
|
1796
|
+
_throwers: undefined,
|
|
1748
1797
|
_printedErrors: new WeakSet<object>(),
|
|
1749
1798
|
_renderErrors: [],
|
|
1750
1799
|
_metricsStore: undefined,
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -345,6 +345,9 @@ export async function dispatch<TEnv = any>(
|
|
|
345
345
|
cacheProfiles: router.cacheProfiles,
|
|
346
346
|
versions,
|
|
347
347
|
});
|
|
348
|
+
// The request's start, as the production handler sets it: onError's
|
|
349
|
+
// `duration` counts from here.
|
|
350
|
+
requestContext._handlerStart = performance.now();
|
|
348
351
|
// Cache degradation (reportCacheError -> _reportBackgroundError) reaches the
|
|
349
352
|
// router's onError, as in the production handler.
|
|
350
353
|
stages.reportBackgroundErrors(requestContext, req, url, ctx.callOnError);
|
|
@@ -26,24 +26,30 @@
|
|
|
26
26
|
* - navigate() drives the navigation lifecycle as a click does:
|
|
27
27
|
* useNavigation() reads loading from the call and idle with the commit,
|
|
28
28
|
* and useLinkStatus().pending is true for a link to the destination until
|
|
29
|
-
* the commit
|
|
29
|
+
* the commit, which React holds with it. A `transition` spec plus a pending
|
|
30
30
|
* Promise seeded via navigate(url, { loaders }) commits through
|
|
31
31
|
* production's commitInTransition (browser/partial-update.ts), which React
|
|
32
32
|
* holds: useNavigation() stays loading and the held reader's
|
|
33
33
|
* useLoader().isLoading reads true until the data lands. Without
|
|
34
34
|
* `transition` the commit is urgent (production's same-structure hold is
|
|
35
|
-
* not modeled).
|
|
35
|
+
* not modeled). navigate(url, { streamUntil }) gives the navigation a
|
|
36
|
+
* response stream that stays open past the commit (isStreaming).
|
|
37
|
+
* useAction().state stays idle: no action runs.
|
|
36
38
|
* - A spec's transition({ when }) decides every commit with production's
|
|
37
39
|
* browser decision: navigate() and useRouter().push model kind "push",
|
|
38
|
-
* useRouter().replace kind "replace", refresh() kind "revalidate"
|
|
39
|
-
*
|
|
40
|
-
* e2e territory.
|
|
40
|
+
* useRouter().replace kind "replace", refresh() kind "revalidate", and a
|
|
41
|
+
* back/forward that restores a page kind "pop". Action and optimistic
|
|
42
|
+
* clientUrls() swap decisions are e2e territory.
|
|
41
43
|
* - Location state reaches readers the way it does in production:
|
|
42
44
|
* NavigationProvider holds the committed entry's state next to the
|
|
43
45
|
* payload, so a navigation whose loader the test holds keeps the entry
|
|
44
46
|
* being left on screen with its own state. A `popstate` event is a
|
|
45
|
-
* back/forward onto the entry history.state holds
|
|
46
|
-
*
|
|
47
|
+
* back/forward onto the entry history.state holds. When it is the
|
|
48
|
+
* mounted entry or one a navigate() wrote, history still holds the state
|
|
49
|
+
* written there, and another page is on screen, its page is restored
|
|
50
|
+
* from the history cache, with the handle values it had, as production's
|
|
51
|
+
* restore is; otherwise the tree keeps its location. A stale entry's
|
|
52
|
+
* revalidation, a restore that fetches, a server action's state, a
|
|
47
53
|
* redirect's and an intercept's are e2e territory.
|
|
48
54
|
* What it DOES cover: client hooks that read NavigationProvider /
|
|
49
55
|
* OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
|
|
@@ -61,6 +67,7 @@
|
|
|
61
67
|
import { decideTransitionGatedOff } from "../browser/transition-when.js";
|
|
62
68
|
import {
|
|
63
69
|
StrictMode,
|
|
70
|
+
startTransition,
|
|
64
71
|
useEffect,
|
|
65
72
|
type ReactNode,
|
|
66
73
|
type ComponentType,
|
|
@@ -78,10 +85,12 @@ import {
|
|
|
78
85
|
shouldStartViewTransition,
|
|
79
86
|
} from "../browser/partial-update.js";
|
|
80
87
|
import type {
|
|
88
|
+
HandleData,
|
|
81
89
|
NavigateOptions,
|
|
82
90
|
NavigateOptionsInternal,
|
|
83
91
|
NavigationStore,
|
|
84
92
|
NavigationBridge,
|
|
93
|
+
NavigationUpdate,
|
|
85
94
|
UpdateSubscriber,
|
|
86
95
|
} from "../browser/types.js";
|
|
87
96
|
import type {
|
|
@@ -101,6 +110,7 @@ import type {
|
|
|
101
110
|
TransitionWhenKind,
|
|
102
111
|
} from "../types.js";
|
|
103
112
|
import {
|
|
113
|
+
setCurrentHistoryStateListener,
|
|
104
114
|
setLocationStateVersion,
|
|
105
115
|
type LocationStateDefinition,
|
|
106
116
|
} from "../browser/react/location-state-shared.js";
|
|
@@ -447,11 +457,40 @@ export interface TestRouterHandle {
|
|
|
447
457
|
* `handles`). They arrive on the response's handle stream, so `useHandle`
|
|
448
458
|
* readers take them with the page's commit: while React holds the page,
|
|
449
459
|
* readers keep the values of the page being left.
|
|
460
|
+
*
|
|
461
|
+
* `options.streamUntil` gives the navigation a response stream, as a real
|
|
462
|
+
* response has: it opens in a task after the call, before the commit (the
|
|
463
|
+
* response arrives, and `useNavigation().pendingUrl` clears), and stays open
|
|
464
|
+
* until the promise settles (a page that streams a tail past its commit:
|
|
465
|
+
* `isStreaming` reads true until then). Without it the navigation has no
|
|
466
|
+
* stream. A navigation that a newer one or a back/forward aborts before its
|
|
467
|
+
* response arrives opens none and commits nothing; one aborted after its
|
|
468
|
+
* response arrived stops counting as streaming at the abort, as in
|
|
469
|
+
* production, where its response streams on and is ignored.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* const tail = deferred();
|
|
473
|
+
* await router.navigate("/products/2", { streamUntil: tail.promise });
|
|
474
|
+
* // useNavigation(): idle, isStreaming true
|
|
475
|
+
* await act(async () => tail.resolve());
|
|
476
|
+
* // useNavigation(): idle, isStreaming false
|
|
477
|
+
*
|
|
478
|
+
* A navigation started while React holds a page shows its page once the
|
|
479
|
+
* call resolves, with or without act(). A `<Link>` click over a held page
|
|
480
|
+
* still goes inside `await act(async () => ...)` (docs/testing.md).
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* await router.navigate("/products/2", { loaders: [[ProductLoader, next]] });
|
|
484
|
+
* // React holds /products/2
|
|
485
|
+
* await router.navigate("/products/3", { transition: false });
|
|
486
|
+
* // /products/3 is on screen
|
|
450
487
|
*/
|
|
451
488
|
navigate(
|
|
452
489
|
url: string,
|
|
453
490
|
options?: Pick<RenderRouteOptions, "loaders" | "handles"> &
|
|
454
|
-
Pick<NavigateOptions, "state" | "replace" | "transition"
|
|
491
|
+
Pick<NavigateOptions, "state" | "replace" | "transition"> & {
|
|
492
|
+
streamUntil?: Promise<unknown>;
|
|
493
|
+
},
|
|
455
494
|
): Promise<void>;
|
|
456
495
|
/**
|
|
457
496
|
* Re-render the current location, as router.refresh() does: a spec's
|
|
@@ -821,7 +860,49 @@ export async function renderRoute(
|
|
|
821
860
|
history?: Pick<NavigateOptionsInternal, "state" | "replace" | "transition">;
|
|
822
861
|
kind?: TransitionWhenKind;
|
|
823
862
|
transition?: boolean;
|
|
863
|
+
streamUntil?: Promise<unknown>;
|
|
824
864
|
};
|
|
865
|
+
|
|
866
|
+
// The page each history entry shows, by the entry's `idx` stamp (the
|
|
867
|
+
// seeded entry has none: 0), so a back/forward can restore it as
|
|
868
|
+
// production restores a history-cache entry. `state` is the entry's
|
|
869
|
+
// history state as the router last wrote it: an entry the test wrote over
|
|
870
|
+
// it (no `idx` reads as 0) is not that page. `handleData` is the page's
|
|
871
|
+
// pushes, taken when it is left.
|
|
872
|
+
interface EntryPage {
|
|
873
|
+
url: URL;
|
|
874
|
+
segments: ResolvedSegment[];
|
|
875
|
+
params: Record<string, string>;
|
|
876
|
+
state: unknown;
|
|
877
|
+
handleData?: HandleData;
|
|
878
|
+
}
|
|
879
|
+
const entries = new Map<number, EntryPage>();
|
|
880
|
+
const entryIndex = (): number => {
|
|
881
|
+
const idx = (window.history.state as { idx?: unknown } | null)?.idx;
|
|
882
|
+
return typeof idx === "number" ? idx : 0;
|
|
883
|
+
};
|
|
884
|
+
let entryOnScreen = entryIndex();
|
|
885
|
+
entries.set(entryOnScreen, {
|
|
886
|
+
url,
|
|
887
|
+
segments: initialSegments,
|
|
888
|
+
params: initialMatch.params,
|
|
889
|
+
state: window.history.state,
|
|
890
|
+
});
|
|
891
|
+
// A write the router makes to the entry history is on (a flash read,
|
|
892
|
+
// Def.write() or delete()) stays that entry's state, as production's
|
|
893
|
+
// per-entry memory follows it (navigation-store.ts). A test's own
|
|
894
|
+
// history.replaceState() does not.
|
|
895
|
+
setCurrentHistoryStateListener(() => {
|
|
896
|
+
store.rememberDisplayedEntry();
|
|
897
|
+
const written = entries.get(entryIndex());
|
|
898
|
+
if (written) written.state = window.history.state;
|
|
899
|
+
});
|
|
900
|
+
/** The page being left keeps its pushes for a back/forward onto it. */
|
|
901
|
+
const leaveEntry = (): void => {
|
|
902
|
+
const left = entries.get(entryOnScreen);
|
|
903
|
+
if (left) left.handleData = eventController.getHandleState().data;
|
|
904
|
+
};
|
|
905
|
+
let popGeneration = 0;
|
|
825
906
|
// The navigation's lifecycle, as navigation-transaction.ts drives it:
|
|
826
907
|
// useNavigation() reads loading from the call, delivered before the commit
|
|
827
908
|
// as a click's notification goes out while its response is in flight, and
|
|
@@ -833,10 +914,35 @@ export async function renderRoute(
|
|
|
833
914
|
const handle = eventController.startNavigation(
|
|
834
915
|
new URL(target, TEST_ORIGIN).href,
|
|
835
916
|
);
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
act(
|
|
917
|
+
// Runs in the caller's task, so a <Link> click's own act() takes it.
|
|
918
|
+
// Awaited: React retries a held page with this update, and an unawaited
|
|
919
|
+
// act() drops that suspended retry for good.
|
|
920
|
+
await act(() => eventController.flushRouteState());
|
|
839
921
|
try {
|
|
922
|
+
const streamUntil = navOptions?.streamUntil;
|
|
923
|
+
if (streamUntil) {
|
|
924
|
+
// The response arrives a task after the request, ahead of the
|
|
925
|
+
// commit (navigation-transaction.ts: the stream opens with it).
|
|
926
|
+
await act(async () => {
|
|
927
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
928
|
+
});
|
|
929
|
+
if (handle.signal.aborted) return;
|
|
930
|
+
const stream = handle.startStreaming();
|
|
931
|
+
await act(() => eventController.flushRouteState());
|
|
932
|
+
// A navigation that is left stops counting as streaming at the abort
|
|
933
|
+
// (partial-update.ts endStream); the abort's notification goes out
|
|
934
|
+
// with the newer navigation's.
|
|
935
|
+
handle.signal.addEventListener("abort", () => stream.end(), {
|
|
936
|
+
once: true,
|
|
937
|
+
});
|
|
938
|
+
const end = async (): Promise<void> => {
|
|
939
|
+
await act(() => {
|
|
940
|
+
stream.end();
|
|
941
|
+
eventController.flushRouteState();
|
|
942
|
+
});
|
|
943
|
+
};
|
|
944
|
+
void streamUntil.then(end, end);
|
|
945
|
+
}
|
|
840
946
|
await commitNavigation(handle, target, navOptions);
|
|
841
947
|
} finally {
|
|
842
948
|
// Back to idle when the commit never came (a render that threw).
|
|
@@ -922,6 +1028,7 @@ export async function renderRoute(
|
|
|
922
1028
|
// A newer navigate() started meanwhile: this one commits nothing, as
|
|
923
1029
|
// production's (browser/partial-update.ts) returns on its aborted signal.
|
|
924
1030
|
if (handle.signal.aborted) return;
|
|
1031
|
+
leaveEntry();
|
|
925
1032
|
handle.complete(nextUrl);
|
|
926
1033
|
eventController.setParams(match.params);
|
|
927
1034
|
store.setCurrentUrl(nextUrl.href);
|
|
@@ -934,6 +1041,13 @@ export async function renderRoute(
|
|
|
934
1041
|
);
|
|
935
1042
|
eventController.commitLocationState(historyState);
|
|
936
1043
|
}
|
|
1044
|
+
entryOnScreen = entryIndex();
|
|
1045
|
+
entries.set(entryOnScreen, {
|
|
1046
|
+
url: nextUrl,
|
|
1047
|
+
segments,
|
|
1048
|
+
params: match.params,
|
|
1049
|
+
state: window.history.state,
|
|
1050
|
+
});
|
|
937
1051
|
const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
|
|
938
1052
|
await act(async () => {
|
|
939
1053
|
// As production's emitters name their cause (suspense-audit.ts I6).
|
|
@@ -961,17 +1075,90 @@ export async function renderRoute(
|
|
|
961
1075
|
return navigate(current.pathname + current.search, { kind: "revalidate" });
|
|
962
1076
|
};
|
|
963
1077
|
|
|
1078
|
+
// A back/forward onto the mounted entry or one a navigation wrote, whose
|
|
1079
|
+
// page is not the one on screen: its page is restored from the entry, as production's
|
|
1080
|
+
// history-cache restore does (navigation-bridge.ts handlePopstate). Not
|
|
1081
|
+
// modeled: a stale entry's revalidation and a restore that has to fetch.
|
|
1082
|
+
const restoreEntry = async (index: number, page: EntryPage) => {
|
|
1083
|
+
const generation = ++popGeneration;
|
|
1084
|
+
eventController.abortNavigation();
|
|
1085
|
+
const historyState = window.history.state;
|
|
1086
|
+
// transition({ when }) decides the restore (kind "pop") against the
|
|
1087
|
+
// entry being left.
|
|
1088
|
+
const gatedOff = decideTransitionGatedOff(page.segments, () => ({
|
|
1089
|
+
kind: "pop",
|
|
1090
|
+
from: {
|
|
1091
|
+
url: eventController.getLocation().href,
|
|
1092
|
+
params: eventController.getParams(),
|
|
1093
|
+
routeName: leaf.name,
|
|
1094
|
+
state: entries.get(entryOnScreen)?.state ?? null,
|
|
1095
|
+
},
|
|
1096
|
+
to: {
|
|
1097
|
+
url: page.url,
|
|
1098
|
+
params: page.params,
|
|
1099
|
+
routeName: leaf.name,
|
|
1100
|
+
state: historyState,
|
|
1101
|
+
},
|
|
1102
|
+
}));
|
|
1103
|
+
const root = await renderSegments(page.segments, {
|
|
1104
|
+
forceAwait: true,
|
|
1105
|
+
outletPending: options.outletPending,
|
|
1106
|
+
transitionGatedOff: gatedOff,
|
|
1107
|
+
});
|
|
1108
|
+
// A later back/forward owns the screen.
|
|
1109
|
+
if (generation !== popGeneration) return;
|
|
1110
|
+
leaveEntry();
|
|
1111
|
+
entryOnScreen = index;
|
|
1112
|
+
store.setCurrentUrl(page.url.href);
|
|
1113
|
+
store.setSegmentIds(page.segments.map((s) => s.id));
|
|
1114
|
+
const update: NavigationUpdate = {
|
|
1115
|
+
root,
|
|
1116
|
+
metadata: {
|
|
1117
|
+
...makeMetadata(page.url.pathname, page.segments, page.params),
|
|
1118
|
+
isPartial: true,
|
|
1119
|
+
cachedHandleData: page.handleData,
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
await act(async () => {
|
|
1123
|
+
// The restored entry's location and params, set where its tree is
|
|
1124
|
+
// handed over, so the hooks take them in that batch.
|
|
1125
|
+
eventController.setLocation(page.url);
|
|
1126
|
+
eventController.setParams(page.params);
|
|
1127
|
+
eventController.commitLocationState(historyState);
|
|
1128
|
+
auditTreeCause("popstate");
|
|
1129
|
+
if (!shouldStartViewTransition(page.segments, gatedOff)) {
|
|
1130
|
+
store.emitUpdate(update);
|
|
1131
|
+
} else {
|
|
1132
|
+
startTransition(() => {
|
|
1133
|
+
store.emitUpdate({ ...update, inTransition: true });
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
};
|
|
1138
|
+
|
|
964
1139
|
let prefetchRoot: HTMLElement | undefined;
|
|
965
1140
|
const bridge: NavigationBridge = {
|
|
966
1141
|
navigate: (target, navigateOptions) =>
|
|
967
1142
|
navigate(target, { history: navigateOptions ?? {} }),
|
|
968
1143
|
refresh: () => refresh(),
|
|
969
|
-
// Back/forward onto the entry history.state holds.
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
//
|
|
973
|
-
//
|
|
1144
|
+
// Back/forward onto the entry history.state holds. Its page is restored
|
|
1145
|
+
// when the mount or a navigation wrote the entry, history still holds that
|
|
1146
|
+
// state and another page is on screen (restoreEntry). Otherwise the tree
|
|
1147
|
+
// keeps its location: a treeless commit of that entry, as production's
|
|
1148
|
+
// traversal that keeps every segment (navigation-transaction.ts commit).
|
|
1149
|
+
// Flushed, not left to the notify timer: the dispatching test's act()
|
|
1150
|
+
// has returned by then.
|
|
974
1151
|
handlePopstate: async () => {
|
|
1152
|
+
const index = entryIndex();
|
|
1153
|
+
const page = entries.get(index);
|
|
1154
|
+
if (
|
|
1155
|
+
page &&
|
|
1156
|
+
index !== entryOnScreen &&
|
|
1157
|
+
page.state === window.history.state
|
|
1158
|
+
) {
|
|
1159
|
+
await restoreEntry(index, page);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
975
1162
|
eventController.commitLocationState(window.history.state, true);
|
|
976
1163
|
eventController.flushRouteState();
|
|
977
1164
|
},
|
|
@@ -1091,6 +1278,7 @@ export async function renderRoute(
|
|
|
1091
1278
|
loaders: navOptions?.loaders,
|
|
1092
1279
|
handles: navOptions?.handles,
|
|
1093
1280
|
transition: navOptions?.transition,
|
|
1281
|
+
streamUntil: navOptions?.streamUntil,
|
|
1094
1282
|
history:
|
|
1095
1283
|
navOptions?.state !== undefined || navOptions?.replace
|
|
1096
1284
|
? navOptions
|
|
@@ -76,7 +76,6 @@ import {
|
|
|
76
76
|
type PprReplayStatus,
|
|
77
77
|
type ShellStatus,
|
|
78
78
|
} from "./shell-status.js";
|
|
79
|
-
import { WIRE } from "../wire-params.js";
|
|
80
79
|
import { writePartialParams } from "../browser/wire-protocol.js";
|
|
81
80
|
|
|
82
81
|
/** Options for {@link serveShellRequest}. */
|
|
@@ -116,7 +115,13 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
116
115
|
* and segments take an action's revalidation defaults; it renders live, as
|
|
117
116
|
* an action does, so neither the route cache nor the document cache answers
|
|
118
117
|
* or stores it, and a `ppr` route's `replayStatus` is
|
|
119
|
-
* `{ outcome: "BYPASS", reason: "stale" }`.
|
|
118
|
+
* `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error
|
|
119
|
+
* segment of a thrown action's boundary the page at `from` shows
|
|
120
|
+
* (`_rsc_boundary`), as the follow-up an earlier action owes that page
|
|
121
|
+
* sends it, with `stale: true` and no `segments`: the response renders
|
|
122
|
+
* every segment above the boundary and nothing at or below it, claims the
|
|
123
|
+
* boundary in `matched` and sends no segment for it. The server reads it
|
|
124
|
+
* only beside `stale`.
|
|
120
125
|
*/
|
|
121
126
|
partial?:
|
|
122
127
|
| true
|
|
@@ -128,6 +133,7 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
128
133
|
fill?: boolean;
|
|
129
134
|
interceptSource?: string;
|
|
130
135
|
stale?: boolean;
|
|
136
|
+
boundary?: string;
|
|
131
137
|
};
|
|
132
138
|
/**
|
|
133
139
|
* POST this form as a submit before hydration: the no-JS request the
|
|
@@ -627,20 +633,23 @@ async function buildRequest(
|
|
|
627
633
|
fill = false,
|
|
628
634
|
interceptSource,
|
|
629
635
|
stale = false,
|
|
636
|
+
boundary,
|
|
630
637
|
} = partial === true ? {} : partial;
|
|
631
638
|
if (stale && prefetch) {
|
|
632
639
|
throw new Error(
|
|
633
640
|
"serveShellRequest: partial.stale cannot be combined with partial.prefetch",
|
|
634
641
|
);
|
|
635
642
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
target
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
643
|
+
// As the browser writes them: browser/navigation-client.ts, and
|
|
644
|
+
// browser/prefetch/fetch.ts for the prefetch header.
|
|
645
|
+
const target = writePartialParams(new URL(url), {
|
|
646
|
+
segmentIds: segments,
|
|
647
|
+
emptySegments: "send",
|
|
648
|
+
stale,
|
|
649
|
+
boundary,
|
|
650
|
+
rewriteTarget,
|
|
651
|
+
fill,
|
|
652
|
+
});
|
|
644
653
|
if (prefetch && !headers.has(PREFETCH_HEADER)) {
|
|
645
654
|
headers.set(PREFETCH_HEADER, "1");
|
|
646
655
|
}
|
package/src/types/error-types.ts
CHANGED
|
@@ -81,19 +81,38 @@ export interface OnErrorContext<TEnv = any> {
|
|
|
81
81
|
actionId?: string;
|
|
82
82
|
/** Environment/bindings (platform context) */
|
|
83
83
|
env?: TEnv;
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Duration from request start to error (milliseconds). For a `background`
|
|
86
|
+
* error it counts from the start of the request that started the work, so
|
|
87
|
+
* it includes that response and any wait before the work ran.
|
|
88
|
+
*/
|
|
85
89
|
duration?: number;
|
|
86
90
|
/**
|
|
87
91
|
* Whether this is a partial/navigation request, on an error the segment
|
|
88
|
-
* walk catches (a handler that throws, a
|
|
89
|
-
* for a navigation, unset for a
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* either render; the `routing` phase reports `false`.
|
|
92
|
+
* walk catches (a handler that throws, a streamed `loading()` handler that
|
|
93
|
+
* rejects, a DSL loader that rejects): `true` for a navigation, unset for a
|
|
94
|
+
* document. A `cache()` record's refresh or proactive write reports the
|
|
95
|
+
* request that started it; a PPR shell capture renders a document. The
|
|
96
|
+
* `routing` phase reports `false`.
|
|
94
97
|
*/
|
|
95
98
|
isPartial?: boolean;
|
|
96
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* `true` when the error came from work whose output goes to a cache and
|
|
101
|
+
* never to the response of the request that started it: a stale `cache()`
|
|
102
|
+
* record's refresh, the write of a navigation that kept a layout mounted, a
|
|
103
|
+
* stale document cache entry's re-run, a PPR shell capture, or a stale
|
|
104
|
+
* `"use cache"` function's or cached loader's refresh, the work's own
|
|
105
|
+
* failure included. `request` and `url` are the starting request's (a
|
|
106
|
+
* shell capture's handler errors name the page it captures). A
|
|
107
|
+
* `router.prerender()` warm's own render is its request's: unset, while a
|
|
108
|
+
* shell capture it makes is `true`. Unset for every other error.
|
|
109
|
+
*/
|
|
110
|
+
background?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Whether an error boundary caught the error and its fallback reached the
|
|
113
|
+
* response. `false` on a `background` error: the failed render is never
|
|
114
|
+
* stored, so no visitor sees the fallback.
|
|
115
|
+
*/
|
|
97
116
|
handledByBoundary?: boolean;
|
|
98
117
|
/** Stack trace (if available) */
|
|
99
118
|
stack?: string;
|
package/src/wire-params.ts
CHANGED
|
@@ -19,6 +19,11 @@ export const WIRE = {
|
|
|
19
19
|
rewriteTarget: "_rsc_rt",
|
|
20
20
|
/** A stale request: `true` (router/navigation-snapshot.ts). */
|
|
21
21
|
stale: "_rsc_stale",
|
|
22
|
+
/**
|
|
23
|
+
* The id of an action's error boundary on screen that a stale request
|
|
24
|
+
* holds: read only beside `stale` (router/navigation-snapshot.ts).
|
|
25
|
+
*/
|
|
26
|
+
boundary: "_rsc_boundary",
|
|
22
27
|
/** A fill request (docs/design/prefetch-false.md): `1`. */
|
|
23
28
|
fill: "_rsc_fill",
|
|
24
29
|
/** A loader fetch's loader id (rsc/loader-fetch.ts). */
|