@tanstack/router-core 1.136.8 → 1.136.9
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/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +3 -2
- package/dist/cjs/ssr/ssr-server.cjs +26 -2
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +32 -35
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.d.cts +4 -1
- package/dist/esm/router.d.ts +3 -2
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +26 -2
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.d.ts +4 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +32 -35
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +3 -3
- package/src/ssr/ssr-server.ts +29 -4
- package/src/ssr/transformStreamWithRouter.ts +35 -39
|
@@ -5,6 +5,7 @@ import minifiedTsrBootStrapScript from "./tsrScript.js";
|
|
|
5
5
|
import { GLOBAL_TSR } from "./constants.js";
|
|
6
6
|
import { defaultSerovalPlugins } from "./serializer/seroval-plugins.js";
|
|
7
7
|
import { makeSsrSerovalPlugin } from "./serializer/transformer.js";
|
|
8
|
+
import { TSR_SCRIPT_BARRIER_ID } from "./transformStreamWithRouter.js";
|
|
8
9
|
const SCOPE_ID = "tsr";
|
|
9
10
|
function dehydrateMatch(match) {
|
|
10
11
|
const dehydratedMatch = {
|
|
@@ -107,8 +108,20 @@ function attachRouterServerSsrUtils({
|
|
|
107
108
|
matchesToDehydrate = matchesToDehydrate.slice(0, 1);
|
|
108
109
|
}
|
|
109
110
|
const matches = matchesToDehydrate.map(dehydrateMatch);
|
|
111
|
+
let manifestToDehydrate = void 0;
|
|
112
|
+
if (manifest) {
|
|
113
|
+
const filteredRoutes = Object.fromEntries(
|
|
114
|
+
router.state.matches.map((k) => [
|
|
115
|
+
k.routeId,
|
|
116
|
+
manifest.routes[k.routeId]
|
|
117
|
+
])
|
|
118
|
+
);
|
|
119
|
+
manifestToDehydrate = {
|
|
120
|
+
routes: filteredRoutes
|
|
121
|
+
};
|
|
122
|
+
}
|
|
110
123
|
const dehydratedRouter = {
|
|
111
|
-
manifest:
|
|
124
|
+
manifest: manifestToDehydrate,
|
|
112
125
|
matches
|
|
113
126
|
};
|
|
114
127
|
const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id;
|
|
@@ -152,8 +165,19 @@ function attachRouterServerSsrUtils({
|
|
|
152
165
|
},
|
|
153
166
|
takeBufferedScripts() {
|
|
154
167
|
const scripts = scriptBuffer.takeAll();
|
|
168
|
+
const serverBufferedScript = {
|
|
169
|
+
tag: "script",
|
|
170
|
+
attrs: {
|
|
171
|
+
nonce: router.options.ssr?.nonce,
|
|
172
|
+
className: "$tsr",
|
|
173
|
+
id: TSR_SCRIPT_BARRIER_ID
|
|
174
|
+
},
|
|
175
|
+
children: scripts
|
|
176
|
+
};
|
|
177
|
+
return serverBufferedScript;
|
|
178
|
+
},
|
|
179
|
+
liftScriptBarrier() {
|
|
155
180
|
scriptBuffer.liftBarrier();
|
|
156
|
-
return scripts;
|
|
157
181
|
}
|
|
158
182
|
};
|
|
159
183
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { createControlledPromise } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { DehydratedMatch } from './ssr-client'\nimport type { DehydratedRouter } from './client'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest } from '../manifest'\nimport type { AnySerializationAdapter } from './serializer/transformer'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n promise: Promise<string>\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n constructor(private router: AnyRouter) {}\n private _queue: Array<string> = [...INITIAL_SCRIPTS]\n private _scriptBarrierLifted = false\n\n enqueue(script: string) {\n if (this._scriptBarrierLifted && this._queue.length === 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n this._queue.push(script)\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n bufferedScripts.push(`${GLOBAL_TSR}.c()`)\n const joinedScripts = bufferedScripts.join(';')\n return joinedScripts\n }\n\n injectBufferedScripts() {\n const scriptsToInject = this.takeAll()\n if (scriptsToInject) {\n this.router.serverSsr!.injectScript(() => scriptsToInject)\n }\n }\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n const listeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n\n router.serverSsr = {\n injectedHtml: [],\n injectHtml: (getHtml) => {\n const promise = Promise.resolve().then(getHtml)\n router.serverSsr!.injectedHtml.push(promise)\n router.emit({\n type: 'onInjectedHtml',\n promise,\n })\n\n return promise.then(() => {})\n },\n injectScript: (getScript) => {\n return router.serverSsr!.injectHtml(async () => {\n const script = await getScript()\n if (!script) {\n return ''\n }\n return `<script${router.options.ssr?.nonce ? `nonce='${router.options.ssr.nonce}' ` : ''} class='$tsr'>${script}</script>`\n })\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n const dehydratedRouter: DehydratedRouter = {\n manifest: router.ssr!.manifest,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const p = createControlledPromise<string>()\n const trackPlugins = { didRun: false }\n const plugins =\n (\n router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n )?.map((t) => makeSsrSerovalPlugin(t, trackPlugins)) ?? []\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins: [...plugins, ...defaultSerovalPlugins],\n onSerialize: (data, initial) => {\n let serialized = initial ? GLOBAL_TSR + '.router=' + data : data\n if (trackPlugins.didRun) {\n serialized = GLOBAL_TSR + '.p(()=>' + serialized + ')'\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.streamEnd=true')\n p.resolve('')\n },\n onError: (err) => p.reject(err),\n })\n // make sure the stream is kept open until the promise is resolved\n router.serverSsr!.injectHtml(() => p)\n },\n isDehydrated() {\n return _dehydrated\n },\n onRenderFinished: (listener) => listeners.push(listener),\n setRenderFinished: () => {\n listeners.forEach((l) => l())\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n scriptBuffer.liftBarrier()\n return scripts\n },\n }\n}\n\nexport function getOrigin(request: Request) {\n const originHeader = request.headers.get('Origin')\n if (originHeader) {\n try {\n new URL(originHeader)\n return originHeader\n } catch {}\n }\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { createControlledPromise } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport { TSR_SCRIPT_BARRIER_ID } from './transformStreamWithRouter'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { DehydratedMatch } from './ssr-client'\nimport type { DehydratedRouter } from './client'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n promise: Promise<string>\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n constructor(private router: AnyRouter) {}\n private _queue: Array<string> = [...INITIAL_SCRIPTS]\n private _scriptBarrierLifted = false\n\n enqueue(script: string) {\n if (this._scriptBarrierLifted && this._queue.length === 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n this._queue.push(script)\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n bufferedScripts.push(`${GLOBAL_TSR}.c()`)\n const joinedScripts = bufferedScripts.join(';')\n return joinedScripts\n }\n\n injectBufferedScripts() {\n const scriptsToInject = this.takeAll()\n if (scriptsToInject) {\n this.router.serverSsr!.injectScript(() => scriptsToInject)\n }\n }\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n const listeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n\n router.serverSsr = {\n injectedHtml: [],\n injectHtml: (getHtml) => {\n const promise = Promise.resolve().then(getHtml)\n router.serverSsr!.injectedHtml.push(promise)\n router.emit({\n type: 'onInjectedHtml',\n promise,\n })\n\n return promise.then(() => {})\n },\n injectScript: (getScript) => {\n return router.serverSsr!.injectHtml(async () => {\n const script = await getScript()\n if (!script) {\n return ''\n }\n return `<script${router.options.ssr?.nonce ? `nonce='${router.options.ssr.nonce}' ` : ''} class='$tsr'>${script}</script>`\n })\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // only send manifest of the current routes to the client\n if (manifest) {\n const filteredRoutes = Object.fromEntries(\n router.state.matches.map((k) => [\n k.routeId,\n manifest.routes[k.routeId],\n ]),\n )\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const p = createControlledPromise<string>()\n const trackPlugins = { didRun: false }\n const plugins =\n (\n router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n )?.map((t) => makeSsrSerovalPlugin(t, trackPlugins)) ?? []\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins: [...plugins, ...defaultSerovalPlugins],\n onSerialize: (data, initial) => {\n let serialized = initial ? GLOBAL_TSR + '.router=' + data : data\n if (trackPlugins.didRun) {\n serialized = GLOBAL_TSR + '.p(()=>' + serialized + ')'\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.streamEnd=true')\n p.resolve('')\n },\n onError: (err) => p.reject(err),\n })\n // make sure the stream is kept open until the promise is resolved\n router.serverSsr!.injectHtml(() => p)\n },\n isDehydrated() {\n return _dehydrated\n },\n onRenderFinished: (listener) => listeners.push(listener),\n setRenderFinished: () => {\n listeners.forEach((l) => l())\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n }\n}\n\nexport function getOrigin(request: Request) {\n const originHeader = request.headers.get('Origin')\n if (originHeader) {\n try {\n new URL(originHeader)\n return originHeader\n } catch {}\n }\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,4 +3,7 @@ import { Readable } from 'node:stream';
|
|
|
3
3
|
import { AnyRouter } from '../router.js';
|
|
4
4
|
export declare function transformReadableStreamWithRouter(router: AnyRouter, routerStream: ReadableStream): ReadableStream<any>;
|
|
5
5
|
export declare function transformPipeableStreamWithRouter(router: AnyRouter, routerStream: Readable): Readable;
|
|
6
|
-
export declare
|
|
6
|
+
export declare const TSR_SCRIPT_BARRIER_ID = "$tsr-stream-barrier";
|
|
7
|
+
export declare function transformStreamWithRouter(router: AnyRouter, appStream: ReadableStream, opts?: {
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}): ReadableStream<any>;
|
|
@@ -9,12 +9,10 @@ function transformPipeableStreamWithRouter(router, routerStream) {
|
|
|
9
9
|
transformStreamWithRouter(router, Readable.toWeb(routerStream))
|
|
10
10
|
);
|
|
11
11
|
}
|
|
12
|
-
const
|
|
12
|
+
const TSR_SCRIPT_BARRIER_ID = "$tsr-stream-barrier";
|
|
13
13
|
const patternBodyEnd = /(<\/body>)/;
|
|
14
14
|
const patternHtmlEnd = /(<\/html>)/;
|
|
15
|
-
const patternHeadStart = /(<head.*?>)/;
|
|
16
15
|
const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g;
|
|
17
|
-
const textDecoder = new TextDecoder();
|
|
18
16
|
function createPassthrough() {
|
|
19
17
|
let controller;
|
|
20
18
|
const encoder = new TextEncoder();
|
|
@@ -26,11 +24,15 @@ function createPassthrough() {
|
|
|
26
24
|
const res = {
|
|
27
25
|
stream,
|
|
28
26
|
write: (chunk) => {
|
|
29
|
-
|
|
27
|
+
if (typeof chunk === "string") {
|
|
28
|
+
controller.enqueue(encoder.encode(chunk));
|
|
29
|
+
} else {
|
|
30
|
+
controller.enqueue(chunk);
|
|
31
|
+
}
|
|
30
32
|
},
|
|
31
33
|
end: (chunk) => {
|
|
32
34
|
if (chunk) {
|
|
33
|
-
|
|
35
|
+
res.write(chunk);
|
|
34
36
|
}
|
|
35
37
|
controller.close();
|
|
36
38
|
res.destroyed = true;
|
|
@@ -54,15 +56,16 @@ async function readStream(stream, opts) {
|
|
|
54
56
|
opts.onError?.(error);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
function transformStreamWithRouter(router, appStream) {
|
|
59
|
+
function transformStreamWithRouter(router, appStream, opts) {
|
|
58
60
|
const finalPassThrough = createPassthrough();
|
|
61
|
+
const textDecoder = new TextDecoder();
|
|
59
62
|
let isAppRendering = true;
|
|
60
63
|
let routerStreamBuffer = "";
|
|
61
64
|
let pendingClosingTags = "";
|
|
62
|
-
let
|
|
63
|
-
let headStarted = false;
|
|
65
|
+
let streamBarrierLifted = false;
|
|
64
66
|
let leftover = "";
|
|
65
67
|
let leftoverHtml = "";
|
|
68
|
+
let timeoutHandle;
|
|
66
69
|
function getBufferedRouterStream() {
|
|
67
70
|
const html = routerStreamBuffer;
|
|
68
71
|
routerStreamBuffer = "";
|
|
@@ -70,7 +73,7 @@ function transformStreamWithRouter(router, appStream) {
|
|
|
70
73
|
}
|
|
71
74
|
function decodeChunk(chunk) {
|
|
72
75
|
if (chunk instanceof Uint8Array) {
|
|
73
|
-
return textDecoder.decode(chunk);
|
|
76
|
+
return textDecoder.decode(chunk, { stream: true });
|
|
74
77
|
}
|
|
75
78
|
return String(chunk);
|
|
76
79
|
}
|
|
@@ -88,7 +91,7 @@ function transformStreamWithRouter(router, appStream) {
|
|
|
88
91
|
function handleInjectedHtml(promise) {
|
|
89
92
|
processingCount++;
|
|
90
93
|
promise.then((html) => {
|
|
91
|
-
if (
|
|
94
|
+
if (isAppRendering) {
|
|
92
95
|
routerStreamBuffer += html;
|
|
93
96
|
} else {
|
|
94
97
|
finalPassThrough.write(html);
|
|
@@ -96,48 +99,33 @@ function transformStreamWithRouter(router, appStream) {
|
|
|
96
99
|
}).catch(injectedHtmlDonePromise.reject).finally(() => {
|
|
97
100
|
processingCount--;
|
|
98
101
|
if (!isAppRendering && processingCount === 0) {
|
|
99
|
-
stopListeningToInjectedHtml();
|
|
100
102
|
injectedHtmlDonePromise.resolve();
|
|
101
103
|
}
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
106
|
injectedHtmlDonePromise.then(() => {
|
|
107
|
+
clearTimeout(timeoutHandle);
|
|
105
108
|
const finalHtml = leftoverHtml + getBufferedRouterStream() + pendingClosingTags;
|
|
106
109
|
finalPassThrough.end(finalHtml);
|
|
107
110
|
}).catch((err) => {
|
|
108
111
|
console.error("Error reading routerStream:", err);
|
|
109
112
|
finalPassThrough.destroy(err);
|
|
110
|
-
});
|
|
113
|
+
}).finally(stopListeningToInjectedHtml);
|
|
111
114
|
readStream(appStream, {
|
|
112
115
|
onData: (chunk) => {
|
|
113
116
|
const text = decodeChunk(chunk.value);
|
|
114
|
-
|
|
117
|
+
const chunkString = leftover + text;
|
|
115
118
|
const bodyEndMatch = chunkString.match(patternBodyEnd);
|
|
116
119
|
const htmlEndMatch = chunkString.match(patternHtmlEnd);
|
|
117
|
-
if (!
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const headStartMatch = chunkString.match(patternHeadStart);
|
|
125
|
-
if (headStartMatch) {
|
|
126
|
-
headStarted = true;
|
|
127
|
-
const index = headStartMatch.index;
|
|
128
|
-
const headTag = headStartMatch[0];
|
|
129
|
-
const remaining = chunkString.slice(index + headTag.length);
|
|
130
|
-
finalPassThrough.write(
|
|
131
|
-
chunkString.slice(0, index) + headTag + getBufferedRouterStream()
|
|
132
|
-
);
|
|
133
|
-
chunkString = remaining;
|
|
120
|
+
if (!streamBarrierLifted) {
|
|
121
|
+
const streamBarrierIdIncluded = chunkString.includes(
|
|
122
|
+
TSR_SCRIPT_BARRIER_ID
|
|
123
|
+
);
|
|
124
|
+
if (streamBarrierIdIncluded) {
|
|
125
|
+
streamBarrierLifted = true;
|
|
126
|
+
router.serverSsr.liftScriptBarrier();
|
|
134
127
|
}
|
|
135
128
|
}
|
|
136
|
-
if (!bodyStarted) {
|
|
137
|
-
finalPassThrough.write(chunkString);
|
|
138
|
-
leftover = "";
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
129
|
if (bodyEndMatch && htmlEndMatch && bodyEndMatch.index < htmlEndMatch.index) {
|
|
142
130
|
const bodyEndIndex = bodyEndMatch.index;
|
|
143
131
|
pendingClosingTags = chunkString.slice(bodyEndIndex);
|
|
@@ -166,16 +154,25 @@ function transformStreamWithRouter(router, appStream) {
|
|
|
166
154
|
router.serverSsr.setRenderFinished();
|
|
167
155
|
if (processingCount === 0) {
|
|
168
156
|
injectedHtmlDonePromise.resolve();
|
|
157
|
+
} else {
|
|
158
|
+
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
159
|
+
timeoutHandle = setTimeout(() => {
|
|
160
|
+
injectedHtmlDonePromise.reject(
|
|
161
|
+
new Error("Injected HTML timeout after app render finished")
|
|
162
|
+
);
|
|
163
|
+
}, timeoutMs);
|
|
169
164
|
}
|
|
170
165
|
},
|
|
171
166
|
onError: (error) => {
|
|
172
167
|
console.error("Error reading appStream:", error);
|
|
173
168
|
finalPassThrough.destroy(error);
|
|
169
|
+
injectedHtmlDonePromise.reject(error);
|
|
174
170
|
}
|
|
175
171
|
});
|
|
176
172
|
return finalPassThrough.stream;
|
|
177
173
|
}
|
|
178
174
|
export {
|
|
175
|
+
TSR_SCRIPT_BARRIER_ID,
|
|
179
176
|
transformPipeableStreamWithRouter,
|
|
180
177
|
transformReadableStreamWithRouter,
|
|
181
178
|
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\n// regex pattern for matching closing body and html tags\nconst patternBodyStart = /(<body)/\nconst patternBodyEnd = /(<\\/body>)/\nconst patternHtmlEnd = /(<\\/html>)/\nconst patternHeadStart = /(<head.*?>)/\n// regex pattern for matching closing tags\nconst patternClosingTag = /(<\\/[a-zA-Z][\\w:.-]*?>)/g\n\nconst textDecoder = new TextDecoder()\n\ntype ReadablePassthrough = {\n stream: ReadableStream\n write: (chunk: string) => void\n end: (chunk?: string) => void\n destroy: (error: unknown) => void\n destroyed: boolean\n}\n\nfunction createPassthrough() {\n let controller: ReadableStreamDefaultController<any>\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n })\n\n const res: ReadablePassthrough = {\n stream,\n write: (chunk) => {\n controller.enqueue(encoder.encode(chunk))\n },\n end: (chunk) => {\n if (chunk) {\n controller.enqueue(encoder.encode(chunk))\n }\n controller.close()\n res.destroyed = true\n },\n destroy: (error) => {\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 try {\n const reader = stream.getReader()\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 }\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n) {\n const finalPassThrough = createPassthrough()\n\n let isAppRendering = true as boolean\n let routerStreamBuffer = ''\n let pendingClosingTags = ''\n let bodyStarted = false as boolean\n let headStarted = false as boolean\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)\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 router.serverSsr!.injectedHtml.forEach((promise) => {\n handleInjectedHtml(promise)\n })\n\n // Listen for any new injected HTML\n const stopListeningToInjectedHtml = router.subscribe(\n 'onInjectedHtml',\n (e) => {\n handleInjectedHtml(e.promise)\n },\n )\n\n function handleInjectedHtml(promise: Promise<string>) {\n processingCount++\n\n promise\n .then((html) => {\n if (!bodyStarted) {\n routerStreamBuffer += html\n } else {\n finalPassThrough.write(html)\n }\n })\n .catch(injectedHtmlDonePromise.reject)\n .finally(() => {\n processingCount--\n\n if (!isAppRendering && processingCount === 0) {\n stopListeningToInjectedHtml()\n injectedHtmlDonePromise.resolve()\n }\n })\n }\n\n injectedHtmlDonePromise\n .then(() => {\n const finalHtml =\n leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n finalPassThrough.end(finalHtml)\n })\n .catch((err) => {\n console.error('Error reading routerStream:', err)\n finalPassThrough.destroy(err)\n })\n\n // Transform the appStream\n readStream(appStream, {\n onData: (chunk) => {\n const text = decodeChunk(chunk.value)\n\n let chunkString = leftover + text\n const bodyEndMatch = chunkString.match(patternBodyEnd)\n const htmlEndMatch = chunkString.match(patternHtmlEnd)\n\n if (!bodyStarted) {\n const bodyStartMatch = chunkString.match(patternBodyStart)\n if (bodyStartMatch) {\n bodyStarted = true\n }\n }\n\n if (!headStarted) {\n const headStartMatch = chunkString.match(patternHeadStart)\n if (headStartMatch) {\n headStarted = true\n const index = headStartMatch.index!\n const headTag = headStartMatch[0]\n const remaining = chunkString.slice(index + headTag.length)\n finalPassThrough.write(\n chunkString.slice(0, index) + headTag + getBufferedRouterStream(),\n )\n // make sure to only write `remaining` until the next closing tag\n chunkString = remaining\n }\n }\n\n if (!bodyStarted) {\n finalPassThrough.write(chunkString)\n leftover = ''\n return\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) + getBufferedRouterStream(),\n )\n\n leftover = ''\n return\n }\n\n let result: RegExpExecArray | null\n let 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 } else {\n leftover = chunkString\n leftoverHtml += getBufferedRouterStream()\n }\n },\n onEnd: () => {\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 }\n },\n onError: (error) => {\n console.error('Error reading appStream:', error)\n finalPassThrough.destroy(error)\n },\n })\n\n return finalPassThrough.stream\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { 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() {\n let controller: ReadableStreamDefaultController<any>\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n })\n\n const res: ReadablePassthrough = {\n stream,\n write: (chunk) => {\n if (typeof chunk === 'string') {\n controller.enqueue(encoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n },\n end: (chunk) => {\n if (chunk) {\n res.write(chunk)\n }\n controller.close()\n res.destroyed = true\n },\n destroy: (error) => {\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 try {\n const reader = stream.getReader()\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 }\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n timeoutMs?: number\n },\n) {\n const finalPassThrough = createPassthrough()\n const textDecoder = new TextDecoder()\n\n let isAppRendering = true as boolean\n let routerStreamBuffer = ''\n let pendingClosingTags = ''\n let streamBarrierLifted = false as boolean\n let leftover = ''\n let leftoverHtml = ''\n let timeoutHandle: NodeJS.Timeout\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 router.serverSsr!.injectedHtml.forEach((promise) => {\n handleInjectedHtml(promise)\n })\n\n // Listen for any new injected HTML\n const stopListeningToInjectedHtml = router.subscribe(\n 'onInjectedHtml',\n (e) => {\n handleInjectedHtml(e.promise)\n },\n )\n\n function handleInjectedHtml(promise: Promise<string>) {\n processingCount++\n\n promise\n .then((html) => {\n if (isAppRendering) {\n routerStreamBuffer += html\n } else {\n finalPassThrough.write(html)\n }\n })\n .catch(injectedHtmlDonePromise.reject)\n .finally(() => {\n processingCount--\n\n if (!isAppRendering && processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n }\n })\n }\n\n injectedHtmlDonePromise\n .then(() => {\n clearTimeout(timeoutHandle)\n const finalHtml =\n leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n finalPassThrough.end(finalHtml)\n })\n .catch((err) => {\n console.error('Error reading routerStream:', err)\n finalPassThrough.destroy(err)\n })\n .finally(stopListeningToInjectedHtml)\n\n // Transform the appStream\n readStream(appStream, {\n onData: (chunk) => {\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) + getBufferedRouterStream(),\n )\n\n leftover = ''\n return\n }\n\n let result: RegExpExecArray | null\n let 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 } else {\n leftover = chunkString\n leftoverHtml += getBufferedRouterStream()\n }\n },\n onEnd: () => {\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 console.error('Error reading appStream:', error)\n finalPassThrough.destroy(error)\n injectedHtmlDonePromise.reject(error)\n },\n })\n\n return finalPassThrough.stream\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/router.ts
CHANGED
|
@@ -83,7 +83,7 @@ import type {
|
|
|
83
83
|
CommitLocationOptions,
|
|
84
84
|
NavigateFn,
|
|
85
85
|
} from './RouterProvider'
|
|
86
|
-
import type { Manifest } from './manifest'
|
|
86
|
+
import type { Manifest, RouterManagedTag } from './manifest'
|
|
87
87
|
import type { AnySchema, AnyValidator } from './validators'
|
|
88
88
|
import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
|
|
89
89
|
import type { NotFoundError } from './not-found'
|
|
@@ -756,7 +756,8 @@ export interface ServerSsr {
|
|
|
756
756
|
isDehydrated: () => boolean
|
|
757
757
|
onRenderFinished: (listener: () => void) => void
|
|
758
758
|
dehydrate: () => Promise<void>
|
|
759
|
-
takeBufferedScripts: () =>
|
|
759
|
+
takeBufferedScripts: () => RouterManagedTag | undefined
|
|
760
|
+
liftScriptBarrier: () => void
|
|
760
761
|
}
|
|
761
762
|
|
|
762
763
|
export type AnyRouterWithContext<TContext> = RouterCore<
|
|
@@ -2096,7 +2097,6 @@ export class RouterCore<
|
|
|
2096
2097
|
updateMatch: this.updateMatch,
|
|
2097
2098
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
2098
2099
|
onReady: async () => {
|
|
2099
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
2100
2100
|
// Wrap batch in framework-specific transition wrapper (e.g., Solid's startTransition)
|
|
2101
2101
|
this.startTransition(() => {
|
|
2102
2102
|
this.startViewTransition(async () => {
|
package/src/ssr/ssr-server.ts
CHANGED
|
@@ -5,12 +5,13 @@ import minifiedTsrBootStrapScript from './tsrScript?script-string'
|
|
|
5
5
|
import { GLOBAL_TSR } from './constants'
|
|
6
6
|
import { defaultSerovalPlugins } from './serializer/seroval-plugins'
|
|
7
7
|
import { makeSsrSerovalPlugin } from './serializer/transformer'
|
|
8
|
+
import { TSR_SCRIPT_BARRIER_ID } from './transformStreamWithRouter'
|
|
9
|
+
import type { AnySerializationAdapter } from './serializer/transformer'
|
|
8
10
|
import type { AnyRouter } from '../router'
|
|
9
11
|
import type { DehydratedMatch } from './ssr-client'
|
|
10
12
|
import type { DehydratedRouter } from './client'
|
|
11
13
|
import type { AnyRouteMatch } from '../Matches'
|
|
12
|
-
import type { Manifest } from '../manifest'
|
|
13
|
-
import type { AnySerializationAdapter } from './serializer/transformer'
|
|
14
|
+
import type { Manifest, RouterManagedTag } from '../manifest'
|
|
14
15
|
|
|
15
16
|
declare module '../router' {
|
|
16
17
|
interface ServerSsr {
|
|
@@ -140,8 +141,21 @@ export function attachRouterServerSsrUtils({
|
|
|
140
141
|
}
|
|
141
142
|
const matches = matchesToDehydrate.map(dehydrateMatch)
|
|
142
143
|
|
|
144
|
+
let manifestToDehydrate: Manifest | undefined = undefined
|
|
145
|
+
// only send manifest of the current routes to the client
|
|
146
|
+
if (manifest) {
|
|
147
|
+
const filteredRoutes = Object.fromEntries(
|
|
148
|
+
router.state.matches.map((k) => [
|
|
149
|
+
k.routeId,
|
|
150
|
+
manifest.routes[k.routeId],
|
|
151
|
+
]),
|
|
152
|
+
)
|
|
153
|
+
manifestToDehydrate = {
|
|
154
|
+
routes: filteredRoutes,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
143
157
|
const dehydratedRouter: DehydratedRouter = {
|
|
144
|
-
manifest:
|
|
158
|
+
manifest: manifestToDehydrate,
|
|
145
159
|
matches,
|
|
146
160
|
}
|
|
147
161
|
const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
|
|
@@ -193,8 +207,19 @@ export function attachRouterServerSsrUtils({
|
|
|
193
207
|
},
|
|
194
208
|
takeBufferedScripts() {
|
|
195
209
|
const scripts = scriptBuffer.takeAll()
|
|
210
|
+
const serverBufferedScript: RouterManagedTag = {
|
|
211
|
+
tag: 'script',
|
|
212
|
+
attrs: {
|
|
213
|
+
nonce: router.options.ssr?.nonce,
|
|
214
|
+
className: '$tsr',
|
|
215
|
+
id: TSR_SCRIPT_BARRIER_ID,
|
|
216
|
+
},
|
|
217
|
+
children: scripts,
|
|
218
|
+
}
|
|
219
|
+
return serverBufferedScript
|
|
220
|
+
},
|
|
221
|
+
liftScriptBarrier() {
|
|
196
222
|
scriptBuffer.liftBarrier()
|
|
197
|
-
return scripts
|
|
198
223
|
},
|
|
199
224
|
}
|
|
200
225
|
}
|
|
@@ -19,19 +19,17 @@ export function transformPipeableStreamWithRouter(
|
|
|
19
19
|
)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'
|
|
23
|
+
|
|
22
24
|
// regex pattern for matching closing body and html tags
|
|
23
|
-
const patternBodyStart = /(<body)/
|
|
24
25
|
const patternBodyEnd = /(<\/body>)/
|
|
25
26
|
const patternHtmlEnd = /(<\/html>)/
|
|
26
|
-
const patternHeadStart = /(<head.*?>)/
|
|
27
27
|
// regex pattern for matching closing tags
|
|
28
28
|
const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g
|
|
29
29
|
|
|
30
|
-
const textDecoder = new TextDecoder()
|
|
31
|
-
|
|
32
30
|
type ReadablePassthrough = {
|
|
33
31
|
stream: ReadableStream
|
|
34
|
-
write: (chunk:
|
|
32
|
+
write: (chunk: unknown) => void
|
|
35
33
|
end: (chunk?: string) => void
|
|
36
34
|
destroy: (error: unknown) => void
|
|
37
35
|
destroyed: boolean
|
|
@@ -49,11 +47,15 @@ function createPassthrough() {
|
|
|
49
47
|
const res: ReadablePassthrough = {
|
|
50
48
|
stream,
|
|
51
49
|
write: (chunk) => {
|
|
52
|
-
|
|
50
|
+
if (typeof chunk === 'string') {
|
|
51
|
+
controller.enqueue(encoder.encode(chunk))
|
|
52
|
+
} else {
|
|
53
|
+
controller.enqueue(chunk)
|
|
54
|
+
}
|
|
53
55
|
},
|
|
54
56
|
end: (chunk) => {
|
|
55
57
|
if (chunk) {
|
|
56
|
-
|
|
58
|
+
res.write(chunk)
|
|
57
59
|
}
|
|
58
60
|
controller.close()
|
|
59
61
|
res.destroyed = true
|
|
@@ -90,16 +92,20 @@ async function readStream(
|
|
|
90
92
|
export function transformStreamWithRouter(
|
|
91
93
|
router: AnyRouter,
|
|
92
94
|
appStream: ReadableStream,
|
|
95
|
+
opts?: {
|
|
96
|
+
timeoutMs?: number
|
|
97
|
+
},
|
|
93
98
|
) {
|
|
94
99
|
const finalPassThrough = createPassthrough()
|
|
100
|
+
const textDecoder = new TextDecoder()
|
|
95
101
|
|
|
96
102
|
let isAppRendering = true as boolean
|
|
97
103
|
let routerStreamBuffer = ''
|
|
98
104
|
let pendingClosingTags = ''
|
|
99
|
-
let
|
|
100
|
-
let headStarted = false as boolean
|
|
105
|
+
let streamBarrierLifted = false as boolean
|
|
101
106
|
let leftover = ''
|
|
102
107
|
let leftoverHtml = ''
|
|
108
|
+
let timeoutHandle: NodeJS.Timeout
|
|
103
109
|
|
|
104
110
|
function getBufferedRouterStream() {
|
|
105
111
|
const html = routerStreamBuffer
|
|
@@ -109,7 +115,7 @@ export function transformStreamWithRouter(
|
|
|
109
115
|
|
|
110
116
|
function decodeChunk(chunk: unknown): string {
|
|
111
117
|
if (chunk instanceof Uint8Array) {
|
|
112
|
-
return textDecoder.decode(chunk)
|
|
118
|
+
return textDecoder.decode(chunk, { stream: true })
|
|
113
119
|
}
|
|
114
120
|
return String(chunk)
|
|
115
121
|
}
|
|
@@ -136,7 +142,7 @@ export function transformStreamWithRouter(
|
|
|
136
142
|
|
|
137
143
|
promise
|
|
138
144
|
.then((html) => {
|
|
139
|
-
if (
|
|
145
|
+
if (isAppRendering) {
|
|
140
146
|
routerStreamBuffer += html
|
|
141
147
|
} else {
|
|
142
148
|
finalPassThrough.write(html)
|
|
@@ -147,7 +153,6 @@ export function transformStreamWithRouter(
|
|
|
147
153
|
processingCount--
|
|
148
154
|
|
|
149
155
|
if (!isAppRendering && processingCount === 0) {
|
|
150
|
-
stopListeningToInjectedHtml()
|
|
151
156
|
injectedHtmlDonePromise.resolve()
|
|
152
157
|
}
|
|
153
158
|
})
|
|
@@ -155,6 +160,7 @@ export function transformStreamWithRouter(
|
|
|
155
160
|
|
|
156
161
|
injectedHtmlDonePromise
|
|
157
162
|
.then(() => {
|
|
163
|
+
clearTimeout(timeoutHandle)
|
|
158
164
|
const finalHtml =
|
|
159
165
|
leftoverHtml + getBufferedRouterStream() + pendingClosingTags
|
|
160
166
|
|
|
@@ -164,44 +170,26 @@ export function transformStreamWithRouter(
|
|
|
164
170
|
console.error('Error reading routerStream:', err)
|
|
165
171
|
finalPassThrough.destroy(err)
|
|
166
172
|
})
|
|
173
|
+
.finally(stopListeningToInjectedHtml)
|
|
167
174
|
|
|
168
175
|
// Transform the appStream
|
|
169
176
|
readStream(appStream, {
|
|
170
177
|
onData: (chunk) => {
|
|
171
178
|
const text = decodeChunk(chunk.value)
|
|
172
|
-
|
|
173
|
-
let chunkString = leftover + text
|
|
179
|
+
const chunkString = leftover + text
|
|
174
180
|
const bodyEndMatch = chunkString.match(patternBodyEnd)
|
|
175
181
|
const htmlEndMatch = chunkString.match(patternHtmlEnd)
|
|
176
182
|
|
|
177
|
-
if (!
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!headStarted) {
|
|
185
|
-
const headStartMatch = chunkString.match(patternHeadStart)
|
|
186
|
-
if (headStartMatch) {
|
|
187
|
-
headStarted = true
|
|
188
|
-
const index = headStartMatch.index!
|
|
189
|
-
const headTag = headStartMatch[0]
|
|
190
|
-
const remaining = chunkString.slice(index + headTag.length)
|
|
191
|
-
finalPassThrough.write(
|
|
192
|
-
chunkString.slice(0, index) + headTag + getBufferedRouterStream(),
|
|
193
|
-
)
|
|
194
|
-
// make sure to only write `remaining` until the next closing tag
|
|
195
|
-
chunkString = remaining
|
|
183
|
+
if (!streamBarrierLifted) {
|
|
184
|
+
const streamBarrierIdIncluded = chunkString.includes(
|
|
185
|
+
TSR_SCRIPT_BARRIER_ID,
|
|
186
|
+
)
|
|
187
|
+
if (streamBarrierIdIncluded) {
|
|
188
|
+
streamBarrierLifted = true
|
|
189
|
+
router.serverSsr!.liftScriptBarrier()
|
|
196
190
|
}
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
if (!bodyStarted) {
|
|
200
|
-
finalPassThrough.write(chunkString)
|
|
201
|
-
leftover = ''
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
|
|
205
193
|
// If either the body end or html end is in the chunk,
|
|
206
194
|
// We need to get all of our data in asap
|
|
207
195
|
if (
|
|
@@ -247,11 +235,19 @@ export function transformStreamWithRouter(
|
|
|
247
235
|
// If there are no pending promises, resolve the injectedHtmlDonePromise
|
|
248
236
|
if (processingCount === 0) {
|
|
249
237
|
injectedHtmlDonePromise.resolve()
|
|
238
|
+
} else {
|
|
239
|
+
const timeoutMs = opts?.timeoutMs ?? 60000
|
|
240
|
+
timeoutHandle = setTimeout(() => {
|
|
241
|
+
injectedHtmlDonePromise.reject(
|
|
242
|
+
new Error('Injected HTML timeout after app render finished'),
|
|
243
|
+
)
|
|
244
|
+
}, timeoutMs)
|
|
250
245
|
}
|
|
251
246
|
},
|
|
252
247
|
onError: (error) => {
|
|
253
248
|
console.error('Error reading appStream:', error)
|
|
254
249
|
finalPassThrough.destroy(error)
|
|
250
|
+
injectedHtmlDonePromise.reject(error)
|
|
255
251
|
},
|
|
256
252
|
})
|
|
257
253
|
|