process-shutdown.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. /** Bounded, escalating process shutdown for the long-lived CLI surfaces. */
  2. /** Maximum grace allowed for the application tree to dispose before process exit. */
  3. export const PROCESS_SHUTDOWN_TIMEOUT_MS = 5_000
  4. /** Process-exit controller shared by normal completion and Unix signal handlers. */
  5. export interface ProcessShutdown {
  6. /** Start or join graceful disposal before allowing natural completion with `code`. */
  7. shutdown(code: number): Promise<void>
  8. /** Start graceful disposal followed by exit, or force exit when shutdown is already running. */
  9. interrupt(code: number): void
  10. }
  11. /**
  12. * Create one process-exit controller around an application disposer.
  13. * @param dispose - Whole-application teardown that resolves at quiescence.
  14. * @param forceExit - Function that exits the process immediately, replaceable by tests.
  15. * @param complete - Function that records the natural completion code, replaceable by tests.
  16. * @param timeoutMs - Grace before forced exit, replaceable by tests.
  17. * @returns A controller whose normal calls coalesce and whose repeated signal call escalates.
  18. */
  19. export function createProcessShutdown(
  20. dispose: () => Promise<void>,
  21. forceExit: (code: number) => void = (code) => { process.exit(code) },
  22. complete: (code: number) => void = (code) => { process.exitCode = code },
  23. timeoutMs = PROCESS_SHUTDOWN_TIMEOUT_MS,
  24. ): ProcessShutdown {
  25. let pending: Promise<void> | undefined
  26. let timeout: ReturnType<typeof setTimeout> | undefined
  27. let completed = false
  28. let forceExited = false
  29. const clearExitTimeout = (): void => {
  30. /* v8 ignore else -- shutdown() arms the timer before any asynchronous exit path can run. */
  31. if (timeout !== undefined) clearTimeout(timeout)
  32. }
  33. const forceExitOnce = (code: number): void => {
  34. if (forceExited) return
  35. forceExited = true
  36. clearExitTimeout()
  37. forceExit(code)
  38. }
  39. const completeOnce = (code: number): void => {
  40. if (completed || forceExited) return
  41. completed = true
  42. clearExitTimeout()
  43. complete(code)
  44. }
  45. const start = (code: number, forceAfterDispose: boolean): Promise<void> => {
  46. if (pending !== undefined) return pending
  47. timeout = setTimeout(() => { forceExitOnce(code) }, timeoutMs)
  48. pending = Promise.resolve().then(dispose).then(
  49. () => {
  50. if (forceAfterDispose) forceExitOnce(code)
  51. else completeOnce(code)
  52. },
  53. () => { forceExitOnce(code) },
  54. )
  55. return pending
  56. }
  57. return {
  58. shutdown(code) {
  59. return start(code, false)
  60. },
  61. interrupt(code) {
  62. if (pending !== undefined) {
  63. forceExitOnce(code)
  64. return
  65. }
  66. void start(code, true)
  67. },
  68. }
  69. }