Files
geo/apps/desktop-client/src/main/view-pool.ts
T

148 lines
3.4 KiB
TypeScript
Raw Normal View History

import { WebContentsView } from "electron/main";
import type { WebContentsView as ElectronWebContentsView } from "electron/main";
import { maybeOpenExecutionDevtools } from "./execution-devtools";
export interface HotViewHandle {
accountId: string;
view: ElectronWebContentsView;
activatedAt: number;
lastReleasedAt: number | null;
retainCount: number;
}
const hotViews = new Map<string, HotViewHandle>();
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?.();
}
export function acquireHotView(accountId: string): HotViewHandle {
startHotViewReaper();
const existing = hotViews.get(accountId);
if (existing) {
return touchHandle(existing);
}
const handle: HotViewHandle = {
accountId,
view: new WebContentsView({
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
}),
activatedAt: Date.now(),
lastReleasedAt: Date.now(),
retainCount: 0,
};
maybeOpenExecutionDevtools(handle.view.webContents, `hot-view:${accountId}`, { background: true });
hotViews.set(accountId, handle);
return handle;
}
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 {
const handle = hotViews.get(accountId);
if (!handle) {
return;
}
if (handle.retainCount > 0) {
handle.retainCount -= 1;
}
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);
}
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 }) => ({
accountId,
activatedAt,
lastReleasedAt,
retainCount,
}));
}
export function getHotViewPoolPolicy() {
return {
idleTTLms: HOT_VIEW_IDLE_TTL_MS,
reapIntervalMs: HOT_VIEW_REAPER_INTERVAL_MS,
};
}