Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/solid-symbols-sink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/router-core': patch
---

delete $\_TSR immediately on stream end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the release note timing.

The implementation still waits for both hydration and stream end before deleting $_TSR, so “immediately on stream end” is a bit too strong. Please align the note with the actual condition to avoid misleading the patch summary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/solid-symbols-sink.md at line 5, The release note is misleading:
it says "delete $_TSR immediately on stream end" but the code waits for both
hydration and stream end before removing $_TSR; update the changelog entry to
reflect the actual behavior by stating that $_TSR is deleted after stream end
and hydration complete (or after both conditions are met) rather than
"immediately on stream end", and mention the combined condition involving
hydration and stream end (reference symbol $_TSR and the hydration/stream-end
condition) so the summary accurately matches the implementation.

13 changes: 2 additions & 11 deletions packages/router-core/src/ssr/tsrScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,8 @@ self.$_TSR = {
},
c() {
if (this.hydrated && this.streamEnded) {
const cleanup = () => {
if (self.$_TSR?.hydrated && self.$_TSR?.streamEnded) {
delete self.$_TSR
delete self.$R['tsr']
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', cleanup, { once: true })
} else {
cleanup()
}
delete self.$_TSR
delete self.$R['tsr']
}
},
p(script) {
Expand Down
46 changes: 46 additions & 0 deletions packages/router-core/tests/tsr-script-teardown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
import minifiedTsrBootStrapScript from '../src/ssr/tsrScript?script-string'

type TsrBootstrap = {
h: () => void
e: () => void
c: () => void
}

// Assign `self.$_TSR`.
function installBootstrap(): TsrBootstrap {
new Function(minifiedTsrBootStrapScript)()
return (window as any).$_TSR
}

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
})

test('does not tear down until both hydrated and streamEnded', () => {
const tsr = installBootstrap()

tsr.h()
expect((window as any).$_TSR).toBeDefined()

tsr.e()
expect((window as any).$_TSR).toBeUndefined()
})

test('removes both $_TSR and $R[tsr] on teardown', () => {
const tsr = installBootstrap()

tsr.h()
tsr.e()

expect((window as any).$_TSR).toBeUndefined()
expect((window as any).$R.tsr).toBeUndefined()
})
Comment on lines +27 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a readyState === 'loading' regression case.

Both tests force document.readyState to 'complete', so the pre-revert DOMContentLoaded-deferred implementation from #7524 would still pass this suite. That means the reverted path is not actually protected here.

🧪 Minimal coverage fix
-  test('tears down immediately when the document is already parsed', () => {
-    setReadyState('complete')
+  test('tears down immediately after stream end even while the document is loading', () => {
+    setReadyState('loading')
     const tsr = installBootstrap()
 
     tsr.h()
     tsr.e()
 
     expect((window as any).$_TSR).toBeUndefined()
     expect((window as any).$R.tsr).toBeUndefined()
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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()
})
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 after stream end even while the document is loading', () => {
setReadyState('loading')
const tsr = installBootstrap()
tsr.h()
tsr.e()
expect((window as any).$_TSR).toBeUndefined()
expect((window as any).$R.tsr).toBeUndefined()
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/tsr-script-teardown.test.ts` around lines 35 - 55,
Add a regression test for document.readyState === 'loading': use
setReadyState('loading'), call installBootstrap() to get tsr, invoke tsr.h() and
then tsr.e() and assert that (window as any).$_TSR and (window as any).$R.tsr
remain defined (teardown deferred), then simulate the document finishing parse
by setReadyState('complete') and dispatching a DOMContentLoaded event (or call
tsr.e() again) and finally assert both (window as any).$_TSR and (window as
any).$R.tsr are undefined; reference the helpers setReadyState, installBootstrap
and the tsr.h()/tsr.e() methods to locate where to add the new test.

})
Loading