Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions packages/router-core/src/ssr/transformStreamWithRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function createPassthrough(onCancel?: () => void) {
res.destroyed = true
},
destroy: (error) => {
res.destroyed = true
controller.error(error)
},
destroyed: false,
Expand All @@ -81,15 +82,17 @@ async function readStream(
onError?: (error: unknown) => void
},
) {
const reader = stream.getReader()
try {
const reader = stream.getReader()
let chunk
while (!(chunk = await reader.read()).done) {
opts.onData?.(chunk)
}
opts.onEnd?.()
} catch (error) {
opts.onError?.(error)
} finally {
reader.releaseLock()
}
}

Expand All @@ -112,10 +115,10 @@ export function transformStreamWithRouter(
})
const textDecoder = new TextDecoder()

let isAppRendering = true as boolean
let isAppRendering = true
let routerStreamBuffer = ''
let pendingClosingTags = ''
let streamBarrierLifted = false as boolean
let streamBarrierLifted = false
let leftover = ''
let leftoverHtml = ''

Expand Down Expand Up @@ -151,13 +154,22 @@ export function transformStreamWithRouter(

promise
.then((html) => {
// Don't write to destroyed stream
if (finalPassThrough.destroyed) {
return
}
if (isAppRendering) {
routerStreamBuffer += html
} else {
finalPassThrough.write(html)
}
})
.catch(injectedHtmlDonePromise.reject)
.catch((err) => {
// Only reject if not already settled
if (!finalPassThrough.destroyed) {
injectedHtmlDonePromise.reject(err)
}
})
.finally(() => {
processingCount--

Expand All @@ -173,7 +185,11 @@ export function transformStreamWithRouter(
.then(() => {
clearTimeout(timeoutHandle)
const finalHtml =
leftoverHtml + getBufferedRouterStream() + pendingClosingTags
leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags

leftover = ''
leftoverHtml = ''
pendingClosingTags = ''

finalPassThrough.end(finalHtml)
})
Expand Down Expand Up @@ -217,15 +233,20 @@ export function transformStreamWithRouter(
pendingClosingTags = chunkString.slice(bodyEndIndex)

finalPassThrough.write(
chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream(),
chunkString.slice(0, bodyEndIndex) +
getBufferedRouterStream() +
leftoverHtml,
)

leftover = ''
leftoverHtml = ''
return
}

let result: RegExpExecArray | null
let lastIndex = 0
// Reset regex lastIndex since it's global and stateful across exec() calls
patternClosingTag.lastIndex = 0
while ((result = patternClosingTag.exec(chunkString)) !== null) {
lastIndex = result.index + result[0].length
}
Expand All @@ -238,12 +259,18 @@ export function transformStreamWithRouter(

finalPassThrough.write(processed)
leftover = chunkString.slice(lastIndex)
leftoverHtml = ''
} else {
leftover = chunkString
leftoverHtml += getBufferedRouterStream()
}
},
onEnd: () => {
// Don't process if stream was already destroyed/cancelled
if (finalPassThrough.destroyed) {
return
}

// Mark the app as done rendering
isAppRendering = false
router.serverSsr!.setRenderFinished()
Expand All @@ -262,6 +289,15 @@ export function transformStreamWithRouter(
},
onError: (error) => {
console.error('Error reading appStream:', error)
isAppRendering = false
router.serverSsr!.setRenderFinished()
// Clear timeout to prevent it from firing after error
clearTimeout(timeoutHandle)
// Clear string buffers to prevent memory leaks
leftover = ''
leftoverHtml = ''
routerStreamBuffer = ''
pendingClosingTags = ''
finalPassThrough.destroy(error)
injectedHtmlDonePromise.reject(error)
},
Expand Down
Loading