11import  {  logger  }  from  "@coder/logger" 
22import  {  promises  as  fs  }  from  "fs" 
3- import  {  wrapper  }  from  "./wrapper " 
3+ import  {  Emitter  }  from  "../common/emitter " 
44
55/** 
66 * Provides a heartbeat using a local file to indicate activity. 
77 */ 
88export  class  Heart  { 
99  private  heartbeatTimer ?: NodeJS . Timeout 
10-   private  idleShutdownTimer ?: NodeJS . Timeout 
1110  private  heartbeatInterval  =  60000 
1211  public  lastHeartbeat  =  0 
12+   private  readonly  _onChange  =  new  Emitter < "alive"  |  "idle"  |  "unknown" > ( ) 
13+   readonly  onChange  =  this . _onChange . event 
14+   private  state : "alive"  |  "idle"  |  "unknown"  =  "idle" 
1315
1416  public  constructor ( 
1517    private  readonly  heartbeatPath : string , 
16-     private  idleTimeoutSeconds : number  |  undefined , 
1718    private  readonly  isActive : ( )  =>  Promise < boolean > , 
1819  )  { 
1920    this . beat  =  this . beat . bind ( this ) 
2021    this . alive  =  this . alive . bind ( this ) 
22+   } 
2123
22-     if  ( this . idleTimeoutSeconds )  { 
23-       this . idleShutdownTimer  =  setTimeout ( ( )  =>  this . exitIfIdle ( ) ,  this . idleTimeoutSeconds  *  1000 ) 
24+   private  setState ( state : typeof  this . state )  { 
25+     if  ( this . state  !==  state )  { 
26+       this . state  =  state 
27+       this . _onChange . emit ( this . state ) 
2428    } 
2529  } 
2630
@@ -35,6 +39,7 @@ export class Heart {
3539   */ 
3640  public  async  beat ( ) : Promise < void >  { 
3741    if  ( this . alive ( ) )  { 
42+       this . setState ( "alive" ) 
3843      return 
3944    } 
4045
@@ -43,13 +48,22 @@ export class Heart {
4348    if  ( typeof  this . heartbeatTimer  !==  "undefined" )  { 
4449      clearTimeout ( this . heartbeatTimer ) 
4550    } 
46-     if  ( typeof  this . idleShutdownTimer  !==  "undefined" )  { 
47-       clearInterval ( this . idleShutdownTimer ) 
48-     } 
49-     this . heartbeatTimer  =  setTimeout ( ( )  =>  heartbeatTimer ( this . isActive ,  this . beat ) ,  this . heartbeatInterval ) 
50-     if  ( this . idleTimeoutSeconds )  { 
51-       this . idleShutdownTimer  =  setTimeout ( ( )  =>  this . exitIfIdle ( ) ,  this . idleTimeoutSeconds  *  1000 ) 
52-     } 
51+ 
52+     this . heartbeatTimer  =  setTimeout ( async  ( )  =>  { 
53+       try  { 
54+         if  ( await  this . isActive ( ) )  { 
55+           this . beat ( ) 
56+         }  else  { 
57+           this . setState ( "idle" ) 
58+         } 
59+       }  catch  ( error : unknown )  { 
60+         logger . warn ( ( error  as  Error ) . message ) 
61+         this . setState ( "unknown" ) 
62+       } 
63+     } ,  this . heartbeatInterval ) 
64+ 
65+     this . setState ( "alive" ) 
66+ 
5367    try  { 
5468      return  await  fs . writeFile ( this . heartbeatPath ,  "" ) 
5569    }  catch  ( error : any )  { 
@@ -65,26 +79,4 @@ export class Heart {
6579      clearTimeout ( this . heartbeatTimer ) 
6680    } 
6781  } 
68- 
69-   private  exitIfIdle ( ) : void { 
70-     logger . warn ( `Idle timeout of ${ this . idleTimeoutSeconds }  ) 
71-     wrapper . exit ( 0 ) 
72-   } 
73- } 
74- 
75- /** 
76-  * Helper function for the heartbeatTimer. 
77-  * 
78-  * If heartbeat is active, call beat. Otherwise do nothing. 
79-  * 
80-  * Extracted to make it easier to test. 
81-  */ 
82- export  async  function  heartbeatTimer ( isActive : Heart [ "isActive" ] ,  beat : Heart [ "beat" ] )  { 
83-   try  { 
84-     if  ( await  isActive ( ) )  { 
85-       beat ( ) 
86-     } 
87-   }  catch  ( error : unknown )  { 
88-     logger . warn ( ( error  as  Error ) . message ) 
89-   } 
9082} 
0 commit comments