@timber-js/app 0.2.0-alpha.127 → 0.2.0-alpha.128
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/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts +5 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +121 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -0
- package/dist/server/ssr-entry.d.ts +1 -72
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +15 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/plugins/fonts.ts +9 -1
- package/src/server/deny-renderer.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +14 -4
- package/src/server/rsc-entry/ssr-bridge.ts +5 -1
- package/src/server/rsc-entry/ssr-renderer.ts +25 -21
- package/src/server/ssr-bridge-types.ts +124 -0
- package/src/server/ssr-entry.ts +9 -77
- package/src/server/ssr-render.ts +40 -4
package/src/server/ssr-entry.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { wrapSsrElement } from './ssr-wrappers.js';
|
|
|
45
45
|
import { withSpan } from './tracing.js';
|
|
46
46
|
import { setCurrentParams } from '../client/use-segment-params.js';
|
|
47
47
|
import { registerSsrDataProvider, type SsrData } from '../client/ssr-data.js';
|
|
48
|
+
import type { NavContext } from './ssr-bridge-types.js';
|
|
48
49
|
|
|
49
50
|
// Pre-import Node.js stream modules at module load time — not per-request.
|
|
50
51
|
// Dynamic imports of node-stream-transforms and node:stream/promises were
|
|
@@ -98,81 +99,6 @@ const ssrDataAls = new AsyncLocalStorage<SsrData>();
|
|
|
98
99
|
// Register the ALS-backed provider so getSsrData() reads from ALS.
|
|
99
100
|
registerSsrDataProvider(() => ssrDataAls.getStore());
|
|
100
101
|
|
|
101
|
-
/**
|
|
102
|
-
* Navigation context passed from the RSC environment to SSR.
|
|
103
|
-
*
|
|
104
|
-
* Per-request state must be explicitly passed across the RSC→SSR
|
|
105
|
-
* environment boundary since they are separate Vite module graphs.
|
|
106
|
-
*/
|
|
107
|
-
export interface NavContext {
|
|
108
|
-
/** The requested pathname */
|
|
109
|
-
pathname: string;
|
|
110
|
-
/** Extracted route params (catch-all segments produce string[]) */
|
|
111
|
-
params: Record<string, string | string[]>;
|
|
112
|
-
/** Search params from the URL */
|
|
113
|
-
searchParams: Record<string, string>;
|
|
114
|
-
/** The committed HTTP status code */
|
|
115
|
-
statusCode: number;
|
|
116
|
-
/** Response headers from middleware/proxy */
|
|
117
|
-
responseHeaders: Headers;
|
|
118
|
-
/** Pre-rendered metadata HTML to inject before </head> */
|
|
119
|
-
headHtml: string;
|
|
120
|
-
/** Inline JS for React's bootstrapScriptContent — kicks off module loading */
|
|
121
|
-
bootstrapScriptContent: string;
|
|
122
|
-
/** Tee'd RSC stream for client-side hydration (inlined into HTML) */
|
|
123
|
-
rscStream?: ReadableStream<Uint8Array>;
|
|
124
|
-
/** Max Suspense hold window (ms). SSR delays the first flush by this
|
|
125
|
-
* duration, racing allReady so that fast-resolving boundaries render inline
|
|
126
|
-
* without ever showing a fallback. Derived from route `deferSuspenseFor` exports.
|
|
127
|
-
* See design/05-streaming.md §"deferSuspenseFor". */
|
|
128
|
-
deferSuspenseFor?: number;
|
|
129
|
-
/** Request abort signal. When the client disconnects (page refresh,
|
|
130
|
-
* navigation away), this signal fires. Passed to renderToReadableStream
|
|
131
|
-
* so React stops rendering and doesn't fire error boundaries for aborts. */
|
|
132
|
-
signal?: AbortSignal;
|
|
133
|
-
/** Request cookies as name→value pairs. Used by useCookie() during SSR
|
|
134
|
-
* to return correct cookie values before hydration. */
|
|
135
|
-
cookies?: Map<string, string>;
|
|
136
|
-
/**
|
|
137
|
-
* Mutable flag: set by TimberErrorBoundary during SSR when it catches
|
|
138
|
-
* a DenySignal (via digest). This tells the RSC entry that the denial
|
|
139
|
-
* was contained by a slot error boundary and should NOT be promoted
|
|
140
|
-
* to a page-level deny. See LOCAL-298.
|
|
141
|
-
*/
|
|
142
|
-
_denyHandledByBoundary?: boolean;
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Mutable: SSR timing data populated by handleSsr().
|
|
146
|
-
* Read by the RSC entry to record sub-phase Server-Timing entries
|
|
147
|
-
* when `serverTiming: 'detailed'` is configured.
|
|
148
|
-
*
|
|
149
|
-
* This bridges the RSC→SSR environment boundary: the SSR entry populates
|
|
150
|
-
* these fields, the RSC entry reads them after callSsr() returns.
|
|
151
|
-
*/
|
|
152
|
-
_ssrTimings?: {
|
|
153
|
-
/** Time to decode RSC stream (createFromReadableStream/createFromNodeStream) */
|
|
154
|
-
decodeMs: number;
|
|
155
|
-
/** Time for Fizz to render the shell (onShellReady) */
|
|
156
|
-
shellMs: number;
|
|
157
|
-
/** Time for pipe() to flush shell bytes to the output stream */
|
|
158
|
-
pipeMs: number;
|
|
159
|
-
/** Time to set up Node.js Transform pipeline / Web Stream transforms */
|
|
160
|
-
pipelineMs: number;
|
|
161
|
-
/** Total SSR time (decode → response ready) */
|
|
162
|
-
totalMs: number;
|
|
163
|
-
/** Whether the Node.js native stream path was used */
|
|
164
|
-
nodeStreams: boolean;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Signal that fires when an unhandled RSC error is detected.
|
|
169
|
-
* The Flight injector checks this before writing each script tag —
|
|
170
|
-
* when aborted, it stops injecting Flight data to prevent corrupted
|
|
171
|
-
* chunks from crashing the client's Flight decoder.
|
|
172
|
-
*/
|
|
173
|
-
_errorSignal?: AbortSignal;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
102
|
/**
|
|
177
103
|
* Handle SSR: decode an RSC stream and render it to hydration-ready HTML.
|
|
178
104
|
*
|
|
@@ -207,7 +133,7 @@ export async function handleSsr(
|
|
|
207
133
|
pathname: navContext.pathname,
|
|
208
134
|
searchParams: navContext.searchParams,
|
|
209
135
|
cookies: navContext.cookies ?? new Map(),
|
|
210
|
-
params: navContext.
|
|
136
|
+
params: navContext.segmentParams,
|
|
211
137
|
_navContext: navContext,
|
|
212
138
|
};
|
|
213
139
|
|
|
@@ -221,7 +147,7 @@ export async function handleSsr(
|
|
|
221
147
|
// useParams reads from getSsrData() during SSR (ALS-backed),
|
|
222
148
|
// but setCurrentParams is kept for the client-side path where
|
|
223
149
|
// the segment router updates params on navigation.
|
|
224
|
-
setCurrentParams(navContext.
|
|
150
|
+
setCurrentParams(navContext.segmentParams);
|
|
225
151
|
|
|
226
152
|
// Decode the RSC stream into a React element tree.
|
|
227
153
|
// createFromReadableStream resolves client component references
|
|
@@ -280,6 +206,9 @@ export async function handleSsr(
|
|
|
280
206
|
deferSuspenseFor: navContext.deferSuspenseFor,
|
|
281
207
|
signal: navContext.signal,
|
|
282
208
|
renderTimeoutMs,
|
|
209
|
+
onFizzAllReadyResolved: (resolved) => {
|
|
210
|
+
navContext._fizzAllReadyResolved = resolved;
|
|
211
|
+
},
|
|
283
212
|
});
|
|
284
213
|
} catch (renderError) {
|
|
285
214
|
logRenderError({ method: '', path: '', error: renderError });
|
|
@@ -346,6 +275,9 @@ export async function handleSsr(
|
|
|
346
275
|
deferSuspenseFor: navContext.deferSuspenseFor,
|
|
347
276
|
signal: navContext.signal,
|
|
348
277
|
renderTimeoutMs,
|
|
278
|
+
onFizzAllReadyResolved: (resolved) => {
|
|
279
|
+
navContext._fizzAllReadyResolved = resolved;
|
|
280
|
+
},
|
|
349
281
|
});
|
|
350
282
|
} catch (renderError) {
|
|
351
283
|
logRenderError({ method: '', path: '', error: renderError });
|
package/src/server/ssr-render.ts
CHANGED
|
@@ -128,6 +128,14 @@ export async function renderSsrStream(
|
|
|
128
128
|
deferSuspenseFor?: number;
|
|
129
129
|
signal?: AbortSignal;
|
|
130
130
|
renderTimeoutMs?: number;
|
|
131
|
+
/**
|
|
132
|
+
* Fired synchronously when the render function is about to resolve
|
|
133
|
+
* (i.e. at hold-end / shell-ready). `resolved` reports whether Fizz's
|
|
134
|
+
* `stream.allReady` had already settled by that moment. Callers use
|
|
135
|
+
* this to decide whether signal promotion can be short-circuited.
|
|
136
|
+
* See NavContext._fizzAllReadyResolved.
|
|
137
|
+
*/
|
|
138
|
+
onFizzAllReadyResolved?: (resolved: boolean) => void;
|
|
131
139
|
}
|
|
132
140
|
): Promise<ReadableStream<Uint8Array>> {
|
|
133
141
|
return renderViaReadableStream(element, options);
|
|
@@ -158,17 +166,26 @@ export async function renderSsrNodeStream(
|
|
|
158
166
|
deferSuspenseFor?: number;
|
|
159
167
|
signal?: AbortSignal;
|
|
160
168
|
renderTimeoutMs?: number;
|
|
169
|
+
/**
|
|
170
|
+
* Fired synchronously at the moment `renderSsrNodeStream`'s returned
|
|
171
|
+
* Promise is about to resolve. `resolved` reports whether Fizz's
|
|
172
|
+
* `onAllReady` had already fired by then. See the twin callback on
|
|
173
|
+
* `renderSsrStream`.
|
|
174
|
+
*/
|
|
175
|
+
onFizzAllReadyResolved?: (resolved: boolean) => void;
|
|
161
176
|
}
|
|
162
177
|
): Promise<import('node:stream').Readable> {
|
|
163
178
|
const signal = options?.signal;
|
|
164
179
|
const deferMs = options?.deferSuspenseFor;
|
|
165
180
|
const timeoutMs = options?.renderTimeoutMs;
|
|
181
|
+
const onFizzAllReadyResolved = options?.onFizzAllReadyResolved;
|
|
166
182
|
|
|
167
183
|
return new Promise<import('node:stream').Readable>((resolve, reject) => {
|
|
168
184
|
const passthrough = new _PassThrough!();
|
|
169
185
|
|
|
170
186
|
let allReadyResolve: (() => void) | null = null;
|
|
171
187
|
let allReadyReject: ((reason?: unknown) => void) | null = null;
|
|
188
|
+
let allReadyFired = false;
|
|
172
189
|
const allReady = new Promise<void>((resolve, reject) => {
|
|
173
190
|
allReadyResolve = resolve;
|
|
174
191
|
allReadyReject = reject;
|
|
@@ -181,16 +198,21 @@ export async function renderSsrNodeStream(
|
|
|
181
198
|
onShellReady() {
|
|
182
199
|
if (deferMs && deferMs > 0) {
|
|
183
200
|
Promise.race([allReady, new Promise<void>((r) => setTimeout(r, deferMs))]).then(() => {
|
|
201
|
+
onFizzAllReadyResolved?.(allReadyFired);
|
|
184
202
|
pipe(passthrough);
|
|
185
203
|
resolve(passthrough);
|
|
186
204
|
});
|
|
187
205
|
} else {
|
|
206
|
+
// No hold — allReady has almost certainly not fired yet.
|
|
207
|
+
// Report false so the RSC entry runs the microtask retry loop.
|
|
208
|
+
onFizzAllReadyResolved?.(allReadyFired);
|
|
188
209
|
pipe(passthrough);
|
|
189
210
|
resolve(passthrough);
|
|
190
211
|
}
|
|
191
212
|
},
|
|
192
213
|
|
|
193
214
|
onAllReady() {
|
|
215
|
+
allReadyFired = true;
|
|
194
216
|
allReadyResolve?.();
|
|
195
217
|
},
|
|
196
218
|
|
|
@@ -271,10 +293,12 @@ async function renderViaReadableStream(
|
|
|
271
293
|
deferSuspenseFor?: number;
|
|
272
294
|
signal?: AbortSignal;
|
|
273
295
|
renderTimeoutMs?: number;
|
|
296
|
+
onFizzAllReadyResolved?: (resolved: boolean) => void;
|
|
274
297
|
}
|
|
275
298
|
): Promise<ReadableStream<Uint8Array>> {
|
|
276
299
|
const signal = options?.signal;
|
|
277
300
|
const timeoutMs = options?.renderTimeoutMs;
|
|
301
|
+
const onFizzAllReadyResolved = options?.onFizzAllReadyResolved;
|
|
278
302
|
|
|
279
303
|
// If a render timeout is configured, create a combined signal that
|
|
280
304
|
// fires on either request abort OR timeout — whichever comes first.
|
|
@@ -329,13 +353,25 @@ async function renderViaReadableStream(
|
|
|
329
353
|
// deferSuspenseFor hold: delay the first read so React can resolve
|
|
330
354
|
// fast-completing Suspense boundaries before we read the shell HTML.
|
|
331
355
|
// See design/05-streaming.md §"deferSuspenseFor"
|
|
356
|
+
//
|
|
357
|
+
// While we're holding, also track whether `stream.allReady` settled
|
|
358
|
+
// before the timer. That boolean is the Option-1 signal-promotion
|
|
359
|
+
// short-circuit key — see NavContext._fizzAllReadyResolved and
|
|
360
|
+
// design/spike-signal-promotion-protocol.md.
|
|
361
|
+
let fizzAllReadyResolved = false;
|
|
362
|
+
const allReadyTap = stream.allReady.then(
|
|
363
|
+
() => {
|
|
364
|
+
fizzAllReadyResolved = true;
|
|
365
|
+
},
|
|
366
|
+
() => {
|
|
367
|
+
// allReady rejections are already swallowed above; keep false.
|
|
368
|
+
}
|
|
369
|
+
);
|
|
332
370
|
const deferMs = options?.deferSuspenseFor;
|
|
333
371
|
if (deferMs && deferMs > 0) {
|
|
334
|
-
await Promise.race([
|
|
335
|
-
stream.allReady,
|
|
336
|
-
new Promise<void>((resolve) => setTimeout(resolve, deferMs)),
|
|
337
|
-
]);
|
|
372
|
+
await Promise.race([allReadyTap, new Promise<void>((resolve) => setTimeout(resolve, deferMs))]);
|
|
338
373
|
}
|
|
374
|
+
onFizzAllReadyResolved?.(fizzAllReadyResolved);
|
|
339
375
|
|
|
340
376
|
return wrapStreamWithErrorHandling(stream, signal);
|
|
341
377
|
}
|