From 38b4ac2c5cf45d8d57964d45e05bb03565cee1c0 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 12 Apr 2026 16:27:02 +0200 Subject: [PATCH] refactor: split agent runtime state --- app/server/modules/agents/agents-manager.ts | 85 +++---------------- .../agents/helpers/runtime-state.dev.ts | 37 ++++++++ .../modules/agents/helpers/runtime-state.ts | 38 +++++++++ 3 files changed, 87 insertions(+), 73 deletions(-) create mode 100644 app/server/modules/agents/helpers/runtime-state.dev.ts create mode 100644 app/server/modules/agents/helpers/runtime-state.ts diff --git a/app/server/modules/agents/agents-manager.ts b/app/server/modules/agents/agents-manager.ts index ab8048cb..5680abbf 100644 --- a/app/server/modules/agents/agents-manager.ts +++ b/app/server/modules/agents/agents-manager.ts @@ -1,16 +1,15 @@ -import type { ChildProcess } from "node:child_process"; import { logger } from "@zerobyte/core/node"; -import type { ResticBackupOutputDto } from "@zerobyte/core/restic"; -import type { BackupProgressPayload, BackupRunPayload } from "@zerobyte/contracts/agent-protocol"; -import type { AgentBackupEventHandlers, AgentManagerRuntime } from "./controller/server"; +import type { BackupRunPayload } from "@zerobyte/contracts/agent-protocol"; +import { config } from "../../core/config"; +import type { AgentBackupEventHandlers } from "./controller/server"; import { spawnLocalAgentProcess, stopLocalAgentProcess } from "./local/process"; +import type { BackupExecutionProgress, BackupExecutionResult } from "./helpers/runtime-state"; +import { createAgentRuntimeState } from "./helpers/runtime-state"; +import { getDevAgentRuntimeState } from "./helpers/runtime-state.dev"; +export type { BackupExecutionProgress, BackupExecutionResult } from "./helpers/runtime-state"; +export type { ProcessWithAgentRuntime } from "./helpers/runtime-state.dev"; -export type BackupExecutionProgress = BackupProgressPayload["progress"]; -export type BackupExecutionResult = - | { status: "unavailable"; error: Error } - | { status: "completed"; exitCode: number; result: ResticBackupOutputDto | null; warningDetails: string | null } - | { status: "failed"; error: string } - | { status: "cancelled"; message?: string }; +const productionRuntimeState = createAgentRuntimeState(); export type AgentRunBackupRequest = { scheduleId: number; @@ -19,71 +18,11 @@ export type AgentRunBackupRequest = { onProgress: (progress: BackupExecutionProgress) => void; }; -type ActiveBackupRun = { - scheduleId: number; - jobId: string; - scheduleShortId: string; - onProgress: (progress: BackupExecutionProgress) => void; - resolve: (result: BackupExecutionResult) => void; - cancellationRequested: boolean; -}; - -type AgentRuntimeState = { - agentManager: AgentManagerRuntime | null; - localAgent: ChildProcess | null; - isStoppingLocalAgent: boolean; - localAgentRestartTimeout: ReturnType | null; - activeBackupsByScheduleId: Map; - activeBackupScheduleIdsByJobId: Map; -}; - -type LegacyAgentRuntimeState = Omit & - Partial>; - -export type ProcessWithAgentRuntime = NodeJS.Process & { - __zerobyteAgentRuntime?: LegacyAgentRuntimeState; -}; - -const getAgentRuntimeState = () => { - const runtimeProcess = process as ProcessWithAgentRuntime; - const existingRuntime = runtimeProcess.__zerobyteAgentRuntime; - - if (existingRuntime) { - const runtime: AgentRuntimeState = { - ...existingRuntime, - activeBackupsByScheduleId: existingRuntime.activeBackupsByScheduleId ?? new Map(), - activeBackupScheduleIdsByJobId: existingRuntime.activeBackupScheduleIdsByJobId ?? new Map(), - }; - - runtimeProcess.__zerobyteAgentRuntime = runtime; - return runtime; - } - - const runtime: AgentRuntimeState = { - agentManager: null, - localAgent: null, - isStoppingLocalAgent: false, - localAgentRestartTimeout: null, - activeBackupsByScheduleId: new Map(), - activeBackupScheduleIdsByJobId: new Map(), - }; - - runtimeProcess.__zerobyteAgentRuntime = runtime; - return runtime; -}; - +const getAgentRuntimeState = () => (config.__prod__ ? productionRuntimeState : getDevAgentRuntimeState()); const getAgentManagerRuntime = () => getAgentRuntimeState().agentManager; const getActiveBackupsByScheduleId = () => getAgentRuntimeState().activeBackupsByScheduleId; const getActiveBackupScheduleIdsByJobId = () => getAgentRuntimeState().activeBackupScheduleIdsByJobId; -const getUnavailableError = (agentId: string) => { - if (agentId === "local") { - return new Error("Local backup agent is not connected"); - } - - return new Error(`Backup agent ${agentId} is not connected`); -}; - const clearActiveBackupRun = (scheduleId: number) => { const activeBackupsByScheduleId = getActiveBackupsByScheduleId(); const activeBackupScheduleIdsByJobId = getActiveBackupScheduleIdsByJobId(); @@ -229,7 +168,7 @@ export const agentManager = { if (!runtime) { return { status: "unavailable", - error: getUnavailableError(agentId), + error: new Error(`Backup agent ${agentId} is not connected`), } satisfies BackupExecutionResult; } @@ -254,7 +193,7 @@ export const agentManager = { clearActiveBackupRun(request.scheduleId); return { status: "unavailable", - error: getUnavailableError(agentId), + error: new Error(`Failed to send backup command to agent ${agentId}`), } satisfies BackupExecutionResult; } diff --git a/app/server/modules/agents/helpers/runtime-state.dev.ts b/app/server/modules/agents/helpers/runtime-state.dev.ts new file mode 100644 index 00000000..daccadbb --- /dev/null +++ b/app/server/modules/agents/helpers/runtime-state.dev.ts @@ -0,0 +1,37 @@ +import { createAgentRuntimeState, type AgentRuntimeState } from "./runtime-state"; + +type LegacyAgentRuntimeState = Omit & + Partial>; + +export type ProcessWithAgentRuntime = NodeJS.Process & { + __zerobyteAgentRuntime?: LegacyAgentRuntimeState; +}; + +const hasActiveBackupMaps = (runtime: LegacyAgentRuntimeState): runtime is AgentRuntimeState => { + return runtime.activeBackupsByScheduleId instanceof Map && runtime.activeBackupScheduleIdsByJobId instanceof Map; +}; + +const hydrateAgentRuntimeState = (runtime: LegacyAgentRuntimeState): AgentRuntimeState => ({ + ...runtime, + activeBackupsByScheduleId: runtime.activeBackupsByScheduleId ?? new Map(), + activeBackupScheduleIdsByJobId: runtime.activeBackupScheduleIdsByJobId ?? new Map(), +}); + +export const getDevAgentRuntimeState = (): AgentRuntimeState => { + // Bun reloads modules in place during development, so keep the live runtime on process. + const runtimeProcess = process as ProcessWithAgentRuntime; + const existingRuntime = runtimeProcess.__zerobyteAgentRuntime; + if (!existingRuntime) { + const runtime = createAgentRuntimeState(); + runtimeProcess.__zerobyteAgentRuntime = runtime; + return runtime; + } + + if (hasActiveBackupMaps(existingRuntime)) { + return existingRuntime; + } + + const runtime = hydrateAgentRuntimeState(existingRuntime); + runtimeProcess.__zerobyteAgentRuntime = runtime; + return runtime; +}; diff --git a/app/server/modules/agents/helpers/runtime-state.ts b/app/server/modules/agents/helpers/runtime-state.ts new file mode 100644 index 00000000..51de3fb4 --- /dev/null +++ b/app/server/modules/agents/helpers/runtime-state.ts @@ -0,0 +1,38 @@ +import type { ChildProcess } from "node:child_process"; +import type { ResticBackupOutputDto } from "@zerobyte/core/restic"; +import type { BackupProgressPayload } from "@zerobyte/contracts/agent-protocol"; +import type { AgentManagerRuntime } from "../controller/server"; + +export type BackupExecutionProgress = BackupProgressPayload["progress"]; +export type BackupExecutionResult = + | { status: "unavailable"; error: Error } + | { status: "completed"; exitCode: number; result: ResticBackupOutputDto | null; warningDetails: string | null } + | { status: "failed"; error: string } + | { status: "cancelled"; message?: string }; + +type ActiveBackupRun = { + scheduleId: number; + jobId: string; + scheduleShortId: string; + onProgress: (progress: BackupExecutionProgress) => void; + resolve: (result: BackupExecutionResult) => void; + cancellationRequested: boolean; +}; + +export type AgentRuntimeState = { + agentManager: AgentManagerRuntime | null; + localAgent: ChildProcess | null; + isStoppingLocalAgent: boolean; + localAgentRestartTimeout: ReturnType | null; + activeBackupsByScheduleId: Map; + activeBackupScheduleIdsByJobId: Map; +}; + +export const createAgentRuntimeState = (): AgentRuntimeState => ({ + agentManager: null, + localAgent: null, + isStoppingLocalAgent: false, + localAgentRestartTimeout: null, + activeBackupsByScheduleId: new Map(), + activeBackupScheduleIdsByJobId: new Map(), +});