Files
AgentCompany/src/main/bootstrap-application.ts
T
ZacharyZhang-NY a2a90f00e6 refactor(bootstrap): extract bootstrap-application into orchestrator + modules
Splits the 391-line bootstrapApplication god function into a thin
orchestrator plus 7 focused modules under src/main/bootstrap/:
prepare-logger, prepare-database, prepare-services (service graph +
ipc wiring), wire-api-server (14-callback dictionary), recover-
scheduled-posts, run-startup-health-checks, recover-interrupted-tasks
(crash notification + clean-shutdown task resumption). Original
startup order, before-quit cleanup, and error handling are preserved;
the entry file drops to 153 lines.
2026-04-16 23:34:13 -04:00

154 lines
5.5 KiB
TypeScript

import { app } from "electron";
import type { AgentApiServer } from "./api-server";
import type { AppDatabase } from "./database";
import type { BrowserManager } from "./browser-manager";
import { prepareHostEnvironment } from "./host-environment-service";
import type { createServiceGraph } from "./service-graph";
import type { createIpcSupport } from "./ipc-support";
import type { createThreadIntegration } from "./thread-integration";
import { prepareLogger } from "./bootstrap/prepare-logger";
import { prepareDatabase } from "./bootstrap/prepare-database";
import { prepareServices } from "./bootstrap/prepare-services";
import { wireApiServer, type CreateApiServerFn } from "./bootstrap/wire-api-server";
import { recoverScheduledPosts } from "./bootstrap/recover-scheduled-posts";
import { runStartupHealthChecks } from "./bootstrap/run-startup-health-checks";
import { emitCrashRecoveryNotification, recoverShutdownInterruptedTasks } from "./bootstrap/recover-interrupted-tasks";
interface BootstrapApplicationDependencies {
hasSingleInstanceLock: boolean;
isDev: boolean;
devServerOrigin: string | null;
profileDir: string;
appDatabaseFactory: (profileDir: string) => AppDatabase;
initSigningKey: (profileDir: string) => void;
windowStatePath: string;
preloadPath: string;
rendererDist: string;
iconPath: string;
devServerUrl?: string;
workerPath: string;
createApiServer: CreateApiServerFn;
createRunToken: (agentId: string, companyId: string, runId: string) => string;
seedDefaultAutomationRules: (companyId: string) => void;
isQuittingRef: { current: boolean };
onRuntimeReady?: (state: {
db: AppDatabase;
apiServer: AgentApiServer;
serviceGraph: ReturnType<typeof createServiceGraph>;
ipcSupport: ReturnType<typeof createIpcSupport>;
threadIntegration: ReturnType<typeof createThreadIntegration>;
}) => void;
}
export async function bootstrapApplication(deps: BootstrapApplicationDependencies) {
await app.whenReady();
const log = prepareLogger();
await prepareHostEnvironment({ logger: log });
const db = prepareDatabase({
profileDir: deps.profileDir,
appDatabaseFactory: deps.appDatabaseFactory,
initSigningKey: deps.initSigningKey,
});
const apiServerRef = { current: null as AgentApiServer | null };
const browserManagerRef = { current: null as BrowserManager | null };
const services = prepareServices({
db,
apiServerRef,
browserManagerRef,
isQuittingRef: deps.isQuittingRef,
preloadPath: deps.preloadPath,
rendererDist: deps.rendererDist,
iconPath: deps.iconPath,
isDev: deps.isDev,
devServerUrl: deps.devServerUrl,
devServerOrigin: deps.devServerOrigin,
windowStatePath: deps.windowStatePath,
workerPath: deps.workerPath,
logger: log,
createRunToken: deps.createRunToken,
seedDefaultAutomationRules: deps.seedDefaultAutomationRules,
});
const { serviceGraph, ipcSupport, threadIntegration, notificationService, checkpointEngine, notify, emitEvent, publishDomainChanged } = services;
const wakeAgentIfPossible = (agentId: string | null | undefined, companyId: string, trigger: string) => {
const orchestration = serviceGraph.getOrchestrationService();
if (!orchestration) throw new Error("Orchestration service is not initialized.");
return orchestration.wakeAgentIfPossible(agentId, companyId, trigger);
};
const apiServer = wireApiServer({
db,
serviceGraph,
threadIntegration,
checkpointEngine,
emitEvent,
notify,
publishDomainChanged,
isDev: deps.isDev,
devServerUrl: deps.devServerUrl,
createApiServer: deps.createApiServer,
});
apiServerRef.current = apiServer;
await apiServer.start();
deps.onRuntimeReady?.({ db, apiServer, serviceGraph, ipcSupport, threadIntegration });
if (apiServer && serviceGraph.getBrowserActionService()) {
apiServer.setBrowserActionHandler(async (actionId, companyId) => {
const browserActions = serviceGraph.getBrowserActionService();
if (!browserActions) throw new Error("Browser action service is not initialized");
await browserActions.dispatchBrowserAction(actionId, companyId);
});
}
const scheduledPostTimers = recoverScheduledPosts({
db,
logger: log,
isQuittingRef: deps.isQuittingRef,
getBrowserActionService: () => serviceGraph.getBrowserActionService(),
});
// Register cleanup for bootstrap-owned resources during quit.
// Runs BEFORE service-graph's onBeforeQuit (which is registered later).
app.on("before-quit", () => {
for (const timer of scheduledPostTimers) clearTimeout(timer);
scheduledPostTimers.clear();
notificationService.destroy();
});
// Show the window immediately so the user sees the app launch without delay.
await serviceGraph.getDesktopShellService()?.ensureWindow();
serviceGraph.getAppLifecycleService()?.register(deps.hasSingleInstanceLock);
// Run connector health checks AFTER the window is visible to unblock startup.
await runStartupHealthChecks(db, serviceGraph.getConnectorHealthService(), log);
serviceGraph.getOrchestrationService()?.startAllSchedulers();
emitCrashRecoveryNotification({
db,
logger: log,
emitEvent,
notify,
publishDomainChanged,
wakeAgentIfPossible,
});
serviceGraph.getOrchestrationService()?.resumeQueuedRuns();
recoverShutdownInterruptedTasks({
db,
logger: log,
emitEvent,
notify,
publishDomainChanged,
wakeAgentIfPossible,
});
return { db, apiServer, serviceGraph, ipcSupport, threadIntegration };
}