| 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 | |
| 4 | import * as NodeWorkerThreads from "node:worker_threads"; |
| 5 | |
| 6 | import type * as Electron from "electron"; |
| 7 | |
| 8 | const FOCUS_TIMEOUT_MS = 1_000; |
| 9 | |
| 10 | export 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 | |
| 18 | export type WindowsForegroundFocusThread = { |
| 19 | readonly prepare: (target: WindowsForegroundFocusTarget) => Promise<boolean>; |
| 20 | readonly focus: (target: WindowsForegroundFocusTarget) => Promise<boolean>; |
| 21 | readonly close: () => void; |
| 22 | }; |
| 23 | |
| 24 | type FocusRequest = { |
| 25 | readonly type: "prepare" | "focus"; |
| 26 | readonly requestId: number; |
| 27 | readonly target: WindowsForegroundFocusTarget; |
| 28 | }; |
| 29 | |
| 30 | type FocusResult = { |
| 31 | readonly type: "result"; |
| 32 | readonly requestId: number; |
| 33 | readonly focused: boolean; |
| 34 | }; |
| 35 | |
| 36 | const unavailableThread = (): WindowsForegroundFocusThread => ({ |
| 37 | prepare: async () => false, |
| 38 | focus: async () => false, |
| 39 | close: () => undefined, |
| 40 | }); |
| 41 | |
| 42 | export 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 | } |