@@ -19,19 +19,17 @@ export function transformPipeableStreamWithRouter(
1919 )
2020}
2121
22+ export const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'
23+
2224// regex pattern for matching closing body and html tags
23- const patternBodyStart = / ( < b o d y ) /
2425const patternBodyEnd = / ( < \/ b o d y > ) /
2526const patternHtmlEnd = / ( < \/ h t m l > ) /
26- const patternHeadStart = / ( < h e a d .* ?> ) /
2727// regex pattern for matching closing tags
2828const patternClosingTag = / ( < \/ [ a - z A - Z ] [ \w : . - ] * ?> ) / g
2929
30- const textDecoder = new TextDecoder ( )
31-
3230type ReadablePassthrough = {
3331 stream : ReadableStream
34- write : ( chunk : string ) => void
32+ write : ( chunk : unknown ) => void
3533 end : ( chunk ?: string ) => void
3634 destroy : ( error : unknown ) => void
3735 destroyed : boolean
@@ -49,11 +47,15 @@ function createPassthrough() {
4947 const res : ReadablePassthrough = {
5048 stream,
5149 write : ( chunk ) => {
52- controller . enqueue ( encoder . encode ( chunk ) )
50+ if ( typeof chunk === 'string' ) {
51+ controller . enqueue ( encoder . encode ( chunk ) )
52+ } else {
53+ controller . enqueue ( chunk )
54+ }
5355 } ,
5456 end : ( chunk ) => {
5557 if ( chunk ) {
56- controller . enqueue ( encoder . encode ( chunk ) )
58+ res . write ( chunk )
5759 }
5860 controller . close ( )
5961 res . destroyed = true
@@ -90,16 +92,20 @@ async function readStream(
9092export function transformStreamWithRouter (
9193 router : AnyRouter ,
9294 appStream : ReadableStream ,
95+ opts ?: {
96+ timeoutMs ?: number
97+ } ,
9398) {
9499 const finalPassThrough = createPassthrough ( )
100+ const textDecoder = new TextDecoder ( )
95101
96102 let isAppRendering = true as boolean
97103 let routerStreamBuffer = ''
98104 let pendingClosingTags = ''
99- let bodyStarted = false as boolean
100- let headStarted = false as boolean
105+ let streamBarrierLifted = false as boolean
101106 let leftover = ''
102107 let leftoverHtml = ''
108+ let timeoutHandle : NodeJS . Timeout
103109
104110 function getBufferedRouterStream ( ) {
105111 const html = routerStreamBuffer
@@ -109,7 +115,7 @@ export function transformStreamWithRouter(
109115
110116 function decodeChunk ( chunk : unknown ) : string {
111117 if ( chunk instanceof Uint8Array ) {
112- return textDecoder . decode ( chunk )
118+ return textDecoder . decode ( chunk , { stream : true } )
113119 }
114120 return String ( chunk )
115121 }
@@ -136,7 +142,7 @@ export function transformStreamWithRouter(
136142
137143 promise
138144 . then ( ( html ) => {
139- if ( ! bodyStarted ) {
145+ if ( isAppRendering ) {
140146 routerStreamBuffer += html
141147 } else {
142148 finalPassThrough . write ( html )
@@ -147,14 +153,14 @@ export function transformStreamWithRouter(
147153 processingCount --
148154
149155 if ( ! isAppRendering && processingCount === 0 ) {
150- stopListeningToInjectedHtml ( )
151156 injectedHtmlDonePromise . resolve ( )
152157 }
153158 } )
154159 }
155160
156161 injectedHtmlDonePromise
157162 . then ( ( ) => {
163+ clearTimeout ( timeoutHandle )
158164 const finalHtml =
159165 leftoverHtml + getBufferedRouterStream ( ) + pendingClosingTags
160166
@@ -164,44 +170,26 @@ export function transformStreamWithRouter(
164170 console . error ( 'Error reading routerStream:' , err )
165171 finalPassThrough . destroy ( err )
166172 } )
173+ . finally ( stopListeningToInjectedHtml )
167174
168175 // Transform the appStream
169176 readStream ( appStream , {
170177 onData : ( chunk ) => {
171178 const text = decodeChunk ( chunk . value )
172-
173- let chunkString = leftover + text
179+ const chunkString = leftover + text
174180 const bodyEndMatch = chunkString . match ( patternBodyEnd )
175181 const htmlEndMatch = chunkString . match ( patternHtmlEnd )
176182
177- if ( ! bodyStarted ) {
178- const bodyStartMatch = chunkString . match ( patternBodyStart )
179- if ( bodyStartMatch ) {
180- bodyStarted = true
181- }
182- }
183-
184- if ( ! headStarted ) {
185- const headStartMatch = chunkString . match ( patternHeadStart )
186- if ( headStartMatch ) {
187- headStarted = true
188- const index = headStartMatch . index !
189- const headTag = headStartMatch [ 0 ]
190- const remaining = chunkString . slice ( index + headTag . length )
191- finalPassThrough . write (
192- chunkString . slice ( 0 , index ) + headTag + getBufferedRouterStream ( ) ,
193- )
194- // make sure to only write `remaining` until the next closing tag
195- chunkString = remaining
183+ if ( ! streamBarrierLifted ) {
184+ const streamBarrierIdIncluded = chunkString . includes (
185+ TSR_SCRIPT_BARRIER_ID ,
186+ )
187+ if ( streamBarrierIdIncluded ) {
188+ streamBarrierLifted = true
189+ router . serverSsr ! . liftScriptBarrier ( )
196190 }
197191 }
198192
199- if ( ! bodyStarted ) {
200- finalPassThrough . write ( chunkString )
201- leftover = ''
202- return
203- }
204-
205193 // If either the body end or html end is in the chunk,
206194 // We need to get all of our data in asap
207195 if (
@@ -247,11 +235,19 @@ export function transformStreamWithRouter(
247235 // If there are no pending promises, resolve the injectedHtmlDonePromise
248236 if ( processingCount === 0 ) {
249237 injectedHtmlDonePromise . resolve ( )
238+ } else {
239+ const timeoutMs = opts ?. timeoutMs ?? 60000
240+ timeoutHandle = setTimeout ( ( ) => {
241+ injectedHtmlDonePromise . reject (
242+ new Error ( 'Injected HTML timeout after app render finished' ) ,
243+ )
244+ } , timeoutMs )
250245 }
251246 } ,
252247 onError : ( error ) => {
253248 console . error ( 'Error reading appStream:' , error )
254249 finalPassThrough . destroy ( error )
250+ injectedHtmlDonePromise . reject ( error )
255251 } ,
256252 } )
257253
0 commit comments