@rangojs/router 0.28.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/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 +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 +26 -0
- package/dist/types/browser/types.d.ts +22 -4
- 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 +15 -19
- package/package.json +1 -1
- package/skills/client-urls/SKILL.md +4 -2
- 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 +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 +32 -10
- 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 +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/segment-reconciler.ts +43 -0
- package/src/browser/server-action-bridge.ts +35 -8
- package/src/browser/types.ts +23 -4
- 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 +199 -13
- package/src/testing/serve-shell-request.ts +19 -10
- package/src/types/error-types.ts +27 -8
- package/src/wire-params.ts +5 -0
|
@@ -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 goes inside
|
|
479
|
+
* `await act(async () => ...)`. Its first update makes React retry the held
|
|
480
|
+
* page, which suspends again; a synchronous act() scope drops that retry
|
|
481
|
+
* and React renders no later update of the same priority.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* await act(async () => {
|
|
485
|
+
* await router.navigate("/products/3", { transition: false });
|
|
486
|
+
* });
|
|
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
|
|
@@ -837,6 +918,29 @@ export async function renderRoute(
|
|
|
837
918
|
// takes it, and no act() of the caller's can overlap it.
|
|
838
919
|
act(() => eventController.flushRouteState());
|
|
839
920
|
try {
|
|
921
|
+
const streamUntil = navOptions?.streamUntil;
|
|
922
|
+
if (streamUntil) {
|
|
923
|
+
// The response arrives a task after the request, ahead of the
|
|
924
|
+
// commit (navigation-transaction.ts: the stream opens with it).
|
|
925
|
+
await act(async () => {
|
|
926
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
927
|
+
});
|
|
928
|
+
if (handle.signal.aborted) return;
|
|
929
|
+
const stream = handle.startStreaming();
|
|
930
|
+
act(() => eventController.flushRouteState());
|
|
931
|
+
// A navigation that is left stops counting as streaming at the abort
|
|
932
|
+
// (partial-update.ts endStream); the abort's notification goes out
|
|
933
|
+
// with the newer navigation's.
|
|
934
|
+
handle.signal.addEventListener("abort", () => stream.end(), {
|
|
935
|
+
once: true,
|
|
936
|
+
});
|
|
937
|
+
const end = (): void =>
|
|
938
|
+
act(() => {
|
|
939
|
+
stream.end();
|
|
940
|
+
eventController.flushRouteState();
|
|
941
|
+
});
|
|
942
|
+
void streamUntil.then(end, end);
|
|
943
|
+
}
|
|
840
944
|
await commitNavigation(handle, target, navOptions);
|
|
841
945
|
} finally {
|
|
842
946
|
// Back to idle when the commit never came (a render that threw).
|
|
@@ -922,6 +1026,7 @@ export async function renderRoute(
|
|
|
922
1026
|
// A newer navigate() started meanwhile: this one commits nothing, as
|
|
923
1027
|
// production's (browser/partial-update.ts) returns on its aborted signal.
|
|
924
1028
|
if (handle.signal.aborted) return;
|
|
1029
|
+
leaveEntry();
|
|
925
1030
|
handle.complete(nextUrl);
|
|
926
1031
|
eventController.setParams(match.params);
|
|
927
1032
|
store.setCurrentUrl(nextUrl.href);
|
|
@@ -934,6 +1039,13 @@ export async function renderRoute(
|
|
|
934
1039
|
);
|
|
935
1040
|
eventController.commitLocationState(historyState);
|
|
936
1041
|
}
|
|
1042
|
+
entryOnScreen = entryIndex();
|
|
1043
|
+
entries.set(entryOnScreen, {
|
|
1044
|
+
url: nextUrl,
|
|
1045
|
+
segments,
|
|
1046
|
+
params: match.params,
|
|
1047
|
+
state: window.history.state,
|
|
1048
|
+
});
|
|
937
1049
|
const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
|
|
938
1050
|
await act(async () => {
|
|
939
1051
|
// As production's emitters name their cause (suspense-audit.ts I6).
|
|
@@ -961,17 +1073,90 @@ export async function renderRoute(
|
|
|
961
1073
|
return navigate(current.pathname + current.search, { kind: "revalidate" });
|
|
962
1074
|
};
|
|
963
1075
|
|
|
1076
|
+
// A back/forward onto the mounted entry or one a navigation wrote, whose
|
|
1077
|
+
// page is not the one on screen: its page is restored from the entry, as production's
|
|
1078
|
+
// history-cache restore does (navigation-bridge.ts handlePopstate). Not
|
|
1079
|
+
// modeled: a stale entry's revalidation and a restore that has to fetch.
|
|
1080
|
+
const restoreEntry = async (index: number, page: EntryPage) => {
|
|
1081
|
+
const generation = ++popGeneration;
|
|
1082
|
+
eventController.abortNavigation();
|
|
1083
|
+
const historyState = window.history.state;
|
|
1084
|
+
// transition({ when }) decides the restore (kind "pop") against the
|
|
1085
|
+
// entry being left.
|
|
1086
|
+
const gatedOff = decideTransitionGatedOff(page.segments, () => ({
|
|
1087
|
+
kind: "pop",
|
|
1088
|
+
from: {
|
|
1089
|
+
url: eventController.getLocation().href,
|
|
1090
|
+
params: eventController.getParams(),
|
|
1091
|
+
routeName: leaf.name,
|
|
1092
|
+
state: entries.get(entryOnScreen)?.state ?? null,
|
|
1093
|
+
},
|
|
1094
|
+
to: {
|
|
1095
|
+
url: page.url,
|
|
1096
|
+
params: page.params,
|
|
1097
|
+
routeName: leaf.name,
|
|
1098
|
+
state: historyState,
|
|
1099
|
+
},
|
|
1100
|
+
}));
|
|
1101
|
+
const root = await renderSegments(page.segments, {
|
|
1102
|
+
forceAwait: true,
|
|
1103
|
+
outletPending: options.outletPending,
|
|
1104
|
+
transitionGatedOff: gatedOff,
|
|
1105
|
+
});
|
|
1106
|
+
// A later back/forward owns the screen.
|
|
1107
|
+
if (generation !== popGeneration) return;
|
|
1108
|
+
leaveEntry();
|
|
1109
|
+
entryOnScreen = index;
|
|
1110
|
+
store.setCurrentUrl(page.url.href);
|
|
1111
|
+
store.setSegmentIds(page.segments.map((s) => s.id));
|
|
1112
|
+
const update: NavigationUpdate = {
|
|
1113
|
+
root,
|
|
1114
|
+
metadata: {
|
|
1115
|
+
...makeMetadata(page.url.pathname, page.segments, page.params),
|
|
1116
|
+
isPartial: true,
|
|
1117
|
+
cachedHandleData: page.handleData,
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1120
|
+
await act(async () => {
|
|
1121
|
+
// The restored entry's location and params, set where its tree is
|
|
1122
|
+
// handed over, so the hooks take them in that batch.
|
|
1123
|
+
eventController.setLocation(page.url);
|
|
1124
|
+
eventController.setParams(page.params);
|
|
1125
|
+
eventController.commitLocationState(historyState);
|
|
1126
|
+
auditTreeCause("popstate");
|
|
1127
|
+
if (!shouldStartViewTransition(page.segments, gatedOff)) {
|
|
1128
|
+
store.emitUpdate(update);
|
|
1129
|
+
} else {
|
|
1130
|
+
startTransition(() => {
|
|
1131
|
+
store.emitUpdate({ ...update, inTransition: true });
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
};
|
|
1136
|
+
|
|
964
1137
|
let prefetchRoot: HTMLElement | undefined;
|
|
965
1138
|
const bridge: NavigationBridge = {
|
|
966
1139
|
navigate: (target, navigateOptions) =>
|
|
967
1140
|
navigate(target, { history: navigateOptions ?? {} }),
|
|
968
1141
|
refresh: () => refresh(),
|
|
969
|
-
// Back/forward onto the entry history.state holds.
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
//
|
|
973
|
-
//
|
|
1142
|
+
// Back/forward onto the entry history.state holds. Its page is restored
|
|
1143
|
+
// when the mount or a navigation wrote the entry, history still holds that
|
|
1144
|
+
// state and another page is on screen (restoreEntry). Otherwise the tree
|
|
1145
|
+
// keeps its location: a treeless commit of that entry, as production's
|
|
1146
|
+
// traversal that keeps every segment (navigation-transaction.ts commit).
|
|
1147
|
+
// Flushed, not left to the notify timer: the dispatching test's act()
|
|
1148
|
+
// has returned by then.
|
|
974
1149
|
handlePopstate: async () => {
|
|
1150
|
+
const index = entryIndex();
|
|
1151
|
+
const page = entries.get(index);
|
|
1152
|
+
if (
|
|
1153
|
+
page &&
|
|
1154
|
+
index !== entryOnScreen &&
|
|
1155
|
+
page.state === window.history.state
|
|
1156
|
+
) {
|
|
1157
|
+
await restoreEntry(index, page);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
975
1160
|
eventController.commitLocationState(window.history.state, true);
|
|
976
1161
|
eventController.flushRouteState();
|
|
977
1162
|
},
|
|
@@ -1091,6 +1276,7 @@ export async function renderRoute(
|
|
|
1091
1276
|
loaders: navOptions?.loaders,
|
|
1092
1277
|
handles: navOptions?.handles,
|
|
1093
1278
|
transition: navOptions?.transition,
|
|
1279
|
+
streamUntil: navOptions?.streamUntil,
|
|
1094
1280
|
history:
|
|
1095
1281
|
navOptions?.state !== undefined || navOptions?.replace
|
|
1096
1282
|
? 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). */
|