/** * 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; constructor(options: Partial = {}) { 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 { await this.cacheManager.clear(); } /** * Execute a request with all configured features */ public async request( url: string | Request, options: IWebrequestOptions = {}, ): Promise { // 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 { 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 => { 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 => { 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( url: string, options: IWebrequestOptions = {}, ): Promise { 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( url: string, data: any, options: IWebrequestOptions = {}, ): Promise { 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( url: string, data: any, options: IWebrequestOptions = {}, ): Promise { 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( url: string, options: IWebrequestOptions = {}, ): Promise { 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(); } }