- 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.
		
			
				
	
	
		
			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();
 | |
|   }
 | |
| }
 |