@@ -14,6 +14,7 @@ export interface StatusClientOptions {
1414 baseURL : string ;
1515 fetchImpl ?: typeof fetch ;
1616 headers ?: Record < string , string > ;
17+ timeoutMs ?: number ;
1718}
1819
1920export class StatusApiError extends Error {
@@ -70,45 +71,53 @@ export class KeeperStatusClient {
7071 readonly baseURL : string ;
7172 readonly fetchImpl : typeof fetch ;
7273 readonly headers : Record < string , string > ;
74+ readonly timeoutMs : number ;
7375
7476 constructor ( opts : StatusClientOptions ) {
7577 this . baseURL = opts . baseURL ;
7678 this . fetchImpl = opts . fetchImpl ?? globalThis . fetch ;
7779 this . headers = opts . headers ?? { } ;
80+ this . timeoutMs = opts . timeoutMs ?? 10_000 ;
7881 if ( ! this . fetchImpl ) {
7982 throw new Error (
8083 "No global fetch found. Pass `fetchImpl` in StatusClientOptions." ,
8184 ) ;
8285 }
8386 }
8487
85- async getStatus ( ) : Promise < KeeperStatusResponse > {
86- return this . getJSON < KeeperStatusResponse > ( "/status" ) ;
88+ async getStatus ( signal ?: AbortSignal ) : Promise < KeeperStatusResponse > {
89+ return this . getJSON < KeeperStatusResponse > ( "/status" , signal ) ;
8790 }
8891
89- async getRound ( roundId : number | bigint | string ) : Promise < KeeperRoundStatusView > {
90- return this . getJSON < KeeperRoundStatusView > ( `/status/rounds/${ roundId } ` ) ;
92+ async getRound ( roundId : number | bigint | string , signal ?: AbortSignal ) : Promise < KeeperRoundStatusView > {
93+ return this . getJSON < KeeperRoundStatusView > ( `/status/rounds/${ roundId } ` , signal ) ;
9194 }
9295
93- async getHealth ( ) : Promise < KeeperHealthResponse > {
94- return this . getJSON < KeeperHealthResponse > ( "/status/health" ) ;
96+ async getHealth ( signal ?: AbortSignal ) : Promise < KeeperHealthResponse > {
97+ return this . getJSON < KeeperHealthResponse > ( "/status/health" , signal ) ;
9598 }
9699
97- async healthz ( ) : Promise < { ok : boolean ; [ k : string ] : unknown } > {
98- return this . getJSON < { ok : boolean ; [ k : string ] : unknown } > ( "/healthz" ) ;
100+ async healthz ( signal ?: AbortSignal ) : Promise < { ok : boolean ; [ k : string ] : unknown } > {
101+ return this . getJSON < { ok : boolean ; [ k : string ] : unknown } > ( "/healthz" , signal ) ;
99102 }
100103
101- async getJSON < T > ( path : string ) : Promise < T > {
104+ async getJSON < T > ( path : string , callerSignal ?: AbortSignal ) : Promise < T > {
102105 const url = fullURL ( this . baseURL , path ) ;
103- const res = await this . fetchImpl ( url , {
104- method : "GET" ,
105- headers : { Accept : "application/json" , ...this . headers } ,
106- } ) ;
107- if ( ! res . ok ) {
108- const body = await parseErrorBody ( res ) ;
109- throw new StatusApiError ( res . status , body ) ;
106+ const controller = new AbortController ( ) ;
107+ const timer = setTimeout ( ( ) => controller . abort ( new Error ( "keeper status request timed out" ) ) , this . timeoutMs ) ;
108+ const forwardAbort = ( ) => controller . abort ( callerSignal ?. reason ) ;
109+ callerSignal ?. addEventListener ( "abort" , forwardAbort , { once : true } ) ;
110+ try {
111+ const res = await this . fetchImpl ( url , { method : "GET" , headers : { Accept : "application/json" , ...this . headers } , signal : controller . signal } ) ;
112+ if ( ! res . ok ) {
113+ const body = await parseErrorBody ( res ) ;
114+ throw new StatusApiError ( res . status , body ) ;
115+ }
116+ return parseSuccessBody < T > ( res ) ;
117+ } finally {
118+ clearTimeout ( timer ) ;
119+ callerSignal ?. removeEventListener ( "abort" , forwardAbort ) ;
110120 }
111- return parseSuccessBody < T > ( res ) ;
112121 }
113122}
114123
0 commit comments