@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
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../deps/rsc-client.js";
|
|
23
23
|
import {
|
|
24
24
|
getRequestContext,
|
|
25
|
+
runAsBackgroundWork,
|
|
25
26
|
type RequestContext,
|
|
26
27
|
} from "../server/request-context.js";
|
|
27
28
|
import { isUnderTestRunner } from "../runtime-env.js";
|
|
@@ -75,7 +76,6 @@ import {
|
|
|
75
76
|
executionStart,
|
|
76
77
|
invalidatedSince,
|
|
77
78
|
predatesInvalidation,
|
|
78
|
-
runAsBackgroundRefresh,
|
|
79
79
|
type ExecutionStart,
|
|
80
80
|
} from "./tag-invalidation.js";
|
|
81
81
|
import type { CacheItemResult } from "./types.js";
|
|
@@ -727,22 +727,22 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
727
727
|
// user's value into the shared entry — with zero visibility to the
|
|
728
728
|
// concurrent foreground.
|
|
729
729
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
observePhase(
|
|
730
|
+
// Re-establish the request-context ALS so a "use cache" body that
|
|
731
|
+
// reads the ambient getRequestContext() (e.g.
|
|
732
|
+
// getRequestContext().env.ApiKey) resolves during the background
|
|
733
|
+
// revalidation instead of throwing "called outside of a request
|
|
734
|
+
// context". runAsBackgroundWork sets the store for fn's synchronous
|
|
735
|
+
// kickoff; its async continuations inherit it. No visitor made the
|
|
736
|
+
// invalidations it runs (no fresh-reads cookie), and onError marks
|
|
737
|
+
// its errors `background`, the failure caught below included.
|
|
738
|
+
//
|
|
739
|
+
// The span opens inside the request ALS and wraps the WHOLE task —
|
|
740
|
+
// fn kickoff through the store write — so its platform spans nest
|
|
741
|
+
// under rango.background and a slow or failing setItem is part of
|
|
742
|
+
// the traced revalidation, not an untraced tail.
|
|
743
|
+
await runAsBackgroundWork(requestCtx, async () => {
|
|
744
|
+
try {
|
|
745
|
+
await observePhase(
|
|
746
746
|
PHASES.background("use-cache-revalidation"),
|
|
747
747
|
async () => {
|
|
748
748
|
const refreshArgs = args.map(refreshView);
|
|
@@ -796,20 +796,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
796
796
|
noteWarmWrite(requestCtx, "item");
|
|
797
797
|
}
|
|
798
798
|
},
|
|
799
|
-
)
|
|
800
|
-
)
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
requestCtx,
|
|
811
|
-
);
|
|
812
|
-
}
|
|
799
|
+
);
|
|
800
|
+
} catch (bgError) {
|
|
801
|
+
bgCapture?.stop();
|
|
802
|
+
reportCacheError(
|
|
803
|
+
bgError,
|
|
804
|
+
"stale-revalidation",
|
|
805
|
+
"[use cache] background revalidation failed",
|
|
806
|
+
requestCtx,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
813
810
|
}),
|
|
814
811
|
);
|
|
815
812
|
return result;
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
createRequestContext,
|
|
24
24
|
getRequestContext,
|
|
25
|
-
|
|
25
|
+
runAsBackgroundWork,
|
|
26
26
|
type RequestContext,
|
|
27
27
|
} from "../server/request-context.js";
|
|
28
28
|
import { deriveRenderPass } from "../server/render-pass.js";
|
|
@@ -378,24 +378,25 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
378
378
|
}),
|
|
379
379
|
});
|
|
380
380
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
381
|
+
// Re-establish the request-context ALS around the whole background
|
|
382
|
+
// task: next() re-runs the full handler pipeline, and on workerd a
|
|
383
|
+
// waitUntil task runs detached from the request's I/O context, so a
|
|
384
|
+
// handler/component reading getRequestContext() would otherwise
|
|
385
|
+
// throw. Same fix as the route-level/use-cache background
|
|
386
|
+
// revalidation paths. The rango.background span (kind=
|
|
387
|
+
// document-revalidation) wraps the re-render AND the store write so
|
|
388
|
+
// the task's spans — the re-run's own rango.* set and the drain/put
|
|
389
|
+
// platform spans — nest under one explanatory parent instead of
|
|
390
|
+
// dangling under the ended foreground phases. Running the put
|
|
391
|
+
// inside the ALS matches the MISS path, where putResponse already
|
|
392
|
+
// executes within the foreground request context. No visitor made
|
|
393
|
+
// the invalidations the re-run makes: the pass's fresh-reads
|
|
394
|
+
// writer is the sink's, which sets nothing. onError marks the
|
|
395
|
+
// re-run's errors `background`, the failure caught below included.
|
|
396
|
+
runBackground(requestCtx, () =>
|
|
397
|
+
runAsBackgroundWork(rerunCtx, async () => {
|
|
398
|
+
try {
|
|
399
|
+
await observePhase(
|
|
399
400
|
PHASES.background("document-revalidation"),
|
|
400
401
|
async () => {
|
|
401
402
|
const fresh = await next();
|
|
@@ -425,24 +426,20 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
425
426
|
`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`,
|
|
426
427
|
);
|
|
427
428
|
},
|
|
428
|
-
)
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
requestCtx,
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
});
|
|
429
|
+
);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
// The route now rewrites (route middleware): nothing to refresh
|
|
432
|
+
// this entry with.
|
|
433
|
+
if (isRewrite(error)) return;
|
|
434
|
+
reportCacheError(
|
|
435
|
+
error,
|
|
436
|
+
"cache-write",
|
|
437
|
+
"[DocumentCache] revalidation",
|
|
438
|
+
requestCtx,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
446
443
|
|
|
447
444
|
return drainOnResponseCallbacks(
|
|
448
445
|
addCacheStatusHeader(cached.response, "STALE"),
|
|
@@ -117,29 +117,31 @@ export async function readThroughItem<T>(
|
|
|
117
117
|
// span cover the write too (a slow or failing store write is part of
|
|
118
118
|
// the revalidation, not an untraced tail). The foreground miss below
|
|
119
119
|
// stays unwrapped: its context is already established.
|
|
120
|
+
// Reported inside the task, within wrapBackground's frame, so onError
|
|
121
|
+
// marks it `background`; rethrown so the span records the failure.
|
|
120
122
|
const revalidateTask = async () => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
try {
|
|
124
|
+
const fresh = await execute();
|
|
125
|
+
const serialized = await serialize(fresh);
|
|
126
|
+
if (serialized !== null) {
|
|
127
|
+
await setItem(key, serialized, storeOptions);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
reportCacheError(
|
|
131
|
+
error,
|
|
132
|
+
"stale-revalidation",
|
|
133
|
+
"[read-through] background revalidation",
|
|
134
|
+
host ?? undefined,
|
|
135
|
+
);
|
|
136
|
+
throw error;
|
|
125
137
|
}
|
|
126
138
|
};
|
|
127
139
|
runBackground(
|
|
128
140
|
host,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: revalidateTask());
|
|
134
|
-
} catch (error) {
|
|
135
|
-
reportCacheError(
|
|
136
|
-
error,
|
|
137
|
-
"stale-revalidation",
|
|
138
|
-
"[read-through] background revalidation",
|
|
139
|
-
host ?? undefined,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
},
|
|
141
|
+
() =>
|
|
142
|
+
(wrapBackground ? wrapBackground(revalidateTask) : revalidateTask())
|
|
143
|
+
// Reported above.
|
|
144
|
+
.catch(() => {}),
|
|
143
145
|
true,
|
|
144
146
|
);
|
|
145
147
|
return data;
|
|
@@ -36,11 +36,9 @@
|
|
|
36
36
|
* predatesInvalidation, and docs/design/caching.md "Read-your-own-writes".
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
40
39
|
import {
|
|
41
40
|
_getRequestContext,
|
|
42
|
-
|
|
43
|
-
type RequestContext,
|
|
41
|
+
isBackgroundWork,
|
|
44
42
|
} from "../server/request-context.js";
|
|
45
43
|
import { reportingAsync } from "./cache-error.js";
|
|
46
44
|
import { normalizeTags } from "./cache-tag.js";
|
|
@@ -134,20 +132,6 @@ function warnPartialTagStore(fn: string, incapable: number): void {
|
|
|
134
132
|
);
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
const backgroundRefresh = new AsyncLocalStorage<true>();
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Run a stale entry's background refresh on the visitor's request context: a
|
|
141
|
-
* "use cache" function's (cache-runtime.ts) or a cached loader's
|
|
142
|
-
* (loader-cache.ts). No visitor made the invalidations in it, so they set no
|
|
143
|
-
* fresh-reads cookie, as in a stale cache() HIT's re-render or a document
|
|
144
|
-
* cache entry's re-run, whose passes take a sink's no-op writer
|
|
145
|
-
* (server/render-pass.ts).
|
|
146
|
-
*/
|
|
147
|
-
export function runAsBackgroundRefresh<T>(ctx: RequestContext, fn: () => T): T {
|
|
148
|
-
return runWithRequestContext(ctx, () => backgroundRefresh.run(true, fn));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
135
|
/**
|
|
152
136
|
* Set the fresh-reads cookie on this request's response for as long as the
|
|
153
137
|
* invalidated stores' isolate memos can stay stale (isolate-tag-memo.ts), so
|
|
@@ -155,7 +139,8 @@ export function runAsBackgroundRefresh<T>(ctx: RequestContext, fn: () => T): T {
|
|
|
155
139
|
*/
|
|
156
140
|
function markFreshReads(stores: SegmentCacheStore[]): void {
|
|
157
141
|
const ctx = _getRequestContext();
|
|
158
|
-
|
|
142
|
+
// No visitor made an invalidation in background work (runAsBackgroundWork).
|
|
143
|
+
if (!ctx?._setFreshReadsCookie || isBackgroundWork()) return;
|
|
159
144
|
let windowMs = 0;
|
|
160
145
|
for (const store of stores) {
|
|
161
146
|
windowMs = Math.max(windowMs, store.freshReadsWindowMs ?? 0);
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import { createElement, type ReactNode } from "react";
|
|
9
9
|
import type { EntryData } from "../server/context.js";
|
|
10
10
|
import { runIdentityExempt } from "../cache/cache-exec-scope.js";
|
|
11
|
+
import {
|
|
12
|
+
_getRequestContext,
|
|
13
|
+
isBackgroundWork,
|
|
14
|
+
} from "../server/request-context.js";
|
|
11
15
|
import type {
|
|
12
16
|
ResolvedSegment,
|
|
13
17
|
ErrorInfo,
|
|
@@ -39,7 +43,7 @@ export interface InvokeOnErrorContext<TEnv = any> {
|
|
|
39
43
|
isPartial?: boolean;
|
|
40
44
|
handledByBoundary?: boolean;
|
|
41
45
|
metadata?: Record<string, unknown>;
|
|
42
|
-
/**
|
|
46
|
+
/** performance.now() at request entry; the request context's by default. */
|
|
43
47
|
requestStartTime?: number;
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -50,6 +54,12 @@ export interface InvokeOnErrorContext<TEnv = any> {
|
|
|
50
54
|
* This is a shared utility used by both the router and RSC handler to ensure
|
|
51
55
|
* consistent error callback behavior across the codebase.
|
|
52
56
|
*
|
|
57
|
+
* `background` is set inside runAsBackgroundWork (server/request-context.ts),
|
|
58
|
+
* and `duration` and `env` come from the request context on the ALS
|
|
59
|
+
* (`_handlerStart`, `env`) when the call site leaves them out, so no call
|
|
60
|
+
* site has to know. A Prerender pass's ctx.env throws without buildEnv, so
|
|
61
|
+
* the walk never reads it eagerly.
|
|
62
|
+
*
|
|
53
63
|
* @param onError - The onError callback to invoke (may be undefined)
|
|
54
64
|
* @param error - The error that occurred
|
|
55
65
|
* @param phase - The phase where the error occurred
|
|
@@ -66,9 +76,10 @@ export function invokeOnError<TEnv = any>(
|
|
|
66
76
|
if (!onError) return;
|
|
67
77
|
|
|
68
78
|
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
const reqCtx = _getRequestContext();
|
|
80
|
+
const background = isBackgroundWork() || undefined;
|
|
81
|
+
const start = context.requestStartTime ?? reqCtx?._handlerStart;
|
|
82
|
+
const duration = start !== undefined ? performance.now() - start : undefined;
|
|
72
83
|
|
|
73
84
|
const errorContext: OnErrorContext<TEnv> = {
|
|
74
85
|
error: errorObj,
|
|
@@ -84,10 +95,11 @@ export function invokeOnError<TEnv = any>(
|
|
|
84
95
|
loaderName: context.loaderName,
|
|
85
96
|
middlewareId: context.middlewareId,
|
|
86
97
|
actionId: context.actionId,
|
|
87
|
-
env: context.env,
|
|
98
|
+
env: context.env ?? (reqCtx?.env as TEnv | undefined),
|
|
88
99
|
duration,
|
|
89
100
|
isPartial: context.isPartial,
|
|
90
|
-
|
|
101
|
+
background,
|
|
102
|
+
handledByBoundary: background ? false : context.handledByBoundary,
|
|
91
103
|
stack: errorObj.stack,
|
|
92
104
|
metadata: context.metadata,
|
|
93
105
|
};
|
|
@@ -164,15 +164,16 @@ export function findInterceptForRoute(
|
|
|
164
164
|
/**
|
|
165
165
|
* An intercept handler's throw or notFound() renders the declaring entry's
|
|
166
166
|
* boundary as the slot component (catchSegmentError, as for route handlers on
|
|
167
|
-
* partial navigations: 500/404 status, onError "handler"
|
|
168
|
-
* The background re-render (`background`) reports the
|
|
169
|
-
* then rethrows it, so a failed re-render aborts its
|
|
170
|
-
* storing the fallback.
|
|
167
|
+
* partial navigations: 500/404 status, onError "handler" naming the slot).
|
|
168
|
+
* Responses rethrow. The background re-render (`background`) reports the
|
|
169
|
+
* error the same way and then rethrows it, so a failed re-render aborts its
|
|
170
|
+
* cache write instead of storing the fallback.
|
|
171
171
|
*/
|
|
172
172
|
function renderInterceptHandlerError<TEnv>(
|
|
173
173
|
pass: ResolutionPass<TEnv>,
|
|
174
174
|
error: unknown,
|
|
175
175
|
parentEntry: EntryData,
|
|
176
|
+
slotSegmentId: string,
|
|
176
177
|
background: boolean | undefined,
|
|
177
178
|
): ReactNode {
|
|
178
179
|
if (error instanceof Response) throw error;
|
|
@@ -193,6 +194,7 @@ function renderInterceptHandlerError<TEnv>(
|
|
|
193
194
|
telemetry,
|
|
194
195
|
},
|
|
195
196
|
pass.context.pathname,
|
|
197
|
+
{ id: slotSegmentId, type: "parallel" },
|
|
196
198
|
);
|
|
197
199
|
if (background) throw error;
|
|
198
200
|
return component;
|
|
@@ -363,6 +365,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
363
365
|
pass,
|
|
364
366
|
error,
|
|
365
367
|
parentEntry,
|
|
368
|
+
slotSegmentId,
|
|
366
369
|
options?.background,
|
|
367
370
|
);
|
|
368
371
|
}
|
|
@@ -402,6 +405,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
402
405
|
context,
|
|
403
406
|
routeKey: interceptEntry.routeName,
|
|
404
407
|
params,
|
|
408
|
+
isPartial: true,
|
|
405
409
|
recover: !options?.background,
|
|
406
410
|
});
|
|
407
411
|
|
|
@@ -429,6 +433,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
429
433
|
pass,
|
|
430
434
|
error,
|
|
431
435
|
parentEntry,
|
|
436
|
+
slotSegmentId,
|
|
432
437
|
options?.background,
|
|
433
438
|
);
|
|
434
439
|
}
|
package/src/router/match-api.ts
CHANGED
|
@@ -514,6 +514,7 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
514
514
|
clientSegmentIds: nav.clientSegmentIds,
|
|
515
515
|
clientSegmentSet,
|
|
516
516
|
stale: nav.stale,
|
|
517
|
+
heldBoundary: nav.heldBoundary,
|
|
517
518
|
prevUrl: nav.prevUrl,
|
|
518
519
|
prevParams: nav.prevParams,
|
|
519
520
|
prevMatch: nav.prevMatch,
|
|
@@ -136,6 +136,8 @@ export interface MatchContext<TEnv = any> {
|
|
|
136
136
|
clientSegmentIds: string[];
|
|
137
137
|
clientSegmentSet: Set<string>;
|
|
138
138
|
stale: boolean;
|
|
139
|
+
/** NavigationSnapshot.heldBoundary: the walk stops above it. */
|
|
140
|
+
heldBoundary?: string;
|
|
139
141
|
|
|
140
142
|
// Previous navigation state
|
|
141
143
|
prevUrl: URL;
|
|
@@ -219,6 +221,7 @@ export function resolutionPassInput<TEnv>(
|
|
|
219
221
|
routeKey: ctx.routeKey,
|
|
220
222
|
actionContext: ctx.actionContext,
|
|
221
223
|
stale: ctx.stale,
|
|
224
|
+
heldBoundary: ctx.heldBoundary,
|
|
222
225
|
};
|
|
223
226
|
}
|
|
224
227
|
|
|
@@ -113,7 +113,7 @@ import { getContext } from "../../server/context.js";
|
|
|
113
113
|
import { executionStart } from "../../cache/tag-invalidation.js";
|
|
114
114
|
import {
|
|
115
115
|
createRequestContext,
|
|
116
|
-
|
|
116
|
+
runAsBackgroundWork,
|
|
117
117
|
type RequestContext,
|
|
118
118
|
} from "../../server/request-context.js";
|
|
119
119
|
import { deriveRenderPass } from "../../server/render-pass.js";
|
|
@@ -141,8 +141,9 @@ import {
|
|
|
141
141
|
* each field and why. The DSL store below has `metrics` unset too (track()
|
|
142
142
|
* reads that one), so nothing lands on the foreground's perf timeline;
|
|
143
143
|
* ctx.Store.run closes over ctx.Store, so the derived store needs its own run.
|
|
144
|
-
*
|
|
145
|
-
* task on workerd loses
|
|
144
|
+
* runAsBackgroundWork also re-establishes the request ALS, which a waitUntil
|
|
145
|
+
* task on workerd loses, and marks the pass's onError reports `background`;
|
|
146
|
+
* the DSL store is a different ALS (build context).
|
|
146
147
|
*/
|
|
147
148
|
export async function rerenderAndCacheRoute<TEnv>(
|
|
148
149
|
ctx: MatchContext<TEnv>,
|
|
@@ -172,7 +173,7 @@ export async function rerenderAndCacheRoute<TEnv>(
|
|
|
172
173
|
store.parent,
|
|
173
174
|
fn,
|
|
174
175
|
);
|
|
175
|
-
return
|
|
176
|
+
return runAsBackgroundWork(renderCtx, async () => {
|
|
176
177
|
const handlerContext = routerCtx.createHandlerContext(
|
|
177
178
|
ctx.matched.params,
|
|
178
179
|
ctx.request,
|
|
@@ -431,13 +431,15 @@ async function* serveStoredSegments<TEnv>(
|
|
|
431
431
|
// route is the same case, served from the overlay first (an overlay entry
|
|
432
432
|
// is newer than the build one, and may be the only one). Passthrough
|
|
433
433
|
// routes keep a liveHandler, so a live render (rendersLive) re-renders
|
|
434
|
-
// them fresh. A
|
|
435
|
-
// (ActionContext.boundary)
|
|
434
|
+
// them fresh. A pass cut at an error boundary walks live too
|
|
435
|
+
// (ActionContext.boundary, a follow-up's held one): the evicted handler is
|
|
436
|
+
// under the cut.
|
|
436
437
|
if (
|
|
437
438
|
(prerenderStoreShortCircuits(ctx.matched.pr, ctx.request) ||
|
|
438
439
|
overlayEligible) &&
|
|
439
440
|
(!rendersLive(ctx) || !isPassthroughPrerenderRoute) &&
|
|
440
|
-
!ctx.actionContext?.boundary
|
|
441
|
+
!ctx.actionContext?.boundary &&
|
|
442
|
+
!ctx.heldBoundary
|
|
441
443
|
) {
|
|
442
444
|
await ensurePrerenderDeps();
|
|
443
445
|
stored = await readPrerenderSource(
|
|
@@ -18,6 +18,13 @@ export interface NavigationSnapshot {
|
|
|
18
18
|
filteredSegmentIds: string[];
|
|
19
19
|
|
|
20
20
|
stale: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The id of an action's error boundary on screen that a stale request holds
|
|
23
|
+
* (`_rsc_boundary`, the action follow-up): read only beside `stale`, so it
|
|
24
|
+
* has every stale guarantee (a live render, no route-cache or document-cache
|
|
25
|
+
* read or store, no PPR replay).
|
|
26
|
+
*/
|
|
27
|
+
heldBoundary?: string;
|
|
21
28
|
|
|
22
29
|
isSameRouteNavigation: boolean;
|
|
23
30
|
|
|
@@ -136,6 +143,8 @@ export async function resolveNavigation(
|
|
|
136
143
|
const clientSegmentIds =
|
|
137
144
|
url.searchParams.get(WIRE.segments)?.split(",").filter(Boolean) || [];
|
|
138
145
|
const stale = url.searchParams.get(WIRE.stale) === "true";
|
|
146
|
+
const heldBoundary =
|
|
147
|
+
(stale && url.searchParams.get(WIRE.boundary)) || undefined;
|
|
139
148
|
const previousUrl = getNavigationContextHeader(request, url.origin);
|
|
140
149
|
const interceptSourceUrl = getInterceptSourceHeader(request);
|
|
141
150
|
const isHmr = !!requestHeaders(request).get("X-RSC-HMR");
|
|
@@ -198,6 +207,7 @@ export async function resolveNavigation(
|
|
|
198
207
|
clientSegmentSet,
|
|
199
208
|
filteredSegmentIds,
|
|
200
209
|
stale,
|
|
210
|
+
heldBoundary,
|
|
201
211
|
isSameRouteNavigation,
|
|
202
212
|
effectiveFromUrl,
|
|
203
213
|
effectiveFromMatch,
|
|
@@ -204,15 +204,49 @@ export interface ErrorReportContext {
|
|
|
204
204
|
routeKey?: string;
|
|
205
205
|
env?: any;
|
|
206
206
|
isPartial?: boolean;
|
|
207
|
-
requestStartTime?: number;
|
|
208
207
|
telemetry?: TelemetrySink;
|
|
209
208
|
}
|
|
210
209
|
|
|
210
|
+
type Thrower = { id: string; type: ErrorInfo["segmentType"] };
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Note that `id` threw `error`, for an error its owning entry's boundary
|
|
214
|
+
* catches (a parallel slot's sync throw, an orphan layout's): onError names
|
|
215
|
+
* the segment that threw, as a streamed slot's rejection does. Kept on the
|
|
216
|
+
* request context, never the module: a reused error object (a constant, a
|
|
217
|
+
* memoized rejection) can be thrown by another request, or by a Prerender
|
|
218
|
+
* pass that rethrows before catchSegmentError reads it. Returns `error`, to
|
|
219
|
+
* rethrow.
|
|
220
|
+
*/
|
|
221
|
+
export function noteThrower(
|
|
222
|
+
error: unknown,
|
|
223
|
+
id: string,
|
|
224
|
+
type: ErrorInfo["segmentType"],
|
|
225
|
+
): unknown {
|
|
226
|
+
const reqCtx = _getRequestContext();
|
|
227
|
+
if (reqCtx && error !== null && typeof error === "object") {
|
|
228
|
+
(reqCtx._throwers ??= new WeakMap()).set(error, { id, type });
|
|
229
|
+
}
|
|
230
|
+
return error;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The segment noteThrower recorded for `error`, read once. */
|
|
234
|
+
function takeThrower(error: unknown): Thrower | undefined {
|
|
235
|
+
const throwers = _getRequestContext()?._throwers;
|
|
236
|
+
if (!throwers || error === null || typeof error !== "object") return;
|
|
237
|
+
const noted = throwers.get(error) as Thrower | undefined;
|
|
238
|
+
throwers.delete(error);
|
|
239
|
+
return noted;
|
|
240
|
+
}
|
|
241
|
+
|
|
211
242
|
/**
|
|
212
243
|
* Handle a caught error during segment resolution by creating an
|
|
213
244
|
* error or not-found segment with the nearest boundary.
|
|
214
245
|
*
|
|
215
246
|
* Called by resolveWithErrorBoundary to produce error/notFound segments.
|
|
247
|
+
* `entry` is where the boundary is looked up; `thrower` names the segment
|
|
248
|
+
* that threw when it is not `entry` (an intercept's slot, under its declaring
|
|
249
|
+
* entry), for the report, the server print and the client's ErrorInfo.
|
|
216
250
|
*/
|
|
217
251
|
export function catchSegmentError<TEnv>(
|
|
218
252
|
error: unknown,
|
|
@@ -221,7 +255,11 @@ export function catchSegmentError<TEnv>(
|
|
|
221
255
|
deps: SegmentResolutionDeps<TEnv>,
|
|
222
256
|
report?: ErrorReportContext,
|
|
223
257
|
pathname?: string,
|
|
258
|
+
thrower?: Thrower,
|
|
224
259
|
): ResolvedSegment {
|
|
260
|
+
const named = thrower ?? takeThrower(error);
|
|
261
|
+
const segmentId = named?.id ?? entry.shortCode;
|
|
262
|
+
const segmentType: ErrorInfo["segmentType"] = named?.type ?? entry.type;
|
|
225
263
|
const reportError = (
|
|
226
264
|
handledByBoundary: boolean,
|
|
227
265
|
metadata?: Record<string, unknown>,
|
|
@@ -232,13 +270,12 @@ export function catchSegmentError<TEnv>(
|
|
|
232
270
|
url: report.url,
|
|
233
271
|
routeKey: report.routeKey,
|
|
234
272
|
params,
|
|
235
|
-
segmentId
|
|
236
|
-
segmentType:
|
|
273
|
+
segmentId,
|
|
274
|
+
segmentType: segmentType as any,
|
|
237
275
|
env: report.env,
|
|
238
276
|
isPartial: report.isPartial,
|
|
239
277
|
handledByBoundary,
|
|
240
278
|
metadata,
|
|
241
|
-
requestStartTime: report.requestStartTime,
|
|
242
279
|
});
|
|
243
280
|
if (report.telemetry) {
|
|
244
281
|
const errorObj =
|
|
@@ -247,8 +284,8 @@ export function catchSegmentError<TEnv>(
|
|
|
247
284
|
type: "handler.error",
|
|
248
285
|
timestamp: performance.now(),
|
|
249
286
|
requestId: report.request ? getRequestId(report.request) : undefined,
|
|
250
|
-
segmentId
|
|
251
|
-
segmentType
|
|
287
|
+
segmentId,
|
|
288
|
+
segmentType,
|
|
252
289
|
error: errorObj,
|
|
253
290
|
handledByBoundary,
|
|
254
291
|
pathname,
|
|
@@ -274,8 +311,8 @@ export function catchSegmentError<TEnv>(
|
|
|
274
311
|
|
|
275
312
|
const notFoundInfo = createNotFoundInfo(
|
|
276
313
|
error,
|
|
277
|
-
|
|
278
|
-
|
|
314
|
+
segmentId,
|
|
315
|
+
segmentType,
|
|
279
316
|
pathname,
|
|
280
317
|
);
|
|
281
318
|
|
|
@@ -285,7 +322,7 @@ export function catchSegmentError<TEnv>(
|
|
|
285
322
|
});
|
|
286
323
|
|
|
287
324
|
debugLog("segment", "notFound boundary handled error", {
|
|
288
|
-
segmentId
|
|
325
|
+
segmentId,
|
|
289
326
|
message: notFoundInfo.message,
|
|
290
327
|
});
|
|
291
328
|
|
|
@@ -302,19 +339,18 @@ export function catchSegmentError<TEnv>(
|
|
|
302
339
|
// The client gets the masked ErrorInfo; the original prints here.
|
|
303
340
|
printServerError(
|
|
304
341
|
error,
|
|
305
|
-
{ label: "Handler error", name:
|
|
342
|
+
{ label: "Handler error", name: segmentId, pathname },
|
|
306
343
|
_getRequestContext(),
|
|
307
344
|
);
|
|
308
345
|
|
|
309
346
|
const fallback = deps.findNearestErrorBoundary(entry);
|
|
310
|
-
const
|
|
311
|
-
const errorInfo = createErrorInfo(error, entry.shortCode, segmentType);
|
|
347
|
+
const errorInfo = createErrorInfo(error, segmentId, segmentType);
|
|
312
348
|
const effectiveFallback = fallback ?? DefaultErrorFallback;
|
|
313
349
|
|
|
314
350
|
reportError(!!effectiveFallback);
|
|
315
351
|
|
|
316
352
|
debugLog("segment", "error boundary handled error", {
|
|
317
|
-
segmentId
|
|
353
|
+
segmentId,
|
|
318
354
|
boundary: fallback ? "custom" : "default",
|
|
319
355
|
message: errorInfo.message,
|
|
320
356
|
});
|