import type { Server as HttpServer } from "node:http"; 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 { NodeBridgeServer } from "../infra/bridge/server.js"; import type { PluginServicesHandle } from "../plugins/services.js"; export function createGatewayCloseHandler(params: { bonjourStop: (() => Promise) | null; tailscaleCleanup: (() => Promise) | null; canvasHost: CanvasHostHandler | null; canvasHostServer: CanvasHostServer | null; bridge: NodeBridgeServer | null; stopChannel: (name: ChannelId, accountId?: string) => Promise; pluginServices: PluginServicesHandle | null; cron: { stop: () => void }; heartbeatRunner: { stop: () => void }; nodePresenceTimers: Map>; broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void; tickInterval: ReturnType; healthInterval: ReturnType; dedupeCleanup: ReturnType; agentUnsub: (() => void) | null; heartbeatUnsub: (() => void) | null; chatRunState: { clear: () => void }; clients: Set<{ socket: { close: (code: number, reason: string) => void } }>; configReloader: { stop: () => Promise }; browserControl: { stop: () => Promise } | null; wss: WebSocketServer; httpServer: HttpServer; }) { return async (opts?: { reason?: string; restartExpectedMs?: number | null }) => { const reasonRaw = typeof opts?.reason === "string" ? opts.reason.trim() : ""; const reason = reasonRaw || "gateway stopping"; const restartExpectedMs = typeof opts?.restartExpectedMs === "number" && Number.isFinite(opts.restartExpectedMs) ? Math.max(0, Math.floor(opts.restartExpectedMs)) : null; if (params.bonjourStop) { try { await params.bonjourStop(); } catch { /* ignore */ } } if (params.tailscaleCleanup) { await params.tailscaleCleanup(); } if (params.canvasHost) { try { await params.canvasHost.close(); } catch { /* ignore */ } } if (params.canvasHostServer) { try { await params.canvasHostServer.close(); } catch { /* ignore */ } } if (params.bridge) { try { await params.bridge.close(); } catch { /* ignore */ } } for (const plugin of listChannelPlugins()) { await params.stopChannel(plugin.id); } if (params.pluginServices) { await params.pluginServices.stop().catch(() => {}); } await stopGmailWatcher(); params.cron.stop(); params.heartbeatRunner.stop(); for (const timer of params.nodePresenceTimers.values()) { clearInterval(timer); } params.nodePresenceTimers.clear(); params.broadcast("shutdown", { reason, restartExpectedMs, }); clearInterval(params.tickInterval); clearInterval(params.healthInterval); clearInterval(params.dedupeCleanup); if (params.agentUnsub) { try { params.agentUnsub(); } catch { /* ignore */ } } if (params.heartbeatUnsub) { try { params.heartbeatUnsub(); } catch { /* ignore */ } } params.chatRunState.clear(); for (const c of params.clients) { try { c.socket.close(1012, "service restart"); } catch { /* ignore */ } } params.clients.clear(); await params.configReloader.stop().catch(() => {}); if (params.browserControl) { await params.browserControl.stop().catch(() => {}); } await new Promise((resolve) => params.wss.close(() => resolve())); await new Promise((resolve, reject) => params.httpServer.close((err) => (err ? reject(err) : resolve())), ); }; }