1// @effect-diagnostics globalTimers:off -- The helper timeout runs at a worker callback boundary outside any Effect fiber.
2// @effect-diagnostics nodeBuiltinImport:off -- This desktop-only helper owns a Node worker.
3
4import * as NodeWorkerThreads from "node:worker_threads";
5
6import type * as Electron from "electron";
7
8const FOCUS_TIMEOUT_MS = 1_000;
9
10export type WindowsForegroundFocusTarget = {
11 readonly windowId: number;
12 readonly processId: number;
13 readonly title: string;
14 readonly bounds: Electron.Rectangle;
15 readonly contentBounds: Electron.Rectangle;
16};
17
18export type WindowsForegroundFocusThread = {
19 readonly prepare: (target: WindowsForegroundFocusTarget) => Promise<boolean>;
20 readonly focus: (target: WindowsForegroundFocusTarget) => Promise<boolean>;
21 readonly close: () => void;
22};
23
24type FocusRequest = {
25 readonly type: "prepare" | "focus";
26 readonly requestId: number;
27 readonly target: WindowsForegroundFocusTarget;
28};
29
30type FocusResult = {
31 readonly type: "result";
32 readonly requestId: number;
33 readonly focused: boolean;
34};
35
36const unavailableThread = (): WindowsForegroundFocusThread => ({
37 prepare: async () => false,
38 focus: async () => false,
39 close: () => undefined,
40});
41
42export function startWindowsForegroundFocusThread(
43 workerPath: string,
44): WindowsForegroundFocusThread {
45 let worker: NodeWorkerThreads.Worker | undefined;
46 let ready = false;
47 let closed = false;
48 let nextRequestId = 1;
49 const pending = new Map<
50 number,
51 {
52 readonly request: FocusRequest;
53 readonly resolve: (focused: boolean) => void;
54 readonly timeout: ReturnType<typeof setTimeout>;
55 }
56 >();
57
58 const finish = (requestId: number, focused: boolean) => {
59 const request = pending.get(requestId);
60 if (!request) return;
61 pending.delete(requestId);
62 clearTimeout(request.timeout);
63 request.resolve(focused);
64 };
65 const send = (request: FocusRequest, targetWorker: NodeWorkerThreads.Worker) => {
66 if (!ready || closed || worker !== targetWorker) return;
67 try {
68 // oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node workers do not accept a target origin.
69 targetWorker.postMessage(request);
70 } catch {
71 finish(request.requestId, false);
72 }
73 };
74 const start = (): boolean => {
75 if (closed) return false;
76 let nextWorker: NodeWorkerThreads.Worker;
77 try {
78 nextWorker = new NodeWorkerThreads.Worker(workerPath);
79 nextWorker.unref();
80 } catch {
81 return false;
82 }
83
84 worker = nextWorker;
85 ready = false;
86 const reset = () => {
87 if (worker !== nextWorker) return;
88 worker = undefined;
89 ready = false;
90 for (const requestId of pending.keys()) finish(requestId, false);
91 };
92 nextWorker.on("message", (rawMessage) => {
93 if (worker !== nextWorker) return;
94 if (rawMessage === "ready") {
95 ready = true;
96 for (const request of pending.values()) send(request.request, nextWorker);
97 return;
98 }
99 const message = rawMessage as FocusResult;
100 if (message.type === "result") finish(message.requestId, message.focused);
101 });
102 nextWorker.once("error", reset);
103 nextWorker.once("exit", reset);
104 return true;
105 };
106
107 if (!start()) return unavailableThread();
108
109 const restart = (timedOutWorker: NodeWorkerThreads.Worker | undefined) => {
110 if (!timedOutWorker || worker !== timedOutWorker) return;
111 worker = undefined;
112 ready = false;
113 for (const requestId of pending.keys()) finish(requestId, false);
114 void timedOutWorker.terminate();
115 start();
116 };
117 const request = (type: FocusRequest["type"], target: WindowsForegroundFocusTarget) =>
118 new Promise<boolean>((resolve) => {
119 if (closed || (!worker && !start())) {
120 resolve(false);
121 return;
122 }
123 const requestId = nextRequestId++;
124 const focusRequest = { type, requestId, target } satisfies FocusRequest;
125 const requestWorker = worker!;
126 const timeout = setTimeout(() => {
127 finish(requestId, false);
128 restart(requestWorker);
129 }, FOCUS_TIMEOUT_MS);
130 timeout.unref();
131 pending.set(requestId, { request: focusRequest, resolve, timeout });
132 send(focusRequest, requestWorker);
133 });
134
135 return {
136 prepare: (target) => request("prepare", target),
137 focus: (target) => request("focus", target),
138 close: () => {
139 if (closed) return;
140 closed = true;
141 const activeWorker = worker;
142 worker = undefined;
143 ready = false;
144 for (const requestId of pending.keys()) finish(requestId, false);
145 if (activeWorker) void activeWorker.terminate();
146 },
147 };
148}