@timber-js/app 0.2.0-alpha.37 → 0.2.0-alpha.39
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 +37 -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/action-handler.d.ts.map +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/flush.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 +135 -24
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -7
- package/dist/server/logger.d.ts.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/route-handler.d.ts.map +1 -1
- 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 +62 -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/action-handler.ts +3 -2
- package/src/server/default-logger.ts +95 -0
- package/src/server/flush.ts +2 -1
- package/src/server/html-injectors.ts +32 -7
- package/src/server/index.ts +4 -0
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +51 -14
- package/src/server/render-timeout.ts +108 -0
- package/src/server/route-handler.ts +2 -1
- 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 +105 -16
package/src/server/ssr-render.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import type { ReactNode } from 'react';
|
|
24
24
|
import { renderToReadableStream } from 'react-dom/server';
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { createRenderTimeout, RenderTimeoutError } from './render-timeout.js';
|
|
27
|
+
import { logRenderError, logStreamingError } from './logger.js';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Inline script that injects <meta name="robots" content="noindex"> into <head>.
|
|
@@ -105,7 +106,12 @@ try {
|
|
|
105
106
|
*/
|
|
106
107
|
export async function renderSsrStream(
|
|
107
108
|
element: ReactNode,
|
|
108
|
-
options?: {
|
|
109
|
+
options?: {
|
|
110
|
+
bootstrapScriptContent?: string;
|
|
111
|
+
deferSuspenseFor?: number;
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
renderTimeoutMs?: number;
|
|
114
|
+
}
|
|
109
115
|
): Promise<ReadableStream<Uint8Array>> {
|
|
110
116
|
return renderViaReadableStream(element, options);
|
|
111
117
|
}
|
|
@@ -130,17 +136,25 @@ export const useNodeStreams = _useNodeStreams;
|
|
|
130
136
|
*/
|
|
131
137
|
export async function renderSsrNodeStream(
|
|
132
138
|
element: ReactNode,
|
|
133
|
-
options?: {
|
|
139
|
+
options?: {
|
|
140
|
+
bootstrapScriptContent?: string;
|
|
141
|
+
deferSuspenseFor?: number;
|
|
142
|
+
signal?: AbortSignal;
|
|
143
|
+
renderTimeoutMs?: number;
|
|
144
|
+
}
|
|
134
145
|
): Promise<import('node:stream').Readable> {
|
|
135
146
|
const signal = options?.signal;
|
|
136
147
|
const deferMs = options?.deferSuspenseFor;
|
|
148
|
+
const timeoutMs = options?.renderTimeoutMs;
|
|
137
149
|
|
|
138
150
|
return new Promise<import('node:stream').Readable>((resolve, reject) => {
|
|
139
151
|
const passthrough = new _PassThrough!();
|
|
140
152
|
|
|
141
153
|
let allReadyResolve: (() => void) | null = null;
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
let allReadyReject: ((reason?: unknown) => void) | null = null;
|
|
155
|
+
const allReady = new Promise<void>((resolve, reject) => {
|
|
156
|
+
allReadyResolve = resolve;
|
|
157
|
+
allReadyReject = reject;
|
|
144
158
|
});
|
|
145
159
|
allReady.catch(() => {});
|
|
146
160
|
|
|
@@ -164,15 +178,22 @@ export async function renderSsrNodeStream(
|
|
|
164
178
|
},
|
|
165
179
|
|
|
166
180
|
onShellError(error: unknown) {
|
|
181
|
+
// Reject allReady so the render timeout is cancelled.
|
|
182
|
+
// Without this, a pre-shell failure leaves the timer
|
|
183
|
+
// running for the full timeout window.
|
|
184
|
+
allReadyReject?.(error);
|
|
167
185
|
reject(error);
|
|
168
186
|
},
|
|
169
187
|
|
|
170
188
|
onError(error: unknown) {
|
|
171
189
|
if (isAbortError(error) || signal?.aborted) return;
|
|
172
|
-
|
|
190
|
+
logRenderError({ method: '', path: '', error });
|
|
173
191
|
},
|
|
174
192
|
});
|
|
175
193
|
|
|
194
|
+
// Wire abort to both request signal AND render timeout.
|
|
195
|
+
// If the client stays connected but a downstream fetch hangs,
|
|
196
|
+
// the timeout ensures abort() is eventually called.
|
|
176
197
|
if (signal) {
|
|
177
198
|
if (signal.aborted) {
|
|
178
199
|
abort();
|
|
@@ -180,6 +201,32 @@ export async function renderSsrNodeStream(
|
|
|
180
201
|
signal.addEventListener('abort', () => abort(), { once: true });
|
|
181
202
|
}
|
|
182
203
|
}
|
|
204
|
+
|
|
205
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
206
|
+
const renderTimeout = createRenderTimeout(timeoutMs, signal);
|
|
207
|
+
renderTimeout.signal.addEventListener(
|
|
208
|
+
'abort',
|
|
209
|
+
() => {
|
|
210
|
+
logRenderError({
|
|
211
|
+
method: '',
|
|
212
|
+
path: '',
|
|
213
|
+
error: new Error(
|
|
214
|
+
`SSR render timed out after ${timeoutMs}ms — aborting. ` +
|
|
215
|
+
'A Suspense component or downstream fetch may be hanging.'
|
|
216
|
+
),
|
|
217
|
+
});
|
|
218
|
+
abort(renderTimeout.signal.reason);
|
|
219
|
+
},
|
|
220
|
+
{ once: true }
|
|
221
|
+
);
|
|
222
|
+
// Cancel the timeout when the render completes OR on pre-shell
|
|
223
|
+
// failure. Without the catch branch, onShellError → reject()
|
|
224
|
+
// would leave the timer running for the full timeout window.
|
|
225
|
+
allReady.then(
|
|
226
|
+
() => renderTimeout.cancel(),
|
|
227
|
+
() => renderTimeout.cancel()
|
|
228
|
+
);
|
|
229
|
+
}
|
|
183
230
|
});
|
|
184
231
|
}
|
|
185
232
|
|
|
@@ -199,21 +246,63 @@ export function nodeReadableToWeb(
|
|
|
199
246
|
|
|
200
247
|
async function renderViaReadableStream(
|
|
201
248
|
element: ReactNode,
|
|
202
|
-
options?: {
|
|
249
|
+
options?: {
|
|
250
|
+
bootstrapScriptContent?: string;
|
|
251
|
+
deferSuspenseFor?: number;
|
|
252
|
+
signal?: AbortSignal;
|
|
253
|
+
renderTimeoutMs?: number;
|
|
254
|
+
}
|
|
203
255
|
): Promise<ReadableStream<Uint8Array>> {
|
|
204
256
|
const signal = options?.signal;
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
257
|
+
const timeoutMs = options?.renderTimeoutMs;
|
|
258
|
+
|
|
259
|
+
// If a render timeout is configured, create a combined signal that
|
|
260
|
+
// fires on either request abort OR timeout — whichever comes first.
|
|
261
|
+
let renderTimeout: import('./render-timeout.js').RenderTimeout | null = null;
|
|
262
|
+
let effectiveSignal = signal;
|
|
263
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
264
|
+
renderTimeout = createRenderTimeout(timeoutMs, signal);
|
|
265
|
+
effectiveSignal = renderTimeout.signal;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let stream: Awaited<ReturnType<typeof renderToReadableStream>>;
|
|
269
|
+
try {
|
|
270
|
+
stream = await renderToReadableStream(element, {
|
|
271
|
+
bootstrapScriptContent: options?.bootstrapScriptContent || undefined,
|
|
272
|
+
signal: effectiveSignal,
|
|
273
|
+
onError(error: unknown) {
|
|
274
|
+
if (isAbortError(error) || effectiveSignal?.aborted) return;
|
|
275
|
+
if (error instanceof RenderTimeoutError) {
|
|
276
|
+
logRenderError({
|
|
277
|
+
method: '',
|
|
278
|
+
path: '',
|
|
279
|
+
error: new Error(
|
|
280
|
+
`SSR render timed out after ${timeoutMs}ms — aborting. ` +
|
|
281
|
+
'A Suspense component or downstream fetch may be hanging.'
|
|
282
|
+
),
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
logRenderError({ method: '', path: '', error });
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// Pre-shell failure (e.g. RSC stream error). Cancel the render
|
|
291
|
+
// timeout so it doesn't leak a timer + abort callback for the
|
|
292
|
+
// full timeout window. Under repeated pre-shell failures this
|
|
293
|
+
// would accumulate unnecessary timers.
|
|
294
|
+
renderTimeout?.cancel();
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
213
297
|
|
|
214
298
|
// Prevent unhandled promise rejection from streaming-phase errors.
|
|
215
299
|
stream.allReady.catch(() => {});
|
|
216
300
|
|
|
301
|
+
// Cancel the render timeout once allReady resolves (render completed).
|
|
302
|
+
if (renderTimeout) {
|
|
303
|
+
stream.allReady.then(() => renderTimeout!.cancel()).catch(() => renderTimeout!.cancel());
|
|
304
|
+
}
|
|
305
|
+
|
|
217
306
|
// deferSuspenseFor hold: delay the first read so React can resolve
|
|
218
307
|
// fast-completing Suspense boundaries before we read the shell HTML.
|
|
219
308
|
// See design/05-streaming.md §"deferSuspenseFor"
|
|
@@ -265,7 +354,7 @@ export function wrapStreamWithErrorHandling(
|
|
|
265
354
|
controller.close();
|
|
266
355
|
return;
|
|
267
356
|
}
|
|
268
|
-
|
|
357
|
+
logStreamingError({ error });
|
|
269
358
|
controller.enqueue(encoder.encode(NOINDEX_SCRIPT));
|
|
270
359
|
controller.close();
|
|
271
360
|
}
|