Skip to content

Commit 03595c8

Browse files
fix: memory leaks (#5896)
1 parent 090bc66 commit 03595c8

File tree

10 files changed

+229
-254
lines changed

10 files changed

+229
-254
lines changed

e2e/react-start/basic/src/routes/users.$userId.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { ErrorComponent, createFileRoute } from '@tanstack/react-router'
22
import axios from 'redaxios'
3+
import { getRouterInstance } from '@tanstack/react-start'
34
import type { ErrorComponentProps } from '@tanstack/react-router'
45

56
import type { User } from '~/utils/users'
67
import { NotFound } from '~/components/NotFound'
78

89
export const Route = createFileRoute('/users/$userId')({
910
loader: async ({ params: { userId } }) => {
11+
const router = await getRouterInstance()
1012
return await axios
11-
.get<User>('/api/users/' + userId)
13+
.get<User>('/api/users/' + userId, { baseURL: router.options.origin })
1214
.then((r) => r.data)
1315
.catch(() => {
1416
throw new Error('Failed to fetch user')

e2e/react-start/basic/src/routes/users.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
2+
import { getRouterInstance } from '@tanstack/react-start'
23
import axios from 'redaxios'
34

45
import type { User } from '~/utils/users'
56

67
export const Route = createFileRoute('/users')({
78
loader: async () => {
9+
const router = await getRouterInstance()
810
return await axios
9-
.get<Array<User>>('/api/users')
11+
.get<Array<User>>('/api/users', { baseURL: router.options.origin })
1012
.then((r) => r.data)
1113
.catch(() => {
1214
throw new Error('Failed to fetch users')

e2e/react-start/basic/tests/prerendering.spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ test.describe('Prerender Static Path Discovery', () => {
1414
// These static routes should be automatically discovered and prerendered
1515
expect(existsSync(join(distDir, 'index.html'))).toBe(true)
1616
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
17-
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
1817
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
1918
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
2019
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
@@ -40,14 +39,5 @@ test.describe('Prerender Static Path Discovery', () => {
4039
const html = readFileSync(join(distDir, 'posts/index.html'), 'utf-8')
4140
expect(html).toContain('Select a post.')
4241
})
43-
44-
test('should contain prerendered content in users.html', () => {
45-
const distDir = join(process.cwd(), 'dist', 'client')
46-
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
47-
48-
// "Select a user." should be in the prerendered HTML
49-
const html = readFileSync(join(distDir, 'users/index.html'), 'utf-8')
50-
expect(html).toContain('Select a user.')
51-
})
5242
})
5343
})

e2e/react-start/basic/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const prerenderConfiguration = {
2121
'/i-do-not-exist',
2222
'/not-found/via-beforeLoad',
2323
'/not-found/via-loader',
24+
'/users',
2425
].some((p) => page.path.includes(p)),
2526
maxRedirects: 100,
2627
}
@@ -33,7 +34,6 @@ export default defineConfig({
3334
tsConfigPaths({
3435
projects: ['./tsconfig.json'],
3536
}),
36-
// @ts-ignore we want to keep one test with verboseFileRoutes off even though the option is hidden
3737
tanstackStart({
3838
spa: isSpaMode ? spaModeConfiguration : undefined,
3939
prerender: isPrerender ? prerenderConfiguration : undefined,

e2e/solid-start/basic/src/routes/users.$userId.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { createFileRoute } from '@tanstack/solid-router'
22
import axios from 'redaxios'
33

4+
import { getRouterInstance } from '@tanstack/solid-start'
45
import type { User } from '~/utils/users'
56
import { NotFound } from '~/components/NotFound'
67
import { UserErrorComponent } from '~/components/UserErrorComponent'
78

89
export const Route = createFileRoute('/users/$userId')({
910
loader: async ({ params: { userId } }) => {
11+
const router = await getRouterInstance()
1012
return await axios
11-
.get<User>('/api/users/' + userId)
13+
.get<User>('/api/users/' + userId, { baseURL: router.options.origin })
1214
.then((r) => r.data)
1315
.catch(() => {
1416
throw new Error('Failed to fetch user')

e2e/solid-start/basic/src/routes/users.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
2+
import { getRouterInstance } from '@tanstack/solid-start'
23
import axios from 'redaxios'
34

45
import type { User } from '~/utils/users'
56

67
export const Route = createFileRoute('/users')({
78
loader: async () => {
9+
const router = await getRouterInstance()
810
return await axios
9-
.get<Array<User>>('/api/users')
11+
.get<Array<User>>('/api/users', { baseURL: router.options.origin })
1012
.then((r) => r.data)
1113
.catch(() => {
1214
throw new Error('Failed to fetch users')

e2e/solid-start/basic/tests/prerendering.spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ test.describe('Prerender Static Path Discovery', () => {
1515
expect(existsSync(join(distDir, 'index.html'))).toBe(true)
1616
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
1717
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
18-
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
1918
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
2019
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
2120
expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)
@@ -40,14 +39,5 @@ test.describe('Prerender Static Path Discovery', () => {
4039
const html = readFileSync(join(distDir, 'posts/index.html'), 'utf-8')
4140
expect(html).toContain('Select a post.')
4241
})
43-
44-
test('should contain prerendered content in users.html', () => {
45-
const distDir = join(process.cwd(), 'dist', 'client')
46-
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
47-
48-
// "Select a user." should be in the prerendered HTML
49-
const html = readFileSync(join(distDir, 'users/index.html'), 'utf-8')
50-
expect(html).toContain('Select a user.')
51-
})
5242
})
5343
})

e2e/solid-start/basic/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const prerenderConfiguration = {
2323
'/not-found/via-loader',
2424
'/search-params/default',
2525
'/transition',
26+
'/users',
2627
].some((p) => page.path.includes(p)),
2728
maxRedirects: 100,
2829
}
@@ -35,7 +36,6 @@ export default defineConfig({
3536
tsConfigPaths({
3637
projects: ['./tsconfig.json'],
3738
}),
38-
// @ts-ignore we want to keep one test with verboseFileRoutes off even though the option is hidden
3939
tanstackStart({
4040
spa: isSpaMode ? spaModeConfiguration : undefined,
4141
prerender: isPrerender ? prerenderConfiguration : undefined,

packages/router-core/src/ssr/transformStreamWithRouter.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,17 @@ type ReadablePassthrough = {
3535
destroyed: boolean
3636
}
3737

38-
function createPassthrough() {
38+
function createPassthrough(onCancel?: () => void) {
3939
let controller: ReadableStreamDefaultController<any>
4040
const encoder = new TextEncoder()
4141
const stream = new ReadableStream({
4242
start(c) {
4343
controller = c
4444
},
45+
cancel() {
46+
res.destroyed = true
47+
onCancel?.()
48+
},
4549
})
4650

4751
const res: ReadablePassthrough = {
@@ -96,7 +100,13 @@ export function transformStreamWithRouter(
96100
timeoutMs?: number
97101
},
98102
) {
99-
const finalPassThrough = createPassthrough()
103+
let stopListeningToInjectedHtml: (() => void) | undefined = undefined
104+
let timeoutHandle: NodeJS.Timeout
105+
106+
const finalPassThrough = createPassthrough(() => {
107+
stopListeningToInjectedHtml?.()
108+
clearTimeout(timeoutHandle)
109+
})
100110
const textDecoder = new TextDecoder()
101111

102112
let isAppRendering = true as boolean
@@ -105,7 +115,6 @@ export function transformStreamWithRouter(
105115
let streamBarrierLifted = false as boolean
106116
let leftover = ''
107117
let leftoverHtml = ''
108-
let timeoutHandle: NodeJS.Timeout
109118

110119
function getBufferedRouterStream() {
111120
const html = routerStreamBuffer
@@ -130,12 +139,9 @@ export function transformStreamWithRouter(
130139
})
131140

132141
// Listen for any new injected HTML
133-
const stopListeningToInjectedHtml = router.subscribe(
134-
'onInjectedHtml',
135-
(e) => {
136-
handleInjectedHtml(e.promise)
137-
},
138-
)
142+
stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', (e) => {
143+
handleInjectedHtml(e.promise)
144+
})
139145

140146
function handleInjectedHtml(promise: Promise<string>) {
141147
processingCount++
@@ -170,7 +176,7 @@ export function transformStreamWithRouter(
170176
console.error('Error reading routerStream:', err)
171177
finalPassThrough.destroy(err)
172178
})
173-
.finally(stopListeningToInjectedHtml)
179+
.finally(() => stopListeningToInjectedHtml?.())
174180

175181
// Transform the appStream
176182
readStream(appStream, {

0 commit comments

Comments
 (0)