- 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.
106 lines
2.4 KiB
TypeScript
106 lines
2.4 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|
|
}
|