@@ -148,8 +148,28 @@ export function wsRoutes (app) {
148148 ws . send ( JSON . stringify ( data ) )
149149 }
150150
151- // In the WebSocket setup, replace the data handler:
152- term . on ( 'data' , function ( data ) {
151+ // Detach listeners from a previous WS connection for the same term.
152+ // Reconnects create a new WS but reuse the same term instance; without
153+ // this each reconnect stacks another 'data' handler, duplicating output
154+ // and pinning closed sockets/buffers in memory.
155+ if ( term . _wsDataHandler && typeof term . off === 'function' ) {
156+ try {
157+ term . off ( 'data' , term . _wsDataHandler )
158+ } catch ( _ ) { }
159+ }
160+ if ( term . _wsPortDataHandler && term . port ) {
161+ try {
162+ term . port . removeListener ( 'data' , term . _wsPortDataHandler )
163+ } catch ( _ ) { }
164+ }
165+ if ( term . _wsCloseHandler && typeof term . off === 'function' ) {
166+ try {
167+ term . off ( 'close' , term . _wsCloseHandler )
168+ term . off ( 'exit' , term . _wsCloseHandler )
169+ } catch ( _ ) { }
170+ }
171+
172+ const onTermData = function ( data ) {
153173 // Check if zmodem session is active and handle data
154174 if ( zmodemManager . isActive ( pid ) ) {
155175 // Let zmodem handle the data, but still log it
@@ -247,25 +267,58 @@ export function wsRoutes (app) {
247267 if ( ! sendTimeout ) {
248268 sendTimeout = setTimeout ( flushBufferedData , flushIntervalMs - elapsed )
249269 }
250- } )
270+ }
271+ term . _wsDataHandler = onTermData
272+ term . on ( 'data' , onTermData )
251273
252274 // For serial terminals, register a raw data listener directly on the port to
253275 // feed binary XMODEM data to xmodemManager without rxLineEnding transformation.
254276 if ( term . port ) {
255- term . port . on ( 'data' , function ( rawData ) {
277+ const onPortData = function ( rawData ) {
256278 if ( xmodemManager . isActive ( pid ) ) {
257279 term . writeLog ( rawData )
258280 xmodemManager . handleData ( pid , rawData , term , ws )
259281 }
260- } )
282+ }
283+ term . _wsPortDataHandler = onPortData
284+ term . port . on ( 'data' , onPortData )
261285 }
262286
287+ let onCloseCalled = false
263288 function onClose ( ) {
289+ if ( onCloseCalled ) return
290+ onCloseCalled = true
291+ // Detach this connection's listeners so a reconnect does not stack
292+ // duplicates on the reused term instance.
293+ if ( term . _wsDataHandler && typeof term . off === 'function' ) {
294+ try {
295+ term . off ( 'data' , term . _wsDataHandler )
296+ } catch ( _ ) { }
297+ }
298+ if ( term . _wsDataHandler === onTermData ) {
299+ term . _wsDataHandler = null
300+ }
301+ if ( term . _wsPortDataHandler && term . port ) {
302+ try {
303+ term . port . removeListener ( 'data' , term . _wsPortDataHandler )
304+ } catch ( _ ) { }
305+ term . _wsPortDataHandler = null
306+ }
307+ if ( typeof term . off === 'function' ) {
308+ try {
309+ term . off ( 'close' , onClose )
310+ term . off ( 'exit' , onClose )
311+ } catch ( _ ) { }
312+ }
313+ if ( term . _wsCloseHandler === onClose ) {
314+ term . _wsCloseHandler = null
315+ }
264316 // Cancel any pending batched send
265317 if ( sendTimeout ) {
266318 clearTimeout ( sendTimeout )
267319 sendTimeout = null
268320 }
321+ dataBuffer . length = 0
269322 // Clean up zmodem session
270323 zmodemManager . destroySession ( pid )
271324 // Clean up trzsz session
@@ -284,6 +337,7 @@ export function wsRoutes (app) {
284337 // own ws close handler; whole-process cleanup is handled on app exit.
285338 }
286339
340+ term . _wsCloseHandler = onClose
287341 term . on ( 'close' , onClose )
288342 if ( term . isLocal && isWin ) {
289343 term . on ( 'exit' , onClose )
0 commit comments