@tanstack/router-core 1.171.6 → 1.171.7

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.
Files changed (95) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/config.cjs.map +1 -1
  3. package/dist/cjs/defer.cjs.map +1 -1
  4. package/dist/cjs/invariant.cjs.map +1 -1
  5. package/dist/cjs/load-matches.cjs.map +1 -1
  6. package/dist/cjs/lru-cache.cjs.map +1 -1
  7. package/dist/cjs/manifest.cjs.map +1 -1
  8. package/dist/cjs/new-process-route-tree.cjs.map +1 -1
  9. package/dist/cjs/not-found.cjs.map +1 -1
  10. package/dist/cjs/path.cjs.map +1 -1
  11. package/dist/cjs/qss.cjs.map +1 -1
  12. package/dist/cjs/redirect.cjs.map +1 -1
  13. package/dist/cjs/rewrite.cjs.map +1 -1
  14. package/dist/cjs/route.cjs.map +1 -1
  15. package/dist/cjs/router.cjs.map +1 -1
  16. package/dist/cjs/router.d.cts +29 -14
  17. package/dist/cjs/scroll-restoration-script/client.cjs.map +1 -1
  18. package/dist/cjs/scroll-restoration-script/server.cjs.map +1 -1
  19. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  20. package/dist/cjs/searchMiddleware.cjs.map +1 -1
  21. package/dist/cjs/searchParams.cjs.map +1 -1
  22. package/dist/cjs/ssr/createRequestHandler.cjs +8 -7
  23. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  24. package/dist/cjs/ssr/handlerCallback.cjs +46 -0
  25. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
  26. package/dist/cjs/ssr/handlerCallback.d.cts +15 -1
  27. package/dist/cjs/ssr/headers.cjs.map +1 -1
  28. package/dist/cjs/ssr/json.cjs.map +1 -1
  29. package/dist/cjs/ssr/serializer/RawStream.cjs.map +1 -1
  30. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -1
  31. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -1
  32. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -1
  33. package/dist/cjs/ssr/server.cjs +6 -1
  34. package/dist/cjs/ssr/server.d.cts +3 -2
  35. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  36. package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
  37. package/dist/cjs/ssr/ssr-server.cjs +131 -49
  38. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  39. package/dist/cjs/ssr/ssr-server.d.cts +0 -14
  40. package/dist/cjs/ssr/transformStreamWithRouter.cjs +455 -203
  41. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  42. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +14 -5
  43. package/dist/cjs/stores.cjs.map +1 -1
  44. package/dist/cjs/utils.cjs.map +1 -1
  45. package/dist/esm/Matches.js.map +1 -1
  46. package/dist/esm/config.js.map +1 -1
  47. package/dist/esm/defer.js.map +1 -1
  48. package/dist/esm/invariant.js.map +1 -1
  49. package/dist/esm/load-matches.js.map +1 -1
  50. package/dist/esm/lru-cache.js.map +1 -1
  51. package/dist/esm/manifest.js.map +1 -1
  52. package/dist/esm/new-process-route-tree.js.map +1 -1
  53. package/dist/esm/not-found.js.map +1 -1
  54. package/dist/esm/path.js.map +1 -1
  55. package/dist/esm/qss.js.map +1 -1
  56. package/dist/esm/redirect.js.map +1 -1
  57. package/dist/esm/rewrite.js.map +1 -1
  58. package/dist/esm/route.js.map +1 -1
  59. package/dist/esm/router.d.ts +29 -14
  60. package/dist/esm/router.js.map +1 -1
  61. package/dist/esm/scroll-restoration-script/client.js.map +1 -1
  62. package/dist/esm/scroll-restoration-script/server.js.map +1 -1
  63. package/dist/esm/scroll-restoration.js.map +1 -1
  64. package/dist/esm/searchMiddleware.js.map +1 -1
  65. package/dist/esm/searchParams.js.map +1 -1
  66. package/dist/esm/ssr/createRequestHandler.js +8 -7
  67. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  68. package/dist/esm/ssr/handlerCallback.d.ts +15 -1
  69. package/dist/esm/ssr/handlerCallback.js +42 -1
  70. package/dist/esm/ssr/handlerCallback.js.map +1 -1
  71. package/dist/esm/ssr/headers.js.map +1 -1
  72. package/dist/esm/ssr/json.js.map +1 -1
  73. package/dist/esm/ssr/serializer/RawStream.js.map +1 -1
  74. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -1
  75. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -1
  76. package/dist/esm/ssr/serializer/transformer.js.map +1 -1
  77. package/dist/esm/ssr/server.d.ts +3 -2
  78. package/dist/esm/ssr/server.js +2 -2
  79. package/dist/esm/ssr/ssr-client.js.map +1 -1
  80. package/dist/esm/ssr/ssr-match-id.js.map +1 -1
  81. package/dist/esm/ssr/ssr-server.d.ts +0 -14
  82. package/dist/esm/ssr/ssr-server.js +131 -49
  83. package/dist/esm/ssr/ssr-server.js.map +1 -1
  84. package/dist/esm/ssr/transformStreamWithRouter.d.ts +14 -5
  85. package/dist/esm/ssr/transformStreamWithRouter.js +455 -203
  86. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  87. package/dist/esm/stores.js.map +1 -1
  88. package/dist/esm/utils.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/router.ts +32 -16
  91. package/src/ssr/createRequestHandler.ts +8 -8
  92. package/src/ssr/handlerCallback.ts +84 -1
  93. package/src/ssr/server.ts +14 -2
  94. package/src/ssr/ssr-server.ts +179 -81
  95. package/src/ssr/transformStreamWithRouter.ts +662 -281
@@ -2,167 +2,309 @@ import "./constants.js";
2
2
  import { ReadableStream } from "node:stream/web";
3
3
  import { Readable } from "node:stream";
4
4
  //#region src/ssr/transformStreamWithRouter.ts
5
- function transformReadableStreamWithRouter(router, routerStream) {
6
- return transformStreamWithRouter(router, routerStream);
5
+ function transformReadableStreamWithRouter(router, routerStream, opts) {
6
+ return transformStreamWithRouter(router, routerStream, opts);
7
7
  }
8
- function transformPipeableStreamWithRouter(router, routerStream) {
9
- return Readable.fromWeb(transformStreamWithRouter(router, Readable.toWeb(routerStream)));
8
+ function transformPipeableStreamWithRouter(router, routerStream, opts) {
9
+ return Readable.fromWeb(transformStreamWithRouter(router, Readable.toWeb(routerStream), opts));
10
10
  }
11
- const BODY_END_TAG = "</body>";
12
- const HTML_END_TAG = "</html>";
13
11
  const MIN_CLOSING_TAG_LENGTH = 4;
14
12
  const DEFAULT_SERIALIZATION_TIMEOUT_MS = 6e4;
15
- const DEFAULT_LIFETIME_TIMEOUT_MS = 6e4;
13
+ const DEFAULT_LIFETIME_TIMEOUT_MS = DEFAULT_SERIALIZATION_TIMEOUT_MS * 2;
14
+ const MAX_LEFTOVER_CHARS = 2048;
15
+ const MAX_TAIL_CHARS = 64 * 1024;
16
+ const MAX_ROUTER_HTML_CHARS = 16 * 1024 * 1024;
17
+ const MAX_PENDING_WRITE_CHARS = 16 * 1024 * 1024;
18
+ const MergeState = {
19
+ ReadingBody: 0,
20
+ HoldingTail: 1,
21
+ AppDone: 2,
22
+ Draining: 3,
23
+ Done: 4
24
+ };
16
25
  const textEncoder = new TextEncoder();
17
- /**
18
- * Finds the position just after the last valid HTML closing tag in the string.
19
- *
20
- * Valid closing tags match the pattern: </[a-zA-Z][\w:.-]*>
21
- * Examples: </div>, </my-component>, </slot:name.nested>
22
- *
23
- * @returns Position after the last closing tag, or -1 if none found
24
- */
25
- function findLastClosingTagEnd(str) {
26
- const len = str.length;
27
- if (len < MIN_CLOSING_TAG_LENGTH) return -1;
28
- let i = len - 1;
29
- while (i >= MIN_CLOSING_TAG_LENGTH - 1) {
30
- if (str.charCodeAt(i) === 62) {
31
- let j = i - 1;
32
- while (j >= 1) {
33
- const code = str.charCodeAt(j);
34
- if (code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95 || code === 58 || code === 46 || code === 45) j--;
35
- else break;
36
- }
37
- const tagNameStart = j + 1;
38
- if (tagNameStart < i) {
39
- const startCode = str.charCodeAt(tagNameStart);
40
- if (startCode >= 97 && startCode <= 122 || startCode >= 65 && startCode <= 90) {
41
- if (j >= 1 && str.charCodeAt(j) === 47 && str.charCodeAt(j - 1) === 60) return i + 1;
26
+ const noop = () => {};
27
+ const resolvedPromise = Promise.resolve();
28
+ function findHtmlBoundary(str) {
29
+ let lastClosingTagEnd = -1;
30
+ let searchFrom = str.length - MIN_CLOSING_TAG_LENGTH;
31
+ while (searchFrom >= 0) {
32
+ const openSlash = str.lastIndexOf("</", searchFrom);
33
+ if (openSlash === -1) break;
34
+ if ((str.charCodeAt(openSlash + 2) | 32) === 98 && (str.charCodeAt(openSlash + 3) | 32) === 111 && (str.charCodeAt(openSlash + 4) | 32) === 100 && (str.charCodeAt(openSlash + 5) | 32) === 121 && str.charCodeAt(openSlash + 6) === 62) return -openSlash - 2;
35
+ if (lastClosingTagEnd === -1) {
36
+ let i = openSlash + 2;
37
+ const startCode = str.charCodeAt(i);
38
+ if (startCode >= 97 && startCode <= 122 || startCode >= 65 && startCode <= 90) {
39
+ i++;
40
+ while (i < str.length) {
41
+ const code = str.charCodeAt(i);
42
+ if (code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95 || code === 58 || code === 46 || code === 45) i++;
43
+ else break;
42
44
  }
45
+ if (str.charCodeAt(i) === 62) lastClosingTagEnd = i + 1;
43
46
  }
44
47
  }
45
- i--;
48
+ searchFrom = openSlash - 1;
49
+ }
50
+ return lastClosingTagEnd;
51
+ }
52
+ function safeReleaseReader(reader) {
53
+ try {
54
+ reader.releaseLock();
55
+ return true;
56
+ } catch {
57
+ return false;
46
58
  }
47
- return -1;
59
+ }
60
+ /**
61
+ * Cancel a reader without producing an unhandled rejection. `reader.cancel()`
62
+ * can reject (e.g. when the underlying source's cancel() throws), and
63
+ * downstream cancel() should still wait for upstream teardown when possible.
64
+ */
65
+ function safeCancelReader(reader, reason) {
66
+ let cancelPromise;
67
+ try {
68
+ cancelPromise = reader.cancel(reason);
69
+ } catch {}
70
+ if (!safeReleaseReader(reader) && cancelPromise) return cancelPromise.then(noop, noop).then(() => {
71
+ safeReleaseReader(reader);
72
+ });
73
+ return cancelPromise ? cancelPromise.then(noop, noop) : resolvedPromise;
74
+ }
75
+ function createReaderState(appStream) {
76
+ const reader = appStream.getReader();
77
+ let released = false;
78
+ return {
79
+ reader,
80
+ cancel: (reason) => {
81
+ if (released) return resolvedPromise;
82
+ released = true;
83
+ return safeCancelReader(reader, reason);
84
+ },
85
+ release: () => {
86
+ if (released) return;
87
+ released = true;
88
+ safeReleaseReader(reader);
89
+ }
90
+ };
91
+ }
92
+ function createAbortNotifier(opts) {
93
+ let abortNotified = false;
94
+ return (reason) => {
95
+ if (abortNotified) return;
96
+ abortNotified = true;
97
+ try {
98
+ opts?.onAbort?.(reason);
99
+ } catch {}
100
+ };
48
101
  }
49
102
  function transformStreamWithRouter(router, appStream, opts) {
50
- const serializationAlreadyFinished = router.serverSsr?.isSerializationFinished() ?? false;
51
- const initialBufferedHtml = router.serverSsr?.takeBufferedHtml();
52
- if (serializationAlreadyFinished && !initialBufferedHtml) {
53
- let cleanedUp = false;
54
- let controller;
55
- let isStreamClosed = false;
56
- let lifetimeTimeoutHandle;
57
- const cleanup = () => {
58
- if (cleanedUp) return;
59
- cleanedUp = true;
60
- if (lifetimeTimeoutHandle !== void 0) {
61
- clearTimeout(lifetimeTimeoutHandle);
62
- lifetimeTimeoutHandle = void 0;
63
- }
64
- router.serverSsr?.cleanup();
65
- };
66
- const safeClose = () => {
67
- if (isStreamClosed) return;
68
- isStreamClosed = true;
69
- try {
70
- controller?.close();
71
- } catch {}
72
- };
73
- const safeError = (error) => {
74
- if (isStreamClosed) return;
75
- isStreamClosed = true;
76
- try {
77
- controller?.error(error);
78
- } catch {}
79
- };
80
- const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS;
81
- lifetimeTimeoutHandle = setTimeout(() => {
82
- if (!cleanedUp && !isStreamClosed) {
83
- console.warn(`SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`);
84
- safeError(/* @__PURE__ */ new Error("Stream lifetime exceeded"));
85
- cleanup();
86
- }
87
- }, lifetimeMs);
88
- const stream = new ReadableStream({
89
- start(c) {
90
- controller = c;
91
- },
92
- cancel() {
93
- isStreamClosed = true;
94
- cleanup();
95
- }
96
- });
97
- (async () => {
98
- const reader = appStream.getReader();
103
+ const serverSsr = router.serverSsr;
104
+ if (!serverSsr) throw new Error("Invariant failed: router.serverSsr is required");
105
+ if (serverSsr.reserveStreamFastPath()) return makeFastPathStream(appStream, opts, serverSsr);
106
+ return makeMainStream(serverSsr, appStream, opts);
107
+ }
108
+ function makeFastPathStream(appStream, opts, serverSsr) {
109
+ let cleanedUp = false;
110
+ let controller;
111
+ let state = MergeState.ReadingBody;
112
+ let lifetimeTimeoutHandle;
113
+ let stopListeningToInjectedHtml;
114
+ const readerState = createReaderState(appStream);
115
+ const notifyAbort = createAbortNotifier(opts);
116
+ const isDone = () => state === MergeState.Done;
117
+ let renderFinished = false;
118
+ const finishSsrRendering = () => {
119
+ if (!serverSsr || renderFinished) return true;
120
+ renderFinished = true;
121
+ try {
122
+ serverSsr.setRenderFinished();
123
+ return true;
124
+ } catch (error) {
125
+ safeError(error);
126
+ cleanup(error);
127
+ return false;
128
+ }
129
+ };
130
+ const cleanup = (reason, cancelReader = true) => {
131
+ if (cleanedUp) return resolvedPromise;
132
+ cleanedUp = true;
133
+ if (lifetimeTimeoutHandle !== void 0) {
134
+ clearTimeout(lifetimeTimeoutHandle);
135
+ lifetimeTimeoutHandle = void 0;
136
+ }
137
+ try {
138
+ stopListeningToInjectedHtml?.();
139
+ } catch {}
140
+ stopListeningToInjectedHtml = void 0;
141
+ if (cancelReader) notifyAbort(reason);
142
+ const readerDone = cancelReader ? readerState.cancel(reason) : (readerState.release(), resolvedPromise);
143
+ if (serverSsr) try {
144
+ serverSsr.cleanup();
145
+ } catch (error) {
146
+ console.error("Error in SSR cleanup:", error);
147
+ }
148
+ return readerDone;
149
+ };
150
+ const safeClose = () => {
151
+ if (isDone()) return;
152
+ state = MergeState.Done;
153
+ try {
154
+ controller?.close();
155
+ } catch {}
156
+ };
157
+ const safeError = (error) => {
158
+ if (isDone()) return;
159
+ state = MergeState.Done;
160
+ try {
161
+ controller?.error(error);
162
+ } catch {}
163
+ };
164
+ if (serverSsr) stopListeningToInjectedHtml = serverSsr.onInjectedHtml(() => {
165
+ const err = /* @__PURE__ */ new Error("SSR router HTML injected during fast path");
166
+ safeError(err);
167
+ cleanup(err);
168
+ });
169
+ const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS;
170
+ lifetimeTimeoutHandle = setTimeout(() => {
171
+ if (!cleanedUp && !isDone()) {
172
+ const err = /* @__PURE__ */ new Error("Stream lifetime exceeded");
173
+ console.warn(`SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`);
174
+ safeError(err);
175
+ cleanup(err);
176
+ }
177
+ }, lifetimeMs);
178
+ return new ReadableStream({
179
+ start(c) {
180
+ controller = c;
181
+ },
182
+ async pull(c) {
183
+ if (cleanedUp || isDone()) return;
99
184
  try {
100
- while (true) {
101
- const { done, value } = await reader.read();
102
- if (done) break;
103
- if (cleanedUp || isStreamClosed) return;
104
- controller?.enqueue(value);
185
+ const { done, value } = await readerState.reader.read();
186
+ if (!done) {
187
+ if (!cleanedUp && !isDone()) c.enqueue(value);
188
+ return;
105
189
  }
106
- if (cleanedUp || isStreamClosed) return;
107
- router.serverSsr?.setRenderFinished();
190
+ if (cleanedUp || isDone()) return;
191
+ if (!finishSsrRendering()) return;
108
192
  safeClose();
109
- cleanup();
193
+ return cleanup(void 0, false);
110
194
  } catch (error) {
111
195
  if (cleanedUp) return;
112
196
  console.error("Error reading appStream:", error);
113
- router.serverSsr?.setRenderFinished();
197
+ if (state < MergeState.AppDone) try {
198
+ serverSsr?.setRenderFinished();
199
+ } catch {}
114
200
  safeError(error);
115
- cleanup();
201
+ return cleanup(error);
116
202
  } finally {
117
- reader.releaseLock();
203
+ if (cleanedUp || isDone()) readerState.release();
118
204
  }
119
- })().catch((error) => {
120
- if (cleanedUp) return;
121
- console.error("Error in stream transform:", error);
122
- safeError(error);
123
- cleanup();
124
- });
125
- return stream;
126
- }
205
+ },
206
+ cancel(reason) {
207
+ state = MergeState.Done;
208
+ return cleanup(reason);
209
+ }
210
+ });
211
+ }
212
+ function makeMainStream(serverSsr, appStream, opts) {
127
213
  let stopListeningToInjectedHtml;
128
214
  let stopListeningToSerializationFinished;
129
215
  let serializationTimeoutHandle;
130
216
  let lifetimeTimeoutHandle;
131
217
  let cleanedUp = false;
132
218
  let controller;
133
- let isStreamClosed = false;
134
- const textDecoder = new TextDecoder();
135
- let pendingRouterHtml = initialBufferedHtml ?? "";
136
- let leftover = "";
137
- let pendingClosingTags = "";
138
- const MAX_LEFTOVER_CHARS = 2048;
139
- let isAppRendering = true;
140
- let streamBarrierLifted = false;
141
- let serializationFinished = serializationAlreadyFinished;
142
- function safeEnqueue(chunk) {
143
- if (isStreamClosed) return;
144
- if (typeof chunk === "string") controller.enqueue(textEncoder.encode(chunk));
145
- else controller.enqueue(chunk);
219
+ let closeWhenDrained = false;
220
+ let state = MergeState.ReadingBody;
221
+ const readerState = createReaderState(appStream);
222
+ const notifyAbort = createAbortNotifier(opts);
223
+ const pendingWrites = [];
224
+ let pendingWriteHead = 0;
225
+ let pendingWriteChars = 0;
226
+ function clearPending() {
227
+ pendingWrites.length = 0;
228
+ pendingWriteHead = 0;
229
+ pendingWriteChars = 0;
230
+ }
231
+ let drainResolve = null;
232
+ const waitForDrain = () => new Promise((r) => {
233
+ drainResolve = r;
234
+ });
235
+ const signalDrain = () => {
236
+ if (drainResolve) {
237
+ const r = drainResolve;
238
+ drainResolve = null;
239
+ r();
240
+ }
241
+ };
242
+ const isDone = () => state === MergeState.Done;
243
+ function drainPending() {
244
+ if (!controller || isDone()) return;
245
+ while (pendingWriteHead < pendingWrites.length) {
246
+ const ds = controller.desiredSize;
247
+ if (ds !== null && ds <= 0) return;
248
+ const next = pendingWrites[pendingWriteHead];
249
+ pendingWrites[pendingWriteHead] = "";
250
+ pendingWriteHead++;
251
+ pendingWriteChars -= next.length;
252
+ try {
253
+ controller.enqueue(textEncoder.encode(next));
254
+ } catch (error) {
255
+ safeError(error);
256
+ cleanup(error);
257
+ return;
258
+ }
259
+ }
260
+ if (pendingWriteHead >= pendingWrites.length) {
261
+ pendingWrites.length = 0;
262
+ pendingWriteHead = 0;
263
+ }
264
+ if (closeWhenDrained && pendingWriteHead >= pendingWrites.length) {
265
+ closeWhenDrained = false;
266
+ safeClose();
267
+ cleanup(void 0, false);
268
+ }
269
+ }
270
+ /**
271
+ * Enqueue a string chunk through the backpressure queue. Stored as a
272
+ * string and encoded only when the downstream actually accepts the chunk
273
+ * — keeps native-memory pressure inside the controller's queue (which
274
+ * honors desiredSize) rather than ours.
275
+ */
276
+ function writeChunk(chunk) {
277
+ if (cleanedUp || isDone()) return;
278
+ if (!chunk.length) return;
279
+ if (pendingWriteChars + chunk.length > MAX_PENDING_WRITE_CHARS) {
280
+ const err = /* @__PURE__ */ new Error("SSR stream pending output exceeded maximum buffer");
281
+ safeError(err);
282
+ cleanup(err);
283
+ return;
284
+ }
285
+ pendingWrites.push(chunk);
286
+ pendingWriteChars += chunk.length;
287
+ drainPending();
146
288
  }
147
289
  function safeClose() {
148
- if (isStreamClosed) return;
149
- isStreamClosed = true;
290
+ if (isDone()) return;
291
+ state = MergeState.Done;
150
292
  try {
151
- controller.close();
293
+ controller?.close();
152
294
  } catch {}
153
295
  }
154
296
  function safeError(error) {
155
- if (isStreamClosed) return;
156
- isStreamClosed = true;
297
+ if (isDone()) return;
298
+ state = MergeState.Done;
157
299
  try {
158
- controller.error(error);
300
+ controller?.error(error);
159
301
  } catch {}
160
302
  }
161
303
  /**
162
304
  * Cleanup with guards; must be idempotent.
163
305
  */
164
- function cleanup() {
165
- if (cleanedUp) return;
306
+ function cleanup(reason, cancelReader = true) {
307
+ if (cleanedUp) return resolvedPromise;
166
308
  cleanedUp = true;
167
309
  try {
168
310
  stopListeningToInjectedHtml?.();
@@ -178,147 +320,257 @@ function transformStreamWithRouter(router, appStream, opts) {
178
320
  clearTimeout(lifetimeTimeoutHandle);
179
321
  lifetimeTimeoutHandle = void 0;
180
322
  }
181
- pendingRouterHtml = "";
323
+ clearPendingRouterHtml();
182
324
  leftover = "";
183
- pendingClosingTags = "";
184
- router.serverSsr?.cleanup();
325
+ pendingTail = "";
326
+ clearPending();
327
+ if (cancelReader) notifyAbort(reason);
328
+ const readerDone = cancelReader ? readerState.cancel(reason) : (readerState.release(), resolvedPromise);
329
+ signalDrain();
330
+ try {
331
+ serverSsr.cleanup();
332
+ } catch (error) {
333
+ console.error("Error in SSR cleanup:", error);
334
+ }
335
+ return readerDone;
336
+ }
337
+ const textDecoder = new TextDecoder();
338
+ const pendingRouterHtml = [];
339
+ let pendingRouterHtmlChars = 0;
340
+ let leftover = "";
341
+ let pendingTail = "";
342
+ let streamBarrierLifted = false;
343
+ let streamBarrierMarkerSeen = false;
344
+ let serializationFinished = false;
345
+ function noteBarrierMarker(chunk) {
346
+ if (streamBarrierMarkerSeen) return;
347
+ if (chunk.includes("$tsr-stream-barrier")) streamBarrierMarkerSeen = true;
348
+ }
349
+ function liftBarrierAfterBoundary() {
350
+ if (streamBarrierLifted) return;
351
+ if (!streamBarrierMarkerSeen) return;
352
+ streamBarrierLifted = true;
353
+ serverSsr.liftScriptBarrier();
185
354
  }
186
355
  const stream = new ReadableStream({
187
356
  start(c) {
188
357
  controller = c;
358
+ drainPending();
359
+ },
360
+ pull() {
361
+ drainPending();
362
+ signalDrain();
189
363
  },
190
- cancel() {
191
- isStreamClosed = true;
192
- cleanup();
364
+ cancel(reason) {
365
+ state = MergeState.Done;
366
+ return cleanup(reason);
193
367
  }
194
368
  });
369
+ function drainRouterHtml() {
370
+ if (cleanedUp || isDone()) return;
371
+ let html;
372
+ try {
373
+ html = serverSsr.takeBufferedHtml();
374
+ } catch (error) {
375
+ safeError(error);
376
+ cleanup(error);
377
+ return;
378
+ }
379
+ if (!html) return;
380
+ if (state >= MergeState.Draining) {
381
+ const err = /* @__PURE__ */ new Error("SSR router HTML injected after stream finalization");
382
+ safeError(err);
383
+ cleanup(err);
384
+ return;
385
+ }
386
+ if (state === MergeState.HoldingTail) {
387
+ flushPendingRouterHtml();
388
+ writeChunk(html);
389
+ } else {
390
+ if (pendingRouterHtmlChars + html.length > MAX_ROUTER_HTML_CHARS) {
391
+ const err = /* @__PURE__ */ new Error("SSR router HTML exceeded maximum buffer");
392
+ safeError(err);
393
+ cleanup(err);
394
+ return;
395
+ }
396
+ pendingRouterHtml.push(html);
397
+ pendingRouterHtmlChars += html.length;
398
+ }
399
+ }
195
400
  function flushPendingRouterHtml() {
196
- if (!pendingRouterHtml) return;
197
- safeEnqueue(pendingRouterHtml);
198
- pendingRouterHtml = "";
401
+ if (!pendingRouterHtml.length) return;
402
+ for (const html of pendingRouterHtml) writeChunk(html);
403
+ clearPendingRouterHtml();
199
404
  }
200
- function appendRouterHtml(html) {
201
- if (!html) return;
202
- pendingRouterHtml += html;
405
+ function clearPendingRouterHtml() {
406
+ pendingRouterHtml.length = 0;
407
+ pendingRouterHtmlChars = 0;
408
+ }
409
+ function appendTail(chunk) {
410
+ pendingTail += chunk;
411
+ if (pendingTail.length > MAX_TAIL_CHARS) throw new Error("SSR stream tail exceeded maximum buffer");
412
+ }
413
+ function waitForBackpressure() {
414
+ return !!(controller && controller.desiredSize !== null && controller.desiredSize <= 0);
415
+ }
416
+ function startSerializationTimeout() {
417
+ if (cleanedUp || isDone()) return;
418
+ if (serializationTimeoutHandle !== void 0) return;
419
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS;
420
+ serializationTimeoutHandle = setTimeout(() => {
421
+ if (!cleanedUp && !isDone()) {
422
+ const err = /* @__PURE__ */ new Error("Serialization timeout after app render finished");
423
+ console.error("Serialization timeout after app render finished");
424
+ safeError(err);
425
+ cleanup(err);
426
+ }
427
+ }, timeoutMs);
203
428
  }
204
429
  /**
205
- * Finish only when app done and serialization complete.
430
+ * Finish only when app done and serialization complete. Queues final
431
+ * output and requests close-when-drained so we don't close ahead of
432
+ * pending writes still waiting on downstream capacity.
206
433
  */
207
434
  function tryFinish() {
208
- if (isAppRendering || !serializationFinished) return;
209
- if (cleanedUp || isStreamClosed) return;
435
+ if (state !== MergeState.AppDone || !serializationFinished) return;
436
+ if (cleanedUp || isDone()) return;
210
437
  if (serializationTimeoutHandle !== void 0) {
211
438
  clearTimeout(serializationTimeoutHandle);
212
439
  serializationTimeoutHandle = void 0;
213
440
  }
441
+ drainRouterHtml();
442
+ if (cleanedUp || isDone()) return;
214
443
  const decoderRemainder = textDecoder.decode();
215
- if (leftover) safeEnqueue(leftover);
216
- if (decoderRemainder) safeEnqueue(decoderRemainder);
444
+ if (leftover) writeChunk(leftover);
445
+ if (cleanedUp || isDone()) return;
446
+ if (decoderRemainder) writeChunk(decoderRemainder);
447
+ if (cleanedUp || isDone()) return;
217
448
  flushPendingRouterHtml();
218
- if (pendingClosingTags) safeEnqueue(pendingClosingTags);
219
- safeClose();
220
- cleanup();
449
+ if (cleanedUp || isDone()) return;
450
+ if (pendingTail) writeChunk(pendingTail);
451
+ if (cleanedUp || isDone()) return;
452
+ leftover = "";
453
+ pendingTail = "";
454
+ state = MergeState.Draining;
455
+ closeWhenDrained = true;
456
+ drainPending();
221
457
  }
222
- const lifetimeMs = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS;
458
+ function finishAppRendering() {
459
+ if (state >= MergeState.AppDone) return;
460
+ state = MergeState.AppDone;
461
+ try {
462
+ serverSsr.setRenderFinished();
463
+ } catch (error) {
464
+ safeError(error);
465
+ cleanup(error);
466
+ return;
467
+ }
468
+ drainRouterHtml();
469
+ if (cleanedUp || isDone()) return;
470
+ serializationFinished = serializationFinished || serverSsr.isSerializationFinished();
471
+ if (serializationFinished) tryFinish();
472
+ else startSerializationTimeout();
473
+ }
474
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS;
475
+ const lifetimeMs = opts?.lifetimeMs ?? timeoutMs * 2;
223
476
  lifetimeTimeoutHandle = setTimeout(() => {
224
- if (!cleanedUp && !isStreamClosed) {
477
+ if (!cleanedUp && !isDone()) {
478
+ const err = /* @__PURE__ */ new Error("Stream lifetime exceeded");
225
479
  console.warn(`SSR stream transform exceeded maximum lifetime (${lifetimeMs}ms), forcing cleanup`);
226
- safeError(/* @__PURE__ */ new Error("Stream lifetime exceeded"));
227
- cleanup();
480
+ safeError(err);
481
+ cleanup(err);
228
482
  }
229
483
  }, lifetimeMs);
230
- if (!serializationAlreadyFinished) {
231
- stopListeningToInjectedHtml = router.subscribe("onInjectedHtml", () => {
232
- if (cleanedUp || isStreamClosed) return;
233
- const html = router.serverSsr?.takeBufferedHtml();
234
- if (!html) return;
235
- if (isAppRendering || leftover || pendingClosingTags) appendRouterHtml(html);
236
- else {
237
- flushPendingRouterHtml();
238
- safeEnqueue(html);
239
- }
240
- });
241
- stopListeningToSerializationFinished = router.subscribe("onSerializationFinished", () => {
242
- serializationFinished = true;
243
- tryFinish();
244
- });
484
+ stopListeningToInjectedHtml = serverSsr.onInjectedHtml(() => {
485
+ drainRouterHtml();
486
+ });
487
+ stopListeningToSerializationFinished = serverSsr.onSerializationFinished(() => {
488
+ serializationFinished = true;
489
+ drainRouterHtml();
490
+ tryFinish();
491
+ });
492
+ drainRouterHtml();
493
+ if (cleanedUp || isDone()) return stream;
494
+ serializationFinished = serializationFinished || serverSsr.isSerializationFinished();
495
+ if (serializationFinished) {
496
+ drainRouterHtml();
497
+ if (cleanedUp || isDone()) return stream;
245
498
  }
246
499
  (async () => {
247
- const reader = appStream.getReader();
248
500
  try {
249
501
  while (true) {
250
- const { done, value } = await reader.read();
502
+ if (waitForBackpressure()) {
503
+ await waitForDrain();
504
+ if (cleanedUp || isDone()) return;
505
+ }
506
+ const { done, value } = await readerState.reader.read();
251
507
  if (done) break;
252
- if (cleanedUp || isStreamClosed) return;
253
- const text = value instanceof Uint8Array ? textDecoder.decode(value, { stream: true }) : String(value);
508
+ if (cleanedUp || isDone()) return;
509
+ const text = typeof value === "string" ? value : textDecoder.decode(value, { stream: true });
254
510
  const chunkString = leftover ? leftover + text : text;
255
- if (!streamBarrierLifted) {
256
- if (chunkString.includes("$tsr-stream-barrier")) {
257
- streamBarrierLifted = true;
258
- router.serverSsr?.liftScriptBarrier();
259
- }
260
- }
261
- if (pendingClosingTags) {
262
- pendingClosingTags += chunkString;
511
+ if (state >= MergeState.HoldingTail) {
512
+ appendTail(chunkString);
263
513
  leftover = "";
264
514
  continue;
265
515
  }
266
- const bodyEndIndex = chunkString.indexOf(BODY_END_TAG);
267
- const htmlEndIndex = chunkString.indexOf(HTML_END_TAG);
268
- if (bodyEndIndex !== -1 && htmlEndIndex !== -1 && bodyEndIndex < htmlEndIndex) {
269
- pendingClosingTags = chunkString.slice(bodyEndIndex);
270
- safeEnqueue(chunkString.slice(0, bodyEndIndex));
516
+ const boundary = findHtmlBoundary(chunkString);
517
+ if (boundary < -1) {
518
+ const bodyEndIndex = -boundary - 2;
519
+ state = MergeState.HoldingTail;
520
+ appendTail(chunkString.slice(bodyEndIndex));
521
+ const bodyChunk = chunkString.slice(0, bodyEndIndex);
522
+ writeChunk(bodyChunk);
523
+ if (cleanedUp || isDone()) return;
524
+ noteBarrierMarker(bodyChunk);
525
+ liftBarrierAfterBoundary();
526
+ if (cleanedUp || isDone()) return;
271
527
  flushPendingRouterHtml();
272
528
  leftover = "";
273
529
  continue;
274
530
  }
275
- const lastClosingTagEnd = findLastClosingTagEnd(chunkString);
531
+ const lastClosingTagEnd = boundary;
276
532
  if (lastClosingTagEnd > 0) {
277
- safeEnqueue(chunkString.slice(0, lastClosingTagEnd));
533
+ const safeChunk = chunkString.slice(0, lastClosingTagEnd);
534
+ writeChunk(safeChunk);
535
+ if (cleanedUp || isDone()) return;
536
+ noteBarrierMarker(safeChunk);
537
+ liftBarrierAfterBoundary();
538
+ if (cleanedUp || isDone()) return;
278
539
  flushPendingRouterHtml();
279
540
  leftover = chunkString.slice(lastClosingTagEnd);
280
541
  if (leftover.length > MAX_LEFTOVER_CHARS) {
281
- safeEnqueue(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS));
282
- leftover = leftover.slice(-MAX_LEFTOVER_CHARS);
542
+ noteBarrierMarker(leftover);
543
+ writeChunk(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS));
544
+ leftover = leftover.slice(-2048);
283
545
  }
284
546
  } else {
285
547
  const combined = chunkString;
286
548
  if (combined.length > MAX_LEFTOVER_CHARS) {
549
+ noteBarrierMarker(combined);
287
550
  const flushUpto = combined.length - MAX_LEFTOVER_CHARS;
288
- safeEnqueue(combined.slice(0, flushUpto));
551
+ writeChunk(combined.slice(0, flushUpto));
289
552
  leftover = combined.slice(flushUpto);
290
553
  } else leftover = combined;
291
554
  }
292
555
  }
293
- if (cleanedUp || isStreamClosed) return;
294
- isAppRendering = false;
295
- router.serverSsr?.setRenderFinished();
296
- if (serializationFinished) tryFinish();
297
- else {
298
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_SERIALIZATION_TIMEOUT_MS;
299
- serializationTimeoutHandle = setTimeout(() => {
300
- if (!cleanedUp && !isStreamClosed) {
301
- console.error("Serialization timeout after app render finished");
302
- safeError(/* @__PURE__ */ new Error("Serialization timeout after app render finished"));
303
- cleanup();
304
- }
305
- }, timeoutMs);
306
- }
556
+ if (cleanedUp || isDone()) return;
557
+ finishAppRendering();
307
558
  } catch (error) {
308
559
  if (cleanedUp) return;
309
560
  console.error("Error reading appStream:", error);
310
- isAppRendering = false;
311
- router.serverSsr?.setRenderFinished();
561
+ if (state < MergeState.AppDone) try {
562
+ serverSsr.setRenderFinished();
563
+ } catch {}
312
564
  safeError(error);
313
- cleanup();
565
+ cleanup(error);
314
566
  } finally {
315
- reader.releaseLock();
567
+ readerState.release();
316
568
  }
317
569
  })().catch((error) => {
318
570
  if (cleanedUp) return;
319
571
  console.error("Error in stream transform:", error);
320
572
  safeError(error);
321
- cleanup();
573
+ cleanup(error);
322
574
  });
323
575
  return stream;
324
576
  }