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