@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.
Files changed (84) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/browser/action-fence.d.ts +2 -1
  3. package/dist/types/browser/event-controller.d.ts +6 -1
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/use-action.d.ts +10 -4
  6. package/dist/types/browser/react/use-handle.d.ts +20 -16
  7. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  8. package/dist/types/browser/react/use-route-state.d.ts +45 -0
  9. package/dist/types/browser/segment-reconciler.d.ts +26 -0
  10. package/dist/types/browser/types.d.ts +30 -8
  11. package/dist/types/browser/wire-protocol.d.ts +1 -0
  12. package/dist/types/cache/tag-invalidation.d.ts +0 -10
  13. package/dist/types/router/error-handling.d.ts +7 -1
  14. package/dist/types/router/match-context.d.ts +2 -0
  15. package/dist/types/router/match-middleware/background-revalidation.d.ts +3 -2
  16. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  17. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  18. package/dist/types/router/segment-resolution/resolution-pass.d.ts +2 -0
  19. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  20. package/dist/types/router/types.d.ts +1 -1
  21. package/dist/types/server/request-context.d.ts +30 -1
  22. package/dist/types/testing/render-route.d.ts +43 -8
  23. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  24. package/dist/types/types/error-types.d.ts +27 -8
  25. package/dist/types/wire-params.d.ts +5 -0
  26. package/dist/vite/index.js +62 -67
  27. package/package.json +1 -1
  28. package/skills/client-urls/SKILL.md +6 -8
  29. package/skills/hooks/SKILL.md +1 -1
  30. package/skills/hooks/data.md +8 -1
  31. package/skills/hooks/handle-and-actions.md +40 -2
  32. package/skills/hooks/navigation.md +121 -15
  33. package/skills/hooks/state.md +6 -4
  34. package/skills/hooks/urls.md +17 -5
  35. package/skills/intercept/SKILL.md +6 -0
  36. package/skills/observability/SKILL.md +30 -0
  37. package/skills/server-actions/SKILL.md +8 -1
  38. package/skills/testing/client-components.md +25 -18
  39. package/skills/testing/serve-shell-request.md +7 -7
  40. package/skills/view-transitions/SKILL.md +1 -1
  41. package/src/browser/action-fence.ts +2 -1
  42. package/src/browser/event-controller.ts +78 -19
  43. package/src/browser/navigation-bridge.ts +45 -13
  44. package/src/browser/navigation-client.ts +2 -0
  45. package/src/browser/network-error-handler.ts +4 -0
  46. package/src/browser/partial-update.ts +33 -6
  47. package/src/browser/react/use-action.ts +27 -50
  48. package/src/browser/react/use-handle.ts +27 -18
  49. package/src/browser/react/use-link-status.ts +48 -60
  50. package/src/browser/react/use-navigation.ts +76 -51
  51. package/src/browser/react/use-params.ts +6 -35
  52. package/src/browser/react/use-pathname.ts +3 -32
  53. package/src/browser/react/use-route-state.ts +141 -0
  54. package/src/browser/react/use-search-params.ts +7 -40
  55. package/src/browser/segment-reconciler.ts +43 -0
  56. package/src/browser/server-action-bridge.ts +35 -8
  57. package/src/browser/types.ts +31 -8
  58. package/src/browser/wire-protocol.ts +2 -0
  59. package/src/cache/cache-runtime.ts +28 -31
  60. package/src/cache/document-cache.ts +34 -37
  61. package/src/cache/read-through-swr.ts +20 -18
  62. package/src/cache/tag-invalidation.ts +3 -18
  63. package/src/router/error-handling.ts +18 -6
  64. package/src/router/intercept-resolution.ts +9 -4
  65. package/src/router/match-api.ts +1 -0
  66. package/src/router/match-context.ts +3 -0
  67. package/src/router/match-middleware/background-revalidation.ts +5 -4
  68. package/src/router/match-middleware/cache-lookup.ts +5 -3
  69. package/src/router/navigation-snapshot.ts +10 -0
  70. package/src/router/segment-resolution/helpers.ts +49 -13
  71. package/src/router/segment-resolution/loader-cache.ts +4 -3
  72. package/src/router/segment-resolution/resolution-pass.ts +2 -0
  73. package/src/router/segment-resolution/revalidation.ts +57 -28
  74. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  75. package/src/router/types.ts +1 -1
  76. package/src/router.ts +7 -4
  77. package/src/rsc/request-stages.ts +4 -2
  78. package/src/rsc/shell-capture.ts +63 -58
  79. package/src/server/request-context.ts +49 -0
  80. package/src/testing/dispatch.ts +3 -0
  81. package/src/testing/render-route.tsx +204 -16
  82. package/src/testing/serve-shell-request.ts +19 -10
  83. package/src/types/error-types.ts +27 -8
  84. package/src/wire-params.ts +5 -0
package/dist/bin/rango.js CHANGED
@@ -1644,7 +1644,7 @@ var init_package = __esm({
1644
1644
  "package.json"() {
1645
1645
  package_default = {
1646
1646
  name: "@rangojs/router",
1647
- version: "0.28.0",
1647
+ version: "0.30.0",
1648
1648
  description: "Django-inspired RSC router with composable URL patterns",
1649
1649
  keywords: [
1650
1650
  "react",
@@ -20,7 +20,8 @@
20
20
  *
21
21
  * The drop runs the batch's follow-up: when an action of the batch did not
22
22
  * land on the page on screen, the action bridge refetches that page once,
23
- * whole, after the navigation the user started, unless an intercept is on
23
+ * whole (or whole above an action's error boundary on screen, which it
24
+ * holds), after the navigation the user started, unless an intercept is on
24
25
  * screen (server-action-bridge.ts followUp; the rules are R1-R7 in
25
26
  * docs/internal/execution-model.md).
26
27
  *
@@ -59,6 +59,11 @@ export interface ActionEntry {
59
59
  startedAt: number;
60
60
  /** Whether action processing is complete (may still be streaming) */
61
61
  completed?: boolean;
62
+ /**
63
+ * Set when a later call started (see startAction): "id" hides this call
64
+ * from every reader, "name" from readers that match by export name.
65
+ */
66
+ supersededBy?: "id" | "name";
62
67
  }
63
68
  /**
64
69
  * Derived navigation state (computed from source of truth)
@@ -70,7 +75,7 @@ export interface DerivedNavigationState {
70
75
  isStreaming: boolean;
71
76
  /** Whether a navigation is active (fetching or streaming, before commit) */
72
77
  isNavigating: boolean;
73
- /** Current committed location */
78
+ /** The location of the last page flushed to React (useNavigation keeps the page on screen) */
74
79
  location: NavigationLocation;
75
80
  /** URL being navigated to (null if idle) */
76
81
  pendingUrl: string | null;
@@ -112,18 +112,29 @@ export type UpdateMode = {
112
112
  * transition({ when }) sees kind "revalidate", not "replace".
113
113
  */
114
114
  refresh?: boolean;
115
+ /** The cross-tab refresh's held boundary (see `action`). */
116
+ boundary?: string;
115
117
  } | {
116
118
  type: "leave-intercept";
117
119
  interceptSourceUrl?: string;
120
+ boundary?: never;
118
121
  } | {
119
122
  type: "stale-revalidation";
120
123
  interceptSourceUrl?: string;
124
+ /** Back/Forward's held boundary (see `action`). */
125
+ boundary?: string;
121
126
  } | {
122
127
  type: "action";
123
128
  interceptSourceUrl?: string;
124
129
  actionId?: string;
125
130
  /** The action's FormData and result, for transition({ when }) `action`. */
126
131
  action?: TransitionWhenActionInput;
132
+ /**
133
+ * An action's error boundary the page shows (actionBoundaryAt): the
134
+ * server renders above it, its cached segment stays, and the page
135
+ * holds only the ids above it.
136
+ */
137
+ boundary?: string;
127
138
  };
128
139
  /**
129
140
  * Type for the fetchPartialUpdate function. In `action` mode it resolves with
@@ -13,19 +13,25 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
13
13
  * tracks the state of individual server action invocations.
14
14
  *
15
15
  * Uses the event controller for reactive state management.
16
- * State is derived from the inflight actions tracked by the controller.
16
+ * State is derived from the calls the controller tracks: those in flight,
17
+ * and each action's last settled call.
17
18
  *
18
19
  * Features:
19
20
  * - Tracks action lifecycle: idle → loading → streaming → idle
20
- * - Captures result/error locally (React handles cleanup)
21
- * - If multiple actions fire, tracks only the last one
21
+ * - Keeps the settled call's state until the action is called again.
22
+ * - If the same action fires several times, reports the latest call to
23
+ * start. An earlier call that settles later changes nothing it reports;
24
+ * a latest call that was aborted reports idle with nothing. One call at a
25
+ * time, not one per argument: a reader filtering on `payload` cannot track
26
+ * an earlier call once a later one starts.
22
27
  * - Supports selector pattern like useNavigation
23
28
  *
24
29
  * Matching behavior:
25
30
  * - **Function reference**: Uses full $$id for exact matching. This is precise
26
31
  * and distinguishes between actions with the same name in different files.
27
32
  * - **String**: Matches by suffix (action name after #). This is convenient
28
- * but may be ambiguous if multiple files export the same action name.
33
+ * but may be ambiguous if multiple files export the same action name: it
34
+ * follows the latest call of any action with that export name.
29
35
  *
30
36
  * @param action - Either a server action function or a string action name.
31
37
  * - **Function**: Must be directly imported in the client component.
@@ -21,21 +21,25 @@ import type { RouteStateSignal } from "../event-controller.js";
21
21
  export declare function useHandle<T, A>(handle: Handle<T, A>): Rango.FlightSerialize<A>;
22
22
  export declare function useHandle<T, A, S>(handle: Handle<T, A>, selector: (data: Rango.FlightSerialize<A>) => S): S;
23
23
  /**
24
- * Route state the controller's handle notifications drive (useHandle,
25
- * useSegments) commits with the page it belongs to. NavigationProvider
26
- * flushes the controller inside every payload update, so a value known then
27
- * is set in the page's lane. A page handed over urgently (signal "release")
28
- * takes a value that arrives before it commits the same way: set at default
29
- * priority, it renders in the page's lane, and the page's effects read it.
30
- * A page in a transition (signal "anchor") does not: until it commits
24
+ * Route state the controller's handle notifications drive commits with the
25
+ * page it belongs to. NavigationProvider flushes the controller inside every
26
+ * payload update, with signal "anchor" for a page in a transition and
27
+ * "release" for an urgent one, so the page's own change is set in the page's
28
+ * lane and the page's effects read it. From the anchor until the page commits
31
29
  * (releaseRouteState() from the provider's layout effect, signal "release"),
32
- * a new value waits, and is applied in the commit's task, right after the
33
- * page's own commit. In the page's lane, a value that suspends in a revealed
34
- * <Suspense> (a crumb's content) would hold the page; in a lane of its own,
35
- * it commits over the page being left and takes the page's transition types
36
- * (#1165); and a transition entangled with the page misses its commit when it
37
- * is set after React rendered the page, before its view transition's
38
- * mutation, and starts a view transition of its own. A value shallow-equal to
39
- * the last one sets nothing, so a reader renders only when its value moves.
30
+ * a later change waits and is applied in the commit's task, right after the
31
+ * page's own commit: set in a lane of its own, it would commit over the page
32
+ * being left (#1165).
33
+ *
34
+ * `waitForRelease`, set by useHandle only: the anchor's own change waits too,
35
+ * so a handle value lands right after its page's commit, never in its lane. A
36
+ * handle value can carry a promise that suspends, and in the page's lane one
37
+ * that suspends in a revealed <Suspense> would hold the page. Nothing
38
+ * useSegments reads can suspend, so it leaves it off and its path and
39
+ * location commit with their page.
40
+ *
41
+ * A value shallow-equal to the last one sets nothing, so a reader renders only
42
+ * when its value moves. A release is compared with what React committed, as in
43
+ * useRouteState.
40
44
  */
41
- export declare function useCommitAnchoredState<T>(init: () => T): [T, (value: T, signal?: RouteStateSignal) => void];
45
+ export declare function useCommitAnchoredState<T>(init: () => T, waitForRelease?: boolean): [T, (value: T, signal?: RouteStateSignal) => void];
@@ -14,8 +14,11 @@ export interface LinkStatus {
14
14
  /**
15
15
  * Hook to track the pending state of a Link component
16
16
  *
17
- * Must be used inside a Link component. Returns `{ pending: true }`
18
- * when navigation to this link's destination is in progress.
17
+ * Must be used inside a Link component. Returns `{ pending: true }` while a
18
+ * navigation to the link's destination is in flight, until the page it
19
+ * brings commits: under transition() while React holds that page, and
20
+ * through a server redirect until the target page commits. A newer
21
+ * navigation ends it at once.
19
22
  *
20
23
  * Useful for showing inline loading indicators on individual links.
21
24
  *
@@ -0,0 +1,45 @@
1
+ import type { RouteStateSignal } from "../event-controller.js";
2
+ /**
3
+ * The value a reader rendered in React's last commit of it, written by its
4
+ * layout effect. A page's release runs in the provider's layout effect, after
5
+ * every reader's (children first), so it reads the commit that brings the
6
+ * page.
7
+ */
8
+ export declare function useCommitted<T>(value: T): {
9
+ readonly current: T;
10
+ };
11
+ /**
12
+ * Sets a route-state reader's value. `next` maps the value React ends on once
13
+ * every set so far commits (`latest`) to the reader's value; `rebase` sets
14
+ * `next` itself, so React recomputes it on whatever base it renders. The value
15
+ * must not be a function.
16
+ */
17
+ export type RouteStateSetter<T> = (next: (base: T) => T, signal?: RouteStateSignal, rebase?: boolean) => void;
18
+ /**
19
+ * Route state a reader sets as the router store notifies it, in the lane of
20
+ * the notification that carries it. A notification equal to `latest` sets
21
+ * nothing, so a reader renders only when its value moves.
22
+ *
23
+ * A release is compared with what React committed, not with `latest`: while
24
+ * React holds a page, its set is not on screen, and an urgent page that ends
25
+ * the hold brings an equal value. A release that finds every set committed
26
+ * with its value sets nothing.
27
+ */
28
+ export declare function useRouteState<T>(init: () => T, same: (a: T, b: T) => boolean): [T, RouteStateSetter<T>];
29
+ /** The route a URL hook reads. */
30
+ export interface UrlRoute {
31
+ location: URL;
32
+ params: Record<string, string>;
33
+ }
34
+ /**
35
+ * A URL hook's value (usePathname, useParams, useSearchParams): `read` of the
36
+ * route, seeded from the hydration snapshot while the document hydrates, then
37
+ * set as the store notifies it and at a page's release (useRouteState). The
38
+ * release rides the handle channel. `read` applies when the store changes,
39
+ * never in a later render: a new one takes effect from the next change.
40
+ *
41
+ * A read equal to the value it is set over keeps that value, so an object
42
+ * reader changes identity once per value: a release that ends a hold sets the
43
+ * held page's object, which React's render of the held lane keeps.
44
+ */
45
+ export declare function useUrlValue<T>(read: (route: UrlRoute) => T, same: (a: T, b: T) => boolean): T;
@@ -71,3 +71,29 @@ export declare function reconcileSegments(input: ReconcileInput): ReconcileResul
71
71
  * other segment merges as an action's does (mergeCached).
72
72
  */
73
73
  export declare function reconcileErrorSegments(cachedSegments: ResolvedSegment[], errorSegments: ResolvedSegment[]): ReconcileResult;
74
+ /**
75
+ * The ids a page holds once an action's error boundary `boundary` shows: the
76
+ * ones above it. Nothing at or below the boundary ran after the action's
77
+ * write (its response and the follow-up cut there), so the next request must
78
+ * not hold it. ShortCodes nest, so those ids start with the boundary's: the
79
+ * boundary, its loaders (`${id}D…`), its slots and every entry under it. A
80
+ * sibling with a longer index (`…R10` beside `…R1`) is kept.
81
+ */
82
+ export declare function idsAboveBoundary(ids: string[], boundary: string | undefined): string[];
83
+ /**
84
+ * The error segment of every action error boundary committed, by identity
85
+ * (server-action-bridge.ts adds and deletes). An entry shows one while its
86
+ * cached list still holds that object: a later commit on the entry replaces
87
+ * the list, and a refetch that holds the boundary keeps it. Weak, so a
88
+ * replaced boundary is not kept alive. Two routers never share a segment
89
+ * object. No hook reads it.
90
+ */
91
+ export declare const actionBoundaries: WeakSet<ResolvedSegment>;
92
+ /**
93
+ * The index of the deepest action error boundary in `segments`, -1 for none.
94
+ * It is that action's outcome, which no GET reproduces: a background refetch
95
+ * of the page (the follow-up, Back/Forward's stale revalidation, the
96
+ * cross-tab refresh) holds it and renders what is above it. At 0 nothing is
97
+ * above it.
98
+ */
99
+ export declare function actionBoundaryAt(segments: readonly ResolvedSegment[]): number;
@@ -191,11 +191,24 @@ export interface InflightAction {
191
191
  * @internal This type is an implementation detail. Use PublicNavigationState instead.
192
192
  */
193
193
  export interface NavigationState {
194
- /** Navigation lifecycle state (idle or loading during navigation) */
194
+ /**
195
+ * Lifecycle state: `"loading"` while a navigation is in flight and while a
196
+ * server action runs, `"idle"` otherwise
197
+ */
195
198
  state: "idle" | "loading";
196
- /** Whether RSC data is currently streaming (initial load or navigation) */
199
+ /**
200
+ * Whether RSC data is currently streaming: a navigation's response, or a
201
+ * server action's response (initial load, navigation or action)
202
+ */
197
203
  isStreaming: boolean;
198
- /** Current location */
204
+ /**
205
+ * True while a navigation is in flight (fetching or streaming, before its
206
+ * commit); false during a server action, whose `state` is still `"loading"`.
207
+ * Drive a navigation progress bar from this and an action spinner from
208
+ * `state`.
209
+ */
210
+ isNavigating: boolean;
211
+ /** The page on screen; moves when the next page shows (`pendingUrl` is the page being fetched) */
199
212
  location: NavigationLocation;
200
213
  /** URL being navigated to (null when idle) */
201
214
  pendingUrl: string | null;
@@ -218,7 +231,11 @@ export type ActionLifecycleState = "idle" | "loading" | "streaming";
218
231
  * @internal This type is an implementation detail and may change without notice.
219
232
  */
220
233
  export interface TrackedActionState {
221
- /** Current lifecycle state of the action */
234
+ /**
235
+ * Current lifecycle state of the action. Once the call settles it is
236
+ * "idle" and the rest of the call's state is kept until the action's next
237
+ * call.
238
+ */
222
239
  state: ActionLifecycleState;
223
240
  /** Server action function ID (e.g., "addToCart") */
224
241
  actionId: string | null;
@@ -226,12 +243,12 @@ export interface TrackedActionState {
226
243
  payload: unknown[] | FormData | null;
227
244
  /** Error if action failed */
228
245
  error: unknown | null;
229
- /** Result data from the action (preserved after completion) */
246
+ /** Result data from the action */
230
247
  result: unknown | null;
231
248
  /**
232
249
  * The error of a loader that threw while it re-ran after this successful
233
- * action (the first, when several did). The loader also throws at its
234
- * useLoader read, where the nearest error boundary catches it.
250
+ * action (the first, when several did). The loader also throws at its useLoader read, where the nearest
251
+ * error boundary catches it.
235
252
  */
236
253
  revalidationError: unknown | null;
237
254
  }
@@ -450,6 +467,8 @@ export interface NavigateOptionsInternal extends Omit<NavigateOptions, "state">
450
467
  _skipCache?: boolean;
451
468
  /** Server redirects already followed by this navigation chain (0 for a user navigation) */
452
469
  _redirectHops?: number;
470
+ /** The URL a redirected navigation chain started from: a link to it stays pending (useLinkStatus) */
471
+ _redirectFrom?: string;
453
472
  }
454
473
  /**
455
474
  * Options for useRouter push/replace methods.
@@ -603,6 +622,8 @@ export interface FetchPartialOptions {
603
622
  signal?: AbortSignal;
604
623
  /** If true, this is a stale cache revalidation request - server should force revalidators */
605
624
  staleRevalidation?: boolean;
625
+ /** Sent as `_rsc_boundary` (wire-params.ts WIRE.boundary). */
626
+ boundary?: string;
606
627
  interceptSourceUrl?: string;
607
628
  /** RSC version for cache invalidation detection */
608
629
  version?: string;
@@ -667,7 +688,8 @@ export interface ServerActionBridgeConfig {
667
688
  */
668
689
  export interface NavigationBridge {
669
690
  navigate(url: string, options?: NavigateOptionsInternal): Promise<void>;
670
- refresh(): Promise<void>;
691
+ /** `crossTab`: another tab's write; an action's error boundary stays. */
692
+ refresh(crossTab?: boolean): Promise<void>;
671
693
  /** `event`: the popstate event; a traversal the browser animated itself (hasUAVisualTransition) is not animated again. */
672
694
  handlePopstate(event?: PopStateEvent): Promise<void>;
673
695
  registerLinkInterception(): () => void;
@@ -11,6 +11,7 @@ export interface PartialRequestInit {
11
11
  */
12
12
  emptySegments: "send" | "omit";
13
13
  stale?: boolean;
14
+ boundary?: string;
14
15
  version?: string;
15
16
  routerId?: string;
16
17
  rewriteTarget?: string;
@@ -35,19 +35,9 @@
35
35
  * request, is neither joined nor written (#973, #977): see
36
36
  * predatesInvalidation, and docs/design/caching.md "Read-your-own-writes".
37
37
  */
38
- import { type RequestContext } from "../server/request-context.js";
39
38
  import type { SegmentCacheStore } from "./types.js";
40
39
  import { type ExecutionStart } from "./invalidation-order.js";
41
40
  export { executionStart, invalidatedSince, markResponseStart, type ExecutionStart, } from "./invalidation-order.js";
42
- /**
43
- * Run a stale entry's background refresh on the visitor's request context: a
44
- * "use cache" function's (cache-runtime.ts) or a cached loader's
45
- * (loader-cache.ts). No visitor made the invalidations in it, so they set no
46
- * fresh-reads cookie, as in a stale cache() HIT's re-render or a document
47
- * cache entry's re-run, whose passes take a sink's no-op writer
48
- * (server/render-pass.ts).
49
- */
50
- export declare function runAsBackgroundRefresh<T>(ctx: RequestContext, fn: () => T): T;
51
41
  /**
52
42
  * The write gate of every cache execution (#977): whether one of `tags` was
53
43
  * invalidated after the execution that produced the value started.
@@ -25,7 +25,7 @@ export interface InvokeOnErrorContext<TEnv = any> {
25
25
  isPartial?: boolean;
26
26
  handledByBoundary?: boolean;
27
27
  metadata?: Record<string, unknown>;
28
- /** Request start time from performance.now() for duration calculation */
28
+ /** performance.now() at request entry; the request context's by default. */
29
29
  requestStartTime?: number;
30
30
  }
31
31
  /**
@@ -35,6 +35,12 @@ export interface InvokeOnErrorContext<TEnv = any> {
35
35
  * This is a shared utility used by both the router and RSC handler to ensure
36
36
  * consistent error callback behavior across the codebase.
37
37
  *
38
+ * `background` is set inside runAsBackgroundWork (server/request-context.ts),
39
+ * and `duration` and `env` come from the request context on the ALS
40
+ * (`_handlerStart`, `env`) when the call site leaves them out, so no call
41
+ * site has to know. A Prerender pass's ctx.env throws without buildEnv, so
42
+ * the walk never reads it eagerly.
43
+ *
38
44
  * @param onError - The onError callback to invoke (may be undefined)
39
45
  * @param error - The error that occurred
40
46
  * @param phase - The phase where the error occurred
@@ -126,6 +126,8 @@ export interface MatchContext<TEnv = any> {
126
126
  clientSegmentIds: string[];
127
127
  clientSegmentSet: Set<string>;
128
128
  stale: boolean;
129
+ /** NavigationSnapshot.heldBoundary: the walk stops above it. */
130
+ heldBoundary?: string;
129
131
  prevUrl: URL;
130
132
  prevParams: Record<string, string>;
131
133
  prevMatch: RouteMatchResult | null;
@@ -125,8 +125,9 @@ import { type RequestContext } from "../../server/request-context.js";
125
125
  * each field and why. The DSL store below has `metrics` unset too (track()
126
126
  * reads that one), so nothing lands on the foreground's perf timeline;
127
127
  * ctx.Store.run closes over ctx.Store, so the derived store needs its own run.
128
- * runWithRequestContext also re-establishes the request ALS, which a waitUntil
129
- * task on workerd loses; the DSL store is a different ALS (build context).
128
+ * runAsBackgroundWork also re-establishes the request ALS, which a waitUntil
129
+ * task on workerd loses, and marks the pass's onError reports `background`;
130
+ * the DSL store is a different ALS (build context).
130
131
  */
131
132
  export declare function rerenderAndCacheRoute<TEnv>(ctx: MatchContext<TEnv>, requestCtx: RequestContext<TEnv>, cacheScope: CacheScope, routerCtx: RouterContext<TEnv>): Promise<number>;
132
133
  /**
@@ -11,6 +11,13 @@ export interface NavigationSnapshot {
11
11
  clientSegmentSet: Set<string>;
12
12
  filteredSegmentIds: string[];
13
13
  stale: boolean;
14
+ /**
15
+ * The id of an action's error boundary on screen that a stale request holds
16
+ * (`_rsc_boundary`, the action follow-up): read only beside `stale`, so it
17
+ * has every stale guarantee (a live render, no route-cache or document-cache
18
+ * read or store, no PPR replay).
19
+ */
20
+ heldBoundary?: string;
14
21
  isSameRouteNavigation: boolean;
15
22
  effectiveFromUrl: URL;
16
23
  effectiveFromMatch: RouteMatchResult | null;
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { ReactNode } from "react";
11
11
  import type { EntryData } from "../../server/context";
12
- import type { ResolvedSegment, HandlerContext } from "../../types";
12
+ import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
13
13
  import type { SegmentResolutionDeps } from "../types.js";
14
14
  import type { TelemetrySink } from "../telemetry.js";
15
15
  /** The errorContext shape wrapLoaderPromise expects as its 5th argument. */
@@ -70,16 +70,32 @@ export interface ErrorReportContext {
70
70
  routeKey?: string;
71
71
  env?: any;
72
72
  isPartial?: boolean;
73
- requestStartTime?: number;
74
73
  telemetry?: TelemetrySink;
75
74
  }
75
+ type Thrower = {
76
+ id: string;
77
+ type: ErrorInfo["segmentType"];
78
+ };
79
+ /**
80
+ * Note that `id` threw `error`, for an error its owning entry's boundary
81
+ * catches (a parallel slot's sync throw, an orphan layout's): onError names
82
+ * the segment that threw, as a streamed slot's rejection does. Kept on the
83
+ * request context, never the module: a reused error object (a constant, a
84
+ * memoized rejection) can be thrown by another request, or by a Prerender
85
+ * pass that rethrows before catchSegmentError reads it. Returns `error`, to
86
+ * rethrow.
87
+ */
88
+ export declare function noteThrower(error: unknown, id: string, type: ErrorInfo["segmentType"]): unknown;
76
89
  /**
77
90
  * Handle a caught error during segment resolution by creating an
78
91
  * error or not-found segment with the nearest boundary.
79
92
  *
80
93
  * Called by resolveWithErrorBoundary to produce error/notFound segments.
94
+ * `entry` is where the boundary is looked up; `thrower` names the segment
95
+ * that threw when it is not `entry` (an intercept's slot, under its declaring
96
+ * entry), for the report, the server print and the client's ErrorInfo.
81
97
  */
82
- export declare function catchSegmentError<TEnv>(error: unknown, entry: EntryData, params: Record<string, string>, deps: SegmentResolutionDeps<TEnv>, report?: ErrorReportContext, pathname?: string): ResolvedSegment;
98
+ export declare function catchSegmentError<TEnv>(error: unknown, entry: EntryData, params: Record<string, string>, deps: SegmentResolutionDeps<TEnv>, report?: ErrorReportContext, pathname?: string, thrower?: Thrower): ResolvedSegment;
83
99
  /**
84
100
  * Generic error boundary wrapper for segment resolution.
85
101
  * Catches non-Response errors and produces an error/notFound segment
@@ -46,6 +46,8 @@ export interface ResolutionPassInput<TEnv = any> {
46
46
  readonly routeKey: string;
47
47
  readonly actionContext?: ActionContext;
48
48
  readonly stale?: boolean;
49
+ /** NavigationSnapshot.heldBoundary: the walk cuts there, sending no segment. */
50
+ readonly heldBoundary?: string;
49
51
  /**
50
52
  * No loader is started and no loader segment is emitted: build time
51
53
  * (prerender-match.ts) and the background re-render of a route record
@@ -32,6 +32,8 @@ export interface StreamedHandlerOptions<TEnv> {
32
32
  context: HandlerContext<any, TEnv>;
33
33
  routeKey?: string;
34
34
  params: Record<string, string>;
35
+ /** onError's `isPartial`, as the walk reports a sync handler's error. */
36
+ isPartial: true | undefined;
35
37
  /** False where the rejection must reach the caller (prerender, intercept background re-render). */
36
38
  recover: boolean;
37
39
  }
@@ -34,13 +34,13 @@ export interface SegmentResolutionDeps<TEnv = any> {
34
34
  params?: Record<string, string>;
35
35
  env?: TEnv;
36
36
  isPartial?: boolean;
37
- requestStartTime?: number;
38
37
  },
39
38
  /** An `ssr: false` loader on a document or capture render. */
40
39
  useDefaultErrorBoundary?: boolean) => Promise<LoaderDataResult<T>>;
41
40
  trackHandler: <T>(promise: Promise<T>, errorContext?: {
42
41
  segmentId?: string;
43
42
  segmentType?: string;
43
+ isPartial?: boolean;
44
44
  }) => Promise<T>;
45
45
  findNearestErrorBoundary: (entry: EntryData | null) => ReactNode | ErrorBoundaryHandler | null;
46
46
  findNearestNotFoundBoundary: (entry: EntryData | null) => ReactNode | NotFoundBoundaryHandler | null;
@@ -683,6 +683,15 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
683
683
  _renderBarrierGuardClosed?: boolean;
684
684
  /** @internal Per-request error dedup set for onError reporting */
685
685
  _reportedErrors: WeakSet<object>;
686
+ /**
687
+ * @internal The segment a parallel slot's or orphan layout's error came
688
+ * from, for the owning entry's catch (segment-resolution/helpers.ts
689
+ * noteThrower). Per request: an error object can be thrown by several.
690
+ */
691
+ _throwers?: WeakMap<object, {
692
+ id: string;
693
+ type: string;
694
+ }>;
686
695
  /**
687
696
  * @internal The errors this request printed to the server console, so a
688
697
  * failure prints once where it originated (server/print-server-error.ts). A derived context shares it through the prototype.
@@ -786,7 +795,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
786
795
  * This is the type exported to library consumers. Internal code should
787
796
  * use the full RequestContext interface directly.
788
797
  */
789
- export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_prerender" | "_onDemandProducer" | "_searchParamsFilter" | "_shellCaptureState" | "_shellImplicitCache" | "_shellLoaderSeed" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_prerenderWarm" | "_runAtRequestEntry" | "_shellFragmentPayload" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_payloadDeferred" | "_requestKind" | "_payloadVisitorLocation" | "_locationState" | "_routeName" | "_prevRouteKey" | "_visitorMoved" | "_inActionRevalidation" | "_actionRan" | "_reportedErrors" | "_printedErrors" | "_renderErrors" | "_recoveredHandlerErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_handlerTiming" | "_tracing" | "_basename" | "_routerId" | "_versions" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
798
+ export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_prerender" | "_onDemandProducer" | "_searchParamsFilter" | "_shellCaptureState" | "_shellImplicitCache" | "_shellLoaderSeed" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_prerenderWarm" | "_runAtRequestEntry" | "_shellFragmentPayload" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_payloadDeferred" | "_requestKind" | "_payloadVisitorLocation" | "_locationState" | "_routeName" | "_prevRouteKey" | "_visitorMoved" | "_inActionRevalidation" | "_actionRan" | "_reportedErrors" | "_throwers" | "_printedErrors" | "_renderErrors" | "_recoveredHandlerErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_handlerTiming" | "_tracing" | "_basename" | "_routerId" | "_versions" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
790
799
  /**
791
800
  * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
792
801
  * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
@@ -816,6 +825,26 @@ export declare function captureRequestEntryContext(): (<T>(fn: () => T) => T) |
816
825
  * Used by the RSC handler to provide context to server actions
817
826
  */
818
827
  export declare function runWithRequestContext<TEnv, T>(context: RequestContext<TEnv>, fn: () => T): T;
828
+ /**
829
+ * Run work whose output goes only to a cache, never to the response of the
830
+ * request that started it, on `context`: a stale entry's refresh (a "use
831
+ * cache" function, a cached loader, a cache() record, a document), a
832
+ * navigation's proactive cache() write, a PPR shell capture. No visitor made
833
+ * the invalidations in it, so they set no fresh-reads cookie
834
+ * (cache/tag-invalidation.ts), and onError marks every error reported inside
835
+ * it `background` (router/error-handling.ts invokeOnError). Its failure is
836
+ * reported inside it too: a catch outside runs on the visitor's frame.
837
+ */
838
+ export declare function runAsBackgroundWork<TEnv, T>(context: RequestContext<TEnv>, fn: () => T): T;
839
+ /** Whether the caller runs inside runAsBackgroundWork. */
840
+ export declare function isBackgroundWork(): boolean;
841
+ /**
842
+ * The set onError's dedupe records a reported error in. Background work keeps
843
+ * its own: an error object both the visitor's render and background work
844
+ * reach (a memoized rejection, a thrown constant) is reported by each, so a
845
+ * background report never takes the place of the visitor's.
846
+ */
847
+ export declare function reportedErrorsOf(context: RequestContext<any>): WeakSet<object>;
819
848
  /**
820
849
  * Get the current request context
821
850
  * Throws if called outside of a request context