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; 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(config: RequestConfig): Promise> { 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; } 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(url: string, config?: Partial): Promise> { return this.request({ ...config, url, method: 'GET' }); } async post(url: string, data?: any, config?: Partial): Promise> { return this.request({ ...config, url, method: 'POST', body: data }); } async put(url: string, data?: any, config?: Partial): Promise> { return this.request({ ...config, url, method: 'PUT', body: data }); } async patch(url: string, data?: any, config?: Partial): Promise> { return this.request({ ...config, url, method: 'PATCH', body: data }); } async delete(url: string, config?: Partial): Promise> { return this.request({ ...config, url, method: 'DELETE' }); } async head(url: string, config?: Partial): Promise> { return this.request({ ...config, url, method: 'HEAD' }); } async options(url: string, config?: Partial): Promise> { return this.request({ ...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): 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 { 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): void { this.on('beforeRequest', handler); } onResponse(handler: (response: HttpResponse) => HttpResponse | Promise): void { this.on('afterResponse', handler); } onError(handler: (error: any) => void): void { this.on('requestError', handler); } }