Skip to content

Commit c4000a3

Browse files
perf(desktop): load Windows foreground helpers only for capture reveals
ElectronWindow started a worker thread that imports the xa11y native module at construction, and every reveal on Windows loaded ffi-rs and opened user32. Users who never capture paid both at launch. The worker now starts on the first capture reveal, and only a window that had prepareReveal called on it takes the Win32 foreground path; ordinary reveals keep Electron's native behaviour from main. Renderer events are sent before the reveal runs and the reveal is best effort, so a foreground refusal on Windows no longer drops the message or duplicates it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 7bc2113 commit c4000a3

4 files changed

Lines changed: 117 additions & 25 deletions

File tree

apps/desktop/src/electron/ElectronWindow.test.ts

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
nativeAppByPidMock,
1818
nativeAppListMock,
1919
shellHostedForegroundMock,
20+
startWindowsForegroundFocusThreadMock,
2021
windowsForegroundFocusMock,
2122
windowsForegroundPrepareMock,
2223
windowsForegroundCloseMock,
@@ -30,6 +31,7 @@ const {
3031
nativeAppByPidMock: vi.fn(),
3132
nativeAppListMock: vi.fn(),
3233
shellHostedForegroundMock: vi.fn(),
34+
startWindowsForegroundFocusThreadMock: vi.fn(),
3335
windowsForegroundFocusMock: vi.fn(),
3436
windowsForegroundPrepareMock: vi.fn(),
3537
windowsForegroundCloseMock: vi.fn(),
@@ -43,11 +45,7 @@ vi.mock("./WindowsForeground.ts", () => ({
4345
}));
4446

4547
vi.mock("./WindowsForegroundFocusThread.ts", () => ({
46-
startWindowsForegroundFocusThread: () => ({
47-
prepare: windowsForegroundPrepareMock,
48-
focus: windowsForegroundFocusMock,
49-
close: windowsForegroundCloseMock,
50-
}),
48+
startWindowsForegroundFocusThread: startWindowsForegroundFocusThreadMock,
5149
}));
5250

5351
vi.mock("@crowecawcaw/xa11y", () => ({
@@ -110,6 +108,11 @@ describe("ElectronWindow", () => {
110108
nativeAppByPidMock.mockReset();
111109
nativeAppListMock.mockReset().mockResolvedValue([]);
112110
shellHostedForegroundMock.mockReset().mockResolvedValue(false);
111+
startWindowsForegroundFocusThreadMock.mockReset().mockReturnValue({
112+
prepare: windowsForegroundPrepareMock,
113+
focus: windowsForegroundFocusMock,
114+
close: windowsForegroundCloseMock,
115+
});
113116
windowsForegroundFocusMock.mockReset().mockResolvedValue(false);
114117
windowsForegroundPrepareMock.mockReset().mockResolvedValue(false);
115118
windowsForegroundCloseMock.mockReset();
@@ -261,6 +264,7 @@ describe("ElectronWindow", () => {
261264
focus: vi.fn(() => operations.push("focus")),
262265
} as unknown as Electron.BrowserWindow;
263266
const electronWindow = yield* ElectronWindow.ElectronWindow;
267+
yield* electronWindow.prepareReveal(window);
264268

265269
yield* electronWindow.reveal(window);
266270

@@ -337,6 +341,7 @@ describe("ElectronWindow", () => {
337341
} as unknown as Electron.BrowserWindow;
338342

339343
const electronWindow = yield* ElectronWindow.ElectronWindow;
344+
yield* electronWindow.prepareReveal(window);
340345
const revealFiber = yield* electronWindow.reveal(window).pipe(
341346
Effect.andThen(
342347
Effect.sync(() => {
@@ -391,6 +396,7 @@ describe("ElectronWindow", () => {
391396
} as unknown as Electron.BrowserWindow;
392397
appFocusMock.mockImplementation(() => operations.push("app-focus"));
393398
const electronWindow = yield* ElectronWindow.ElectronWindow;
399+
yield* electronWindow.prepareReveal(window);
394400

395401
yield* electronWindow.reveal(window);
396402

@@ -459,6 +465,8 @@ describe("ElectronWindow", () => {
459465
window.getNativeWindowHandle.mockReturnValue(handle);
460466
activeWindowMock.mockResolvedValue({ id: hwnd, owner: { processId: process.pid } });
461467
const electronWindow = yield* ElectronWindow.ElectronWindow;
468+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
469+
window.getTitle.mockClear();
462470

463471
yield* electronWindow.reveal(window as unknown as Electron.BrowserWindow);
464472

@@ -488,6 +496,7 @@ describe("ElectronWindow", () => {
488496
},
489497
]);
490498
const electronWindow = yield* ElectronWindow.ElectronWindow;
499+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
491500

492501
yield* electronWindow.reveal(window as unknown as Electron.BrowserWindow);
493502

@@ -510,6 +519,7 @@ describe("ElectronWindow", () => {
510519
},
511520
]);
512521
const electronWindow = yield* ElectronWindow.ElectronWindow;
522+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
513523

514524
yield* electronWindow.reveal(window as unknown as Electron.BrowserWindow);
515525

@@ -533,6 +543,7 @@ describe("ElectronWindow", () => {
533543
},
534544
]);
535545
const electronWindow = yield* ElectronWindow.ElectronWindow;
546+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
536547

537548
yield* electronWindow.reveal(window as unknown as Electron.BrowserWindow);
538549

@@ -546,6 +557,7 @@ describe("ElectronWindow", () => {
546557
const window = makeWindowsRevealWindow();
547558
activateWindowsForegroundMock.mockRejectedValue(cause);
548559
const electronWindow = yield* ElectronWindow.ElectronWindow;
560+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
549561

550562
const exit = yield* Effect.exit(
551563
electronWindow.reveal(window as unknown as Electron.BrowserWindow),
@@ -578,6 +590,8 @@ describe("ElectronWindow", () => {
578590
return foreground.promise;
579591
});
580592
const electronWindow = yield* ElectronWindow.ElectronWindow;
593+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
594+
window.getTitle.mockClear();
581595

582596
const revealFiber = yield* electronWindow
583597
.reveal(window as unknown as Electron.BrowserWindow)
@@ -610,6 +624,7 @@ describe("ElectronWindow", () => {
610624
new Error("Windows initially refused foreground activation"),
611625
);
612626
const electronWindow = yield* ElectronWindow.ElectronWindow;
627+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
613628

614629
const revealFiber = yield* electronWindow
615630
.reveal(window as unknown as Electron.BrowserWindow)
@@ -684,9 +699,49 @@ describe("ElectronWindow", () => {
684699
assert.equal(vi.mocked(laterWindow.destroy).mock.calls.length, 1);
685700
}).pipe(Effect.provide(TestLayer)),
686701
);
687-
it.effect("closes the Windows focus worker when its layer is released", () =>
702+
it.effect("an ordinary reveal on Windows does not touch the Win32 foreground helpers", () =>
703+
Effect.gen(function* () {
704+
const window = makeWindowsRevealWindow();
705+
const electronWindow = yield* ElectronWindow.ElectronWindow;
706+
707+
yield* electronWindow.reveal(window as unknown as Electron.BrowserWindow);
708+
709+
assert.lengthOf(activateWindowsForegroundMock.mock.calls, 0);
710+
assert.lengthOf(windowsForegroundFocusMock.mock.calls, 0);
711+
assert.lengthOf(shellHostedForegroundMock.mock.calls, 0);
712+
assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 0);
713+
assert.lengthOf(window.focus.mock.calls, 1);
714+
}).pipe(Effect.provide(testLayer("win32"))),
715+
);
716+
717+
it.effect("a capture reveal on Windows uses the Win32 path only once", () =>
718+
Effect.gen(function* () {
719+
const window = makeWindowsRevealWindow();
720+
const electronWindow = yield* ElectronWindow.ElectronWindow;
721+
722+
yield* electronWindow.prepareReveal(window as unknown as Electron.BrowserWindow);
723+
yield* electronWindow.reveal(window as unknown as Electron.BrowserWindow);
724+
yield* electronWindow.reveal(window as unknown as Electron.BrowserWindow);
725+
726+
assert.lengthOf(activateWindowsForegroundMock.mock.calls, 1);
727+
assert.lengthOf(window.focus.mock.calls, 2);
728+
}).pipe(Effect.provide(testLayer("win32"))),
729+
);
730+
731+
it.effect("starts the Windows focus worker lazily and closes it with the layer", () =>
688732
Effect.gen(function* () {
689733
yield* ElectronWindow.ElectronWindow.pipe(Effect.provide(testLayer("win32")));
734+
assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 0);
735+
assert.lengthOf(windowsForegroundCloseMock.mock.calls, 0);
736+
737+
yield* Effect.gen(function* () {
738+
const electronWindow = yield* ElectronWindow.ElectronWindow;
739+
const window = makeWindowsRevealWindow() as unknown as Electron.BrowserWindow;
740+
yield* electronWindow.prepareReveal(window);
741+
yield* electronWindow.prepareReveal(window);
742+
assert.lengthOf(startWindowsForegroundFocusThreadMock.mock.calls, 1);
743+
assert.lengthOf(windowsForegroundCloseMock.mock.calls, 0);
744+
}).pipe(Effect.provide(testLayer("win32")));
690745
assert.lengthOf(windowsForegroundCloseMock.mock.calls, 1);
691746
}),
692747
);

apps/desktop/src/electron/ElectronWindow.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import * as Ref from "effect/Ref";
1313
import * as Schema from "effect/Schema";
1414

1515
import * as Electron from "electron";
16-
import { activeWindow } from "get-windows";
1716

1817
import { activateWindowsForeground, isWindowsShellHostedForeground } from "./WindowsForeground.ts";
1918
import { startWindowsForegroundFocusThread } from "./WindowsForegroundFocusThread.ts";
@@ -29,6 +28,7 @@ function windowsForegroundFocusTarget(window: Electron.BrowserWindow) {
2928
}
3029

3130
async function isWindowsBrowserWindowForeground(window: Electron.BrowserWindow): Promise<boolean> {
31+
const { activeWindow } = await import("get-windows");
3232
const foreground = await activeWindow().catch(() => undefined);
3333
if (window.isDestroyed() || foreground?.owner.processId !== process.pid) return false;
3434
const handle = window.getNativeWindowHandle();
@@ -154,15 +154,18 @@ export class ElectronWindow extends Context.Service<
154154

155155
export const make = Effect.gen(function* () {
156156
const platform = yield* HostProcessPlatform;
157-
const windowsForegroundFocus =
158-
platform === "win32"
159-
? startWindowsForegroundFocusThread(
160-
NodePath.join(__dirname, "electron", "WindowsForegroundFocusWorker.cjs"),
161-
)
162-
: undefined;
163-
if (windowsForegroundFocus) {
164-
yield* Effect.addFinalizer(() => Effect.sync(() => windowsForegroundFocus.close()));
165-
}
157+
// The focus worker loads a native accessibility module. Start it on the first
158+
// capture reveal so users who never capture pay nothing at launch.
159+
let windowsForegroundFocus: ReturnType<typeof startWindowsForegroundFocusThread> | undefined;
160+
const ensureWindowsForegroundFocus = () => {
161+
windowsForegroundFocus ??= startWindowsForegroundFocusThread(
162+
NodePath.join(__dirname, "electron", "WindowsForegroundFocusWorker.cjs"),
163+
);
164+
return windowsForegroundFocus;
165+
};
166+
// Tracks a capture reveal in flight. Ordinary reveals keep Electron's native path.
167+
const captureRevealWindows = new Set<number>();
168+
yield* Effect.addFinalizer(() => Effect.sync(() => windowsForegroundFocus?.close()));
166169
const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
167170

168171
const listWindows = Effect.try({
@@ -275,10 +278,11 @@ export const make = Effect.gen(function* () {
275278
}),
276279
prepareReveal: (window) =>
277280
Effect.promise(async () => {
278-
if (platform !== "win32" || !windowsForegroundFocus || window.isDestroyed()) {
281+
if (platform !== "win32" || window.isDestroyed()) {
279282
return false;
280283
}
281-
return windowsForegroundFocus
284+
captureRevealWindows.add(window.id);
285+
return ensureWindowsForegroundFocus()
282286
.prepare(windowsForegroundFocusTarget(window))
283287
.catch(() => false);
284288
}),
@@ -289,22 +293,25 @@ export const make = Effect.gen(function* () {
289293
return;
290294
}
291295

296+
// Only a capture reveal fights another process for the foreground, which
297+
// needs Win32 calls that load native modules. Everything else stays native.
298+
const captureReveal = platform === "win32" && captureRevealWindows.delete(window.id);
292299
const shellHostedForeground =
293-
platform === "win32" && (await isWindowsShellHostedForeground().catch(() => false));
300+
captureReveal && (await isWindowsShellHostedForeground().catch(() => false));
294301

295302
if (window.isMinimized()) {
296303
window.restore();
297304
}
298305

299-
if (platform === "win32") {
306+
if (captureReveal) {
300307
Electron.app.focus();
301308
}
302309

303-
if (platform === "win32" || !window.isVisible()) {
310+
if (captureReveal || !window.isVisible()) {
304311
window.show();
305312
}
306313

307-
if (platform === "win32") {
314+
if (captureReveal) {
308315
window.moveTop();
309316
}
310317

@@ -314,7 +321,7 @@ export const make = Effect.gen(function* () {
314321

315322
window.focus();
316323

317-
if (platform === "win32") {
324+
if (captureReveal) {
318325
if (shellHostedForeground) {
319326
await windowsForegroundFocus
320327
?.focus(windowsForegroundFocusTarget(window))

apps/desktop/src/window/DesktopWindow.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1301,7 +1301,7 @@ describe("DesktopWindow", () => {
13011301
});
13021302

13031303
assert.equal(foreground, "Explorer");
1304-
assert.deepEqual(operations, ["reveal", "send", "send", "send"]);
1304+
assert.deepEqual(operations, ["send", "reveal", "send", "send"]);
13051305
assert.deepEqual(fakeWindow.send.mock.calls, [
13061306
[MENU_ACTION_CHANNEL, "snap-shot-started:capture-1"],
13071307
[SNAP_SHOT_READY_CHANNEL, "capture-1"],
@@ -1311,6 +1311,32 @@ describe("DesktopWindow", () => {
13111311
}),
13121312
);
13131313

1314+
it.effect("delivers the renderer event even when the reveal fails", () =>
1315+
Effect.gen(function* () {
1316+
const fakeWindow = makeFakeBrowserWindow();
1317+
const createCount = yield* Ref.make(0);
1318+
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1319+
const layer = makeTestLayer({
1320+
window: fakeWindow.window,
1321+
createCount,
1322+
mainWindow,
1323+
onReveal: () => {
1324+
throw new Error("another process kept the foreground");
1325+
},
1326+
});
1327+
1328+
yield* Effect.gen(function* () {
1329+
const desktopWindow = yield* DesktopWindow.DesktopWindow;
1330+
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1331+
yield* Effect.exit(desktopWindow.dispatchMenuAction("snap-shot-started:capture-1"));
1332+
1333+
assert.deepEqual(fakeWindow.send.mock.calls, [
1334+
[MENU_ACTION_CHANNEL, "snap-shot-started:capture-1"],
1335+
]);
1336+
}).pipe(Effect.provide(layer));
1337+
}),
1338+
);
1339+
13141340
it.effect("leaves a completed capture pending while only the connecting splash exists", () =>
13151341
Effect.gen(function* () {
13161342
const splash = makeFakeBrowserWindow();

apps/desktop/src/window/DesktopWindow.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,11 @@ export const make = Effect.gen(function* () {
869869
const send = Effect.sync(() => {
870870
if (!targetWindow.isDestroyed()) targetWindow.webContents.send(channel, payload);
871871
});
872-
const dispatch = reveal ? electronWindow.reveal(targetWindow).pipe(Effect.andThen(send)) : send;
872+
// The renderer must learn about the event even when another process refuses to
873+
// yield the foreground, so send first and treat the reveal as best effort.
874+
const dispatch = reveal
875+
? send.pipe(Effect.andThen(electronWindow.reveal(targetWindow).pipe(Effect.ignoreCause)))
876+
: send;
873877
if (targetWindow.webContents.isLoadingMainFrame()) {
874878
targetWindow.webContents.once("did-finish-load", () => void runPromise(dispatch));
875879
return;

0 commit comments

Comments
 (0)