From 3c69237d9b2efe8e84e9047290df31ee92803816 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 5 Dec 2024 23:03:30 -0500 Subject: [PATCH] Prevent crashes when user spams Cmd:T (#1404) When a user spams Cmd:T, a WaveTabView might be created but never end up getting mounted to the window, since another will come along before it can. In these cases, the WaveTabView is essentially in a bad state and attempting to switch to it will result in the window becoming unresponsive. While we could recover it by running waveInit again, it's easier to just dispose of it and treat it as an unloaded tab next time it gets switched to. This adds a timeout to each WaveTabView where once it gets assigned a tab ID, it has 1 second to respond "ready" or it will be destroyed. This should help prevent resource leakages for these dead views. --- emain/emain-tabview.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts index 4085521d8..aaac9b409 100644 --- a/emain/emain-tabview.ts +++ b/emain/emain-tabview.ts @@ -32,15 +32,18 @@ export function getWaveTabViewByWebContentsId(webContentsId: number): WaveTabVie export class WaveTabView extends WebContentsView { isActiveTab: boolean; waveWindowId: string; // set when showing in an active window - waveTabId: string; // always set, WaveTabViews are unique per tab + private _waveTabId: string; // always set, WaveTabViews are unique per tab lastUsedTs: number; // ts milliseconds createdTs: number; // ts milliseconds initPromise: Promise; + initResolve: () => void; savedInitOpts: WaveInitOpts; waveReadyPromise: Promise; - initResolve: () => void; waveReadyResolve: () => void; + // used to destroy the tab if it is not initialized within a certain time after being assigned a tabId + private destroyTabTimeout: NodeJS.Timeout; + constructor(fullConfig: FullConfigType) { console.log("createBareTabView"); super({ @@ -60,6 +63,13 @@ export class WaveTabView extends WebContentsView { this.waveReadyPromise = new Promise((resolve, _) => { this.waveReadyResolve = resolve; }); + + // Once the frontend is ready, we can cancel the destroyTabTimeout, assuming the tab hasn't been destroyed yet + // Only after a tab is ready will we add it to the wcvCache + this.waveReadyPromise.then(() => { + clearTimeout(this.destroyTabTimeout); + setWaveTabView(this.waveTabId, this); + }); wcIdToWaveTabMap.set(this.webContents.id, this); if (isDevVite) { this.webContents.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html}`); @@ -73,6 +83,17 @@ export class WaveTabView extends WebContentsView { this.setBackgroundColor(computeBgColor(fullConfig)); } + get waveTabId(): string { + return this._waveTabId; + } + + set waveTabId(waveTabId: string) { + this._waveTabId = waveTabId; + this.destroyTabTimeout = setTimeout(() => { + this.destroy(); + }, 1000); + } + positionTabOnScreen(winBounds: Rectangle) { const curBounds = this.getBounds(); if ( @@ -102,7 +123,7 @@ export class WaveTabView extends WebContentsView { destroy() { console.log("destroy tab", this.waveTabId); - this.webContents.close(); + this.webContents?.close(); removeWaveTabView(this.waveTabId); // TODO: circuitous @@ -171,7 +192,6 @@ export function getOrCreateWebViewForTab(fullConfig: FullConfigType, tabId: stri tabView = getSpareTab(fullConfig); tabView.lastUsedTs = Date.now(); tabView.waveTabId = tabId; - setWaveTabView(tabId, tabView); tabView.webContents.on("will-navigate", shNavHandler); tabView.webContents.on("will-frame-navigate", shFrameNavHandler); tabView.webContents.on("did-attach-webview", (event, wc) => {