@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.
- package/dist/cjs/ssr/createRequestHandler.cjs +32 -24
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +19 -3
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.d.cts +1 -0
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +54 -14
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/esm/ssr/createRequestHandler.js +32 -24
- package/dist/esm/ssr/createRequestHandler.js.map +1 -1
- package/dist/esm/ssr/ssr-server.d.ts +1 -0
- package/dist/esm/ssr/ssr-server.js +19 -3
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +54 -14
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/package.json +1 -1
- package/src/ssr/createRequestHandler.ts +40 -26
- package/src/ssr/ssr-server.ts +28 -4
- package/src/ssr/transformStreamWithRouter.ts +80 -17
|
@@ -35,7 +35,7 @@ type ReadablePassthrough = {
|
|
|
35
35
|
destroyed: boolean
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function createPassthrough(onCancel
|
|
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
|
-
|
|
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
|
|
130
|
+
let isAppRendering = true
|
|
116
131
|
let routerStreamBuffer = ''
|
|
117
132
|
let pendingClosingTags = ''
|
|
118
|
-
let streamBarrierLifted = false
|
|
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(
|
|
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) +
|
|
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
|
},
|