@tanstack/router-core 1.139.10 → 1.139.12

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.
@@ -35,7 +35,7 @@ type ReadablePassthrough = {
35
35
  destroyed: boolean
36
36
  }
37
37
 
38
- function createPassthrough(onCancel?: () => void) {
38
+ function createPassthrough(onCancel: () => void) {
39
39
  let controller: ReadableStreamDefaultController<any>
40
40
  const encoder = new TextEncoder()
41
41
  const stream = new ReadableStream({
@@ -44,13 +44,15 @@ function createPassthrough(onCancel?: () => void) {
44
44
  },
45
45
  cancel() {
46
46
  res.destroyed = true
47
- onCancel?.()
47
+ onCancel()
48
48
  },
49
49
  })
50
50
 
51
51
  const res: ReadablePassthrough = {
52
52
  stream,
53
53
  write: (chunk) => {
54
+ // Don't write to destroyed stream
55
+ if (res.destroyed) return
54
56
  if (typeof chunk === 'string') {
55
57
  controller.enqueue(encoder.encode(chunk))
56
58
  } else {
@@ -58,13 +60,18 @@ function createPassthrough(onCancel?: () => void) {
58
60
  }
59
61
  },
60
62
  end: (chunk) => {
63
+ // Don't end already destroyed stream
64
+ if (res.destroyed) return
61
65
  if (chunk) {
62
66
  res.write(chunk)
63
67
  }
64
- controller.close()
65
68
  res.destroyed = true
69
+ controller.close()
66
70
  },
67
71
  destroy: (error) => {
72
+ // Don't destroy already destroyed stream
73
+ if (res.destroyed) return
74
+ res.destroyed = true
68
75
  controller.error(error)
69
76
  },
70
77
  destroyed: false,
@@ -81,8 +88,8 @@ async function readStream(
81
88
  onError?: (error: unknown) => void
82
89
  },
83
90
  ) {
91
+ const reader = stream.getReader()
84
92
  try {
85
- const reader = stream.getReader()
86
93
  let chunk
87
94
  while (!(chunk = await reader.read()).done) {
88
95
  opts.onData?.(chunk)
@@ -90,6 +97,8 @@ async function readStream(
90
97
  opts.onEnd?.()
91
98
  } catch (error) {
92
99
  opts.onError?.(error)
100
+ } finally {
101
+ reader.releaseLock()
93
102
  }
94
103
  }
95
104
 
@@ -102,20 +111,26 @@ export function transformStreamWithRouter(
102
111
  ) {
103
112
  let stopListeningToInjectedHtml: (() => void) | undefined = undefined
104
113
  let timeoutHandle: NodeJS.Timeout
114
+ let cleanedUp = false
105
115
 
106
- const finalPassThrough = createPassthrough(() => {
116
+ function cleanup() {
117
+ if (cleanedUp) return
118
+ cleanedUp = true
107
119
  if (stopListeningToInjectedHtml) {
108
120
  stopListeningToInjectedHtml()
109
121
  stopListeningToInjectedHtml = undefined
110
122
  }
111
123
  clearTimeout(timeoutHandle)
112
- })
124
+ router.serverSsr?.cleanup()
125
+ }
126
+
127
+ const finalPassThrough = createPassthrough(cleanup)
113
128
  const textDecoder = new TextDecoder()
114
129
 
115
- let isAppRendering = true as boolean
130
+ let isAppRendering = true
116
131
  let routerStreamBuffer = ''
117
132
  let pendingClosingTags = ''
118
- let streamBarrierLifted = false as boolean
133
+ let streamBarrierLifted = false
119
134
  let leftover = ''
120
135
  let leftoverHtml = ''
121
136
 
@@ -146,18 +161,27 @@ export function transformStreamWithRouter(
146
161
  )
147
162
 
148
163
  function handleInjectedHtml() {
164
+ // Don't process if already cleaned up
165
+ if (cleanedUp) return
166
+
149
167
  router.serverSsr!.injectedHtml.forEach((promise) => {
150
168
  processingCount++
151
169
 
152
170
  promise
153
171
  .then((html) => {
172
+ // Don't write to destroyed stream or after cleanup
173
+ if (cleanedUp || finalPassThrough.destroyed) {
174
+ return
175
+ }
154
176
  if (isAppRendering) {
155
177
  routerStreamBuffer += html
156
178
  } else {
157
179
  finalPassThrough.write(html)
158
180
  }
159
181
  })
160
- .catch(injectedHtmlDonePromise.reject)
182
+ .catch((err) => {
183
+ injectedHtmlDonePromise.reject(err)
184
+ })
161
185
  .finally(() => {
162
186
  processingCount--
163
187
 
@@ -171,26 +195,40 @@ export function transformStreamWithRouter(
171
195
 
172
196
  injectedHtmlDonePromise
173
197
  .then(() => {
198
+ // Don't process if already cleaned up or destroyed
199
+ if (cleanedUp || finalPassThrough.destroyed) {
200
+ return
201
+ }
202
+
174
203
  clearTimeout(timeoutHandle)
175
204
  const finalHtml =
176
- leftoverHtml + getBufferedRouterStream() + pendingClosingTags
205
+ leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags
206
+
207
+ leftover = ''
208
+ leftoverHtml = ''
209
+ pendingClosingTags = ''
177
210
 
178
211
  finalPassThrough.end(finalHtml)
179
212
  })
180
213
  .catch((err) => {
214
+ // Don't process if already cleaned up
215
+ if (cleanedUp || finalPassThrough.destroyed) {
216
+ return
217
+ }
218
+
181
219
  console.error('Error reading routerStream:', err)
182
220
  finalPassThrough.destroy(err)
183
221
  })
184
- .finally(() => {
185
- if (stopListeningToInjectedHtml) {
186
- stopListeningToInjectedHtml()
187
- stopListeningToInjectedHtml = undefined
188
- }
189
- })
222
+ .finally(cleanup)
190
223
 
191
224
  // Transform the appStream
192
225
  readStream(appStream, {
193
226
  onData: (chunk) => {
227
+ // Don't process if already cleaned up
228
+ if (cleanedUp || finalPassThrough.destroyed) {
229
+ return
230
+ }
231
+
194
232
  const text = decodeChunk(chunk.value)
195
233
  const chunkString = leftover + text
196
234
  const bodyEndMatch = chunkString.match(patternBodyEnd)
@@ -217,15 +255,20 @@ export function transformStreamWithRouter(
217
255
  pendingClosingTags = chunkString.slice(bodyEndIndex)
218
256
 
219
257
  finalPassThrough.write(
220
- chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream(),
258
+ chunkString.slice(0, bodyEndIndex) +
259
+ getBufferedRouterStream() +
260
+ leftoverHtml,
221
261
  )
222
262
 
223
263
  leftover = ''
264
+ leftoverHtml = ''
224
265
  return
225
266
  }
226
267
 
227
268
  let result: RegExpExecArray | null
228
269
  let lastIndex = 0
270
+ // Reset regex lastIndex since it's global and stateful across exec() calls
271
+ patternClosingTag.lastIndex = 0
229
272
  while ((result = patternClosingTag.exec(chunkString)) !== null) {
230
273
  lastIndex = result.index + result[0].length
231
274
  }
@@ -238,12 +281,18 @@ export function transformStreamWithRouter(
238
281
 
239
282
  finalPassThrough.write(processed)
240
283
  leftover = chunkString.slice(lastIndex)
284
+ leftoverHtml = ''
241
285
  } else {
242
286
  leftover = chunkString
243
287
  leftoverHtml += getBufferedRouterStream()
244
288
  }
245
289
  },
246
290
  onEnd: () => {
291
+ // Don't process if stream was already destroyed/cancelled or cleaned up
292
+ if (cleanedUp || finalPassThrough.destroyed) {
293
+ return
294
+ }
295
+
247
296
  // Mark the app as done rendering
248
297
  isAppRendering = false
249
298
  router.serverSsr!.setRenderFinished()
@@ -261,7 +310,21 @@ export function transformStreamWithRouter(
261
310
  }
262
311
  },
263
312
  onError: (error) => {
313
+ // Don't process if already cleaned up
314
+ if (cleanedUp) {
315
+ return
316
+ }
317
+
264
318
  console.error('Error reading appStream:', error)
319
+ isAppRendering = false
320
+ router.serverSsr!.setRenderFinished()
321
+ // Clear timeout to prevent it from firing after error
322
+ clearTimeout(timeoutHandle)
323
+ // Clear string buffers to prevent memory leaks
324
+ leftover = ''
325
+ leftoverHtml = ''
326
+ routerStreamBuffer = ''
327
+ pendingClosingTags = ''
265
328
  finalPassThrough.destroy(error)
266
329
  injectedHtmlDonePromise.reject(error)
267
330
  },