199 lines
6.2 KiB
TypeScript
199 lines
6.2 KiB
TypeScript
import { EventEmitter } from 'eventemitter3';
|
|
import type {
|
|
HttpClientConfig,
|
|
RequestConfig,
|
|
HttpResponse,
|
|
ConnectionStats,
|
|
HttpClientError,
|
|
TimeoutError
|
|
} from './types';
|
|
import { ConnectionPool } from './ConnectionPool';
|
|
import { RetryHandler } from './RetryHandler';
|
|
|
|
export class BunHttpClient extends EventEmitter {
|
|
private connectionPool: ConnectionPool;
|
|
private retryHandler: RetryHandler;
|
|
private defaultConfig: Required<HttpClientConfig>;
|
|
|
|
constructor(config: HttpClientConfig = {}) {
|
|
super();
|
|
|
|
this.defaultConfig = {
|
|
baseURL: '',
|
|
timeout: 30000,
|
|
headers: {},
|
|
retries: 3,
|
|
retryDelay: 1000,
|
|
maxConcurrency: 10,
|
|
keepAlive: true,
|
|
validateStatus: (status: number) => status < 400,
|
|
...config
|
|
};
|
|
|
|
this.connectionPool = new ConnectionPool({
|
|
maxConnections: this.defaultConfig.maxConcurrency,
|
|
maxConnectionsPerHost: Math.ceil(this.defaultConfig.maxConcurrency / 4),
|
|
keepAlive: this.defaultConfig.keepAlive,
|
|
maxIdleTime: 60000,
|
|
connectionTimeout: this.defaultConfig.timeout
|
|
});
|
|
|
|
this.retryHandler = new RetryHandler({
|
|
maxRetries: this.defaultConfig.retries,
|
|
baseDelay: this.defaultConfig.retryDelay,
|
|
maxDelay: 30000,
|
|
exponentialBackoff: true
|
|
});
|
|
|
|
// Forward events from connection pool and retry handler
|
|
this.connectionPool.on('response', (data) => this.emit('response', data));
|
|
this.connectionPool.on('error', (data) => this.emit('error', data));
|
|
this.retryHandler.on('retryAttempt', (data) => this.emit('retryAttempt', data));
|
|
this.retryHandler.on('retrySuccess', (data) => this.emit('retrySuccess', data));
|
|
this.retryHandler.on('retryExhausted', (data) => this.emit('retryExhausted', data));
|
|
}
|
|
|
|
async request<T = any>(config: RequestConfig): Promise<HttpResponse<T>> {
|
|
const fullConfig = this.mergeConfig(config);
|
|
|
|
return this.retryHandler.execute(async () => {
|
|
const startTime = performance.now();
|
|
|
|
try {
|
|
// Add timing metadata
|
|
fullConfig.metadata = {
|
|
...fullConfig.metadata,
|
|
startTime
|
|
};
|
|
|
|
const response = await this.connectionPool.request(fullConfig);
|
|
return response as HttpResponse<T>;
|
|
|
|
} catch (error: any) {
|
|
// Convert fetch errors to our error types
|
|
if (error.name === 'AbortError') {
|
|
throw new TimeoutError(fullConfig, fullConfig.timeout || this.defaultConfig.timeout);
|
|
}
|
|
|
|
// Re-throw as HttpClientError if not already
|
|
if (!(error instanceof HttpClientError)) {
|
|
const httpError = new HttpClientError(
|
|
error.message || 'Request failed',
|
|
error.code,
|
|
error.status,
|
|
error.response,
|
|
fullConfig
|
|
);
|
|
throw httpError;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}, fullConfig);
|
|
}
|
|
|
|
// Convenience methods
|
|
async get<T = any>(url: string, config?: Partial<RequestConfig>): Promise<HttpResponse<T>> {
|
|
return this.request<T>({ ...config, url, method: 'GET' });
|
|
}
|
|
|
|
async post<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<HttpResponse<T>> {
|
|
return this.request<T>({ ...config, url, method: 'POST', body: data });
|
|
}
|
|
|
|
async put<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<HttpResponse<T>> {
|
|
return this.request<T>({ ...config, url, method: 'PUT', body: data });
|
|
}
|
|
|
|
async patch<T = any>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<HttpResponse<T>> {
|
|
return this.request<T>({ ...config, url, method: 'PATCH', body: data });
|
|
}
|
|
|
|
async delete<T = any>(url: string, config?: Partial<RequestConfig>): Promise<HttpResponse<T>> {
|
|
return this.request<T>({ ...config, url, method: 'DELETE' });
|
|
}
|
|
|
|
async head<T = any>(url: string, config?: Partial<RequestConfig>): Promise<HttpResponse<T>> {
|
|
return this.request<T>({ ...config, url, method: 'HEAD' });
|
|
}
|
|
|
|
async options<T = any>(url: string, config?: Partial<RequestConfig>): Promise<HttpResponse<T>> {
|
|
return this.request<T>({ ...config, url, method: 'OPTIONS' });
|
|
}
|
|
|
|
private mergeConfig(config: RequestConfig): RequestConfig {
|
|
return {
|
|
timeout: this.defaultConfig.timeout,
|
|
retries: this.defaultConfig.retries,
|
|
headers: { ...this.defaultConfig.headers, ...config.headers },
|
|
validateStatus: this.defaultConfig.validateStatus,
|
|
url: this.buildUrl(config.url),
|
|
...config
|
|
};
|
|
}
|
|
|
|
private buildUrl(url: string): string {
|
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
return url;
|
|
}
|
|
|
|
if (this.defaultConfig.baseURL) {
|
|
const baseURL = this.defaultConfig.baseURL.replace(/\/$/, '');
|
|
const path = url.replace(/^\//, '');
|
|
return `${baseURL}/${path}`;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
// Configuration methods
|
|
setBaseURL(baseURL: string): void {
|
|
this.defaultConfig.baseURL = baseURL;
|
|
}
|
|
|
|
setDefaultHeaders(headers: Record<string, string>): void {
|
|
this.defaultConfig.headers = { ...this.defaultConfig.headers, ...headers };
|
|
}
|
|
|
|
setTimeout(timeout: number): void {
|
|
this.defaultConfig.timeout = timeout;
|
|
}
|
|
|
|
setMaxConcurrency(maxConcurrency: number): void {
|
|
this.defaultConfig.maxConcurrency = maxConcurrency;
|
|
}
|
|
|
|
// Statistics and monitoring
|
|
getStats(): ConnectionStats {
|
|
return this.connectionPool.getStats();
|
|
}
|
|
|
|
async healthCheck(): Promise<{ healthy: boolean; details: any }> {
|
|
return this.connectionPool.healthCheck();
|
|
}
|
|
|
|
// Lifecycle management
|
|
async close(): Promise<void> {
|
|
await this.connectionPool.close();
|
|
this.removeAllListeners();
|
|
}
|
|
|
|
// Create a new instance with different configuration
|
|
create(config: HttpClientConfig): BunHttpClient {
|
|
const mergedConfig = { ...this.defaultConfig, ...config };
|
|
return new BunHttpClient(mergedConfig);
|
|
}
|
|
|
|
// Interceptor-like functionality through events
|
|
onRequest(handler: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>): void {
|
|
this.on('beforeRequest', handler);
|
|
}
|
|
|
|
onResponse(handler: (response: HttpResponse) => HttpResponse | Promise<HttpResponse>): void {
|
|
this.on('afterResponse', handler);
|
|
}
|
|
|
|
onError(handler: (error: any) => void): void {
|
|
this.on('requestError', handler);
|
|
}
|
|
}
|