singularity-forge/web/lib/shutdown-gate.ts
ace-pm 35dc87ef53 chore: sync workspace state after rebrand
- Rebrand commits already in history (gsd → forge)
- Sync pre-existing doc, docker, and CI config updates
- All rebrand artifacts verified in place:
  * Native crates: forge-engine, forge-ast, forge-grep
  * Log prefixes: [forge] across 22+ files
  * Binary: ~/bin/sf-run
  * Workspace scopes: @sf-run/*, @singularity-forge/*
  * Nix flake: Rust toolchain ready

System ready for: nix develop && bun run build:native

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:54:20 +02:00

69 lines
2 KiB
TypeScript

/**
* 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<typeof setTimeout> | 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;
}