@tanstack/router-core 1.157.3 → 1.157.5

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.
@@ -1,10 +1,12 @@
1
1
  import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'
2
2
  import invariant from 'tiny-invariant'
3
3
  import { decodePath } from '../utils'
4
+ import { createLRUCache } from '../lru-cache'
4
5
  import minifiedTsrBootStrapScript from './tsrScript?script-string'
5
6
  import { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'
6
7
  import { defaultSerovalPlugins } from './serializer/seroval-plugins'
7
8
  import { makeSsrSerovalPlugin } from './serializer/transformer'
9
+ import type { LRUCache } from '../lru-cache'
8
10
  import type { DehydratedMatch, DehydratedRouter } from './types'
9
11
  import type { AnySerializationAdapter } from './serializer/transformer'
10
12
  import type { AnyRouter } from '../router'
@@ -28,6 +30,10 @@ declare module '../router' {
28
30
 
29
31
  const SCOPE_ID = 'tsr'
30
32
 
33
+ const TSR_PREFIX = GLOBAL_TSR + '.router='
34
+ const P_PREFIX = GLOBAL_TSR + '.p(()=>'
35
+ const P_SUFFIX = ')'
36
+
31
37
  export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
32
38
  const dehydratedMatch: DehydratedMatch = {
33
39
  i: match.id,
@@ -116,6 +122,10 @@ class ScriptBuffer {
116
122
  if (bufferedScripts.length === 0) {
117
123
  return undefined
118
124
  }
125
+ // Optimization: if only one script, avoid join
126
+ if (bufferedScripts.length === 1) {
127
+ return bufferedScripts[0] + ';document.currentScript.remove()'
128
+ }
119
129
  // Append cleanup script and join - avoid push() to not mutate then iterate
120
130
  return bufferedScripts.join(';') + ';document.currentScript.remove()'
121
131
  }
@@ -137,6 +147,23 @@ class ScriptBuffer {
137
147
  }
138
148
  }
139
149
 
150
+ const isProd = process.env.NODE_ENV === 'production'
151
+
152
+ type FilteredRoutes = Manifest['routes']
153
+
154
+ type ManifestLRU = LRUCache<string, FilteredRoutes>
155
+
156
+ const MANIFEST_CACHE_SIZE = 100
157
+ const manifestCaches = new WeakMap<Manifest, ManifestLRU>()
158
+
159
+ function getManifestCache(manifest: Manifest): ManifestLRU {
160
+ const cache = manifestCaches.get(manifest)
161
+ if (cache) return cache
162
+ const newCache = createLRUCache<string, FilteredRoutes>(MANIFEST_CACHE_SIZE)
163
+ manifestCaches.set(manifest, newCache)
164
+ return newCache
165
+ }
166
+
140
167
  export function attachRouterServerSsrUtils({
141
168
  router,
142
169
  manifest,
@@ -152,13 +179,13 @@ export function attachRouterServerSsrUtils({
152
179
  const renderFinishedListeners: Array<() => void> = []
153
180
  const serializationFinishedListeners: Array<() => void> = []
154
181
  const scriptBuffer = new ScriptBuffer(router)
155
- let injectedHtmlBuffer: Array<string> = []
182
+ let injectedHtmlBuffer = ''
156
183
 
157
184
  router.serverSsr = {
158
185
  injectHtml: (html: string) => {
159
186
  if (!html) return
160
187
  // Buffer the HTML so it can be retrieved via takeBufferedHtml()
161
- injectedHtmlBuffer.push(html)
188
+ injectedHtmlBuffer += html
162
189
  // Emit event to notify subscribers that new HTML is available
163
190
  router.emit({
164
191
  type: 'onInjectedHtml',
@@ -182,31 +209,41 @@ export function attachRouterServerSsrUtils({
182
209
  // For currently matched routes, send full manifest (preloads + assets)
183
210
  // For all other routes, only send assets (no preloads as they are handled via dynamic imports)
184
211
  if (manifest) {
185
- const currentRouteIds = new Set(
186
- router.state.matches.map((k) => k.routeId),
187
- )
188
- const filteredRoutes = Object.fromEntries(
189
- Object.entries(manifest.routes).flatMap(
190
- ([routeId, routeManifest]) => {
191
- if (currentRouteIds.has(routeId)) {
192
- return [[routeId, routeManifest]]
193
- } else if (
194
- routeManifest.assets &&
195
- routeManifest.assets.length > 0
196
- ) {
197
- return [
198
- [
199
- routeId,
200
- {
201
- assets: routeManifest.assets,
202
- },
203
- ],
204
- ]
212
+ // Prod-only caching; in dev manifests may be replaced/updated (HMR)
213
+ const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)
214
+ const manifestCacheKey = currentRouteIdsList.join('\0')
215
+
216
+ let filteredRoutes: FilteredRoutes | undefined
217
+
218
+ if (isProd) {
219
+ filteredRoutes = getManifestCache(manifest).get(manifestCacheKey)
220
+ }
221
+
222
+ if (!filteredRoutes) {
223
+ const currentRouteIds = new Set(currentRouteIdsList)
224
+ const nextFilteredRoutes: FilteredRoutes = {}
225
+
226
+ for (const routeId in manifest.routes) {
227
+ const routeManifest = manifest.routes[routeId]!
228
+ if (currentRouteIds.has(routeId)) {
229
+ nextFilteredRoutes[routeId] = routeManifest
230
+ } else if (
231
+ routeManifest.assets &&
232
+ routeManifest.assets.length > 0
233
+ ) {
234
+ nextFilteredRoutes[routeId] = {
235
+ assets: routeManifest.assets,
205
236
  }
206
- return []
207
- },
208
- ),
209
- )
237
+ }
238
+ }
239
+
240
+ if (isProd) {
241
+ getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)
242
+ }
243
+
244
+ filteredRoutes = nextFilteredRoutes
245
+ }
246
+
210
247
  manifestToDehydrate = {
211
248
  routes: filteredRoutes,
212
249
  }
@@ -252,9 +289,9 @@ export function attachRouterServerSsrUtils({
252
289
  refs: new Map(),
253
290
  plugins,
254
291
  onSerialize: (data, initial) => {
255
- let serialized = initial ? GLOBAL_TSR + '.router=' + data : data
292
+ let serialized = initial ? TSR_PREFIX + data : data
256
293
  if (trackPlugins.didRun) {
257
- serialized = GLOBAL_TSR + '.p(()=>' + serialized + ')'
294
+ serialized = P_PREFIX + serialized + P_SUFFIX
258
295
  }
259
296
  scriptBuffer.enqueue(serialized)
260
297
  },
@@ -310,11 +347,11 @@ export function attachRouterServerSsrUtils({
310
347
  scriptBuffer.liftBarrier()
311
348
  },
312
349
  takeBufferedHtml() {
313
- if (injectedHtmlBuffer.length === 0) {
350
+ if (!injectedHtmlBuffer) {
314
351
  return undefined
315
352
  }
316
- const buffered = injectedHtmlBuffer.join('')
317
- injectedHtmlBuffer = []
353
+ const buffered = injectedHtmlBuffer
354
+ injectedHtmlBuffer = ''
318
355
  return buffered
319
356
  },
320
357
  cleanup() {
@@ -322,7 +359,7 @@ export function attachRouterServerSsrUtils({
322
359
  if (!router.serverSsr) return
323
360
  renderFinishedListeners.length = 0
324
361
  serializationFinishedListeners.length = 0
325
- injectedHtmlBuffer = []
362
+ injectedHtmlBuffer = ''
326
363
  scriptBuffer.cleanup()
327
364
  router.serverSsr = undefined
328
365
  },
@@ -107,60 +107,136 @@ export function transformStreamWithRouter(
107
107
  lifetimeMs?: number
108
108
  },
109
109
  ) {
110
- let stopListeningToInjectedHtml: (() => void) | undefined
111
- let stopListeningToSerializationFinished: (() => void) | undefined
112
- let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined
113
- let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
114
- let cleanedUp = false
115
-
116
- let controller: ReadableStreamDefaultController<any>
117
- let isStreamClosed = false
118
-
119
110
  // Check upfront if serialization already finished synchronously
120
111
  // This is the fast path for routes with no deferred data
121
112
  const serializationAlreadyFinished =
122
113
  router.serverSsr?.isSerializationFinished() ?? false
123
114
 
124
- /**
125
- * Cleanup function with guards against multiple calls.
126
- * Unsubscribes listeners, clears timeouts, frees buffers, and cleans up router SSR state.
127
- */
128
- function cleanup() {
129
- // Guard against multiple cleanup calls - set flag first to prevent re-entry
130
- if (cleanedUp) return
131
- cleanedUp = true
115
+ // Take any HTML that was buffered before we started listening
116
+ const initialBufferedHtml = router.serverSsr?.takeBufferedHtml()
132
117
 
133
- // Unsubscribe listeners first (wrap in try-catch for safety)
134
- try {
135
- stopListeningToInjectedHtml?.()
136
- stopListeningToSerializationFinished?.()
137
- } catch (e) {
138
- // Ignore errors during unsubscription
118
+ // True passthrough: if serialization already finished and nothing buffered,
119
+ // we can avoid any decoding/scanning while still honoring cleanup + setRenderFinished.
120
+ if (serializationAlreadyFinished && !initialBufferedHtml) {
121
+ let cleanedUp = false
122
+ let controller: ReadableStreamDefaultController<Uint8Array> | undefined
123
+ let isStreamClosed = false
124
+ let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
125
+
126
+ const cleanup = () => {
127
+ if (cleanedUp) return
128
+ cleanedUp = true
129
+
130
+ if (lifetimeTimeoutHandle !== undefined) {
131
+ clearTimeout(lifetimeTimeoutHandle)
132
+ lifetimeTimeoutHandle = undefined
133
+ }
134
+
135
+ router.serverSsr?.cleanup()
139
136
  }
140
- stopListeningToInjectedHtml = undefined
141
- stopListeningToSerializationFinished = undefined
142
137
 
143
- // Clear all timeouts
144
- if (serializationTimeoutHandle !== undefined) {
145
- clearTimeout(serializationTimeoutHandle)
146
- serializationTimeoutHandle = undefined
138
+ const safeClose = () => {
139
+ if (isStreamClosed) return
140
+ isStreamClosed = true
141
+ try {
142
+ controller?.close()
143
+ } catch {
144
+ // ignore
145
+ }
147
146
  }
148
- if (lifetimeTimeoutHandle !== undefined) {
149
- clearTimeout(lifetimeTimeoutHandle)
150
- lifetimeTimeoutHandle = undefined
147
+
148
+ const safeError = (error: unknown) => {
149
+ if (isStreamClosed) return
150
+ isStreamClosed = true
151
+ try {
152
+ controller?.error(error)
153
+ } catch {
154
+ // ignore
155
+ }
151
156
  }
152
157
 
153
- // Clear buffers to free memory
154
- pendingRouterHtmlParts = []
155
- leftover = ''
156
- pendingClosingTags = ''
158
+ const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS
159
+ lifetimeTimeoutHandle = setTimeout(() => {
160
+ if (!cleanedUp && !isStreamClosed) {
161
+ console.warn(
162
+ `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,
163
+ )
164
+ safeError(new Error('Stream lifetime exceeded'))
165
+ cleanup()
166
+ }
167
+ }, lifetimeMs)
157
168
 
158
- // Clean up router SSR state (has its own guard)
159
- router.serverSsr?.cleanup()
169
+ const stream = new ReadableStream<Uint8Array>({
170
+ start(c) {
171
+ controller = c
172
+ },
173
+ cancel() {
174
+ isStreamClosed = true
175
+ cleanup()
176
+ },
177
+ })
178
+
179
+ ;(async () => {
180
+ const reader = appStream.getReader()
181
+ try {
182
+ while (true) {
183
+ const { done, value } = await reader.read()
184
+ if (done) break
185
+ if (cleanedUp || isStreamClosed) return
186
+ controller?.enqueue(value as unknown as Uint8Array)
187
+ }
188
+
189
+ if (cleanedUp || isStreamClosed) return
190
+
191
+ router.serverSsr?.setRenderFinished()
192
+ safeClose()
193
+ cleanup()
194
+ } catch (error) {
195
+ if (cleanedUp) return
196
+ console.error('Error reading appStream:', error)
197
+ router.serverSsr?.setRenderFinished()
198
+ safeError(error)
199
+ cleanup()
200
+ } finally {
201
+ reader.releaseLock()
202
+ }
203
+ })().catch((error) => {
204
+ if (cleanedUp) return
205
+ console.error('Error in stream transform:', error)
206
+ safeError(error)
207
+ cleanup()
208
+ })
209
+
210
+ return stream
160
211
  }
161
212
 
213
+ let stopListeningToInjectedHtml: (() => void) | undefined
214
+ let stopListeningToSerializationFinished: (() => void) | undefined
215
+ let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined
216
+ let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
217
+ let cleanedUp = false
218
+
219
+ let controller: ReadableStreamDefaultController<any>
220
+ let isStreamClosed = false
221
+
162
222
  const textDecoder = new TextDecoder()
163
223
 
224
+ // concat'd router HTML; avoids array joins on each flush
225
+ let pendingRouterHtml = initialBufferedHtml ?? ''
226
+
227
+ // between-chunk text buffer; keep bounded to avoid unbounded memory
228
+ let leftover = ''
229
+
230
+ // captured closing tags from </body> onward
231
+ let pendingClosingTags = ''
232
+
233
+ // conservative cap: enough to hold any partial closing tag + a bit
234
+ const MAX_LEFTOVER_CHARS = 2048
235
+
236
+ let isAppRendering = true
237
+ let streamBarrierLifted = false
238
+ let serializationFinished = serializationAlreadyFinished
239
+
164
240
  function safeEnqueue(chunk: string | Uint8Array) {
165
241
  if (isStreamClosed) return
166
242
  if (typeof chunk === 'string') {
@@ -176,7 +252,7 @@ export function transformStreamWithRouter(
176
252
  try {
177
253
  controller.close()
178
254
  } catch {
179
- // Stream may already be errored or closed by consumer - safe to ignore
255
+ // ignore
180
256
  }
181
257
  }
182
258
 
@@ -186,10 +262,42 @@ export function transformStreamWithRouter(
186
262
  try {
187
263
  controller.error(error)
188
264
  } catch {
189
- // Stream may already be errored or closed by consumer - safe to ignore
265
+ // ignore
190
266
  }
191
267
  }
192
268
 
269
+ /**
270
+ * Cleanup with guards; must be idempotent.
271
+ */
272
+ function cleanup() {
273
+ if (cleanedUp) return
274
+ cleanedUp = true
275
+
276
+ try {
277
+ stopListeningToInjectedHtml?.()
278
+ stopListeningToSerializationFinished?.()
279
+ } catch {
280
+ // ignore
281
+ }
282
+ stopListeningToInjectedHtml = undefined
283
+ stopListeningToSerializationFinished = undefined
284
+
285
+ if (serializationTimeoutHandle !== undefined) {
286
+ clearTimeout(serializationTimeoutHandle)
287
+ serializationTimeoutHandle = undefined
288
+ }
289
+ if (lifetimeTimeoutHandle !== undefined) {
290
+ clearTimeout(lifetimeTimeoutHandle)
291
+ lifetimeTimeoutHandle = undefined
292
+ }
293
+
294
+ pendingRouterHtml = ''
295
+ leftover = ''
296
+ pendingClosingTags = ''
297
+
298
+ router.serverSsr?.cleanup()
299
+ }
300
+
193
301
  const stream = new ReadableStream({
194
302
  start(c) {
195
303
  controller = c
@@ -200,36 +308,24 @@ export function transformStreamWithRouter(
200
308
  },
201
309
  })
202
310
 
203
- let isAppRendering = true
204
- let streamBarrierLifted = false
205
- let leftover = ''
206
- let pendingClosingTags = ''
207
- let serializationFinished = serializationAlreadyFinished
208
-
209
- let pendingRouterHtmlParts: Array<string> = []
210
-
211
- // Take any HTML that was buffered before we started listening
212
- const bufferedHtml = router.serverSsr?.takeBufferedHtml()
213
- if (bufferedHtml) {
214
- pendingRouterHtmlParts.push(bufferedHtml)
311
+ function flushPendingRouterHtml() {
312
+ if (!pendingRouterHtml) return
313
+ safeEnqueue(pendingRouterHtml)
314
+ pendingRouterHtml = ''
215
315
  }
216
316
 
217
- function flushPendingRouterHtml() {
218
- if (pendingRouterHtmlParts.length > 0) {
219
- safeEnqueue(pendingRouterHtmlParts.join(''))
220
- pendingRouterHtmlParts = []
221
- }
317
+ function appendRouterHtml(html: string) {
318
+ if (!html) return
319
+ pendingRouterHtml += html
222
320
  }
223
321
 
224
322
  /**
225
- * Attempts to finish the stream if all conditions are met.
323
+ * Finish only when app done and serialization complete.
226
324
  */
227
325
  function tryFinish() {
228
- // Can only finish when app is done rendering and serialization is complete
229
326
  if (isAppRendering || !serializationFinished) return
230
327
  if (cleanedUp || isStreamClosed) return
231
328
 
232
- // Clear serialization timeout since we're finishing
233
329
  if (serializationTimeoutHandle !== undefined) {
234
330
  clearTimeout(serializationTimeoutHandle)
235
331
  serializationTimeoutHandle = undefined
@@ -247,8 +343,7 @@ export function transformStreamWithRouter(
247
343
  cleanup()
248
344
  }
249
345
 
250
- // Set up lifetime timeout as a safety net
251
- // This ensures cleanup happens even if the stream is never consumed or gets stuck
346
+ // Safety net: cleanup even if consumer never reads
252
347
  const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS
253
348
  lifetimeTimeoutHandle = setTimeout(() => {
254
349
  if (!cleanedUp && !isStreamClosed) {
@@ -260,27 +355,21 @@ export function transformStreamWithRouter(
260
355
  }
261
356
  }, lifetimeMs)
262
357
 
263
- // Only set up listeners if serialization hasn't already finished
264
- // This avoids unnecessary subscriptions for the common case of no deferred data
265
358
  if (!serializationAlreadyFinished) {
266
- // Listen for injected HTML (for deferred data that resolves later)
267
359
  stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {
268
360
  if (cleanedUp || isStreamClosed) return
269
-
270
- // Retrieve buffered HTML
271
361
  const html = router.serverSsr?.takeBufferedHtml()
272
362
  if (!html) return
273
363
 
274
- if (isAppRendering) {
275
- // Buffer for insertion at next valid position
276
- pendingRouterHtmlParts.push(html)
364
+ // If we've already captured </body> (pendingClosingTags), we must keep appending
365
+ // so injection stays before the stored closing tags.
366
+ if (isAppRendering || leftover || pendingClosingTags) {
367
+ appendRouterHtml(html)
277
368
  } else {
278
- // App is done rendering, write directly to output
279
369
  safeEnqueue(html)
280
370
  }
281
371
  })
282
372
 
283
- // Listen for serialization finished
284
373
  stopListeningToSerializationFinished = router.subscribe(
285
374
  'onSerializationFinished',
286
375
  () => {
@@ -298,16 +387,16 @@ export function transformStreamWithRouter(
298
387
  const { done, value } = await reader.read()
299
388
  if (done) break
300
389
 
301
- // Don't process if already cleaned up
302
390
  if (cleanedUp || isStreamClosed) return
303
391
 
304
392
  const text =
305
393
  value instanceof Uint8Array
306
394
  ? textDecoder.decode(value, { stream: true })
307
395
  : String(value)
308
- const chunkString = leftover + text
309
396
 
310
- // Check for stream barrier (script placeholder) - use indexOf for efficiency
397
+ // Fast path: most chunks have no pending left-over.
398
+ const chunkString = leftover ? leftover + text : text
399
+
311
400
  if (!streamBarrierLifted) {
312
401
  if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {
313
402
  streamBarrierLifted = true
@@ -315,70 +404,63 @@ export function transformStreamWithRouter(
315
404
  }
316
405
  }
317
406
 
318
- // Check for body/html end tags
407
+ // If we already saw </body>, everything else is part of tail; buffer it.
408
+ if (pendingClosingTags) {
409
+ pendingClosingTags += chunkString
410
+ leftover = ''
411
+ continue
412
+ }
413
+
319
414
  const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)
320
415
  const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)
321
416
 
322
- // If we have both </body> and </html> in proper order,
323
- // insert router HTML before </body> and hold the closing tags
324
417
  if (
325
418
  bodyEndIndex !== -1 &&
326
419
  htmlEndIndex !== -1 &&
327
420
  bodyEndIndex < htmlEndIndex
328
421
  ) {
329
422
  pendingClosingTags = chunkString.slice(bodyEndIndex)
330
-
331
423
  safeEnqueue(chunkString.slice(0, bodyEndIndex))
332
424
  flushPendingRouterHtml()
333
-
334
425
  leftover = ''
335
426
  continue
336
427
  }
337
428
 
338
- // Handling partial closing tags split across chunks:
339
- //
340
- // Since `chunkString = leftover + text`, any incomplete tag fragment from the
341
- // previous chunk is prepended to the current chunk, allowing split tags like
342
- // "</di" + "v>" to be re-detected as a complete "</div>" in the combined string.
343
- //
344
- // - If a closing tag IS found (lastClosingTagEnd > 0): We enqueue content up to
345
- // the end of that tag, flush router HTML, and store the remainder in `leftover`.
346
- // This remainder may contain a partial tag (e.g., "</sp") which will be
347
- // prepended to the next chunk for re-detection.
348
- //
349
- // - If NO closing tag is found: The entire chunk is buffered in `leftover` and
350
- // will be prepended to the next chunk. This ensures partial tags are never
351
- // lost and will be detected once the rest of the tag arrives.
352
- //
353
- // This approach guarantees correct injection points even when closing tags span
354
- // chunk boundaries.
355
429
  const lastClosingTagEnd = findLastClosingTagEnd(chunkString)
356
430
 
357
431
  if (lastClosingTagEnd > 0) {
358
- // Found a closing tag - insert router HTML after it
359
432
  safeEnqueue(chunkString.slice(0, lastClosingTagEnd))
360
433
  flushPendingRouterHtml()
361
434
 
362
435
  leftover = chunkString.slice(lastClosingTagEnd)
436
+ if (leftover.length > MAX_LEFTOVER_CHARS) {
437
+ // Ensure bounded memory even if a consumer streams long text sequences
438
+ // without any closing tags. This may reduce injection granularity but is correct.
439
+ safeEnqueue(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS))
440
+ leftover = leftover.slice(-MAX_LEFTOVER_CHARS)
441
+ }
363
442
  } else {
364
- // No closing tag found - buffer the entire chunk
365
- leftover = chunkString
366
- // Any pending router HTML will be inserted when we find a valid position
443
+ // No closing tag found; keep small tail to handle split closing tags,
444
+ // but stream older bytes to prevent unbounded buffering.
445
+ const combined = chunkString
446
+ if (combined.length > MAX_LEFTOVER_CHARS) {
447
+ const flushUpto = combined.length - MAX_LEFTOVER_CHARS
448
+ safeEnqueue(combined.slice(0, flushUpto))
449
+ leftover = combined.slice(flushUpto)
450
+ } else {
451
+ leftover = combined
452
+ }
367
453
  }
368
454
  }
369
455
 
370
- // Stream ended
371
456
  if (cleanedUp || isStreamClosed) return
372
457
 
373
- // Mark the app as done rendering
374
458
  isAppRendering = false
375
459
  router.serverSsr?.setRenderFinished()
376
460
 
377
- // Try to finish if serialization is already done
378
461
  if (serializationFinished) {
379
462
  tryFinish()
380
463
  } else {
381
- // Set a timeout for serialization to complete
382
464
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS
383
465
  serializationTimeoutHandle = setTimeout(() => {
384
466
  if (!cleanedUp && !isStreamClosed) {
@@ -401,7 +483,6 @@ export function transformStreamWithRouter(
401
483
  reader.releaseLock()
402
484
  }
403
485
  })().catch((error) => {
404
- // Handle any errors that occur outside the try block (e.g., getReader() failure)
405
486
  if (cleanedUp) return
406
487
  console.error('Error in stream transform:', error)
407
488
  safeError(error)