Home

mweinbach / agent-coworker

publicmweinbach/agent-coworker
Overview Code History Branches Pull requestsIssuesInsights
main
HomeOverview Code PRsIssues
mweinbach/agent-coworker/apps/desktop/electron/services/windowCloseCoordinator.ts
Raw
1import { randomUUID } from "node:crypto";2 3import { DESKTOP_EVENT_CHANNELS, type WindowCloseResponseInput } from "../../src/lib/desktopApi";4 5type NativeCloseEvent = {6  preventDefault(): void;
7
};
8 
9export type NativeCloseWebContents = {
10 id: number;
11 send(channel: string, payload: unknown): void;
12};
13 
14export type NativeCloseWindow = {
15 webContents: NativeCloseWebContents;
16 isDestroyed(): boolean;
17 on(event: "close" | "closed", listener: (event?: NativeCloseEvent) => void): void;
18 off(event: "close" | "closed", listener: (event?: NativeCloseEvent) => void): void;
19 close(): void;
20};
21 
22type NativeWindowCloseCoordinatorOptions = {
23 createRequestId?: () => string;
24};
25 
26type TrackedWindow = {
27 window: NativeCloseWindow;
28 pendingRequestId: string | null;
29 allowNextClose: boolean;
30 closeListener: (event?: NativeCloseEvent) => void;
31 closedListener: () => void;
32};
33 
34export class NativeWindowCloseCoordinator {
35 private readonly trackedByWebContentsId = new Map<number, TrackedWindow>();
36 private readonly createRequestId: () => string;
37 
38 constructor(options: NativeWindowCloseCoordinatorOptions = {}) {
39 this.createRequestId = options.createRequestId ?? randomUUID;
40 }
41 
42 track(window: NativeCloseWindow): () => void {
43 const webContentsId = window.webContents.id;
44 this.untrack(webContentsId);
45 const tracked: TrackedWindow = {
46 window,
47 pendingRequestId: null,
48 allowNextClose: false,
49 closeListener: (event) => {
50 if (!event) {
51 return;
52 }
53 if (tracked.allowNextClose) {
54 tracked.allowNextClose = false;
55 return;
56 }
57 event.preventDefault();
58 if (tracked.pendingRequestId || window.isDestroyed()) {
59 return;
60 }
61 const requestId = this.createRequestId();
62 tracked.pendingRequestId = requestId;
63 window.webContents.send(DESKTOP_EVENT_CHANNELS.windowCloseRequested, { requestId });
64 },
65 closedListener: () => {
66 this.untrack(webContentsId);
67 },
68 };
69 this.trackedByWebContentsId.set(webContentsId, tracked);
70 window.on("close", tracked.closeListener);
71 window.on("closed", tracked.closedListener);
72 return () => this.untrack(webContentsId);
73 }
74 
75 resolve(sender: NativeCloseWebContents, response: WindowCloseResponseInput): void {
76 const tracked = this.trackedByWebContentsId.get(sender.id);
77 if (
78 !tracked ||
79 tracked.window.webContents !== sender ||
80 tracked.pendingRequestId !== response.requestId
81 ) {
82 return;
83 }
84 tracked.pendingRequestId = null;
85 if (!response.canClose || tracked.window.isDestroyed()) {
86 return;
87 }
88 tracked.allowNextClose = true;
89 tracked.window.close();
90 }
91 
92 private untrack(webContentsId: number): void {
93 const tracked = this.trackedByWebContentsId.get(webContentsId);
94 if (!tracked) {
95 return;
96 }
97 this.trackedByWebContentsId.delete(webContentsId);
98 tracked.window.off("close", tracked.closeListener);
99 tracked.window.off("closed", tracked.closedListener);
100 }
101}
102