@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.
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/config.cjs.map +1 -1
- package/dist/cjs/defer.cjs.map +1 -1
- package/dist/cjs/invariant.cjs.map +1 -1
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/lru-cache.cjs.map +1 -1
- package/dist/cjs/manifest.cjs.map +1 -1
- package/dist/cjs/new-process-route-tree.cjs.map +1 -1
- package/dist/cjs/not-found.cjs.map +1 -1
- package/dist/cjs/path.cjs.map +1 -1
- package/dist/cjs/qss.cjs.map +1 -1
- package/dist/cjs/redirect.cjs.map +1 -1
- package/dist/cjs/rewrite.cjs.map +1 -1
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +29 -14
- package/dist/cjs/scroll-restoration-script/client.cjs.map +1 -1
- package/dist/cjs/scroll-restoration-script/server.cjs.map +1 -1
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/cjs/searchMiddleware.cjs.map +1 -1
- package/dist/cjs/searchParams.cjs.map +1 -1
- package/dist/cjs/ssr/createRequestHandler.cjs +8 -7
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/ssr/handlerCallback.cjs +46 -0
- package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
- package/dist/cjs/ssr/handlerCallback.d.cts +15 -1
- package/dist/cjs/ssr/headers.cjs.map +1 -1
- package/dist/cjs/ssr/json.cjs.map +1 -1
- package/dist/cjs/ssr/serializer/RawStream.cjs.map +1 -1
- package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -1
- package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -1
- package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -1
- package/dist/cjs/ssr/server.cjs +6 -1
- package/dist/cjs/ssr/server.d.cts +3 -2
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +131 -49
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.d.cts +0 -14
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +455 -203
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.d.cts +14 -5
- package/dist/cjs/stores.cjs.map +1 -1
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/defer.js.map +1 -1
- package/dist/esm/invariant.js.map +1 -1
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/lru-cache.js.map +1 -1
- package/dist/esm/manifest.js.map +1 -1
- package/dist/esm/new-process-route-tree.js.map +1 -1
- package/dist/esm/not-found.js.map +1 -1
- package/dist/esm/path.js.map +1 -1
- package/dist/esm/qss.js.map +1 -1
- package/dist/esm/redirect.js.map +1 -1
- package/dist/esm/rewrite.js.map +1 -1
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +29 -14
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration-script/client.js.map +1 -1
- package/dist/esm/scroll-restoration-script/server.js.map +1 -1
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/dist/esm/searchMiddleware.js.map +1 -1
- package/dist/esm/searchParams.js.map +1 -1
- package/dist/esm/ssr/createRequestHandler.js +8 -7
- package/dist/esm/ssr/createRequestHandler.js.map +1 -1
- package/dist/esm/ssr/handlerCallback.d.ts +15 -1
- package/dist/esm/ssr/handlerCallback.js +42 -1
- package/dist/esm/ssr/handlerCallback.js.map +1 -1
- package/dist/esm/ssr/headers.js.map +1 -1
- package/dist/esm/ssr/json.js.map +1 -1
- package/dist/esm/ssr/serializer/RawStream.js.map +1 -1
- package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -1
- package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -1
- package/dist/esm/ssr/serializer/transformer.js.map +1 -1
- package/dist/esm/ssr/server.d.ts +3 -2
- package/dist/esm/ssr/server.js +2 -2
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/ssr/ssr-match-id.js.map +1 -1
- package/dist/esm/ssr/ssr-server.d.ts +0 -14
- package/dist/esm/ssr/ssr-server.js +131 -49
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.d.ts +14 -5
- package/dist/esm/ssr/transformStreamWithRouter.js +455 -203
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/dist/esm/stores.js.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +32 -16
- package/src/ssr/createRequestHandler.ts +8 -8
- package/src/ssr/handlerCallback.ts +84 -1
- package/src/ssr/server.ts +14 -2
- package/src/ssr/ssr-server.ts +179 -81
- 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 =
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
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 ||
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
+
if (cleanedUp || isDone()) readerState.release();
|
|
118
204
|
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 (
|
|
149
|
-
|
|
290
|
+
if (isDone()) return;
|
|
291
|
+
state = MergeState.Done;
|
|
150
292
|
try {
|
|
151
|
-
controller
|
|
293
|
+
controller?.close();
|
|
152
294
|
} catch {}
|
|
153
295
|
}
|
|
154
296
|
function safeError(error) {
|
|
155
|
-
if (
|
|
156
|
-
|
|
297
|
+
if (isDone()) return;
|
|
298
|
+
state = MergeState.Done;
|
|
157
299
|
try {
|
|
158
|
-
controller
|
|
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
|
-
|
|
323
|
+
clearPendingRouterHtml();
|
|
182
324
|
leftover = "";
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
401
|
+
if (!pendingRouterHtml.length) return;
|
|
402
|
+
for (const html of pendingRouterHtml) writeChunk(html);
|
|
403
|
+
clearPendingRouterHtml();
|
|
199
404
|
}
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
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 (
|
|
209
|
-
if (cleanedUp ||
|
|
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)
|
|
216
|
-
if (
|
|
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 (
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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 && !
|
|
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(
|
|
227
|
-
cleanup();
|
|
480
|
+
safeError(err);
|
|
481
|
+
cleanup(err);
|
|
228
482
|
}
|
|
229
483
|
}, lifetimeMs);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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 ||
|
|
253
|
-
const text = 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 (
|
|
256
|
-
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 =
|
|
531
|
+
const lastClosingTagEnd = boundary;
|
|
276
532
|
if (lastClosingTagEnd > 0) {
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
leftover
|
|
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
|
-
|
|
551
|
+
writeChunk(combined.slice(0, flushUpto));
|
|
289
552
|
leftover = combined.slice(flushUpto);
|
|
290
553
|
} else leftover = combined;
|
|
291
554
|
}
|
|
292
555
|
}
|
|
293
|
-
if (cleanedUp ||
|
|
294
|
-
|
|
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
|
-
|
|
311
|
-
|
|
561
|
+
if (state < MergeState.AppDone) try {
|
|
562
|
+
serverSsr.setRenderFinished();
|
|
563
|
+
} catch {}
|
|
312
564
|
safeError(error);
|
|
313
|
-
cleanup();
|
|
565
|
+
cleanup(error);
|
|
314
566
|
} finally {
|
|
315
|
-
|
|
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
|
}
|