From 928c70fb6beef9d6ddb7aadb9c1e131a987bdece Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 3 May 2026 14:30:30 +0100 Subject: [PATCH] perf(gateway): trim startup watcher imports --- src/gateway/config-diff.ts | 33 ++++++++++ src/gateway/config-reload-settings.ts | 26 ++++++++ src/gateway/config-reload.ts | 62 ++----------------- src/gateway/server-aux-handlers.ts | 4 +- src/gateway/server-import-boundary.test.ts | 10 +++ .../server-methods/config-write-flow.ts | 3 +- src/gateway/server-methods/config.ts | 2 +- src/gateway/server-runtime-state.ts | 3 +- src/gateway/server-shared-auth-generation.ts | 2 +- 9 files changed, 81 insertions(+), 64 deletions(-) create mode 100644 src/gateway/config-diff.ts create mode 100644 src/gateway/config-reload-settings.ts diff --git a/src/gateway/config-diff.ts b/src/gateway/config-diff.ts new file mode 100644 index 00000000000..92b67dbec2c --- /dev/null +++ b/src/gateway/config-diff.ts @@ -0,0 +1,33 @@ +import { isDeepStrictEqual } from "node:util"; +import { isPlainObject } from "../utils.js"; + +export function diffConfigPaths(prev: unknown, next: unknown, prefix = ""): string[] { + if (prev === next) { + return []; + } + if (isPlainObject(prev) && isPlainObject(next)) { + const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); + const paths: string[] = []; + for (const key of keys) { + const prevValue = prev[key]; + const nextValue = next[key]; + if (prevValue === undefined && nextValue === undefined) { + continue; + } + const childPrefix = prefix ? `${prefix}.${key}` : key; + const childPaths = diffConfigPaths(prevValue, nextValue, childPrefix); + if (childPaths.length > 0) { + paths.push(...childPaths); + } + } + return paths; + } + if (Array.isArray(prev) && Array.isArray(next)) { + // Arrays can contain object entries (for example memory.qmd.paths/scope.rules); + // compare structurally so identical values are not reported as changed. + if (isDeepStrictEqual(prev, next)) { + return []; + } + } + return [prefix || ""]; +} diff --git a/src/gateway/config-reload-settings.ts b/src/gateway/config-reload-settings.ts new file mode 100644 index 00000000000..955976c357a --- /dev/null +++ b/src/gateway/config-reload-settings.ts @@ -0,0 +1,26 @@ +import type { GatewayReloadMode } from "../config/types.gateway.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; + +export type GatewayReloadSettings = { + mode: GatewayReloadMode; + debounceMs: number; +}; + +const DEFAULT_RELOAD_SETTINGS: GatewayReloadSettings = { + mode: "hybrid", + debounceMs: 300, +}; + +export function resolveGatewayReloadSettings(cfg: OpenClawConfig): GatewayReloadSettings { + const rawMode = cfg.gateway?.reload?.mode; + const mode = + rawMode === "off" || rawMode === "restart" || rawMode === "hot" || rawMode === "hybrid" + ? rawMode + : DEFAULT_RELOAD_SETTINGS.mode; + const debounceRaw = cfg.gateway?.reload?.debounceMs; + const debounceMs = + typeof debounceRaw === "number" && Number.isFinite(debounceRaw) + ? Math.max(0, Math.floor(debounceRaw)) + : DEFAULT_RELOAD_SETTINGS.debounceMs; + return { mode, debounceMs }; +} diff --git a/src/gateway/config-reload.ts b/src/gateway/config-reload.ts index ba2f12e5e84..b21ae5ac6d5 100644 --- a/src/gateway/config-reload.ts +++ b/src/gateway/config-reload.ts @@ -1,4 +1,3 @@ -import { isDeepStrictEqual } from "node:util"; import chokidar from "chokidar"; import { bumpSkillsSnapshotVersion } from "../agents/skills/refresh-state.js"; import type { ConfigWriteNotification } from "../config/io.js"; @@ -9,7 +8,6 @@ import { shouldAttemptLastKnownGoodRecovery, } from "../config/recovery-policy.js"; import { resolveConfigWriteFollowUp } from "../config/runtime-snapshot.js"; -import type { GatewayReloadMode } from "../config/types.gateway.js"; import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; import { validateConfigObjectWithPlugins } from "../config/validation.js"; @@ -17,30 +15,23 @@ import { loadInstalledPluginIndexInstallRecords, loadInstalledPluginIndexInstallRecordsSync, } from "../plugins/installed-plugin-index-records.js"; -import { isPlainObject } from "../utils.js"; +import { diffConfigPaths } from "./config-diff.js"; import { buildGatewayReloadPlan, listPluginInstallTimestampMetadataPaths, listPluginInstallWholeRecordPaths, type GatewayReloadPlan, } from "./config-reload-plan.js"; +import { resolveGatewayReloadSettings } from "./config-reload-settings.js"; export { buildGatewayReloadPlan, + diffConfigPaths, listPluginInstallTimestampMetadataPaths, listPluginInstallWholeRecordPaths, + resolveGatewayReloadSettings, }; export type { ChannelKind, GatewayReloadPlan } from "./config-reload-plan.js"; - -type GatewayReloadSettings = { - mode: GatewayReloadMode; - debounceMs: number; -}; - -const DEFAULT_RELOAD_SETTINGS: GatewayReloadSettings = { - mode: "hybrid", - debounceMs: 300, -}; const MISSING_CONFIG_RETRY_DELAY_MS = 150; const MISSING_CONFIG_MAX_RETRIES = 2; @@ -115,51 +106,6 @@ function resolvePluginLocalInvalidReloadSnapshot(params: { }; } -export function diffConfigPaths(prev: unknown, next: unknown, prefix = ""): string[] { - if (prev === next) { - return []; - } - if (isPlainObject(prev) && isPlainObject(next)) { - const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); - const paths: string[] = []; - for (const key of keys) { - const prevValue = prev[key]; - const nextValue = next[key]; - if (prevValue === undefined && nextValue === undefined) { - continue; - } - const childPrefix = prefix ? `${prefix}.${key}` : key; - const childPaths = diffConfigPaths(prevValue, nextValue, childPrefix); - if (childPaths.length > 0) { - paths.push(...childPaths); - } - } - return paths; - } - if (Array.isArray(prev) && Array.isArray(next)) { - // Arrays can contain object entries (for example memory.qmd.paths/scope.rules); - // compare structurally so identical values are not reported as changed. - if (isDeepStrictEqual(prev, next)) { - return []; - } - } - return [prefix || ""]; -} - -export function resolveGatewayReloadSettings(cfg: OpenClawConfig): GatewayReloadSettings { - const rawMode = cfg.gateway?.reload?.mode; - const mode = - rawMode === "off" || rawMode === "restart" || rawMode === "hot" || rawMode === "hybrid" - ? rawMode - : DEFAULT_RELOAD_SETTINGS.mode; - const debounceRaw = cfg.gateway?.reload?.debounceMs; - const debounceMs = - typeof debounceRaw === "number" && Number.isFinite(debounceRaw) - ? Math.max(0, Math.floor(debounceRaw)) - : DEFAULT_RELOAD_SETTINGS.debounceMs; - return { mode, debounceMs }; -} - type GatewayConfigReloader = { stop: () => Promise; }; diff --git a/src/gateway/server-aux-handlers.ts b/src/gateway/server-aux-handlers.ts index 21a665d6f57..fba7148d764 100644 --- a/src/gateway/server-aux-handlers.ts +++ b/src/gateway/server-aux-handlers.ts @@ -10,12 +10,12 @@ import { activateSecretsRuntimeSnapshot, getActiveSecretsRuntimeSnapshot, } from "../secrets/runtime.js"; +import { diffConfigPaths } from "./config-diff.js"; import { buildGatewayReloadPlan, - diffConfigPaths, type ChannelKind, type GatewayReloadPlan, -} from "./config-reload.js"; +} from "./config-reload-plan.js"; import { createExecApprovalIosPushDelivery } from "./exec-approval-ios-push.js"; import { ExecApprovalManager } from "./exec-approval-manager.js"; import { createExecApprovalHandlers } from "./server-methods/exec-approval.js"; diff --git a/src/gateway/server-import-boundary.test.ts b/src/gateway/server-import-boundary.test.ts index 228d287bac5..259fcaecf1f 100644 --- a/src/gateway/server-import-boundary.test.ts +++ b/src/gateway/server-import-boundary.test.ts @@ -16,6 +16,16 @@ describe("gateway startup import boundaries", () => { expect(serverImpl).not.toContain('from "./server-cron.js"'); expect(serverImpl).toContain('from "./server-cron-lazy.js"'); expect(serverImpl).not.toContain('from "./server-methods.js"'); + expect(serverImpl).not.toContain('from "./config-reload.js"'); + expect(readSource("src/gateway/server-shared-auth-generation.ts")).not.toContain( + 'from "./config-reload.js"', + ); + expect(readSource("src/gateway/server-aux-handlers.ts")).not.toContain( + 'from "./config-reload.js"', + ); + expect(readSource("src/gateway/server-runtime-state.ts")).not.toContain( + 'createCanvasHostHandler } from "../canvas-host/server.js"', + ); expect(serverImpl).not.toContain('from "../plugins/hook-runner-global.js"'); expect(validation).not.toContain("legacy-secretref-env-marker"); expect(validation).not.toContain("commands/doctor"); diff --git a/src/gateway/server-methods/config-write-flow.ts b/src/gateway/server-methods/config-write-flow.ts index ea6eda4b361..522a8988335 100644 --- a/src/gateway/server-methods/config-write-flow.ts +++ b/src/gateway/server-methods/config-write-flow.ts @@ -14,7 +14,8 @@ import { import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { getActiveSecretsRuntimeSnapshot } from "../../secrets/runtime.js"; import { resolveEffectiveSharedGatewayAuth } from "../auth.js"; -import { buildGatewayReloadPlan, resolveGatewayReloadSettings } from "../config-reload.js"; +import { buildGatewayReloadPlan } from "../config-reload-plan.js"; +import { resolveGatewayReloadSettings } from "../config-reload-settings.js"; import { formatControlPlaneActor, type ControlPlaneActor } from "../control-plane-audit.js"; import { parseRestartRequestParams } from "./restart-request.js"; import type { GatewayRequestContext } from "./types.js"; diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 9ff87500120..759d93c33ef 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -22,7 +22,7 @@ import { prepareSecretsRuntimeSnapshot, type PreparedSecretsRuntimeSnapshot, } from "../../secrets/runtime.js"; -import { diffConfigPaths } from "../config-reload.js"; +import { diffConfigPaths } from "../config-diff.js"; import { formatControlPlaneActor, resolveControlPlaneActor, diff --git a/src/gateway/server-runtime-state.ts b/src/gateway/server-runtime-state.ts index 55eb571a84c..d1d9b05bf61 100644 --- a/src/gateway/server-runtime-state.ts +++ b/src/gateway/server-runtime-state.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, Server as HttpServer, ServerResponse } from "node:http"; import { WebSocketServer } from "ws"; import { CANVAS_HOST_PATH } from "../canvas-host/a2ui.js"; -import { type CanvasHostHandler, createCanvasHostHandler } from "../canvas-host/server.js"; +import type { CanvasHostHandler } from "../canvas-host/server.js"; import type { CliDeps } from "../cli/deps.types.js"; import type { createSubsystemLogger } from "../logging/subsystem.js"; import type { PluginRegistry } from "../plugins/registry.js"; @@ -120,6 +120,7 @@ export async function createGatewayRuntimeState(params: { let canvasHost: CanvasHostHandler | null = null; if (params.canvasHostEnabled) { try { + const { createCanvasHostHandler } = await import("../canvas-host/server.js"); const handler = await createCanvasHostHandler({ runtime: params.canvasRuntime, rootDir: params.cfg.canvasHost?.root, diff --git a/src/gateway/server-shared-auth-generation.ts b/src/gateway/server-shared-auth-generation.ts index 58c51775768..24105b7f6de 100644 --- a/src/gateway/server-shared-auth-generation.ts +++ b/src/gateway/server-shared-auth-generation.ts @@ -1,5 +1,5 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolveGatewayReloadSettings } from "./config-reload.js"; +import { resolveGatewayReloadSettings } from "./config-reload-settings.js"; export type SharedGatewayAuthClient = { usesSharedGatewayAuth?: boolean;