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 } seconds exceeded` )
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