327 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			327 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * WebrequestClient - Advanced configuration and global interceptors | ||
|  |  */ | ||
|  | 
 | ||
|  | import type { IWebrequestOptions } from './webrequest.types.js'; | ||
|  | import type { | ||
|  |   TRequestInterceptor, | ||
|  |   TResponseInterceptor, | ||
|  |   TErrorInterceptor, | ||
|  | } from './interceptors/interceptor.types.js'; | ||
|  | import { InterceptorManager } from './interceptors/interceptor.manager.js'; | ||
|  | import { CacheManager } from './cache/cache.manager.js'; | ||
|  | import { RetryManager } from './retry/retry.manager.js'; | ||
|  | import { RequestDeduplicator } from './utils/deduplicator.js'; | ||
|  | import { fetchWithTimeout } from './utils/timeout.js'; | ||
|  | 
 | ||
|  | export class WebrequestClient { | ||
|  |   private interceptorManager: InterceptorManager; | ||
|  |   private cacheManager: CacheManager; | ||
|  |   private deduplicator: RequestDeduplicator; | ||
|  |   private defaultOptions: Partial<IWebrequestOptions>; | ||
|  | 
 | ||
|  |   constructor(options: Partial<IWebrequestOptions> = {}) { | ||
|  |     this.defaultOptions = options; | ||
|  |     this.interceptorManager = new InterceptorManager(); | ||
|  |     this.cacheManager = new CacheManager(); | ||
|  |     this.deduplicator = new RequestDeduplicator(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Add a global request interceptor | ||
|  |    */ | ||
|  |   public addRequestInterceptor(interceptor: TRequestInterceptor): void { | ||
|  |     this.interceptorManager.addRequestInterceptor(interceptor); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Add a global response interceptor | ||
|  |    */ | ||
|  |   public addResponseInterceptor(interceptor: TResponseInterceptor): void { | ||
|  |     this.interceptorManager.addResponseInterceptor(interceptor); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Add a global error interceptor | ||
|  |    */ | ||
|  |   public addErrorInterceptor(interceptor: TErrorInterceptor): void { | ||
|  |     this.interceptorManager.addErrorInterceptor(interceptor); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove a request interceptor | ||
|  |    */ | ||
|  |   public removeRequestInterceptor(interceptor: TRequestInterceptor): void { | ||
|  |     this.interceptorManager.removeRequestInterceptor(interceptor); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove a response interceptor | ||
|  |    */ | ||
|  |   public removeResponseInterceptor(interceptor: TResponseInterceptor): void { | ||
|  |     this.interceptorManager.removeResponseInterceptor(interceptor); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove an error interceptor | ||
|  |    */ | ||
|  |   public removeErrorInterceptor(interceptor: TErrorInterceptor): void { | ||
|  |     this.interceptorManager.removeErrorInterceptor(interceptor); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Clear all interceptors | ||
|  |    */ | ||
|  |   public clearInterceptors(): void { | ||
|  |     this.interceptorManager.clearAll(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Clear the cache | ||
|  |    */ | ||
|  |   public async clearCache(): Promise<void> { | ||
|  |     await this.cacheManager.clear(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Execute a request with all configured features | ||
|  |    */ | ||
|  |   public async request( | ||
|  |     url: string | Request, | ||
|  |     options: IWebrequestOptions = {}, | ||
|  |   ): Promise<Response> { | ||
|  |     // Merge default options with request options
 | ||
|  |     const mergedOptions: IWebrequestOptions = { | ||
|  |       ...this.defaultOptions, | ||
|  |       ...options, | ||
|  |     }; | ||
|  | 
 | ||
|  |     // Create Request object
 | ||
|  |     let request: Request; | ||
|  |     if (typeof url === 'string') { | ||
|  |       request = new Request(url, mergedOptions); | ||
|  |     } else { | ||
|  |       request = url; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Process through request interceptors
 | ||
|  |     request = await this.interceptorManager.processRequest(request); | ||
|  | 
 | ||
|  |     // Add per-request interceptors if provided
 | ||
|  |     if (mergedOptions.interceptors?.request) { | ||
|  |       for (const interceptor of mergedOptions.interceptors.request) { | ||
|  |         request = await interceptor(request); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     // Execute with deduplication if enabled
 | ||
|  |     const deduplicate = mergedOptions.deduplicate ?? false; | ||
|  | 
 | ||
|  |     if (deduplicate) { | ||
|  |       const dedupeKey = this.deduplicator.generateKey(request); | ||
|  |       const result = await this.deduplicator.execute(dedupeKey, async () => { | ||
|  |         return await this.executeRequest(request, mergedOptions); | ||
|  |       }); | ||
|  |       return result.response; | ||
|  |     } | ||
|  | 
 | ||
|  |     return await this.executeRequest(request, mergedOptions); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Internal request execution with caching and retry | ||
|  |    */ | ||
|  |   private async executeRequest( | ||
|  |     request: Request, | ||
|  |     options: IWebrequestOptions, | ||
|  |   ): Promise<Response> { | ||
|  |     try { | ||
|  |       // Determine if retry is enabled
 | ||
|  |       const retryOptions = | ||
|  |         typeof options.retry === 'object' | ||
|  |           ? options.retry | ||
|  |           : options.retry | ||
|  |             ? {} | ||
|  |             : undefined; | ||
|  | 
 | ||
|  |       // Create fetch function for Request objects (used with caching)
 | ||
|  |       const fetchFnForRequest = async (req: Request): Promise<Response> => { | ||
|  |         const timeout = options.timeout ?? 60000; | ||
|  |         return await fetchWithTimeout( | ||
|  |           req.url, | ||
|  |           { | ||
|  |             method: req.method, | ||
|  |             headers: req.headers, | ||
|  |             body: req.body, | ||
|  |             ...options, | ||
|  |           }, | ||
|  |           timeout, | ||
|  |         ); | ||
|  |       }; | ||
|  | 
 | ||
|  |       // Create fetch function for fallbacks (url + init)
 | ||
|  |       const fetchFnForFallbacks = async (url: string, init: RequestInit): Promise<Response> => { | ||
|  |         const timeout = options.timeout ?? 60000; | ||
|  |         return await fetchWithTimeout(url, init, timeout); | ||
|  |       }; | ||
|  | 
 | ||
|  |       let response: Response; | ||
|  | 
 | ||
|  |       // Execute with retry if enabled
 | ||
|  |       if (retryOptions) { | ||
|  |         const retryManager = new RetryManager(retryOptions); | ||
|  | 
 | ||
|  |         // Handle fallback URLs if provided
 | ||
|  |         if (options.fallbackUrls && options.fallbackUrls.length > 0) { | ||
|  |           const allUrls = [request.url, ...options.fallbackUrls]; | ||
|  |           response = await retryManager.executeWithFallbacks( | ||
|  |             allUrls, | ||
|  |             { | ||
|  |               method: request.method, | ||
|  |               headers: request.headers, | ||
|  |               body: request.body, | ||
|  |               ...options, | ||
|  |             }, | ||
|  |             fetchFnForFallbacks, | ||
|  |           ); | ||
|  |         } else { | ||
|  |           response = await retryManager.execute(async () => { | ||
|  |             // Execute with caching
 | ||
|  |             const result = await this.cacheManager.execute( | ||
|  |               request, | ||
|  |               options, | ||
|  |               fetchFnForRequest, | ||
|  |             ); | ||
|  |             return result.response; | ||
|  |           }); | ||
|  |         } | ||
|  |       } else { | ||
|  |         // Execute with caching (no retry)
 | ||
|  |         const result = await this.cacheManager.execute( | ||
|  |           request, | ||
|  |           options, | ||
|  |           fetchFnForRequest, | ||
|  |         ); | ||
|  |         response = result.response; | ||
|  |       } | ||
|  | 
 | ||
|  |       // Process through response interceptors
 | ||
|  |       response = await this.interceptorManager.processResponse(response); | ||
|  | 
 | ||
|  |       // Add per-request response interceptors if provided
 | ||
|  |       if (options.interceptors?.response) { | ||
|  |         for (const interceptor of options.interceptors.response) { | ||
|  |           response = await interceptor(response); | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       return response; | ||
|  |     } catch (error) { | ||
|  |       // Process through error interceptors
 | ||
|  |       const processedError = await this.interceptorManager.processError( | ||
|  |         error instanceof Error ? error : new Error(String(error)), | ||
|  |       ); | ||
|  | 
 | ||
|  |       throw processedError; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Convenience method: GET request returning JSON | ||
|  |    */ | ||
|  |   public async getJson<T = any>( | ||
|  |     url: string, | ||
|  |     options: IWebrequestOptions = {}, | ||
|  |   ): Promise<T> { | ||
|  |     const response = await this.request(url, { | ||
|  |       ...options, | ||
|  |       method: 'GET', | ||
|  |       headers: { | ||
|  |         Accept: 'application/json', | ||
|  |         ...((options.headers as any) || {}), | ||
|  |       }, | ||
|  |     }); | ||
|  | 
 | ||
|  |     if (!response.ok) { | ||
|  |       throw new Error(`HTTP ${response.status}: ${response.statusText}`); | ||
|  |     } | ||
|  | 
 | ||
|  |     return await response.json(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Convenience method: POST request with JSON body | ||
|  |    */ | ||
|  |   public async postJson<T = any>( | ||
|  |     url: string, | ||
|  |     data: any, | ||
|  |     options: IWebrequestOptions = {}, | ||
|  |   ): Promise<T> { | ||
|  |     const response = await this.request(url, { | ||
|  |       ...options, | ||
|  |       method: 'POST', | ||
|  |       headers: { | ||
|  |         'Content-Type': 'application/json', | ||
|  |         Accept: 'application/json', | ||
|  |         ...((options.headers as any) || {}), | ||
|  |       }, | ||
|  |       body: JSON.stringify(data), | ||
|  |     }); | ||
|  | 
 | ||
|  |     if (!response.ok) { | ||
|  |       throw new Error(`HTTP ${response.status}: ${response.statusText}`); | ||
|  |     } | ||
|  | 
 | ||
|  |     return await response.json(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Convenience method: PUT request with JSON body | ||
|  |    */ | ||
|  |   public async putJson<T = any>( | ||
|  |     url: string, | ||
|  |     data: any, | ||
|  |     options: IWebrequestOptions = {}, | ||
|  |   ): Promise<T> { | ||
|  |     const response = await this.request(url, { | ||
|  |       ...options, | ||
|  |       method: 'PUT', | ||
|  |       headers: { | ||
|  |         'Content-Type': 'application/json', | ||
|  |         Accept: 'application/json', | ||
|  |         ...((options.headers as any) || {}), | ||
|  |       }, | ||
|  |       body: JSON.stringify(data), | ||
|  |     }); | ||
|  | 
 | ||
|  |     if (!response.ok) { | ||
|  |       throw new Error(`HTTP ${response.status}: ${response.statusText}`); | ||
|  |     } | ||
|  | 
 | ||
|  |     return await response.json(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Convenience method: DELETE request | ||
|  |    */ | ||
|  |   public async deleteJson<T = any>( | ||
|  |     url: string, | ||
|  |     options: IWebrequestOptions = {}, | ||
|  |   ): Promise<T> { | ||
|  |     const response = await this.request(url, { | ||
|  |       ...options, | ||
|  |       method: 'DELETE', | ||
|  |       headers: { | ||
|  |         Accept: 'application/json', | ||
|  |         ...((options.headers as any) || {}), | ||
|  |       }, | ||
|  |     }); | ||
|  | 
 | ||
|  |     if (!response.ok) { | ||
|  |       throw new Error(`HTTP ${response.status}: ${response.statusText}`); | ||
|  |     } | ||
|  | 
 | ||
|  |     return await response.json(); | ||
|  |   } | ||
|  | } |