You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@tanstack/router-core

Package Overview
Dependencies
Maintainers
4
Versions
432
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tanstack/router-core - npm Package Compare versions

Comparing version
1.157.3
to
1.157.4
+1
-1
dist/cjs/ssr/transformStreamWithRouter.cjs

@@ -167,3 +167,3 @@ "use strict";

if (!html) return;
if (isAppRendering) {
if (isAppRendering || leftover) {
pendingRouterHtmlParts.push(html);

@@ -170,0 +170,0 @@ } else {

@@ -1,1 +0,1 @@

{"version":3,"file":"transformStreamWithRouter.cjs","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { TSR_SCRIPT_BARRIER_ID } from './constants'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\n// Use string constants for simple indexOf matching\nconst BODY_END_TAG = '</body>'\nconst HTML_END_TAG = '</html>'\n\n// Minimum length of a valid closing tag: </a> = 4 characters\nconst MIN_CLOSING_TAG_LENGTH = 4\n\n// Default timeout values (in milliseconds)\nconst DEFAULT_SERIALIZATION_TIMEOUT_MS = 60000\nconst DEFAULT_LIFETIME_TIMEOUT_MS = 60000\n\n// Module-level encoder (stateless, safe to reuse)\nconst textEncoder = new TextEncoder()\n\n/**\n * Finds the position just after the last valid HTML closing tag in the string.\n *\n * Valid closing tags match the pattern: </[a-zA-Z][\\w:.-]*>\n * Examples: </div>, </my-component>, </slot:name.nested>\n *\n * @returns Position after the last closing tag, or -1 if none found\n */\nfunction findLastClosingTagEnd(str: string): number {\n const len = str.length\n if (len < MIN_CLOSING_TAG_LENGTH) return -1\n\n let i = len - 1\n\n while (i >= MIN_CLOSING_TAG_LENGTH - 1) {\n // Look for > (charCode 62)\n if (str.charCodeAt(i) === 62) {\n // Look backwards for valid tag name characters\n let j = i - 1\n\n // Skip through valid tag name characters\n while (j >= 1) {\n const code = str.charCodeAt(j)\n // Check if it's a valid tag name char: [a-zA-Z0-9_:.-]\n if (\n (code >= 97 && code <= 122) || // a-z\n (code >= 65 && code <= 90) || // A-Z\n (code >= 48 && code <= 57) || // 0-9\n code === 95 || // _\n code === 58 || // :\n code === 46 || // .\n code === 45 // -\n ) {\n j--\n } else {\n break\n }\n }\n\n // Check if the first char after </ is a valid start char (letter only)\n const tagNameStart = j + 1\n if (tagNameStart < i) {\n const startCode = str.charCodeAt(tagNameStart)\n // Tag name must start with a letter (a-z or A-Z)\n if (\n (startCode >= 97 && startCode <= 122) ||\n (startCode >= 65 && startCode <= 90)\n ) {\n // Check for </ (charCodes: < = 60, / = 47)\n if (\n j >= 1 &&\n str.charCodeAt(j) === 47 &&\n str.charCodeAt(j - 1) === 60\n ) {\n return i + 1 // Return position after the closing >\n }\n }\n }\n }\n i--\n }\n return -1\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n /** Timeout for serialization to complete after app render finishes (default: 60000ms) */\n timeoutMs?: number\n /** Maximum lifetime of the stream transform (default: 60000ms). Safety net for cleanup. */\n lifetimeMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined\n let stopListeningToSerializationFinished: (() => void) | undefined\n let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let cleanedUp = false\n\n let controller: ReadableStreamDefaultController<any>\n let isStreamClosed = false\n\n // Check upfront if serialization already finished synchronously\n // This is the fast path for routes with no deferred data\n const serializationAlreadyFinished =\n router.serverSsr?.isSerializationFinished() ?? false\n\n /**\n * Cleanup function with guards against multiple calls.\n * Unsubscribes listeners, clears timeouts, frees buffers, and cleans up router SSR state.\n */\n function cleanup() {\n // Guard against multiple cleanup calls - set flag first to prevent re-entry\n if (cleanedUp) return\n cleanedUp = true\n\n // Unsubscribe listeners first (wrap in try-catch for safety)\n try {\n stopListeningToInjectedHtml?.()\n stopListeningToSerializationFinished?.()\n } catch (e) {\n // Ignore errors during unsubscription\n }\n stopListeningToInjectedHtml = undefined\n stopListeningToSerializationFinished = undefined\n\n // Clear all timeouts\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n // Clear buffers to free memory\n pendingRouterHtmlParts = []\n leftover = ''\n pendingClosingTags = ''\n\n // Clean up router SSR state (has its own guard)\n router.serverSsr?.cleanup()\n }\n\n const textDecoder = new TextDecoder()\n\n function safeEnqueue(chunk: string | Uint8Array) {\n if (isStreamClosed) return\n if (typeof chunk === 'string') {\n controller.enqueue(textEncoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n }\n\n function safeClose() {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.close()\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n function safeError(error: unknown) {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.error(error)\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n let isAppRendering = true\n let streamBarrierLifted = false\n let leftover = ''\n let pendingClosingTags = ''\n let serializationFinished = serializationAlreadyFinished\n\n let pendingRouterHtmlParts: Array<string> = []\n\n // Take any HTML that was buffered before we started listening\n const bufferedHtml = router.serverSsr?.takeBufferedHtml()\n if (bufferedHtml) {\n pendingRouterHtmlParts.push(bufferedHtml)\n }\n\n function flushPendingRouterHtml() {\n if (pendingRouterHtmlParts.length > 0) {\n safeEnqueue(pendingRouterHtmlParts.join(''))\n pendingRouterHtmlParts = []\n }\n }\n\n /**\n * Attempts to finish the stream if all conditions are met.\n */\n function tryFinish() {\n // Can only finish when app is done rendering and serialization is complete\n if (isAppRendering || !serializationFinished) return\n if (cleanedUp || isStreamClosed) return\n\n // Clear serialization timeout since we're finishing\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n\n // Flush any remaining bytes in the TextDecoder\n const decoderRemainder = textDecoder.decode()\n\n if (leftover) safeEnqueue(leftover)\n if (decoderRemainder) safeEnqueue(decoderRemainder)\n flushPendingRouterHtml()\n if (pendingClosingTags) safeEnqueue(pendingClosingTags)\n\n safeClose()\n cleanup()\n }\n\n // Set up lifetime timeout as a safety net\n // This ensures cleanup happens even if the stream is never consumed or gets stuck\n const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS\n lifetimeTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.warn(\n `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,\n )\n safeError(new Error('Stream lifetime exceeded'))\n cleanup()\n }\n }, lifetimeMs)\n\n // Only set up listeners if serialization hasn't already finished\n // This avoids unnecessary subscriptions for the common case of no deferred data\n if (!serializationAlreadyFinished) {\n // Listen for injected HTML (for deferred data that resolves later)\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {\n if (cleanedUp || isStreamClosed) return\n\n // Retrieve buffered HTML\n const html = router.serverSsr?.takeBufferedHtml()\n if (!html) return\n\n if (isAppRendering) {\n // Buffer for insertion at next valid position\n pendingRouterHtmlParts.push(html)\n } else {\n // App is done rendering, write directly to output\n safeEnqueue(html)\n }\n })\n\n // Listen for serialization finished\n stopListeningToSerializationFinished = router.subscribe(\n 'onSerializationFinished',\n () => {\n serializationFinished = true\n tryFinish()\n },\n )\n }\n\n // Transform the appStream\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n // Don't process if already cleaned up\n if (cleanedUp || isStreamClosed) return\n\n const text =\n value instanceof Uint8Array\n ? textDecoder.decode(value, { stream: true })\n : String(value)\n const chunkString = leftover + text\n\n // Check for stream barrier (script placeholder) - use indexOf for efficiency\n if (!streamBarrierLifted) {\n if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {\n streamBarrierLifted = true\n router.serverSsr?.liftScriptBarrier()\n }\n }\n\n // Check for body/html end tags\n const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)\n const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)\n\n // If we have both </body> and </html> in proper order,\n // insert router HTML before </body> and hold the closing tags\n if (\n bodyEndIndex !== -1 &&\n htmlEndIndex !== -1 &&\n bodyEndIndex < htmlEndIndex\n ) {\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n safeEnqueue(chunkString.slice(0, bodyEndIndex))\n flushPendingRouterHtml()\n\n leftover = ''\n continue\n }\n\n // Handling partial closing tags split across chunks:\n //\n // Since `chunkString = leftover + text`, any incomplete tag fragment from the\n // previous chunk is prepended to the current chunk, allowing split tags like\n // \"</di\" + \"v>\" to be re-detected as a complete \"</div>\" in the combined string.\n //\n // - If a closing tag IS found (lastClosingTagEnd > 0): We enqueue content up to\n // the end of that tag, flush router HTML, and store the remainder in `leftover`.\n // This remainder may contain a partial tag (e.g., \"</sp\") which will be\n // prepended to the next chunk for re-detection.\n //\n // - If NO closing tag is found: The entire chunk is buffered in `leftover` and\n // will be prepended to the next chunk. This ensures partial tags are never\n // lost and will be detected once the rest of the tag arrives.\n //\n // This approach guarantees correct injection points even when closing tags span\n // chunk boundaries.\n const lastClosingTagEnd = findLastClosingTagEnd(chunkString)\n\n if (lastClosingTagEnd > 0) {\n // Found a closing tag - insert router HTML after it\n safeEnqueue(chunkString.slice(0, lastClosingTagEnd))\n flushPendingRouterHtml()\n\n leftover = chunkString.slice(lastClosingTagEnd)\n } else {\n // No closing tag found - buffer the entire chunk\n leftover = chunkString\n // Any pending router HTML will be inserted when we find a valid position\n }\n }\n\n // Stream ended\n if (cleanedUp || isStreamClosed) return\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n\n // Try to finish if serialization is already done\n if (serializationFinished) {\n tryFinish()\n } else {\n // Set a timeout for serialization to complete\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS\n serializationTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.error('Serialization timeout after app render finished')\n safeError(\n new Error('Serialization timeout after app render finished'),\n )\n cleanup()\n }\n }, timeoutMs)\n }\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n // Handle any errors that occur outside the try block (e.g., getReader() failure)\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n}\n"],"names":["Readable","ReadableStream","TSR_SCRIPT_BARRIER_ID"],"mappings":"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"}
{"version":3,"file":"transformStreamWithRouter.cjs","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { TSR_SCRIPT_BARRIER_ID } from './constants'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\n// Use string constants for simple indexOf matching\nconst BODY_END_TAG = '</body>'\nconst HTML_END_TAG = '</html>'\n\n// Minimum length of a valid closing tag: </a> = 4 characters\nconst MIN_CLOSING_TAG_LENGTH = 4\n\n// Default timeout values (in milliseconds)\nconst DEFAULT_SERIALIZATION_TIMEOUT_MS = 60000\nconst DEFAULT_LIFETIME_TIMEOUT_MS = 60000\n\n// Module-level encoder (stateless, safe to reuse)\nconst textEncoder = new TextEncoder()\n\n/**\n * Finds the position just after the last valid HTML closing tag in the string.\n *\n * Valid closing tags match the pattern: </[a-zA-Z][\\w:.-]*>\n * Examples: </div>, </my-component>, </slot:name.nested>\n *\n * @returns Position after the last closing tag, or -1 if none found\n */\nfunction findLastClosingTagEnd(str: string): number {\n const len = str.length\n if (len < MIN_CLOSING_TAG_LENGTH) return -1\n\n let i = len - 1\n\n while (i >= MIN_CLOSING_TAG_LENGTH - 1) {\n // Look for > (charCode 62)\n if (str.charCodeAt(i) === 62) {\n // Look backwards for valid tag name characters\n let j = i - 1\n\n // Skip through valid tag name characters\n while (j >= 1) {\n const code = str.charCodeAt(j)\n // Check if it's a valid tag name char: [a-zA-Z0-9_:.-]\n if (\n (code >= 97 && code <= 122) || // a-z\n (code >= 65 && code <= 90) || // A-Z\n (code >= 48 && code <= 57) || // 0-9\n code === 95 || // _\n code === 58 || // :\n code === 46 || // .\n code === 45 // -\n ) {\n j--\n } else {\n break\n }\n }\n\n // Check if the first char after </ is a valid start char (letter only)\n const tagNameStart = j + 1\n if (tagNameStart < i) {\n const startCode = str.charCodeAt(tagNameStart)\n // Tag name must start with a letter (a-z or A-Z)\n if (\n (startCode >= 97 && startCode <= 122) ||\n (startCode >= 65 && startCode <= 90)\n ) {\n // Check for </ (charCodes: < = 60, / = 47)\n if (\n j >= 1 &&\n str.charCodeAt(j) === 47 &&\n str.charCodeAt(j - 1) === 60\n ) {\n return i + 1 // Return position after the closing >\n }\n }\n }\n }\n i--\n }\n return -1\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n /** Timeout for serialization to complete after app render finishes (default: 60000ms) */\n timeoutMs?: number\n /** Maximum lifetime of the stream transform (default: 60000ms). Safety net for cleanup. */\n lifetimeMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined\n let stopListeningToSerializationFinished: (() => void) | undefined\n let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let cleanedUp = false\n\n let controller: ReadableStreamDefaultController<any>\n let isStreamClosed = false\n\n // Check upfront if serialization already finished synchronously\n // This is the fast path for routes with no deferred data\n const serializationAlreadyFinished =\n router.serverSsr?.isSerializationFinished() ?? false\n\n /**\n * Cleanup function with guards against multiple calls.\n * Unsubscribes listeners, clears timeouts, frees buffers, and cleans up router SSR state.\n */\n function cleanup() {\n // Guard against multiple cleanup calls - set flag first to prevent re-entry\n if (cleanedUp) return\n cleanedUp = true\n\n // Unsubscribe listeners first (wrap in try-catch for safety)\n try {\n stopListeningToInjectedHtml?.()\n stopListeningToSerializationFinished?.()\n } catch (e) {\n // Ignore errors during unsubscription\n }\n stopListeningToInjectedHtml = undefined\n stopListeningToSerializationFinished = undefined\n\n // Clear all timeouts\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n // Clear buffers to free memory\n pendingRouterHtmlParts = []\n leftover = ''\n pendingClosingTags = ''\n\n // Clean up router SSR state (has its own guard)\n router.serverSsr?.cleanup()\n }\n\n const textDecoder = new TextDecoder()\n\n function safeEnqueue(chunk: string | Uint8Array) {\n if (isStreamClosed) return\n if (typeof chunk === 'string') {\n controller.enqueue(textEncoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n }\n\n function safeClose() {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.close()\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n function safeError(error: unknown) {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.error(error)\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n let isAppRendering = true\n let streamBarrierLifted = false\n let leftover = ''\n let pendingClosingTags = ''\n let serializationFinished = serializationAlreadyFinished\n\n let pendingRouterHtmlParts: Array<string> = []\n\n // Take any HTML that was buffered before we started listening\n const bufferedHtml = router.serverSsr?.takeBufferedHtml()\n if (bufferedHtml) {\n pendingRouterHtmlParts.push(bufferedHtml)\n }\n\n function flushPendingRouterHtml() {\n if (pendingRouterHtmlParts.length > 0) {\n safeEnqueue(pendingRouterHtmlParts.join(''))\n pendingRouterHtmlParts = []\n }\n }\n\n /**\n * Attempts to finish the stream if all conditions are met.\n */\n function tryFinish() {\n // Can only finish when app is done rendering and serialization is complete\n if (isAppRendering || !serializationFinished) return\n if (cleanedUp || isStreamClosed) return\n\n // Clear serialization timeout since we're finishing\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n\n // Flush any remaining bytes in the TextDecoder\n const decoderRemainder = textDecoder.decode()\n\n if (leftover) safeEnqueue(leftover)\n if (decoderRemainder) safeEnqueue(decoderRemainder)\n flushPendingRouterHtml()\n if (pendingClosingTags) safeEnqueue(pendingClosingTags)\n\n safeClose()\n cleanup()\n }\n\n // Set up lifetime timeout as a safety net\n // This ensures cleanup happens even if the stream is never consumed or gets stuck\n const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS\n lifetimeTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.warn(\n `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,\n )\n safeError(new Error('Stream lifetime exceeded'))\n cleanup()\n }\n }, lifetimeMs)\n\n // Only set up listeners if serialization hasn't already finished\n // This avoids unnecessary subscriptions for the common case of no deferred data\n if (!serializationAlreadyFinished) {\n // Listen for injected HTML (for deferred data that resolves later)\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {\n if (cleanedUp || isStreamClosed) return\n\n // Retrieve buffered HTML\n const html = router.serverSsr?.takeBufferedHtml()\n if (!html) return\n\n if (isAppRendering || leftover) {\n // Buffer when app is still rendering OR when there's leftover content\n // that hasn't been flushed yet. This prevents race conditions where\n // injected HTML appears before buffered app content\n pendingRouterHtmlParts.push(html)\n } else {\n // App done rendering and no leftover - safe to write directly for better streaming\n safeEnqueue(html)\n }\n })\n\n // Listen for serialization finished\n stopListeningToSerializationFinished = router.subscribe(\n 'onSerializationFinished',\n () => {\n serializationFinished = true\n tryFinish()\n },\n )\n }\n\n // Transform the appStream\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n // Don't process if already cleaned up\n if (cleanedUp || isStreamClosed) return\n\n const text =\n value instanceof Uint8Array\n ? textDecoder.decode(value, { stream: true })\n : String(value)\n const chunkString = leftover + text\n\n // Check for stream barrier (script placeholder) - use indexOf for efficiency\n if (!streamBarrierLifted) {\n if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {\n streamBarrierLifted = true\n router.serverSsr?.liftScriptBarrier()\n }\n }\n\n // Check for body/html end tags\n const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)\n const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)\n\n // If we have both </body> and </html> in proper order,\n // insert router HTML before </body> and hold the closing tags\n if (\n bodyEndIndex !== -1 &&\n htmlEndIndex !== -1 &&\n bodyEndIndex < htmlEndIndex\n ) {\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n safeEnqueue(chunkString.slice(0, bodyEndIndex))\n flushPendingRouterHtml()\n\n leftover = ''\n continue\n }\n\n // Handling partial closing tags split across chunks:\n //\n // Since `chunkString = leftover + text`, any incomplete tag fragment from the\n // previous chunk is prepended to the current chunk, allowing split tags like\n // \"</di\" + \"v>\" to be re-detected as a complete \"</div>\" in the combined string.\n //\n // - If a closing tag IS found (lastClosingTagEnd > 0): We enqueue content up to\n // the end of that tag, flush router HTML, and store the remainder in `leftover`.\n // This remainder may contain a partial tag (e.g., \"</sp\") which will be\n // prepended to the next chunk for re-detection.\n //\n // - If NO closing tag is found: The entire chunk is buffered in `leftover` and\n // will be prepended to the next chunk. This ensures partial tags are never\n // lost and will be detected once the rest of the tag arrives.\n //\n // This approach guarantees correct injection points even when closing tags span\n // chunk boundaries.\n const lastClosingTagEnd = findLastClosingTagEnd(chunkString)\n\n if (lastClosingTagEnd > 0) {\n // Found a closing tag - insert router HTML after it\n safeEnqueue(chunkString.slice(0, lastClosingTagEnd))\n flushPendingRouterHtml()\n\n leftover = chunkString.slice(lastClosingTagEnd)\n } else {\n // No closing tag found - buffer the entire chunk\n leftover = chunkString\n // Any pending router HTML will be inserted when we find a valid position\n }\n }\n\n // Stream ended\n if (cleanedUp || isStreamClosed) return\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n\n // Try to finish if serialization is already done\n if (serializationFinished) {\n tryFinish()\n } else {\n // Set a timeout for serialization to complete\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS\n serializationTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.error('Serialization timeout after app render finished')\n safeError(\n new Error('Serialization timeout after app render finished'),\n )\n cleanup()\n }\n }, timeoutMs)\n }\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n // Handle any errors that occur outside the try block (e.g., getReader() failure)\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n}\n"],"names":["Readable","ReadableStream","TSR_SCRIPT_BARRIER_ID"],"mappings":"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"}

@@ -165,3 +165,3 @@ import { ReadableStream } from "node:stream/web";

if (!html) return;
if (isAppRendering) {
if (isAppRendering || leftover) {
pendingRouterHtmlParts.push(html);

@@ -168,0 +168,0 @@ } else {

@@ -1,1 +0,1 @@

{"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { TSR_SCRIPT_BARRIER_ID } from './constants'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\n// Use string constants for simple indexOf matching\nconst BODY_END_TAG = '</body>'\nconst HTML_END_TAG = '</html>'\n\n// Minimum length of a valid closing tag: </a> = 4 characters\nconst MIN_CLOSING_TAG_LENGTH = 4\n\n// Default timeout values (in milliseconds)\nconst DEFAULT_SERIALIZATION_TIMEOUT_MS = 60000\nconst DEFAULT_LIFETIME_TIMEOUT_MS = 60000\n\n// Module-level encoder (stateless, safe to reuse)\nconst textEncoder = new TextEncoder()\n\n/**\n * Finds the position just after the last valid HTML closing tag in the string.\n *\n * Valid closing tags match the pattern: </[a-zA-Z][\\w:.-]*>\n * Examples: </div>, </my-component>, </slot:name.nested>\n *\n * @returns Position after the last closing tag, or -1 if none found\n */\nfunction findLastClosingTagEnd(str: string): number {\n const len = str.length\n if (len < MIN_CLOSING_TAG_LENGTH) return -1\n\n let i = len - 1\n\n while (i >= MIN_CLOSING_TAG_LENGTH - 1) {\n // Look for > (charCode 62)\n if (str.charCodeAt(i) === 62) {\n // Look backwards for valid tag name characters\n let j = i - 1\n\n // Skip through valid tag name characters\n while (j >= 1) {\n const code = str.charCodeAt(j)\n // Check if it's a valid tag name char: [a-zA-Z0-9_:.-]\n if (\n (code >= 97 && code <= 122) || // a-z\n (code >= 65 && code <= 90) || // A-Z\n (code >= 48 && code <= 57) || // 0-9\n code === 95 || // _\n code === 58 || // :\n code === 46 || // .\n code === 45 // -\n ) {\n j--\n } else {\n break\n }\n }\n\n // Check if the first char after </ is a valid start char (letter only)\n const tagNameStart = j + 1\n if (tagNameStart < i) {\n const startCode = str.charCodeAt(tagNameStart)\n // Tag name must start with a letter (a-z or A-Z)\n if (\n (startCode >= 97 && startCode <= 122) ||\n (startCode >= 65 && startCode <= 90)\n ) {\n // Check for </ (charCodes: < = 60, / = 47)\n if (\n j >= 1 &&\n str.charCodeAt(j) === 47 &&\n str.charCodeAt(j - 1) === 60\n ) {\n return i + 1 // Return position after the closing >\n }\n }\n }\n }\n i--\n }\n return -1\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n /** Timeout for serialization to complete after app render finishes (default: 60000ms) */\n timeoutMs?: number\n /** Maximum lifetime of the stream transform (default: 60000ms). Safety net for cleanup. */\n lifetimeMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined\n let stopListeningToSerializationFinished: (() => void) | undefined\n let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let cleanedUp = false\n\n let controller: ReadableStreamDefaultController<any>\n let isStreamClosed = false\n\n // Check upfront if serialization already finished synchronously\n // This is the fast path for routes with no deferred data\n const serializationAlreadyFinished =\n router.serverSsr?.isSerializationFinished() ?? false\n\n /**\n * Cleanup function with guards against multiple calls.\n * Unsubscribes listeners, clears timeouts, frees buffers, and cleans up router SSR state.\n */\n function cleanup() {\n // Guard against multiple cleanup calls - set flag first to prevent re-entry\n if (cleanedUp) return\n cleanedUp = true\n\n // Unsubscribe listeners first (wrap in try-catch for safety)\n try {\n stopListeningToInjectedHtml?.()\n stopListeningToSerializationFinished?.()\n } catch (e) {\n // Ignore errors during unsubscription\n }\n stopListeningToInjectedHtml = undefined\n stopListeningToSerializationFinished = undefined\n\n // Clear all timeouts\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n // Clear buffers to free memory\n pendingRouterHtmlParts = []\n leftover = ''\n pendingClosingTags = ''\n\n // Clean up router SSR state (has its own guard)\n router.serverSsr?.cleanup()\n }\n\n const textDecoder = new TextDecoder()\n\n function safeEnqueue(chunk: string | Uint8Array) {\n if (isStreamClosed) return\n if (typeof chunk === 'string') {\n controller.enqueue(textEncoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n }\n\n function safeClose() {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.close()\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n function safeError(error: unknown) {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.error(error)\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n let isAppRendering = true\n let streamBarrierLifted = false\n let leftover = ''\n let pendingClosingTags = ''\n let serializationFinished = serializationAlreadyFinished\n\n let pendingRouterHtmlParts: Array<string> = []\n\n // Take any HTML that was buffered before we started listening\n const bufferedHtml = router.serverSsr?.takeBufferedHtml()\n if (bufferedHtml) {\n pendingRouterHtmlParts.push(bufferedHtml)\n }\n\n function flushPendingRouterHtml() {\n if (pendingRouterHtmlParts.length > 0) {\n safeEnqueue(pendingRouterHtmlParts.join(''))\n pendingRouterHtmlParts = []\n }\n }\n\n /**\n * Attempts to finish the stream if all conditions are met.\n */\n function tryFinish() {\n // Can only finish when app is done rendering and serialization is complete\n if (isAppRendering || !serializationFinished) return\n if (cleanedUp || isStreamClosed) return\n\n // Clear serialization timeout since we're finishing\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n\n // Flush any remaining bytes in the TextDecoder\n const decoderRemainder = textDecoder.decode()\n\n if (leftover) safeEnqueue(leftover)\n if (decoderRemainder) safeEnqueue(decoderRemainder)\n flushPendingRouterHtml()\n if (pendingClosingTags) safeEnqueue(pendingClosingTags)\n\n safeClose()\n cleanup()\n }\n\n // Set up lifetime timeout as a safety net\n // This ensures cleanup happens even if the stream is never consumed or gets stuck\n const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS\n lifetimeTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.warn(\n `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,\n )\n safeError(new Error('Stream lifetime exceeded'))\n cleanup()\n }\n }, lifetimeMs)\n\n // Only set up listeners if serialization hasn't already finished\n // This avoids unnecessary subscriptions for the common case of no deferred data\n if (!serializationAlreadyFinished) {\n // Listen for injected HTML (for deferred data that resolves later)\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {\n if (cleanedUp || isStreamClosed) return\n\n // Retrieve buffered HTML\n const html = router.serverSsr?.takeBufferedHtml()\n if (!html) return\n\n if (isAppRendering) {\n // Buffer for insertion at next valid position\n pendingRouterHtmlParts.push(html)\n } else {\n // App is done rendering, write directly to output\n safeEnqueue(html)\n }\n })\n\n // Listen for serialization finished\n stopListeningToSerializationFinished = router.subscribe(\n 'onSerializationFinished',\n () => {\n serializationFinished = true\n tryFinish()\n },\n )\n }\n\n // Transform the appStream\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n // Don't process if already cleaned up\n if (cleanedUp || isStreamClosed) return\n\n const text =\n value instanceof Uint8Array\n ? textDecoder.decode(value, { stream: true })\n : String(value)\n const chunkString = leftover + text\n\n // Check for stream barrier (script placeholder) - use indexOf for efficiency\n if (!streamBarrierLifted) {\n if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {\n streamBarrierLifted = true\n router.serverSsr?.liftScriptBarrier()\n }\n }\n\n // Check for body/html end tags\n const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)\n const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)\n\n // If we have both </body> and </html> in proper order,\n // insert router HTML before </body> and hold the closing tags\n if (\n bodyEndIndex !== -1 &&\n htmlEndIndex !== -1 &&\n bodyEndIndex < htmlEndIndex\n ) {\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n safeEnqueue(chunkString.slice(0, bodyEndIndex))\n flushPendingRouterHtml()\n\n leftover = ''\n continue\n }\n\n // Handling partial closing tags split across chunks:\n //\n // Since `chunkString = leftover + text`, any incomplete tag fragment from the\n // previous chunk is prepended to the current chunk, allowing split tags like\n // \"</di\" + \"v>\" to be re-detected as a complete \"</div>\" in the combined string.\n //\n // - If a closing tag IS found (lastClosingTagEnd > 0): We enqueue content up to\n // the end of that tag, flush router HTML, and store the remainder in `leftover`.\n // This remainder may contain a partial tag (e.g., \"</sp\") which will be\n // prepended to the next chunk for re-detection.\n //\n // - If NO closing tag is found: The entire chunk is buffered in `leftover` and\n // will be prepended to the next chunk. This ensures partial tags are never\n // lost and will be detected once the rest of the tag arrives.\n //\n // This approach guarantees correct injection points even when closing tags span\n // chunk boundaries.\n const lastClosingTagEnd = findLastClosingTagEnd(chunkString)\n\n if (lastClosingTagEnd > 0) {\n // Found a closing tag - insert router HTML after it\n safeEnqueue(chunkString.slice(0, lastClosingTagEnd))\n flushPendingRouterHtml()\n\n leftover = chunkString.slice(lastClosingTagEnd)\n } else {\n // No closing tag found - buffer the entire chunk\n leftover = chunkString\n // Any pending router HTML will be inserted when we find a valid position\n }\n }\n\n // Stream ended\n if (cleanedUp || isStreamClosed) return\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n\n // Try to finish if serialization is already done\n if (serializationFinished) {\n tryFinish()\n } else {\n // Set a timeout for serialization to complete\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS\n serializationTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.error('Serialization timeout after app render finished')\n safeError(\n new Error('Serialization timeout after app render finished'),\n )\n cleanup()\n }\n }, timeoutMs)\n }\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n // Handle any errors that occur outside the try block (e.g., getReader() failure)\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n}\n"],"names":[],"mappings":"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"}
{"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { TSR_SCRIPT_BARRIER_ID } from './constants'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\n// Use string constants for simple indexOf matching\nconst BODY_END_TAG = '</body>'\nconst HTML_END_TAG = '</html>'\n\n// Minimum length of a valid closing tag: </a> = 4 characters\nconst MIN_CLOSING_TAG_LENGTH = 4\n\n// Default timeout values (in milliseconds)\nconst DEFAULT_SERIALIZATION_TIMEOUT_MS = 60000\nconst DEFAULT_LIFETIME_TIMEOUT_MS = 60000\n\n// Module-level encoder (stateless, safe to reuse)\nconst textEncoder = new TextEncoder()\n\n/**\n * Finds the position just after the last valid HTML closing tag in the string.\n *\n * Valid closing tags match the pattern: </[a-zA-Z][\\w:.-]*>\n * Examples: </div>, </my-component>, </slot:name.nested>\n *\n * @returns Position after the last closing tag, or -1 if none found\n */\nfunction findLastClosingTagEnd(str: string): number {\n const len = str.length\n if (len < MIN_CLOSING_TAG_LENGTH) return -1\n\n let i = len - 1\n\n while (i >= MIN_CLOSING_TAG_LENGTH - 1) {\n // Look for > (charCode 62)\n if (str.charCodeAt(i) === 62) {\n // Look backwards for valid tag name characters\n let j = i - 1\n\n // Skip through valid tag name characters\n while (j >= 1) {\n const code = str.charCodeAt(j)\n // Check if it's a valid tag name char: [a-zA-Z0-9_:.-]\n if (\n (code >= 97 && code <= 122) || // a-z\n (code >= 65 && code <= 90) || // A-Z\n (code >= 48 && code <= 57) || // 0-9\n code === 95 || // _\n code === 58 || // :\n code === 46 || // .\n code === 45 // -\n ) {\n j--\n } else {\n break\n }\n }\n\n // Check if the first char after </ is a valid start char (letter only)\n const tagNameStart = j + 1\n if (tagNameStart < i) {\n const startCode = str.charCodeAt(tagNameStart)\n // Tag name must start with a letter (a-z or A-Z)\n if (\n (startCode >= 97 && startCode <= 122) ||\n (startCode >= 65 && startCode <= 90)\n ) {\n // Check for </ (charCodes: < = 60, / = 47)\n if (\n j >= 1 &&\n str.charCodeAt(j) === 47 &&\n str.charCodeAt(j - 1) === 60\n ) {\n return i + 1 // Return position after the closing >\n }\n }\n }\n }\n i--\n }\n return -1\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n /** Timeout for serialization to complete after app render finishes (default: 60000ms) */\n timeoutMs?: number\n /** Maximum lifetime of the stream transform (default: 60000ms). Safety net for cleanup. */\n lifetimeMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined\n let stopListeningToSerializationFinished: (() => void) | undefined\n let serializationTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n let cleanedUp = false\n\n let controller: ReadableStreamDefaultController<any>\n let isStreamClosed = false\n\n // Check upfront if serialization already finished synchronously\n // This is the fast path for routes with no deferred data\n const serializationAlreadyFinished =\n router.serverSsr?.isSerializationFinished() ?? false\n\n /**\n * Cleanup function with guards against multiple calls.\n * Unsubscribes listeners, clears timeouts, frees buffers, and cleans up router SSR state.\n */\n function cleanup() {\n // Guard against multiple cleanup calls - set flag first to prevent re-entry\n if (cleanedUp) return\n cleanedUp = true\n\n // Unsubscribe listeners first (wrap in try-catch for safety)\n try {\n stopListeningToInjectedHtml?.()\n stopListeningToSerializationFinished?.()\n } catch (e) {\n // Ignore errors during unsubscription\n }\n stopListeningToInjectedHtml = undefined\n stopListeningToSerializationFinished = undefined\n\n // Clear all timeouts\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n // Clear buffers to free memory\n pendingRouterHtmlParts = []\n leftover = ''\n pendingClosingTags = ''\n\n // Clean up router SSR state (has its own guard)\n router.serverSsr?.cleanup()\n }\n\n const textDecoder = new TextDecoder()\n\n function safeEnqueue(chunk: string | Uint8Array) {\n if (isStreamClosed) return\n if (typeof chunk === 'string') {\n controller.enqueue(textEncoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n }\n\n function safeClose() {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.close()\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n function safeError(error: unknown) {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller.error(error)\n } catch {\n // Stream may already be errored or closed by consumer - safe to ignore\n }\n }\n\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n let isAppRendering = true\n let streamBarrierLifted = false\n let leftover = ''\n let pendingClosingTags = ''\n let serializationFinished = serializationAlreadyFinished\n\n let pendingRouterHtmlParts: Array<string> = []\n\n // Take any HTML that was buffered before we started listening\n const bufferedHtml = router.serverSsr?.takeBufferedHtml()\n if (bufferedHtml) {\n pendingRouterHtmlParts.push(bufferedHtml)\n }\n\n function flushPendingRouterHtml() {\n if (pendingRouterHtmlParts.length > 0) {\n safeEnqueue(pendingRouterHtmlParts.join(''))\n pendingRouterHtmlParts = []\n }\n }\n\n /**\n * Attempts to finish the stream if all conditions are met.\n */\n function tryFinish() {\n // Can only finish when app is done rendering and serialization is complete\n if (isAppRendering || !serializationFinished) return\n if (cleanedUp || isStreamClosed) return\n\n // Clear serialization timeout since we're finishing\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n\n // Flush any remaining bytes in the TextDecoder\n const decoderRemainder = textDecoder.decode()\n\n if (leftover) safeEnqueue(leftover)\n if (decoderRemainder) safeEnqueue(decoderRemainder)\n flushPendingRouterHtml()\n if (pendingClosingTags) safeEnqueue(pendingClosingTags)\n\n safeClose()\n cleanup()\n }\n\n // Set up lifetime timeout as a safety net\n // This ensures cleanup happens even if the stream is never consumed or gets stuck\n const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS\n lifetimeTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.warn(\n `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`,\n )\n safeError(new Error('Stream lifetime exceeded'))\n cleanup()\n }\n }, lifetimeMs)\n\n // Only set up listeners if serialization hasn't already finished\n // This avoids unnecessary subscriptions for the common case of no deferred data\n if (!serializationAlreadyFinished) {\n // Listen for injected HTML (for deferred data that resolves later)\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {\n if (cleanedUp || isStreamClosed) return\n\n // Retrieve buffered HTML\n const html = router.serverSsr?.takeBufferedHtml()\n if (!html) return\n\n if (isAppRendering || leftover) {\n // Buffer when app is still rendering OR when there's leftover content\n // that hasn't been flushed yet. This prevents race conditions where\n // injected HTML appears before buffered app content\n pendingRouterHtmlParts.push(html)\n } else {\n // App done rendering and no leftover - safe to write directly for better streaming\n safeEnqueue(html)\n }\n })\n\n // Listen for serialization finished\n stopListeningToSerializationFinished = router.subscribe(\n 'onSerializationFinished',\n () => {\n serializationFinished = true\n tryFinish()\n },\n )\n }\n\n // Transform the appStream\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n // Don't process if already cleaned up\n if (cleanedUp || isStreamClosed) return\n\n const text =\n value instanceof Uint8Array\n ? textDecoder.decode(value, { stream: true })\n : String(value)\n const chunkString = leftover + text\n\n // Check for stream barrier (script placeholder) - use indexOf for efficiency\n if (!streamBarrierLifted) {\n if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {\n streamBarrierLifted = true\n router.serverSsr?.liftScriptBarrier()\n }\n }\n\n // Check for body/html end tags\n const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)\n const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)\n\n // If we have both </body> and </html> in proper order,\n // insert router HTML before </body> and hold the closing tags\n if (\n bodyEndIndex !== -1 &&\n htmlEndIndex !== -1 &&\n bodyEndIndex < htmlEndIndex\n ) {\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n safeEnqueue(chunkString.slice(0, bodyEndIndex))\n flushPendingRouterHtml()\n\n leftover = ''\n continue\n }\n\n // Handling partial closing tags split across chunks:\n //\n // Since `chunkString = leftover + text`, any incomplete tag fragment from the\n // previous chunk is prepended to the current chunk, allowing split tags like\n // \"</di\" + \"v>\" to be re-detected as a complete \"</div>\" in the combined string.\n //\n // - If a closing tag IS found (lastClosingTagEnd > 0): We enqueue content up to\n // the end of that tag, flush router HTML, and store the remainder in `leftover`.\n // This remainder may contain a partial tag (e.g., \"</sp\") which will be\n // prepended to the next chunk for re-detection.\n //\n // - If NO closing tag is found: The entire chunk is buffered in `leftover` and\n // will be prepended to the next chunk. This ensures partial tags are never\n // lost and will be detected once the rest of the tag arrives.\n //\n // This approach guarantees correct injection points even when closing tags span\n // chunk boundaries.\n const lastClosingTagEnd = findLastClosingTagEnd(chunkString)\n\n if (lastClosingTagEnd > 0) {\n // Found a closing tag - insert router HTML after it\n safeEnqueue(chunkString.slice(0, lastClosingTagEnd))\n flushPendingRouterHtml()\n\n leftover = chunkString.slice(lastClosingTagEnd)\n } else {\n // No closing tag found - buffer the entire chunk\n leftover = chunkString\n // Any pending router HTML will be inserted when we find a valid position\n }\n }\n\n // Stream ended\n if (cleanedUp || isStreamClosed) return\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n\n // Try to finish if serialization is already done\n if (serializationFinished) {\n tryFinish()\n } else {\n // Set a timeout for serialization to complete\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS\n serializationTimeoutHandle = setTimeout(() => {\n if (!cleanedUp && !isStreamClosed) {\n console.error('Serialization timeout after app render finished')\n safeError(\n new Error('Serialization timeout after app render finished'),\n )\n cleanup()\n }\n }, timeoutMs)\n }\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n // Handle any errors that occur outside the try block (e.g., getReader() failure)\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n}\n"],"names":[],"mappings":"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"}
{
"name": "@tanstack/router-core",
"version": "1.157.3",
"version": "1.157.4",
"description": "Modern and scalable routing for React applications",

@@ -5,0 +5,0 @@ "author": "Tanner Linsley",

@@ -274,7 +274,9 @@ import { ReadableStream } from 'node:stream/web'

if (isAppRendering) {
// Buffer for insertion at next valid position
if (isAppRendering || leftover) {
// Buffer when app is still rendering OR when there's leftover content
// that hasn't been flushed yet. This prevents race conditions where
// injected HTML appears before buffered app content
pendingRouterHtmlParts.push(html)
} else {
// App is done rendering, write directly to output
// App done rendering and no leftover - safe to write directly for better streaming
safeEnqueue(html)

@@ -281,0 +283,0 @@ }