/** * Shutdown gate — defers process.exit() so that page refreshes (which fire * `pagehide` then immediately re-boot) don't kill the server. * * Flow: * pagehide → POST /api/shutdown → scheduleShutdown() → timer starts * refresh → GET /api/boot → cancelShutdown() → timer cleared * tab close → timer fires → process.exit(0) * * When SF_WEB_DAEMON_MODE=1, the server is running as a persistent daemon * (e.g. behind a reverse proxy for remote access). In this mode, * scheduleShutdown() is a no-op — no client tab should be able to exit the * server. The /api/shutdown endpoint still returns { ok: true } so the * client beacon doesn't produce a network error. */ const SHUTDOWN_DELAY_MS = 3_000; let shutdownTimer: ReturnType | null = null; /** * Returns true when the server is running in daemon mode. * In daemon mode, shutdown requests from browser tabs are ignored. */ export function isDaemonMode(): boolean { return process.env.SF_WEB_DAEMON_MODE === "1"; } /** * Schedule a graceful process exit after SHUTDOWN_DELAY_MS. * If cancelShutdown() is called before the timer fires (e.g. a page refresh * triggers a boot request), the exit is aborted. * * No-op when SF_WEB_DAEMON_MODE=1 — the server should outlive any * individual browser session. */ export function scheduleShutdown(): void { if (isDaemonMode()) { return; } // Don't stack timers — reset if already scheduled if (shutdownTimer !== null) { clearTimeout(shutdownTimer); } shutdownTimer = setTimeout(() => { shutdownTimer = null; process.exit(0); }, SHUTDOWN_DELAY_MS); } /** * Cancel a pending shutdown. Called by any incoming API request that proves * the client is still alive (boot, SSE reconnect, etc.). */ export function cancelShutdown(): void { if (shutdownTimer !== null) { clearTimeout(shutdownTimer); shutdownTimer = null; } } /** * Check whether a shutdown is currently pending. */ export function isShutdownPending(): boolean { return shutdownTimer !== null; }