@tanstack/router-core 1.157.4 → 1.157.6
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/load-matches.cjs +4 -4
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/router.cjs +29 -13
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +0 -9
- package/dist/cjs/ssr/ssr-client.cjs +2 -2
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +48 -29
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +138 -42
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/utils/batch.cjs +16 -0
- package/dist/cjs/utils/batch.cjs.map +1 -0
- package/dist/cjs/utils/batch.d.cts +1 -0
- package/dist/cjs/utils.cjs +4 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/load-matches.js +1 -1
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/router.d.ts +0 -9
- package/dist/esm/router.js +28 -12
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/ssr-client.js +1 -1
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +48 -29
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +138 -42
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/dist/esm/utils/batch.d.ts +1 -0
- package/dist/esm/utils/batch.js +16 -0
- package/dist/esm/utils/batch.js.map +1 -0
- package/dist/esm/utils.js +4 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/load-matches.ts +1 -1
- package/src/router.ts +39 -14
- package/src/ssr/ssr-client.ts +1 -1
- package/src/ssr/ssr-server.ts +69 -32
- package/src/ssr/transformStreamWithRouter.ts +187 -108
- package/src/utils/batch.ts +18 -0
- package/src/utils.ts +4 -0
|
@@ -107,60 +107,136 @@ export function transformStreamWithRouter(
|
|
|
107
107
|
lifetimeMs?: number
|
|
108
108
|
},
|
|
109
109
|
) {
|
|
110
|
-
let stopListeningToInjectedHtml: (() => void) | undefined
|
|
111
|
-
let stopListeningToSerializationFinished: (() => void) | undefined
|
|
112
|
-
let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
113
|
-
let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
114
|
-
let cleanedUp = false
|
|
115
|
-
|
|
116
|
-
let controller: ReadableStreamDefaultController<any>
|
|
117
|
-
let isStreamClosed = false
|
|
118
|
-
|
|
119
110
|
// Check upfront if serialization already finished synchronously
|
|
120
111
|
// This is the fast path for routes with no deferred data
|
|
121
112
|
const serializationAlreadyFinished =
|
|
122
113
|
router.serverSsr?.isSerializationFinished() ?? false
|
|
123
114
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* Unsubscribes listeners, clears timeouts, frees buffers, and cleans up router SSR state.
|
|
127
|
-
*/
|
|
128
|
-
function cleanup() {
|
|
129
|
-
// Guard against multiple cleanup calls - set flag first to prevent re-entry
|
|
130
|
-
if (cleanedUp) return
|
|
131
|
-
cleanedUp = true
|
|
115
|
+
// Take any HTML that was buffered before we started listening
|
|
116
|
+
const initialBufferedHtml = router.serverSsr?.takeBufferedHtml()
|
|
132
117
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
118
|
+
// True passthrough: if serialization already finished and nothing buffered,
|
|
119
|
+
// we can avoid any decoding/scanning while still honoring cleanup + setRenderFinished.
|
|
120
|
+
if (serializationAlreadyFinished && !initialBufferedHtml) {
|
|
121
|
+
let cleanedUp = false
|
|
122
|
+
let controller: ReadableStreamDefaultController<Uint8Array> | undefined
|
|
123
|
+
let isStreamClosed = false
|
|
124
|
+
let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
125
|
+
|
|
126
|
+
const cleanup = () => {
|
|
127
|
+
if (cleanedUp) return
|
|
128
|
+
cleanedUp = true
|
|
129
|
+
|
|
130
|
+
if (lifetimeTimeoutHandle !== undefined) {
|
|
131
|
+
clearTimeout(lifetimeTimeoutHandle)
|
|
132
|
+
lifetimeTimeoutHandle = undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
router.serverSsr?.cleanup()
|
|
139
136
|
}
|
|
140
|
-
stopListeningToInjectedHtml = undefined
|
|
141
|
-
stopListeningToSerializationFinished = undefined
|
|
142
137
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
const safeClose = () => {
|
|
139
|
+
if (isStreamClosed) return
|
|
140
|
+
isStreamClosed = true
|
|
141
|
+
try {
|
|
142
|
+
controller?.close()
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
147
146
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
|
|
148
|
+
const safeError = (error: unknown) => {
|
|
149
|
+
if (isStreamClosed) return
|
|
150
|
+
isStreamClosed = true
|
|
151
|
+
try {
|
|
152
|
+
controller?.error(error)
|
|
153
|
+
} catch {
|
|
154
|
+
// ignore
|
|
155
|
+
}
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS
|
|
159
|
+
lifetimeTimeoutHandle = setTimeout(() => {
|
|
160
|
+
if (!cleanedUp && !isStreamClosed) {
|
|
161
|
+
console.warn(
|
|
162
|
+
`SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,
|
|
163
|
+
)
|
|
164
|
+
safeError(new Error('Stream lifetime exceeded'))
|
|
165
|
+
cleanup()
|
|
166
|
+
}
|
|
167
|
+
}, lifetimeMs)
|
|
157
168
|
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
170
|
+
start(c) {
|
|
171
|
+
controller = c
|
|
172
|
+
},
|
|
173
|
+
cancel() {
|
|
174
|
+
isStreamClosed = true
|
|
175
|
+
cleanup()
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
;(async () => {
|
|
180
|
+
const reader = appStream.getReader()
|
|
181
|
+
try {
|
|
182
|
+
while (true) {
|
|
183
|
+
const { done, value } = await reader.read()
|
|
184
|
+
if (done) break
|
|
185
|
+
if (cleanedUp || isStreamClosed) return
|
|
186
|
+
controller?.enqueue(value as unknown as Uint8Array)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (cleanedUp || isStreamClosed) return
|
|
190
|
+
|
|
191
|
+
router.serverSsr?.setRenderFinished()
|
|
192
|
+
safeClose()
|
|
193
|
+
cleanup()
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (cleanedUp) return
|
|
196
|
+
console.error('Error reading appStream:', error)
|
|
197
|
+
router.serverSsr?.setRenderFinished()
|
|
198
|
+
safeError(error)
|
|
199
|
+
cleanup()
|
|
200
|
+
} finally {
|
|
201
|
+
reader.releaseLock()
|
|
202
|
+
}
|
|
203
|
+
})().catch((error) => {
|
|
204
|
+
if (cleanedUp) return
|
|
205
|
+
console.error('Error in stream transform:', error)
|
|
206
|
+
safeError(error)
|
|
207
|
+
cleanup()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
return stream
|
|
160
211
|
}
|
|
161
212
|
|
|
213
|
+
let stopListeningToInjectedHtml: (() => void) | undefined
|
|
214
|
+
let stopListeningToSerializationFinished: (() => void) | undefined
|
|
215
|
+
let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
216
|
+
let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
217
|
+
let cleanedUp = false
|
|
218
|
+
|
|
219
|
+
let controller: ReadableStreamDefaultController<any>
|
|
220
|
+
let isStreamClosed = false
|
|
221
|
+
|
|
162
222
|
const textDecoder = new TextDecoder()
|
|
163
223
|
|
|
224
|
+
// concat'd router HTML; avoids array joins on each flush
|
|
225
|
+
let pendingRouterHtml = initialBufferedHtml ?? ''
|
|
226
|
+
|
|
227
|
+
// between-chunk text buffer; keep bounded to avoid unbounded memory
|
|
228
|
+
let leftover = ''
|
|
229
|
+
|
|
230
|
+
// captured closing tags from </body> onward
|
|
231
|
+
let pendingClosingTags = ''
|
|
232
|
+
|
|
233
|
+
// conservative cap: enough to hold any partial closing tag + a bit
|
|
234
|
+
const MAX_LEFTOVER_CHARS = 2048
|
|
235
|
+
|
|
236
|
+
let isAppRendering = true
|
|
237
|
+
let streamBarrierLifted = false
|
|
238
|
+
let serializationFinished = serializationAlreadyFinished
|
|
239
|
+
|
|
164
240
|
function safeEnqueue(chunk: string | Uint8Array) {
|
|
165
241
|
if (isStreamClosed) return
|
|
166
242
|
if (typeof chunk === 'string') {
|
|
@@ -176,7 +252,7 @@ export function transformStreamWithRouter(
|
|
|
176
252
|
try {
|
|
177
253
|
controller.close()
|
|
178
254
|
} catch {
|
|
179
|
-
//
|
|
255
|
+
// ignore
|
|
180
256
|
}
|
|
181
257
|
}
|
|
182
258
|
|
|
@@ -186,10 +262,42 @@ export function transformStreamWithRouter(
|
|
|
186
262
|
try {
|
|
187
263
|
controller.error(error)
|
|
188
264
|
} catch {
|
|
189
|
-
//
|
|
265
|
+
// ignore
|
|
190
266
|
}
|
|
191
267
|
}
|
|
192
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Cleanup with guards; must be idempotent.
|
|
271
|
+
*/
|
|
272
|
+
function cleanup() {
|
|
273
|
+
if (cleanedUp) return
|
|
274
|
+
cleanedUp = true
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
stopListeningToInjectedHtml?.()
|
|
278
|
+
stopListeningToSerializationFinished?.()
|
|
279
|
+
} catch {
|
|
280
|
+
// ignore
|
|
281
|
+
}
|
|
282
|
+
stopListeningToInjectedHtml = undefined
|
|
283
|
+
stopListeningToSerializationFinished = undefined
|
|
284
|
+
|
|
285
|
+
if (serializationTimeoutHandle !== undefined) {
|
|
286
|
+
clearTimeout(serializationTimeoutHandle)
|
|
287
|
+
serializationTimeoutHandle = undefined
|
|
288
|
+
}
|
|
289
|
+
if (lifetimeTimeoutHandle !== undefined) {
|
|
290
|
+
clearTimeout(lifetimeTimeoutHandle)
|
|
291
|
+
lifetimeTimeoutHandle = undefined
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
pendingRouterHtml = ''
|
|
295
|
+
leftover = ''
|
|
296
|
+
pendingClosingTags = ''
|
|
297
|
+
|
|
298
|
+
router.serverSsr?.cleanup()
|
|
299
|
+
}
|
|
300
|
+
|
|
193
301
|
const stream = new ReadableStream({
|
|
194
302
|
start(c) {
|
|
195
303
|
controller = c
|
|
@@ -200,36 +308,24 @@ export function transformStreamWithRouter(
|
|
|
200
308
|
},
|
|
201
309
|
})
|
|
202
310
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
let serializationFinished = serializationAlreadyFinished
|
|
208
|
-
|
|
209
|
-
let pendingRouterHtmlParts: Array<string> = []
|
|
210
|
-
|
|
211
|
-
// Take any HTML that was buffered before we started listening
|
|
212
|
-
const bufferedHtml = router.serverSsr?.takeBufferedHtml()
|
|
213
|
-
if (bufferedHtml) {
|
|
214
|
-
pendingRouterHtmlParts.push(bufferedHtml)
|
|
311
|
+
function flushPendingRouterHtml() {
|
|
312
|
+
if (!pendingRouterHtml) return
|
|
313
|
+
safeEnqueue(pendingRouterHtml)
|
|
314
|
+
pendingRouterHtml = ''
|
|
215
315
|
}
|
|
216
316
|
|
|
217
|
-
function
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
pendingRouterHtmlParts = []
|
|
221
|
-
}
|
|
317
|
+
function appendRouterHtml(html: string) {
|
|
318
|
+
if (!html) return
|
|
319
|
+
pendingRouterHtml += html
|
|
222
320
|
}
|
|
223
321
|
|
|
224
322
|
/**
|
|
225
|
-
*
|
|
323
|
+
* Finish only when app done and serialization complete.
|
|
226
324
|
*/
|
|
227
325
|
function tryFinish() {
|
|
228
|
-
// Can only finish when app is done rendering and serialization is complete
|
|
229
326
|
if (isAppRendering || !serializationFinished) return
|
|
230
327
|
if (cleanedUp || isStreamClosed) return
|
|
231
328
|
|
|
232
|
-
// Clear serialization timeout since we're finishing
|
|
233
329
|
if (serializationTimeoutHandle !== undefined) {
|
|
234
330
|
clearTimeout(serializationTimeoutHandle)
|
|
235
331
|
serializationTimeoutHandle = undefined
|
|
@@ -247,8 +343,7 @@ export function transformStreamWithRouter(
|
|
|
247
343
|
cleanup()
|
|
248
344
|
}
|
|
249
345
|
|
|
250
|
-
//
|
|
251
|
-
// This ensures cleanup happens even if the stream is never consumed or gets stuck
|
|
346
|
+
// Safety net: cleanup even if consumer never reads
|
|
252
347
|
const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS
|
|
253
348
|
lifetimeTimeoutHandle = setTimeout(() => {
|
|
254
349
|
if (!cleanedUp && !isStreamClosed) {
|
|
@@ -260,29 +355,21 @@ export function transformStreamWithRouter(
|
|
|
260
355
|
}
|
|
261
356
|
}, lifetimeMs)
|
|
262
357
|
|
|
263
|
-
// Only set up listeners if serialization hasn't already finished
|
|
264
|
-
// This avoids unnecessary subscriptions for the common case of no deferred data
|
|
265
358
|
if (!serializationAlreadyFinished) {
|
|
266
|
-
// Listen for injected HTML (for deferred data that resolves later)
|
|
267
359
|
stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {
|
|
268
360
|
if (cleanedUp || isStreamClosed) return
|
|
269
|
-
|
|
270
|
-
// Retrieve buffered HTML
|
|
271
361
|
const html = router.serverSsr?.takeBufferedHtml()
|
|
272
362
|
if (!html) return
|
|
273
363
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
pendingRouterHtmlParts.push(html)
|
|
364
|
+
// If we've already captured </body> (pendingClosingTags), we must keep appending
|
|
365
|
+
// so injection stays before the stored closing tags.
|
|
366
|
+
if (isAppRendering || leftover || pendingClosingTags) {
|
|
367
|
+
appendRouterHtml(html)
|
|
279
368
|
} else {
|
|
280
|
-
// App done rendering and no leftover - safe to write directly for better streaming
|
|
281
369
|
safeEnqueue(html)
|
|
282
370
|
}
|
|
283
371
|
})
|
|
284
372
|
|
|
285
|
-
// Listen for serialization finished
|
|
286
373
|
stopListeningToSerializationFinished = router.subscribe(
|
|
287
374
|
'onSerializationFinished',
|
|
288
375
|
() => {
|
|
@@ -300,16 +387,16 @@ export function transformStreamWithRouter(
|
|
|
300
387
|
const { done, value } = await reader.read()
|
|
301
388
|
if (done) break
|
|
302
389
|
|
|
303
|
-
// Don't process if already cleaned up
|
|
304
390
|
if (cleanedUp || isStreamClosed) return
|
|
305
391
|
|
|
306
392
|
const text =
|
|
307
393
|
value instanceof Uint8Array
|
|
308
394
|
? textDecoder.decode(value, { stream: true })
|
|
309
395
|
: String(value)
|
|
310
|
-
const chunkString = leftover + text
|
|
311
396
|
|
|
312
|
-
//
|
|
397
|
+
// Fast path: most chunks have no pending left-over.
|
|
398
|
+
const chunkString = leftover ? leftover + text : text
|
|
399
|
+
|
|
313
400
|
if (!streamBarrierLifted) {
|
|
314
401
|
if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {
|
|
315
402
|
streamBarrierLifted = true
|
|
@@ -317,70 +404,63 @@ export function transformStreamWithRouter(
|
|
|
317
404
|
}
|
|
318
405
|
}
|
|
319
406
|
|
|
320
|
-
//
|
|
407
|
+
// If we already saw </body>, everything else is part of tail; buffer it.
|
|
408
|
+
if (pendingClosingTags) {
|
|
409
|
+
pendingClosingTags += chunkString
|
|
410
|
+
leftover = ''
|
|
411
|
+
continue
|
|
412
|
+
}
|
|
413
|
+
|
|
321
414
|
const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)
|
|
322
415
|
const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)
|
|
323
416
|
|
|
324
|
-
// If we have both </body> and </html> in proper order,
|
|
325
|
-
// insert router HTML before </body> and hold the closing tags
|
|
326
417
|
if (
|
|
327
418
|
bodyEndIndex !== -1 &&
|
|
328
419
|
htmlEndIndex !== -1 &&
|
|
329
420
|
bodyEndIndex < htmlEndIndex
|
|
330
421
|
) {
|
|
331
422
|
pendingClosingTags = chunkString.slice(bodyEndIndex)
|
|
332
|
-
|
|
333
423
|
safeEnqueue(chunkString.slice(0, bodyEndIndex))
|
|
334
424
|
flushPendingRouterHtml()
|
|
335
|
-
|
|
336
425
|
leftover = ''
|
|
337
426
|
continue
|
|
338
427
|
}
|
|
339
428
|
|
|
340
|
-
// Handling partial closing tags split across chunks:
|
|
341
|
-
//
|
|
342
|
-
// Since `chunkString = leftover + text`, any incomplete tag fragment from the
|
|
343
|
-
// previous chunk is prepended to the current chunk, allowing split tags like
|
|
344
|
-
// "</di" + "v>" to be re-detected as a complete "</div>" in the combined string.
|
|
345
|
-
//
|
|
346
|
-
// - If a closing tag IS found (lastClosingTagEnd > 0): We enqueue content up to
|
|
347
|
-
// the end of that tag, flush router HTML, and store the remainder in `leftover`.
|
|
348
|
-
// This remainder may contain a partial tag (e.g., "</sp") which will be
|
|
349
|
-
// prepended to the next chunk for re-detection.
|
|
350
|
-
//
|
|
351
|
-
// - If NO closing tag is found: The entire chunk is buffered in `leftover` and
|
|
352
|
-
// will be prepended to the next chunk. This ensures partial tags are never
|
|
353
|
-
// lost and will be detected once the rest of the tag arrives.
|
|
354
|
-
//
|
|
355
|
-
// This approach guarantees correct injection points even when closing tags span
|
|
356
|
-
// chunk boundaries.
|
|
357
429
|
const lastClosingTagEnd = findLastClosingTagEnd(chunkString)
|
|
358
430
|
|
|
359
431
|
if (lastClosingTagEnd > 0) {
|
|
360
|
-
// Found a closing tag - insert router HTML after it
|
|
361
432
|
safeEnqueue(chunkString.slice(0, lastClosingTagEnd))
|
|
362
433
|
flushPendingRouterHtml()
|
|
363
434
|
|
|
364
435
|
leftover = chunkString.slice(lastClosingTagEnd)
|
|
436
|
+
if (leftover.length > MAX_LEFTOVER_CHARS) {
|
|
437
|
+
// Ensure bounded memory even if a consumer streams long text sequences
|
|
438
|
+
// without any closing tags. This may reduce injection granularity but is correct.
|
|
439
|
+
safeEnqueue(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS))
|
|
440
|
+
leftover = leftover.slice(-MAX_LEFTOVER_CHARS)
|
|
441
|
+
}
|
|
365
442
|
} else {
|
|
366
|
-
// No closing tag found
|
|
367
|
-
|
|
368
|
-
|
|
443
|
+
// No closing tag found; keep small tail to handle split closing tags,
|
|
444
|
+
// but stream older bytes to prevent unbounded buffering.
|
|
445
|
+
const combined = chunkString
|
|
446
|
+
if (combined.length > MAX_LEFTOVER_CHARS) {
|
|
447
|
+
const flushUpto = combined.length - MAX_LEFTOVER_CHARS
|
|
448
|
+
safeEnqueue(combined.slice(0, flushUpto))
|
|
449
|
+
leftover = combined.slice(flushUpto)
|
|
450
|
+
} else {
|
|
451
|
+
leftover = combined
|
|
452
|
+
}
|
|
369
453
|
}
|
|
370
454
|
}
|
|
371
455
|
|
|
372
|
-
// Stream ended
|
|
373
456
|
if (cleanedUp || isStreamClosed) return
|
|
374
457
|
|
|
375
|
-
// Mark the app as done rendering
|
|
376
458
|
isAppRendering = false
|
|
377
459
|
router.serverSsr?.setRenderFinished()
|
|
378
460
|
|
|
379
|
-
// Try to finish if serialization is already done
|
|
380
461
|
if (serializationFinished) {
|
|
381
462
|
tryFinish()
|
|
382
463
|
} else {
|
|
383
|
-
// Set a timeout for serialization to complete
|
|
384
464
|
const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS
|
|
385
465
|
serializationTimeoutHandle = setTimeout(() => {
|
|
386
466
|
if (!cleanedUp && !isStreamClosed) {
|
|
@@ -403,7 +483,6 @@ export function transformStreamWithRouter(
|
|
|
403
483
|
reader.releaseLock()
|
|
404
484
|
}
|
|
405
485
|
})().catch((error) => {
|
|
406
|
-
// Handle any errors that occur outside the try block (e.g., getReader() failure)
|
|
407
486
|
if (cleanedUp) return
|
|
408
487
|
console.error('Error in stream transform:', error)
|
|
409
488
|
safeError(error)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { batch as storeBatch } from '@tanstack/store'
|
|
2
|
+
|
|
3
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
4
|
+
|
|
5
|
+
// `@tanstack/store`'s `batch` is for reactive notification batching.
|
|
6
|
+
// On the server we don't subscribe/render reactively, so a lightweight
|
|
7
|
+
// implementation that just executes is enough.
|
|
8
|
+
export function batch<T>(fn: () => T): T {
|
|
9
|
+
if (isServer) {
|
|
10
|
+
return fn()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let result!: T
|
|
14
|
+
storeBatch(() => {
|
|
15
|
+
result = fn()
|
|
16
|
+
})
|
|
17
|
+
return result
|
|
18
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
1
2
|
import type { RouteIds } from './routeInfo'
|
|
2
3
|
import type { AnyRouter } from './router'
|
|
3
4
|
|
|
@@ -221,6 +222,9 @@ const isEnumerable = Object.prototype.propertyIsEnumerable
|
|
|
221
222
|
* Do not use this with signals
|
|
222
223
|
*/
|
|
223
224
|
export function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {
|
|
225
|
+
if (isServer) {
|
|
226
|
+
return _next
|
|
227
|
+
}
|
|
224
228
|
if (prev === _next) {
|
|
225
229
|
return prev
|
|
226
230
|
}
|