@rangojs/router 0.4.1 → 0.4.3
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/types/browser/navigation-client.d.ts +1 -1
- package/dist/types/browser/prefetch/cache.d.ts +20 -8
- package/dist/types/cache/cf/cf-cache-store.d.ts +19 -8
- package/dist/types/cache/cf/cf-cache-types.d.ts +4 -2
- package/dist/types/cache/read-through-swr.d.ts +7 -6
- package/dist/types/cloudflare/tracing.d.ts +16 -12
- package/dist/types/router/instrument.d.ts +45 -15
- package/dist/types/router/telemetry-otel.d.ts +4 -4
- package/dist/types/router/timeout.d.ts +12 -1
- package/dist/types/router/tracing.d.ts +31 -17
- package/dist/types/rsc/capture-queue.d.ts +22 -1
- package/dist/types/rsc/shell-capture.d.ts +17 -9
- package/dist/types/rsc/stream-idle.d.ts +60 -0
- package/dist/types/segment-fragments.d.ts +27 -4
- package/dist/types/server/request-context.d.ts +28 -9
- package/dist/types/vercel/tracing.d.ts +9 -8
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/cloudflare/SKILL.md +4 -2
- package/skills/observability/SKILL.md +33 -4
- package/skills/ppr/SKILL.md +25 -9
- package/src/browser/navigation-client.ts +77 -27
- package/src/browser/prefetch/cache.ts +40 -10
- package/src/browser/prefetch/fetch.ts +5 -0
- package/src/browser/rsc-router.tsx +12 -6
- package/src/cache/cache-runtime.ts +39 -25
- package/src/cache/cache-scope.ts +5 -1
- package/src/cache/cf/cf-cache-store.ts +423 -90
- package/src/cache/cf/cf-cache-types.ts +4 -2
- package/src/cache/document-cache.ts +63 -25
- package/src/cache/read-through-swr.ts +22 -16
- package/src/cloudflare/tracing.ts +16 -12
- package/src/router/instrument.ts +61 -15
- package/src/router/segment-resolution/loader-cache.ts +12 -1
- package/src/router/segment-resolution/revalidation.ts +16 -1
- package/src/router/telemetry-otel.ts +4 -4
- package/src/router/timeout.ts +12 -1
- package/src/router/tracing.ts +37 -17
- package/src/rsc/capture-queue.ts +72 -17
- package/src/rsc/handler.ts +171 -73
- package/src/rsc/rsc-rendering.ts +61 -6
- package/src/rsc/shell-capture.ts +83 -31
- package/src/rsc/stream-idle.ts +137 -0
- package/src/segment-fragments.ts +45 -4
- package/src/server/request-context.ts +29 -8
- package/src/testing/dispatch.ts +55 -1
- package/src/vercel/tracing.ts +9 -8
|
@@ -14,4 +14,4 @@ import type { NavigationClient, RscBrowserDependencies } from "./types.js";
|
|
|
14
14
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
15
15
|
* @returns NavigationClient instance
|
|
16
16
|
*/
|
|
17
|
-
export declare function createNavigationClient(deps: Pick<RscBrowserDependencies, "createFromFetch">): NavigationClient;
|
|
17
|
+
export declare function createNavigationClient(deps: Pick<RscBrowserDependencies, "createFromFetch" | "createFromReadableStream">): NavigationClient;
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
* scopes are in play:
|
|
11
11
|
* - Wildcard (default): built by `buildPrefetchKey(rangoState, target)` —
|
|
12
12
|
* shape `rangoState\0/target?...`. Shared across all source pages and
|
|
13
|
-
*
|
|
14
|
-
* server-action invalidation).
|
|
13
|
+
* source segment trees; `_rsc_segments` is omitted from this key. Invalidated
|
|
14
|
+
* automatically when Rango state bumps (deploy or server-action invalidation).
|
|
15
15
|
* - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
|
|
16
16
|
* — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
|
|
17
|
-
* (so rotation invalidates source-scoped entries too)
|
|
18
|
-
*
|
|
17
|
+
* (so rotation invalidates source-scoped entries too), the source href, and
|
|
18
|
+
* the exact source segment tree. Populated when the
|
|
19
19
|
* server tags a response with `X-RSC-Prefetch-Scope: source` (intercept
|
|
20
20
|
* modals etc.), OR when a Link opts in with `prefetchKey=":source"` — in
|
|
21
21
|
* both cases so source-sensitive responses cannot bleed into navigations
|
|
@@ -91,7 +91,9 @@ export declare function isPrefetchCacheDisabled(): boolean;
|
|
|
91
91
|
* - Wildcard (source-agnostic): prefix is the Rango state value from
|
|
92
92
|
* `getRangoState()`. Shared across all source pages. Invalidated
|
|
93
93
|
* automatically when Rango state bumps (deploy or server-action).
|
|
94
|
-
*
|
|
94
|
+
* `_rsc_segments` is omitted so sibling payloads prefetched from one page
|
|
95
|
+
* remain reusable after another prefetched navigation commits. Key shape:
|
|
96
|
+
* `rangoState\0/target?...`.
|
|
95
97
|
* - Source-scoped: use `buildSourceKey()`. Key shape:
|
|
96
98
|
* `rangoState\0sourceHref\0/target?...` — embeds the Rango state so
|
|
97
99
|
* rotation invalidates source-scoped entries alongside wildcard ones,
|
|
@@ -100,9 +102,10 @@ export declare function isPrefetchCacheDisabled(): boolean;
|
|
|
100
102
|
* `X-RSC-Prefetch-Scope: source` (intercept modals, etc.) or when a
|
|
101
103
|
* Link opts in via `prefetchKey=":source"`.
|
|
102
104
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
105
|
+
* Source-scoped keys retain `_rsc_segments` because their exact source tree is
|
|
106
|
+
* part of the opt-in contract. Wildcard responses are source-agnostic by
|
|
107
|
+
* contract; callers use source scope when a custom revalidation decision makes
|
|
108
|
+
* the response depend on the current URL or segment tree.
|
|
106
109
|
*/
|
|
107
110
|
export declare function buildPrefetchKey(prefix: string, targetUrl: URL): string;
|
|
108
111
|
/**
|
|
@@ -180,4 +183,13 @@ export declare function setInflightPromise(key: string, promise: Promise<Decoded
|
|
|
180
183
|
*/
|
|
181
184
|
export declare function setInflightPromiseWithAliases(keys: string[], promise: Promise<DecodedPrefetch | null>): void;
|
|
182
185
|
export declare function clearPrefetchInflight(key: string): void;
|
|
186
|
+
/** Whether this document may request stored segment fragment envelopes. */
|
|
187
|
+
export declare function isFragmentPassthroughEnabled(): boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Disable fragment passthrough for the rest of this document after one stored
|
|
190
|
+
* fragment fails to decode. The recovery replaces shared cache entries, while
|
|
191
|
+
* this guard prevents the current tab's HTTP cache from serving its already-
|
|
192
|
+
* cached corrupt variant again.
|
|
193
|
+
*/
|
|
194
|
+
export declare function disableFragmentPassthrough(): void;
|
|
183
195
|
export declare function clearPrefetchCache(rotateRangoState?: boolean): void;
|
|
@@ -70,6 +70,8 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
|
|
|
70
70
|
* @internal
|
|
71
71
|
*/
|
|
72
72
|
private emitDebug;
|
|
73
|
+
/** Build-time-gated shell tier trace for deployed cross-colo diagnostics. */
|
|
74
|
+
private debugShell;
|
|
73
75
|
/**
|
|
74
76
|
* Resolve the cache-key base URL for the current cache operation.
|
|
75
77
|
* Prefers an explicit `baseUrl` option; otherwise derives it from the live
|
|
@@ -263,10 +265,10 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
|
|
|
263
265
|
*/
|
|
264
266
|
private warnShellFamilyInertOnce;
|
|
265
267
|
/**
|
|
266
|
-
* Get a cached PPR shell entry
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
268
|
+
* Get a cached PPR shell entry from Cache API, falling through to KV and
|
|
269
|
+
* promoting a valid KV hit. Both tiers store one envelope so the prelude,
|
|
270
|
+
* postponed state, snapshot, versions, and generation metadata cannot mix.
|
|
271
|
+
* SWR remains a plain staleness flag; the capture scheduler's module-level
|
|
270
272
|
* in-flight set is the recapture stampede guard.
|
|
271
273
|
*/
|
|
272
274
|
getShell(key: string): Promise<{
|
|
@@ -274,12 +276,21 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
|
|
|
274
276
|
shouldRevalidate?: boolean;
|
|
275
277
|
} | null>;
|
|
276
278
|
/**
|
|
277
|
-
* Store a PPR shell
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
279
|
+
* Store a PPR shell envelope in Cache API and, when its retention meets KV's
|
|
280
|
+
* 60-second floor, KV. The shared write is registered with waitUntil and
|
|
281
|
+
* awaited so invalidation rejection can be acknowledged to the capture
|
|
282
|
+
* scheduler. Short-lived shells remain useful in L1 even though KV rejects
|
|
283
|
+
* them.
|
|
281
284
|
*/
|
|
282
285
|
putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
|
|
286
|
+
/** Rebuild the public shell entry from its validated storage envelope. */
|
|
287
|
+
private shellEnvelopeToEntry;
|
|
288
|
+
/** Build the Cache API representation of the coupled shell envelope. */
|
|
289
|
+
private shellEnvelopeResponse;
|
|
290
|
+
/** KV shell fallback with corruption checks and background L1 promotion. */
|
|
291
|
+
private kvGetShell;
|
|
292
|
+
/** Promote a valid KV shell into the per-colo Cache API tier. */
|
|
293
|
+
private promoteShellToL1;
|
|
283
294
|
/**
|
|
284
295
|
* Convert string key to Request object for CF Cache API.
|
|
285
296
|
* Includes version in URL if specified (for cache invalidation on code changes).
|
|
@@ -189,8 +189,10 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
189
189
|
* per-request memo is consulted (synchronously, no KV read), so a request
|
|
190
190
|
* that ran `updateTag()` still masks its own not-yet-purged entries
|
|
191
191
|
* (read-your-own-writes).
|
|
192
|
-
* - The KV tier and PPR
|
|
193
|
-
*
|
|
192
|
+
* - The KV tier and PPR shell reads still use the KV markers. Runtime shell
|
|
193
|
+
* L1 entries are purgeable, but their taggedAt is the capture-start time: a
|
|
194
|
+
* capture can finish after an invalidation purge, so eviction alone cannot
|
|
195
|
+
* reject that older generation. Keep `kv` configured for shell L2 + markers.
|
|
194
196
|
* Without `kv`, purge mode is the ONLY tag invalidation and the store is
|
|
195
197
|
* L1-only: a supported configuration (previously tag invalidation without
|
|
196
198
|
* KV had no read-side effect at all). One KV-less caveat: an entry whose
|
|
@@ -25,13 +25,14 @@ export interface ReadThroughItemConfig<T> {
|
|
|
25
25
|
/** Execute the underlying function/loader on miss or revalidation */
|
|
26
26
|
execute: () => Promise<T>;
|
|
27
27
|
/**
|
|
28
|
-
* Optional wrapper applied to
|
|
29
|
-
*
|
|
30
|
-
* is already established
|
|
31
|
-
* a detached waitUntil task loses on workerd
|
|
32
|
-
*
|
|
28
|
+
* Optional wrapper applied to the WHOLE background stale-revalidation task
|
|
29
|
+
* (execute + serialize + setItem) — not the foreground miss, where the
|
|
30
|
+
* caller's context is already established. Used to re-establish the
|
|
31
|
+
* request-context ALS, which a detached waitUntil task loses on workerd,
|
|
32
|
+
* and to open the rango.background span so the write is covered too.
|
|
33
|
+
* Defaults to running the task directly.
|
|
33
34
|
*/
|
|
34
|
-
wrapBackground?: (run: () => Promise<
|
|
35
|
+
wrapBackground?: (run: () => Promise<void>) => Promise<void>;
|
|
35
36
|
/** Serialize result for storage. Return null to skip caching. */
|
|
36
37
|
serialize: (data: T) => Promise<string | null>;
|
|
37
38
|
/** Deserialize cached value back to the original type */
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cloudflare custom-spans integration.
|
|
3
3
|
*
|
|
4
|
-
* Bridges the router's
|
|
5
|
-
*
|
|
6
|
-
* up in the trace waterfall and OpenTelemetry exports next
|
|
7
|
-
* automatic spans (KV reads, D1 queries, fetch calls), with
|
|
4
|
+
* Bridges the router's observable phases (request, middleware, action, loaders,
|
|
5
|
+
* handler, render, ssr, response, background) onto Cloudflare Workers custom
|
|
6
|
+
* spans so they show up in the trace waterfall and OpenTelemetry exports next
|
|
7
|
+
* to the platform's automatic spans (KV reads, D1 queries, fetch calls), with
|
|
8
|
+
* correct nesting.
|
|
8
9
|
*
|
|
9
10
|
* Usage (Cloudflare preset only):
|
|
10
11
|
*
|
|
@@ -27,12 +28,15 @@
|
|
|
27
28
|
*
|
|
28
29
|
* Span duration note: enterSpan ends a span when its callback's returned value
|
|
29
30
|
* (or promise) settles. For the streaming phases (request/render/ssr) that is at
|
|
30
|
-
* stream CONSTRUCTION, not body-drain.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* stream CONSTRUCTION, not body-drain; rango.response marks that handoff
|
|
32
|
+
* boundary explicitly (it wraps only response finalization, never the body).
|
|
33
|
+
* Instrumentation is best-effort and never wraps or buffers the response body,
|
|
34
|
+
* so it cannot regress streaming or latency. A loader/Suspense child that
|
|
35
|
+
* resolves mid-stream therefore keeps a rango.loader span that can extend past
|
|
36
|
+
* its render parent — overlapping spans are valid. Uses only the typed enterSpan
|
|
37
|
+
* API; spans bound work up to stream-handoff, matching the co-emitted perf
|
|
38
|
+
* metric. On deployed Workers a rango.response span may report 0 ms (non-I/O
|
|
39
|
+
* timers are frozen); its position and attributes are the value.
|
|
36
40
|
*/
|
|
37
41
|
import { type RouterTracingConfig, type TracingToggleOptions } from "../router/tracing.js";
|
|
38
42
|
/**
|
|
@@ -44,8 +48,8 @@ export type CloudflareTracingOptions = TracingToggleOptions;
|
|
|
44
48
|
/**
|
|
45
49
|
* Create the tracing config for a Cloudflare router. Pass the result to
|
|
46
50
|
* `createRouter({ tracing })`. Spans are emitted for the request, middleware,
|
|
47
|
-
* action, loaders, handler, render, and
|
|
48
|
-
* individual phases off.
|
|
51
|
+
* action, loaders, handler, render, ssr, response, and background phases; pass
|
|
52
|
+
* `spans` to turn individual phases off.
|
|
49
53
|
*
|
|
50
54
|
* @see createOTelTracing (`@rangojs/router`) for the same slot on any platform
|
|
51
55
|
* with an OpenTelemetry SDK.
|
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
* The router exposes the same work on three surfaces, and the rule is: each
|
|
6
6
|
* surface has exactly one owner here, so they cannot drift.
|
|
7
7
|
*
|
|
8
|
-
* - observePhase(): a span of work.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* decomposition (request, middleware) pass `metric: false` and get the span
|
|
14
|
-
* only — still co-located, still one owner per surface.
|
|
8
|
+
* - observePhase(): a span of work. From one wrap site, emits the platform
|
|
9
|
+
* span (tracing runner -> Cloudflare custom spans / OTel) and, when the
|
|
10
|
+
* phase has one, its `debugPerformance` metric (metrics store -> [RSC Perf]
|
|
11
|
+
* timeline + Server-Timing). Phases that meter their own perf metric with a
|
|
12
|
+
* finer decomposition, or are deliberately span-only, pass `metric: false`.
|
|
15
13
|
* - observeEvent(): a discrete fact (TelemetrySink): cache decisions,
|
|
16
14
|
* revalidation decisions, handler errors, timeouts, origin rejections.
|
|
17
15
|
* Event-shaped, not phase-shaped — derived from the same call sites but a
|
|
@@ -59,6 +57,11 @@ export interface PhaseSpec {
|
|
|
59
57
|
*/
|
|
60
58
|
lazyAttributes?: () => Record<string, string | number | boolean> | undefined;
|
|
61
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The detached background lanes wrapped by the rango.background span (the
|
|
62
|
+
* `rango.background.kind` attribute). One name per task execution boundary.
|
|
63
|
+
*/
|
|
64
|
+
export type BackgroundTaskKind = "shell-capture" | "document-revalidation" | "loader-revalidation" | "use-cache-revalidation";
|
|
62
65
|
/**
|
|
63
66
|
* The router's observable phases. One definition per phase keeps the `rango.*`
|
|
64
67
|
* span names, perf-metric labels, and identifying attributes from spreading
|
|
@@ -99,6 +102,32 @@ export declare const PHASES: {
|
|
|
99
102
|
/** SSR HTML render from the RSC stream. Colon-delimited like the other ssr:*
|
|
100
103
|
* setup metrics (ssr:module-load / ssr:stream-mode). */
|
|
101
104
|
readonly ssr: PhaseSpec;
|
|
105
|
+
/**
|
|
106
|
+
* One detached background task (waitUntil work that outlives the response):
|
|
107
|
+
* a PPR shell capture or an SWR background revalidation. The wrapper is the
|
|
108
|
+
* explanatory parent for everything the task does — the platform's automatic
|
|
109
|
+
* KV/fetch/cache spans, and (for the revalidation lanes) the re-run's own
|
|
110
|
+
* rango.* phase spans — so post-handoff work never appears as orphan spans
|
|
111
|
+
* dangling under an ended foreground phase. `kind` identifies the lane.
|
|
112
|
+
* Span only (metric:false): the perf timeline is finalized with the
|
|
113
|
+
* response, so a background metric could never reach it. The shell-capture
|
|
114
|
+
* lane keeps its INNER phase spans suppressed (derived context strips
|
|
115
|
+
* _tracing — a capture re-render duplicating the foreground's span set was
|
|
116
|
+
* the original #670 leak); the revalidation lanes keep theirs, nested here.
|
|
117
|
+
*/
|
|
118
|
+
readonly background: (kind: BackgroundTaskKind) => PhaseSpec;
|
|
119
|
+
/** Final response construction + host handoff: opened AFTER downstream
|
|
120
|
+
* middleware/core execution returns a Response, wrapping only finalization
|
|
121
|
+
* (redirect interception/guarding, Server-Timing mutation, final response
|
|
122
|
+
* selection) and ending immediately before the router handler returns the
|
|
123
|
+
* response to the host. Handoff-bound, never drain-bound — it must never
|
|
124
|
+
* read, tee, buffer, or await response.body, so it cannot regress streaming.
|
|
125
|
+
* At most one per traced request; absent when the request throws before a
|
|
126
|
+
* response exists. Span only (metric:false) — a co-emitted metric would
|
|
127
|
+
* circularly mutate the Server-Timing header this phase itself finalizes.
|
|
128
|
+
* On Cloudflare it may report 0 ms (non-I/O timers are frozen); its position
|
|
129
|
+
* and attributes (status/mode/body kind, set by the handler) are the value. */
|
|
130
|
+
readonly response: PhaseSpec;
|
|
102
131
|
};
|
|
103
132
|
/**
|
|
104
133
|
* Instrument one unit of work: open its span AND (unless `metric: false`) record
|
|
@@ -108,14 +137,15 @@ export declare const PHASES: {
|
|
|
108
137
|
* returns a promise both the metric duration and the span end when it settles.
|
|
109
138
|
*
|
|
110
139
|
* This is the ONLY phase primitive: every phase (request/middleware/action/
|
|
111
|
-
* loader/handler/render/ssr)
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* really did take that
|
|
140
|
+
* loader/handler/render/ssr/response/background) settles when fn's own work
|
|
141
|
+
* completes. For streaming phases that means when the RSC/HTML stream is
|
|
142
|
+
* constructed, NOT when the body drains; for response, when finalization hands
|
|
143
|
+
* the stream to the host. Instrumentation is strictly best-effort: it never
|
|
144
|
+
* wraps or buffers the response and adds no work on the streaming path, so it
|
|
145
|
+
* cannot regress response latency or streaming. A loader that resolves while
|
|
146
|
+
* the body streams therefore keeps a rango.loader span that may extend past its
|
|
147
|
+
* render parent — overlapping spans are valid; the loader really did take that
|
|
148
|
+
* long.
|
|
119
149
|
*
|
|
120
150
|
* Reads the metrics store + tracing off the RequestContext ALS, which is active
|
|
121
151
|
* for the WHOLE request — contrast observeEvent, which reads the RouterContext
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* - createOTelTracing(tracer): the OTel adapter for the `tracing` SLOT — the
|
|
7
7
|
* canonical phase-span layer. It bridges observePhase's callback boundary
|
|
8
8
|
* onto OTel's callback-bound `startActiveSpan`, so the router's phase spans
|
|
9
|
-
* (rango.request/middleware/loader/render/ssr
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `createRouter({ tracing })`.
|
|
9
|
+
* (rango.request/middleware/action/loader/handler/render/ssr/response/
|
|
10
|
+
* background) nest by async context and the loader's own OTel spans
|
|
11
|
+
* (db/fetch) land under rango.loader. This is the OTel equivalent of
|
|
12
|
+
* createCloudflareTracing — pass it to `createRouter({ tracing })`.
|
|
13
13
|
*
|
|
14
14
|
* - createOTelSink(tracer): the OTel adapter for the `telemetry` SLOT — a
|
|
15
15
|
* TelemetrySink for the EVENT-shaped facts (handler errors, cache decisions,
|
|
@@ -10,7 +10,18 @@ export interface RouterTimeouts {
|
|
|
10
10
|
actionMs?: number;
|
|
11
11
|
/** Timeout for initial render/response production (ms). */
|
|
12
12
|
renderStartMs?: number;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Timeout for idle streaming after the response is handed off (ms): when no
|
|
15
|
+
* chunk flows to the client for this long, the body stream is errored with a
|
|
16
|
+
* RouterTimeoutError ("stream-idle") and the source render is canceled —
|
|
17
|
+
* bounding hangs from never-settling promises embedded in the payload.
|
|
18
|
+
* END-TO-END semantics: a stalled slow client counts as idle the same as a
|
|
19
|
+
* wedged producer, so pick generous budgets (seconds). Opt-in (unset =
|
|
20
|
+
* unbounded, and the `timeout` shorthand deliberately does not apply);
|
|
21
|
+
* onTimeout does NOT fire — no replacement Response can be served
|
|
22
|
+
* mid-stream — the trip reports via onError + the request.timeout event.
|
|
23
|
+
* Enforcement: rsc/stream-idle.ts, wired at the handler's response tail.
|
|
24
|
+
*/
|
|
14
25
|
streamIdleMs?: number;
|
|
15
26
|
}
|
|
16
27
|
export type TimeoutPhase = "action" | "render-start" | "stream-idle";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Span tracing hook (platform-agnostic).
|
|
3
3
|
*
|
|
4
|
-
* The core router emits its
|
|
5
|
-
* loaders, render, ssr) as spans by calling
|
|
6
|
-
* execution boundaries. When no tracing is
|
|
7
|
-
* pass-through: fn is invoked with a no-op
|
|
8
|
-
* allocation, so a non-traced request behaves
|
|
4
|
+
* The core router emits its observable phases (request, middleware, action,
|
|
5
|
+
* loaders, handler, render, ssr, response, background) as spans by calling
|
|
6
|
+
* traceSpan() at a small set of execution boundaries. When no tracing is
|
|
7
|
+
* configured the call is a direct pass-through: fn is invoked with a no-op
|
|
8
|
+
* span, with no wrapper and no allocation, so a non-traced request behaves
|
|
9
|
+
* exactly as before.
|
|
9
10
|
*
|
|
10
11
|
* A platform integration supplies a SpanRunner that wraps fn in a real span.
|
|
11
12
|
* Two runners ship: the Cloudflare one (createCloudflareTracing in
|
|
@@ -29,7 +30,10 @@
|
|
|
29
30
|
* useLoader, plus the fetchable path), rango.handler (span-only, one per segment
|
|
30
31
|
* route/layout handler execution; the handler:<id> perf metric is owned by the
|
|
31
32
|
* track() at the call site), rango.render (render:total:<route>; normal AND
|
|
32
|
-
* action-revalidation renders), rango.ssr (ssr:render-html).
|
|
33
|
+
* action-revalidation renders), rango.ssr (ssr:render-html), rango.response
|
|
34
|
+
* (span-only; final response construction + host handoff — the explicit
|
|
35
|
+
* stream-handoff marker), and rango.background (span-only; detached work, see
|
|
36
|
+
* below).
|
|
33
37
|
*
|
|
34
38
|
* Span-duration caveat (best-effort, never buffers): a span ends when its
|
|
35
39
|
* callback's value (or promise) settles. For the streaming phases (request,
|
|
@@ -40,6 +44,19 @@
|
|
|
40
44
|
* rango.loader span that can extend past its render parent; overlapping spans are
|
|
41
45
|
* valid (the loader really did take that long). Phase spans bound the work up to
|
|
42
46
|
* stream-handoff, which is also what the co-emitted perf metric measures.
|
|
47
|
+
* rango.response makes that handoff boundary explicit: it wraps only response
|
|
48
|
+
* finalization (redirect interception/guarding, Server-Timing mutation, final
|
|
49
|
+
* response selection) after downstream execution returns, ending immediately
|
|
50
|
+
* before the handler returns the Response to the host. It is handoff-bound,
|
|
51
|
+
* never drain-bound — it never reads, tees, or awaits response.body, and it is
|
|
52
|
+
* absent when the request throws before a response exists.
|
|
53
|
+
*
|
|
54
|
+
* rango.background wraps DETACHED work (waitUntil tasks that outlive the
|
|
55
|
+
* response): PPR shell captures and SWR background revalidations. It is the
|
|
56
|
+
* explanatory parent for spans those tasks produce after the foreground
|
|
57
|
+
* phases ended — without it, post-handoff work reads as orphan spans dangling
|
|
58
|
+
* under an ended parent. See PHASES.background (instrument.ts) for the lane
|
|
59
|
+
* kinds and per-lane inner-span policy.
|
|
43
60
|
*
|
|
44
61
|
* Both shipped runners (Cloudflare, OTel) keep the core agnostic: the
|
|
45
62
|
* platform-specific bridge lives at the edge behind the SpanRunner contract.
|
|
@@ -58,17 +75,14 @@ export interface TraceSpan {
|
|
|
58
75
|
*/
|
|
59
76
|
export type SpanRunner = <T>(name: string, fn: (span: TraceSpan) => T) => T;
|
|
60
77
|
/** The router phases that can be wrapped in a span. */
|
|
61
|
-
export type TracePhase = "request" | "middleware" | "action" | "loader" | "handler" | "render" | "ssr";
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
render?: boolean;
|
|
70
|
-
ssr?: boolean;
|
|
71
|
-
}
|
|
78
|
+
export type TracePhase = "request" | "middleware" | "action" | "loader" | "handler" | "render" | "ssr" | "response" | "background";
|
|
79
|
+
/**
|
|
80
|
+
* Per-phase span toggles. Omitted phases default to enabled. Derived from
|
|
81
|
+
* TracePhase so a new phase cannot be silently missing here — the other phase
|
|
82
|
+
* lists in this file (ALL_PHASES_ON, resolveTracing) are exhaustiveness-checked
|
|
83
|
+
* by their Record<TracePhase, boolean> annotations.
|
|
84
|
+
*/
|
|
85
|
+
export type TracePhaseToggles = Partial<Record<TracePhase, boolean>>;
|
|
72
86
|
/**
|
|
73
87
|
* The option pair shared by every tracing factory (enabled master switch +
|
|
74
88
|
* per-phase span toggles). Extended by OTelTracingOptions,
|
|
@@ -4,9 +4,30 @@ export declare const MAX_ADMITTED_CAPTURES: number;
|
|
|
4
4
|
export declare class CaptureQueueFullError extends Error {
|
|
5
5
|
constructor();
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Budget on how long a capture may WAIT in the queue before it is dropped
|
|
9
|
+
* instead of run. On workerd a queued capture rides the scheduling request's
|
|
10
|
+
* waitUntil, whose post-response lifetime is bounded (~30s): a capture that
|
|
11
|
+
* already burned 15s+ parked behind a slow predecessor would start its own
|
|
12
|
+
* up-to-15s attempt (captureTimeout) with no budget left and risk being
|
|
13
|
+
* killed mid-store-write. Dropping at the budget is safe — captures are
|
|
14
|
+
* best-effort by contract, and a later request re-probes the key (the drop
|
|
15
|
+
* does NOT mark backoff: the route is not doomed, the isolate was busy).
|
|
16
|
+
* Matches one attempt's SHELL_CAPTURE_MAX_WAIT_MS so wait + attempt fits the
|
|
17
|
+
* platform budget. Field-observed: a navigation-shell capture parked ~24s
|
|
18
|
+
* behind a predecessor's two-attempt cycle.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CAPTURE_QUEUE_WAIT_BUDGET_MS: number;
|
|
21
|
+
/** Thrown (async) when the queue wait exceeded the budget; the task never ran. */
|
|
22
|
+
export declare class CaptureQueueWaitTimeoutError extends Error {
|
|
23
|
+
readonly waitedMs: number;
|
|
24
|
+
constructor(waitedMs: number);
|
|
25
|
+
}
|
|
7
26
|
/**
|
|
8
27
|
* Run `task` after every previously enqueued capture has settled. Returns a
|
|
9
28
|
* promise for THIS task's completion (rejections propagate to the caller —
|
|
10
29
|
* the queue itself is insulated).
|
|
11
30
|
*/
|
|
12
|
-
export declare function enqueueSerializedCapture(task: () => Promise<void
|
|
31
|
+
export declare function enqueueSerializedCapture(task: () => Promise<void>, opts?: {
|
|
32
|
+
maxQueueWaitMs?: number;
|
|
33
|
+
}): Promise<void>;
|
|
@@ -125,10 +125,14 @@ export interface ShellCaptureDebugEvent {
|
|
|
125
125
|
* - skip-backoff: the key is inside its refused-capture backoff window and
|
|
126
126
|
* the capture was not attempted
|
|
127
127
|
* - skip-capacity: the isolate capture queue is full; a later request may retry
|
|
128
|
+
* - skip-queue-timeout: the capture waited past CAPTURE_QUEUE_WAIT_BUDGET_MS
|
|
129
|
+
* behind other captures and was dropped unrun (no backoff — the route is
|
|
130
|
+
* not doomed, the isolate was busy; a later request re-probes). Carries
|
|
131
|
+
* queueWaitMs.
|
|
128
132
|
* - backoff: the key entered (or escalated) backoff after a terminal
|
|
129
133
|
* no-shell — carries the new backoff state
|
|
130
134
|
*/
|
|
131
|
-
outcome: "stored" | "redirect" | "no-shell" | "refused" | "error" | "skip-in-flight" | "skip-backoff" | "skip-capacity" | "backoff";
|
|
135
|
+
outcome: "stored" | "redirect" | "no-shell" | "refused" | "error" | "skip-in-flight" | "skip-backoff" | "skip-capacity" | "skip-queue-timeout" | "backoff";
|
|
132
136
|
/** Attempt number (1 = first, 2 = in-place retry). Absent on skips. */
|
|
133
137
|
attempt?: number;
|
|
134
138
|
/** Wall-clock ms of the whole attempt (barrier + render + drain + put). */
|
|
@@ -157,6 +161,8 @@ export interface ShellCaptureDebugEvent {
|
|
|
157
161
|
backoffFailures?: number;
|
|
158
162
|
/** Ms remaining in the key's backoff window, when one exists. */
|
|
159
163
|
backoffRemainingMs?: number;
|
|
164
|
+
/** Ms the capture waited in the serialized queue (skip-queue-timeout). */
|
|
165
|
+
queueWaitMs?: number;
|
|
160
166
|
}
|
|
161
167
|
/**
|
|
162
168
|
* Debug sink for the capture pipeline, mirroring {@link CFCacheDebug}: `true`
|
|
@@ -304,8 +310,9 @@ export declare function scheduleShellCapture(ctx: HandlerContext<any>, request:
|
|
|
304
310
|
* I/O failure is reported separately and does NOT make the attempt retryable —
|
|
305
311
|
* the capture itself worked).
|
|
306
312
|
* - `redirect`: the matched route redirects, so there is no shell to capture.
|
|
307
|
-
* - `no-shell`:
|
|
308
|
-
*
|
|
313
|
+
* - `no-shell`: captureShellHTML returned null because the prelude was unusable
|
|
314
|
+
* or its private capture abort landed before the shell completed. This is the
|
|
315
|
+
* only RETRYABLE outcome.
|
|
309
316
|
*/
|
|
310
317
|
type CaptureAttemptOutcome = "stored" | "redirect" | "no-shell" | "refused";
|
|
311
318
|
/**
|
|
@@ -327,9 +334,9 @@ type CaptureAttemptStats = Pick<ShellCaptureDebugEvent, "barrierWaitMs" | "write
|
|
|
327
334
|
* when we froze the shell; the attempt itself warmed the module graph, so a second
|
|
328
335
|
* attempt a short beat later usually completes the shell in the SAME background
|
|
329
336
|
* task. That kills the old multi-request warmup where the caller had to re-issue
|
|
330
|
-
* several HTTP requests before a capture stuck. We retry ONLY on `no-shell
|
|
331
|
-
*
|
|
332
|
-
*
|
|
337
|
+
* several HTTP requests before a capture stuck. We retry ONLY on `no-shell`; a
|
|
338
|
+
* genuine render error is NOT retried — it propagates to scheduleShellCapture's
|
|
339
|
+
* reportCacheError. See docs/design/ppr-shell-resume.md.
|
|
333
340
|
*
|
|
334
341
|
* `retryDelayMs` is a parameter (defaulting to the module const) so unit tests can
|
|
335
342
|
* drive the retry without a real 400ms wall-clock wait.
|
|
@@ -369,9 +376,10 @@ export declare function deriveShellCaptureContext(reqCtx: RequestContext<any>, d
|
|
|
369
376
|
* capture worked; only the store I/O failed). `ssrModule.captureShellHTML` MUST be
|
|
370
377
|
* present (eligibility is checked before scheduling).
|
|
371
378
|
*
|
|
372
|
-
* A `no-shell` result
|
|
373
|
-
*
|
|
374
|
-
*
|
|
379
|
+
* A `no-shell` result from captureShellHTML is the only retryable outcome. Every
|
|
380
|
+
* captureShellHTML error propagates to reportCacheError and is NOT retried. The
|
|
381
|
+
* capture handler converts only its own private abort sentinel to null before this
|
|
382
|
+
* layer sees it.
|
|
375
383
|
*/
|
|
376
384
|
declare function captureAndStoreShell(ssrModule: SSRModule, rscStream: ReadableStream<Uint8Array>, handleStore: HandleStore, reqCtx: RequestContext<any>, capture: ShellCaptureDescriptor, stats?: CaptureAttemptStats, captureStartedAt?: number): Promise<Exclude<CaptureAttemptOutcome, "redirect">>;
|
|
377
385
|
export { runShellCapture, captureAndStoreShell, delay, SHELL_CAPTURE_RETRY_DELAY_MS, };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream-idle timeout — the enforcement for `timeouts.streamIdleMs` (the
|
|
3
|
+
* "stream-idle" TimeoutPhase, reserved since the timeout API landed).
|
|
4
|
+
*
|
|
5
|
+
* WHY: nothing else bounds a streaming response body's lifetime.
|
|
6
|
+
* `renderStartMs` only bounds time-to-Response-construction; once the body is
|
|
7
|
+
* streaming, a single never-settling promise embedded in the payload (a
|
|
8
|
+
* deferred loader that never resolves, an orphaned ctx.rendered() barrier)
|
|
9
|
+
* holds the Flight/HTML stream — and the client's connection — open forever.
|
|
10
|
+
* Deferred HANDLES have a 10s auto-resolve net (defer.ts); loader promises
|
|
11
|
+
* and everything else have none. Field-observed as multi-second dead
|
|
12
|
+
* connections only closed by client cancellation.
|
|
13
|
+
*
|
|
14
|
+
* SEMANTICS — end-to-end idle flow, deliberately: the watchdog re-arms on
|
|
15
|
+
* every chunk that flows THROUGH to the client, so "idle" means "no bytes
|
|
16
|
+
* reached the wire for N ms". A stalled slow client counts as idle the same
|
|
17
|
+
* as a wedged producer — the two are indistinguishable here without buffering,
|
|
18
|
+
* and buffering is banned on the streaming path. Pick budgets accordingly
|
|
19
|
+
* (generous, seconds not millis); the feature is OPT-IN (unset = today's
|
|
20
|
+
* unbounded behavior, and the `timeout` shorthand deliberately does not apply
|
|
21
|
+
* to streamIdleMs — see resolveTimeouts).
|
|
22
|
+
*
|
|
23
|
+
* ON TRIP: the client-facing stream is errored with a RouterTimeoutError
|
|
24
|
+
* ("stream-idle") — the host terminates the response visibly (truncated
|
|
25
|
+
* chunked encoding), which is debuggable, unlike a silent clean close that
|
|
26
|
+
* would present a half-document as complete. Erroring the transform makes the
|
|
27
|
+
* pipe cancel its SOURCE, which aborts React's fizz/flight render — but only
|
|
28
|
+
* when the watchdog's branch is the sole consumer. When an upstream layer
|
|
29
|
+
* teed or cloned the body first (the document-cache MISS drain,
|
|
30
|
+
* response-cache clones), the watchdog holds one tee branch: tee semantics
|
|
31
|
+
* cancel the underlying source only once EVERY branch cancels, so the cache
|
|
32
|
+
* branch keeps the wedged render alive, bounded only by the platform's
|
|
33
|
+
* waitUntil budget — exactly as it was before this feature. The client-facing
|
|
34
|
+
* bound is unconditional; source teardown is best-effort. `onTimeout` does
|
|
35
|
+
* NOT apply: the response already left the handler, so no replacement
|
|
36
|
+
* Response can be served — the trip is reported via onError + the
|
|
37
|
+
* request.timeout telemetry event instead.
|
|
38
|
+
*
|
|
39
|
+
* COST: one identity TransformStream hop per chunk, only when the timeout is
|
|
40
|
+
* enabled. Never buffers, never reads ahead, adds no work when disabled.
|
|
41
|
+
*/
|
|
42
|
+
import { RouterTimeoutError } from "../router/timeout.js";
|
|
43
|
+
/** What the watchdog observed when it tripped. */
|
|
44
|
+
export interface StreamIdleTrip {
|
|
45
|
+
/** ms since the watchdog armed (response handoff) — the stream's lifetime. */
|
|
46
|
+
totalMs: number;
|
|
47
|
+
/** Chunks forwarded before the trip. */
|
|
48
|
+
chunks: number;
|
|
49
|
+
/** The error the client-facing stream was terminated with. */
|
|
50
|
+
error: RouterTimeoutError;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Wrap `response`'s body in an idle watchdog. Returns a NEW Response with the
|
|
54
|
+
* same status/headers whose body errors (and cancels the source) once no chunk
|
|
55
|
+
* has flowed for `idleMs`. Callers gate on isTimeoutEnabled + body presence +
|
|
56
|
+
* NOT a websocket upgrade (a 101/webSocket response must never be
|
|
57
|
+
* reconstructed); this function assumes those checks were made. `onTrip` fires
|
|
58
|
+
* at most once, after the client-facing stream has been errored.
|
|
59
|
+
*/
|
|
60
|
+
export declare function applyStreamIdleTimeout(response: Response, idleMs: number, onTrip: (trip: StreamIdleTrip) => void): Response;
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* fragment STRINGS ride the outgoing payload verbatim inside these envelopes
|
|
7
7
|
* — the outer Flight render copies a string instead of re-serializing a whole
|
|
8
8
|
* element tree — and the CONSUMER expands each envelope through its own Flight
|
|
9
|
-
* deserializer: the SSR resume pass (ssr/ssr-root.tsx)
|
|
10
|
-
*
|
|
11
|
-
* independently, exactly like the segment codec's
|
|
12
|
-
* there is no Flight row-id collision or shared-row
|
|
9
|
+
* deserializer: the SSR resume pass (ssr/ssr-root.tsx), browser hydration, and
|
|
10
|
+
* capability-gated partial navigation/prefetch decoders. Each fragment is its
|
|
11
|
+
* own row space, decoded independently, exactly like the segment codec's
|
|
12
|
+
* per-record decode — so there is no Flight row-id collision or shared-row
|
|
13
|
+
* dedupe hazard.
|
|
13
14
|
*
|
|
14
15
|
* Envelopes appear ONLY on fields that hold ReactNodes (component / layout /
|
|
15
16
|
* loading). A plain object is never a valid ReactNode, so the marker cannot
|
|
@@ -20,6 +21,15 @@
|
|
|
20
21
|
* it must stay dependency-free: no plugin-rsc, no request-context.
|
|
21
22
|
*/
|
|
22
23
|
import type { ResolvedSegment } from "./types.js";
|
|
24
|
+
/** Partial-request capability gate for browser-side fragment expansion. */
|
|
25
|
+
export declare const SEGMENT_FRAGMENT_CAPABILITY_HEADER: string;
|
|
26
|
+
/** One-shot request marker that forces server-side fragment validation. */
|
|
27
|
+
export declare const SEGMENT_FRAGMENT_RECOVERY_HEADER: string;
|
|
28
|
+
/** A stored fragment failed in its consumer-side Flight decoder. */
|
|
29
|
+
export declare class SegmentFragmentDecodeError extends Error {
|
|
30
|
+
readonly cause: unknown;
|
|
31
|
+
constructor(cause: unknown);
|
|
32
|
+
}
|
|
23
33
|
/**
|
|
24
34
|
* One RSC-encoded fragment traveling inside an RscPayload segment field.
|
|
25
35
|
* `f` is the stored Flight document (segment-codec output) for that field.
|
|
@@ -49,6 +59,19 @@ export type FragmentDecoder = (stream: ReadableStream<Uint8Array>) => Promise<un
|
|
|
49
59
|
* the same posture as a parseable-but-mismatched postponed blob.
|
|
50
60
|
*/
|
|
51
61
|
export declare function expandSegmentFragments(segments: ResolvedSegment[] | undefined, decode: FragmentDecoder): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Expand every envelope in an RscPayload-shaped object, in place. Decode
|
|
64
|
+
* failures use a distinct error so partial navigation can retry once through
|
|
65
|
+
* the server's decode-and-evict path. The
|
|
66
|
+
* hasSegmentFragments pre-scan keeps envelope-free payloads — every
|
|
67
|
+
* non-replay response — to one synchronous field walk; this runs on every
|
|
68
|
+
* navigation and prefetch decode.
|
|
69
|
+
*/
|
|
70
|
+
export declare function expandPayloadFragments(payload: {
|
|
71
|
+
metadata?: {
|
|
72
|
+
segments?: ResolvedSegment[];
|
|
73
|
+
};
|
|
74
|
+
}, decode: FragmentDecoder): Promise<void>;
|
|
52
75
|
/**
|
|
53
76
|
* True when any segment carries an unexpanded envelope. Cheap scan used by
|
|
54
77
|
* consumers that only need to know whether an expansion pass is required.
|