/** * Main-thread driver for the Win32 folder dialog: spawns the dialog worker * (which blocks inside the modal `Show`), maps its message protocol onto a * promise, and services aborts by posting `WM_CLOSE` to the dialog thread's * windows until the worker reports back. The real worker/window surface is * injectable so every driver path is testable on any platform. */ import { closeThreadWindows as hostCloseThreadWindows, spawnDialogWorker } from './win32-dialog-host.ts' import type { Win32DialogWorkerData, Win32DialogWorkerMessage } from './win32-dialog-worker.ts' /** The worker surface the driver drives (satisfied by `node:worker_threads`). */ export interface Win32DialogWorkerLike { /** * Subscribe to a worker event. * @param event - `message`, `error`, or `exit`. * @param listener - the event consumer. */ on(event: 'message', listener: (message: Win32DialogWorkerMessage) => void): unknown on(event: 'error', listener: (error: Error) => void): unknown on(event: 'exit', listener: (code: number) => void): unknown /** * Force-stop the worker; the abort path's last resort when `WM_CLOSE` * never lands (e.g. the dialog window was never created). * @returns settles when the thread is gone. */ terminate(): Promise /** * Release the event-loop reference. Called once the pick settles so a * worker stuck in the native modal call (terminate cannot interrupt * native code) never blocks process exit. */ unref?(): void } /** Injectable process surface for deterministic driver tests. */ export interface Win32DialogInternals { /** Replaces the real worker spawn (`win32-dialog-host.ts`). */ spawnWorker?: (data: Win32DialogWorkerData) => Win32DialogWorkerLike /** Replaces the real `WM_CLOSE` poster (`win32-dialog-host.ts`). */ closeThreadWindows?: (threadId: number) => Promise /** Abort-service cadence override so tests never wait wall-clock time. */ closeRetryMs?: number } /** The dialog title every host shows. */ export const DIALOG_TITLE = 'Select Workspace Directory' /** `WM_CLOSE` re-post cadence while an abort waits for the worker to unwind. */ const CLOSE_RETRY_MS = 150 /** Abort-service attempts before force-terminating the worker. */ const CLOSE_MAX_ATTEMPTS = 20 /** * Open the modern Win32 folder picker off the event loop. * @param signal - caller lifetime; abort closes the dialog and rejects. * @param internals - worker/window seams for deterministic tests. * @returns the selected path, or null when the user cancels. */ export async function pickWin32Directory( signal: AbortSignal, internals: Win32DialogInternals = {}, ): Promise { if (signal.aborted) throw new Error('native directory picker aborted') const spawnWorker = internals.spawnWorker ?? spawnDialogWorker const closeWindows = internals.closeThreadWindows ?? hostCloseThreadWindows const closeRetryMs = internals.closeRetryMs ?? CLOSE_RETRY_MS const worker = spawnWorker({ title: DIALOG_TITLE }) let dialogThreadId: number | undefined let closeTimer: NodeJS.Timeout | undefined let settled = false return await new Promise((resolve, reject) => { const settle = (outcome: () => void): void => { if (settled) return settled = true if (closeTimer !== undefined) clearInterval(closeTimer) signal.removeEventListener('abort', onAbort) worker.unref?.() outcome() } const serviceAbort = (): void => { let attempts = 0 // The `showing` notice precedes the blocking `Show`, so the very first // WM_CLOSE can race the window's creation; re-post until the worker // reports back, then force-terminate as a last resort. closeTimer = setInterval(() => { attempts += 1 if (attempts > CLOSE_MAX_ATTEMPTS) { settle(() => { void worker.terminate() reject(new Error('native directory picker aborted (dialog unresponsive; worker terminated)')) }) return } void closeWindows(dialogThreadId as number).catch(() => undefined) }, closeRetryMs) void closeWindows(dialogThreadId as number).catch(() => undefined) } const onAbort = (): void => { if (dialogThreadId !== undefined) serviceAbort() // Not shown yet: the `showing` handler below starts the service loop. } signal.addEventListener('abort', onAbort, { once: true }) worker.on('message', (message: Win32DialogWorkerMessage) => { switch (message.kind) { case 'showing': dialogThreadId = message.threadId if (signal.aborted) serviceAbort() return case 'done': settle(() => { if (signal.aborted) reject(new Error('native directory picker aborted')) else resolve(message.path) }) return case 'error': settle(() =>{ reject(new Error(`win32 folder dialog failed: ${message.message}`)) }) } }) worker.on('error', (error: Error) =>{ settle(() =>{ reject(error) }) }) worker.on('exit', () =>{ settle(() =>{ reject(new Error('win32 folder dialog worker exited before reporting a result')) }) }) }) }