@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
|
@@ -72,6 +72,7 @@ import { runIdentityExempt } from "../../cache/cache-exec-scope.js";
|
|
|
72
72
|
import {
|
|
73
73
|
getRequestContext,
|
|
74
74
|
_getRequestContext,
|
|
75
|
+
runAsBackgroundWork,
|
|
75
76
|
type RequestContext,
|
|
76
77
|
} from "../../server/request-context.js";
|
|
77
78
|
import { observePhase, PHASES } from "../instrument.js";
|
|
@@ -92,7 +93,6 @@ import { isWarmReplace, noteWarmWrite } from "../../prerender/warm-request.js";
|
|
|
92
93
|
import {
|
|
93
94
|
executionStart,
|
|
94
95
|
predatesInvalidation,
|
|
95
|
-
runAsBackgroundRefresh,
|
|
96
96
|
} from "../../cache/tag-invalidation.js";
|
|
97
97
|
import {
|
|
98
98
|
maskNestedContainerThenables,
|
|
@@ -815,10 +815,11 @@ function executeLoaderData<TEnv>(
|
|
|
815
815
|
// under the ended foreground phases. Inside the request context so
|
|
816
816
|
// observePhase can read tracing on workerd (ALS detaches in waitUntil).
|
|
817
817
|
// No visitor made the invalidations it runs: they set no fresh-reads
|
|
818
|
-
// cookie
|
|
818
|
+
// cookie, and onError marks its errors `background`
|
|
819
|
+
// (runAsBackgroundWork).
|
|
819
820
|
wrapBackground: (run) => {
|
|
820
821
|
revalidating = true;
|
|
821
|
-
return
|
|
822
|
+
return runAsBackgroundWork(requestCtxForExecute, () =>
|
|
822
823
|
observePhase(PHASES.background("loader-revalidation"), run),
|
|
823
824
|
);
|
|
824
825
|
},
|
|
@@ -51,6 +51,8 @@ export interface ResolutionPassInput<TEnv = any> {
|
|
|
51
51
|
readonly routeKey: string;
|
|
52
52
|
readonly actionContext?: ActionContext;
|
|
53
53
|
readonly stale?: boolean;
|
|
54
|
+
/** NavigationSnapshot.heldBoundary: the walk cuts there, sending no segment. */
|
|
55
|
+
readonly heldBoundary?: string;
|
|
54
56
|
/**
|
|
55
57
|
* No loader is started and no loader segment is emitted: build time
|
|
56
58
|
* (prerender-match.ts) and the background re-render of a route record
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
resolveLayoutComponent,
|
|
63
63
|
resolveWithErrorBoundary,
|
|
64
64
|
buildLoaderErrorContext,
|
|
65
|
+
noteThrower,
|
|
65
66
|
} from "./helpers.js";
|
|
66
67
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
67
68
|
import { isLayoutLoaderCopy } from "../../client-urls/layout-loader-copies.js";
|
|
@@ -801,28 +802,34 @@ async function resolveParallelSlotComponent<TEnv>(
|
|
|
801
802
|
handler,
|
|
802
803
|
slotContext,
|
|
803
804
|
);
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if (
|
|
807
|
-
|
|
808
|
-
result
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
805
|
+
// A throw before the slot streams reaches the owning entry's boundary.
|
|
806
|
+
try {
|
|
807
|
+
if (hasLoadingFallback) {
|
|
808
|
+
const result = run();
|
|
809
|
+
if (result instanceof Promise) {
|
|
810
|
+
warnOnStreamedResponse(result, parallelId);
|
|
811
|
+
result.finally(doneHandler).catch(() => {});
|
|
812
|
+
component = trackStreamedHandler(deps, result, {
|
|
813
|
+
boundaryEntry,
|
|
814
|
+
segmentId: parallelId,
|
|
815
|
+
segmentType: "parallel",
|
|
816
|
+
context,
|
|
817
|
+
routeKey,
|
|
818
|
+
params,
|
|
819
|
+
isPartial: reportsPartial(pass),
|
|
820
|
+
recover: !pass.throwOnError,
|
|
821
|
+
});
|
|
822
|
+
} else {
|
|
823
|
+
doneHandler();
|
|
824
|
+
component = result as ReactNode;
|
|
825
|
+
}
|
|
818
826
|
} else {
|
|
827
|
+
// A static value (a Promise included) goes to render as is.
|
|
828
|
+
component = typeof handler === "function" ? await run() : handler;
|
|
819
829
|
doneHandler();
|
|
820
|
-
component = result as ReactNode;
|
|
821
830
|
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
component = typeof handler === "function" ? await run() : handler;
|
|
825
|
-
doneHandler();
|
|
831
|
+
} catch (error) {
|
|
832
|
+
throw noteThrower(error, parallelId, "parallel");
|
|
826
833
|
}
|
|
827
834
|
}
|
|
828
835
|
}
|
|
@@ -1180,6 +1187,7 @@ async function resolveRouteComponent<TEnv>(
|
|
|
1180
1187
|
context,
|
|
1181
1188
|
routeKey,
|
|
1182
1189
|
params,
|
|
1190
|
+
isPartial: reportsPartial(pass),
|
|
1183
1191
|
recover: !pass.throwOnError,
|
|
1184
1192
|
}),
|
|
1185
1193
|
};
|
|
@@ -1504,6 +1512,9 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
1504
1512
|
const doneHandler = track(`handler:${orphan.id}`, 2);
|
|
1505
1513
|
try {
|
|
1506
1514
|
component = await resolveLayoutComponent(orphan, context);
|
|
1515
|
+
} catch (error) {
|
|
1516
|
+
// Its owning entry's boundary catches it.
|
|
1517
|
+
throw noteThrower(error, orphan.shortCode, orphan.type);
|
|
1507
1518
|
} finally {
|
|
1508
1519
|
internal._currentSegmentId = pinned;
|
|
1509
1520
|
}
|
|
@@ -1598,6 +1609,15 @@ export interface RevalidatingWalkOptions {
|
|
|
1598
1609
|
readonly chain?: readonly EntryData[];
|
|
1599
1610
|
}
|
|
1600
1611
|
|
|
1612
|
+
/**
|
|
1613
|
+
* The onError `isPartial` of an error the walk catches. FRESH: a full match
|
|
1614
|
+
* reports it unset. A re-render a navigation started reports it true
|
|
1615
|
+
* (partialRequest).
|
|
1616
|
+
*/
|
|
1617
|
+
function reportsPartial(pass: ResolutionPass<any>): true | undefined {
|
|
1618
|
+
return pass.client !== FRESH || pass.partialRequest ? true : undefined;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1601
1621
|
/** An entry's error or not-found segment, as its whole result. */
|
|
1602
1622
|
function boundaryResult(seg: ResolvedSegment): SegmentRevalidationResult {
|
|
1603
1623
|
return { segments: [seg], matchedIds: [seg.id] };
|
|
@@ -1633,14 +1653,12 @@ export async function resolveAllSegments<TEnv>(
|
|
|
1633
1653
|
try {
|
|
1634
1654
|
telemetry = getRouterContext()?.telemetry;
|
|
1635
1655
|
} catch {}
|
|
1636
|
-
//
|
|
1637
|
-
// build-time pass rethrows it (throwOnError). A re-render a navigation
|
|
1638
|
-
// started reports it with isPartial (partialRequest).
|
|
1656
|
+
// A build-time pass rethrows an entry's error (throwOnError).
|
|
1639
1657
|
const report = {
|
|
1640
1658
|
request,
|
|
1641
1659
|
url: context.url,
|
|
1642
1660
|
routeKey,
|
|
1643
|
-
isPartial: pass
|
|
1661
|
+
isPartial: reportsPartial(pass),
|
|
1644
1662
|
telemetry,
|
|
1645
1663
|
};
|
|
1646
1664
|
const fillMatch = {
|
|
@@ -1650,16 +1668,19 @@ export async function resolveAllSegments<TEnv>(
|
|
|
1650
1668
|
};
|
|
1651
1669
|
|
|
1652
1670
|
const boundary = pass.actionContext?.boundary;
|
|
1653
|
-
const
|
|
1671
|
+
const cutId = boundary?.id ?? pass.heldBoundary;
|
|
1672
|
+
let cut = false;
|
|
1654
1673
|
for (const entry of entries) {
|
|
1655
|
-
if (entry.shortCode ===
|
|
1674
|
+
if (entry.shortCode === cutId) {
|
|
1656
1675
|
// The outlet's loader ids stay claimed. _handlerRan makes the browser
|
|
1657
|
-
// clear the outlet's handle values.
|
|
1658
|
-
|
|
1659
|
-
|
|
1676
|
+
// clear the outlet's handle values. A held boundary sends no segment:
|
|
1677
|
+
// the client keeps its own, with its handle values.
|
|
1678
|
+
if (boundary) allSegments.push({ ...boundary, _handlerRan: true });
|
|
1679
|
+
matchedIds.push(cutId);
|
|
1660
1680
|
entry.loader?.forEach((loaderEntry, i) =>
|
|
1661
1681
|
matchedIds.push(`${entry.shortCode}D${i}.${loaderEntry.loader.$$id}`),
|
|
1662
1682
|
);
|
|
1683
|
+
cut = true;
|
|
1663
1684
|
break;
|
|
1664
1685
|
}
|
|
1665
1686
|
if (entry.type === "route" && interceptResult) {
|
|
@@ -1721,6 +1742,14 @@ export async function resolveAllSegments<TEnv>(
|
|
|
1721
1742
|
}
|
|
1722
1743
|
}
|
|
1723
1744
|
|
|
1745
|
+
// A held boundary that names no entry (HMR rewrote the routes since the
|
|
1746
|
+
// page rendered): the page renders whole, as without one.
|
|
1747
|
+
if (pass.heldBoundary && !cut && process.env.NODE_ENV !== "production") {
|
|
1748
|
+
console.warn(
|
|
1749
|
+
`[@rangojs/router] A refetch held the error boundary "${pass.heldBoundary}", ` +
|
|
1750
|
+
`which is not on the route's chain (${routeKey}): the page renders whole.`,
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1724
1753
|
deliverStartedLoaders(context, allSegments);
|
|
1725
1754
|
return { segments: allSegments, matchedIds };
|
|
1726
1755
|
}
|
|
@@ -44,6 +44,8 @@ export interface StreamedHandlerOptions<TEnv> {
|
|
|
44
44
|
context: HandlerContext<any, TEnv>;
|
|
45
45
|
routeKey?: string;
|
|
46
46
|
params: Record<string, string>;
|
|
47
|
+
/** onError's `isPartial`, as the walk reports a sync handler's error. */
|
|
48
|
+
isPartial: true | undefined;
|
|
47
49
|
/** False where the rejection must reach the caller (prerender, intercept background re-render). */
|
|
48
50
|
recover: boolean;
|
|
49
51
|
}
|
|
@@ -64,6 +66,7 @@ export function trackStreamedHandler<TEnv>(
|
|
|
64
66
|
const tracked = deps.trackHandler(result as Promise<ReactNode>, {
|
|
65
67
|
segmentId,
|
|
66
68
|
segmentType,
|
|
69
|
+
isPartial: options.isPartial,
|
|
67
70
|
});
|
|
68
71
|
observeStreamedHandler(
|
|
69
72
|
tracked,
|
package/src/router/types.ts
CHANGED
|
@@ -57,7 +57,6 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
57
57
|
params?: Record<string, string>;
|
|
58
58
|
env?: TEnv;
|
|
59
59
|
isPartial?: boolean;
|
|
60
|
-
requestStartTime?: number;
|
|
61
60
|
},
|
|
62
61
|
/** An `ssr: false` loader on a document or capture render. */
|
|
63
62
|
useDefaultErrorBoundary?: boolean,
|
|
@@ -67,6 +66,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
67
66
|
errorContext?: {
|
|
68
67
|
segmentId?: string;
|
|
69
68
|
segmentType?: string;
|
|
69
|
+
isPartial?: boolean;
|
|
70
70
|
},
|
|
71
71
|
) => Promise<T>;
|
|
72
72
|
findNearestErrorBoundary: (
|
package/src/router.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { createHandleStore, type HandleStore } from "./server/handle-store.js";
|
|
|
44
44
|
import {
|
|
45
45
|
getRequestContext,
|
|
46
46
|
_getRequestContext,
|
|
47
|
+
reportedErrorsOf,
|
|
47
48
|
} from "./server/request-context.js";
|
|
48
49
|
import type {
|
|
49
50
|
ErrorPhase,
|
|
@@ -315,7 +316,8 @@ export function createRouter<TEnv = any>(
|
|
|
315
316
|
* Wrapper for invokeOnError that binds the router's onError callback.
|
|
316
317
|
* Uses the shared utility from router/error-handling.ts for consistent behavior.
|
|
317
318
|
*
|
|
318
|
-
* Deduplicates via per-request WeakSet stored on the ALS request context
|
|
319
|
+
* Deduplicates via per-request WeakSet stored on the ALS request context
|
|
320
|
+
* (background work keeps its own, reportedErrorsOf).
|
|
319
321
|
* A closure-level WeakSet would silently swallow errors if the same object
|
|
320
322
|
* instance is thrown across separate requests (e.g. a singleton error).
|
|
321
323
|
*/
|
|
@@ -325,7 +327,8 @@ export function createRouter<TEnv = any>(
|
|
|
325
327
|
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
326
328
|
): void {
|
|
327
329
|
if (error != null && typeof error === "object") {
|
|
328
|
-
const
|
|
330
|
+
const reqCtx = _getRequestContext();
|
|
331
|
+
const reportedErrors = reqCtx && reportedErrorsOf(reqCtx);
|
|
329
332
|
if (reportedErrors) {
|
|
330
333
|
if (reportedErrors.has(error)) return;
|
|
331
334
|
reportedErrors.add(error);
|
|
@@ -488,6 +491,7 @@ export function createRouter<TEnv = any>(
|
|
|
488
491
|
errorContext?: {
|
|
489
492
|
segmentId?: string;
|
|
490
493
|
segmentType?: string;
|
|
494
|
+
isPartial?: boolean;
|
|
491
495
|
},
|
|
492
496
|
): Promise<T> => {
|
|
493
497
|
// One ALS read serves both the store lookup and the onError closure.
|
|
@@ -520,6 +524,7 @@ export function createRouter<TEnv = any>(
|
|
|
520
524
|
env: reqCtx.env as TEnv,
|
|
521
525
|
segmentId: errorContext?.segmentId,
|
|
522
526
|
segmentType: errorContext?.segmentType as any,
|
|
527
|
+
isPartial: errorContext?.isPartial,
|
|
523
528
|
handledByBoundary: true,
|
|
524
529
|
});
|
|
525
530
|
});
|
|
@@ -542,7 +547,6 @@ export function createRouter<TEnv = any>(
|
|
|
542
547
|
params?: Record<string, string>;
|
|
543
548
|
env?: TEnv;
|
|
544
549
|
isPartial?: boolean;
|
|
545
|
-
requestStartTime?: number;
|
|
546
550
|
},
|
|
547
551
|
useDefaultErrorBoundary?: boolean,
|
|
548
552
|
): Promise<LoaderDataResult<T>> {
|
|
@@ -592,7 +596,6 @@ export function createRouter<TEnv = any>(
|
|
|
592
596
|
env: errorContext.env,
|
|
593
597
|
isPartial: errorContext.isPartial,
|
|
594
598
|
handledByBoundary: ctx.handledByBoundary,
|
|
595
|
-
requestStartTime: errorContext.requestStartTime,
|
|
596
599
|
});
|
|
597
600
|
if (emitTelemetry) {
|
|
598
601
|
const errorObj =
|
|
@@ -22,6 +22,7 @@ import type { MiddlewareEntry } from "../router/middleware-types.js";
|
|
|
22
22
|
import {
|
|
23
23
|
_getRequestContext,
|
|
24
24
|
getRequestContext,
|
|
25
|
+
reportedErrorsOf,
|
|
25
26
|
setRequestContextParams,
|
|
26
27
|
type RequestContext,
|
|
27
28
|
} from "../server/request-context.js";
|
|
@@ -158,8 +159,9 @@ export function createErrorReporter<TEnv>(router: {
|
|
|
158
159
|
// case — the error is from a cancelled stream, not a real failure.
|
|
159
160
|
const reqCtx = _getRequestContext();
|
|
160
161
|
if (error != null && typeof error === "object" && reqCtx) {
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
const reportedErrors = reportedErrorsOf(reqCtx);
|
|
163
|
+
if (reportedErrors.has(error)) return;
|
|
164
|
+
reportedErrors.add(error);
|
|
163
165
|
}
|
|
164
166
|
invokeOnError(router.onError, error, phase, context, "RSC");
|
|
165
167
|
};
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
|
48
48
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
49
49
|
import type { TraceSpan } from "../router/tracing.js";
|
|
50
50
|
import {
|
|
51
|
+
runAsBackgroundWork,
|
|
51
52
|
runWithRequestContext,
|
|
52
53
|
setRequestContextParams,
|
|
53
54
|
UNTRACKED_BACKGROUND_TASK,
|
|
@@ -1279,69 +1280,73 @@ export function planShellCapture(input: ShellCaptureInput): void {
|
|
|
1279
1280
|
// (observed in production: ~24s of zero I/O before capture start).
|
|
1280
1281
|
// observePhase reads tracing off the ALS context captured when runBackground
|
|
1281
1282
|
// registered the task — the foreground request context, tracing intact.
|
|
1283
|
+
// onError marks every error of the capture `background`, the task's own
|
|
1284
|
+
// failure (captureTask's catch) included.
|
|
1282
1285
|
const serializedTask = async () => {
|
|
1283
|
-
await
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
(
|
|
1286
|
+
await runAsBackgroundWork(reqCtx, () =>
|
|
1287
|
+
observePhase(PHASES.background("shell-capture"), async (span) => {
|
|
1288
|
+
span.setAttribute("rango.shell_key", key);
|
|
1289
|
+
// A production page can enqueue several navigation-only captures through
|
|
1290
|
+
// viewport prefetching. Let a later document MISS overtake that queued
|
|
1291
|
+
// speculative work so it can become a shell HIT before the 15s queue
|
|
1292
|
+
// budget expires. Never preempts the active capture. Priority and the
|
|
1293
|
+
// backlog ahead at enqueue ride the span (and the skip event below), so
|
|
1294
|
+
// a skip-queue-timeout diagnoses itself instead of needing a trace dive.
|
|
1295
|
+
const queuePriority = descriptor.navigationOnly
|
|
1296
|
+
? ("navigation" as const)
|
|
1297
|
+
: ("document" as const);
|
|
1298
|
+
const depths = captureQueueDepths();
|
|
1299
|
+
const queueAhead =
|
|
1300
|
+
(depths.running ? 1 : 0) +
|
|
1301
|
+
depths.document +
|
|
1302
|
+
(queuePriority === "navigation" ? depths.navigation : 0);
|
|
1303
|
+
span.setAttribute("rango.background.queue_priority", queuePriority);
|
|
1304
|
+
span.setAttribute("rango.background.queue_ahead", queueAhead);
|
|
1305
|
+
const queueStart = performance.now();
|
|
1306
|
+
try {
|
|
1307
|
+
await enqueueSerializedCapture(
|
|
1308
|
+
() => {
|
|
1309
|
+
span.setAttribute(
|
|
1310
|
+
"rango.background.queue_wait_ms",
|
|
1311
|
+
Math.round(performance.now() - queueStart),
|
|
1312
|
+
);
|
|
1313
|
+
return captureTask(span);
|
|
1314
|
+
},
|
|
1315
|
+
{ priority: queuePriority },
|
|
1316
|
+
);
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
if (error instanceof CaptureQueueFullError) {
|
|
1319
|
+
releaseCaptureGuard(key, guardToken);
|
|
1320
|
+
span.setAttribute("rango.background.outcome", "skip-capacity");
|
|
1321
|
+
publishCaptureDebugEvent(descriptor, {
|
|
1322
|
+
key,
|
|
1323
|
+
outcome: "skip-capacity",
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (error instanceof CaptureQueueWaitTimeoutError) {
|
|
1328
|
+
// Dropped unrun after waiting past the queue budget: no backoff (the
|
|
1329
|
+
// route is not doomed, the isolate was busy) — a later request
|
|
1330
|
+
// re-probes the key.
|
|
1331
|
+
releaseCaptureGuard(key, guardToken);
|
|
1332
|
+
span.setAttribute("rango.background.outcome", "skip-queue-timeout");
|
|
1305
1333
|
span.setAttribute(
|
|
1306
1334
|
"rango.background.queue_wait_ms",
|
|
1307
|
-
Math.round(
|
|
1335
|
+
Math.round(error.waitedMs),
|
|
1308
1336
|
);
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
outcome: "skip-capacity",
|
|
1320
|
-
});
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
if (error instanceof CaptureQueueWaitTimeoutError) {
|
|
1324
|
-
// Dropped unrun after waiting past the queue budget: no backoff (the
|
|
1325
|
-
// route is not doomed, the isolate was busy) — a later request
|
|
1326
|
-
// re-probes the key.
|
|
1327
|
-
releaseCaptureGuard(key, guardToken);
|
|
1328
|
-
span.setAttribute("rango.background.outcome", "skip-queue-timeout");
|
|
1329
|
-
span.setAttribute(
|
|
1330
|
-
"rango.background.queue_wait_ms",
|
|
1331
|
-
Math.round(error.waitedMs),
|
|
1332
|
-
);
|
|
1333
|
-
publishCaptureDebugEvent(descriptor, {
|
|
1334
|
-
key,
|
|
1335
|
-
outcome: "skip-queue-timeout",
|
|
1336
|
-
queueWaitMs: Math.round(error.waitedMs),
|
|
1337
|
-
queuePriority,
|
|
1338
|
-
queueAhead,
|
|
1339
|
-
});
|
|
1340
|
-
return;
|
|
1337
|
+
publishCaptureDebugEvent(descriptor, {
|
|
1338
|
+
key,
|
|
1339
|
+
outcome: "skip-queue-timeout",
|
|
1340
|
+
queueWaitMs: Math.round(error.waitedMs),
|
|
1341
|
+
queuePriority,
|
|
1342
|
+
queueAhead,
|
|
1343
|
+
});
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
throw error;
|
|
1341
1347
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
});
|
|
1348
|
+
}),
|
|
1349
|
+
);
|
|
1345
1350
|
};
|
|
1346
1351
|
// The capture's own task must NOT enter reqCtx._pendingBackgroundTasks: the
|
|
1347
1352
|
// capture drains that list before rendering (the write-barrier ordering edge),
|
|
@@ -833,6 +833,13 @@ export interface RequestContext<
|
|
|
833
833
|
/** @internal Per-request error dedup set for onError reporting */
|
|
834
834
|
_reportedErrors: WeakSet<object>;
|
|
835
835
|
|
|
836
|
+
/**
|
|
837
|
+
* @internal The segment a parallel slot's or orphan layout's error came
|
|
838
|
+
* from, for the owning entry's catch (segment-resolution/helpers.ts
|
|
839
|
+
* noteThrower). Per request: an error object can be thrown by several.
|
|
840
|
+
*/
|
|
841
|
+
_throwers?: WeakMap<object, { id: string; type: string }>;
|
|
842
|
+
|
|
836
843
|
/**
|
|
837
844
|
* @internal The errors this request printed to the server console, so a
|
|
838
845
|
* failure prints once where it originated (server/print-server-error.ts). A derived context shares it through the prototype.
|
|
@@ -1005,6 +1012,7 @@ export type PublicRequestContext<
|
|
|
1005
1012
|
| "_inActionRevalidation"
|
|
1006
1013
|
| "_actionRan"
|
|
1007
1014
|
| "_reportedErrors"
|
|
1015
|
+
| "_throwers"
|
|
1008
1016
|
| "_printedErrors"
|
|
1009
1017
|
| "_renderErrors"
|
|
1010
1018
|
| "_recoveredHandlerErrors"
|
|
@@ -1140,6 +1148,46 @@ export function runWithRequestContext<TEnv, T>(
|
|
|
1140
1148
|
return requestContextStorage.run(context, fn);
|
|
1141
1149
|
}
|
|
1142
1150
|
|
|
1151
|
+
/** Inside background work: the errors its onError reports named. */
|
|
1152
|
+
const backgroundWork = new AsyncLocalStorage<WeakSet<object>>();
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Run work whose output goes only to a cache, never to the response of the
|
|
1156
|
+
* request that started it, on `context`: a stale entry's refresh (a "use
|
|
1157
|
+
* cache" function, a cached loader, a cache() record, a document), a
|
|
1158
|
+
* navigation's proactive cache() write, a PPR shell capture. No visitor made
|
|
1159
|
+
* the invalidations in it, so they set no fresh-reads cookie
|
|
1160
|
+
* (cache/tag-invalidation.ts), and onError marks every error reported inside
|
|
1161
|
+
* it `background` (router/error-handling.ts invokeOnError). Its failure is
|
|
1162
|
+
* reported inside it too: a catch outside runs on the visitor's frame.
|
|
1163
|
+
*/
|
|
1164
|
+
export function runAsBackgroundWork<TEnv, T>(
|
|
1165
|
+
context: RequestContext<TEnv>,
|
|
1166
|
+
fn: () => T,
|
|
1167
|
+
): T {
|
|
1168
|
+
const reported = backgroundWork.getStore() ?? new WeakSet<object>();
|
|
1169
|
+
return requestContextStorage.run(context, () =>
|
|
1170
|
+
backgroundWork.run(reported, fn),
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Whether the caller runs inside runAsBackgroundWork. */
|
|
1175
|
+
export function isBackgroundWork(): boolean {
|
|
1176
|
+
return backgroundWork.getStore() !== undefined;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* The set onError's dedupe records a reported error in. Background work keeps
|
|
1181
|
+
* its own: an error object both the visitor's render and background work
|
|
1182
|
+
* reach (a memoized rejection, a thrown constant) is reported by each, so a
|
|
1183
|
+
* background report never takes the place of the visitor's.
|
|
1184
|
+
*/
|
|
1185
|
+
export function reportedErrorsOf(
|
|
1186
|
+
context: RequestContext<any>,
|
|
1187
|
+
): WeakSet<object> {
|
|
1188
|
+
return backgroundWork.getStore() ?? context._reportedErrors;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1143
1191
|
/**
|
|
1144
1192
|
* Get the current request context
|
|
1145
1193
|
* Throws if called outside of a request context
|
|
@@ -1745,6 +1793,7 @@ export function createRequestContext<TEnv>(
|
|
|
1745
1793
|
_locationState: undefined,
|
|
1746
1794
|
|
|
1747
1795
|
_reportedErrors: new WeakSet<object>(),
|
|
1796
|
+
_throwers: undefined,
|
|
1748
1797
|
_printedErrors: new WeakSet<object>(),
|
|
1749
1798
|
_renderErrors: [],
|
|
1750
1799
|
_metricsStore: undefined,
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -345,6 +345,9 @@ export async function dispatch<TEnv = any>(
|
|
|
345
345
|
cacheProfiles: router.cacheProfiles,
|
|
346
346
|
versions,
|
|
347
347
|
});
|
|
348
|
+
// The request's start, as the production handler sets it: onError's
|
|
349
|
+
// `duration` counts from here.
|
|
350
|
+
requestContext._handlerStart = performance.now();
|
|
348
351
|
// Cache degradation (reportCacheError -> _reportBackgroundError) reaches the
|
|
349
352
|
// router's onError, as in the production handler.
|
|
350
353
|
stages.reportBackgroundErrors(requestContext, req, url, ctx.callOnError);
|