@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.
Files changed (68) hide show
  1. package/dist/adapters/nitro.d.ts.map +1 -1
  2. package/dist/adapters/nitro.js +27 -4
  3. package/dist/adapters/nitro.js.map +1 -1
  4. package/dist/cache/index.d.ts +5 -2
  5. package/dist/cache/index.d.ts.map +1 -1
  6. package/dist/cache/index.js +37 -8
  7. package/dist/cache/index.js.map +1 -1
  8. package/dist/cache/singleflight.d.ts +18 -1
  9. package/dist/cache/singleflight.d.ts.map +1 -1
  10. package/dist/cache/timber-cache.d.ts.map +1 -1
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +53 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  16. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  17. package/dist/plugins/entries.d.ts.map +1 -1
  18. package/dist/server/action-handler.d.ts.map +1 -1
  19. package/dist/server/default-logger.d.ts +22 -0
  20. package/dist/server/default-logger.d.ts.map +1 -0
  21. package/dist/server/flush.d.ts.map +1 -1
  22. package/dist/server/html-injectors.d.ts +2 -2
  23. package/dist/server/html-injectors.d.ts.map +1 -1
  24. package/dist/server/index.d.ts +2 -0
  25. package/dist/server/index.d.ts.map +1 -1
  26. package/dist/server/index.js +135 -24
  27. package/dist/server/index.js.map +1 -1
  28. package/dist/server/logger.d.ts +24 -7
  29. package/dist/server/logger.d.ts.map +1 -1
  30. package/dist/server/node-stream-transforms.d.ts +13 -1
  31. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  32. package/dist/server/render-timeout.d.ts +51 -0
  33. package/dist/server/render-timeout.d.ts.map +1 -0
  34. package/dist/server/route-handler.d.ts.map +1 -1
  35. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  36. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  37. package/dist/server/rsc-entry/index.d.ts +6 -1
  38. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  39. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  40. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -0
  41. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  42. package/dist/server/ssr-entry.d.ts.map +1 -1
  43. package/dist/server/ssr-render.d.ts +2 -0
  44. package/dist/server/ssr-render.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/adapters/nitro.ts +27 -4
  47. package/src/cache/index.ts +5 -2
  48. package/src/cache/singleflight.ts +62 -4
  49. package/src/cache/timber-cache.ts +17 -16
  50. package/src/index.ts +12 -0
  51. package/src/plugins/dev-error-overlay.ts +70 -1
  52. package/src/plugins/dev-server.ts +38 -4
  53. package/src/plugins/entries.ts +1 -0
  54. package/src/server/action-handler.ts +3 -2
  55. package/src/server/default-logger.ts +95 -0
  56. package/src/server/flush.ts +2 -1
  57. package/src/server/html-injectors.ts +32 -7
  58. package/src/server/index.ts +4 -0
  59. package/src/server/logger.ts +38 -35
  60. package/src/server/node-stream-transforms.ts +51 -14
  61. package/src/server/render-timeout.ts +108 -0
  62. package/src/server/route-handler.ts +2 -1
  63. package/src/server/rsc-entry/helpers.ts +122 -3
  64. package/src/server/rsc-entry/index.ts +34 -4
  65. package/src/server/rsc-entry/rsc-payload.ts +11 -3
  66. package/src/server/rsc-entry/rsc-stream.ts +24 -3
  67. package/src/server/ssr-entry.ts +9 -2
  68. package/src/server/ssr-render.ts +105 -16
@@ -23,7 +23,8 @@
23
23
  import type { ReactNode } from 'react';
24
24
  import { renderToReadableStream } from 'react-dom/server';
25
25
 
26
- import { formatSsrError } from './error-formatter.js';
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?: { bootstrapScriptContent?: string; deferSuspenseFor?: number; signal?: AbortSignal }
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?: { bootstrapScriptContent?: string; deferSuspenseFor?: number; signal?: AbortSignal }
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
- const allReady = new Promise<void>((r) => {
143
- allReadyResolve = r;
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
- console.error('[timber] SSR render error:', formatSsrError(error));
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?: { bootstrapScriptContent?: string; deferSuspenseFor?: number; signal?: AbortSignal }
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 stream = await renderToReadableStream(element, {
206
- bootstrapScriptContent: options?.bootstrapScriptContent || undefined,
207
- signal,
208
- onError(error: unknown) {
209
- if (isAbortError(error) || signal?.aborted) return;
210
- console.error('[timber] SSR render error:', formatSsrError(error));
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
- console.error('[timber] SSR streaming error (post-shell):', formatSsrError(error));
357
+ logStreamingError({ error });
269
358
  controller.enqueue(encoder.encode(NOINDEX_SCRIPT));
270
359
  controller.close();
271
360
  }