@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.
Files changed (79) 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 +7 -2
  6. package/dist/types/browser/react/use-handle.d.ts +19 -16
  7. package/dist/types/browser/react/use-link-status.d.ts +5 -2
  8. package/dist/types/browser/segment-reconciler.d.ts +26 -0
  9. package/dist/types/browser/types.d.ts +22 -4
  10. package/dist/types/browser/wire-protocol.d.ts +1 -0
  11. package/dist/types/cache/tag-invalidation.d.ts +0 -10
  12. package/dist/types/router/error-handling.d.ts +7 -1
  13. package/dist/types/router/match-context.d.ts +2 -0
  14. package/dist/types/router/match-middleware/background-revalidation.d.ts +3 -2
  15. package/dist/types/router/navigation-snapshot.d.ts +7 -0
  16. package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
  17. package/dist/types/router/segment-resolution/resolution-pass.d.ts +2 -0
  18. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
  19. package/dist/types/router/types.d.ts +1 -1
  20. package/dist/types/server/request-context.d.ts +30 -1
  21. package/dist/types/testing/render-route.d.ts +43 -8
  22. package/dist/types/testing/serve-shell-request.d.ts +8 -1
  23. package/dist/types/types/error-types.d.ts +27 -8
  24. package/dist/types/wire-params.d.ts +5 -0
  25. package/dist/vite/index.js +15 -19
  26. package/package.json +1 -1
  27. package/skills/client-urls/SKILL.md +4 -2
  28. package/skills/hooks/SKILL.md +1 -1
  29. package/skills/hooks/data.md +8 -1
  30. package/skills/hooks/handle-and-actions.md +33 -2
  31. package/skills/hooks/navigation.md +121 -15
  32. package/skills/hooks/state.md +6 -4
  33. package/skills/hooks/urls.md +16 -5
  34. package/skills/intercept/SKILL.md +6 -0
  35. package/skills/observability/SKILL.md +30 -0
  36. package/skills/server-actions/SKILL.md +8 -1
  37. package/skills/testing/client-components.md +25 -18
  38. package/skills/testing/serve-shell-request.md +7 -7
  39. package/skills/view-transitions/SKILL.md +1 -1
  40. package/src/browser/action-fence.ts +2 -1
  41. package/src/browser/event-controller.ts +32 -10
  42. package/src/browser/navigation-bridge.ts +45 -13
  43. package/src/browser/navigation-client.ts +2 -0
  44. package/src/browser/network-error-handler.ts +4 -0
  45. package/src/browser/partial-update.ts +33 -6
  46. package/src/browser/react/use-action.ts +24 -48
  47. package/src/browser/react/use-handle.ts +22 -18
  48. package/src/browser/react/use-link-status.ts +48 -60
  49. package/src/browser/react/use-navigation.ts +92 -41
  50. package/src/browser/segment-reconciler.ts +43 -0
  51. package/src/browser/server-action-bridge.ts +35 -8
  52. package/src/browser/types.ts +23 -4
  53. package/src/browser/wire-protocol.ts +2 -0
  54. package/src/cache/cache-runtime.ts +28 -31
  55. package/src/cache/document-cache.ts +34 -37
  56. package/src/cache/read-through-swr.ts +20 -18
  57. package/src/cache/tag-invalidation.ts +3 -18
  58. package/src/router/error-handling.ts +18 -6
  59. package/src/router/intercept-resolution.ts +9 -4
  60. package/src/router/match-api.ts +1 -0
  61. package/src/router/match-context.ts +3 -0
  62. package/src/router/match-middleware/background-revalidation.ts +5 -4
  63. package/src/router/match-middleware/cache-lookup.ts +5 -3
  64. package/src/router/navigation-snapshot.ts +10 -0
  65. package/src/router/segment-resolution/helpers.ts +49 -13
  66. package/src/router/segment-resolution/loader-cache.ts +4 -3
  67. package/src/router/segment-resolution/resolution-pass.ts +2 -0
  68. package/src/router/segment-resolution/revalidation.ts +57 -28
  69. package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
  70. package/src/router/types.ts +1 -1
  71. package/src/router.ts +7 -4
  72. package/src/rsc/request-stages.ts +4 -2
  73. package/src/rsc/shell-capture.ts +63 -58
  74. package/src/server/request-context.ts +49 -0
  75. package/src/testing/dispatch.ts +3 -0
  76. package/src/testing/render-route.tsx +199 -13
  77. package/src/testing/serve-shell-request.ts +19 -10
  78. package/src/types/error-types.ts +27 -8
  79. 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.29.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
@@ -18,14 +18,19 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
18
18
  * Features:
19
19
  * - Tracks action lifecycle: idle → loading → streaming → idle
20
20
  * - Captures result/error locally (React handles cleanup)
21
- * - If multiple actions fire, tracks only the last one
21
+ * - If the same action fires several times, reports the latest call to
22
+ * start. An earlier call that settles later changes nothing it reports;
23
+ * a latest call that was aborted reports idle with nothing. One call at a
24
+ * time, not one per argument: a reader filtering on `payload` cannot track
25
+ * an earlier call once a later one starts.
22
26
  * - Supports selector pattern like useNavigation
23
27
  *
24
28
  * Matching behavior:
25
29
  * - **Function reference**: Uses full $$id for exact matching. This is precise
26
30
  * and distinguishes between actions with the same name in different files.
27
31
  * - **String**: Matches by suffix (action name after #). This is convenient
28
- * but may be ambiguous if multiple files export the same action name.
32
+ * but may be ambiguous if multiple files export the same action name: it
33
+ * follows the latest call of any action with that export name.
29
34
  *
30
35
  * @param action - Either a server action function or a string action name.
31
36
  * - **Function**: Must be directly imported in the client component.
@@ -21,21 +21,24 @@ 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.
40
43
  */
41
- export declare function useCommitAnchoredState<T>(init: () => T): [T, (value: T, signal?: RouteStateSignal) => void];
44
+ 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
  *
@@ -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;
@@ -450,6 +463,8 @@ export interface NavigateOptionsInternal extends Omit<NavigateOptions, "state">
450
463
  _skipCache?: boolean;
451
464
  /** Server redirects already followed by this navigation chain (0 for a user navigation) */
452
465
  _redirectHops?: number;
466
+ /** The URL a redirected navigation chain started from: a link to it stays pending (useLinkStatus) */
467
+ _redirectFrom?: string;
453
468
  }
454
469
  /**
455
470
  * Options for useRouter push/replace methods.
@@ -603,6 +618,8 @@ export interface FetchPartialOptions {
603
618
  signal?: AbortSignal;
604
619
  /** If true, this is a stale cache revalidation request - server should force revalidators */
605
620
  staleRevalidation?: boolean;
621
+ /** Sent as `_rsc_boundary` (wire-params.ts WIRE.boundary). */
622
+ boundary?: string;
606
623
  interceptSourceUrl?: string;
607
624
  /** RSC version for cache invalidation detection */
608
625
  version?: string;
@@ -667,7 +684,8 @@ export interface ServerActionBridgeConfig {
667
684
  */
668
685
  export interface NavigationBridge {
669
686
  navigate(url: string, options?: NavigateOptionsInternal): Promise<void>;
670
- refresh(): Promise<void>;
687
+ /** `crossTab`: another tab's write; an action's error boundary stays. */
688
+ refresh(crossTab?: boolean): Promise<void>;
671
689
  /** `event`: the popstate event; a traversal the browser animated itself (hasUAVisualTransition) is not animated again. */
672
690
  handlePopstate(event?: PopStateEvent): Promise<void>;
673
691
  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
@@ -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 is handed to React. A `transition` spec plus a pending
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). useAction().state stays idle: no action runs.
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
- * Back/forward, action and optimistic clientUrls() swap decisions are
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; the tree has one
46
- * location, so no page is restored with it. A server action's state, a
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,
@@ -361,8 +367,37 @@ export interface TestRouterHandle {
361
367
  * `handles`). They arrive on the response's handle stream, so `useHandle`
362
368
  * readers take them with the page's commit: while React holds the page,
363
369
  * readers keep the values of the page being left.
370
+ *
371
+ * `options.streamUntil` gives the navigation a response stream, as a real
372
+ * response has: it opens in a task after the call, before the commit (the
373
+ * response arrives, and `useNavigation().pendingUrl` clears), and stays open
374
+ * until the promise settles (a page that streams a tail past its commit:
375
+ * `isStreaming` reads true until then). Without it the navigation has no
376
+ * stream. A navigation that a newer one or a back/forward aborts before its
377
+ * response arrives opens none and commits nothing; one aborted after its
378
+ * response arrived stops counting as streaming at the abort, as in
379
+ * production, where its response streams on and is ignored.
380
+ *
381
+ * @example
382
+ * const tail = deferred();
383
+ * await router.navigate("/products/2", { streamUntil: tail.promise });
384
+ * // useNavigation(): idle, isStreaming true
385
+ * await act(async () => tail.resolve());
386
+ * // useNavigation(): idle, isStreaming false
387
+ *
388
+ * A navigation started while React holds a page goes inside
389
+ * `await act(async () => ...)`. Its first update makes React retry the held
390
+ * page, which suspends again; a synchronous act() scope drops that retry
391
+ * and React renders no later update of the same priority.
392
+ *
393
+ * @example
394
+ * await act(async () => {
395
+ * await router.navigate("/products/3", { transition: false });
396
+ * });
364
397
  */
365
- navigate(url: string, options?: Pick<RenderRouteOptions, "loaders" | "handles"> & Pick<NavigateOptions, "state" | "replace" | "transition">): Promise<void>;
398
+ navigate(url: string, options?: Pick<RenderRouteOptions, "loaders" | "handles"> & Pick<NavigateOptions, "state" | "replace" | "transition"> & {
399
+ streamUntil?: Promise<unknown>;
400
+ }): Promise<void>;
366
401
  /**
367
402
  * Re-render the current location, as router.refresh() does: a spec's
368
403
  * transition({ when }) decides it with kind "revalidate" (`to` is `from`).
@@ -81,7 +81,13 @@ export interface ServeShellRequestOptions<TEnv = any> {
81
81
  * and segments take an action's revalidation defaults; it renders live, as
82
82
  * an action does, so neither the route cache nor the document cache answers
83
83
  * or stores it, and a `ppr` route's `replayStatus` is
84
- * `{ outcome: "BYPASS", reason: "stale" }`.
84
+ * `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error
85
+ * segment of a thrown action's boundary the page at `from` shows
86
+ * (`_rsc_boundary`), as the follow-up an earlier action owes that page
87
+ * sends it, with `stale: true` and no `segments`: the response renders
88
+ * every segment above the boundary and nothing at or below it, claims the
89
+ * boundary in `matched` and sends no segment for it. The server reads it
90
+ * only beside `stale`.
85
91
  */
86
92
  partial?: true | {
87
93
  from?: string;
@@ -91,6 +97,7 @@ export interface ServeShellRequestOptions<TEnv = any> {
91
97
  fill?: boolean;
92
98
  interceptSource?: string;
93
99
  stale?: boolean;
100
+ boundary?: string;
94
101
  };
95
102
  /**
96
103
  * POST this form as a submit before hydration: the no-JS request the
@@ -67,19 +67,38 @@ export interface OnErrorContext<TEnv = any> {
67
67
  actionId?: string;
68
68
  /** Environment/bindings (platform context) */
69
69
  env?: TEnv;
70
- /** Duration from request start to error (milliseconds) */
70
+ /**
71
+ * Duration from request start to error (milliseconds). For a `background`
72
+ * error it counts from the start of the request that started the work, so
73
+ * it includes that response and any wait before the work ran.
74
+ */
71
75
  duration?: number;
72
76
  /**
73
77
  * Whether this is a partial/navigation request, on an error the segment
74
- * walk catches (a handler that throws, a DSL loader that rejects): `true`
75
- * for a navigation, unset for a document. An error in the background
76
- * re-render of a `cache()` record (a stale HIT's refresh, the write of a
77
- * navigation that kept a layout mounted) reports the request that started
78
- * it. A streamed (`loading()`) handler's rejection does not carry it, in
79
- * either render; the `routing` phase reports `false`.
78
+ * walk catches (a handler that throws, a streamed `loading()` handler that
79
+ * rejects, a DSL loader that rejects): `true` for a navigation, unset for a
80
+ * document. A `cache()` record's refresh or proactive write reports the
81
+ * request that started it; a PPR shell capture renders a document. The
82
+ * `routing` phase reports `false`.
80
83
  */
81
84
  isPartial?: boolean;
82
- /** Whether an error boundary caught the error */
85
+ /**
86
+ * `true` when the error came from work whose output goes to a cache and
87
+ * never to the response of the request that started it: a stale `cache()`
88
+ * record's refresh, the write of a navigation that kept a layout mounted, a
89
+ * stale document cache entry's re-run, a PPR shell capture, or a stale
90
+ * `"use cache"` function's or cached loader's refresh, the work's own
91
+ * failure included. `request` and `url` are the starting request's (a
92
+ * shell capture's handler errors name the page it captures). A
93
+ * `router.prerender()` warm's own render is its request's: unset, while a
94
+ * shell capture it makes is `true`. Unset for every other error.
95
+ */
96
+ background?: boolean;
97
+ /**
98
+ * Whether an error boundary caught the error and its fallback reached the
99
+ * response. `false` on a `background` error: the failed render is never
100
+ * stored, so no visitor sees the fallback.
101
+ */
83
102
  handledByBoundary?: boolean;
84
103
  /** Stack trace (if available) */
85
104
  stack?: string;