Skip to content

Commit 9f6cdfd

Browse files
authored
closetab / tab destroy fixes (#1424)
1 parent 72ea582 commit 9f6cdfd

2 files changed

Lines changed: 121 additions & 72 deletions

File tree

emain/emain-tabview.ts

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
import { FileService } from "@/app/store/services";
55
import { adaptFromElectronKeyEvent } from "@/util/keyutil";
66
import { Rectangle, shell, WebContentsView } from "electron";
7+
import { getWaveWindowById } from "emain/emain-window";
78
import path from "path";
89
import { configureAuthKeyRequestInjection } from "./authkey";
910
import { setWasActive } from "./emain-activity";
1011
import { handleCtrlShiftFocus, handleCtrlShiftState, shFrameNavHandler, shNavHandler } from "./emain-util";
11-
import { waveWindowMap } from "./emain-window";
1212
import { getElectronAppBasePath, isDevVite } from "./platform";
1313

1414
function computeBgColor(fullConfig: FullConfigType): string {
@@ -31,8 +31,8 @@ export function getWaveTabViewByWebContentsId(webContentsId: number): WaveTabVie
3131
}
3232

3333
export class WaveTabView extends WebContentsView {
34+
waveWindowId: string; // this will be set for any tabviews that are initialized. (unset for the hot spare)
3435
isActiveTab: boolean;
35-
waveWindowId: string; // set when showing in an active window
3636
private _waveTabId: string; // always set, WaveTabViews are unique per tab
3737
lastUsedTs: number; // ts milliseconds
3838
createdTs: number; // ts milliseconds
@@ -43,9 +43,7 @@ export class WaveTabView extends WebContentsView {
4343
waveReadyResolve: () => void;
4444
isInitialized: boolean = false;
4545
isWaveReady: boolean = false;
46-
47-
// used to destroy the tab if it is not initialized within a certain time after being assigned a tabId
48-
private destroyTabTimeout: NodeJS.Timeout;
46+
isDestroyed: boolean = false;
4947

5048
constructor(fullConfig: FullConfigType) {
5149
console.log("createBareTabView");
@@ -67,13 +65,8 @@ export class WaveTabView extends WebContentsView {
6765
this.waveReadyPromise = new Promise((resolve, _) => {
6866
this.waveReadyResolve = resolve;
6967
});
70-
71-
// Once the frontend is ready, we can cancel the destroyTabTimeout, assuming the tab hasn't been destroyed yet
72-
// Only after a tab is ready will we add it to the wcvCache
7368
this.waveReadyPromise.then(() => {
7469
this.isWaveReady = true;
75-
clearTimeout(this.destroyTabTimeout);
76-
setWaveTabView(this.waveTabId, this);
7770
});
7871
wcIdToWaveTabMap.set(this.webContents.id, this);
7972
if (isDevVite) {
@@ -84,6 +77,7 @@ export class WaveTabView extends WebContentsView {
8477
this.webContents.on("destroyed", () => {
8578
wcIdToWaveTabMap.delete(this.webContents.id);
8679
removeWaveTabView(this.waveTabId);
80+
this.isDestroyed = true;
8781
});
8882
this.setBackgroundColor(computeBgColor(fullConfig));
8983
}
@@ -94,9 +88,6 @@ export class WaveTabView extends WebContentsView {
9488

9589
set waveTabId(waveTabId: string) {
9690
this._waveTabId = waveTabId;
97-
this.destroyTabTimeout = setTimeout(() => {
98-
this.destroy();
99-
}, 1000);
10091
}
10192

10293
positionTabOnScreen(winBounds: Rectangle) {
@@ -128,14 +119,11 @@ export class WaveTabView extends WebContentsView {
128119

129120
destroy() {
130121
console.log("destroy tab", this.waveTabId);
131-
this.webContents?.close();
132122
removeWaveTabView(this.waveTabId);
133-
134-
// TODO: circuitous
135-
const waveWindow = waveWindowMap.get(this.waveWindowId);
136-
if (waveWindow) {
137-
waveWindow.allLoadedTabViews.delete(this.waveTabId);
123+
if (!this.isDestroyed) {
124+
this.webContents?.close();
138125
}
126+
this.isDestroyed = true;
139127
}
140128
}
141129

@@ -155,6 +143,31 @@ export function getWaveTabView(waveTabId: string): WaveTabView | undefined {
155143
return rtn;
156144
}
157145

146+
function tryEvictEntry(waveTabId: string): boolean {
147+
const tabView = wcvCache.get(waveTabId);
148+
if (!tabView) {
149+
return false;
150+
}
151+
if (tabView.isActiveTab) {
152+
return false;
153+
}
154+
const lastUsedDiff = Date.now() - tabView.lastUsedTs;
155+
if (lastUsedDiff < 1000) {
156+
return false;
157+
}
158+
const ww = getWaveWindowById(tabView.waveWindowId);
159+
if (!ww) {
160+
// this shouldn't happen, but if it does, just destroy the tabview
161+
console.log("[error] WaveWindow not found for WaveTabView", tabView.waveTabId);
162+
tabView.destroy();
163+
return true;
164+
} else {
165+
// will trigger a destroy on the tabview
166+
ww.removeTabView(tabView.waveTabId, false);
167+
return true;
168+
}
169+
}
170+
158171
function checkAndEvictCache(): void {
159172
if (wcvCache.size <= MaxCacheSize) {
160173
return;
@@ -167,36 +180,31 @@ function checkAndEvictCache(): void {
167180
// Otherwise, sort by lastUsedTs
168181
return a.lastUsedTs - b.lastUsedTs;
169182
});
183+
const now = Date.now();
170184
for (let i = 0; i < sorted.length - MaxCacheSize; i++) {
171-
if (sorted[i].isActiveTab) {
172-
// don't evict WaveTabViews that are currently showing in a window
173-
continue;
174-
}
175-
const tabView = sorted[i];
176-
tabView?.destroy();
185+
tryEvictEntry(sorted[i].waveTabId);
177186
}
178187
}
179188

180189
export function clearTabCache() {
181190
const wcVals = Array.from(wcvCache.values());
182191
for (let i = 0; i < wcVals.length; i++) {
183192
const tabView = wcVals[i];
184-
if (tabView.isActiveTab) {
185-
continue;
186-
}
187-
tabView?.destroy();
193+
tryEvictEntry(tabView.waveTabId);
188194
}
189195
}
190196

191197
// returns [tabview, initialized]
192-
export async function getOrCreateWebViewForTab(tabId: string): Promise<[WaveTabView, boolean]> {
198+
export async function getOrCreateWebViewForTab(waveWindowId: string, tabId: string): Promise<[WaveTabView, boolean]> {
193199
let tabView = getWaveTabView(tabId);
194200
if (tabView) {
195201
return [tabView, true];
196202
}
197203
const fullConfig = await FileService.GetFullConfig();
198204
tabView = getSpareTab(fullConfig);
205+
tabView.waveWindowId = waveWindowId;
199206
tabView.lastUsedTs = Date.now();
207+
setWaveTabView(tabId, tabView);
200208
tabView.waveTabId = tabId;
201209
tabView.webContents.on("will-navigate", shNavHandler);
202210
tabView.webContents.on("will-frame-navigate", shFrameNavHandler);
@@ -231,11 +239,17 @@ export async function getOrCreateWebViewForTab(tabId: string): Promise<[WaveTabV
231239
}
232240

233241
export function setWaveTabView(waveTabId: string, wcv: WaveTabView): void {
242+
if (waveTabId == null) {
243+
return;
244+
}
234245
wcvCache.set(waveTabId, wcv);
235246
checkAndEvictCache();
236247
}
237248

238249
function removeWaveTabView(waveTabId: string): void {
250+
if (waveTabId == null) {
251+
return;
252+
}
239253
wcvCache.delete(waveTabId);
240254
}
241255

emain/emain-window.ts

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ async function getClientId() {
3838

3939
type TabSwitchQueueEntry =
4040
| {
41-
createTab: false;
41+
op: "switch";
4242
tabId: string;
4343
setInBackend: boolean;
4444
}
4545
| {
46-
createTab: true;
46+
op: "create";
4747
pinned: boolean;
48+
}
49+
| {
50+
op: "close";
51+
tabId: string;
4852
};
4953

5054
export class WaveBrowserWindow extends BaseWindow {
@@ -252,6 +256,11 @@ export class WaveBrowserWindow extends BaseWindow {
252256
console.log("win quitting or updating", this.waveWindowId);
253257
return;
254258
}
259+
waveWindowMap.delete(this.waveWindowId);
260+
if (focusedWaveWindow == this) {
261+
focusedWaveWindow = null;
262+
}
263+
this.removeAllChildViews();
255264
if (getGlobalIsRelaunching()) {
256265
console.log("win relaunching", this.waveWindowId);
257266
this.destroy();
@@ -266,17 +275,19 @@ export class WaveBrowserWindow extends BaseWindow {
266275
console.log("win removing window from backend DB", this.waveWindowId);
267276
fireAndForget(() => WindowService.CloseWindow(this.waveWindowId, true));
268277
}
269-
for (const tabView of this.allLoadedTabViews.values()) {
270-
tabView?.destroy();
271-
}
272-
waveWindowMap.delete(this.waveWindowId);
273-
if (focusedWaveWindow == this) {
274-
focusedWaveWindow = null;
275-
}
276278
});
277279
waveWindowMap.set(waveWindow.oid, this);
278280
}
279281

282+
removeAllChildViews() {
283+
for (const tabView of this.allLoadedTabViews.values()) {
284+
if (!this.isDestroyed()) {
285+
this.contentView.removeChildView(tabView);
286+
}
287+
tabView?.destroy();
288+
}
289+
}
290+
280291
async switchWorkspace(workspaceId: string) {
281292
console.log("switchWorkspace", workspaceId, this.waveWindowId);
282293
if (workspaceId == this.workspaceId) {
@@ -311,12 +322,7 @@ export class WaveBrowserWindow extends BaseWindow {
311322
return;
312323
}
313324
console.log("switchWorkspace newWs", newWs);
314-
if (this.allLoadedTabViews.size) {
315-
for (const tab of this.allLoadedTabViews.values()) {
316-
this.contentView.removeChildView(tab);
317-
tab?.destroy();
318-
}
319-
}
325+
this.removeAllChildViews();
320326
console.log("destroyed all tabs", this.waveWindowId);
321327
this.workspaceId = workspaceId;
322328
this.allLoadedTabViews = new Map();
@@ -329,22 +335,7 @@ export class WaveBrowserWindow extends BaseWindow {
329335
}
330336

331337
async closeTab(tabId: string) {
332-
console.log(`closeTab tabid=${tabId} ws=${this.workspaceId} window=${this.waveWindowId}`);
333-
const rtn = await WorkspaceService.CloseTab(this.workspaceId, tabId, true);
334-
if (rtn == null) {
335-
console.log("[error] closeTab: no return value", tabId, this.workspaceId, this.waveWindowId);
336-
return;
337-
}
338-
if (rtn.closewindow) {
339-
this.close();
340-
return;
341-
}
342-
if (!rtn.newactivetabid) {
343-
console.log("[error] closeTab, no new active tab", tabId, this.workspaceId, this.waveWindowId);
344-
return;
345-
}
346-
await this.setActiveTab(rtn.newactivetabid, false);
347-
this.allLoadedTabViews.delete(tabId);
338+
await this.queueCloseTab(tabId);
348339
}
349340

350341
async initializeTab(tabView: WaveTabView) {
@@ -447,11 +438,15 @@ export class WaveBrowserWindow extends BaseWindow {
447438
}
448439

449440
async queueTabSwitch(tabId: string, setInBackend: boolean) {
450-
await this._queueTabSwitchInternal({ createTab: false, tabId, setInBackend });
441+
await this._queueTabSwitchInternal({ op: "switch", tabId, setInBackend });
451442
}
452443

453444
async queueCreateTab(pinned = false) {
454-
await this._queueTabSwitchInternal({ createTab: true, pinned });
445+
await this._queueTabSwitchInternal({ op: "create", pinned });
446+
}
447+
448+
async queueCloseTab(tabId: string) {
449+
await this._queueTabSwitchInternal({ op: "close", tabId });
455450
}
456451

457452
async _queueTabSwitchInternal(entry: TabSwitchQueueEntry) {
@@ -466,6 +461,12 @@ export class WaveBrowserWindow extends BaseWindow {
466461
}
467462
}
468463

464+
removeTabViewLater(tabId: string, delayMs: number) {
465+
setTimeout(() => {
466+
this.removeTabView(tabId, false);
467+
}, 1000);
468+
}
469+
469470
// the queue and this function are used to serialize tab switches
470471
// [0] => the tab that is currently being switched to
471472
// [1] => the tab that will be switched to next
@@ -478,10 +479,10 @@ export class WaveBrowserWindow extends BaseWindow {
478479
const entry = this.tabSwitchQueue[0];
479480
let tabId: string = null;
480481
// have to use "===" here to get the typechecker to work :/
481-
if (entry.createTab === true) {
482+
if (entry.op === "create") {
482483
const { pinned } = entry;
483484
tabId = await WorkspaceService.CreateTab(this.workspaceId, null, true, pinned);
484-
} else if (entry.createTab === false) {
485+
} else if (entry.op === "switch") {
485486
let setInBackend: boolean = false;
486487
({ tabId, setInBackend } = entry);
487488
if (this.activeTabView?.waveTabId == tabId) {
@@ -490,11 +491,28 @@ export class WaveBrowserWindow extends BaseWindow {
490491
if (setInBackend) {
491492
await WorkspaceService.SetActiveTab(this.workspaceId, tabId);
492493
}
494+
} else if (entry.op === "close") {
495+
console.log("processTabSwitchQueue closeTab", entry.tabId);
496+
tabId = entry.tabId;
497+
const rtn = await WorkspaceService.CloseTab(this.workspaceId, tabId, true);
498+
if (rtn == null) {
499+
console.log("[error] closeTab: no return value", tabId, this.workspaceId, this.waveWindowId);
500+
return;
501+
}
502+
this.removeTabViewLater(tabId, 1000);
503+
if (rtn.closewindow) {
504+
this.close();
505+
return;
506+
}
507+
if (!rtn.newactivetabid) {
508+
return;
509+
}
510+
tabId = rtn.newactivetabid;
493511
}
494512
if (tabId == null) {
495513
return;
496514
}
497-
const [tabView, tabInitialized] = await getOrCreateWebViewForTab(tabId);
515+
const [tabView, tabInitialized] = await getOrCreateWebViewForTab(this.waveWindowId, tabId);
498516
await this.setTabViewIntoWindow(tabView, tabInitialized);
499517
} catch (e) {
500518
console.log("error caught in processTabSwitchQueue", e);
@@ -520,6 +538,22 @@ export class WaveBrowserWindow extends BaseWindow {
520538
}
521539
}
522540

541+
removeTabView(tabId: string, force: boolean) {
542+
if (!force && this.activeTabView?.waveTabId == tabId) {
543+
console.log("cannot remove active tab", tabId, this.waveWindowId);
544+
return;
545+
}
546+
const tabView = this.allLoadedTabViews.get(tabId);
547+
if (tabView == null) {
548+
console.log("removeTabView -- tabView not found", tabId, this.waveWindowId);
549+
// the tab was never loaded, so just return
550+
return;
551+
}
552+
this.contentView.removeChildView(tabView);
553+
this.allLoadedTabViews.delete(tabId);
554+
tabView.destroy();
555+
}
556+
523557
destroy() {
524558
console.log("destroy win", this.waveWindowId);
525559
this.deleteAllowed = true;
@@ -607,9 +641,7 @@ ipcMain.on("close-tab", async (event, workspaceId, tabId) => {
607641
console.log(`close-tab: no window found for workspace ws=${workspaceId} tab=${tabId}`);
608642
return;
609643
}
610-
if (ww != null) {
611-
await ww.closeTab(tabId);
612-
}
644+
await ww.queueCloseTab(tabId);
613645
event.returnValue = true;
614646
return null;
615647
});
@@ -685,9 +717,12 @@ export async function relaunchBrowserWindows() {
685717
console.log("relaunchBrowserWindows");
686718
setGlobalIsRelaunching(true);
687719
const windows = getAllWaveWindows();
688-
for (const window of windows) {
689-
console.log("relaunch -- closing window", window.waveWindowId);
690-
window.close();
720+
if (windows.length > 0) {
721+
for (const window of windows) {
722+
console.log("relaunch -- closing window", window.waveWindowId);
723+
window.close();
724+
}
725+
await delay(1200);
691726
}
692727
setGlobalIsRelaunching(false);
693728

0 commit comments

Comments
 (0)