@@ -13,13 +13,29 @@ const defaultOption: RequestOptions = {
1313type OptionReturnType < Opt , T > = Opt extends { unwrapData : false } ? AxiosResponse < T > : Opt extends { unwrapData : true } ? T : T
1414
1515export type APIClientOptions = {
16- wrapResponseErrors : boolean
16+ wrapResponseErrors : boolean ;
17+ timeout ?: number ;
18+ retryConfig ?: {
19+ maxRetries : number ;
20+ baseDelay : number ;
21+ } ;
1722}
1823
1924export class API {
2025 private axios : AxiosInstance
2126
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+ ) {
2339 if ( ! accessToken ) {
2440 throw new HackMDErrors . MissingRequiredArgument ( 'Missing access token when creating HackMD client' )
2541 }
@@ -29,7 +45,7 @@ export class API {
2945 headers :{
3046 "Content-Type" : "application/json" ,
3147 } ,
32- timeout : 30000 , // Increased timeout for low bandwidth
48+ timeout : options . timeout
3349 } )
3450
3551 this . axios . interceptors . request . use (
@@ -77,39 +93,42 @@ export class API {
7793 }
7894 ) ;
7995 }
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+ }
8199 }
82100
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 ;
86103 }
87104
88105 private isRetryableError ( error : AxiosError ) : boolean {
89- // Retry on network errors, 5xx errors, and rate limiting (429)
90106 return (
91107 ! error . response ||
92108 ( error . response . status >= 500 && error . response . status < 600 ) ||
93109 error . response . status === 429
94110 ) ;
95111 }
96112
97- // Create retry interceptor function
98- private createRetryInterceptor ( axiosInstance : AxiosInstance , maxRetries : number ) : void {
113+ private createRetryInterceptor ( axiosInstance : AxiosInstance , maxRetries : number , baseDelay : number ) : void {
99114 let retryCount = 0 ;
100115
101116 axiosInstance . interceptors . response . use (
102117 response => response ,
103118 async error => {
104119 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+ }
110129 }
111130
112- retryCount = 0 ; // Reset retry count after a successful request
131+ retryCount = 0 ; // Reset retry count after a successful request or when not retrying
113132 return Promise . reject ( error ) ;
114133 }
115134 ) ;
0 commit comments