From c7d42b28dbc0d19b79b17a883159818bd32bbc79 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Tue, 25 Nov 2025 20:58:33 +0100 Subject: [PATCH 1/2] fix: unsubscribe from query cache when rendering finished --- packages/router-ssr-query-core/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/router-ssr-query-core/src/index.ts b/packages/router-ssr-query-core/src/index.ts index 535facecf07..736503fdee8 100644 --- a/packages/router-ssr-query-core/src/index.ts +++ b/packages/router-ssr-query-core/src/index.ts @@ -38,10 +38,14 @@ export function setupCoreRouterSsrQueryIntegration({ if (router.isServer) { const sentQueries = new Set() const queryStream = createPushableStream() - + let unsubscribe: (() => void) | undefined = undefined router.options.dehydrate = async (): Promise => { - router.serverSsr!.onRenderFinished(() => queryStream.close()) + router.serverSsr!.onRenderFinished(() => { + queryStream.close() + unsubscribe?.() + unsubscribe = undefined + }) const ogDehydrated = await ogDehydrate?.() const dehydratedRouter = { @@ -70,7 +74,7 @@ export function setupCoreRouterSsrQueryIntegration({ }, }) - queryClient.getQueryCache().subscribe((event) => { + unsubscribe = queryClient.getQueryCache().subscribe((event) => { // before rendering starts, we do not stream individual queries // instead we dehydrate the entire query client in router's dehydrate() // if attachRouterServerSsrUtils() has not been called yet, `router.serverSsr` will be undefined and we also do not stream From 9123be7f8c10380732eeb7a6f8a608995c8b0663 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Tue, 25 Nov 2025 20:58:33 +0100 Subject: [PATCH 2/2] fix: clean injected promises and don't hold router references after unsubscribing --- .../src/ssr/transformStreamWithRouter.ts | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/router-core/src/ssr/transformStreamWithRouter.ts b/packages/router-core/src/ssr/transformStreamWithRouter.ts index ee2abcab437..8322b8da880 100644 --- a/packages/router-core/src/ssr/transformStreamWithRouter.ts +++ b/packages/router-core/src/ssr/transformStreamWithRouter.ts @@ -104,7 +104,10 @@ export function transformStreamWithRouter( let timeoutHandle: NodeJS.Timeout const finalPassThrough = createPassthrough(() => { - stopListeningToInjectedHtml?.() + if (stopListeningToInjectedHtml) { + stopListeningToInjectedHtml() + stopListeningToInjectedHtml = undefined + } clearTimeout(timeoutHandle) }) const textDecoder = new TextDecoder() @@ -134,34 +137,36 @@ export function transformStreamWithRouter( let processingCount = 0 // Process any already-injected HTML - router.serverSsr!.injectedHtml.forEach((promise) => { - handleInjectedHtml(promise) - }) + handleInjectedHtml() // Listen for any new injected HTML - stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', (e) => { - handleInjectedHtml(e.promise) - }) - - function handleInjectedHtml(promise: Promise) { - processingCount++ - - promise - .then((html) => { - if (isAppRendering) { - routerStreamBuffer += html - } else { - finalPassThrough.write(html) - } - }) - .catch(injectedHtmlDonePromise.reject) - .finally(() => { - processingCount-- + stopListeningToInjectedHtml = router.subscribe( + 'onInjectedHtml', + handleInjectedHtml, + ) - if (!isAppRendering && processingCount === 0) { - injectedHtmlDonePromise.resolve() - } - }) + function handleInjectedHtml() { + router.serverSsr!.injectedHtml.forEach((promise) => { + processingCount++ + + promise + .then((html) => { + if (isAppRendering) { + routerStreamBuffer += html + } else { + finalPassThrough.write(html) + } + }) + .catch(injectedHtmlDonePromise.reject) + .finally(() => { + processingCount-- + + if (!isAppRendering && processingCount === 0) { + injectedHtmlDonePromise.resolve() + } + }) + }) + router.serverSsr!.injectedHtml = [] } injectedHtmlDonePromise @@ -176,7 +181,12 @@ export function transformStreamWithRouter( console.error('Error reading routerStream:', err) finalPassThrough.destroy(err) }) - .finally(() => stopListeningToInjectedHtml?.()) + .finally(() => { + if (stopListeningToInjectedHtml) { + stopListeningToInjectedHtml() + stopListeningToInjectedHtml = undefined + } + }) // Transform the appStream readStream(appStream, {