mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
perf(electron): defer hidden-start renderer creation (#10327)
Merged — locally validated (6/6 focused lazy-window tests, plus the wider electron suite, typecheck:core clean, gates green). Thanks!
This commit is contained in:
28
electron/lib/windowLifecycle.js
Normal file
28
electron/lib/windowLifecycle.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/** Pure helpers for deciding and driving the Electron dashboard window lifecycle. */
|
||||
|
||||
function shouldStartHidden({ argv = [], loginItemSettings = {} } = {}) {
|
||||
return (
|
||||
argv.includes("--hidden") ||
|
||||
argv.includes("--minimized") ||
|
||||
loginItemSettings.wasOpenedAsHidden === true
|
||||
);
|
||||
}
|
||||
|
||||
function showOrCreateWindow({ appReady, getWindow, createWindow }) {
|
||||
if (!appReady) return null;
|
||||
|
||||
const currentWindow = getWindow();
|
||||
if (!currentWindow || currentWindow.isDestroyed()) {
|
||||
return createWindow();
|
||||
}
|
||||
|
||||
if (currentWindow.isMinimized()) currentWindow.restore();
|
||||
currentWindow.show();
|
||||
currentWindow.focus();
|
||||
return currentWindow;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldStartHidden,
|
||||
showOrCreateWindow,
|
||||
};
|
||||
@@ -40,6 +40,7 @@ const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper");
|
||||
const { resolveRemoteServerUrl, isValidHttpUrl } = require("./lib/resolveRemoteServerUrl");
|
||||
const { writeRemoteServerUrl } = require("./lib/remoteServerPreferences");
|
||||
const { buildReadinessUrl, waitForServer } = require("./lib/serverReadiness");
|
||||
const { shouldStartHidden, showOrCreateWindow } = require("./lib/windowLifecycle");
|
||||
|
||||
// ── Single Instance Lock ───────────────────────────────────
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -49,11 +50,7 @@ if (!gotTheLock) {
|
||||
}
|
||||
|
||||
app.on("second-instance", () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
showMainWindow();
|
||||
});
|
||||
|
||||
// ── Environment Detection ──────────────────────────────────
|
||||
@@ -71,6 +68,7 @@ let nextServer = null;
|
||||
let serverPort = 20128;
|
||||
let isServerStopped = false;
|
||||
let remoteServerPromptWindow = null;
|
||||
let keepAliveWithoutWindows = false;
|
||||
|
||||
// ── Remote Server Mode ──────────────────────────────────────
|
||||
// Lets the desktop shell attach to an already-running OmniRoute server (e.g. a
|
||||
@@ -366,6 +364,8 @@ function setupContentSecurityPolicy() {
|
||||
|
||||
// ── Create Window ──────────────────────────────────────────
|
||||
function createWindow() {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) return mainWindow;
|
||||
|
||||
// Platform-conditional options (#9)
|
||||
const platformWindowOptions =
|
||||
process.platform === "darwin"
|
||||
@@ -397,16 +397,10 @@ function createWindow() {
|
||||
mainWindow.webContents.openDevTools({ mode: "detach" });
|
||||
}
|
||||
|
||||
// Show window when ready (unless starting minimized/hidden in tray)
|
||||
// Hidden startup skips createWindow() entirely; any created dashboard is explicit.
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
const startHidden =
|
||||
process.argv.includes("--hidden") ||
|
||||
process.argv.includes("--minimized") ||
|
||||
app.getLoginItemSettings().wasOpenedAsHidden;
|
||||
if (!startHidden) {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.show();
|
||||
} else {
|
||||
console.log("[Electron] Launched hidden in background tray");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -437,6 +431,16 @@ function createWindow() {
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function showMainWindow() {
|
||||
return showOrCreateWindow({
|
||||
appReady: app.isReady(),
|
||||
getWindow: () => mainWindow,
|
||||
createWindow,
|
||||
});
|
||||
}
|
||||
|
||||
// ── System Tray ────────────────────────────────────────────
|
||||
@@ -465,12 +469,7 @@ function createTray() {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Open OmniRoute",
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
},
|
||||
click: () => showMainWindow(),
|
||||
},
|
||||
{
|
||||
label: "Open Dashboard",
|
||||
@@ -523,10 +522,7 @@ function createTray() {
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
tray.on("double-click", () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
showMainWindow();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1094,9 +1090,20 @@ app.whenReady().then(async () => {
|
||||
process.argv.includes("--headless") ||
|
||||
process.argv.includes("--cli") ||
|
||||
process.env.OMNIROUTE_HEADLESS === "true";
|
||||
const startHidden =
|
||||
!isHeadless &&
|
||||
shouldStartHidden({
|
||||
argv: process.argv,
|
||||
loginItemSettings: app.getLoginItemSettings(),
|
||||
});
|
||||
keepAliveWithoutWindows = startHidden;
|
||||
|
||||
// Fix #1: Start server and WAIT for readiness before showing window
|
||||
startNextServer();
|
||||
if (!isHeadless) {
|
||||
createTray();
|
||||
}
|
||||
|
||||
let serverReady = true;
|
||||
if (!isDev) {
|
||||
// Probe the lightweight auth-exempt endpoint instead of aggregating full monitoring state.
|
||||
@@ -1105,9 +1112,10 @@ app.whenReady().then(async () => {
|
||||
|
||||
if (isHeadless) {
|
||||
console.log("[Electron] Headless mode active — UI window and tray icon skipped");
|
||||
} else if (startHidden) {
|
||||
console.log("[Electron] Launched hidden in background tray without a renderer");
|
||||
} else {
|
||||
createWindow();
|
||||
createTray();
|
||||
showMainWindow();
|
||||
}
|
||||
|
||||
setupIpcHandlers();
|
||||
@@ -1115,7 +1123,7 @@ app.whenReady().then(async () => {
|
||||
|
||||
// If readiness timed out (e.g. very long first-launch migrations), don't leave the
|
||||
// window stuck on a hanging connection — keep polling and reload once it responds (#2460).
|
||||
if (!isDev && !serverReady && !isHeadless) {
|
||||
if (!isDev && !serverReady && !isHeadless && !startHidden) {
|
||||
void waitForServer(getServerReadinessUrl(), 300000).then((ready) => {
|
||||
if (ready && mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.loadURL(getServerUrl());
|
||||
@@ -1133,11 +1141,7 @@ app.whenReady().then(async () => {
|
||||
// macOS: recreate window when dock icon clicked
|
||||
app.on("activate", () => {
|
||||
if (isHeadless) return;
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
} else if (mainWindow) {
|
||||
mainWindow.show();
|
||||
}
|
||||
showMainWindow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1147,7 +1151,7 @@ app.on("window-all-closed", () => {
|
||||
process.argv.includes("--headless") ||
|
||||
process.argv.includes("--cli") ||
|
||||
process.env.OMNIROUTE_HEADLESS === "true";
|
||||
if (process.platform !== "darwin" && !isHeadless) {
|
||||
if (process.platform !== "darwin" && !isHeadless && !keepAliveWithoutWindows) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"remoteServerPromptRenderer.js",
|
||||
"lib/resolveServerEntry.js",
|
||||
"lib/resolveNodeHelper.js",
|
||||
"lib/windowLifecycle.js",
|
||||
"lib/resolveRemoteServerUrl.js",
|
||||
"lib/remoteServerPreferences.js",
|
||||
"lib/serverReadiness.js",
|
||||
|
||||
105
tests/unit/electron-lazy-window.test.ts
Normal file
105
tests/unit/electron-lazy-window.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { shouldStartHidden, showOrCreateWindow } = require("../../electron/lib/windowLifecycle");
|
||||
|
||||
describe("Electron hidden-start window lifecycle", () => {
|
||||
it("detects explicit hidden flags and OS login-item hidden launches", () => {
|
||||
assert.equal(shouldStartHidden({ argv: ["electron", "--hidden"] }), true);
|
||||
assert.equal(shouldStartHidden({ argv: ["electron", "--minimized"] }), true);
|
||||
assert.equal(
|
||||
shouldStartHidden({ argv: ["electron"], loginItemSettings: { wasOpenedAsHidden: true } }),
|
||||
true
|
||||
);
|
||||
assert.equal(shouldStartHidden({ argv: ["electron"], loginItemSettings: {} }), false);
|
||||
});
|
||||
|
||||
it("creates the dashboard only when an explicit open action has no live window", () => {
|
||||
const createdWindow = { id: "created" };
|
||||
let createCalls = 0;
|
||||
const result = showOrCreateWindow({
|
||||
appReady: true,
|
||||
getWindow: () => null,
|
||||
createWindow: () => {
|
||||
createCalls += 1;
|
||||
return createdWindow;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, createdWindow);
|
||||
assert.equal(createCalls, 1);
|
||||
});
|
||||
|
||||
it("restores, shows, and focuses an existing dashboard without recreating it", () => {
|
||||
const calls: string[] = [];
|
||||
const existingWindow = {
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => true,
|
||||
restore: () => calls.push("restore"),
|
||||
show: () => calls.push("show"),
|
||||
focus: () => calls.push("focus"),
|
||||
};
|
||||
|
||||
const result = showOrCreateWindow({
|
||||
appReady: true,
|
||||
getWindow: () => existingWindow,
|
||||
createWindow: () => {
|
||||
throw new Error("must not recreate a live dashboard");
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, existingWindow);
|
||||
assert.deepEqual(calls, ["restore", "show", "focus"]);
|
||||
});
|
||||
|
||||
it("does not create a BrowserWindow before Electron is ready", () => {
|
||||
let createCalls = 0;
|
||||
const result = showOrCreateWindow({
|
||||
appReady: false,
|
||||
getWindow: () => null,
|
||||
createWindow: () => {
|
||||
createCalls += 1;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
assert.equal(createCalls, 0);
|
||||
});
|
||||
|
||||
it("routes tray, second-instance, and macOS activation opens through the lazy helper", () => {
|
||||
const mainSource = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8");
|
||||
|
||||
assert.match(mainSource, /app\.on\("second-instance", \(\) => \{\s*showMainWindow\(\);/);
|
||||
assert.match(mainSource, /label: "Open OmniRoute",\s*click: \(\) => showMainWindow\(\)/);
|
||||
assert.match(mainSource, /tray\.on\("double-click", \(\) => \{\s*showMainWindow\(\);/);
|
||||
assert.match(mainSource, /app\.on\("activate", \(\) => \{[\s\S]*?showMainWindow\(\);/);
|
||||
});
|
||||
|
||||
it("keeps hidden startup renderer-free until an explicit open action", () => {
|
||||
const mainSource = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8");
|
||||
const readyBlock = mainSource.slice(mainSource.indexOf("app.whenReady().then"));
|
||||
|
||||
assert.match(
|
||||
readyBlock,
|
||||
/startNextServer\(\);\s*if \(!isHeadless\) \{\s*createTray\(\);\s*\}/,
|
||||
"the server and tray must start before the hidden/visible renderer decision"
|
||||
);
|
||||
assert.match(
|
||||
readyBlock,
|
||||
/if \(isHeadless\)[\s\S]*?else if \(startHidden\)[\s\S]*?else \{\s*showMainWindow\(\);/
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
readyBlock.match(/else if \(startHidden\)[\s\S]*?\} else \{/s)?.[0] ?? "",
|
||||
/createWindow\(|showMainWindow\(/
|
||||
);
|
||||
assert.match(
|
||||
readyBlock,
|
||||
/\}\s*setupIpcHandlers\(\);\s*setupAutoUpdater\(\);/,
|
||||
"IPC and updater setup must remain active when no renderer was created"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user