@tanstack/router-core 1.157.3 → 1.157.5
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/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/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/package.json +1 -1
- package/src/ssr/ssr-server.ts +69 -32
- package/src/ssr/transformStreamWithRouter.ts +187 -106
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { crossSerializeStream, getCrossReferenceHeader } from "seroval";
|
|
2
2
|
import invariant from "tiny-invariant";
|
|
3
3
|
import { decodePath } from "../utils.js";
|
|
4
|
+
import { createLRUCache } from "../lru-cache.js";
|
|
4
5
|
import minifiedTsrBootStrapScript from "./tsrScript.js";
|
|
5
6
|
import { TSR_SCRIPT_BARRIER_ID, GLOBAL_TSR } from "./constants.js";
|
|
6
7
|
import { defaultSerovalPlugins } from "./serializer/seroval-plugins.js";
|
|
7
8
|
import { makeSsrSerovalPlugin } from "./serializer/transformer.js";
|
|
8
9
|
const SCOPE_ID = "tsr";
|
|
10
|
+
const TSR_PREFIX = GLOBAL_TSR + ".router=";
|
|
11
|
+
const P_PREFIX = GLOBAL_TSR + ".p(()=>";
|
|
12
|
+
const P_SUFFIX = ")";
|
|
9
13
|
function dehydrateMatch(match) {
|
|
10
14
|
const dehydratedMatch = {
|
|
11
15
|
i: match.id,
|
|
@@ -81,6 +85,9 @@ class ScriptBuffer {
|
|
|
81
85
|
if (bufferedScripts.length === 0) {
|
|
82
86
|
return void 0;
|
|
83
87
|
}
|
|
88
|
+
if (bufferedScripts.length === 1) {
|
|
89
|
+
return bufferedScripts[0] + ";document.currentScript.remove()";
|
|
90
|
+
}
|
|
84
91
|
return bufferedScripts.join(";") + ";document.currentScript.remove()";
|
|
85
92
|
}
|
|
86
93
|
injectBufferedScripts() {
|
|
@@ -97,6 +104,16 @@ class ScriptBuffer {
|
|
|
97
104
|
this.router = void 0;
|
|
98
105
|
}
|
|
99
106
|
}
|
|
107
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
108
|
+
const MANIFEST_CACHE_SIZE = 100;
|
|
109
|
+
const manifestCaches = /* @__PURE__ */ new WeakMap();
|
|
110
|
+
function getManifestCache(manifest) {
|
|
111
|
+
const cache = manifestCaches.get(manifest);
|
|
112
|
+
if (cache) return cache;
|
|
113
|
+
const newCache = createLRUCache(MANIFEST_CACHE_SIZE);
|
|
114
|
+
manifestCaches.set(manifest, newCache);
|
|
115
|
+
return newCache;
|
|
116
|
+
}
|
|
100
117
|
function attachRouterServerSsrUtils({
|
|
101
118
|
router,
|
|
102
119
|
manifest
|
|
@@ -109,11 +126,11 @@ function attachRouterServerSsrUtils({
|
|
|
109
126
|
const renderFinishedListeners = [];
|
|
110
127
|
const serializationFinishedListeners = [];
|
|
111
128
|
const scriptBuffer = new ScriptBuffer(router);
|
|
112
|
-
let injectedHtmlBuffer =
|
|
129
|
+
let injectedHtmlBuffer = "";
|
|
113
130
|
router.serverSsr = {
|
|
114
131
|
injectHtml: (html) => {
|
|
115
132
|
if (!html) return;
|
|
116
|
-
injectedHtmlBuffer
|
|
133
|
+
injectedHtmlBuffer += html;
|
|
117
134
|
router.emit({
|
|
118
135
|
type: "onInjectedHtml"
|
|
119
136
|
});
|
|
@@ -132,28 +149,30 @@ function attachRouterServerSsrUtils({
|
|
|
132
149
|
const matches = matchesToDehydrate.map(dehydrateMatch);
|
|
133
150
|
let manifestToDehydrate = void 0;
|
|
134
151
|
if (manifest) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
return [];
|
|
152
|
+
const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId);
|
|
153
|
+
const manifestCacheKey = currentRouteIdsList.join("\0");
|
|
154
|
+
let filteredRoutes;
|
|
155
|
+
if (isProd) {
|
|
156
|
+
filteredRoutes = getManifestCache(manifest).get(manifestCacheKey);
|
|
157
|
+
}
|
|
158
|
+
if (!filteredRoutes) {
|
|
159
|
+
const currentRouteIds = new Set(currentRouteIdsList);
|
|
160
|
+
const nextFilteredRoutes = {};
|
|
161
|
+
for (const routeId in manifest.routes) {
|
|
162
|
+
const routeManifest = manifest.routes[routeId];
|
|
163
|
+
if (currentRouteIds.has(routeId)) {
|
|
164
|
+
nextFilteredRoutes[routeId] = routeManifest;
|
|
165
|
+
} else if (routeManifest.assets && routeManifest.assets.length > 0) {
|
|
166
|
+
nextFilteredRoutes[routeId] = {
|
|
167
|
+
assets: routeManifest.assets
|
|
168
|
+
};
|
|
154
169
|
}
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
}
|
|
171
|
+
if (isProd) {
|
|
172
|
+
getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes);
|
|
173
|
+
}
|
|
174
|
+
filteredRoutes = nextFilteredRoutes;
|
|
175
|
+
}
|
|
157
176
|
manifestToDehydrate = {
|
|
158
177
|
routes: filteredRoutes
|
|
159
178
|
};
|
|
@@ -190,9 +209,9 @@ function attachRouterServerSsrUtils({
|
|
|
190
209
|
refs: /* @__PURE__ */ new Map(),
|
|
191
210
|
plugins,
|
|
192
211
|
onSerialize: (data, initial) => {
|
|
193
|
-
let serialized = initial ?
|
|
212
|
+
let serialized = initial ? TSR_PREFIX + data : data;
|
|
194
213
|
if (trackPlugins.didRun) {
|
|
195
|
-
serialized =
|
|
214
|
+
serialized = P_PREFIX + serialized + P_SUFFIX;
|
|
196
215
|
}
|
|
197
216
|
scriptBuffer.enqueue(serialized);
|
|
198
217
|
},
|
|
@@ -243,18 +262,18 @@ function attachRouterServerSsrUtils({
|
|
|
243
262
|
scriptBuffer.liftBarrier();
|
|
244
263
|
},
|
|
245
264
|
takeBufferedHtml() {
|
|
246
|
-
if (injectedHtmlBuffer
|
|
265
|
+
if (!injectedHtmlBuffer) {
|
|
247
266
|
return void 0;
|
|
248
267
|
}
|
|
249
|
-
const buffered = injectedHtmlBuffer
|
|
250
|
-
injectedHtmlBuffer =
|
|
268
|
+
const buffered = injectedHtmlBuffer;
|
|
269
|
+
injectedHtmlBuffer = "";
|
|
251
270
|
return buffered;
|
|
252
271
|
},
|
|
253
272
|
cleanup() {
|
|
254
273
|
if (!router.serverSsr) return;
|
|
255
274
|
renderFinishedListeners.length = 0;
|
|
256
275
|
serializationFinishedListeners.length = 0;
|
|
257
|
-
injectedHtmlBuffer =
|
|
276
|
+
injectedHtmlBuffer = "";
|
|
258
277
|
scriptBuffer.cleanup();
|
|
259
278
|
router.serverSsr = void 0;
|
|
260
279
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { decodePath } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { DehydratedMatch, DehydratedRouter } from './types'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n cleanup: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n }\n onSerializationFinished: {\n type: 'onSerializationFinished'\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n private router: AnyRouter | undefined\n private _queue: Array<string>\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n private _pendingMicrotask = false\n\n constructor(router: AnyRouter) {\n this.router = router\n // Copy INITIAL_SCRIPTS to avoid mutating the shared array\n this._queue = INITIAL_SCRIPTS.slice()\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\n this._queue.push(script)\n // If barrier is lifted, schedule injection (if not already scheduled)\n if (this._scriptBarrierLifted && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted || this._cleanedUp) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0 && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n /**\n * Flushes any pending scripts synchronously.\n * Call this before emitting onSerializationFinished to ensure all scripts are injected.\n *\n * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,\n * scripts should remain in the queue so takeBufferedScripts() can retrieve them\n */\n flush() {\n if (!this._scriptBarrierLifted) return\n if (this._cleanedUp) return\n this._pendingMicrotask = false\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n // Append cleanup script and join - avoid push() to not mutate then iterate\n return bufferedScripts.join(';') + ';document.currentScript.remove()'\n }\n\n injectBufferedScripts() {\n if (this._cleanedUp) return\n // Early return if queue is empty (avoids unnecessary takeAll() call)\n if (this._queue.length === 0) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n let _serializationFinished = false\n const renderFinishedListeners: Array<() => void> = []\n const serializationFinishedListeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n let injectedHtmlBuffer: Array<string> = []\n\n router.serverSsr = {\n injectHtml: (html: string) => {\n if (!html) return\n // Buffer the HTML so it can be retrieved via takeBufferedHtml()\n injectedHtmlBuffer.push(html)\n // Emit event to notify subscribers that new HTML is available\n router.emit({\n type: 'onInjectedHtml',\n })\n },\n injectScript: (script: string) => {\n if (!script) return\n const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`\n router.serverSsr!.injectHtml(html)\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // For currently matched routes, send full manifest (preloads + assets)\n // For all other routes, only send assets (no preloads as they are handled via dynamic imports)\n if (manifest) {\n const currentRouteIds = new Set(\n router.state.matches.map((k) => k.routeId),\n )\n const filteredRoutes = Object.fromEntries(\n Object.entries(manifest.routes).flatMap(\n ([routeId, routeManifest]) => {\n if (currentRouteIds.has(routeId)) {\n return [[routeId, routeManifest]]\n } else if (\n routeManifest.assets &&\n routeManifest.assets.length > 0\n ) {\n return [\n [\n routeId,\n {\n assets: routeManifest.assets,\n },\n ],\n ]\n }\n return []\n },\n ),\n )\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const trackPlugins = { didRun: false }\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n const plugins = serializationAdapters\n ? serializationAdapters\n .map((t) => makeSsrSerovalPlugin(t, trackPlugins))\n .concat(defaultSerovalPlugins)\n : defaultSerovalPlugins\n\n const signalSerializationComplete = () => {\n _serializationFinished = true\n try {\n serializationFinishedListeners.forEach((l) => l())\n router.emit({ type: 'onSerializationFinished' })\n } catch (err) {\n console.error('Serialization listener error:', err)\n } finally {\n serializationFinishedListeners.length = 0\n renderFinishedListeners.length = 0\n }\n }\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins,\n onSerialize: (data, initial) => {\n let serialized = initial ? GLOBAL_TSR + '.router=' + data : data\n if (trackPlugins.didRun) {\n serialized = GLOBAL_TSR + '.p(()=>' + serialized + ')'\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.e()')\n // Flush all pending scripts synchronously before signaling completion\n // This ensures all scripts are injected before onSerializationFinished is emitted\n scriptBuffer.flush()\n signalSerializationComplete()\n },\n onError: (err) => {\n console.error('Serialization error:', err)\n signalSerializationComplete()\n },\n })\n },\n isDehydrated() {\n return _dehydrated\n },\n isSerializationFinished() {\n return _serializationFinished\n },\n onRenderFinished: (listener) => renderFinishedListeners.push(listener),\n onSerializationFinished: (listener) =>\n serializationFinishedListeners.push(listener),\n setRenderFinished: () => {\n // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called\n try {\n renderFinishedListeners.forEach((l) => l())\n } catch (err) {\n console.error('Error in render finished listener:', err)\n } finally {\n // Clear listeners after calling them to prevent memory leaks\n renderFinishedListeners.length = 0\n }\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n takeBufferedHtml() {\n if (injectedHtmlBuffer.length === 0) {\n return undefined\n }\n const buffered = injectedHtmlBuffer.join('')\n injectedHtmlBuffer = []\n return buffered\n },\n cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n renderFinishedListeners.length = 0\n serializationFinishedListeners.length = 0\n injectedHtmlBuffer = []\n scriptBuffer.cleanup()\n router.serverSsr = undefined\n },\n }\n}\n\n/**\n * Get the origin for the request.\n *\n * SECURITY: We intentionally do NOT trust the Origin header for determining\n * the router's origin. The Origin header can be spoofed by attackers, which\n * could lead to SSRF-like vulnerabilities where redirects are constructed\n * using a malicious origin (CVE-2024-34351).\n *\n * Instead, we derive the origin from request.url, which is typically set by\n * the server infrastructure (not client-controlled headers).\n *\n * For applications behind proxies that need to trust forwarded headers,\n * use the router's `origin` option to explicitly configure a trusted origin.\n */\nexport function getOrigin(request: Request) {\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n\n// server and browser can decode/encode characters differently in paths and search params.\n// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.\n// for example, in paths \"|\" is not encoded on the server but is encoded on chromium (and not on firefox) while \"대\" is encoded on both sides.\n// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.\n// new URLSearchParams() encodes \"|\" while new URL() does not, and in this instance\n// chromium treats search params differently than paths, i.e. \"|\" is not encoded in search params.\nexport function getNormalizedURL(url: string | URL, base?: string | URL) {\n // ensure backslashes are encoded correctly in the URL\n if (typeof url === 'string') url = url.replace('\\\\', '%5C')\n\n const rawUrl = new URL(url, base)\n const decodedPathname = decodePath(rawUrl.pathname)\n const searchParams = new URLSearchParams(rawUrl.search)\n const normalizedHref =\n decodedPathname +\n (searchParams.size > 0 ? '?' : '') +\n searchParams.toString() +\n rawUrl.hash\n\n return new URL(normalizedHref, rawUrl.origin)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { decodePath } from '../utils'\nimport { createLRUCache } from '../lru-cache'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { LRUCache } from '../lru-cache'\nimport type { DehydratedMatch, DehydratedRouter } from './types'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n cleanup: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n }\n onSerializationFinished: {\n type: 'onSerializationFinished'\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nconst TSR_PREFIX = GLOBAL_TSR + '.router='\nconst P_PREFIX = GLOBAL_TSR + '.p(()=>'\nconst P_SUFFIX = ')'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n private router: AnyRouter | undefined\n private _queue: Array<string>\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n private _pendingMicrotask = false\n\n constructor(router: AnyRouter) {\n this.router = router\n // Copy INITIAL_SCRIPTS to avoid mutating the shared array\n this._queue = INITIAL_SCRIPTS.slice()\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\n this._queue.push(script)\n // If barrier is lifted, schedule injection (if not already scheduled)\n if (this._scriptBarrierLifted && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted || this._cleanedUp) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0 && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n /**\n * Flushes any pending scripts synchronously.\n * Call this before emitting onSerializationFinished to ensure all scripts are injected.\n *\n * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,\n * scripts should remain in the queue so takeBufferedScripts() can retrieve them\n */\n flush() {\n if (!this._scriptBarrierLifted) return\n if (this._cleanedUp) return\n this._pendingMicrotask = false\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n // Optimization: if only one script, avoid join\n if (bufferedScripts.length === 1) {\n return bufferedScripts[0] + ';document.currentScript.remove()'\n }\n // Append cleanup script and join - avoid push() to not mutate then iterate\n return bufferedScripts.join(';') + ';document.currentScript.remove()'\n }\n\n injectBufferedScripts() {\n if (this._cleanedUp) return\n // Early return if queue is empty (avoids unnecessary takeAll() call)\n if (this._queue.length === 0) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nconst isProd = process.env.NODE_ENV === 'production'\n\ntype FilteredRoutes = Manifest['routes']\n\ntype ManifestLRU = LRUCache<string, FilteredRoutes>\n\nconst MANIFEST_CACHE_SIZE = 100\nconst manifestCaches = new WeakMap<Manifest, ManifestLRU>()\n\nfunction getManifestCache(manifest: Manifest): ManifestLRU {\n const cache = manifestCaches.get(manifest)\n if (cache) return cache\n const newCache = createLRUCache<string, FilteredRoutes>(MANIFEST_CACHE_SIZE)\n manifestCaches.set(manifest, newCache)\n return newCache\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n let _serializationFinished = false\n const renderFinishedListeners: Array<() => void> = []\n const serializationFinishedListeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n let injectedHtmlBuffer = ''\n\n router.serverSsr = {\n injectHtml: (html: string) => {\n if (!html) return\n // Buffer the HTML so it can be retrieved via takeBufferedHtml()\n injectedHtmlBuffer += html\n // Emit event to notify subscribers that new HTML is available\n router.emit({\n type: 'onInjectedHtml',\n })\n },\n injectScript: (script: string) => {\n if (!script) return\n const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`\n router.serverSsr!.injectHtml(html)\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // For currently matched routes, send full manifest (preloads + assets)\n // For all other routes, only send assets (no preloads as they are handled via dynamic imports)\n if (manifest) {\n // Prod-only caching; in dev manifests may be replaced/updated (HMR)\n const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)\n const manifestCacheKey = currentRouteIdsList.join('\\0')\n\n let filteredRoutes: FilteredRoutes | undefined\n\n if (isProd) {\n filteredRoutes = getManifestCache(manifest).get(manifestCacheKey)\n }\n\n if (!filteredRoutes) {\n const currentRouteIds = new Set(currentRouteIdsList)\n const nextFilteredRoutes: FilteredRoutes = {}\n\n for (const routeId in manifest.routes) {\n const routeManifest = manifest.routes[routeId]!\n if (currentRouteIds.has(routeId)) {\n nextFilteredRoutes[routeId] = routeManifest\n } else if (\n routeManifest.assets &&\n routeManifest.assets.length > 0\n ) {\n nextFilteredRoutes[routeId] = {\n assets: routeManifest.assets,\n }\n }\n }\n\n if (isProd) {\n getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)\n }\n\n filteredRoutes = nextFilteredRoutes\n }\n\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const trackPlugins = { didRun: false }\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n const plugins = serializationAdapters\n ? serializationAdapters\n .map((t) => makeSsrSerovalPlugin(t, trackPlugins))\n .concat(defaultSerovalPlugins)\n : defaultSerovalPlugins\n\n const signalSerializationComplete = () => {\n _serializationFinished = true\n try {\n serializationFinishedListeners.forEach((l) => l())\n router.emit({ type: 'onSerializationFinished' })\n } catch (err) {\n console.error('Serialization listener error:', err)\n } finally {\n serializationFinishedListeners.length = 0\n renderFinishedListeners.length = 0\n }\n }\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins,\n onSerialize: (data, initial) => {\n let serialized = initial ? TSR_PREFIX + data : data\n if (trackPlugins.didRun) {\n serialized = P_PREFIX + serialized + P_SUFFIX\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.e()')\n // Flush all pending scripts synchronously before signaling completion\n // This ensures all scripts are injected before onSerializationFinished is emitted\n scriptBuffer.flush()\n signalSerializationComplete()\n },\n onError: (err) => {\n console.error('Serialization error:', err)\n signalSerializationComplete()\n },\n })\n },\n isDehydrated() {\n return _dehydrated\n },\n isSerializationFinished() {\n return _serializationFinished\n },\n onRenderFinished: (listener) => renderFinishedListeners.push(listener),\n onSerializationFinished: (listener) =>\n serializationFinishedListeners.push(listener),\n setRenderFinished: () => {\n // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called\n try {\n renderFinishedListeners.forEach((l) => l())\n } catch (err) {\n console.error('Error in render finished listener:', err)\n } finally {\n // Clear listeners after calling them to prevent memory leaks\n renderFinishedListeners.length = 0\n }\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n takeBufferedHtml() {\n if (!injectedHtmlBuffer) {\n return undefined\n }\n const buffered = injectedHtmlBuffer\n injectedHtmlBuffer = ''\n return buffered\n },\n cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n renderFinishedListeners.length = 0\n serializationFinishedListeners.length = 0\n injectedHtmlBuffer = ''\n scriptBuffer.cleanup()\n router.serverSsr = undefined\n },\n }\n}\n\n/**\n * Get the origin for the request.\n *\n * SECURITY: We intentionally do NOT trust the Origin header for determining\n * the router's origin. The Origin header can be spoofed by attackers, which\n * could lead to SSRF-like vulnerabilities where redirects are constructed\n * using a malicious origin (CVE-2024-34351).\n *\n * Instead, we derive the origin from request.url, which is typically set by\n * the server infrastructure (not client-controlled headers).\n *\n * For applications behind proxies that need to trust forwarded headers,\n * use the router's `origin` option to explicitly configure a trusted origin.\n */\nexport function getOrigin(request: Request) {\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n\n// server and browser can decode/encode characters differently in paths and search params.\n// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.\n// for example, in paths \"|\" is not encoded on the server but is encoded on chromium (and not on firefox) while \"대\" is encoded on both sides.\n// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.\n// new URLSearchParams() encodes \"|\" while new URL() does not, and in this instance\n// chromium treats search params differently than paths, i.e. \"|\" is not encoded in search params.\nexport function getNormalizedURL(url: string | URL, base?: string | URL) {\n // ensure backslashes are encoded correctly in the URL\n if (typeof url === 'string') url = url.replace('\\\\', '%5C')\n\n const rawUrl = new URL(url, base)\n const decodedPathname = decodePath(rawUrl.pathname)\n const searchParams = new URLSearchParams(rawUrl.search)\n const normalizedHref =\n decodedPathname +\n (searchParams.size > 0 ? '?' : '') +\n searchParams.toString() +\n rawUrl.hash\n\n return new URL(normalizedHref, rawUrl.origin)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -51,6 +51,87 @@ function findLastClosingTagEnd(str) {
|
|
|
51
51
|
return -1;
|
|
52
52
|
}
|
|
53
53
|
function transformStreamWithRouter(router, appStream, opts) {
|
|
54
|
+
const serializationAlreadyFinished = router.serverSsr?.isSerializationFinished() ?? false;
|
|
55
|
+
const initialBufferedHtml = router.serverSsr?.takeBufferedHtml();
|
|
56
|
+
if (serializationAlreadyFinished && !initialBufferedHtml) {
|
|
57
|
+
let cleanedUp2 = false;
|
|
58
|
+
let controller2;
|
|
59
|
+
let isStreamClosed2 = false;
|
|
60
|
+
let lifetimeTimeoutHandle2;
|
|
61
|
+
const cleanup2 = () => {
|
|
62
|
+
if (cleanedUp2) return;
|
|
63
|
+
cleanedUp2 = true;
|
|
64
|
+
if (lifetimeTimeoutHandle2 !== void 0) {
|
|
65
|
+
clearTimeout(lifetimeTimeoutHandle2);
|
|
66
|
+
lifetimeTimeoutHandle2 = void 0;
|
|
67
|
+
}
|
|
68
|
+
router.serverSsr?.cleanup();
|
|
69
|
+
};
|
|
70
|
+
const safeClose2 = () => {
|
|
71
|
+
if (isStreamClosed2) return;
|
|
72
|
+
isStreamClosed2 = true;
|
|
73
|
+
try {
|
|
74
|
+
controller2?.close();
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const safeError2 = (error) => {
|
|
79
|
+
if (isStreamClosed2) return;
|
|
80
|
+
isStreamClosed2 = true;
|
|
81
|
+
try {
|
|
82
|
+
controller2?.error(error);
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const lifetimeMs2 = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS;
|
|
87
|
+
lifetimeTimeoutHandle2 = setTimeout(() => {
|
|
88
|
+
if (!cleanedUp2 && !isStreamClosed2) {
|
|
89
|
+
console.warn(
|
|
90
|
+
`SSR stream transform exceeded maximum lifetime (${lifetimeMs2}ms), forcing cleanup`
|
|
91
|
+
);
|
|
92
|
+
safeError2(new Error("Stream lifetime exceeded"));
|
|
93
|
+
cleanup2();
|
|
94
|
+
}
|
|
95
|
+
}, lifetimeMs2);
|
|
96
|
+
const stream2 = new ReadableStream({
|
|
97
|
+
start(c) {
|
|
98
|
+
controller2 = c;
|
|
99
|
+
},
|
|
100
|
+
cancel() {
|
|
101
|
+
isStreamClosed2 = true;
|
|
102
|
+
cleanup2();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
(async () => {
|
|
106
|
+
const reader = appStream.getReader();
|
|
107
|
+
try {
|
|
108
|
+
while (true) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done) break;
|
|
111
|
+
if (cleanedUp2 || isStreamClosed2) return;
|
|
112
|
+
controller2?.enqueue(value);
|
|
113
|
+
}
|
|
114
|
+
if (cleanedUp2 || isStreamClosed2) return;
|
|
115
|
+
router.serverSsr?.setRenderFinished();
|
|
116
|
+
safeClose2();
|
|
117
|
+
cleanup2();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (cleanedUp2) return;
|
|
120
|
+
console.error("Error reading appStream:", error);
|
|
121
|
+
router.serverSsr?.setRenderFinished();
|
|
122
|
+
safeError2(error);
|
|
123
|
+
cleanup2();
|
|
124
|
+
} finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
})().catch((error) => {
|
|
128
|
+
if (cleanedUp2) return;
|
|
129
|
+
console.error("Error in stream transform:", error);
|
|
130
|
+
safeError2(error);
|
|
131
|
+
cleanup2();
|
|
132
|
+
});
|
|
133
|
+
return stream2;
|
|
134
|
+
}
|
|
54
135
|
let stopListeningToInjectedHtml;
|
|
55
136
|
let stopListeningToSerializationFinished;
|
|
56
137
|
let serializationTimeoutHandle;
|
|
@@ -58,31 +139,14 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
58
139
|
let cleanedUp = false;
|
|
59
140
|
let controller;
|
|
60
141
|
let isStreamClosed = false;
|
|
61
|
-
const serializationAlreadyFinished = router.serverSsr?.isSerializationFinished() ?? false;
|
|
62
|
-
function cleanup() {
|
|
63
|
-
if (cleanedUp) return;
|
|
64
|
-
cleanedUp = true;
|
|
65
|
-
try {
|
|
66
|
-
stopListeningToInjectedHtml?.();
|
|
67
|
-
stopListeningToSerializationFinished?.();
|
|
68
|
-
} catch (e) {
|
|
69
|
-
}
|
|
70
|
-
stopListeningToInjectedHtml = void 0;
|
|
71
|
-
stopListeningToSerializationFinished = void 0;
|
|
72
|
-
if (serializationTimeoutHandle !== void 0) {
|
|
73
|
-
clearTimeout(serializationTimeoutHandle);
|
|
74
|
-
serializationTimeoutHandle = void 0;
|
|
75
|
-
}
|
|
76
|
-
if (lifetimeTimeoutHandle !== void 0) {
|
|
77
|
-
clearTimeout(lifetimeTimeoutHandle);
|
|
78
|
-
lifetimeTimeoutHandle = void 0;
|
|
79
|
-
}
|
|
80
|
-
pendingRouterHtmlParts = [];
|
|
81
|
-
leftover = "";
|
|
82
|
-
pendingClosingTags = "";
|
|
83
|
-
router.serverSsr?.cleanup();
|
|
84
|
-
}
|
|
85
142
|
const textDecoder = new TextDecoder();
|
|
143
|
+
let pendingRouterHtml = initialBufferedHtml ?? "";
|
|
144
|
+
let leftover = "";
|
|
145
|
+
let pendingClosingTags = "";
|
|
146
|
+
const MAX_LEFTOVER_CHARS = 2048;
|
|
147
|
+
let isAppRendering = true;
|
|
148
|
+
let streamBarrierLifted = false;
|
|
149
|
+
let serializationFinished = serializationAlreadyFinished;
|
|
86
150
|
function safeEnqueue(chunk) {
|
|
87
151
|
if (isStreamClosed) return;
|
|
88
152
|
if (typeof chunk === "string") {
|
|
@@ -107,6 +171,29 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
107
171
|
} catch {
|
|
108
172
|
}
|
|
109
173
|
}
|
|
174
|
+
function cleanup() {
|
|
175
|
+
if (cleanedUp) return;
|
|
176
|
+
cleanedUp = true;
|
|
177
|
+
try {
|
|
178
|
+
stopListeningToInjectedHtml?.();
|
|
179
|
+
stopListeningToSerializationFinished?.();
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
stopListeningToInjectedHtml = void 0;
|
|
183
|
+
stopListeningToSerializationFinished = void 0;
|
|
184
|
+
if (serializationTimeoutHandle !== void 0) {
|
|
185
|
+
clearTimeout(serializationTimeoutHandle);
|
|
186
|
+
serializationTimeoutHandle = void 0;
|
|
187
|
+
}
|
|
188
|
+
if (lifetimeTimeoutHandle !== void 0) {
|
|
189
|
+
clearTimeout(lifetimeTimeoutHandle);
|
|
190
|
+
lifetimeTimeoutHandle = void 0;
|
|
191
|
+
}
|
|
192
|
+
pendingRouterHtml = "";
|
|
193
|
+
leftover = "";
|
|
194
|
+
pendingClosingTags = "";
|
|
195
|
+
router.serverSsr?.cleanup();
|
|
196
|
+
}
|
|
110
197
|
const stream = new ReadableStream({
|
|
111
198
|
start(c) {
|
|
112
199
|
controller = c;
|
|
@@ -116,21 +203,14 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
116
203
|
cleanup();
|
|
117
204
|
}
|
|
118
205
|
});
|
|
119
|
-
let isAppRendering = true;
|
|
120
|
-
let streamBarrierLifted = false;
|
|
121
|
-
let leftover = "";
|
|
122
|
-
let pendingClosingTags = "";
|
|
123
|
-
let serializationFinished = serializationAlreadyFinished;
|
|
124
|
-
let pendingRouterHtmlParts = [];
|
|
125
|
-
const bufferedHtml = router.serverSsr?.takeBufferedHtml();
|
|
126
|
-
if (bufferedHtml) {
|
|
127
|
-
pendingRouterHtmlParts.push(bufferedHtml);
|
|
128
|
-
}
|
|
129
206
|
function flushPendingRouterHtml() {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
207
|
+
if (!pendingRouterHtml) return;
|
|
208
|
+
safeEnqueue(pendingRouterHtml);
|
|
209
|
+
pendingRouterHtml = "";
|
|
210
|
+
}
|
|
211
|
+
function appendRouterHtml(html) {
|
|
212
|
+
if (!html) return;
|
|
213
|
+
pendingRouterHtml += html;
|
|
134
214
|
}
|
|
135
215
|
function tryFinish() {
|
|
136
216
|
if (isAppRendering || !serializationFinished) return;
|
|
@@ -162,8 +242,8 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
162
242
|
if (cleanedUp || isStreamClosed) return;
|
|
163
243
|
const html = router.serverSsr?.takeBufferedHtml();
|
|
164
244
|
if (!html) return;
|
|
165
|
-
if (isAppRendering) {
|
|
166
|
-
|
|
245
|
+
if (isAppRendering || leftover || pendingClosingTags) {
|
|
246
|
+
appendRouterHtml(html);
|
|
167
247
|
} else {
|
|
168
248
|
safeEnqueue(html);
|
|
169
249
|
}
|
|
@@ -184,13 +264,18 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
184
264
|
if (done) break;
|
|
185
265
|
if (cleanedUp || isStreamClosed) return;
|
|
186
266
|
const text = value instanceof Uint8Array ? textDecoder.decode(value, { stream: true }) : String(value);
|
|
187
|
-
const chunkString = leftover + text;
|
|
267
|
+
const chunkString = leftover ? leftover + text : text;
|
|
188
268
|
if (!streamBarrierLifted) {
|
|
189
269
|
if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {
|
|
190
270
|
streamBarrierLifted = true;
|
|
191
271
|
router.serverSsr?.liftScriptBarrier();
|
|
192
272
|
}
|
|
193
273
|
}
|
|
274
|
+
if (pendingClosingTags) {
|
|
275
|
+
pendingClosingTags += chunkString;
|
|
276
|
+
leftover = "";
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
194
279
|
const bodyEndIndex = chunkString.indexOf(BODY_END_TAG);
|
|
195
280
|
const htmlEndIndex = chunkString.indexOf(HTML_END_TAG);
|
|
196
281
|
if (bodyEndIndex !== -1 && htmlEndIndex !== -1 && bodyEndIndex < htmlEndIndex) {
|
|
@@ -205,8 +290,19 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
205
290
|
safeEnqueue(chunkString.slice(0, lastClosingTagEnd));
|
|
206
291
|
flushPendingRouterHtml();
|
|
207
292
|
leftover = chunkString.slice(lastClosingTagEnd);
|
|
293
|
+
if (leftover.length > MAX_LEFTOVER_CHARS) {
|
|
294
|
+
safeEnqueue(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS));
|
|
295
|
+
leftover = leftover.slice(-MAX_LEFTOVER_CHARS);
|
|
296
|
+
}
|
|
208
297
|
} else {
|
|
209
|
-
|
|
298
|
+
const combined = chunkString;
|
|
299
|
+
if (combined.length > MAX_LEFTOVER_CHARS) {
|
|
300
|
+
const flushUpto = combined.length - MAX_LEFTOVER_CHARS;
|
|
301
|
+
safeEnqueue(combined.slice(0, flushUpto));
|
|
302
|
+
leftover = combined.slice(flushUpto);
|
|
303
|
+
} else {
|
|
304
|
+
leftover = combined;
|
|
305
|
+
}
|
|
210
306
|
}
|
|
211
307
|
}
|
|
212
308
|
if (cleanedUp || isStreamClosed) return;
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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 // 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 // Take any HTML that was buffered before we started listening\n const initialBufferedHtml = router.serverSsr?.takeBufferedHtml()\n\n // True passthrough: if serialization already finished and nothing buffered,\n // we can avoid any decoding/scanning while still honoring cleanup + setRenderFinished.\n if (serializationAlreadyFinished && !initialBufferedHtml) {\n let cleanedUp = false\n let controller: ReadableStreamDefaultController<Uint8Array> | undefined\n let isStreamClosed = false\n let lifetimeTimeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const cleanup = () => {\n if (cleanedUp) return\n cleanedUp = true\n\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n router.serverSsr?.cleanup()\n }\n\n const safeClose = () => {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller?.close()\n } catch {\n // ignore\n }\n }\n\n const safeError = (error: unknown) => {\n if (isStreamClosed) return\n isStreamClosed = true\n try {\n controller?.error(error)\n } catch {\n // ignore\n }\n }\n\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 const stream = new ReadableStream<Uint8Array>({\n start(c) {\n controller = c\n },\n cancel() {\n isStreamClosed = true\n cleanup()\n },\n })\n\n ;(async () => {\n const reader = appStream.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n if (cleanedUp || isStreamClosed) return\n controller?.enqueue(value as unknown as Uint8Array)\n }\n\n if (cleanedUp || isStreamClosed) return\n\n router.serverSsr?.setRenderFinished()\n safeClose()\n cleanup()\n } catch (error) {\n if (cleanedUp) return\n console.error('Error reading appStream:', error)\n router.serverSsr?.setRenderFinished()\n safeError(error)\n cleanup()\n } finally {\n reader.releaseLock()\n }\n })().catch((error) => {\n if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\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 const textDecoder = new TextDecoder()\n\n // concat'd router HTML; avoids array joins on each flush\n let pendingRouterHtml = initialBufferedHtml ?? ''\n\n // between-chunk text buffer; keep bounded to avoid unbounded memory\n let leftover = ''\n\n // captured closing tags from </body> onward\n let pendingClosingTags = ''\n\n // conservative cap: enough to hold any partial closing tag + a bit\n const MAX_LEFTOVER_CHARS = 2048\n\n let isAppRendering = true\n let streamBarrierLifted = false\n let serializationFinished = serializationAlreadyFinished\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 // 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 // ignore\n }\n }\n\n /**\n * Cleanup with guards; must be idempotent.\n */\n function cleanup() {\n if (cleanedUp) return\n cleanedUp = true\n\n try {\n stopListeningToInjectedHtml?.()\n stopListeningToSerializationFinished?.()\n } catch {\n // ignore\n }\n stopListeningToInjectedHtml = undefined\n stopListeningToSerializationFinished = undefined\n\n if (serializationTimeoutHandle !== undefined) {\n clearTimeout(serializationTimeoutHandle)\n serializationTimeoutHandle = undefined\n }\n if (lifetimeTimeoutHandle !== undefined) {\n clearTimeout(lifetimeTimeoutHandle)\n lifetimeTimeoutHandle = undefined\n }\n\n pendingRouterHtml = ''\n leftover = ''\n pendingClosingTags = ''\n\n router.serverSsr?.cleanup()\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 function flushPendingRouterHtml() {\n if (!pendingRouterHtml) return\n safeEnqueue(pendingRouterHtml)\n pendingRouterHtml = ''\n }\n\n function appendRouterHtml(html: string) {\n if (!html) return\n pendingRouterHtml += html\n }\n\n /**\n * Finish only when app done and serialization complete.\n */\n function tryFinish() {\n if (isAppRendering || !serializationFinished) return\n if (cleanedUp || isStreamClosed) return\n\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 // Safety net: cleanup even if consumer never reads\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 if (!serializationAlreadyFinished) {\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', () => {\n if (cleanedUp || isStreamClosed) return\n const html = router.serverSsr?.takeBufferedHtml()\n if (!html) return\n\n // If we've already captured </body> (pendingClosingTags), we must keep appending\n // so injection stays before the stored closing tags.\n if (isAppRendering || leftover || pendingClosingTags) {\n appendRouterHtml(html)\n } else {\n safeEnqueue(html)\n }\n })\n\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 if (cleanedUp || isStreamClosed) return\n\n const text =\n value instanceof Uint8Array\n ? textDecoder.decode(value, { stream: true })\n : String(value)\n\n // Fast path: most chunks have no pending left-over.\n const chunkString = leftover ? leftover + text : text\n\n if (!streamBarrierLifted) {\n if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {\n streamBarrierLifted = true\n router.serverSsr?.liftScriptBarrier()\n }\n }\n\n // If we already saw </body>, everything else is part of tail; buffer it.\n if (pendingClosingTags) {\n pendingClosingTags += chunkString\n leftover = ''\n continue\n }\n\n const bodyEndIndex = chunkString.indexOf(BODY_END_TAG)\n const htmlEndIndex = chunkString.indexOf(HTML_END_TAG)\n\n if (\n bodyEndIndex !== -1 &&\n htmlEndIndex !== -1 &&\n bodyEndIndex < htmlEndIndex\n ) {\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n safeEnqueue(chunkString.slice(0, bodyEndIndex))\n flushPendingRouterHtml()\n leftover = ''\n continue\n }\n\n const lastClosingTagEnd = findLastClosingTagEnd(chunkString)\n\n if (lastClosingTagEnd > 0) {\n safeEnqueue(chunkString.slice(0, lastClosingTagEnd))\n flushPendingRouterHtml()\n\n leftover = chunkString.slice(lastClosingTagEnd)\n if (leftover.length > MAX_LEFTOVER_CHARS) {\n // Ensure bounded memory even if a consumer streams long text sequences\n // without any closing tags. This may reduce injection granularity but is correct.\n safeEnqueue(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS))\n leftover = leftover.slice(-MAX_LEFTOVER_CHARS)\n }\n } else {\n // No closing tag found; keep small tail to handle split closing tags,\n // but stream older bytes to prevent unbounded buffering.\n const combined = chunkString\n if (combined.length > MAX_LEFTOVER_CHARS) {\n const flushUpto = combined.length - MAX_LEFTOVER_CHARS\n safeEnqueue(combined.slice(0, flushUpto))\n leftover = combined.slice(flushUpto)\n } else {\n leftover = combined\n }\n }\n }\n\n if (cleanedUp || isStreamClosed) return\n\n isAppRendering = false\n router.serverSsr?.setRenderFinished()\n\n if (serializationFinished) {\n tryFinish()\n } else {\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 if (cleanedUp) return\n console.error('Error in stream transform:', error)\n safeError(error)\n cleanup()\n })\n\n return stream\n}\n"],"names":["cleanedUp","controller","isStreamClosed","lifetimeTimeoutHandle","cleanup","safeClose","safeError","lifetimeMs","stream"],"mappings":"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"}
|