fix: reschedule heartbeat on hot reload

Co-authored-by: Seb Slight <sebslight@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-21 00:52:06 +00:00
parent eff292eda4
commit d8abd53a1d
6 changed files with 184 additions and 42 deletions

View File

@@ -3,6 +3,7 @@ import type { WebSocketServer } from "ws";
import type { CanvasHostHandler, CanvasHostServer } from "../canvas-host/server.js";
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
import { stopGmailWatcher } from "../hooks/gmail-watcher.js";
import type { HeartbeatRunner } from "../infra/heartbeat-runner.js";
import type { PluginServicesHandle } from "../plugins/services.js";
export function createGatewayCloseHandler(params: {
@@ -13,7 +14,7 @@ export function createGatewayCloseHandler(params: {
stopChannel: (name: ChannelId, accountId?: string) => Promise<void>;
pluginServices: PluginServicesHandle | null;
cron: { stop: () => void };
heartbeatRunner: { stop: () => void };
heartbeatRunner: HeartbeatRunner;
nodePresenceTimers: Map<string, ReturnType<typeof setInterval>>;
broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void;
tickInterval: ReturnType<typeof setInterval>;

View File

@@ -1,7 +1,7 @@
import type { CliDeps } from "../cli/deps.js";
import type { loadConfig } from "../config/config.js";
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
import type { HeartbeatRunner } from "../infra/heartbeat-runner.js";
import { resetDirectoryCache } from "../infra/outbound/target-resolver.js";
import {
authorizeGatewaySigusr1Restart,
@@ -18,7 +18,7 @@ import { buildGatewayCronService, type GatewayCronState } from "./server-cron.js
type GatewayHotReloadState = {
hooksConfig: ReturnType<typeof resolveHooksConfig>;
heartbeatRunner: { stop: () => void };
heartbeatRunner: HeartbeatRunner;
cronState: GatewayCronState;
browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> | null;
};
@@ -57,8 +57,7 @@ export function createGatewayReloadHandlers(params: {
}
if (plan.restartHeartbeat) {
state.heartbeatRunner.stop();
nextState.heartbeatRunner = startHeartbeatRunner({ cfg: nextConfig });
nextState.heartbeatRunner.updateConfig(nextConfig);
}
resetDirectoryCache();

View File

@@ -21,7 +21,11 @@ const hoisted = vi.hoisted(() => {
}));
const heartbeatStop = vi.fn();
const startHeartbeatRunner = vi.fn(() => ({ stop: heartbeatStop }));
const heartbeatUpdateConfig = vi.fn();
const startHeartbeatRunner = vi.fn(() => ({
stop: heartbeatStop,
updateConfig: heartbeatUpdateConfig,
}));
const startGmailWatcher = vi.fn(async () => ({ started: true }));
const stopGmailWatcher = vi.fn(async () => {});
@@ -116,6 +120,7 @@ const hoisted = vi.hoisted(() => {
browserStop,
startBrowserControlServerIfEnabled,
heartbeatStop,
heartbeatUpdateConfig,
startHeartbeatRunner,
startGmailWatcher,
stopGmailWatcher,
@@ -237,8 +242,9 @@ describe("gateway hot reload", () => {
expect(hoisted.browserStop).toHaveBeenCalledTimes(1);
expect(hoisted.startBrowserControlServerIfEnabled).toHaveBeenCalledTimes(2);
expect(hoisted.startHeartbeatRunner).toHaveBeenCalledTimes(2);
expect(hoisted.heartbeatStop).toHaveBeenCalledTimes(1);
expect(hoisted.startHeartbeatRunner).toHaveBeenCalledTimes(1);
expect(hoisted.heartbeatUpdateConfig).toHaveBeenCalledTimes(1);
expect(hoisted.heartbeatUpdateConfig).toHaveBeenCalledWith(nextConfig);
expect(hoisted.cronInstances.length).toBe(2);
expect(hoisted.cronInstances[0].stop).toHaveBeenCalledTimes(1);