fix: release idle remote terminal multiplexers (#4101)

This commit is contained in:
Neil 2026-05-31 04:19:32 -07:00 committed by GitHub
parent 4615f2e169
commit 4b1f7b33ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 62 additions and 3 deletions

View File

@ -81,7 +81,13 @@ class RemoteRuntimeTerminalMultiplexer {
private ready = false
private nextStreamId = 1
constructor(private readonly environmentId: string) {}
constructor(
private readonly environmentId: string,
private readonly releaseIfCurrent: (
environmentId: string,
multiplexer: RemoteRuntimeTerminalMultiplexer
) => void
) {}
async subscribeTerminal(args: {
terminal: string
@ -347,6 +353,10 @@ class RemoteRuntimeTerminalMultiplexer {
stream.callbacks.onError?.(message)
}
}
// Why: a closed transport has no live streams or subscription; keeping it
// in the module map only retains callbacks for an environment that must
// reconnect through a fresh subscription anyway.
this.releaseIfCurrent(this.environmentId, this)
}
private closeIfIdle(): void {
@ -357,22 +367,39 @@ class RemoteRuntimeTerminalMultiplexer {
this.subscription = null
this.connectPromise = null
this.ready = false
this.releaseIfCurrent(this.environmentId, this)
}
}
const multiplexers = new Map<string, RemoteRuntimeTerminalMultiplexer>()
function releaseRemoteRuntimeTerminalMultiplexer(
environmentId: string,
multiplexer: RemoteRuntimeTerminalMultiplexer
): void {
if (multiplexers.get(environmentId) === multiplexer) {
multiplexers.delete(environmentId)
}
}
export function getRemoteRuntimeTerminalMultiplexer(
environmentId: string
): RemoteRuntimeTerminalMultiplexer {
let multiplexer = multiplexers.get(environmentId)
if (!multiplexer) {
multiplexer = new RemoteRuntimeTerminalMultiplexer(environmentId)
multiplexer = new RemoteRuntimeTerminalMultiplexer(
environmentId,
releaseRemoteRuntimeTerminalMultiplexer
)
multiplexers.set(environmentId, multiplexer)
}
return multiplexer
}
export function _getRemoteRuntimeTerminalMultiplexerCountForTest(): number {
return multiplexers.size
}
export function resetRemoteRuntimeTerminalMultiplexersForTests(): void {
multiplexers.clear()
}

View File

@ -6,7 +6,10 @@ import {
encodeTerminalStreamFrame,
encodeTerminalStreamText
} from '../../../shared/terminal-stream-protocol'
import { resetRemoteRuntimeTerminalMultiplexersForTests } from './remote-runtime-terminal-multiplexer'
import {
_getRemoteRuntimeTerminalMultiplexerCountForTest,
resetRemoteRuntimeTerminalMultiplexersForTests
} from './remote-runtime-terminal-multiplexer'
import {
getRemoteRuntimePtyEnvironmentId,
getRemoteRuntimeTerminalHandle,
@ -106,8 +109,36 @@ describe('remote runtime terminal data subscriptions', () => {
)
expect(watcher).toHaveBeenCalledWith('live')
expect(_getRemoteRuntimeTerminalMultiplexerCountForTest()).toBe(1)
dispose()
expect(unsubscribe).toHaveBeenCalled()
expect(_getRemoteRuntimeTerminalMultiplexerCountForTest()).toBe(0)
})
it('keeps the shared terminal multiplexer until the last watcher closes', async () => {
const firstDispose = await subscribeToRuntimeTerminalData(
{ activeRuntimeEnvironmentId: 'env-fallback' },
'remote:env-1@@terminal-1',
'watcher-1',
vi.fn()
)
const secondDispose = await subscribeToRuntimeTerminalData(
{ activeRuntimeEnvironmentId: 'env-fallback' },
'remote:env-1@@terminal-2',
'watcher-2',
vi.fn()
)
expect(runtimeSubscribe).toHaveBeenCalledTimes(1)
expect(_getRemoteRuntimeTerminalMultiplexerCountForTest()).toBe(1)
firstDispose()
expect(unsubscribe).not.toHaveBeenCalled()
expect(_getRemoteRuntimeTerminalMultiplexerCountForTest()).toBe(1)
secondDispose()
expect(unsubscribe).toHaveBeenCalledOnce()
expect(_getRemoteRuntimeTerminalMultiplexerCountForTest()).toBe(0)
})
it('rejects remote terminal subscriptions when the multiplex connection fails', async () => {
@ -123,5 +154,6 @@ describe('remote runtime terminal data subscriptions', () => {
).rejects.toThrow('offline')
expect(sendBinary).not.toHaveBeenCalled()
expect(_getRemoteRuntimeTerminalMultiplexerCountForTest()).toBe(0)
})
})