@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.
@@ -10,30 +10,38 @@ function createRequestHandler({
10
10
  }) {
11
11
  return async (cb) => {
12
12
  const router = createRouter();
13
- ssrServer.attachRouterServerSsrUtils({
14
- router,
15
- manifest: await getRouterManifest?.()
16
- });
17
- const url = new URL(request.url, "http://localhost");
18
- const origin = ssrServer.getOrigin(request);
19
- const href = url.href.replace(url.origin, "");
20
- const history$1 = history.createMemoryHistory({
21
- initialEntries: [href]
22
- });
23
- router.update({
24
- history: history$1,
25
- origin: router.options.origin ?? origin
26
- });
27
- await router.load();
28
- await router.serverSsr?.dehydrate();
29
- const responseHeaders = getRequestHeaders({
30
- router
31
- });
32
- return cb({
33
- request,
34
- router,
35
- responseHeaders
36
- });
13
+ let cbWillCleanup = false;
14
+ try {
15
+ ssrServer.attachRouterServerSsrUtils({
16
+ router,
17
+ manifest: await getRouterManifest?.()
18
+ });
19
+ const url = new URL(request.url, "http://localhost");
20
+ const origin = ssrServer.getOrigin(request);
21
+ const href = url.href.replace(url.origin, "");
22
+ const history$1 = history.createMemoryHistory({
23
+ initialEntries: [href]
24
+ });
25
+ router.update({
26
+ history: history$1,
27
+ origin: router.options.origin ?? origin
28
+ });
29
+ await router.load();
30
+ await router.serverSsr?.dehydrate();
31
+ const responseHeaders = getRequestHeaders({
32
+ router
33
+ });
34
+ cbWillCleanup = true;
35
+ return cb({
36
+ request,
37
+ router,
38
+ responseHeaders
39
+ });
40
+ } finally {
41
+ if (!cbWillCleanup) {
42
+ router.serverSsr?.cleanup();
43
+ }
44
+ }
37
45
  };
38
46
  }
39
47
  function getRequestHeaders(opts) {
@@ -1 +1 @@
1
- {"version":3,"file":"createRequestHandler.cjs","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 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 return cb({\n request,\n router,\n responseHeaders,\n } as any)\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":["attachRouterServerSsrUtils","getOrigin","history","createMemoryHistory","headers","mergeHeaders"],"mappings":";;;;;AAWO,SAAS,qBAAgD;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AACF,GAI4B;AAC1B,SAAO,OAAO,OAAO;AACnB,UAAM,SAAS,aAAA;AAEfA,yCAA2B;AAAA,MACzB;AAAA,MACA,UAAU,MAAM,oBAAA;AAAA,IAAoB,CACrC;AAED,UAAM,MAAM,IAAI,IAAI,QAAQ,KAAK,kBAAkB;AACnD,UAAM,SAASC,UAAAA,UAAU,OAAO;AAChC,UAAM,OAAO,IAAI,KAAK,QAAQ,IAAI,QAAQ,EAAE;AAG5C,UAAMC,YAAUC,QAAAA,oBAAoB;AAAA,MAClC,gBAAgB,CAAC,IAAI;AAAA,IAAA,CACtB;AAGD,WAAO,OAAO;AAAA,MAAA,SACZD;AAAAA,MACA,QAAQ,OAAO,QAAQ,UAAU;AAAA,IAAA,CAClC;AAED,UAAM,OAAO,KAAA;AAEb,UAAM,OAAO,WAAW,UAAA;AAExB,UAAM,kBAAkB,kBAAkB;AAAA,MACxC;AAAA,IAAA,CACD;AAED,WAAO,GAAG;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IAAA,CACM;AAAA,EACV;AACF;AAEA,SAAS,kBAAkB,MAAsC;AAC/D,MAAIE,YAAUC,QAAAA;AAAAA,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;AACZD,gBAAUC,QAAAA,aAAaD,WAAS,SAAS,OAAO;AAAA,EAClD;AAEA,SAAOA;AACT;;"}
1
+ {"version":3,"file":"createRequestHandler.cjs","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":["attachRouterServerSsrUtils","getOrigin","history","createMemoryHistory","headers","mergeHeaders"],"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;AACFA,2CAA2B;AAAA,QACzB;AAAA,QACA,UAAU,MAAM,oBAAA;AAAA,MAAoB,CACrC;AAED,YAAM,MAAM,IAAI,IAAI,QAAQ,KAAK,kBAAkB;AACnD,YAAM,SAASC,UAAAA,UAAU,OAAO;AAChC,YAAM,OAAO,IAAI,KAAK,QAAQ,IAAI,QAAQ,EAAE;AAG5C,YAAMC,YAAUC,QAAAA,oBAAoB;AAAA,QAClC,gBAAgB,CAAC,IAAI;AAAA,MAAA,CACtB;AAGD,aAAO,OAAO;AAAA,QAAA,SACZD;AAAAA,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,MAAIE,YAAUC,QAAAA;AAAAA,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;AACZD,gBAAUC,QAAAA,aAAaD,WAAS,SAAS,OAAO;AAAA,EAClD;AAEA,SAAOA;AACT;;"}
@@ -34,11 +34,13 @@ const INITIAL_SCRIPTS = [
34
34
  ];
35
35
  class ScriptBuffer {
36
36
  constructor(router) {
37
- this.router = router;
38
37
  this._queue = [...INITIAL_SCRIPTS];
39
38
  this._scriptBarrierLifted = false;
39
+ this._cleanedUp = false;
40
+ this.router = router;
40
41
  }
41
42
  enqueue(script) {
43
+ if (this._cleanedUp) return;
42
44
  if (this._scriptBarrierLifted && this._queue.length === 0) {
43
45
  queueMicrotask(() => {
44
46
  this.injectBufferedScripts();
@@ -47,7 +49,7 @@ class ScriptBuffer {
47
49
  this._queue.push(script);
48
50
  }
49
51
  liftBarrier() {
50
- if (this._scriptBarrierLifted) return;
52
+ if (this._scriptBarrierLifted || this._cleanedUp) return;
51
53
  this._scriptBarrierLifted = true;
52
54
  if (this._queue.length > 0) {
53
55
  queueMicrotask(() => {
@@ -66,11 +68,17 @@ class ScriptBuffer {
66
68
  return joinedScripts;
67
69
  }
68
70
  injectBufferedScripts() {
71
+ if (this._cleanedUp) return;
69
72
  const scriptsToInject = this.takeAll();
70
- if (scriptsToInject) {
73
+ if (scriptsToInject && this.router?.serverSsr) {
71
74
  this.router.serverSsr.injectScript(() => scriptsToInject);
72
75
  }
73
76
  }
77
+ cleanup() {
78
+ this._cleanedUp = true;
79
+ this._queue = [];
80
+ this.router = void 0;
81
+ }
74
82
  }
75
83
  function attachRouterServerSsrUtils({
76
84
  router,
@@ -163,6 +171,7 @@ function attachRouterServerSsrUtils({
163
171
  onRenderFinished: (listener) => listeners.push(listener),
164
172
  setRenderFinished: () => {
165
173
  listeners.forEach((l) => l());
174
+ listeners.length = 0;
166
175
  scriptBuffer.liftBarrier();
167
176
  },
168
177
  takeBufferedScripts() {
@@ -180,6 +189,13 @@ function attachRouterServerSsrUtils({
180
189
  },
181
190
  liftScriptBarrier() {
182
191
  scriptBuffer.liftBarrier();
192
+ },
193
+ cleanup() {
194
+ if (!router.serverSsr) return;
195
+ listeners.length = 0;
196
+ scriptBuffer.cleanup();
197
+ router.serverSsr.injectedHtml = [];
198
+ router.serverSsr = void 0;
183
199
  }
184
200
  };
185
201
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-server.cjs","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":["getCrossReferenceHeader","minifiedTsrBootStrapScript","GLOBAL_TSR","createControlledPromise","makeSsrSerovalPlugin","crossSerializeStream","defaultSerovalPlugins","TSR_SCRIPT_BARRIER_ID"],"mappings":"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"}
1
+ {"version":3,"file":"ssr-server.cjs","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":["getCrossReferenceHeader","minifiedTsrBootStrapScript","GLOBAL_TSR","createControlledPromise","makeSsrSerovalPlugin","crossSerializeStream","defaultSerovalPlugins","TSR_SCRIPT_BARRIER_ID"],"mappings":"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"}
@@ -5,6 +5,7 @@ import { Manifest } from '../manifest.cjs';
5
5
  declare module '../router' {
6
6
  interface ServerSsr {
7
7
  setRenderFinished: () => void;
8
+ cleanup: () => void;
8
9
  }
9
10
  interface RouterEvents {
10
11
  onInjectedHtml: {
@@ -24,12 +24,13 @@ function createPassthrough(onCancel) {
24
24
  },
25
25
  cancel() {
26
26
  res.destroyed = true;
27
- onCancel?.();
27
+ onCancel();
28
28
  }
29
29
  });
30
30
  const res = {
31
31
  stream,
32
32
  write: (chunk) => {
33
+ if (res.destroyed) return;
33
34
  if (typeof chunk === "string") {
34
35
  controller.enqueue(encoder.encode(chunk));
35
36
  } else {
@@ -37,13 +38,15 @@ function createPassthrough(onCancel) {
37
38
  }
38
39
  },
39
40
  end: (chunk) => {
41
+ if (res.destroyed) return;
40
42
  if (chunk) {
41
43
  res.write(chunk);
42
44
  }
43
- controller.close();
44
45
  res.destroyed = true;
46
+ controller.close();
45
47
  },
46
48
  destroy: (error) => {
49
+ if (res.destroyed) return;
47
50
  res.destroyed = true;
48
51
  controller.error(error);
49
52
  },
@@ -68,13 +71,18 @@ async function readStream(stream, opts) {
68
71
  function transformStreamWithRouter(router, appStream, opts) {
69
72
  let stopListeningToInjectedHtml = void 0;
70
73
  let timeoutHandle;
71
- const finalPassThrough = createPassthrough(() => {
74
+ let cleanedUp = false;
75
+ function cleanup() {
76
+ if (cleanedUp) return;
77
+ cleanedUp = true;
72
78
  if (stopListeningToInjectedHtml) {
73
79
  stopListeningToInjectedHtml();
74
80
  stopListeningToInjectedHtml = void 0;
75
81
  }
76
82
  clearTimeout(timeoutHandle);
77
- });
83
+ router.serverSsr?.cleanup();
84
+ }
85
+ const finalPassThrough = createPassthrough(cleanup);
78
86
  const textDecoder = new TextDecoder();
79
87
  let isAppRendering = true;
80
88
  let routerStreamBuffer = "";
@@ -101,10 +109,11 @@ function transformStreamWithRouter(router, appStream, opts) {
101
109
  handleInjectedHtml
102
110
  );
103
111
  function handleInjectedHtml() {
112
+ if (cleanedUp) return;
104
113
  router.serverSsr.injectedHtml.forEach((promise) => {
105
114
  processingCount++;
106
115
  promise.then((html) => {
107
- if (finalPassThrough.destroyed) {
116
+ if (cleanedUp || finalPassThrough.destroyed) {
108
117
  return;
109
118
  }
110
119
  if (isAppRendering) {
@@ -113,9 +122,7 @@ function transformStreamWithRouter(router, appStream, opts) {
113
122
  finalPassThrough.write(html);
114
123
  }
115
124
  }).catch((err) => {
116
- if (!finalPassThrough.destroyed) {
117
- injectedHtmlDonePromise.reject(err);
118
- }
125
+ injectedHtmlDonePromise.reject(err);
119
126
  }).finally(() => {
120
127
  processingCount--;
121
128
  if (!isAppRendering && processingCount === 0) {
@@ -126,6 +133,9 @@ function transformStreamWithRouter(router, appStream, opts) {
126
133
  router.serverSsr.injectedHtml = [];
127
134
  }
128
135
  injectedHtmlDonePromise.then(() => {
136
+ if (cleanedUp || finalPassThrough.destroyed) {
137
+ return;
138
+ }
129
139
  clearTimeout(timeoutHandle);
130
140
  const finalHtml = leftover + leftoverHtml + getBufferedRouterStream() + pendingClosingTags;
131
141
  leftover = "";
@@ -133,16 +143,17 @@ function transformStreamWithRouter(router, appStream, opts) {
133
143
  pendingClosingTags = "";
134
144
  finalPassThrough.end(finalHtml);
135
145
  }).catch((err) => {
146
+ if (cleanedUp || finalPassThrough.destroyed) {
147
+ return;
148
+ }
136
149
  console.error("Error reading routerStream:", err);
137
150
  finalPassThrough.destroy(err);
138
- }).finally(() => {
139
- if (stopListeningToInjectedHtml) {
140
- stopListeningToInjectedHtml();
141
- stopListeningToInjectedHtml = void 0;
142
- }
143
- });
151
+ }).finally(cleanup);
144
152
  readStream(appStream, {
145
153
  onData: (chunk) => {
154
+ if (cleanedUp || finalPassThrough.destroyed) {
155
+ return;
156
+ }
146
157
  const text = decodeChunk(chunk.value);
147
158
  const chunkString = leftover + text;
148
159
  const bodyEndMatch = chunkString.match(patternBodyEnd);
@@ -183,7 +194,7 @@ function transformStreamWithRouter(router, appStream, opts) {
183
194
  }
184
195
  },
185
196
  onEnd: () => {
186
- if (finalPassThrough.destroyed) {
197
+ if (cleanedUp || finalPassThrough.destroyed) {
187
198
  return;
188
199
  }
189
200
  isAppRendering = false;
@@ -200,6 +211,9 @@ function transformStreamWithRouter(router, appStream, opts) {
200
211
  }
201
212
  },
202
213
  onError: (error) => {
214
+ if (cleanedUp) {
215
+ return;
216
+ }
203
217
  console.error("Error reading appStream:", error);
204
218
  isAppRendering = false;
205
219
  router.serverSsr.setRenderFinished();
@@ -1 +1 @@
1
- {"version":3,"file":"transformStreamWithRouter.cjs","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":["Readable","ReadableStream","createControlledPromise"],"mappings":"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"}
1
+ {"version":3,"file":"transformStreamWithRouter.cjs","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":["Readable","ReadableStream","createControlledPromise"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { Store, batch } from "@tanstack/store";
2
2
  import { createBrowserHistory, parseHref } from "@tanstack/history";
3
- import { createControlledPromise, deepEqual, replaceEqualDeep, decodePath, last, findLast, functionalUpdate } from "./utils.js";
3
+ import { createControlledPromise, deepEqual, replaceEqualDeep, decodePath, functionalUpdate, last, findLast } from "./utils.js";
4
4
  import { processRouteTree, processRouteMasks, findSingleMatch, findRouteMatch, findFlatMatch } from "./new-process-route-tree.js";
5
5
  import { trimPath, resolvePath, cleanPath, trimPathRight, interpolatePath } from "./path.js";
6
6
  import { createLRUCache } from "./lru-cache.js";
@@ -396,11 +396,16 @@ class RouterCore {
396
396
  );
397
397
  if (match) {
398
398
  Object.assign(params, match.params);
399
- const { from: _from, ...maskProps } = match.route;
399
+ const {
400
+ from: _from,
401
+ params: maskParams,
402
+ ...maskProps
403
+ } = match.route;
404
+ const nextParams = maskParams === false || maskParams === null ? {} : (maskParams ?? true) === true ? params : Object.assign(params, functionalUpdate(maskParams, params));
400
405
  maskedDest = {
401
406
  from: opts.from,
402
407
  ...maskProps,
403
- params
408
+ params: nextParams
404
409
  };
405
410
  maskedNext = build(maskedDest);
406
411
  }