@timber-js/app 0.2.0-alpha.37 → 0.2.0-alpha.38
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/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +27 -4
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +32 -8
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +2 -2
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +27 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +13 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +2 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/nitro.ts +27 -4
- package/src/cache/index.ts +5 -2
- package/src/cache/singleflight.ts +54 -4
- package/src/cache/timber-cache.ts +17 -16
- package/src/index.ts +12 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +1 -0
- package/src/server/html-injectors.ts +32 -7
- package/src/server/index.ts +4 -0
- package/src/server/node-stream-transforms.ts +49 -13
- package/src/server/render-timeout.ts +108 -0
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +34 -4
- package/src/server/rsc-entry/rsc-payload.ts +11 -3
- package/src/server/rsc-entry/rsc-stream.ts +24 -3
- package/src/server/ssr-entry.ts +9 -2
- package/src/server/ssr-render.ts +94 -13
|
@@ -42,7 +42,19 @@ export declare function createNodeHeadInjector(headHtml: string): Transform;
|
|
|
42
42
|
* stream). We read from it using the Web API — this is the one bridge
|
|
43
43
|
* point between Web Streams and Node.js streams in the pipeline.
|
|
44
44
|
*/
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Options for the Node.js flight injector.
|
|
47
|
+
*/
|
|
48
|
+
export interface NodeFlightInjectorOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Timeout in milliseconds for individual RSC stream reads.
|
|
51
|
+
* If a single `rscReader.read()` call does not resolve within
|
|
52
|
+
* this duration, the read is aborted and the stream errors with
|
|
53
|
+
* a RenderTimeoutError. Default: 30000 (30s).
|
|
54
|
+
*/
|
|
55
|
+
renderTimeoutMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare function createNodeFlightInjector(rscStream: ReadableStream<Uint8Array> | undefined, options?: NodeFlightInjectorOptions): Transform;
|
|
46
58
|
/**
|
|
47
59
|
* Node.js Transform that catches post-shell streaming errors.
|
|
48
60
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiBxC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA+ClE;AAID;;;;;;;;;;;;;;;;GAgBG;AACH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,EACjD,OAAO,CAAC,EAAE,yBAAyB,GAClC,SAAS,CAuJX;AAOD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAwBtE;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,OAAO,EACvB,eAAe,EAAE,OAAO,GACvB,SAAS,GAAG,IAAI,CA8BlB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render timeout utilities for SSR streaming pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Provides a RenderTimeoutError class and a helper to create
|
|
5
|
+
* timeout-guarded AbortSignals. Used to defend against hung RSC
|
|
6
|
+
* streams and infinite SSR renders.
|
|
7
|
+
*
|
|
8
|
+
* Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Error thrown when an SSR render or RSC stream read exceeds the
|
|
12
|
+
* configured timeout. Callers can check `instanceof RenderTimeoutError`
|
|
13
|
+
* to distinguish timeout from other errors and return a 504 or close
|
|
14
|
+
* the connection cleanly.
|
|
15
|
+
*/
|
|
16
|
+
export declare class RenderTimeoutError extends Error {
|
|
17
|
+
readonly timeoutMs: number;
|
|
18
|
+
constructor(timeoutMs: number, context?: string);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Result of createRenderTimeout — an AbortSignal that fires after
|
|
22
|
+
* the given duration, plus a cancel function to clear the timer
|
|
23
|
+
* when the render completes normally.
|
|
24
|
+
*/
|
|
25
|
+
export interface RenderTimeout {
|
|
26
|
+
/** AbortSignal that aborts after timeoutMs. */
|
|
27
|
+
signal: AbortSignal;
|
|
28
|
+
/** Cancel the timeout timer. Call this when the render completes. */
|
|
29
|
+
cancel: () => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a render timeout that aborts after the given duration.
|
|
33
|
+
*
|
|
34
|
+
* Returns an AbortSignal and a cancel function. The signal fires
|
|
35
|
+
* with a RenderTimeoutError as the abort reason after `timeoutMs`.
|
|
36
|
+
* Call `cancel()` when the render completes to prevent the timeout
|
|
37
|
+
* from firing.
|
|
38
|
+
*
|
|
39
|
+
* If an existing `parentSignal` is provided, the returned signal
|
|
40
|
+
* aborts when either the parent signal or the timeout fires —
|
|
41
|
+
* whichever comes first.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createRenderTimeout(timeoutMs: number, parentSignal?: AbortSignal): RenderTimeout;
|
|
44
|
+
/**
|
|
45
|
+
* Race a promise against a timeout. Rejects with RenderTimeoutError
|
|
46
|
+
* if the promise does not resolve within `timeoutMs`.
|
|
47
|
+
*
|
|
48
|
+
* Used to guard individual `rscReader.read()` calls inside pullLoop.
|
|
49
|
+
*/
|
|
50
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, context?: string): Promise<T>;
|
|
51
|
+
//# sourceMappingURL=render-timeout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-timeout.d.ts","sourceRoot":"","sources":["../../src/server/render-timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAQhD;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,MAAM,EAAE,WAAW,CAAC;IACpB,qEAAqE;IACrE,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,WAAW,GAAG,aAAa,CA+BhG;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,CAAC,CAAC,CAWZ"}
|
|
@@ -17,14 +17,57 @@ export declare const RSC_CONTENT_TYPE = "text/x-component";
|
|
|
17
17
|
* stream that we drain and discard.
|
|
18
18
|
*
|
|
19
19
|
* See design/13-security.md §"Server component source leak"
|
|
20
|
-
*
|
|
21
|
-
* TODO: In the future, expose this debug data to the browser in dev mode
|
|
22
|
-
* for inline error overlays (e.g. component stack traces).
|
|
23
20
|
*/
|
|
24
21
|
export declare function createDebugChannelSink(): {
|
|
25
22
|
readable: ReadableStream;
|
|
26
23
|
writable: WritableStream;
|
|
27
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Parsed component debug info extracted from the Flight debug channel.
|
|
27
|
+
*
|
|
28
|
+
* Contains only component names, environment labels, and stack frames —
|
|
29
|
+
* never source code or props. See design/13-security.md §"Server source
|
|
30
|
+
* never reaches the client".
|
|
31
|
+
*/
|
|
32
|
+
export interface DebugComponentEntry {
|
|
33
|
+
name: string;
|
|
34
|
+
env: string | null;
|
|
35
|
+
key: string | null;
|
|
36
|
+
stack: unknown[] | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A debug channel that collects Flight debug rows instead of discarding them.
|
|
40
|
+
*
|
|
41
|
+
* Used in dev mode to capture server component tree information for the
|
|
42
|
+
* Vite error overlay. The collector provides the same `{ readable, writable }`
|
|
43
|
+
* shape as the discard sink, plus methods to retrieve collected data.
|
|
44
|
+
*
|
|
45
|
+
* Security: only component names, environments, and stack frames are
|
|
46
|
+
* extracted — props and source code are stripped. In production builds,
|
|
47
|
+
* use `createDebugChannelSink()` instead (this function is never called).
|
|
48
|
+
*/
|
|
49
|
+
export interface DebugChannelCollector {
|
|
50
|
+
readable: ReadableStream;
|
|
51
|
+
writable: WritableStream;
|
|
52
|
+
/** Get the raw collected text from the debug channel. */
|
|
53
|
+
getCollectedText(): string;
|
|
54
|
+
/** Get parsed component entries (names, stacks — no props or source). */
|
|
55
|
+
getComponents(): DebugComponentEntry[];
|
|
56
|
+
}
|
|
57
|
+
export declare function createDebugChannelCollector(): DebugChannelCollector;
|
|
58
|
+
/**
|
|
59
|
+
* Parse React Flight debug rows into component entries.
|
|
60
|
+
*
|
|
61
|
+
* The Flight debug channel writes rows in `hexId:json\n` format. Each row
|
|
62
|
+
* with a JSON object containing a `name` field is a component debug info
|
|
63
|
+
* entry. Rows without `name` (timing rows, reference rows like `D"$id"`)
|
|
64
|
+
* are skipped.
|
|
65
|
+
*
|
|
66
|
+
* Security: `props` are explicitly stripped from parsed entries — they may
|
|
67
|
+
* contain rendered output or user data. Only `name`, `env`, `key`, and
|
|
68
|
+
* `stack` are retained.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseDebugRows(text: string): DebugComponentEntry[];
|
|
28
71
|
/**
|
|
29
72
|
* Build segment metadata for the X-Timber-Segments response header.
|
|
30
73
|
* Describes the rendered segment chain with async status, enabling
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,CAQ/F;AAID;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,yDAAyD;IACzD,gBAAgB,IAAI,MAAM,CAAC;IAC3B,yEAAyE;IACzE,aAAa,IAAI,mBAAmB,EAAE,CAAC;CACxC;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAkCnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAoClE;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,KAAK,CAAC;IACtB,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC,GACD,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA0B3C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,cAAc,EACtB,eAAe,EAAE,OAAO,GACvB,QAAQ,CAOV;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIpD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAW1E"}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { type DebugComponentEntry } from './helpers.js';
|
|
1
2
|
/**
|
|
2
3
|
* Set the dev pipeline error handler.
|
|
3
4
|
*
|
|
4
5
|
* Called by the dev server after importing this module to wire pipeline
|
|
5
6
|
* errors into the Vite browser error overlay. No-op in production.
|
|
7
|
+
*
|
|
8
|
+
* The handler receives an optional third argument with RSC debug component
|
|
9
|
+
* info — component names, environments, and stack frames from the Flight
|
|
10
|
+
* debug channel. This is only populated for render-phase errors.
|
|
6
11
|
*/
|
|
7
|
-
export declare function setDevPipelineErrorHandler(handler: (error: Error, phase: string) => void): void;
|
|
12
|
+
export declare function setDevPipelineErrorHandler(handler: (error: Error, phase: string, debugComponents?: DebugComponentEntry[]) => void): void;
|
|
8
13
|
export { runWithEarlyHintsSender } from '#/server/early-hints-sender.js';
|
|
9
14
|
export { runWithWaitUntil } from '#/server/waituntil-bridge.js';
|
|
10
15
|
declare const _default: (req: Request) => Promise<Response>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA8DA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAiCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AA+bD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;8BAhS3C,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAkShD,wBAAiE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC3F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAQrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,YAAY,EAAE,WAAW,EAAE,EAC3B,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC3F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAQrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,YAAY,EAAE,WAAW,EAAE,EAC3B,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC,QAAQ,CAAC,CAkInB"}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* 13-security.md §"Errors don't leak"
|
|
11
11
|
*/
|
|
12
12
|
import { DenySignal, RedirectSignal } from '#/server/primitives.js';
|
|
13
|
+
import { type DebugComponentEntry } from './helpers.js';
|
|
13
14
|
/**
|
|
14
15
|
* Mutable signal state captured during RSC rendering.
|
|
15
16
|
*
|
|
@@ -33,6 +34,8 @@ export interface RenderSignals {
|
|
|
33
34
|
export interface RscStreamResult {
|
|
34
35
|
rscStream: ReadableStream<Uint8Array> | undefined;
|
|
35
36
|
signals: RenderSignals;
|
|
37
|
+
/** Dev-only: server component debug info from the Flight debug channel. */
|
|
38
|
+
getDebugComponents?: () => DebugComponentEntry[];
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
38
41
|
* Render a React element tree to an RSC Flight stream.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAe,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAe,MAAM,wBAAwB,CAAC;AAGjF,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAItB;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;IACtC,WAAW,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvD,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;IAClD,OAAO,EAAE,aAAa,CAAC;IACvB,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,MAAM,mBAAmB,EAAE,CAAC;CAClD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CAyH1F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0EH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE;QACZ,gFAAgF;QAChF,QAAQ,EAAE,MAAM,CAAC;QACjB,uDAAuD;QACvD,OAAO,EAAE,MAAM,CAAC;QAChB,gEAAgE;QAChE,MAAM,EAAE,MAAM,CAAC;QACf,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC;QACnB,+CAA+C;QAC/C,OAAO,EAAE,MAAM,CAAC;QAChB,sDAAsD;QACtD,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0EH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE;QACZ,gFAAgF;QAChF,QAAQ,EAAE,MAAM,CAAC;QACjB,uDAAuD;QACvD,OAAO,EAAE,MAAM,CAAC;QAChB,gEAAgE;QAChE,MAAM,EAAE,MAAM,CAAC;QACf,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC;QACnB,+CAA+C;QAC/C,OAAO,EAAE,MAAM,CAAC;QAChB,sDAAsD;QACtD,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAgKnB;AAED,eAAe,SAAS,CAAC"}
|
|
@@ -38,6 +38,7 @@ export declare function renderSsrStream(element: ReactNode, options?: {
|
|
|
38
38
|
bootstrapScriptContent?: string;
|
|
39
39
|
deferSuspenseFor?: number;
|
|
40
40
|
signal?: AbortSignal;
|
|
41
|
+
renderTimeoutMs?: number;
|
|
41
42
|
}): Promise<ReadableStream<Uint8Array>>;
|
|
42
43
|
/** Whether the current platform uses native Node.js streams for SSR. */
|
|
43
44
|
export declare const useNodeStreams: boolean;
|
|
@@ -52,6 +53,7 @@ export declare function renderSsrNodeStream(element: ReactNode, options?: {
|
|
|
52
53
|
bootstrapScriptContent?: string;
|
|
53
54
|
deferSuspenseFor?: number;
|
|
54
55
|
signal?: AbortSignal;
|
|
56
|
+
renderTimeoutMs?: number;
|
|
55
57
|
}): Promise<import('node:stream').Readable>;
|
|
56
58
|
/** Convert a Node.js Readable to a Web ReadableStream (zero-copy bridge). */
|
|
57
59
|
export declare function nodeReadableToWeb(readable: import('node:stream').Readable): ReadableStream<Uint8Array>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAsEvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,CAkFzC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,aAAa,EAAE,QAAQ,GACvC,cAAc,CAAC,UAAU,CAAC,CAE5B;AAgFD;;;;;;;;;;;;GAYG;AACH,2CAA2C;AAC3C,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,UAAU,CAAC,CA2B5B;AAWD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,OAAO,GACvB,QAAQ,CASV"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.38",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
package/src/adapters/nitro.ts
CHANGED
|
@@ -565,14 +565,37 @@ const server = createServer(async (req, res) => {
|
|
|
565
565
|
|
|
566
566
|
if (webResponse.body) {
|
|
567
567
|
const reader = webResponse.body.getReader();
|
|
568
|
-
|
|
568
|
+
|
|
569
|
+
// Cancel the reader when the client disconnects. This causes any
|
|
570
|
+
// pending reader.read() to reject, breaking the pump loop. Critical
|
|
571
|
+
// for SSE and other infinite streams — without this, disconnected
|
|
572
|
+
// clients leak readers.
|
|
573
|
+
let clientDisconnected = false;
|
|
574
|
+
const onClose = () => {
|
|
575
|
+
clientDisconnected = true;
|
|
576
|
+
reader.cancel('Client disconnected').catch(() => {});
|
|
577
|
+
};
|
|
578
|
+
res.on('close', onClose);
|
|
579
|
+
|
|
580
|
+
try {
|
|
569
581
|
while (true) {
|
|
570
582
|
const { done, value } = await reader.read();
|
|
571
|
-
if (done)
|
|
583
|
+
if (done) break;
|
|
572
584
|
res.write(value);
|
|
573
585
|
}
|
|
574
|
-
}
|
|
575
|
-
|
|
586
|
+
} catch (err) {
|
|
587
|
+
// reader.cancel() from the close handler causes read() to reject.
|
|
588
|
+
// This is expected on client disconnect — not an error.
|
|
589
|
+
if (!clientDisconnected) {
|
|
590
|
+
throw err;
|
|
591
|
+
}
|
|
592
|
+
} finally {
|
|
593
|
+
res.off('close', onClose);
|
|
594
|
+
reader.releaseLock();
|
|
595
|
+
if (!res.writableEnded) {
|
|
596
|
+
res.end();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
576
599
|
} else {
|
|
577
600
|
res.end();
|
|
578
601
|
}
|
package/src/cache/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface CacheOptions<Fn extends (...args: any[]) => any> {
|
|
|
12
12
|
key?: (...args: Parameters<Fn>) => string;
|
|
13
13
|
staleWhileRevalidate?: boolean;
|
|
14
14
|
tags?: string[] | ((...args: Parameters<Fn>) => string[]);
|
|
15
|
+
/** Timeout (ms) for singleflight-coalesced calls. Prevents hung fn() from
|
|
16
|
+
* permanently blocking all future callers for the same cache key. See TIM-518. */
|
|
17
|
+
timeoutMs?: number;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export interface MemoryCacheHandlerOptions {
|
|
@@ -87,5 +90,5 @@ export { createCache } from './timber-cache';
|
|
|
87
90
|
export { registerCachedFunction } from './register-cached-function';
|
|
88
91
|
export type { RegisterCachedFunctionOptions } from './register-cached-function';
|
|
89
92
|
export { stableStringify } from './stable-stringify';
|
|
90
|
-
export { createSingleflight } from './singleflight';
|
|
91
|
-
export type { Singleflight } from './singleflight';
|
|
93
|
+
export { createSingleflight, SingleflightTimeoutError } from './singleflight';
|
|
94
|
+
export type { Singleflight, SingleflightOptions } from './singleflight';
|
|
@@ -3,24 +3,74 @@
|
|
|
3
3
|
* execution. All callers receive the same result (or error).
|
|
4
4
|
*
|
|
5
5
|
* Per-process, in-memory. Each process coalesces independently.
|
|
6
|
+
*
|
|
7
|
+
* An optional `timeoutMs` prevents hung `fn()` calls from permanently
|
|
8
|
+
* blocking all future callers for that key. When set, `fn()` is raced
|
|
9
|
+
* against a timeout — if the timeout fires first, the promise rejects
|
|
10
|
+
* with `SingleflightTimeoutError`, `finally` cleans up the key, and
|
|
11
|
+
* subsequent callers can retry. See TIM-518.
|
|
6
12
|
*/
|
|
13
|
+
|
|
14
|
+
export interface SingleflightOptions {
|
|
15
|
+
/** Maximum time (ms) a coalesced call may run before being rejected. */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export interface Singleflight {
|
|
8
20
|
do<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
9
21
|
}
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Error thrown when a singleflight call exceeds `timeoutMs`.
|
|
25
|
+
* Exported so callers can distinguish timeout from other errors.
|
|
26
|
+
*/
|
|
27
|
+
export class SingleflightTimeoutError extends Error {
|
|
28
|
+
constructor(key: string, timeoutMs: number) {
|
|
29
|
+
super(`Singleflight timeout: key "${key}" exceeded ${timeoutMs}ms`);
|
|
30
|
+
this.name = 'SingleflightTimeoutError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSingleflight(opts?: SingleflightOptions): Singleflight {
|
|
12
35
|
const inflight = new Map<string, Promise<unknown>>();
|
|
36
|
+
const timeoutMs = opts?.timeoutMs;
|
|
13
37
|
|
|
14
38
|
return {
|
|
15
39
|
do<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
16
40
|
const existing = inflight.get(key);
|
|
17
41
|
if (existing) return existing as Promise<T>;
|
|
18
42
|
|
|
19
|
-
|
|
43
|
+
let promise: Promise<T>;
|
|
44
|
+
|
|
45
|
+
if (timeoutMs != null && timeoutMs > 0) {
|
|
46
|
+
// Race fn() against a timeout to prevent hung calls from
|
|
47
|
+
// permanently blocking the key. See TIM-518.
|
|
48
|
+
promise = new Promise<T>((resolve, reject) => {
|
|
49
|
+
const timer = setTimeout(
|
|
50
|
+
() => reject(new SingleflightTimeoutError(key, timeoutMs)),
|
|
51
|
+
timeoutMs
|
|
52
|
+
);
|
|
53
|
+
fn().then(
|
|
54
|
+
(value) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolve(value);
|
|
57
|
+
},
|
|
58
|
+
(err) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
reject(err);
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
promise = fn();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tracked = promise.finally(() => {
|
|
20
69
|
inflight.delete(key);
|
|
21
70
|
});
|
|
22
|
-
|
|
23
|
-
|
|
71
|
+
|
|
72
|
+
inflight.set(key, tracked);
|
|
73
|
+
return tracked as Promise<T>;
|
|
24
74
|
},
|
|
25
75
|
};
|
|
26
76
|
}
|
|
@@ -4,7 +4,7 @@ import { createSingleflight } from './singleflight';
|
|
|
4
4
|
import { addSpanEventSync } from '#/server/tracing.js';
|
|
5
5
|
import { fnv1aHash } from './fast-hash.js';
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const defaultSingleflight = createSingleflight();
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Generate a cache key from function identity and serialized args.
|
|
@@ -55,6 +55,9 @@ export function createCache<Fn extends (...args: any[]) => Promise<any>>(
|
|
|
55
55
|
handler: CacheHandler
|
|
56
56
|
): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {
|
|
57
57
|
const fnId = `timber-cache:${fnIdCounter++}`;
|
|
58
|
+
const sf = opts.timeoutMs
|
|
59
|
+
? createSingleflight({ timeoutMs: opts.timeoutMs })
|
|
60
|
+
: defaultSingleflight;
|
|
58
61
|
|
|
59
62
|
return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {
|
|
60
63
|
const key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId, args);
|
|
@@ -80,25 +83,23 @@ export function createCache<Fn extends (...args: any[]) => Promise<any>>(
|
|
|
80
83
|
stale: true,
|
|
81
84
|
});
|
|
82
85
|
// Serve stale immediately, trigger background refetch
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Singleflight promise rejection handled — stale continues.
|
|
96
|
-
});
|
|
86
|
+
sf.do(`swr:${key}`, async () => {
|
|
87
|
+
try {
|
|
88
|
+
const fresh = await fn(...args);
|
|
89
|
+
const tags = resolveTags(opts, args);
|
|
90
|
+
await handler.set(key, fresh, { ttl: opts.ttl, tags });
|
|
91
|
+
} catch {
|
|
92
|
+
// Failed refetch — stale entry continues to be served.
|
|
93
|
+
// Error is swallowed per design doc: "Error is logged."
|
|
94
|
+
}
|
|
95
|
+
}).catch(() => {
|
|
96
|
+
// Singleflight promise rejection handled — stale continues.
|
|
97
|
+
});
|
|
97
98
|
return cached.value as Awaited<ReturnType<Fn>>;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
// Cache miss (or stale without SWR) — execute with singleflight
|
|
101
|
-
const result = await
|
|
102
|
+
const result = await sf.do(key, () => fn(...args));
|
|
102
103
|
const tags = resolveTags(opts, args);
|
|
103
104
|
await handler.set(key, result, { ttl: opts.ttl, tags });
|
|
104
105
|
|
package/src/index.ts
CHANGED
|
@@ -90,6 +90,18 @@ export interface TimberUserConfig {
|
|
|
90
90
|
* See design/17-logging.md §"slowRequestMs".
|
|
91
91
|
*/
|
|
92
92
|
slowRequestMs?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Render timeout in milliseconds. If an SSR render or RSC stream read
|
|
95
|
+
* does not complete within this duration, the render is aborted and
|
|
96
|
+
* the connection is closed. Protects against hung fetches and Suspense
|
|
97
|
+
* components that never resolve.
|
|
98
|
+
*
|
|
99
|
+
* Set to 0 to disable (not recommended in production).
|
|
100
|
+
* Default: 30000 (30 seconds).
|
|
101
|
+
*
|
|
102
|
+
* See design/02-rendering-pipeline.md §"Streaming Constraints".
|
|
103
|
+
*/
|
|
104
|
+
renderTimeoutMs?: number;
|
|
93
105
|
/** Dev-mode options. These have no effect in production builds. */
|
|
94
106
|
dev?: {
|
|
95
107
|
/** Threshold in ms to highlight slow phases in dev logging output. Default: 200. */
|
|
@@ -180,6 +180,63 @@ export function formatTerminalError(error: Error, phase: ErrorPhase, projectRoot
|
|
|
180
180
|
return lines.join('\n');
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// ─── RSC Debug Context ──────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Component info extracted from the RSC debug channel.
|
|
187
|
+
* Contains only names, environments, and stack frames — never source code.
|
|
188
|
+
*/
|
|
189
|
+
export interface RscDebugComponentInfo {
|
|
190
|
+
name: string;
|
|
191
|
+
env: string | null;
|
|
192
|
+
stack: unknown[] | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Format RSC debug component info into a readable string for the overlay.
|
|
197
|
+
*
|
|
198
|
+
* Renders the server component tree that was active when an error occurred,
|
|
199
|
+
* including component names and source locations from stack frames. This
|
|
200
|
+
* gives developers visibility into which server components were rendering
|
|
201
|
+
* without exposing source code.
|
|
202
|
+
*
|
|
203
|
+
* Returns an empty string if no components are provided.
|
|
204
|
+
*/
|
|
205
|
+
export function formatRscDebugContext(components: RscDebugComponentInfo[]): string {
|
|
206
|
+
if (!components || components.length === 0) return '';
|
|
207
|
+
|
|
208
|
+
// Deduplicate by name — the debug channel may emit the same component
|
|
209
|
+
// multiple times (e.g., when re-rendered or when multiple instances exist).
|
|
210
|
+
const seen = new Set<string>();
|
|
211
|
+
const unique: RscDebugComponentInfo[] = [];
|
|
212
|
+
for (const c of components) {
|
|
213
|
+
if (!seen.has(c.name)) {
|
|
214
|
+
seen.add(c.name);
|
|
215
|
+
unique.push(c);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lines: string[] = ['Server Component Tree:'];
|
|
220
|
+
for (let i = 0; i < unique.length; i++) {
|
|
221
|
+
const c = unique[i]!;
|
|
222
|
+
const indent = ' '.repeat(i + 1);
|
|
223
|
+
const envLabel = c.env ? ` [${c.env}]` : '';
|
|
224
|
+
|
|
225
|
+
// Extract file location from stack frames if available
|
|
226
|
+
let locStr = '';
|
|
227
|
+
if (c.stack && c.stack.length > 0) {
|
|
228
|
+
const frame = c.stack[0] as [string, string, number, number] | undefined;
|
|
229
|
+
if (Array.isArray(frame) && frame.length >= 3) {
|
|
230
|
+
locStr = ` (${frame[1]}:${frame[2]})`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
lines.push(`${indent}${c.name}${envLabel}${locStr}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return lines.join('\n');
|
|
238
|
+
}
|
|
239
|
+
|
|
183
240
|
// ─── Overlay Integration ────────────────────────────────────────────────────
|
|
184
241
|
|
|
185
242
|
/**
|
|
@@ -188,13 +245,19 @@ export function formatTerminalError(error: Error, phase: ErrorPhase, projectRoot
|
|
|
188
245
|
* Uses `server.ssrFixStacktrace()` to map stack traces back to source,
|
|
189
246
|
* then sends the error via `server.hot.send()` for the browser overlay.
|
|
190
247
|
*
|
|
248
|
+
* When `rscDebugComponents` is provided (dev mode only), the server
|
|
249
|
+
* component tree context is appended to the error message. This helps
|
|
250
|
+
* developers identify which server component caused the error without
|
|
251
|
+
* exposing source code.
|
|
252
|
+
*
|
|
191
253
|
* The dev server remains running — errors are handled, not fatal.
|
|
192
254
|
*/
|
|
193
255
|
export function sendErrorToOverlay(
|
|
194
256
|
server: ViteDevServer,
|
|
195
257
|
error: Error,
|
|
196
258
|
phase: ErrorPhase,
|
|
197
|
-
projectRoot: string
|
|
259
|
+
projectRoot: string,
|
|
260
|
+
rscDebugComponents?: RscDebugComponentInfo[]
|
|
198
261
|
): void {
|
|
199
262
|
// Fix stack trace to use source-mapped positions
|
|
200
263
|
server.ssrFixStacktrace(error);
|
|
@@ -212,6 +275,12 @@ export function sendErrorToOverlay(
|
|
|
212
275
|
message = `${error.message}\n\nComponent Stack:\n${componentStack.trim()}`;
|
|
213
276
|
}
|
|
214
277
|
|
|
278
|
+
// Append RSC debug context if available (dev mode only)
|
|
279
|
+
const debugContext = formatRscDebugContext(rscDebugComponents ?? []);
|
|
280
|
+
if (debugContext) {
|
|
281
|
+
message = `${message}\n\n${debugContext}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
215
284
|
// Send to browser via Vite's error overlay protocol
|
|
216
285
|
try {
|
|
217
286
|
server.hot.send({
|
|
@@ -161,12 +161,26 @@ function createTimberMiddleware(server: ViteDevServer, projectRoot: string) {
|
|
|
161
161
|
|
|
162
162
|
// Wire pipeline errors into the browser error overlay.
|
|
163
163
|
// setDevPipelineErrorHandler is only defined in dev (rsc-entry.ts exports it).
|
|
164
|
+
// The handler receives optional RSC debug component data (component names,
|
|
165
|
+
// environments, stack frames) from the Flight debug channel for render errors.
|
|
164
166
|
const setHandler = rscModule.setDevPipelineErrorHandler as
|
|
165
|
-
| ((
|
|
167
|
+
| ((
|
|
168
|
+
fn: (
|
|
169
|
+
error: Error,
|
|
170
|
+
phase: string,
|
|
171
|
+
debugComponents?: Array<{ name: string; env: string | null; stack: unknown[] | null }>
|
|
172
|
+
) => void
|
|
173
|
+
) => void)
|
|
166
174
|
| undefined;
|
|
167
175
|
if (typeof setHandler === 'function') {
|
|
168
|
-
setHandler((error) => {
|
|
169
|
-
sendErrorToOverlay(
|
|
176
|
+
setHandler((error, _phase, debugComponents) => {
|
|
177
|
+
sendErrorToOverlay(
|
|
178
|
+
server,
|
|
179
|
+
error,
|
|
180
|
+
classifyErrorPhase(error, projectRoot),
|
|
181
|
+
projectRoot,
|
|
182
|
+
debugComponents
|
|
183
|
+
);
|
|
170
184
|
});
|
|
171
185
|
}
|
|
172
186
|
} catch (error) {
|
|
@@ -317,6 +331,17 @@ async function sendWebResponse(nodeRes: ServerResponse, webResponse: Response):
|
|
|
317
331
|
nodeRes.flushHeaders();
|
|
318
332
|
|
|
319
333
|
const reader = webResponse.body.getReader();
|
|
334
|
+
|
|
335
|
+
// Cancel the reader when the client disconnects. This causes any pending
|
|
336
|
+
// reader.read() to reject, breaking the pump loop. Critical for SSE and
|
|
337
|
+
// other infinite streams — without this, disconnected clients leak readers.
|
|
338
|
+
let clientDisconnected = false;
|
|
339
|
+
const onClose = () => {
|
|
340
|
+
clientDisconnected = true;
|
|
341
|
+
reader.cancel('Client disconnected').catch(() => {});
|
|
342
|
+
};
|
|
343
|
+
nodeRes.on('close', onClose);
|
|
344
|
+
|
|
320
345
|
try {
|
|
321
346
|
while (true) {
|
|
322
347
|
const { done, value } = await reader.read();
|
|
@@ -325,9 +350,18 @@ async function sendWebResponse(nodeRes: ServerResponse, webResponse: Response):
|
|
|
325
350
|
// don't need back-pressure here — just keep pushing chunks.
|
|
326
351
|
nodeRes.write(value);
|
|
327
352
|
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
// reader.cancel() from the close handler causes read() to reject.
|
|
355
|
+
// This is expected on client disconnect — not an error.
|
|
356
|
+
if (!clientDisconnected) {
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
328
359
|
} finally {
|
|
360
|
+
nodeRes.off('close', onClose);
|
|
329
361
|
reader.releaseLock();
|
|
330
|
-
nodeRes.
|
|
362
|
+
if (!nodeRes.writableEnded) {
|
|
363
|
+
nodeRes.end();
|
|
364
|
+
}
|
|
331
365
|
}
|
|
332
366
|
}
|
|
333
367
|
|
package/src/plugins/entries.ts
CHANGED
|
@@ -106,6 +106,7 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
106
106
|
topLoader: ctx.config.topLoader,
|
|
107
107
|
debug: ctx.config.debug ?? false,
|
|
108
108
|
serverTiming: ctx.config.serverTiming,
|
|
109
|
+
renderTimeoutMs: ctx.config.renderTimeoutMs ?? 30_000,
|
|
109
110
|
// Per-build deployment ID for version skew detection (TIM-446).
|
|
110
111
|
// Null in dev mode — HMR handles code updates without full reloads.
|
|
111
112
|
deploymentId: ctx.deploymentId ?? null,
|