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