@@ -13,13 +13,29 @@ const defaultOption: RequestOptions = {
13
13
type OptionReturnType < Opt , T > = Opt extends { unwrapData : false } ? AxiosResponse < T > : Opt extends { unwrapData : true } ? T : T
14
14
15
15
export type APIClientOptions = {
16
- wrapResponseErrors : boolean
16
+ wrapResponseErrors : boolean ;
17
+ timeout ?: number ;
18
+ retryConfig ?: {
19
+ maxRetries : number ;
20
+ baseDelay : number ;
21
+ } ;
17
22
}
18
23
19
24
export class API {
20
25
private axios : AxiosInstance
21
26
22
- constructor ( readonly accessToken : string , public hackmdAPIEndpointURL : string = "https://api.hackmd.io/v1" , public options : APIClientOptions = { wrapResponseErrors : true } ) {
27
+ constructor (
28
+ readonly accessToken : string ,
29
+ public hackmdAPIEndpointURL : string = "https://api.hackmd.io/v1" ,
30
+ public options : APIClientOptions = {
31
+ wrapResponseErrors : true ,
32
+ timeout : 30000 ,
33
+ retryConfig : {
34
+ maxRetries : 3 ,
35
+ baseDelay : 100 ,
36
+ } ,
37
+ }
38
+ ) {
23
39
if ( ! accessToken ) {
24
40
throw new HackMDErrors . MissingRequiredArgument ( 'Missing access token when creating HackMD client' )
25
41
}
@@ -28,7 +44,8 @@ export class API {
28
44
baseURL : hackmdAPIEndpointURL ,
29
45
headers :{
30
46
"Content-Type" : "application/json" ,
31
- }
47
+ } ,
48
+ timeout : options . timeout
32
49
} )
33
50
34
51
this . axios . interceptors . request . use (
@@ -71,13 +88,51 @@ export class API {
71
88
`Received an error response (${ err . response . status } ${ err . response . statusText } ) from HackMD` ,
72
89
err . response . status ,
73
90
err . response . statusText ,
74
- )
91
+ ) ;
75
92
}
76
93
}
77
- )
94
+ ) ;
95
+ }
96
+ if ( options . retryConfig ) {
97
+ this . createRetryInterceptor ( this . axios , options . retryConfig . maxRetries , options . retryConfig . baseDelay ) ;
78
98
}
79
99
}
80
100
101
+ private exponentialBackoff ( retries : number , baseDelay : number ) : number {
102
+ return Math . pow ( 2 , retries ) * baseDelay ;
103
+ }
104
+
105
+ private isRetryableError ( error : AxiosError ) : boolean {
106
+ return (
107
+ ! error . response ||
108
+ ( error . response . status >= 500 && error . response . status < 600 ) ||
109
+ error . response . status === 429
110
+ ) ;
111
+ }
112
+
113
+ private createRetryInterceptor ( axiosInstance : AxiosInstance , maxRetries : number , baseDelay : number ) : void {
114
+ let retryCount = 0 ;
115
+
116
+ axiosInstance . interceptors . response . use (
117
+ response => response ,
118
+ async error => {
119
+ if ( retryCount < maxRetries && this . isRetryableError ( error ) ) {
120
+ const remainingCredits = parseInt ( error . response ?. headers [ 'x-ratelimit-userremaining' ] , 10 ) ;
121
+
122
+ if ( isNaN ( remainingCredits ) || remainingCredits > 0 ) {
123
+ retryCount ++ ;
124
+ const delay = this . exponentialBackoff ( retryCount , baseDelay ) ;
125
+ console . warn ( `Retrying request... attempt #${ retryCount } after delay of ${ delay } ms` ) ;
126
+ await new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
127
+ return axiosInstance ( error . config ) ;
128
+ }
129
+ }
130
+
131
+ retryCount = 0 ; // Reset retry count after a successful request or when not retrying
132
+ return Promise . reject ( error ) ;
133
+ }
134
+ ) ;
135
+ }
81
136
async getMe < Opt extends RequestOptions > ( options = defaultOption as Opt ) : Promise < OptionReturnType < Opt , GetMe > > {
82
137
return this . unwrapData ( this . axios . get < GetMe > ( "me" ) , options . unwrapData ) as unknown as OptionReturnType < Opt , GetMe >
83
138
}
0 commit comments