feat: Implement comprehensive web request handling with caching, retry, and interceptors
- Added cache strategies: NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly, and CacheOnly. - Introduced InterceptorManager for managing request, response, and error interceptors. - Developed RetryManager for handling request retries with customizable backoff strategies. - Implemented RequestDeduplicator to prevent simultaneous identical requests. - Created timeout utilities for handling request timeouts. - Enhanced WebrequestClient to support global interceptors, caching, and retry logic. - Added convenience methods for common HTTP methods (GET, POST, PUT, DELETE) with JSON handling. - Established a fetch-compatible webrequest function for seamless integration. - Defined core type structures for caching, retry options, interceptors, and web request configurations.
This commit is contained in:
		
							
								
								
									
										326
									
								
								ts/webrequest.client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								ts/webrequest.client.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,326 @@ | ||||
| /** | ||||
|  * 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(); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user