From 7d8930f09d0a936ab738b5fe7afa2305d010b82a Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 3 Jun 2026 13:58:03 +0200 Subject: [PATCH 1/2] test: regression test for streaming teardown --- .../tests/tsr-script-teardown.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 packages/router-core/tests/tsr-script-teardown.test.ts diff --git a/packages/router-core/tests/tsr-script-teardown.test.ts b/packages/router-core/tests/tsr-script-teardown.test.ts new file mode 100644 index 00000000000..6cf0129e590 --- /dev/null +++ b/packages/router-core/tests/tsr-script-teardown.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeEach, describe, expect, test } from 'vitest' +import minifiedTsrBootStrapScript from '../src/ssr/tsrScript?script-string' + +type TsrBootstrap = { + h: () => void + e: () => void + c: () => void +} + +// Evaluate the real (minified) client bootstrap; it assigns `self.$_TSR`. +function installBootstrap(): TsrBootstrap { + new Function(minifiedTsrBootStrapScript)() + return (window as any).$_TSR +} + +function setReadyState(value: DocumentReadyState) { + Object.defineProperty(document, 'readyState', { + configurable: true, + get: () => value, + }) +} + +describe('$_TSR client teardown', () => { + beforeEach(() => { + ;(window as any).$R = { tsr: [] } + delete (window as any).$_TSR + }) + + afterEach(() => { + delete (window as any).$_TSR + delete (window as any).$R + setReadyState('complete') + }) + + test('does not tear down until both hydrated and streamEnded', () => { + setReadyState('complete') + const tsr = installBootstrap() + + tsr.h() + expect((window as any).$_TSR).toBeDefined() + + tsr.e() + expect((window as any).$_TSR).toBeUndefined() + }) + + test('tears down immediately when the document is already parsed', () => { + setReadyState('complete') + const tsr = installBootstrap() + + tsr.h() + tsr.e() + + expect((window as any).$_TSR).toBeUndefined() + expect((window as any).$R.tsr).toBeUndefined() + }) + + // Regression: deferred/streamed deserializations can still arrive after the + // `$_TSR.e()` stream-end marker (e.g. Solid 2 streams resource hydration past + // it). Tearing `$_TSR` down immediately would make those late references + // throw `$_TSR is not defined`, so teardown waits for the document to finish + // parsing. + test('keeps $_TSR alive until DOMContentLoaded while the document is still loading', () => { + setReadyState('loading') + const tsr = installBootstrap() + + tsr.h() + tsr.e() + + // Still available for late streamed scripts. + expect((window as any).$_TSR).toBeDefined() + expect((window as any).$R.tsr).toBeDefined() + + document.dispatchEvent(new Event('DOMContentLoaded')) + + expect((window as any).$_TSR).toBeUndefined() + expect((window as any).$R.tsr).toBeUndefined() + }) +}) From dec26192b103d242f06a9d8834ba2bf63e21a169 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 3 Jun 2026 14:01:13 +0200 Subject: [PATCH 2/2] improve wording --- packages/router-core/tests/tsr-script-teardown.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router-core/tests/tsr-script-teardown.test.ts b/packages/router-core/tests/tsr-script-teardown.test.ts index 6cf0129e590..e3985dc555d 100644 --- a/packages/router-core/tests/tsr-script-teardown.test.ts +++ b/packages/router-core/tests/tsr-script-teardown.test.ts @@ -7,7 +7,7 @@ type TsrBootstrap = { c: () => void } -// Evaluate the real (minified) client bootstrap; it assigns `self.$_TSR`. +// Assign `self.$_TSR`. function installBootstrap(): TsrBootstrap { new Function(minifiedTsrBootStrapScript)() return (window as any).$_TSR