[eric] windows: lazy backend boot, render in parallel with python cold-start (exp.2)

This commit is contained in:
ciregenz
2026-05-21 13:52:54 -07:00
parent 47c8f8f2a9
commit ddd82ef239
3 changed files with 37 additions and 7 deletions
+34 -4
View File
@@ -447,7 +447,7 @@ async function pickBackendPort() {
} }
async function startBackend() { async function startBackend() {
backendPort = await pickBackendPort(); if (!backendPort) backendPort = await pickBackendPort();
const pythonPath = getPythonPath(); const pythonPath = getPythonPath();
const backendDir = getResourcePath('backend'); const backendDir = getResourcePath('backend');
@@ -581,6 +581,7 @@ async function startBackend() {
// any browser on the machine could hit our localhost API and // any browser on the machine could hit our localhost API and
// impersonate the user. See backend/auth.py. // impersonate the user. See backend/auth.py.
await loadAuthToken(); await loadAuthToken();
markBackendReady();
} }
// Per-install auth token read from <data-root>/auth.token (backend // Per-install auth token read from <data-root>/auth.token (backend
@@ -589,6 +590,16 @@ async function startBackend() {
// retry a few times. // retry a few times.
let authToken = ''; let authToken = '';
// Lazy-backend gate: renderer fetches block on this until backend is reachable AND auth token is loaded. Lets the main window open immediately while Python is still cold-starting on Windows.
let backendReady = false;
let _backendReadyResolve;
const backendReadyPromise = new Promise((resolve) => { _backendReadyResolve = resolve; });
function markBackendReady() {
if (backendReady) return;
backendReady = true;
_backendReadyResolve();
}
function getAuthTokenFilePath() { function getAuthTokenFilePath() {
// Mirrors backend/config/paths.py. On macOS the file lives at // Mirrors backend/config/paths.py. On macOS the file lives at
// ~/Library/Application Support/OpenSwarm/data/auth.token; on // ~/Library/Application Support/OpenSwarm/data/auth.token; on
@@ -900,8 +911,14 @@ app.whenReady().then(async () => {
backendPort = parseInt(process.env.OPENSWARM_PORT || '8324', 10); backendPort = parseInt(process.env.OPENSWARM_PORT || '8324', 10);
console.log(`Dev mode: using existing backend on port ${backendPort}`); console.log(`Dev mode: using existing backend on port ${backendPort}`);
emitSplashStatus('Connecting to dev backend…'); emitSplashStatus('Connecting to dev backend…');
markBackendReady();
} else { } else {
await startBackend(); // Kick off backend without awaiting so the window can paint while Python is still cold-starting. Renderer fetches lazy-await markBackendReady() via the get-auth-token IPC; splash status updates still fire from inside startBackend.
backendPort = await pickBackendPort();
const _backendBoot = startBackend().catch((err) => {
console.error('[boot] backend startup failed:', err && err.message);
emitSplashStatus({ text: 'Backend failed to start', level: 'error', logs: recentBackendStderr.slice(-20).join('') });
});
} }
emitSplashStatus('Almost ready…'); emitSplashStatus('Almost ready…');
createWindow(); createWindow();
@@ -919,9 +936,14 @@ app.whenReady().then(async () => {
// Swap splash → main only once React has actually painted. ready-to-show // Swap splash → main only once React has actually painted. ready-to-show
// fires after the renderer's first frame, eliminating the white-flash // fires after the renderer's first frame, eliminating the white-flash
// that would otherwise pop between splash close and React mount. // that would otherwise pop between splash close and React mount.
// Also gated on backendReady: with lazy backend, ready-to-show can fire while React is still showing null (SignInGateLoader returns null until settings load), so we'd show a blank window if we swapped early.
if (mainWindow) { if (mainWindow) {
const swapToMain = () => { const swapToMain = () => {
if (mainWindowReady || mainWindow.isDestroyed()) return; if (mainWindowReady || mainWindow.isDestroyed()) return;
if (!backendReady) {
backendReadyPromise.then(() => swapToMain()).catch(() => {});
return;
}
mainWindowReady = true; mainWindowReady = true;
try { mainWindow.show(); mainWindow.focus(); } catch (_) {} try { mainWindow.show(); mainWindow.focus(); } catch (_) {}
// Tiny delay so the OS gets a chance to bring main to front // Tiny delay so the OS gets a chance to bring main to front
@@ -941,7 +963,13 @@ app.whenReady().then(async () => {
// sees the load error in the window itself. // sees the load error in the window itself.
mainWindow.webContents.once('did-fail-load', (_e, errorCode, errorDescription, validatedURL) => { mainWindow.webContents.once('did-fail-load', (_e, errorCode, errorDescription, validatedURL) => {
console.warn('[boot] mainWindow load failed:', errorCode, errorDescription, validatedURL); console.warn('[boot] mainWindow load failed:', errorCode, errorDescription, validatedURL);
if (isDev) swapToMain(); if (isDev) {
// Force-skip the backend gate so dev sees the error.
mainWindowReady = true;
try { mainWindow.show(); mainWindow.focus(); } catch (_) {}
if (splashWindow && !splashWindow.isDestroyed()) splashWindow.destroy();
splashWindow = null;
}
}); });
} }
@@ -1294,7 +1322,9 @@ ipcMain.on('splash:action', (_event, action) => {
}); });
ipcMain.handle('get-backend-port', () => backendPort); ipcMain.handle('get-backend-port', () => backendPort);
ipcMain.handle('get-auth-token', () => { ipcMain.handle('get-auth-token', async () => {
// Wait for backend if it's still cold-starting; this is the lazy-backend gate that lets the window open while Python is warming up.
if (!backendReady) await backendReadyPromise;
// Re-read the file every time. The backend rotates the token on each // Re-read the file every time. The backend rotates the token on each
// start, and during dev hot-reload the cached value could go stale // start, and during dev hot-reload the cached value could go stale
// while the renderer stays alive. Re-reading is cheap (small file, // while the renderer stays alive. Re-reading is cheap (small file,
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "openswarm", "name": "openswarm",
"version": "1.1.41-exp.1", "version": "1.1.40-exp.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openswarm", "name": "openswarm",
"version": "1.1.41-exp.1", "version": "1.1.40-exp.2",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"electron-updater": "^6.3.0", "electron-updater": "^6.3.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "openswarm", "name": "openswarm",
"version": "1.1.41-exp.1", "version": "1.1.40-exp.2",
"description": "OpenSwarm — AI Agent Orchestrator", "description": "OpenSwarm — AI Agent Orchestrator",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {