From 9d0cbebdecc9b48871d2c08dbbe394d50e084621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Vivet?= Date: Thu, 8 Oct 2026 15:18:27 +0200 Subject: [PATCH] Make disposed debugger clients inert Detach the WebSocket handlers and cancel the pending hard reload before closing, drop messages sent after dispose, and release the debugger client and in-game editor references when the game is disposed. --- .../abstract-debugger-client.ts | 4 ++++ .../websocket-debugger-client.ts | 21 ++++++++++++++++++- .../window-message-debugger-client.ts | 4 +++- GDJS/Runtime/runtimegame.ts | 4 ++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/GDJS/Runtime/debugger-client/abstract-debugger-client.ts b/GDJS/Runtime/debugger-client/abstract-debugger-client.ts index 984944f93d..2c2a681778 100644 --- a/GDJS/Runtime/debugger-client/abstract-debugger-client.ts +++ b/GDJS/Runtime/debugger-client/abstract-debugger-client.ts @@ -614,6 +614,10 @@ namespace gdjs { } } + /** + * Release the listeners and the connection used by the client, which must + * not be used anymore afterwards: messages are then silently dropped. + */ abstract dispose(): void; /** diff --git a/GDJS/Runtime/debugger-client/websocket-debugger-client.ts b/GDJS/Runtime/debugger-client/websocket-debugger-client.ts index 7af40df5a1..c4fe525ffd 100644 --- a/GDJS/Runtime/debugger-client/websocket-debugger-client.ts +++ b/GDJS/Runtime/debugger-client/websocket-debugger-client.ts @@ -63,7 +63,7 @@ namespace gdjs { // the WebSocket connection between the editor and the game is closed. When we are in in-game edition, // we can't afford to lose the connection because it means the editor is unusable. // In this case, we hard reload the game to re-establish a new connection. - setTimeout(() => { + that._hardReloadTimeoutId = setTimeout(() => { logger.info( 'Debugger connection closed while in in-game edition - this is suspicious so hard reloading to re-establish a new connection.' ); @@ -86,14 +86,33 @@ namespace gdjs { } private hasLoggedError: boolean = false; + private _wasDisposed: boolean = false; + /** The hard reload scheduled when the connection is lost in in-game edition. */ + private _hardReloadTimeoutId: NodeJS.Timeout | null = null; dispose(): void { + this._wasDisposed = true; + if (this._hardReloadTimeoutId !== null) { + clearTimeout(this._hardReloadTimeoutId); + this._hardReloadTimeoutId = null; + } if (this._ws) { + // Detach the handlers before closing: `close()` fires `onclose` (and + // `onerror` if the connection was still being established), which must + // not trigger the hard reload done when the connection is lost in + // in-game edition. + this._ws.onopen = null; + this._ws.onclose = null; + this._ws.onerror = null; + this._ws.onmessage = null; this._ws.close(); } } protected _sendMessage(message: string) { + // Once disposed, messages are silently dropped: the connection is closed + // and logging a warning would call this method again. + if (this._wasDisposed) return; if (!this._ws) { // The error can be logged only once, since logger.warn will call this function again, // leading to an endless recursive call if we do not call it only once. diff --git a/GDJS/Runtime/debugger-client/window-message-debugger-client.ts b/GDJS/Runtime/debugger-client/window-message-debugger-client.ts index c764daac4f..f8a246d77f 100644 --- a/GDJS/Runtime/debugger-client/window-message-debugger-client.ts +++ b/GDJS/Runtime/debugger-client/window-message-debugger-client.ts @@ -31,8 +31,10 @@ namespace gdjs { window.addEventListener('message', this._onWindowMessage); } - dispose() { + dispose(): void { window.removeEventListener('message', this._onWindowMessage); + // Drop the opener so that no message is sent anymore once disposed. + this._opener = null; } protected _sendMessage(message: string) { diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index 6fb8bfd678..c3b2b22ab6 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -1448,6 +1448,10 @@ namespace gdjs { this._sceneStack.dispose(); this._renderer.dispose(removeCanvas); this._resourcesLoader.dispose(); + // Drop the disposed helpers so that any late call (uncaught exception + // reporting, status updates...) is a no-op. + this._inGameEditor = null; + this._debuggerClient = null; this._wasDisposed = true; }