@@ -6,11 +6,43 @@ interface ErrorWithCode {
66}
77
88const BROKEN_PIPE_CODES = new Set ( [ "EPIPE" , "ERR_STREAM_DESTROYED" , "ECONNRESET" ] ) ;
9+ const DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1000 ;
10+ const MIN_IDLE_TIMEOUT_MS = 60 * 1000 ;
11+ const DEFAULT_PARENT_POLL_MS = 5 * 1000 ;
12+ const MIN_PARENT_POLL_MS = 1 * 1000 ;
913
1014export interface CleanupOptions {
1115 stopTracker : ( ) => void ;
1216 closeServer : ( ) => Promise < void > | void ;
1317 closeTransport : ( ) => Promise < void > | void ;
18+ stopMonitors ?: ( ) => void ;
19+ }
20+
21+ export interface IdleMonitor {
22+ touch : ( ) => void ;
23+ stop : ( ) => void ;
24+ }
25+
26+ export interface IdleMonitorOptions {
27+ timeoutMs : number ;
28+ onIdle : ( ) => void ;
29+ }
30+
31+ export interface ParentMonitorOptions {
32+ parentPid : number ;
33+ pollIntervalMs ?: number ;
34+ onParentExit : ( ) => void ;
35+ isProcessAlive ?: ( pid : number ) => boolean ;
36+ }
37+
38+ function toIntegerOr ( value : string | undefined , fallback : number ) : number {
39+ if ( ! value ) return fallback ;
40+ const parsed = Number . parseInt ( value , 10 ) ;
41+ return Number . isFinite ( parsed ) ? parsed : fallback ;
42+ }
43+
44+ function unrefHandle ( handle : { unref ?: ( ) => void } | null ) : void {
45+ handle ?. unref ?.( ) ;
1446}
1547
1648export function isBrokenPipeError ( error : unknown ) : boolean {
@@ -19,7 +51,89 @@ export function isBrokenPipeError(error: unknown): boolean {
1951 return typeof code === "string" && BROKEN_PIPE_CODES . has ( code ) ;
2052}
2153
54+ export function getIdleShutdownMs ( value : string | undefined ) : number {
55+ const normalized = value ?. trim ( ) . toLowerCase ( ) ;
56+ if ( normalized && [ "0" , "false" , "off" , "disabled" , "none" ] . includes ( normalized ) ) return 0 ;
57+ return Math . max ( MIN_IDLE_TIMEOUT_MS , toIntegerOr ( value , DEFAULT_IDLE_TIMEOUT_MS ) ) ;
58+ }
59+
60+ export function getParentPollMs ( value : string | undefined ) : number {
61+ return Math . max ( MIN_PARENT_POLL_MS , toIntegerOr ( value , DEFAULT_PARENT_POLL_MS ) ) ;
62+ }
63+
64+ export function isProcessAlive ( pid : number , killCheck : ( pid : number , signal : number ) => void = process . kill ) : boolean {
65+ if ( ! Number . isFinite ( pid ) || pid <= 0 ) return false ;
66+
67+ try {
68+ killCheck ( pid , 0 ) ;
69+ return true ;
70+ } catch ( error ) {
71+ if ( ! error || typeof error !== "object" ) return false ;
72+ const { code } = error as ErrorWithCode ;
73+ return code !== "ESRCH" ;
74+ }
75+ }
76+
77+ export function createIdleMonitor ( options : IdleMonitorOptions ) : IdleMonitor {
78+ if ( options . timeoutMs <= 0 ) {
79+ return {
80+ touch : ( ) => { } ,
81+ stop : ( ) => { } ,
82+ } ;
83+ }
84+
85+ let timer : NodeJS . Timeout | null = null ;
86+
87+ const schedule = ( ) : void => {
88+ if ( timer ) clearTimeout ( timer ) ;
89+ timer = setTimeout ( ( ) => {
90+ timer = null ;
91+ options . onIdle ( ) ;
92+ } , options . timeoutMs ) ;
93+ unrefHandle ( timer ) ;
94+ } ;
95+
96+ schedule ( ) ;
97+
98+ return {
99+ touch : schedule ,
100+ stop : ( ) => {
101+ if ( ! timer ) return ;
102+ clearTimeout ( timer ) ;
103+ timer = null ;
104+ } ,
105+ } ;
106+ }
107+
108+ export function startParentMonitor ( options : ParentMonitorOptions ) : ( ) => void {
109+ if ( ! Number . isFinite ( options . parentPid ) || options . parentPid <= 1 || options . parentPid === process . pid ) {
110+ return ( ) => { } ;
111+ }
112+
113+ const pollIntervalMs = Math . max ( MIN_PARENT_POLL_MS , Math . floor ( options . pollIntervalMs ?? DEFAULT_PARENT_POLL_MS ) ) ;
114+ const isAlive = options . isProcessAlive ?? isProcessAlive ;
115+ let stopped = false ;
116+
117+ const stop = ( ) : void => {
118+ if ( stopped ) return ;
119+ stopped = true ;
120+ clearInterval ( interval ) ;
121+ } ;
122+
123+ const interval = setInterval ( ( ) => {
124+ if ( stopped ) return ;
125+ if ( process . ppid !== options . parentPid || ! isAlive ( options . parentPid ) ) {
126+ stop ( ) ;
127+ options . onParentExit ( ) ;
128+ }
129+ } , pollIntervalMs ) ;
130+
131+ unrefHandle ( interval ) ;
132+ return stop ;
133+ }
134+
22135export async function runCleanup ( options : CleanupOptions ) : Promise < void > {
136+ options . stopMonitors ?.( ) ;
23137 options . stopTracker ( ) ;
24138 await Promise . allSettled ( [
25139 Promise . resolve ( options . closeServer ( ) ) ,
0 commit comments