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:
105
ts/utils/deduplicator.ts
Normal file
105
ts/utils/deduplicator.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Request deduplication system
|
||||
* Prevents multiple simultaneous identical requests
|
||||
*/
|
||||
|
||||
import * as plugins from '../webrequest.plugins.js';
|
||||
|
||||
export class RequestDeduplicator {
|
||||
private inFlightRequests: Map<
|
||||
string,
|
||||
plugins.smartpromise.Deferred<Response>
|
||||
> = new Map();
|
||||
|
||||
/**
|
||||
* Generate a deduplication key from a request
|
||||
*/
|
||||
public generateKey(request: Request): string {
|
||||
// Use URL + method as the base key
|
||||
const url = request.url;
|
||||
const method = request.method;
|
||||
|
||||
// For GET/HEAD requests, just use URL + method
|
||||
if (method === 'GET' || method === 'HEAD') {
|
||||
return `${method}:${url}`;
|
||||
}
|
||||
|
||||
// For other methods, we can't deduplicate as easily
|
||||
// (body might be different)
|
||||
// Use a timestamp to make it unique
|
||||
return `${method}:${url}:${Date.now()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a request with deduplication
|
||||
*/
|
||||
public async execute(
|
||||
key: string,
|
||||
executeFn: () => Promise<Response>,
|
||||
): Promise<{ response: Response; wasDeduplicated: boolean }> {
|
||||
// Check if request is already in flight
|
||||
const existingDeferred = this.inFlightRequests.get(key);
|
||||
|
||||
if (existingDeferred) {
|
||||
// Wait for the existing request to complete
|
||||
const response = await existingDeferred.promise;
|
||||
|
||||
// Clone the response so it can be used multiple times
|
||||
return {
|
||||
response: response.clone(),
|
||||
wasDeduplicated: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a new deferred for this request
|
||||
const deferred = plugins.smartpromise.defer<Response>();
|
||||
this.inFlightRequests.set(key, deferred);
|
||||
|
||||
try {
|
||||
// Execute the request
|
||||
const response = await executeFn();
|
||||
|
||||
// Resolve the deferred
|
||||
deferred.resolve(response);
|
||||
|
||||
// Clean up
|
||||
this.inFlightRequests.delete(key);
|
||||
|
||||
// Return the original response
|
||||
return {
|
||||
response,
|
||||
wasDeduplicated: false,
|
||||
};
|
||||
} catch (error) {
|
||||
// Reject the deferred
|
||||
deferred.reject(error);
|
||||
|
||||
// Clean up
|
||||
this.inFlightRequests.delete(key);
|
||||
|
||||
// Re-throw the error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a request is currently in flight
|
||||
*/
|
||||
public isInFlight(key: string): boolean {
|
||||
return this.inFlightRequests.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of in-flight requests
|
||||
*/
|
||||
public getInFlightCount(): number {
|
||||
return this.inFlightRequests.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all in-flight requests
|
||||
*/
|
||||
public clear(): void {
|
||||
this.inFlightRequests.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user