@tanstack/router-core 1.136.6 → 1.136.9

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.
@@ -19,19 +19,17 @@ export function transformPipeableStreamWithRouter(
19
19
  )
20
20
  }
21
21
 
22
+ export const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'
23
+
22
24
  // regex pattern for matching closing body and html tags
23
- const patternBodyStart = /(<body)/
24
25
  const patternBodyEnd = /(<\/body>)/
25
26
  const patternHtmlEnd = /(<\/html>)/
26
- const patternHeadStart = /(<head.*?>)/
27
27
  // regex pattern for matching closing tags
28
28
  const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g
29
29
 
30
- const textDecoder = new TextDecoder()
31
-
32
30
  type ReadablePassthrough = {
33
31
  stream: ReadableStream
34
- write: (chunk: string) => void
32
+ write: (chunk: unknown) => void
35
33
  end: (chunk?: string) => void
36
34
  destroy: (error: unknown) => void
37
35
  destroyed: boolean
@@ -49,11 +47,15 @@ function createPassthrough() {
49
47
  const res: ReadablePassthrough = {
50
48
  stream,
51
49
  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
+ }
53
55
  },
54
56
  end: (chunk) => {
55
57
  if (chunk) {
56
- controller.enqueue(encoder.encode(chunk))
58
+ res.write(chunk)
57
59
  }
58
60
  controller.close()
59
61
  res.destroyed = true
@@ -90,16 +92,20 @@ async function readStream(
90
92
  export function transformStreamWithRouter(
91
93
  router: AnyRouter,
92
94
  appStream: ReadableStream,
95
+ opts?: {
96
+ timeoutMs?: number
97
+ },
93
98
  ) {
94
99
  const finalPassThrough = createPassthrough()
100
+ const textDecoder = new TextDecoder()
95
101
 
96
102
  let isAppRendering = true as boolean
97
103
  let routerStreamBuffer = ''
98
104
  let pendingClosingTags = ''
99
- let bodyStarted = false as boolean
100
- let headStarted = false as boolean
105
+ let streamBarrierLifted = false as boolean
101
106
  let leftover = ''
102
107
  let leftoverHtml = ''
108
+ let timeoutHandle: NodeJS.Timeout
103
109
 
104
110
  function getBufferedRouterStream() {
105
111
  const html = routerStreamBuffer
@@ -109,7 +115,7 @@ export function transformStreamWithRouter(
109
115
 
110
116
  function decodeChunk(chunk: unknown): string {
111
117
  if (chunk instanceof Uint8Array) {
112
- return textDecoder.decode(chunk)
118
+ return textDecoder.decode(chunk, { stream: true })
113
119
  }
114
120
  return String(chunk)
115
121
  }
@@ -136,7 +142,7 @@ export function transformStreamWithRouter(
136
142
 
137
143
  promise
138
144
  .then((html) => {
139
- if (!bodyStarted) {
145
+ if (isAppRendering) {
140
146
  routerStreamBuffer += html
141
147
  } else {
142
148
  finalPassThrough.write(html)
@@ -147,7 +153,6 @@ export function transformStreamWithRouter(
147
153
  processingCount--
148
154
 
149
155
  if (!isAppRendering && processingCount === 0) {
150
- stopListeningToInjectedHtml()
151
156
  injectedHtmlDonePromise.resolve()
152
157
  }
153
158
  })
@@ -155,6 +160,7 @@ export function transformStreamWithRouter(
155
160
 
156
161
  injectedHtmlDonePromise
157
162
  .then(() => {
163
+ clearTimeout(timeoutHandle)
158
164
  const finalHtml =
159
165
  leftoverHtml + getBufferedRouterStream() + pendingClosingTags
160
166
 
@@ -164,44 +170,26 @@ export function transformStreamWithRouter(
164
170
  console.error('Error reading routerStream:', err)
165
171
  finalPassThrough.destroy(err)
166
172
  })
173
+ .finally(stopListeningToInjectedHtml)
167
174
 
168
175
  // Transform the appStream
169
176
  readStream(appStream, {
170
177
  onData: (chunk) => {
171
178
  const text = decodeChunk(chunk.value)
172
-
173
- let chunkString = leftover + text
179
+ const chunkString = leftover + text
174
180
  const bodyEndMatch = chunkString.match(patternBodyEnd)
175
181
  const htmlEndMatch = chunkString.match(patternHtmlEnd)
176
182
 
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()
196
190
  }
197
191
  }
198
192
 
199
- if (!bodyStarted) {
200
- finalPassThrough.write(chunkString)
201
- leftover = ''
202
- return
203
- }
204
-
205
193
  // If either the body end or html end is in the chunk,
206
194
  // We need to get all of our data in asap
207
195
  if (
@@ -247,11 +235,19 @@ export function transformStreamWithRouter(
247
235
  // If there are no pending promises, resolve the injectedHtmlDonePromise
248
236
  if (processingCount === 0) {
249
237
  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)
250
245
  }
251
246
  },
252
247
  onError: (error) => {
253
248
  console.error('Error reading appStream:', error)
254
249
  finalPassThrough.destroy(error)
250
+ injectedHtmlDonePromise.reject(error)
255
251
  },
256
252
  })
257
253