2026-04-19 14:18:20 +08:00
|
|
|
import { WebContentsView } from "electron/main";
|
|
|
|
|
import type { WebContentsView as ElectronWebContentsView } from "electron/main";
|
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
import { maybeOpenExecutionDevtools } from "./execution-devtools";
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
export interface HotViewHandle {
|
|
|
|
|
accountId: string;
|
|
|
|
|
view: ElectronWebContentsView;
|
|
|
|
|
activatedAt: number;
|
2026-04-20 11:07:58 +08:00
|
|
|
lastReleasedAt: number | null;
|
|
|
|
|
retainCount: number;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hotViews = new Map<string, HotViewHandle>();
|
2026-04-20 11:07:58 +08:00
|
|
|
const HOT_VIEW_IDLE_TTL_MS = 5 * 60_000;
|
|
|
|
|
const HOT_VIEW_REAPER_INTERVAL_MS = 60_000;
|
|
|
|
|
let reaperHandle: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
|
|
|
|
function closeView(handle: HotViewHandle): void {
|
|
|
|
|
if (!handle.view.webContents.isDestroyed()) {
|
|
|
|
|
handle.view.webContents.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function touchHandle(handle: HotViewHandle): HotViewHandle {
|
|
|
|
|
handle.activatedAt = Date.now();
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function demoteViewInternal(accountId: string): boolean {
|
|
|
|
|
const handle = hotViews.get(accountId);
|
|
|
|
|
if (!handle) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeView(handle);
|
|
|
|
|
hotViews.delete(accountId);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function startHotViewReaper(): void {
|
|
|
|
|
if (reaperHandle) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reaperHandle = setInterval(() => {
|
|
|
|
|
reapIdleHotViews();
|
|
|
|
|
}, HOT_VIEW_REAPER_INTERVAL_MS);
|
|
|
|
|
reaperHandle.unref?.();
|
|
|
|
|
}
|
2026-04-19 14:18:20 +08:00
|
|
|
|
|
|
|
|
export function acquireHotView(accountId: string): HotViewHandle {
|
2026-04-20 11:07:58 +08:00
|
|
|
startHotViewReaper();
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
const existing = hotViews.get(accountId);
|
|
|
|
|
if (existing) {
|
2026-04-20 11:07:58 +08:00
|
|
|
return touchHandle(existing);
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handle: HotViewHandle = {
|
|
|
|
|
accountId,
|
|
|
|
|
view: new WebContentsView({
|
|
|
|
|
webPreferences: {
|
|
|
|
|
sandbox: true,
|
|
|
|
|
contextIsolation: true,
|
|
|
|
|
nodeIntegration: false,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
activatedAt: Date.now(),
|
2026-04-20 11:07:58 +08:00
|
|
|
lastReleasedAt: Date.now(),
|
|
|
|
|
retainCount: 0,
|
2026-04-19 14:18:20 +08:00
|
|
|
};
|
2026-04-22 00:24:21 +08:00
|
|
|
maybeOpenExecutionDevtools(handle.view.webContents, `hot-view:${accountId}`, { background: true });
|
2026-04-19 14:18:20 +08:00
|
|
|
hotViews.set(accountId, handle);
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 11:07:58 +08:00
|
|
|
export function retainHotView(accountId: string): HotViewHandle {
|
|
|
|
|
const handle = touchHandle(acquireHotView(accountId));
|
|
|
|
|
handle.retainCount += 1;
|
|
|
|
|
handle.lastReleasedAt = null;
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function releaseHotView(accountId: string): void {
|
2026-04-19 14:18:20 +08:00
|
|
|
const handle = hotViews.get(accountId);
|
|
|
|
|
if (!handle) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 11:07:58 +08:00
|
|
|
if (handle.retainCount > 0) {
|
|
|
|
|
handle.retainCount -= 1;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
2026-04-20 11:07:58 +08:00
|
|
|
|
|
|
|
|
if (handle.retainCount === 0) {
|
|
|
|
|
handle.lastReleasedAt = Date.now();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function demoteView(accountId: string): boolean {
|
|
|
|
|
const handle = hotViews.get(accountId);
|
|
|
|
|
if (!handle || handle.retainCount > 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return demoteViewInternal(accountId);
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 11:07:58 +08:00
|
|
|
export function reapIdleHotViews(
|
|
|
|
|
now = Date.now(),
|
|
|
|
|
idleTTLms = HOT_VIEW_IDLE_TTL_MS,
|
|
|
|
|
): string[] {
|
|
|
|
|
const reclaimed: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const [accountId, handle] of hotViews.entries()) {
|
|
|
|
|
if (handle.retainCount > 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const idleSince = handle.lastReleasedAt ?? handle.activatedAt;
|
|
|
|
|
if (now - idleSince < idleTTLms) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (demoteViewInternal(accountId)) {
|
|
|
|
|
reclaimed.push(accountId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return reclaimed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listHotViews(): Array<Pick<HotViewHandle, "accountId" | "activatedAt" | "lastReleasedAt" | "retainCount">> {
|
|
|
|
|
return [...hotViews.values()].map(({ accountId, activatedAt, lastReleasedAt, retainCount }) => ({
|
2026-04-19 14:18:20 +08:00
|
|
|
accountId,
|
|
|
|
|
activatedAt,
|
2026-04-20 11:07:58 +08:00
|
|
|
lastReleasedAt,
|
|
|
|
|
retainCount,
|
2026-04-19 14:18:20 +08:00
|
|
|
}));
|
|
|
|
|
}
|
2026-04-20 11:07:58 +08:00
|
|
|
|
|
|
|
|
export function getHotViewPoolPolicy() {
|
|
|
|
|
return {
|
|
|
|
|
idleTTLms: HOT_VIEW_IDLE_TTL_MS,
|
|
|
|
|
reapIntervalMs: HOT_VIEW_REAPER_INTERVAL_MS,
|
|
|
|
|
};
|
|
|
|
|
}
|