/** * Graceful shutdown helper for `spectral serve`. * * Why a separate module: * - The shutdown flag (`shutdownState.isShuttingDown`) is read by the relay * dispatcher to reject new `client_message` envelopes. Putting it on * `serve.ts` would create a circular import (dispatcher → serve → relay * client → …); pulling it out keeps the dependency graph linear. * - The orchestration is small but has enough edge cases (idempotent on a * second SIGTERM, bounded wait for in-flight streams, best-effort store * close) that it's worth one focused unit test. * * Behaviour summary (matches the contract in the Batch 6 brief): * 1. First SIGINT/SIGTERM: * - Log "Shutting down…" to stderr. * - Flip `shutdownState.isShuttingDown = true`. From this point on the * dispatcher refuses new `client_message` frames with an error * wrapped as a `ws_event` (so the browser sees the rejection on the * same stream it was using). * - Wait up to `gracePeriodMs` (default 5_000 ms) for the in-flight * stream count reported by `inFlightCount()` to drain to 0. Polled * every 100 ms — any non-zero value indicates an active turn. * - Close the relay (code 1000, reason "shutdown") and dispose the * manager. The dispose order matters: relay first so the backend * sees a clean close before our local state goes; manager second so * any spectral processes get torn down deterministically. * - Close the SQLite store (best-effort — a failure here just gets * logged; the process is exiting anyway). * - Exit with the supplied code (default 0). * 2. Second SIGINT/SIGTERM during the grace period: skip the wait, force * immediate exit with code 1. We don't want a hung spectral process to * prevent operators from killing the server with a second Ctrl-C. * * The function is intentionally framework-free — it takes plain callbacks * for everything I/O-shaped so the unit test can exercise the orchestration * without spawning a real relay or sqlite handle. */ /** * Process-wide shutdown flag. Read by the dispatcher to gate new * `client_message` envelopes; written by `gracefulShutdown` (and by tests * via `resetShutdownState`). * * Module-level singleton because there is exactly one server process per * `spectral serve` invocation; co-locating with serve.ts would require * threading the flag through the dispatcher's deps (already a small * object) and risks dispatcher tests carrying serve.ts state. */ export declare const shutdownState: { isShuttingDown: boolean; }; /** * Reset the singleton flag. Tests use this in `beforeEach` so a previous * test's shutdown doesn't bleed into the next. Production code must NOT * call this — once the process is shutting down, it's shutting down. */ export declare function resetShutdownState(): void; export interface GracefulShutdownOptions { /** Logger; defaults to `console`. Tests inject a noop or a recorder. */ logger?: Pick; /** * Reports in-flight stream count (e.g. `() => manager.activeTurnCount()`). * Polled every 100 ms until it returns 0 or `gracePeriodMs` elapses. * If omitted we skip the wait entirely (treated as "nothing in flight"). */ inFlightCount?: () => number; /** Close the relay with code 1000 / reason "shutdown". */ closeRelay?: () => void | Promise; /** Dispose the SessionStreamManager (kills spectral processes). */ disposeManager?: () => void | Promise; /** Close the SQLite store. Failures are logged, not rethrown. */ closeStore?: () => void; /** Close the Playwright browser service so Chrome for Testing exits. */ closeBrowser?: () => void | Promise; /** Bound on how long to wait for in-flight streams. Default 5_000 ms. */ gracePeriodMs?: number; /** Polling interval for the in-flight wait. Default 100 ms. */ pollIntervalMs?: number; /** Exit code on a clean first-signal shutdown. Default 0. */ exitCode?: number; /** * Override `process.exit` for tests. Not called as a function reference * directly so the test can assert on what was requested without actually * killing vitest. */ exitFn?: (code: number) => void; /** * Sleep helper. Tests inject a mock clock; production uses real * `setTimeout`. Resolves after `ms` milliseconds. */ sleep?: (ms: number) => Promise; } /** Test-only: reset the entry counter alongside `shutdownState`. */ export declare function resetShutdownEntryCount(): void; /** * Run the graceful shutdown sequence. Idempotent on the entry-count * dimension: the second concurrent call resolves immediately after * triggering an immediate exit. * * Returns once the supplied `exitFn` has been invoked (or would have been * invoked in production where `process.exit` does not return). Tests can * await it to assert ordering. */ export declare function gracefulShutdown(opts?: GracefulShutdownOptions): Promise; //# sourceMappingURL=shutdown.d.ts.map