@tanstack/router-core 1.142.6 → 1.142.8

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.
@@ -1,6 +1,6 @@
1
1
  import { ReadableStream } from "node:stream/web";
2
2
  import { Readable } from "node:stream";
3
- import { createControlledPromise } from "../utils.js";
3
+ import { TSR_SCRIPT_BARRIER_ID } from "./constants.js";
4
4
  function transformReadableStreamWithRouter(router, routerStream) {
5
5
  return transformStreamWithRouter(router, routerStream);
6
6
  }
@@ -9,225 +9,242 @@ function transformPipeableStreamWithRouter(router, routerStream) {
9
9
  transformStreamWithRouter(router, Readable.toWeb(routerStream))
10
10
  );
11
11
  }
12
- const TSR_SCRIPT_BARRIER_ID = "$tsr-stream-barrier";
13
- const patternBodyEnd = /(<\/body>)/;
14
- const patternHtmlEnd = /(<\/html>)/;
15
- const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g;
16
- function createPassthrough(onCancel) {
17
- let controller;
18
- const encoder = new TextEncoder();
19
- const stream = new ReadableStream({
20
- start(c) {
21
- controller = c;
22
- },
23
- cancel() {
24
- res.destroyed = true;
25
- onCancel();
26
- }
27
- });
28
- const res = {
29
- stream,
30
- write: (chunk) => {
31
- if (res.destroyed) return;
32
- if (typeof chunk === "string") {
33
- controller.enqueue(encoder.encode(chunk));
34
- } else {
35
- controller.enqueue(chunk);
12
+ const BODY_END_TAG = "</body>";
13
+ const HTML_END_TAG = "</html>";
14
+ const MIN_CLOSING_TAG_LENGTH = 4;
15
+ const DEFAULT_SERIALIZATION_TIMEOUT_MS = 6e4;
16
+ const DEFAULT_LIFETIME_TIMEOUT_MS = 6e4;
17
+ const textEncoder = new TextEncoder();
18
+ function findLastClosingTagEnd(str) {
19
+ const len = str.length;
20
+ if (len < MIN_CLOSING_TAG_LENGTH) return -1;
21
+ let i = len - 1;
22
+ while (i >= MIN_CLOSING_TAG_LENGTH - 1) {
23
+ if (str.charCodeAt(i) === 62) {
24
+ let j = i - 1;
25
+ while (j >= 1) {
26
+ const code = str.charCodeAt(j);
27
+ if (code >= 97 && code <= 122 || // a-z
28
+ code >= 65 && code <= 90 || // A-Z
29
+ code >= 48 && code <= 57 || // 0-9
30
+ code === 95 || // _
31
+ code === 58 || // :
32
+ code === 46 || // .
33
+ code === 45) {
34
+ j--;
35
+ } else {
36
+ break;
37
+ }
36
38
  }
37
- },
38
- end: (chunk) => {
39
- if (res.destroyed) return;
40
- if (chunk) {
41
- res.write(chunk);
39
+ const tagNameStart = j + 1;
40
+ if (tagNameStart < i) {
41
+ const startCode = str.charCodeAt(tagNameStart);
42
+ if (startCode >= 97 && startCode <= 122 || startCode >= 65 && startCode <= 90) {
43
+ if (j >= 1 && str.charCodeAt(j) === 47 && str.charCodeAt(j - 1) === 60) {
44
+ return i + 1;
45
+ }
46
+ }
42
47
  }
43
- res.destroyed = true;
44
- controller.close();
45
- },
46
- destroy: (error) => {
47
- if (res.destroyed) return;
48
- res.destroyed = true;
49
- controller.error(error);
50
- },
51
- destroyed: false
52
- };
53
- return res;
54
- }
55
- async function readStream(stream, opts) {
56
- const reader = stream.getReader();
57
- try {
58
- let chunk;
59
- while (!(chunk = await reader.read()).done) {
60
- opts.onData?.(chunk);
61
48
  }
62
- opts.onEnd?.();
63
- } catch (error) {
64
- opts.onError?.(error);
65
- } finally {
66
- reader.releaseLock();
49
+ i--;
67
50
  }
51
+ return -1;
68
52
  }
69
53
  function transformStreamWithRouter(router, appStream, opts) {
70
- let stopListeningToInjectedHtml = void 0;
71
- let timeoutHandle;
54
+ let stopListeningToInjectedHtml;
55
+ let stopListeningToSerializationFinished;
56
+ let serializationTimeoutHandle;
57
+ let lifetimeTimeoutHandle;
72
58
  let cleanedUp = false;
59
+ let controller;
60
+ let isStreamClosed = false;
61
+ const serializationAlreadyFinished = router.serverSsr?.isSerializationFinished() ?? false;
73
62
  function cleanup() {
74
63
  if (cleanedUp) return;
75
64
  cleanedUp = true;
76
- if (stopListeningToInjectedHtml) {
77
- stopListeningToInjectedHtml();
78
- stopListeningToInjectedHtml = void 0;
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;
79
75
  }
80
- clearTimeout(timeoutHandle);
76
+ if (lifetimeTimeoutHandle !== void 0) {
77
+ clearTimeout(lifetimeTimeoutHandle);
78
+ lifetimeTimeoutHandle = void 0;
79
+ }
80
+ pendingRouterHtmlParts = [];
81
+ leftover = "";
82
+ pendingClosingTags = "";
81
83
  router.serverSsr?.cleanup();
82
84
  }
83
- const finalPassThrough = createPassthrough(cleanup);
84
85
  const textDecoder = new TextDecoder();
86
+ function safeEnqueue(chunk) {
87
+ if (isStreamClosed) return;
88
+ if (typeof chunk === "string") {
89
+ controller.enqueue(textEncoder.encode(chunk));
90
+ } else {
91
+ controller.enqueue(chunk);
92
+ }
93
+ }
94
+ function safeClose() {
95
+ if (isStreamClosed) return;
96
+ isStreamClosed = true;
97
+ try {
98
+ controller.close();
99
+ } catch {
100
+ }
101
+ }
102
+ function safeError(error) {
103
+ if (isStreamClosed) return;
104
+ isStreamClosed = true;
105
+ try {
106
+ controller.error(error);
107
+ } catch {
108
+ }
109
+ }
110
+ const stream = new ReadableStream({
111
+ start(c) {
112
+ controller = c;
113
+ },
114
+ cancel() {
115
+ isStreamClosed = true;
116
+ cleanup();
117
+ }
118
+ });
85
119
  let isAppRendering = true;
86
- let routerStreamBuffer = "";
87
- let pendingClosingTags = "";
88
120
  let streamBarrierLifted = false;
89
121
  let leftover = "";
90
- let leftoverHtml = "";
91
- function getBufferedRouterStream() {
92
- const html = routerStreamBuffer;
93
- routerStreamBuffer = "";
94
- return html;
122
+ let pendingClosingTags = "";
123
+ let serializationFinished = serializationAlreadyFinished;
124
+ let pendingRouterHtmlParts = [];
125
+ const bufferedHtml = router.serverSsr?.takeBufferedHtml();
126
+ if (bufferedHtml) {
127
+ pendingRouterHtmlParts.push(bufferedHtml);
95
128
  }
96
- function decodeChunk(chunk) {
97
- if (chunk instanceof Uint8Array) {
98
- return textDecoder.decode(chunk, { stream: true });
129
+ function flushPendingRouterHtml() {
130
+ if (pendingRouterHtmlParts.length > 0) {
131
+ safeEnqueue(pendingRouterHtmlParts.join(""));
132
+ pendingRouterHtmlParts = [];
99
133
  }
100
- return String(chunk);
101
134
  }
102
- const injectedHtmlDonePromise = createControlledPromise();
103
- let processingCount = 0;
104
- handleInjectedHtml();
105
- stopListeningToInjectedHtml = router.subscribe(
106
- "onInjectedHtml",
107
- handleInjectedHtml
108
- );
109
- function handleInjectedHtml() {
110
- if (cleanedUp) return;
111
- router.serverSsr.injectedHtml.forEach((promise) => {
112
- processingCount++;
113
- promise.then((html) => {
114
- if (cleanedUp || finalPassThrough.destroyed) {
115
- return;
116
- }
117
- if (isAppRendering) {
118
- routerStreamBuffer += html;
119
- } else {
120
- finalPassThrough.write(html);
121
- }
122
- }).catch((err) => {
123
- injectedHtmlDonePromise.reject(err);
124
- }).finally(() => {
125
- processingCount--;
126
- if (!isAppRendering && processingCount === 0) {
127
- injectedHtmlDonePromise.resolve();
128
- }
129
- });
130
- });
131
- router.serverSsr.injectedHtml = [];
132
- }
133
- injectedHtmlDonePromise.then(() => {
134
- if (cleanedUp || finalPassThrough.destroyed) {
135
- return;
135
+ function tryFinish() {
136
+ if (isAppRendering || !serializationFinished) return;
137
+ if (cleanedUp || isStreamClosed) return;
138
+ if (serializationTimeoutHandle !== void 0) {
139
+ clearTimeout(serializationTimeoutHandle);
140
+ serializationTimeoutHandle = void 0;
136
141
  }
137
- clearTimeout(timeoutHandle);
138
- const finalHtml = leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags;
139
- leftover = "";
140
- leftoverHtml = "";
141
- pendingClosingTags = "";
142
- finalPassThrough.end(finalHtml);
143
- }).catch((err) => {
144
- if (cleanedUp || finalPassThrough.destroyed) {
145
- return;
142
+ const decoderRemainder = textDecoder.decode();
143
+ if (leftover) safeEnqueue(leftover);
144
+ if (decoderRemainder) safeEnqueue(decoderRemainder);
145
+ flushPendingRouterHtml();
146
+ if (pendingClosingTags) safeEnqueue(pendingClosingTags);
147
+ safeClose();
148
+ cleanup();
149
+ }
150
+ const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS;
151
+ lifetimeTimeoutHandle = setTimeout(() => {
152
+ if (!cleanedUp && !isStreamClosed) {
153
+ console.warn(
154
+ `SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`
155
+ );
156
+ safeError(new Error("Stream lifetime exceeded"));
157
+ cleanup();
146
158
  }
147
- console.error("Error reading routerStream:", err);
148
- finalPassThrough.destroy(err);
149
- }).finally(cleanup);
150
- readStream(appStream, {
151
- onData: (chunk) => {
152
- if (cleanedUp || finalPassThrough.destroyed) {
153
- return;
154
- }
155
- const text = decodeChunk(chunk.value);
156
- const chunkString = leftover + text;
157
- const bodyEndMatch = chunkString.match(patternBodyEnd);
158
- const htmlEndMatch = chunkString.match(patternHtmlEnd);
159
- if (!streamBarrierLifted) {
160
- const streamBarrierIdIncluded = chunkString.includes(
161
- TSR_SCRIPT_BARRIER_ID
162
- );
163
- if (streamBarrierIdIncluded) {
164
- streamBarrierLifted = true;
165
- router.serverSsr.liftScriptBarrier();
166
- }
167
- }
168
- if (bodyEndMatch && htmlEndMatch && bodyEndMatch.index < htmlEndMatch.index) {
169
- const bodyEndIndex = bodyEndMatch.index;
170
- pendingClosingTags = chunkString.slice(bodyEndIndex);
171
- finalPassThrough.write(
172
- chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream() + leftoverHtml
173
- );
174
- leftover = "";
175
- leftoverHtml = "";
176
- return;
177
- }
178
- let result;
179
- let lastIndex = 0;
180
- patternClosingTag.lastIndex = 0;
181
- while ((result = patternClosingTag.exec(chunkString)) !== null) {
182
- lastIndex = result.index + result[0].length;
183
- }
184
- if (lastIndex > 0) {
185
- const processed = chunkString.slice(0, lastIndex) + getBufferedRouterStream() + leftoverHtml;
186
- finalPassThrough.write(processed);
187
- leftover = chunkString.slice(lastIndex);
188
- leftoverHtml = "";
159
+ }, lifetimeMs);
160
+ if (!serializationAlreadyFinished) {
161
+ stopListeningToInjectedHtml = router.subscribe("onInjectedHtml", () => {
162
+ if (cleanedUp || isStreamClosed) return;
163
+ const html = router.serverSsr?.takeBufferedHtml();
164
+ if (!html) return;
165
+ if (isAppRendering) {
166
+ pendingRouterHtmlParts.push(html);
189
167
  } else {
190
- leftover = chunkString;
191
- leftoverHtml += getBufferedRouterStream();
168
+ safeEnqueue(html);
192
169
  }
193
- },
194
- onEnd: () => {
195
- if (cleanedUp || finalPassThrough.destroyed) {
196
- return;
170
+ });
171
+ stopListeningToSerializationFinished = router.subscribe(
172
+ "onSerializationFinished",
173
+ () => {
174
+ serializationFinished = true;
175
+ tryFinish();
176
+ }
177
+ );
178
+ }
179
+ (async () => {
180
+ const reader = appStream.getReader();
181
+ try {
182
+ while (true) {
183
+ const { done, value } = await reader.read();
184
+ if (done) break;
185
+ if (cleanedUp || isStreamClosed) return;
186
+ const text = value instanceof Uint8Array ? textDecoder.decode(value, { stream: true }) : String(value);
187
+ const chunkString = leftover + text;
188
+ if (!streamBarrierLifted) {
189
+ if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {
190
+ streamBarrierLifted = true;
191
+ router.serverSsr?.liftScriptBarrier();
192
+ }
193
+ }
194
+ const bodyEndIndex = chunkString.indexOf(BODY_END_TAG);
195
+ const htmlEndIndex = chunkString.indexOf(HTML_END_TAG);
196
+ if (bodyEndIndex !== -1 && htmlEndIndex !== -1 && bodyEndIndex < htmlEndIndex) {
197
+ pendingClosingTags = chunkString.slice(bodyEndIndex);
198
+ safeEnqueue(chunkString.slice(0, bodyEndIndex));
199
+ flushPendingRouterHtml();
200
+ leftover = "";
201
+ continue;
202
+ }
203
+ const lastClosingTagEnd = findLastClosingTagEnd(chunkString);
204
+ if (lastClosingTagEnd > 0) {
205
+ safeEnqueue(chunkString.slice(0, lastClosingTagEnd));
206
+ flushPendingRouterHtml();
207
+ leftover = chunkString.slice(lastClosingTagEnd);
208
+ } else {
209
+ leftover = chunkString;
210
+ }
197
211
  }
212
+ if (cleanedUp || isStreamClosed) return;
198
213
  isAppRendering = false;
199
- router.serverSsr.setRenderFinished();
200
- if (processingCount === 0) {
201
- injectedHtmlDonePromise.resolve();
214
+ router.serverSsr?.setRenderFinished();
215
+ if (serializationFinished) {
216
+ tryFinish();
202
217
  } else {
203
- const timeoutMs = opts?.timeoutMs ?? 6e4;
204
- timeoutHandle = setTimeout(() => {
205
- injectedHtmlDonePromise.reject(
206
- new Error("Injected HTML timeout after app render finished")
207
- );
218
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS;
219
+ serializationTimeoutHandle = setTimeout(() => {
220
+ if (!cleanedUp && !isStreamClosed) {
221
+ console.error("Serialization timeout after app render finished");
222
+ safeError(
223
+ new Error("Serialization timeout after app render finished")
224
+ );
225
+ cleanup();
226
+ }
208
227
  }, timeoutMs);
209
228
  }
210
- },
211
- onError: (error) => {
212
- if (cleanedUp) {
213
- return;
214
- }
229
+ } catch (error) {
230
+ if (cleanedUp) return;
215
231
  console.error("Error reading appStream:", error);
216
232
  isAppRendering = false;
217
- router.serverSsr.setRenderFinished();
218
- clearTimeout(timeoutHandle);
219
- leftover = "";
220
- leftoverHtml = "";
221
- routerStreamBuffer = "";
222
- pendingClosingTags = "";
223
- finalPassThrough.destroy(error);
224
- injectedHtmlDonePromise.reject(error);
233
+ router.serverSsr?.setRenderFinished();
234
+ safeError(error);
235
+ cleanup();
236
+ } finally {
237
+ reader.releaseLock();
225
238
  }
239
+ })().catch((error) => {
240
+ if (cleanedUp) return;
241
+ console.error("Error in stream transform:", error);
242
+ safeError(error);
243
+ cleanup();
226
244
  });
227
- return finalPassThrough.stream;
245
+ return stream;
228
246
  }
229
247
  export {
230
- TSR_SCRIPT_BARRIER_ID,
231
248
  transformPipeableStreamWithRouter,
232
249
  transformReadableStreamWithRouter,
233
250
  transformStreamWithRouter
@@ -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 { createControlledPromise } from '../utils'\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\nexport const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'\n\n// regex pattern for matching closing body and html tags\nconst patternBodyEnd = /(<\\/body>)/\nconst patternHtmlEnd = /(<\\/html>)/\n// regex pattern for matching closing tags\nconst patternClosingTag = /(<\\/[a-zA-Z][\\w:.-]*?>)/g\n\ntype ReadablePassthrough = {\n stream: ReadableStream\n write: (chunk: unknown) => void\n end: (chunk?: string) => void\n destroy: (error: unknown) => void\n destroyed: boolean\n}\n\nfunction createPassthrough(onCancel: () => void) {\n let controller: ReadableStreamDefaultController<any>\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n res.destroyed = true\n onCancel()\n },\n })\n\n const res: ReadablePassthrough = {\n stream,\n write: (chunk) => {\n // Don't write to destroyed stream\n if (res.destroyed) return\n if (typeof chunk === 'string') {\n controller.enqueue(encoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n },\n end: (chunk) => {\n // Don't end already destroyed stream\n if (res.destroyed) return\n if (chunk) {\n res.write(chunk)\n }\n res.destroyed = true\n controller.close()\n },\n destroy: (error) => {\n // Don't destroy already destroyed stream\n if (res.destroyed) return\n res.destroyed = true\n controller.error(error)\n },\n destroyed: false,\n }\n\n return res\n}\n\nasync function readStream(\n stream: ReadableStream,\n opts: {\n onData?: (chunk: ReadableStreamReadValueResult<any>) => void\n onEnd?: () => void\n onError?: (error: unknown) => void\n },\n) {\n const reader = stream.getReader()\n try {\n let chunk\n while (!(chunk = await reader.read()).done) {\n opts.onData?.(chunk)\n }\n opts.onEnd?.()\n } catch (error) {\n opts.onError?.(error)\n } finally {\n reader.releaseLock()\n }\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n timeoutMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined = undefined\n let timeoutHandle: NodeJS.Timeout\n let cleanedUp = false\n\n function cleanup() {\n if (cleanedUp) return\n cleanedUp = true\n if (stopListeningToInjectedHtml) {\n stopListeningToInjectedHtml()\n stopListeningToInjectedHtml = undefined\n }\n clearTimeout(timeoutHandle)\n router.serverSsr?.cleanup()\n }\n\n const finalPassThrough = createPassthrough(cleanup)\n const textDecoder = new TextDecoder()\n\n let isAppRendering = true\n let routerStreamBuffer = ''\n let pendingClosingTags = ''\n let streamBarrierLifted = false\n let leftover = ''\n let leftoverHtml = ''\n\n function getBufferedRouterStream() {\n const html = routerStreamBuffer\n routerStreamBuffer = ''\n return html\n }\n\n function decodeChunk(chunk: unknown): string {\n if (chunk instanceof Uint8Array) {\n return textDecoder.decode(chunk, { stream: true })\n }\n return String(chunk)\n }\n\n const injectedHtmlDonePromise = createControlledPromise<void>()\n\n let processingCount = 0\n\n // Process any already-injected HTML\n handleInjectedHtml()\n\n // Listen for any new injected HTML\n stopListeningToInjectedHtml = router.subscribe(\n 'onInjectedHtml',\n handleInjectedHtml,\n )\n\n function handleInjectedHtml() {\n // Don't process if already cleaned up\n if (cleanedUp) return\n\n router.serverSsr!.injectedHtml.forEach((promise) => {\n processingCount++\n\n promise\n .then((html) => {\n // Don't write to destroyed stream or after cleanup\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n if (isAppRendering) {\n routerStreamBuffer += html\n } else {\n finalPassThrough.write(html)\n }\n })\n .catch((err) => {\n injectedHtmlDonePromise.reject(err)\n })\n .finally(() => {\n processingCount--\n\n if (!isAppRendering && processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n }\n })\n })\n router.serverSsr!.injectedHtml = []\n }\n\n injectedHtmlDonePromise\n .then(() => {\n // Don't process if already cleaned up or destroyed\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n clearTimeout(timeoutHandle)\n const finalHtml =\n leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n leftover = ''\n leftoverHtml = ''\n pendingClosingTags = ''\n\n finalPassThrough.end(finalHtml)\n })\n .catch((err) => {\n // Don't process if already cleaned up\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n console.error('Error reading routerStream:', err)\n finalPassThrough.destroy(err)\n })\n .finally(cleanup)\n\n // Transform the appStream\n readStream(appStream, {\n onData: (chunk) => {\n // Don't process if already cleaned up\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n const text = decodeChunk(chunk.value)\n const chunkString = leftover + text\n const bodyEndMatch = chunkString.match(patternBodyEnd)\n const htmlEndMatch = chunkString.match(patternHtmlEnd)\n\n if (!streamBarrierLifted) {\n const streamBarrierIdIncluded = chunkString.includes(\n TSR_SCRIPT_BARRIER_ID,\n )\n if (streamBarrierIdIncluded) {\n streamBarrierLifted = true\n router.serverSsr!.liftScriptBarrier()\n }\n }\n\n // If either the body end or html end is in the chunk,\n // We need to get all of our data in asap\n if (\n bodyEndMatch &&\n htmlEndMatch &&\n bodyEndMatch.index! < htmlEndMatch.index!\n ) {\n const bodyEndIndex = bodyEndMatch.index!\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n finalPassThrough.write(\n chunkString.slice(0, bodyEndIndex) +\n getBufferedRouterStream() +\n leftoverHtml,\n )\n\n leftover = ''\n leftoverHtml = ''\n return\n }\n\n let result: RegExpExecArray | null\n let lastIndex = 0\n // Reset regex lastIndex since it's global and stateful across exec() calls\n patternClosingTag.lastIndex = 0\n while ((result = patternClosingTag.exec(chunkString)) !== null) {\n lastIndex = result.index + result[0].length\n }\n\n if (lastIndex > 0) {\n const processed =\n chunkString.slice(0, lastIndex) +\n getBufferedRouterStream() +\n leftoverHtml\n\n finalPassThrough.write(processed)\n leftover = chunkString.slice(lastIndex)\n leftoverHtml = ''\n } else {\n leftover = chunkString\n leftoverHtml += getBufferedRouterStream()\n }\n },\n onEnd: () => {\n // Don't process if stream was already destroyed/cancelled or cleaned up\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n\n // If there are no pending promises, resolve the injectedHtmlDonePromise\n if (processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n } else {\n const timeoutMs = opts?.timeoutMs ?? 60000\n timeoutHandle = setTimeout(() => {\n injectedHtmlDonePromise.reject(\n new Error('Injected HTML timeout after app render finished'),\n )\n }, timeoutMs)\n }\n },\n onError: (error) => {\n // Don't process if already cleaned up\n if (cleanedUp) {\n return\n }\n\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n // Clear timeout to prevent it from firing after error\n clearTimeout(timeoutHandle)\n // Clear string buffers to prevent memory leaks\n leftover = ''\n leftoverHtml = ''\n routerStreamBuffer = ''\n pendingClosingTags = ''\n finalPassThrough.destroy(error)\n injectedHtmlDonePromise.reject(error)\n },\n })\n\n return finalPassThrough.stream\n}\n"],"names":[],"mappings":";;;AAKO,SAAS,kCACd,QACA,cACA;AACA,SAAO,0BAA0B,QAAQ,YAAY;AACvD;AAEO,SAAS,kCACd,QACA,cACA;AACA,SAAO,SAAS;AAAA,IACd,0BAA0B,QAAQ,SAAS,MAAM,YAAY,CAAC;AAAA,EAAA;AAElE;AAEO,MAAM,wBAAwB;AAGrC,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AAEvB,MAAM,oBAAoB;AAU1B,SAAS,kBAAkB,UAAsB;AAC/C,MAAI;AACJ,QAAM,UAAU,IAAI,YAAA;AACpB,QAAM,SAAS,IAAI,eAAe;AAAA,IAChC,MAAM,GAAG;AACP,mBAAa;AAAA,IACf;AAAA,IACA,SAAS;AACP,UAAI,YAAY;AAChB,eAAA;AAAA,IACF;AAAA,EAAA,CACD;AAED,QAAM,MAA2B;AAAA,IAC/B;AAAA,IACA,OAAO,CAAC,UAAU;AAEhB,UAAI,IAAI,UAAW;AACnB,UAAI,OAAO,UAAU,UAAU;AAC7B,mBAAW,QAAQ,QAAQ,OAAO,KAAK,CAAC;AAAA,MAC1C,OAAO;AACL,mBAAW,QAAQ,KAAK;AAAA,MAC1B;AAAA,IACF;AAAA,IACA,KAAK,CAAC,UAAU;AAEd,UAAI,IAAI,UAAW;AACnB,UAAI,OAAO;AACT,YAAI,MAAM,KAAK;AAAA,MACjB;AACA,UAAI,YAAY;AAChB,iBAAW,MAAA;AAAA,IACb;AAAA,IACA,SAAS,CAAC,UAAU;AAElB,UAAI,IAAI,UAAW;AACnB,UAAI,YAAY;AAChB,iBAAW,MAAM,KAAK;AAAA,IACxB;AAAA,IACA,WAAW;AAAA,EAAA;AAGb,SAAO;AACT;AAEA,eAAe,WACb,QACA,MAKA;AACA,QAAM,SAAS,OAAO,UAAA;AACtB,MAAI;AACF,QAAI;AACJ,WAAO,EAAE,QAAQ,MAAM,OAAO,KAAA,GAAQ,MAAM;AAC1C,WAAK,SAAS,KAAK;AAAA,IACrB;AACA,SAAK,QAAA;AAAA,EACP,SAAS,OAAO;AACd,SAAK,UAAU,KAAK;AAAA,EACtB,UAAA;AACE,WAAO,YAAA;AAAA,EACT;AACF;AAEO,SAAS,0BACd,QACA,WACA,MAGA;AACA,MAAI,8BAAwD;AAC5D,MAAI;AACJ,MAAI,YAAY;AAEhB,WAAS,UAAU;AACjB,QAAI,UAAW;AACf,gBAAY;AACZ,QAAI,6BAA6B;AAC/B,kCAAA;AACA,oCAA8B;AAAA,IAChC;AACA,iBAAa,aAAa;AAC1B,WAAO,WAAW,QAAA;AAAA,EACpB;AAEA,QAAM,mBAAmB,kBAAkB,OAAO;AAClD,QAAM,cAAc,IAAI,YAAA;AAExB,MAAI,iBAAiB;AACrB,MAAI,qBAAqB;AACzB,MAAI,qBAAqB;AACzB,MAAI,sBAAsB;AAC1B,MAAI,WAAW;AACf,MAAI,eAAe;AAEnB,WAAS,0BAA0B;AACjC,UAAM,OAAO;AACb,yBAAqB;AACrB,WAAO;AAAA,EACT;AAEA,WAAS,YAAY,OAAwB;AAC3C,QAAI,iBAAiB,YAAY;AAC/B,aAAO,YAAY,OAAO,OAAO,EAAE,QAAQ,MAAM;AAAA,IACnD;AACA,WAAO,OAAO,KAAK;AAAA,EACrB;AAEA,QAAM,0BAA0B,wBAAA;AAEhC,MAAI,kBAAkB;AAGtB,qBAAA;AAGA,gCAA8B,OAAO;AAAA,IACnC;AAAA,IACA;AAAA,EAAA;AAGF,WAAS,qBAAqB;AAE5B,QAAI,UAAW;AAEf,WAAO,UAAW,aAAa,QAAQ,CAAC,YAAY;AAClD;AAEA,cACG,KAAK,CAAC,SAAS;AAEd,YAAI,aAAa,iBAAiB,WAAW;AAC3C;AAAA,QACF;AACA,YAAI,gBAAgB;AAClB,gCAAsB;AAAA,QACxB,OAAO;AACL,2BAAiB,MAAM,IAAI;AAAA,QAC7B;AAAA,MACF,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,gCAAwB,OAAO,GAAG;AAAA,MACpC,CAAC,EACA,QAAQ,MAAM;AACb;AAEA,YAAI,CAAC,kBAAkB,oBAAoB,GAAG;AAC5C,kCAAwB,QAAA;AAAA,QAC1B;AAAA,MACF,CAAC;AAAA,IACL,CAAC;AACD,WAAO,UAAW,eAAe,CAAA;AAAA,EACnC;AAEA,0BACG,KAAK,MAAM;AAEV,QAAI,aAAa,iBAAiB,WAAW;AAC3C;AAAA,IACF;AAEA,iBAAa,aAAa;AAC1B,UAAM,YACJ,WAAW,eAAe,wBAAA,IAA4B;AAExD,eAAW;AACX,mBAAe;AACf,yBAAqB;AAErB,qBAAiB,IAAI,SAAS;AAAA,EAChC,CAAC,EACA,MAAM,CAAC,QAAQ;AAEd,QAAI,aAAa,iBAAiB,WAAW;AAC3C;AAAA,IACF;AAEA,YAAQ,MAAM,+BAA+B,GAAG;AAChD,qBAAiB,QAAQ,GAAG;AAAA,EAC9B,CAAC,EACA,QAAQ,OAAO;AAGlB,aAAW,WAAW;AAAA,IACpB,QAAQ,CAAC,UAAU;AAEjB,UAAI,aAAa,iBAAiB,WAAW;AAC3C;AAAA,MACF;AAEA,YAAM,OAAO,YAAY,MAAM,KAAK;AACpC,YAAM,cAAc,WAAW;AAC/B,YAAM,eAAe,YAAY,MAAM,cAAc;AACrD,YAAM,eAAe,YAAY,MAAM,cAAc;AAErD,UAAI,CAAC,qBAAqB;AACxB,cAAM,0BAA0B,YAAY;AAAA,UAC1C;AAAA,QAAA;AAEF,YAAI,yBAAyB;AAC3B,gCAAsB;AACtB,iBAAO,UAAW,kBAAA;AAAA,QACpB;AAAA,MACF;AAIA,UACE,gBACA,gBACA,aAAa,QAAS,aAAa,OACnC;AACA,cAAM,eAAe,aAAa;AAClC,6BAAqB,YAAY,MAAM,YAAY;AAEnD,yBAAiB;AAAA,UACf,YAAY,MAAM,GAAG,YAAY,IAC/B,4BACA;AAAA,QAAA;AAGJ,mBAAW;AACX,uBAAe;AACf;AAAA,MACF;AAEA,UAAI;AACJ,UAAI,YAAY;AAEhB,wBAAkB,YAAY;AAC9B,cAAQ,SAAS,kBAAkB,KAAK,WAAW,OAAO,MAAM;AAC9D,oBAAY,OAAO,QAAQ,OAAO,CAAC,EAAE;AAAA,MACvC;AAEA,UAAI,YAAY,GAAG;AACjB,cAAM,YACJ,YAAY,MAAM,GAAG,SAAS,IAC9B,4BACA;AAEF,yBAAiB,MAAM,SAAS;AAChC,mBAAW,YAAY,MAAM,SAAS;AACtC,uBAAe;AAAA,MACjB,OAAO;AACL,mBAAW;AACX,wBAAgB,wBAAA;AAAA,MAClB;AAAA,IACF;AAAA,IACA,OAAO,MAAM;AAEX,UAAI,aAAa,iBAAiB,WAAW;AAC3C;AAAA,MACF;AAGA,uBAAiB;AACjB,aAAO,UAAW,kBAAA;AAGlB,UAAI,oBAAoB,GAAG;AACzB,gCAAwB,QAAA;AAAA,MAC1B,OAAO;AACL,cAAM,YAAY,MAAM,aAAa;AACrC,wBAAgB,WAAW,MAAM;AAC/B,kCAAwB;AAAA,YACtB,IAAI,MAAM,iDAAiD;AAAA,UAAA;AAAA,QAE/D,GAAG,SAAS;AAAA,MACd;AAAA,IACF;AAAA,IACA,SAAS,CAAC,UAAU;AAElB,UAAI,WAAW;AACb;AAAA,MACF;AAEA,cAAQ,MAAM,4BAA4B,KAAK;AAC/C,uBAAiB;AACjB,aAAO,UAAW,kBAAA;AAElB,mBAAa,aAAa;AAE1B,iBAAW;AACX,qBAAe;AACf,2BAAqB;AACrB,2BAAqB;AACrB,uBAAiB,QAAQ,KAAK;AAC9B,8BAAwB,OAAO,KAAK;AAAA,IACtC;AAAA,EAAA,CACD;AAED,SAAO,iBAAiB;AAC1B;"}
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":";;;AAKO,SAAS,kCACd,QACA,cACA;AACA,SAAO,0BAA0B,QAAQ,YAAY;AACvD;AAEO,SAAS,kCACd,QACA,cACA;AACA,SAAO,SAAS;AAAA,IACd,0BAA0B,QAAQ,SAAS,MAAM,YAAY,CAAC;AAAA,EAAA;AAElE;AAGA,MAAM,eAAe;AACrB,MAAM,eAAe;AAGrB,MAAM,yBAAyB;AAG/B,MAAM,mCAAmC;AACzC,MAAM,8BAA8B;AAGpC,MAAM,cAAc,IAAI,YAAA;AAUxB,SAAS,sBAAsB,KAAqB;AAClD,QAAM,MAAM,IAAI;AAChB,MAAI,MAAM,uBAAwB,QAAO;AAEzC,MAAI,IAAI,MAAM;AAEd,SAAO,KAAK,yBAAyB,GAAG;AAEtC,QAAI,IAAI,WAAW,CAAC,MAAM,IAAI;AAE5B,UAAI,IAAI,IAAI;AAGZ,aAAO,KAAK,GAAG;AACb,cAAM,OAAO,IAAI,WAAW,CAAC;AAE7B,YACG,QAAQ,MAAM,QAAQ;AAAA,QACtB,QAAQ,MAAM,QAAQ;AAAA,QACtB,QAAQ,MAAM,QAAQ;AAAA,QACvB,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS,IACT;AACA;AAAA,QACF,OAAO;AACL;AAAA,QACF;AAAA,MACF;AAGA,YAAM,eAAe,IAAI;AACzB,UAAI,eAAe,GAAG;AACpB,cAAM,YAAY,IAAI,WAAW,YAAY;AAE7C,YACG,aAAa,MAAM,aAAa,OAChC,aAAa,MAAM,aAAa,IACjC;AAEA,cACE,KAAK,KACL,IAAI,WAAW,CAAC,MAAM,MACtB,IAAI,WAAW,IAAI,CAAC,MAAM,IAC1B;AACA,mBAAO,IAAI;AAAA,UACb;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,0BACd,QACA,WACA,MAMA;AACA,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI,YAAY;AAEhB,MAAI;AACJ,MAAI,iBAAiB;AAIrB,QAAM,+BACJ,OAAO,WAAW,wBAAA,KAA6B;AAMjD,WAAS,UAAU;AAEjB,QAAI,UAAW;AACf,gBAAY;AAGZ,QAAI;AACF,oCAAA;AACA,6CAAA;AAAA,IACF,SAAS,GAAG;AAAA,IAEZ;AACA,kCAA8B;AAC9B,2CAAuC;AAGvC,QAAI,+BAA+B,QAAW;AAC5C,mBAAa,0BAA0B;AACvC,mCAA6B;AAAA,IAC/B;AACA,QAAI,0BAA0B,QAAW;AACvC,mBAAa,qBAAqB;AAClC,8BAAwB;AAAA,IAC1B;AAGA,6BAAyB,CAAA;AACzB,eAAW;AACX,yBAAqB;AAGrB,WAAO,WAAW,QAAA;AAAA,EACpB;AAEA,QAAM,cAAc,IAAI,YAAA;AAExB,WAAS,YAAY,OAA4B;AAC/C,QAAI,eAAgB;AACpB,QAAI,OAAO,UAAU,UAAU;AAC7B,iBAAW,QAAQ,YAAY,OAAO,KAAK,CAAC;AAAA,IAC9C,OAAO;AACL,iBAAW,QAAQ,KAAK;AAAA,IAC1B;AAAA,EACF;AAEA,WAAS,YAAY;AACnB,QAAI,eAAgB;AACpB,qBAAiB;AACjB,QAAI;AACF,iBAAW,MAAA;AAAA,IACb,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,WAAS,UAAU,OAAgB;AACjC,QAAI,eAAgB;AACpB,qBAAiB;AACjB,QAAI;AACF,iBAAW,MAAM,KAAK;AAAA,IACxB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,eAAe;AAAA,IAChC,MAAM,GAAG;AACP,mBAAa;AAAA,IACf;AAAA,IACA,SAAS;AACP,uBAAiB;AACjB,cAAA;AAAA,IACF;AAAA,EAAA,CACD;AAED,MAAI,iBAAiB;AACrB,MAAI,sBAAsB;AAC1B,MAAI,WAAW;AACf,MAAI,qBAAqB;AACzB,MAAI,wBAAwB;AAE5B,MAAI,yBAAwC,CAAA;AAG5C,QAAM,eAAe,OAAO,WAAW,iBAAA;AACvC,MAAI,cAAc;AAChB,2BAAuB,KAAK,YAAY;AAAA,EAC1C;AAEA,WAAS,yBAAyB;AAChC,QAAI,uBAAuB,SAAS,GAAG;AACrC,kBAAY,uBAAuB,KAAK,EAAE,CAAC;AAC3C,+BAAyB,CAAA;AAAA,IAC3B;AAAA,EACF;AAKA,WAAS,YAAY;AAEnB,QAAI,kBAAkB,CAAC,sBAAuB;AAC9C,QAAI,aAAa,eAAgB;AAGjC,QAAI,+BAA+B,QAAW;AAC5C,mBAAa,0BAA0B;AACvC,mCAA6B;AAAA,IAC/B;AAGA,UAAM,mBAAmB,YAAY,OAAA;AAErC,QAAI,sBAAsB,QAAQ;AAClC,QAAI,8BAA8B,gBAAgB;AAClD,2BAAA;AACA,QAAI,gCAAgC,kBAAkB;AAEtD,cAAA;AACA,YAAA;AAAA,EACF;AAIA,QAAM,aAAa,MAAM,cAAc;AACvC,0BAAwB,WAAW,MAAM;AACvC,QAAI,CAAC,aAAa,CAAC,gBAAgB;AACjC,cAAQ;AAAA,QACN,mDAAmD,UAAU;AAAA,MAAA;AAE/D,gBAAU,IAAI,MAAM,0BAA0B,CAAC;AAC/C,cAAA;AAAA,IACF;AAAA,EACF,GAAG,UAAU;AAIb,MAAI,CAAC,8BAA8B;AAEjC,kCAA8B,OAAO,UAAU,kBAAkB,MAAM;AACrE,UAAI,aAAa,eAAgB;AAGjC,YAAM,OAAO,OAAO,WAAW,iBAAA;AAC/B,UAAI,CAAC,KAAM;AAEX,UAAI,gBAAgB;AAElB,+BAAuB,KAAK,IAAI;AAAA,MAClC,OAAO;AAEL,oBAAY,IAAI;AAAA,MAClB;AAAA,IACF,CAAC;AAGD,2CAAuC,OAAO;AAAA,MAC5C;AAAA,MACA,MAAM;AACJ,gCAAwB;AACxB,kBAAA;AAAA,MACF;AAAA,IAAA;AAAA,EAEJ;AAGC,GAAC,YAAY;AACZ,UAAM,SAAS,UAAU,UAAA;AACzB,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,YAAI,KAAM;AAGV,YAAI,aAAa,eAAgB;AAEjC,cAAM,OACJ,iBAAiB,aACb,YAAY,OAAO,OAAO,EAAE,QAAQ,KAAA,CAAM,IAC1C,OAAO,KAAK;AAClB,cAAM,cAAc,WAAW;AAG/B,YAAI,CAAC,qBAAqB;AACxB,cAAI,YAAY,SAAS,qBAAqB,GAAG;AAC/C,kCAAsB;AACtB,mBAAO,WAAW,kBAAA;AAAA,UACpB;AAAA,QACF;AAGA,cAAM,eAAe,YAAY,QAAQ,YAAY;AACrD,cAAM,eAAe,YAAY,QAAQ,YAAY;AAIrD,YACE,iBAAiB,MACjB,iBAAiB,MACjB,eAAe,cACf;AACA,+BAAqB,YAAY,MAAM,YAAY;AAEnD,sBAAY,YAAY,MAAM,GAAG,YAAY,CAAC;AAC9C,iCAAA;AAEA,qBAAW;AACX;AAAA,QACF;AAmBA,cAAM,oBAAoB,sBAAsB,WAAW;AAE3D,YAAI,oBAAoB,GAAG;AAEzB,sBAAY,YAAY,MAAM,GAAG,iBAAiB,CAAC;AACnD,iCAAA;AAEA,qBAAW,YAAY,MAAM,iBAAiB;AAAA,QAChD,OAAO;AAEL,qBAAW;AAAA,QAEb;AAAA,MACF;AAGA,UAAI,aAAa,eAAgB;AAGjC,uBAAiB;AACjB,aAAO,WAAW,kBAAA;AAGlB,UAAI,uBAAuB;AACzB,kBAAA;AAAA,MACF,OAAO;AAEL,cAAM,YAAY,MAAM,aAAa;AACrC,qCAA6B,WAAW,MAAM;AAC5C,cAAI,CAAC,aAAa,CAAC,gBAAgB;AACjC,oBAAQ,MAAM,iDAAiD;AAC/D;AAAA,cACE,IAAI,MAAM,iDAAiD;AAAA,YAAA;AAE7D,oBAAA;AAAA,UACF;AAAA,QACF,GAAG,SAAS;AAAA,MACd;AAAA,IACF,SAAS,OAAO;AACd,UAAI,UAAW;AACf,cAAQ,MAAM,4BAA4B,KAAK;AAC/C,uBAAiB;AACjB,aAAO,WAAW,kBAAA;AAClB,gBAAU,KAAK;AACf,cAAA;AAAA,IACF,UAAA;AACE,aAAO,YAAA;AAAA,IACT;AAAA,EACF,GAAA,EAAK,MAAM,CAAC,UAAU;AAEpB,QAAI,UAAW;AACf,YAAQ,MAAM,8BAA8B,KAAK;AACjD,cAAU,KAAK;AACf,YAAA;AAAA,EACF,CAAC;AAED,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.142.6",
3
+ "version": "1.142.8",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -895,22 +895,8 @@ function getNodeMatch<T extends RouteLike>(
895
895
 
896
896
  const isBeyondPath = index === partsLength
897
897
  if (isBeyondPath) {
898
- if (node.route && (!pathIsIndex || node.kind === SEGMENT_TYPE_INDEX)) {
899
- if (isFrameMoreSpecific(bestMatch, frame)) {
900
- bestMatch = frame
901
- }
902
-
903
- // perfect match, no need to continue
904
- // this is an optimization, algorithm should work correctly without this block
905
- if (
906
- statics === partsLength &&
907
- !dynamics &&
908
- !optionals &&
909
- !skipped &&
910
- node.kind === SEGMENT_TYPE_INDEX
911
- ) {
912
- return bestMatch
913
- }
898
+ if (node.route && !pathIsIndex && isFrameMoreSpecific(bestMatch, frame)) {
899
+ bestMatch = frame
914
900
  }
915
901
  // beyond the length of the path parts, only index segments, or skipped optional segments, or wildcard segments can match
916
902
  if (!node.optional && !node.wildcard && !node.index) continue
@@ -919,6 +905,28 @@ function getNodeMatch<T extends RouteLike>(
919
905
  const part = isBeyondPath ? undefined : parts[index]!
920
906
  let lowerPart: string
921
907
 
908
+ // 0. Try index match
909
+ if (isBeyondPath && node.index) {
910
+ const indexFrame = {
911
+ node: node.index,
912
+ index,
913
+ skipped,
914
+ depth: depth + 1,
915
+ statics,
916
+ dynamics,
917
+ optionals,
918
+ }
919
+ // perfect match, no need to continue
920
+ // this is an optimization, algorithm should work correctly without this block
921
+ if (statics === partsLength && !dynamics && !optionals && !skipped) {
922
+ return indexFrame
923
+ }
924
+ if (isFrameMoreSpecific(bestMatch, indexFrame)) {
925
+ // index matches skip the stack because they cannot have children
926
+ bestMatch = indexFrame
927
+ }
928
+ }
929
+
922
930
  // 5. Try wildcard match
923
931
  if (node.wildcard && isFrameMoreSpecific(wildcardMatch, frame)) {
924
932
  for (const segment of node.wildcard) {
@@ -937,9 +945,10 @@ function getNodeMatch<T extends RouteLike>(
937
945
  if (casePart !== suffix) continue
938
946
  }
939
947
  // the first wildcard match is the highest priority one
948
+ // wildcard matches skip the stack because they cannot have children
940
949
  wildcardMatch = {
941
950
  node: segment,
942
- index,
951
+ index: partsLength,
943
952
  skipped,
944
953
  depth,
945
954
  statics,
@@ -1048,19 +1057,6 @@ function getNodeMatch<T extends RouteLike>(
1048
1057
  })
1049
1058
  }
1050
1059
  }
1051
-
1052
- // 0. Try index match
1053
- if (isBeyondPath && node.index) {
1054
- stack.push({
1055
- node: node.index,
1056
- index,
1057
- skipped,
1058
- depth: depth + 1,
1059
- statics,
1060
- dynamics,
1061
- optionals,
1062
- })
1063
- }
1064
1060
  }
1065
1061
 
1066
1062
  if (bestMatch && wildcardMatch) {
package/src/router.ts CHANGED
@@ -747,16 +747,27 @@ export type ClearCacheFn<TRouter extends AnyRouter> = (opts?: {
747
747
  }) => void
748
748
 
749
749
  export interface ServerSsr {
750
- injectedHtml: Array<InjectedHtmlEntry>
751
- injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
752
- injectScript: (
753
- getScript: () => string | Promise<string>,
754
- opts?: { logScript?: boolean },
755
- ) => Promise<void>
750
+ /**
751
+ * Injects HTML synchronously into the stream.
752
+ * Emits an onInjectedHtml event that listeners can handle.
753
+ * If no subscriber is listening, the HTML is buffered and can be retrieved via takeBufferedHtml().
754
+ */
755
+ injectHtml: (html: string) => void
756
+ /**
757
+ * Injects a script tag synchronously into the stream.
758
+ */
759
+ injectScript: (script: string) => void
756
760
  isDehydrated: () => boolean
761
+ isSerializationFinished: () => boolean
757
762
  onRenderFinished: (listener: () => void) => void
763
+ onSerializationFinished: (listener: () => void) => void
758
764
  dehydrate: () => Promise<void>
759
765
  takeBufferedScripts: () => RouterManagedTag | undefined
766
+ /**
767
+ * Takes any buffered HTML that was injected.
768
+ * Returns the buffered HTML string (which may include multiple script tags) or undefined if empty.
769
+ */
770
+ takeBufferedHtml: () => string | undefined
760
771
  liftScriptBarrier: () => void
761
772
  }
762
773
 
@@ -1,2 +1,3 @@
1
1
  export const GLOBAL_TSR = '$_TSR'
2
2
  export declare const GLOBAL_SEROVAL: '$R'
3
+ export const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'