@@ -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
}
@@ -29,7 +45,7 @@ export class API {
29
45
headers :{
30
46
"Content-Type" : "application/json" ,
31
47
} ,
32
- timeout : 30000 , // Increased timeout for low bandwidth
48
+ timeout : options . timeout
33
49
} )
34
50
35
51
this . axios . interceptors . request . use (
@@ -77,39 +93,42 @@ export class API {
77
93
}
78
94
) ;
79
95
}
80
- this . createRetryInterceptor ( this . axios , 3 ) ; // Add retry interceptor with maxRetries = 3
96
+ if ( options . retryConfig ) {
97
+ this . createRetryInterceptor ( this . axios , options . retryConfig . maxRetries , options . retryConfig . baseDelay ) ;
98
+ }
81
99
}
82
100
83
- // Utility functions for exponential backoff and retry logic
84
- private exponentialBackoff ( retries : number ) : number {
85
- return Math . pow ( 2 , retries ) * 100 ; // Exponential backoff with base delay of 100ms
101
+ private exponentialBackoff ( retries : number , baseDelay : number ) : number {
102
+ return Math . pow ( 2 , retries ) * baseDelay ;
86
103
}
87
104
88
105
private isRetryableError ( error : AxiosError ) : boolean {
89
- // Retry on network errors, 5xx errors, and rate limiting (429)
90
106
return (
91
107
! error . response ||
92
108
( error . response . status >= 500 && error . response . status < 600 ) ||
93
109
error . response . status === 429
94
110
) ;
95
111
}
96
112
97
- // Create retry interceptor function
98
- private createRetryInterceptor ( axiosInstance : AxiosInstance , maxRetries : number ) : void {
113
+ private createRetryInterceptor ( axiosInstance : AxiosInstance , maxRetries : number , baseDelay : number ) : void {
99
114
let retryCount = 0 ;
100
115
101
116
axiosInstance . interceptors . response . use (
102
117
response => response ,
103
118
async error => {
104
119
if ( retryCount < maxRetries && this . isRetryableError ( error ) ) {
105
- retryCount ++ ;
106
- const delay = this . exponentialBackoff ( retryCount ) ;
107
- console . warn ( `Retrying request... attempt #${ retryCount } after delay of ${ delay } ms` ) ;
108
- await new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
109
- return axiosInstance ( error . config ) ;
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
+ }
110
129
}
111
130
112
- retryCount = 0 ; // Reset retry count after a successful request
131
+ retryCount = 0 ; // Reset retry count after a successful request or when not retrying
113
132
return Promise . reject ( error ) ;
114
133
}
115
134
) ;
0 commit comments