Add periodic browser resource cleanup to prevent memory leaks

- Cap message queue at 1000 entries; trim oldest on overflow in send()
- Add per-instance 30s cleanup timer that trims stale queued messages
- Add module-level 30s sweep that disposes terminal instances whose
  DOM containers have been removed (el.isConnected === false)
- Wire cleanup timer start/stop into initialize() and dispose()
This commit is contained in:
GitHub Copilot
2026-02-17 20:26:03 +00:00
parent 594533eae6
commit 7e35d4cb0b
2 changed files with 57 additions and 8 deletions
File diff suppressed because one or more lines are too long
+49
View File
@@ -8,6 +8,11 @@
import { Terminal, FitAddon, Ghostty, type ITerminalOptions, type ITheme } from "ghostty-web";
/** Maximum queued messages before oldest are dropped */
const MAX_MESSAGE_QUEUE_SIZE = 1000;
/** How often to run periodic resource cleanup (ms) */
const RESOURCE_CLEANUP_INTERVAL_MS = 30_000;
/** Default font stack - prefers system monospace, falls back through programming fonts */
const DEFAULT_FONT_FAMILY =
'ui-monospace, "SFMono-Regular", "FiraCode Nerd Font", "FiraMono Nerd Font", ' +
@@ -592,6 +597,7 @@ class WebTerminal {
private pendingFn = false;
private fontFamily: string;
private fontSize: number;
private cleanupTimer: number | undefined;
private constructor(
container: HTMLElement,
@@ -781,6 +787,9 @@ class WebTerminal {
this.setupMobileKeybar();
}
// Start periodic resource cleanup
this.startResourceCleanup();
// Connect WebSocket
this.connect();
@@ -1565,8 +1574,36 @@ class WebTerminal {
}
}
/** Start periodic resource cleanup to prevent memory leaks */
private startResourceCleanup(): void {
this.cleanupTimer = window.setInterval(() => {
this.trimMessageQueue();
}, RESOURCE_CLEANUP_INTERVAL_MS);
}
/** Stop periodic resource cleanup */
private stopResourceCleanup(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
}
/** Drop oldest messages when the queue exceeds the cap */
private trimMessageQueue(): void {
if (this.messageQueue.length > MAX_MESSAGE_QUEUE_SIZE) {
const dropped = this.messageQueue.length - MAX_MESSAGE_QUEUE_SIZE;
this.messageQueue = this.messageQueue.slice(-MAX_MESSAGE_QUEUE_SIZE);
console.warn(`[webterm] Trimmed ${dropped} stale messages from queue`);
}
}
/** Send message to server with queueing support */
private send(message: [string, unknown]): void {
if (this.messageQueue.length >= MAX_MESSAGE_QUEUE_SIZE) {
this.messageQueue = this.messageQueue.slice(-Math.floor(MAX_MESSAGE_QUEUE_SIZE / 2));
console.warn("[webterm] Message queue overflow; trimmed old messages");
}
this.messageQueue.push(message);
this.processMessageQueue();
}
@@ -1611,6 +1648,7 @@ class WebTerminal {
/** Clean up resources */
dispose(): void {
this.stopResourceCleanup();
this.stopHeartbeatWatchdog();
this.socket?.close();
if (this.mobileInput) {
@@ -1643,6 +1681,17 @@ class WebTerminal {
// Store instances for potential external access
const instances: Map<HTMLElement, WebTerminal> = new Map();
// Periodically sweep stale terminal instances whose containers were removed from the DOM
setInterval(() => {
for (const [el, terminal] of instances) {
if (!el.isConnected) {
terminal.dispose();
instances.delete(el);
console.log("[webterm] Cleaned up stale terminal instance");
}
}
}, RESOURCE_CLEANUP_INTERVAL_MS);
/** Initialize all terminal containers on page load */
async function initTerminals(): Promise<void> {
console.log("[webterm:init] initTerminals() called");