@tanstack/router-core 1.139.11 → 1.139.13
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 +7 -2
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/ssr/createRequestHandler.cjs +32 -24
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +19 -3
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.d.cts +1 -0
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +29 -15
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/esm/router.js +8 -3
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/createRequestHandler.js +32 -24
- package/dist/esm/ssr/createRequestHandler.js.map +1 -1
- package/dist/esm/ssr/ssr-server.d.ts +1 -0
- package/dist/esm/ssr/ssr-server.js +19 -3
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +29 -15
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +16 -2
- package/src/ssr/createRequestHandler.ts +40 -26
- package/src/ssr/ssr-server.ts +28 -4
- package/src/ssr/transformStreamWithRouter.ts +46 -19
|
@@ -8,30 +8,38 @@ function createRequestHandler({
|
|
|
8
8
|
}) {
|
|
9
9
|
return async (cb) => {
|
|
10
10
|
const router = createRouter();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
router
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
let cbWillCleanup = false;
|
|
12
|
+
try {
|
|
13
|
+
attachRouterServerSsrUtils({
|
|
14
|
+
router,
|
|
15
|
+
manifest: await getRouterManifest?.()
|
|
16
|
+
});
|
|
17
|
+
const url = new URL(request.url, "http://localhost");
|
|
18
|
+
const origin = getOrigin(request);
|
|
19
|
+
const href = url.href.replace(url.origin, "");
|
|
20
|
+
const history = createMemoryHistory({
|
|
21
|
+
initialEntries: [href]
|
|
22
|
+
});
|
|
23
|
+
router.update({
|
|
24
|
+
history,
|
|
25
|
+
origin: router.options.origin ?? origin
|
|
26
|
+
});
|
|
27
|
+
await router.load();
|
|
28
|
+
await router.serverSsr?.dehydrate();
|
|
29
|
+
const responseHeaders = getRequestHeaders({
|
|
30
|
+
router
|
|
31
|
+
});
|
|
32
|
+
cbWillCleanup = true;
|
|
33
|
+
return cb({
|
|
34
|
+
request,
|
|
35
|
+
router,
|
|
36
|
+
responseHeaders
|
|
37
|
+
});
|
|
38
|
+
} finally {
|
|
39
|
+
if (!cbWillCleanup) {
|
|
40
|
+
router.serverSsr?.cleanup();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
45
|
function getRequestHeaders(opts) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createRequestHandler.js","sources":["../../../src/ssr/createRequestHandler.ts"],"sourcesContent":["import { createMemoryHistory } from '@tanstack/history'\nimport { mergeHeaders } from './headers'\nimport { attachRouterServerSsrUtils, getOrigin } from './ssr-server'\nimport type { HandlerCallback } from './handlerCallback'\nimport type { AnyRouter } from '../router'\nimport type { Manifest } from '../manifest'\n\nexport type RequestHandler<TRouter extends AnyRouter> = (\n cb: HandlerCallback<TRouter>,\n) => Promise<Response>\n\nexport function createRequestHandler<TRouter extends AnyRouter>({\n createRouter,\n request,\n getRouterManifest,\n}: {\n createRouter: () => TRouter\n request: Request\n getRouterManifest?: () => Manifest | Promise<Manifest>\n}): RequestHandler<TRouter> {\n return async (cb) => {\n const router = createRouter()\n\n attachRouterServerSsrUtils({\n
|
|
1
|
+
{"version":3,"file":"createRequestHandler.js","sources":["../../../src/ssr/createRequestHandler.ts"],"sourcesContent":["import { createMemoryHistory } from '@tanstack/history'\nimport { mergeHeaders } from './headers'\nimport { attachRouterServerSsrUtils, getOrigin } from './ssr-server'\nimport type { HandlerCallback } from './handlerCallback'\nimport type { AnyRouter } from '../router'\nimport type { Manifest } from '../manifest'\n\nexport type RequestHandler<TRouter extends AnyRouter> = (\n cb: HandlerCallback<TRouter>,\n) => Promise<Response>\n\nexport function createRequestHandler<TRouter extends AnyRouter>({\n createRouter,\n request,\n getRouterManifest,\n}: {\n createRouter: () => TRouter\n request: Request\n getRouterManifest?: () => Manifest | Promise<Manifest>\n}): RequestHandler<TRouter> {\n return async (cb) => {\n const router = createRouter()\n // Track whether the callback will handle cleanup\n let cbWillCleanup = false\n\n try {\n attachRouterServerSsrUtils({\n router,\n manifest: await getRouterManifest?.(),\n })\n\n const url = new URL(request.url, 'http://localhost')\n const origin = getOrigin(request)\n const href = url.href.replace(url.origin, '')\n\n // Create a history for the router\n const history = createMemoryHistory({\n initialEntries: [href],\n })\n\n // Update the router with the history and context\n router.update({\n history,\n origin: router.options.origin ?? origin,\n })\n\n await router.load()\n\n await router.serverSsr?.dehydrate()\n\n const responseHeaders = getRequestHeaders({\n router,\n })\n\n // Mark that the callback will handle cleanup\n cbWillCleanup = true\n return cb({\n request,\n router,\n responseHeaders,\n } as any)\n } finally {\n if (!cbWillCleanup) {\n // Clean up router SSR state if the callback won't handle it\n // (e.g., if an error occurred before the callback was invoked).\n // When the callback runs, it handles cleanup (either via transformStreamWithRouter\n // for streaming, or directly in renderRouterToString for non-streaming).\n router.serverSsr?.cleanup()\n }\n }\n }\n}\n\nfunction getRequestHeaders(opts: { router: AnyRouter }): Headers {\n let headers = mergeHeaders(\n {\n 'Content-Type': 'text/html; charset=UTF-8',\n },\n ...opts.router.state.matches.map((match) => {\n return match.headers\n }),\n )\n\n // Handle Redirects\n const { redirect } = opts.router.state\n\n if (redirect) {\n headers = mergeHeaders(headers, redirect.headers)\n }\n\n return headers\n}\n"],"names":[],"mappings":";;;AAWO,SAAS,qBAAgD;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AACF,GAI4B;AAC1B,SAAO,OAAO,OAAO;AACnB,UAAM,SAAS,aAAA;AAEf,QAAI,gBAAgB;AAEpB,QAAI;AACF,iCAA2B;AAAA,QACzB;AAAA,QACA,UAAU,MAAM,oBAAA;AAAA,MAAoB,CACrC;AAED,YAAM,MAAM,IAAI,IAAI,QAAQ,KAAK,kBAAkB;AACnD,YAAM,SAAS,UAAU,OAAO;AAChC,YAAM,OAAO,IAAI,KAAK,QAAQ,IAAI,QAAQ,EAAE;AAG5C,YAAM,UAAU,oBAAoB;AAAA,QAClC,gBAAgB,CAAC,IAAI;AAAA,MAAA,CACtB;AAGD,aAAO,OAAO;AAAA,QACZ;AAAA,QACA,QAAQ,OAAO,QAAQ,UAAU;AAAA,MAAA,CAClC;AAED,YAAM,OAAO,KAAA;AAEb,YAAM,OAAO,WAAW,UAAA;AAExB,YAAM,kBAAkB,kBAAkB;AAAA,QACxC;AAAA,MAAA,CACD;AAGD,sBAAgB;AAChB,aAAO,GAAG;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MAAA,CACM;AAAA,IACV,UAAA;AACE,UAAI,CAAC,eAAe;AAKlB,eAAO,WAAW,QAAA;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,MAAsC;AAC/D,MAAI,UAAU;AAAA,IACZ;AAAA,MACE,gBAAgB;AAAA,IAAA;AAAA,IAElB,GAAG,KAAK,OAAO,MAAM,QAAQ,IAAI,CAAC,UAAU;AAC1C,aAAO,MAAM;AAAA,IACf,CAAC;AAAA,EAAA;AAIH,QAAM,EAAE,SAAA,IAAa,KAAK,OAAO;AAEjC,MAAI,UAAU;AACZ,cAAU,aAAa,SAAS,SAAS,OAAO;AAAA,EAClD;AAEA,SAAO;AACT;"}
|
|
@@ -32,11 +32,13 @@ const INITIAL_SCRIPTS = [
|
|
|
32
32
|
];
|
|
33
33
|
class ScriptBuffer {
|
|
34
34
|
constructor(router) {
|
|
35
|
-
this.router = router;
|
|
36
35
|
this._queue = [...INITIAL_SCRIPTS];
|
|
37
36
|
this._scriptBarrierLifted = false;
|
|
37
|
+
this._cleanedUp = false;
|
|
38
|
+
this.router = router;
|
|
38
39
|
}
|
|
39
40
|
enqueue(script) {
|
|
41
|
+
if (this._cleanedUp) return;
|
|
40
42
|
if (this._scriptBarrierLifted && this._queue.length === 0) {
|
|
41
43
|
queueMicrotask(() => {
|
|
42
44
|
this.injectBufferedScripts();
|
|
@@ -45,7 +47,7 @@ class ScriptBuffer {
|
|
|
45
47
|
this._queue.push(script);
|
|
46
48
|
}
|
|
47
49
|
liftBarrier() {
|
|
48
|
-
if (this._scriptBarrierLifted) return;
|
|
50
|
+
if (this._scriptBarrierLifted || this._cleanedUp) return;
|
|
49
51
|
this._scriptBarrierLifted = true;
|
|
50
52
|
if (this._queue.length > 0) {
|
|
51
53
|
queueMicrotask(() => {
|
|
@@ -64,11 +66,17 @@ class ScriptBuffer {
|
|
|
64
66
|
return joinedScripts;
|
|
65
67
|
}
|
|
66
68
|
injectBufferedScripts() {
|
|
69
|
+
if (this._cleanedUp) return;
|
|
67
70
|
const scriptsToInject = this.takeAll();
|
|
68
|
-
if (scriptsToInject) {
|
|
71
|
+
if (scriptsToInject && this.router?.serverSsr) {
|
|
69
72
|
this.router.serverSsr.injectScript(() => scriptsToInject);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
cleanup() {
|
|
76
|
+
this._cleanedUp = true;
|
|
77
|
+
this._queue = [];
|
|
78
|
+
this.router = void 0;
|
|
79
|
+
}
|
|
72
80
|
}
|
|
73
81
|
function attachRouterServerSsrUtils({
|
|
74
82
|
router,
|
|
@@ -161,6 +169,7 @@ function attachRouterServerSsrUtils({
|
|
|
161
169
|
onRenderFinished: (listener) => listeners.push(listener),
|
|
162
170
|
setRenderFinished: () => {
|
|
163
171
|
listeners.forEach((l) => l());
|
|
172
|
+
listeners.length = 0;
|
|
164
173
|
scriptBuffer.liftBarrier();
|
|
165
174
|
},
|
|
166
175
|
takeBufferedScripts() {
|
|
@@ -178,6 +187,13 @@ function attachRouterServerSsrUtils({
|
|
|
178
187
|
},
|
|
179
188
|
liftScriptBarrier() {
|
|
180
189
|
scriptBuffer.liftBarrier();
|
|
190
|
+
},
|
|
191
|
+
cleanup() {
|
|
192
|
+
if (!router.serverSsr) return;
|
|
193
|
+
listeners.length = 0;
|
|
194
|
+
scriptBuffer.cleanup();
|
|
195
|
+
router.serverSsr.injectedHtml = [];
|
|
196
|
+
router.serverSsr = void 0;
|
|
181
197
|
}
|
|
182
198
|
};
|
|
183
199
|
}
|
|
@@ -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 { 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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"}
|
|
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 cleanup: () => 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 private router: AnyRouter | undefined\n private _queue: Array<string> = [...INITIAL_SCRIPTS]\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n\n constructor(router: AnyRouter) {\n this.router = router\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\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 || this._cleanedUp) 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 if (this._cleanedUp) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(() => scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n 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 // Clear listeners after calling them to prevent memory leaks\n listeners.length = 0\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 cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n listeners.length = 0\n scriptBuffer.cleanup()\n router.serverSsr.injectedHtml = []\n router.serverSsr = undefined\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":";;;;;;;;AA4BA,MAAM,WAAW;AAEV,SAAS,eAAe,OAAuC;AACpE,QAAM,kBAAmC;AAAA,IACvC,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,EAAA;AAGX,QAAM,aAAa;AAAA,IACjB,CAAC,uBAAuB,GAAG;AAAA,IAC3B,CAAC,cAAc,GAAG;AAAA,IAClB,CAAC,SAAS,GAAG;AAAA,IACb,CAAC,OAAO,KAAK;AAAA,EAAA;AAGf,aAAW,CAAC,KAAK,SAAS,KAAK,YAAY;AACzC,QAAI,MAAM,GAAG,MAAM,QAAW;AAC5B,sBAAgB,SAAS,IAAI,MAAM,GAAG;AAAA,IACxC;AAAA,EACF;AACA,SAAO;AACT;AAEA,MAAM,kBAAkB;AAAA,EACtB,wBAAwB,QAAQ;AAAA,EAChC;AACF;AAEA,MAAM,aAAa;AAAA,EAMjB,YAAY,QAAmB;AAJ/B,SAAQ,SAAwB,CAAC,GAAG,eAAe;AACnD,SAAQ,uBAAuB;AAC/B,SAAQ,aAAa;AAGnB,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,QAAQ,QAAgB;AACtB,QAAI,KAAK,WAAY;AACrB,QAAI,KAAK,wBAAwB,KAAK,OAAO,WAAW,GAAG;AACzD,qBAAe,MAAM;AACnB,aAAK,sBAAA;AAAA,MACP,CAAC;AAAA,IACH;AACA,SAAK,OAAO,KAAK,MAAM;AAAA,EACzB;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,wBAAwB,KAAK,WAAY;AAClD,SAAK,uBAAuB;AAC5B,QAAI,KAAK,OAAO,SAAS,GAAG;AAC1B,qBAAe,MAAM;AACnB,aAAK,sBAAA;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,UAAU;AACR,UAAM,kBAAkB,KAAK;AAC7B,SAAK,SAAS,CAAA;AACd,QAAI,gBAAgB,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AACA,oBAAgB,KAAK,GAAG,UAAU,MAAM;AACxC,UAAM,gBAAgB,gBAAgB,KAAK,GAAG;AAC9C,WAAO;AAAA,EACT;AAAA,EAEA,wBAAwB;AACtB,QAAI,KAAK,WAAY;AACrB,UAAM,kBAAkB,KAAK,QAAA;AAC7B,QAAI,mBAAmB,KAAK,QAAQ,WAAW;AAC7C,WAAK,OAAO,UAAU,aAAa,MAAM,eAAe;AAAA,IAC1D;AAAA,EACF;AAAA,EAEA,UAAU;AACR,SAAK,aAAa;AAClB,SAAK,SAAS,CAAA;AACd,SAAK,SAAS;AAAA,EAChB;AACF;AAEO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AACF,GAGG;AACD,SAAO,MAAM;AAAA,IACX;AAAA,EAAA;AAEF,MAAI,cAAc;AAClB,QAAM,YAA+B,CAAA;AACrC,QAAM,eAAe,IAAI,aAAa,MAAM;AAE5C,SAAO,YAAY;AAAA,IACjB,cAAc,CAAA;AAAA,IACd,YAAY,CAAC,YAAY;AACvB,YAAM,UAAU,QAAQ,QAAA,EAAU,KAAK,OAAO;AAC9C,aAAO,UAAW,aAAa,KAAK,OAAO;AAC3C,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN;AAAA,MAAA,CACD;AAED,aAAO,QAAQ,KAAK,MAAM;AAAA,MAAC,CAAC;AAAA,IAC9B;AAAA,IACA,cAAc,CAAC,cAAc;AAC3B,aAAO,OAAO,UAAW,WAAW,YAAY;AAC9C,cAAM,SAAS,MAAM,UAAA;AACrB,YAAI,CAAC,QAAQ;AACX,iBAAO;AAAA,QACT;AACA,eAAO,UAAU,OAAO,QAAQ,KAAK,QAAQ,WAAW,OAAO,QAAQ,IAAI,KAAK,MAAM,EAAE,iBAAiB,MAAM;AAAA,MACjH,CAAC;AAAA,IACH;AAAA,IACA,WAAW,YAAY;AACrB,gBAAU,CAAC,aAAa,+BAA+B;AACvD,UAAI,qBAAqB,OAAO,MAAM;AACtC,UAAI,OAAO,WAAW;AAEpB,6BAAqB,mBAAmB,MAAM,GAAG,CAAC;AAAA,MACpD;AACA,YAAM,UAAU,mBAAmB,IAAI,cAAc;AAErD,UAAI,sBAA4C;AAEhD,UAAI,UAAU;AACZ,cAAM,iBAAiB,OAAO;AAAA,UAC5B,OAAO,MAAM,QAAQ,IAAI,CAAC,MAAM;AAAA,YAC9B,EAAE;AAAA,YACF,SAAS,OAAO,EAAE,OAAO;AAAA,UAAA,CAC1B;AAAA,QAAA;AAEH,8BAAsB;AAAA,UACpB,QAAQ;AAAA,QAAA;AAAA,MAEZ;AACA,YAAM,mBAAqC;AAAA,QACzC,UAAU;AAAA,QACV;AAAA,MAAA;AAEF,YAAM,cAAc,mBAAmB,mBAAmB,SAAS,CAAC,GAAG;AACvE,UAAI,aAAa;AACf,yBAAiB,cAAc;AAAA,MACjC;AACA,YAAM,iBAAiB,MAAM,OAAO,QAAQ,YAAA;AAC5C,UAAI,gBAAgB;AAClB,yBAAiB,iBAAiB;AAAA,MACpC;AACA,oBAAc;AAEd,YAAM,IAAI,wBAAA;AACV,YAAM,eAAe,EAAE,QAAQ,MAAA;AAC/B,YAAM,UAEF,OAAO,QAAQ,uBAGd,IAAI,CAAC,MAAM,qBAAqB,GAAG,YAAY,CAAC,KAAK,CAAA;AAE1D,2BAAqB,kBAAkB;AAAA,QACrC,0BAAU,IAAA;AAAA,QACV,SAAS,CAAC,GAAG,SAAS,GAAG,qBAAqB;AAAA,QAC9C,aAAa,CAAC,MAAM,YAAY;AAC9B,cAAI,aAAa,UAAU,aAAa,aAAa,OAAO;AAC5D,cAAI,aAAa,QAAQ;AACvB,yBAAa,aAAa,YAAY,aAAa;AAAA,UACrD;AACA,uBAAa,QAAQ,UAAU;AAAA,QACjC;AAAA,QACA,SAAS;AAAA,QACT,QAAQ,MAAM;AACZ,uBAAa,QAAQ,aAAa,iBAAiB;AACnD,YAAE,QAAQ,EAAE;AAAA,QACd;AAAA,QACA,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG;AAAA,MAAA,CAC/B;AAED,aAAO,UAAW,WAAW,MAAM,CAAC;AAAA,IACtC;AAAA,IACA,eAAe;AACb,aAAO;AAAA,IACT;AAAA,IACA,kBAAkB,CAAC,aAAa,UAAU,KAAK,QAAQ;AAAA,IACvD,mBAAmB,MAAM;AACvB,gBAAU,QAAQ,CAAC,MAAM,EAAA,CAAG;AAE5B,gBAAU,SAAS;AACnB,mBAAa,YAAA;AAAA,IACf;AAAA,IACA,sBAAsB;AACpB,YAAM,UAAU,aAAa,QAAA;AAC7B,YAAM,uBAAyC;AAAA,QAC7C,KAAK;AAAA,QACL,OAAO;AAAA,UACL,OAAO,OAAO,QAAQ,KAAK;AAAA,UAC3B,WAAW;AAAA,UACX,IAAI;AAAA,QAAA;AAAA,QAEN,UAAU;AAAA,MAAA;AAEZ,aAAO;AAAA,IACT;AAAA,IACA,oBAAoB;AAClB,mBAAa,YAAA;AAAA,IACf;AAAA,IACA,UAAU;AAER,UAAI,CAAC,OAAO,UAAW;AACvB,gBAAU,SAAS;AACnB,mBAAa,QAAA;AACb,aAAO,UAAU,eAAe,CAAA;AAChC,aAAO,YAAY;AAAA,IACrB;AAAA,EAAA;AAEJ;AAEO,SAAS,UAAU,SAAkB;AAC1C,QAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ;AACjD,MAAI,cAAc;AAChB,QAAI;AACF,UAAI,IAAI,YAAY;AACpB,aAAO;AAAA,IACT,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,MAAI;AACF,WAAO,IAAI,IAAI,QAAQ,GAAG,EAAE;AAAA,EAC9B,QAAQ;AAAA,EAAC;AACT,SAAO;AACT;"}
|
|
@@ -22,12 +22,13 @@ function createPassthrough(onCancel) {
|
|
|
22
22
|
},
|
|
23
23
|
cancel() {
|
|
24
24
|
res.destroyed = true;
|
|
25
|
-
onCancel
|
|
25
|
+
onCancel();
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
28
|
const res = {
|
|
29
29
|
stream,
|
|
30
30
|
write: (chunk) => {
|
|
31
|
+
if (res.destroyed) return;
|
|
31
32
|
if (typeof chunk === "string") {
|
|
32
33
|
controller.enqueue(encoder.encode(chunk));
|
|
33
34
|
} else {
|
|
@@ -35,13 +36,15 @@ function createPassthrough(onCancel) {
|
|
|
35
36
|
}
|
|
36
37
|
},
|
|
37
38
|
end: (chunk) => {
|
|
39
|
+
if (res.destroyed) return;
|
|
38
40
|
if (chunk) {
|
|
39
41
|
res.write(chunk);
|
|
40
42
|
}
|
|
41
|
-
controller.close();
|
|
42
43
|
res.destroyed = true;
|
|
44
|
+
controller.close();
|
|
43
45
|
},
|
|
44
46
|
destroy: (error) => {
|
|
47
|
+
if (res.destroyed) return;
|
|
45
48
|
res.destroyed = true;
|
|
46
49
|
controller.error(error);
|
|
47
50
|
},
|
|
@@ -66,13 +69,18 @@ async function readStream(stream, opts) {
|
|
|
66
69
|
function transformStreamWithRouter(router, appStream, opts) {
|
|
67
70
|
let stopListeningToInjectedHtml = void 0;
|
|
68
71
|
let timeoutHandle;
|
|
69
|
-
|
|
72
|
+
let cleanedUp = false;
|
|
73
|
+
function cleanup() {
|
|
74
|
+
if (cleanedUp) return;
|
|
75
|
+
cleanedUp = true;
|
|
70
76
|
if (stopListeningToInjectedHtml) {
|
|
71
77
|
stopListeningToInjectedHtml();
|
|
72
78
|
stopListeningToInjectedHtml = void 0;
|
|
73
79
|
}
|
|
74
80
|
clearTimeout(timeoutHandle);
|
|
75
|
-
|
|
81
|
+
router.serverSsr?.cleanup();
|
|
82
|
+
}
|
|
83
|
+
const finalPassThrough = createPassthrough(cleanup);
|
|
76
84
|
const textDecoder = new TextDecoder();
|
|
77
85
|
let isAppRendering = true;
|
|
78
86
|
let routerStreamBuffer = "";
|
|
@@ -99,10 +107,11 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
99
107
|
handleInjectedHtml
|
|
100
108
|
);
|
|
101
109
|
function handleInjectedHtml() {
|
|
110
|
+
if (cleanedUp) return;
|
|
102
111
|
router.serverSsr.injectedHtml.forEach((promise) => {
|
|
103
112
|
processingCount++;
|
|
104
113
|
promise.then((html) => {
|
|
105
|
-
if (finalPassThrough.destroyed) {
|
|
114
|
+
if (cleanedUp || finalPassThrough.destroyed) {
|
|
106
115
|
return;
|
|
107
116
|
}
|
|
108
117
|
if (isAppRendering) {
|
|
@@ -111,9 +120,7 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
111
120
|
finalPassThrough.write(html);
|
|
112
121
|
}
|
|
113
122
|
}).catch((err) => {
|
|
114
|
-
|
|
115
|
-
injectedHtmlDonePromise.reject(err);
|
|
116
|
-
}
|
|
123
|
+
injectedHtmlDonePromise.reject(err);
|
|
117
124
|
}).finally(() => {
|
|
118
125
|
processingCount--;
|
|
119
126
|
if (!isAppRendering && processingCount === 0) {
|
|
@@ -124,6 +131,9 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
124
131
|
router.serverSsr.injectedHtml = [];
|
|
125
132
|
}
|
|
126
133
|
injectedHtmlDonePromise.then(() => {
|
|
134
|
+
if (cleanedUp || finalPassThrough.destroyed) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
127
137
|
clearTimeout(timeoutHandle);
|
|
128
138
|
const finalHtml = leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags;
|
|
129
139
|
leftover = "";
|
|
@@ -131,16 +141,17 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
131
141
|
pendingClosingTags = "";
|
|
132
142
|
finalPassThrough.end(finalHtml);
|
|
133
143
|
}).catch((err) => {
|
|
144
|
+
if (cleanedUp || finalPassThrough.destroyed) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
134
147
|
console.error("Error reading routerStream:", err);
|
|
135
148
|
finalPassThrough.destroy(err);
|
|
136
|
-
}).finally(
|
|
137
|
-
if (stopListeningToInjectedHtml) {
|
|
138
|
-
stopListeningToInjectedHtml();
|
|
139
|
-
stopListeningToInjectedHtml = void 0;
|
|
140
|
-
}
|
|
141
|
-
});
|
|
149
|
+
}).finally(cleanup);
|
|
142
150
|
readStream(appStream, {
|
|
143
151
|
onData: (chunk) => {
|
|
152
|
+
if (cleanedUp || finalPassThrough.destroyed) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
144
155
|
const text = decodeChunk(chunk.value);
|
|
145
156
|
const chunkString = leftover + text;
|
|
146
157
|
const bodyEndMatch = chunkString.match(patternBodyEnd);
|
|
@@ -181,7 +192,7 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
181
192
|
}
|
|
182
193
|
},
|
|
183
194
|
onEnd: () => {
|
|
184
|
-
if (finalPassThrough.destroyed) {
|
|
195
|
+
if (cleanedUp || finalPassThrough.destroyed) {
|
|
185
196
|
return;
|
|
186
197
|
}
|
|
187
198
|
isAppRendering = false;
|
|
@@ -198,6 +209,9 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
198
209
|
}
|
|
199
210
|
},
|
|
200
211
|
onError: (error) => {
|
|
212
|
+
if (cleanedUp) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
201
215
|
console.error("Error reading appStream:", error);
|
|
202
216
|
isAppRendering = false;
|
|
203
217
|
router.serverSsr.setRenderFinished();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { createControlledPromise } from '../utils'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\nexport const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'\n\n// regex pattern for matching closing body and html tags\nconst patternBodyEnd = /(<\\/body>)/\nconst patternHtmlEnd = /(<\\/html>)/\n// regex pattern for matching closing tags\nconst patternClosingTag = /(<\\/[a-zA-Z][\\w:.-]*?>)/g\n\ntype ReadablePassthrough = {\n stream: ReadableStream\n write: (chunk: unknown) => void\n end: (chunk?: string) => void\n destroy: (error: unknown) => void\n destroyed: boolean\n}\n\nfunction createPassthrough(onCancel?: () => void) {\n let controller: ReadableStreamDefaultController<any>\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n res.destroyed = true\n onCancel?.()\n },\n })\n\n const res: ReadablePassthrough = {\n stream,\n write: (chunk) => {\n 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 res.destroyed = true\n controller.error(error)\n },\n destroyed: false,\n }\n\n return res\n}\n\nasync function readStream(\n stream: ReadableStream,\n opts: {\n onData?: (chunk: ReadableStreamReadValueResult<any>) => void\n onEnd?: () => void\n onError?: (error: unknown) => void\n },\n) {\n const reader = stream.getReader()\n try {\n let chunk\n while (!(chunk = await reader.read()).done) {\n opts.onData?.(chunk)\n }\n opts.onEnd?.()\n } catch (error) {\n opts.onError?.(error)\n } finally {\n reader.releaseLock()\n }\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n timeoutMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined = undefined\n let timeoutHandle: NodeJS.Timeout\n\n const finalPassThrough = createPassthrough(() => {\n if (stopListeningToInjectedHtml) {\n stopListeningToInjectedHtml()\n stopListeningToInjectedHtml = undefined\n }\n clearTimeout(timeoutHandle)\n })\n const textDecoder = new TextDecoder()\n\n let isAppRendering = true\n let routerStreamBuffer = ''\n let pendingClosingTags = ''\n let streamBarrierLifted = false\n let leftover = ''\n let leftoverHtml = ''\n\n function getBufferedRouterStream() {\n const html = routerStreamBuffer\n routerStreamBuffer = ''\n return html\n }\n\n function decodeChunk(chunk: unknown): string {\n if (chunk instanceof Uint8Array) {\n return textDecoder.decode(chunk, { stream: true })\n }\n return String(chunk)\n }\n\n const injectedHtmlDonePromise = createControlledPromise<void>()\n\n let processingCount = 0\n\n // Process any already-injected HTML\n handleInjectedHtml()\n\n // Listen for any new injected HTML\n stopListeningToInjectedHtml = router.subscribe(\n 'onInjectedHtml',\n handleInjectedHtml,\n )\n\n function handleInjectedHtml() {\n router.serverSsr!.injectedHtml.forEach((promise) => {\n processingCount++\n\n promise\n .then((html) => {\n // Don't write to destroyed stream\n if (finalPassThrough.destroyed) {\n return\n }\n if (isAppRendering) {\n routerStreamBuffer += html\n } else {\n finalPassThrough.write(html)\n }\n })\n .catch((err) => {\n // Only reject if not already settled\n if (!finalPassThrough.destroyed) {\n injectedHtmlDonePromise.reject(err)\n }\n })\n .finally(() => {\n processingCount--\n\n if (!isAppRendering && processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n }\n })\n })\n router.serverSsr!.injectedHtml = []\n }\n\n injectedHtmlDonePromise\n .then(() => {\n clearTimeout(timeoutHandle)\n const finalHtml =\n leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n leftover = ''\n leftoverHtml = ''\n pendingClosingTags = ''\n\n finalPassThrough.end(finalHtml)\n })\n .catch((err) => {\n console.error('Error reading routerStream:', err)\n finalPassThrough.destroy(err)\n })\n .finally(() => {\n if (stopListeningToInjectedHtml) {\n stopListeningToInjectedHtml()\n stopListeningToInjectedHtml = undefined\n }\n })\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) +\n getBufferedRouterStream() +\n leftoverHtml,\n )\n\n leftover = ''\n leftoverHtml = ''\n return\n }\n\n let result: RegExpExecArray | null\n let lastIndex = 0\n // Reset regex lastIndex since it's global and stateful across exec() calls\n patternClosingTag.lastIndex = 0\n while ((result = patternClosingTag.exec(chunkString)) !== null) {\n lastIndex = result.index + result[0].length\n }\n\n if (lastIndex > 0) {\n const processed =\n chunkString.slice(0, lastIndex) +\n getBufferedRouterStream() +\n leftoverHtml\n\n finalPassThrough.write(processed)\n leftover = chunkString.slice(lastIndex)\n leftoverHtml = ''\n } else {\n leftover = chunkString\n leftoverHtml += getBufferedRouterStream()\n }\n },\n onEnd: () => {\n // Don't process if stream was already destroyed/cancelled\n if (finalPassThrough.destroyed) {\n return\n }\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n\n // If there are no pending promises, resolve the injectedHtmlDonePromise\n if (processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n } else {\n const timeoutMs = opts?.timeoutMs ?? 60000\n timeoutHandle = setTimeout(() => {\n injectedHtmlDonePromise.reject(\n new Error('Injected HTML timeout after app render finished'),\n )\n }, timeoutMs)\n }\n },\n onError: (error) => {\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n // Clear timeout to prevent it from firing after error\n clearTimeout(timeoutHandle)\n // Clear string buffers to prevent memory leaks\n leftover = ''\n leftoverHtml = ''\n routerStreamBuffer = ''\n pendingClosingTags = ''\n finalPassThrough.destroy(error)\n injectedHtmlDonePromise.reject(error)\n },\n })\n\n return finalPassThrough.stream\n}\n"],"names":[],"mappings":"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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(onCancel: () => void) {\n let controller: ReadableStreamDefaultController<any>\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n res.destroyed = true\n onCancel()\n },\n })\n\n const res: ReadablePassthrough = {\n stream,\n write: (chunk) => {\n // Don't write to destroyed stream\n if (res.destroyed) return\n if (typeof chunk === 'string') {\n controller.enqueue(encoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n },\n end: (chunk) => {\n // Don't end already destroyed stream\n if (res.destroyed) return\n if (chunk) {\n res.write(chunk)\n }\n res.destroyed = true\n controller.close()\n },\n destroy: (error) => {\n // Don't destroy already destroyed stream\n if (res.destroyed) return\n res.destroyed = true\n controller.error(error)\n },\n destroyed: false,\n }\n\n return res\n}\n\nasync function readStream(\n stream: ReadableStream,\n opts: {\n onData?: (chunk: ReadableStreamReadValueResult<any>) => void\n onEnd?: () => void\n onError?: (error: unknown) => void\n },\n) {\n const reader = stream.getReader()\n try {\n let chunk\n while (!(chunk = await reader.read()).done) {\n opts.onData?.(chunk)\n }\n opts.onEnd?.()\n } catch (error) {\n opts.onError?.(error)\n } finally {\n reader.releaseLock()\n }\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n timeoutMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined = undefined\n let timeoutHandle: NodeJS.Timeout\n let cleanedUp = false\n\n function cleanup() {\n if (cleanedUp) return\n cleanedUp = true\n if (stopListeningToInjectedHtml) {\n stopListeningToInjectedHtml()\n stopListeningToInjectedHtml = undefined\n }\n clearTimeout(timeoutHandle)\n router.serverSsr?.cleanup()\n }\n\n const finalPassThrough = createPassthrough(cleanup)\n const textDecoder = new TextDecoder()\n\n let isAppRendering = true\n let routerStreamBuffer = ''\n let pendingClosingTags = ''\n let streamBarrierLifted = false\n let leftover = ''\n let leftoverHtml = ''\n\n function getBufferedRouterStream() {\n const html = routerStreamBuffer\n routerStreamBuffer = ''\n return html\n }\n\n function decodeChunk(chunk: unknown): string {\n if (chunk instanceof Uint8Array) {\n return textDecoder.decode(chunk, { stream: true })\n }\n return String(chunk)\n }\n\n const injectedHtmlDonePromise = createControlledPromise<void>()\n\n let processingCount = 0\n\n // Process any already-injected HTML\n handleInjectedHtml()\n\n // Listen for any new injected HTML\n stopListeningToInjectedHtml = router.subscribe(\n 'onInjectedHtml',\n handleInjectedHtml,\n )\n\n function handleInjectedHtml() {\n // Don't process if already cleaned up\n if (cleanedUp) return\n\n router.serverSsr!.injectedHtml.forEach((promise) => {\n processingCount++\n\n promise\n .then((html) => {\n // Don't write to destroyed stream or after cleanup\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n if (isAppRendering) {\n routerStreamBuffer += html\n } else {\n finalPassThrough.write(html)\n }\n })\n .catch((err) => {\n injectedHtmlDonePromise.reject(err)\n })\n .finally(() => {\n processingCount--\n\n if (!isAppRendering && processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n }\n })\n })\n router.serverSsr!.injectedHtml = []\n }\n\n injectedHtmlDonePromise\n .then(() => {\n // Don't process if already cleaned up or destroyed\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n clearTimeout(timeoutHandle)\n const finalHtml =\n leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n leftover = ''\n leftoverHtml = ''\n pendingClosingTags = ''\n\n finalPassThrough.end(finalHtml)\n })\n .catch((err) => {\n // Don't process if already cleaned up\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n console.error('Error reading routerStream:', err)\n finalPassThrough.destroy(err)\n })\n .finally(cleanup)\n\n // Transform the appStream\n readStream(appStream, {\n onData: (chunk) => {\n // Don't process if already cleaned up\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n const text = decodeChunk(chunk.value)\n const chunkString = leftover + text\n const bodyEndMatch = chunkString.match(patternBodyEnd)\n const htmlEndMatch = chunkString.match(patternHtmlEnd)\n\n if (!streamBarrierLifted) {\n const streamBarrierIdIncluded = chunkString.includes(\n TSR_SCRIPT_BARRIER_ID,\n )\n if (streamBarrierIdIncluded) {\n streamBarrierLifted = true\n router.serverSsr!.liftScriptBarrier()\n }\n }\n\n // If either the body end or html end is in the chunk,\n // We need to get all of our data in asap\n if (\n bodyEndMatch &&\n htmlEndMatch &&\n bodyEndMatch.index! < htmlEndMatch.index!\n ) {\n const bodyEndIndex = bodyEndMatch.index!\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n finalPassThrough.write(\n chunkString.slice(0, bodyEndIndex) +\n getBufferedRouterStream() +\n leftoverHtml,\n )\n\n leftover = ''\n leftoverHtml = ''\n return\n }\n\n let result: RegExpExecArray | null\n let lastIndex = 0\n // Reset regex lastIndex since it's global and stateful across exec() calls\n patternClosingTag.lastIndex = 0\n while ((result = patternClosingTag.exec(chunkString)) !== null) {\n lastIndex = result.index + result[0].length\n }\n\n if (lastIndex > 0) {\n const processed =\n chunkString.slice(0, lastIndex) +\n getBufferedRouterStream() +\n leftoverHtml\n\n finalPassThrough.write(processed)\n leftover = chunkString.slice(lastIndex)\n leftoverHtml = ''\n } else {\n leftover = chunkString\n leftoverHtml += getBufferedRouterStream()\n }\n },\n onEnd: () => {\n // Don't process if stream was already destroyed/cancelled or cleaned up\n if (cleanedUp || finalPassThrough.destroyed) {\n return\n }\n\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n\n // If there are no pending promises, resolve the injectedHtmlDonePromise\n if (processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n } else {\n const timeoutMs = opts?.timeoutMs ?? 60000\n timeoutHandle = setTimeout(() => {\n injectedHtmlDonePromise.reject(\n new Error('Injected HTML timeout after app render finished'),\n )\n }, timeoutMs)\n }\n },\n onError: (error) => {\n // Don't process if already cleaned up\n if (cleanedUp) {\n return\n }\n\n console.error('Error reading appStream:', error)\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n // Clear timeout to prevent it from firing after error\n clearTimeout(timeoutHandle)\n // Clear string buffers to prevent memory leaks\n leftover = ''\n leftoverHtml = ''\n routerStreamBuffer = ''\n pendingClosingTags = ''\n finalPassThrough.destroy(error)\n injectedHtmlDonePromise.reject(error)\n },\n })\n\n return finalPassThrough.stream\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/router.ts
CHANGED
|
@@ -1793,11 +1793,25 @@ export class RouterCore<
|
|
|
1793
1793
|
)
|
|
1794
1794
|
if (match) {
|
|
1795
1795
|
Object.assign(params, match.params) // Copy params, because they're cached
|
|
1796
|
-
const {
|
|
1796
|
+
const {
|
|
1797
|
+
from: _from,
|
|
1798
|
+
params: maskParams,
|
|
1799
|
+
...maskProps
|
|
1800
|
+
} = match.route
|
|
1801
|
+
|
|
1802
|
+
// If mask has a params function, call it with the matched params as context
|
|
1803
|
+
// Otherwise, use the matched params or the provided params value
|
|
1804
|
+
const nextParams =
|
|
1805
|
+
maskParams === false || maskParams === null
|
|
1806
|
+
? {}
|
|
1807
|
+
: (maskParams ?? true) === true
|
|
1808
|
+
? params
|
|
1809
|
+
: Object.assign(params, functionalUpdate(maskParams, params))
|
|
1810
|
+
|
|
1797
1811
|
maskedDest = {
|
|
1798
1812
|
from: opts.from,
|
|
1799
1813
|
...maskProps,
|
|
1800
|
-
params,
|
|
1814
|
+
params: nextParams,
|
|
1801
1815
|
}
|
|
1802
1816
|
maskedNext = build(maskedDest)
|
|
1803
1817
|
}
|
|
@@ -20,40 +20,54 @@ export function createRequestHandler<TRouter extends AnyRouter>({
|
|
|
20
20
|
}): RequestHandler<TRouter> {
|
|
21
21
|
return async (cb) => {
|
|
22
22
|
const router = createRouter()
|
|
23
|
+
// Track whether the callback will handle cleanup
|
|
24
|
+
let cbWillCleanup = false
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
try {
|
|
27
|
+
attachRouterServerSsrUtils({
|
|
28
|
+
router,
|
|
29
|
+
manifest: await getRouterManifest?.(),
|
|
30
|
+
})
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const url = new URL(request.url, 'http://localhost')
|
|
33
|
+
const origin = getOrigin(request)
|
|
34
|
+
const href = url.href.replace(url.origin, '')
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
// Create a history for the router
|
|
37
|
+
const history = createMemoryHistory({
|
|
38
|
+
initialEntries: [href],
|
|
39
|
+
})
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Update the router with the history and context
|
|
42
|
+
router.update({
|
|
43
|
+
history,
|
|
44
|
+
origin: router.options.origin ?? origin,
|
|
45
|
+
})
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
await router.load()
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
await router.serverSsr?.dehydrate()
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const responseHeaders = getRequestHeaders({
|
|
52
|
+
router,
|
|
53
|
+
})
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Mark that the callback will handle cleanup
|
|
56
|
+
cbWillCleanup = true
|
|
57
|
+
return cb({
|
|
58
|
+
request,
|
|
59
|
+
router,
|
|
60
|
+
responseHeaders,
|
|
61
|
+
} as any)
|
|
62
|
+
} finally {
|
|
63
|
+
if (!cbWillCleanup) {
|
|
64
|
+
// Clean up router SSR state if the callback won't handle it
|
|
65
|
+
// (e.g., if an error occurred before the callback was invoked).
|
|
66
|
+
// When the callback runs, it handles cleanup (either via transformStreamWithRouter
|
|
67
|
+
// for streaming, or directly in renderRouterToString for non-streaming).
|
|
68
|
+
router.serverSsr?.cleanup()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
|
package/src/ssr/ssr-server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { Manifest, RouterManagedTag } from '../manifest'
|
|
|
16
16
|
declare module '../router' {
|
|
17
17
|
interface ServerSsr {
|
|
18
18
|
setRenderFinished: () => void
|
|
19
|
+
cleanup: () => void
|
|
19
20
|
}
|
|
20
21
|
interface RouterEvents {
|
|
21
22
|
onInjectedHtml: {
|
|
@@ -55,11 +56,17 @@ const INITIAL_SCRIPTS = [
|
|
|
55
56
|
]
|
|
56
57
|
|
|
57
58
|
class ScriptBuffer {
|
|
58
|
-
|
|
59
|
+
private router: AnyRouter | undefined
|
|
59
60
|
private _queue: Array<string> = [...INITIAL_SCRIPTS]
|
|
60
61
|
private _scriptBarrierLifted = false
|
|
62
|
+
private _cleanedUp = false
|
|
63
|
+
|
|
64
|
+
constructor(router: AnyRouter) {
|
|
65
|
+
this.router = router
|
|
66
|
+
}
|
|
61
67
|
|
|
62
68
|
enqueue(script: string) {
|
|
69
|
+
if (this._cleanedUp) return
|
|
63
70
|
if (this._scriptBarrierLifted && this._queue.length === 0) {
|
|
64
71
|
queueMicrotask(() => {
|
|
65
72
|
this.injectBufferedScripts()
|
|
@@ -69,7 +76,7 @@ class ScriptBuffer {
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
liftBarrier() {
|
|
72
|
-
if (this._scriptBarrierLifted) return
|
|
79
|
+
if (this._scriptBarrierLifted || this._cleanedUp) return
|
|
73
80
|
this._scriptBarrierLifted = true
|
|
74
81
|
if (this._queue.length > 0) {
|
|
75
82
|
queueMicrotask(() => {
|
|
@@ -90,11 +97,18 @@ class ScriptBuffer {
|
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
injectBufferedScripts() {
|
|
100
|
+
if (this._cleanedUp) return
|
|
93
101
|
const scriptsToInject = this.takeAll()
|
|
94
|
-
if (scriptsToInject) {
|
|
95
|
-
this.router.serverSsr
|
|
102
|
+
if (scriptsToInject && this.router?.serverSsr) {
|
|
103
|
+
this.router.serverSsr.injectScript(() => scriptsToInject)
|
|
96
104
|
}
|
|
97
105
|
}
|
|
106
|
+
|
|
107
|
+
cleanup() {
|
|
108
|
+
this._cleanedUp = true
|
|
109
|
+
this._queue = []
|
|
110
|
+
this.router = undefined
|
|
111
|
+
}
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
export function attachRouterServerSsrUtils({
|
|
@@ -203,6 +217,8 @@ export function attachRouterServerSsrUtils({
|
|
|
203
217
|
onRenderFinished: (listener) => listeners.push(listener),
|
|
204
218
|
setRenderFinished: () => {
|
|
205
219
|
listeners.forEach((l) => l())
|
|
220
|
+
// Clear listeners after calling them to prevent memory leaks
|
|
221
|
+
listeners.length = 0
|
|
206
222
|
scriptBuffer.liftBarrier()
|
|
207
223
|
},
|
|
208
224
|
takeBufferedScripts() {
|
|
@@ -221,6 +237,14 @@ export function attachRouterServerSsrUtils({
|
|
|
221
237
|
liftScriptBarrier() {
|
|
222
238
|
scriptBuffer.liftBarrier()
|
|
223
239
|
},
|
|
240
|
+
cleanup() {
|
|
241
|
+
// Guard against multiple cleanup calls
|
|
242
|
+
if (!router.serverSsr) return
|
|
243
|
+
listeners.length = 0
|
|
244
|
+
scriptBuffer.cleanup()
|
|
245
|
+
router.serverSsr.injectedHtml = []
|
|
246
|
+
router.serverSsr = undefined
|
|
247
|
+
},
|
|
224
248
|
}
|
|
225
249
|
}
|
|
226
250
|
|