Skip to content
3 changes: 3 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopRemoteUpdates from "../updates/DesktopRemoteUpdates.ts";
import * as DesktopTray from "./DesktopTray.ts";
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
Expand Down Expand Up @@ -242,6 +243,7 @@ const startup = Effect.gen(function* () {
const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
const preReadyElectronOptions = yield* DesktopPreReadyPlatform.DesktopPreReadyElectronOptions;
const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
const tray = yield* DesktopTray.DesktopTray;
const updates = yield* DesktopUpdates.DesktopUpdates;
const environment = yield* DesktopEnvironment.DesktopEnvironment;

Expand Down Expand Up @@ -298,6 +300,7 @@ const startup = Effect.gen(function* () {
}
yield* appIdentity.configure;
yield* applicationMenu.configure;
yield* tray.configure;
yield* updates.configure;
yield* DesktopRemoteUpdates.listen;
yield* linuxUrlHandler.register;
Expand Down
128 changes: 128 additions & 0 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopTray from "./DesktopTray.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";

function makeDesktopTrayLayer(isAvailable = true) {
return Layer.succeed(DesktopTray.DesktopTray, {
configure: Effect.void,
isAvailable: Effect.succeed(isAvailable),
destroy: Effect.void,
} satisfies DesktopTray.DesktopTray["Service"]);
}

function makeElectronAppLayer(
appListeners: Map<string, (...args: readonly unknown[]) => void>,
quit: Effect.Effect<void> = Effect.void,
Expand Down Expand Up @@ -122,6 +131,7 @@ describe("DesktopLifecycle", () => {
),
),
Layer.provideMerge(makeDesktopWindowLayer()),
Layer.provideMerge(makeDesktopTrayLayer()),
Layer.provideMerge(environmentLayer),
Layer.provideMerge(DesktopShutdown.layer),
Layer.provideMerge(DesktopState.layer),
Expand Down Expand Up @@ -156,6 +166,122 @@ describe("DesktopLifecycle", () => {
});
}

it.effect("does not quit on window-all-closed on win32 (with tray) or darwin", () =>
Effect.gen(function* () {
for (const platform of ["win32", "darwin"] as const) {
const appListeners = new Map<string, (...args: readonly unknown[]) => void>();
let quitCalled = false;
const quit = Effect.sync(() => {
quitCalled = true;
});

const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform,
isDevelopment: false,
} as DesktopEnvironment.DesktopEnvironment["Service"]);

const layer = DesktopLifecycle.layer.pipe(
Layer.provideMerge(makeElectronAppLayer(appListeners, quit)),
Layer.provideMerge(electronThemeLayer),
Layer.provideMerge(makeElectronWindowLayer()),
Layer.provideMerge(makeDesktopWindowLayer()),
Layer.provideMerge(makeDesktopTrayLayer(true)),
Layer.provideMerge(environmentLayer),
Layer.provideMerge(DesktopShutdown.layer),
Layer.provideMerge(DesktopState.layer),
);

yield* Effect.scoped(
Effect.gen(function* () {
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
yield* lifecycle.register;

appListeners.get("window-all-closed")?.();
yield* Effect.yieldNow;

assert.isFalse(quitCalled, `expected quit not to be called on ${platform}`);
}),
).pipe(Effect.provide(layer));
}
}),
);

it.effect("quits on window-all-closed on win32 when tray is unavailable", () =>
Effect.gen(function* () {
const appListeners = new Map<string, (...args: readonly unknown[]) => void>();
let quitCalled = false;
const quit = Effect.sync(() => {
quitCalled = true;
});

const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform: "win32",
isDevelopment: false,
} as DesktopEnvironment.DesktopEnvironment["Service"]);

const layer = DesktopLifecycle.layer.pipe(
Layer.provideMerge(makeElectronAppLayer(appListeners, quit)),
Layer.provideMerge(electronThemeLayer),
Layer.provideMerge(makeElectronWindowLayer()),
Layer.provideMerge(makeDesktopWindowLayer()),
Layer.provideMerge(makeDesktopTrayLayer(false)),
Layer.provideMerge(environmentLayer),
Layer.provideMerge(DesktopShutdown.layer),
Layer.provideMerge(DesktopState.layer),
);

yield* Effect.scoped(
Effect.gen(function* () {
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
yield* lifecycle.register;

appListeners.get("window-all-closed")?.();
yield* Effect.yieldNow;

assert.isTrue(quitCalled, "expected quit to be called on win32 when tray unavailable");
}),
).pipe(Effect.provide(layer));
}),
);

it.effect("quits on window-all-closed on linux", () =>
Effect.gen(function* () {
const appListeners = new Map<string, (...args: readonly unknown[]) => void>();
let quitCalled = false;
const quit = Effect.sync(() => {
quitCalled = true;
});

const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform: "linux",
isDevelopment: false,
} as DesktopEnvironment.DesktopEnvironment["Service"]);

const layer = DesktopLifecycle.layer.pipe(
Layer.provideMerge(makeElectronAppLayer(appListeners, quit)),
Layer.provideMerge(electronThemeLayer),
Layer.provideMerge(makeElectronWindowLayer()),
Layer.provideMerge(makeDesktopWindowLayer()),
Layer.provideMerge(makeDesktopTrayLayer()),
Layer.provideMerge(environmentLayer),
Layer.provideMerge(DesktopShutdown.layer),
Layer.provideMerge(DesktopState.layer),
);

yield* Effect.scoped(
Effect.gen(function* () {
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
yield* lifecycle.register;

appListeners.get("window-all-closed")?.();
yield* Effect.yieldNow;

assert.isTrue(quitCalled, "expected quit to be called on linux");
}),
).pipe(Effect.provide(layer));
}),
);

it.effect("destroys windows before waiting for backend shutdown", () =>
Effect.gen(function* () {
const appListeners = new Map<string, (...args: readonly unknown[]) => void>();
Expand Down Expand Up @@ -194,6 +320,7 @@ describe("DesktopLifecycle", () => {
Layer.provideMerge(electronThemeLayer),
Layer.provideMerge(makeElectronWindowLayer(destroyAll)),
Layer.provideMerge(makeDesktopWindowLayer({ flushMainWindowBounds })),
Layer.provideMerge(makeDesktopTrayLayer()),
Layer.provideMerge(environmentLayer),
Layer.provideMerge(desktopShutdownLayer),
Layer.provideMerge(DesktopState.layer),
Expand Down Expand Up @@ -235,6 +362,7 @@ describe("DesktopLifecycle", () => {
Layer.provideMerge(electronThemeLayer),
Layer.provideMerge(makeElectronWindowLayer()),
Layer.provideMerge(makeDesktopWindowLayer({ activate })),
Layer.provideMerge(makeDesktopTrayLayer()),
Layer.provideMerge(environmentLayer),
Layer.provideMerge(DesktopShutdown.layer),
Layer.provideMerge(DesktopState.layer),
Expand Down
22 changes: 19 additions & 3 deletions apps/desktop/src/app/DesktopLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type * as Electron from "electron";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import { makeComponentLogger } from "./DesktopObservability.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import * as DesktopTray from "./DesktopTray.ts";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
Expand All @@ -32,6 +33,7 @@ export type DesktopLifecycleRuntimeServices =
| DesktopEnvironment.DesktopEnvironment
| DesktopShutdown.DesktopShutdown
| DesktopState.DesktopState
| DesktopTray.DesktopTray
| DesktopWindow.DesktopWindow
| ElectronApp.ElectronApp
| ElectronTheme.ElectronTheme;
Expand All @@ -41,7 +43,7 @@ type DesktopLifecycleRegistrationServices =
| ElectronWindow.ElectronWindow;

/**
* @effect-expect-leaking DesktopEnvironment | DesktopShutdown | DesktopState | DesktopWindow | ElectronApp | ElectronTheme | ElectronWindow
* @effect-expect-leaking DesktopEnvironment | DesktopShutdown | DesktopState | DesktopTray | DesktopWindow | ElectronApp | ElectronTheme | ElectronWindow
*/
export class DesktopLifecycle extends Context.Service<
DesktopLifecycle,
Expand Down Expand Up @@ -190,6 +192,7 @@ export const make = DesktopLifecycle.of({
}),
register: Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const desktopTray = yield* DesktopTray.DesktopTray;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const electronApp = yield* ElectronApp.ElectronApp;
const electronTheme = yield* ElectronTheme.ElectronTheme;
Expand Down Expand Up @@ -246,9 +249,22 @@ export const make = DesktopLifecycle.of({
Effect.gen(function* () {
const app = yield* ElectronApp.ElectronApp;
const state = yield* DesktopState.DesktopState;
if (environment.platform !== "darwin" && !(yield* Ref.get(state.quitting))) {
yield* app.quit;
if (yield* Ref.get(state.quitting)) {
return;
}

if (environment.platform === "darwin") {
return;
}

if (environment.platform === "win32") {
const hasTray = yield* desktopTray.isAvailable;
if (hasTray) {
return;
}
}

yield* app.quit;
}).pipe(Effect.withSpan("desktop.lifecycle.windowAllClosed")),
);
});
Expand Down
Loading
Loading