removed doodoo projects, getting ready for restructuring

This commit is contained in:
Bojan Kucera 2025-06-04 14:48:03 -04:00
parent 674112af05
commit 5c64b1ccf8
82 changed files with 5 additions and 8917 deletions

View file

@ -1,187 +0,0 @@
# @stock-bot/http-client
High-performance HTTP client for Stock Bot microservices built on Bun's native fetch API.
## Features
- **Ultra-fast performance** - Built on Bun's native fetch implementation
- **Connection pooling** - Efficiently manages connections to prevent overwhelming servers
- **Automatic retries** - Handles transient network errors with configurable retry strategies
- **Timeout management** - Prevents requests from hanging indefinitely
- **Streaming support** - Efficient handling of large responses
- **TypeScript support** - Full type safety for all operations
- **Metrics & monitoring** - Built-in performance statistics
## Installation
```bash
bun add @stock-bot/http-client
```
## Basic Usage
```typescript
import { BunHttpClient } from '@stock-bot/http-client';
// Create a client
const client = new BunHttpClient({
baseURL: 'https://api.example.com',
timeout: 5000,
retries: 3
});
// Make requests
async function fetchData() {
try {
// GET request
const response = await client.get('/users');
console.log(response.data);
// POST request with data
const createResponse = await client.post('/users', {
name: 'John Doe',
email: 'john@example.com'
});
console.log(createResponse.data);
} catch (error) {
console.error('Request failed:', error.message);
}
}
// Close when done
await client.close();
```
## Advanced Configuration
```typescript
const client = new BunHttpClient({
baseURL: 'https://api.example.com',
timeout: 10000,
retries: 3,
retryDelay: 1000,
maxConcurrency: 20,
keepAlive: true,
headers: {
'User-Agent': 'StockBot/1.0',
'Authorization': 'Bearer token'
},
validateStatus: (status) => status >= 200 && status < 300
});
```
## Connection Pooling
The HTTP client automatically manages connection pooling with smart limits:
```typescript
// Get connection statistics
const stats = client.getStats();
console.log(`Active connections: ${stats.activeConnections}`);
console.log(`Success rate: ${stats.successfulRequests / (stats.successfulRequests + stats.failedRequests)}`);
console.log(`Average response time: ${stats.averageResponseTime}ms`);
// Health check
const health = await client.healthCheck();
if (health.healthy) {
console.log('HTTP client is healthy');
} else {
console.log('HTTP client is degraded:', health.details);
}
```
## Event Handling
```typescript
// Listen for specific events
client.on('response', ({ host, response }) => {
console.log(`Response from ${host}: ${response.status}`);
});
client.on('error', ({ host, error }) => {
console.log(`Error from ${host}: ${error.message}`);
});
client.on('retryAttempt', (data) => {
console.log(`Retrying request (${data.attempt}/${data.config.retries}): ${data.error.message}`);
});
```
## API Reference
### BunHttpClient
Main HTTP client class with connection pooling and retry support.
#### Methods
- `request(config)`: Make a request with full configuration options
- `get(url, config?)`: Make a GET request
- `post(url, data?, config?)`: Make a POST request with data
- `put(url, data?, config?)`: Make a PUT request with data
- `patch(url, data?, config?)`: Make a PATCH request with data
- `delete(url, config?)`: Make a DELETE request
- `head(url, config?)`: Make a HEAD request
- `options(url, config?)`: Make an OPTIONS request
- `getStats()`: Get connection statistics
- `healthCheck()`: Check health of the client
- `close()`: Close all connections
- `setBaseURL(url)`: Update the base URL
- `setDefaultHeaders(headers)`: Update default headers
- `setTimeout(timeout)`: Update default timeout
- `create(config)`: Create a new instance with different config
### Request Configuration
```typescript
interface RequestConfig {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
headers?: Record<string, string>;
body?: any;
timeout?: number;
retries?: number;
validateStatus?: (status: number) => boolean;
metadata?: Record<string, any>;
}
```
### Response Object
```typescript
interface HttpResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: Record<string, string>;
config: RequestConfig;
timing: {
start: number;
end: number;
duration: number;
};
}
```
## Error Handling
```typescript
try {
const response = await client.get('/resource-that-might-fail');
processData(response.data);
} catch (error) {
if (error instanceof TimeoutError) {
console.log('Request timed out');
} else if (error instanceof RetryExhaustedError) {
console.log(`Request failed after ${error.config.retries} retries`);
} else if (error instanceof HttpClientError) {
console.log(`HTTP error: ${error.status} - ${error.message}`);
} else {
console.log('Unexpected error', error);
}
}
```
## License
MIT

View file

@ -1,11 +0,0 @@
# HTTP Client Library Bun Test Configuration
[test]
# Test configuration
timeout = 5000
# Enable TypeScript paths resolution
[bun]
paths = {
"@/*" = ["./src/*"]
}

View file

@ -1,91 +0,0 @@
// Example usage of the @stock-bot/http-client library
import { BunHttpClient } from '../src';
async function main() {
// Create a client instance
const client = new BunHttpClient({
baseURL: 'https://api.polygon.io',
timeout: 10000,
retries: 2,
retryDelay: 500,
headers: {
'X-API-Key': process.env.POLYGON_API_KEY || 'demo'
}
});
// Add event listeners for monitoring
client.on('response', ({host, response}) => {
console.log(`📦 Response from ${host}: ${response.status} (${response.timing.duration.toFixed(2)}ms)`);
});
client.on('error', ({host, error}) => {
console.error(`❌ Error from ${host}: ${error.message}`);
});
client.on('retryAttempt', ({attempt, config, delay}) => {
console.warn(`⚠️ Retry ${attempt}/${config.retries} for ${config.url} in ${delay}ms`);
});
try {
console.log('Fetching market data...');
// Make a GET request
const tickerResponse = await client.get('/v3/reference/tickers', {
headers: {
'Accept': 'application/json'
}
});
console.log(`Found ${tickerResponse.data.results.length} tickers`);
console.log(`First ticker: ${JSON.stringify(tickerResponse.data.results[0], null, 2)}`);
// Make a request that will fail
try {
await client.get('/non-existent-endpoint');
} catch (error) {
console.log('Expected error caught:', error.message);
}
// Multiple parallel requests
console.log('Making parallel requests...');
const [aaplData, msftData, amznData] = await Promise.all([
client.get('/v2/aggs/ticker/AAPL/range/1/day/2023-01-01/2023-01-15'),
client.get('/v2/aggs/ticker/MSFT/range/1/day/2023-01-01/2023-01-15'),
client.get('/v2/aggs/ticker/AMZN/range/1/day/2023-01-01/2023-01-15')
]);
console.log('Parallel requests completed:');
console.log(`- AAPL: ${aaplData.status}, data points: ${aaplData.data.results?.length || 0}`);
console.log(`- MSFT: ${msftData.status}, data points: ${msftData.data.results?.length || 0}`);
console.log(`- AMZN: ${amznData.status}, data points: ${amznData.data.results?.length || 0}`);
// Get client statistics
const stats = client.getStats();
console.log('\nClient Stats:');
console.log(`- Active connections: ${stats.activeConnections}`);
console.log(`- Total connections: ${stats.totalConnections}`);
console.log(`- Successful requests: ${stats.successfulRequests}`);
console.log(`- Failed requests: ${stats.failedRequests}`);
console.log(`- Average response time: ${stats.averageResponseTime.toFixed(2)}ms`);
console.log(`- Requests per second: ${stats.requestsPerSecond.toFixed(2)}`);
// Health check
const health = await client.healthCheck();
console.log(`\nClient health: ${health.healthy ? 'HEALTHY' : 'DEGRADED'}`);
console.log(`Health details: ${JSON.stringify(health.details, null, 2)}`);
} catch (error) {
console.error('Error in example:', error);
} finally {
// Always close the client when done to clean up resources
await client.close();
console.log('HTTP client closed');
}
}
// Run the example
main().catch(err => {
console.error('Example failed:', err);
process.exit(1);
});

View file

@ -1,34 +0,0 @@
{
"name": "@stock-bot/http-client",
"version": "1.0.0",
"description": "High-performance HTTP client for Stock Bot using Bun's native fetch",
"main": "src/index.ts",
"type": "module",
"scripts": {
"build": "tsc",
"test": "bun test",
"lint": "eslint src/**/*.ts",
"type-check": "tsc --noEmit"
},
"dependencies": {
"eventemitter3": "^5.0.1"
},
"devDependencies": {
"@types/node": "^20.11.0",
"typescript": "^5.3.0",
"eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"bun-types": "^1.2.15"
},
"keywords": [
"http-client",
"fetch",
"bun",
"performance",
"connection-pooling"
],
"exports": {
".": "./src/index.ts"
}
}

View file

@ -1,182 +0,0 @@
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
import { BunHttpClient, HttpClientError, TimeoutError } from "../src";
// Mock GlobalFetch to avoid making real network requests
const mockFetchSuccess = mock(() =>
Promise.resolve(new Response(
JSON.stringify({ result: "success" }),
{ status: 200, headers: { "content-type": "application/json" } }
))
);
const mockFetchFailure = mock(() =>
Promise.resolve(new Response(
JSON.stringify({ error: "Not found" }),
{ status: 404, headers: { "content-type": "application/json" } }
))
);
const mockFetchTimeout = mock(() => {
return new Promise((_, reject) => {
setTimeout(() => {
const error = new Error("Timeout");
error.name = "AbortError";
reject(error);
}, 10);
});
});
describe("BunHttpClient", () => {
let client: BunHttpClient;
const originalFetch = global.fetch;
beforeEach(() => {
// Create a fresh client for each test
client = new BunHttpClient({
baseURL: "https://api.example.com",
timeout: 1000,
retries: 1
});
});
afterEach(async () => {
// Cleanup after each test
await client.close();
global.fetch = originalFetch;
});
test("should make successful GET requests", async () => {
global.fetch = mockFetchSuccess;
const response = await client.get("/users");
expect(response.status).toBe(200);
expect(response.data).toEqual({ result: "success" });
expect(mockFetchSuccess).toHaveBeenCalledTimes(1);
});
test("should handle failed requests", async () => {
global.fetch = mockFetchFailure;
try {
await client.get("/missing");
expect("Should have thrown").toBe("But didn't");
} catch (error) {
expect(error).toBeInstanceOf(HttpClientError);
expect(error.status).toBe(404);
}
expect(mockFetchFailure).toHaveBeenCalledTimes(1);
});
test("should handle request timeouts", async () => {
global.fetch = mockFetchTimeout;
try {
await client.get("/slow");
expect("Should have thrown").toBe("But didn't");
} catch (error) {
expect(error).toBeInstanceOf(TimeoutError);
}
});
test("should build full URLs properly", async () => {
global.fetch = mockFetchSuccess;
await client.get("/users/123");
expect(mockFetchSuccess).toHaveBeenCalledWith(
"https://api.example.com/users/123",
expect.objectContaining({
method: "GET"
})
);
});
test("should make POST requests with body", async () => {
global.fetch = mockFetchSuccess;
const data = { name: "John", email: "john@example.com" };
await client.post("/users", data);
expect(mockFetchSuccess).toHaveBeenCalledWith(
"https://api.example.com/users",
expect.objectContaining({
method: "POST",
body: JSON.stringify(data)
})
);
});
test("should provide convenience methods for all HTTP verbs", async () => {
global.fetch = mockFetchSuccess;
await client.get("/users");
await client.post("/users", { name: "Test" });
await client.put("/users/1", { name: "Updated" });
await client.patch("/users/1", { status: "active" });
await client.delete("/users/1");
await client.head("/users");
await client.options("/users");
expect(mockFetchSuccess).toHaveBeenCalledTimes(7);
});
test("should merge config options correctly", async () => {
global.fetch = mockFetchSuccess;
await client.get("/users", {
headers: { "X-Custom": "Value" },
timeout: 5000
});
expect(mockFetchSuccess).toHaveBeenCalledWith(
"https://api.example.com/users",
expect.objectContaining({
headers: expect.objectContaining({
"X-Custom": "Value"
})
})
);
});
test("should handle absolute URLs", async () => {
global.fetch = mockFetchSuccess;
await client.get("https://other-api.com/endpoint");
expect(mockFetchSuccess).toHaveBeenCalledWith(
"https://other-api.com/endpoint",
expect.anything()
);
});
test("should update configuration", async () => {
global.fetch = mockFetchSuccess;
client.setBaseURL("https://new-api.com");
client.setDefaultHeaders({ "Authorization": "Bearer token" });
client.setTimeout(2000);
await client.get("/resource");
expect(mockFetchSuccess).toHaveBeenCalledWith(
"https://new-api.com/resource",
expect.objectContaining({
headers: expect.objectContaining({
"Authorization": "Bearer token"
})
})
);
});
test("should get connection stats", async () => {
global.fetch = mockFetchSuccess;
await client.get("/users");
const stats = client.getStats();
expect(stats).toHaveProperty("successfulRequests", 1);
expect(stats).toHaveProperty("activeConnections");
expect(stats).toHaveProperty("averageResponseTime");
});
});

View file

@ -1,199 +0,0 @@
import { EventEmitter } from 'eventemitter3';
import {
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 {
...config,
timeout: this.defaultConfig.timeout,
retries: this.defaultConfig.retries,
headers: { ...this.defaultConfig.headers, ...config.headers },
validateStatus: this.defaultConfig.validateStatus,
url: this.buildUrl(config.url),
};
}
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);
}
}

View file

@ -1,331 +0,0 @@
import { EventEmitter } from 'eventemitter3';
import type {
ConnectionPoolConfig,
ConnectionStats,
QueuedRequest,
RequestConfig
} from './types';
export class ConnectionPool extends EventEmitter {
private activeConnections = new Map<string, number>();
private requestQueue: QueuedRequest[] = [];
private stats = {
totalConnections: 0,
activeRequests: 0,
successfulRequests: 0,
failedRequests: 0,
totalResponseTime: 0,
requestCount: 0,
startTime: Date.now()
};
private isProcessingQueue = false;
private queueProcessor?: NodeJS.Timeout;
constructor(private config: ConnectionPoolConfig) {
super();
this.startQueueProcessor();
}
async request(requestConfig: RequestConfig): Promise<any> {
return new Promise((resolve, reject) => {
const host = this.extractHost(requestConfig.url);
const queuedRequest: QueuedRequest = {
id: this.generateRequestId(),
config: requestConfig,
resolve,
reject,
timestamp: Date.now(),
retryCount: 0,
host
};
this.requestQueue.push(queuedRequest);
this.processQueue();
});
}
private async processQueue(): Promise<void> {
if (this.isProcessingQueue || this.requestQueue.length === 0) {
return;
}
this.isProcessingQueue = true;
while (this.requestQueue.length > 0) {
const request = this.requestQueue.shift()!;
try {
const currentConnections = this.activeConnections.get(request.host) || 0;
// Check per-host connection limits
if (currentConnections >= this.config.maxConnectionsPerHost) {
this.requestQueue.unshift(request);
break;
}
// Check global connection limit
const totalActive = Array.from(this.activeConnections.values())
.reduce((sum, count) => sum + count, 0);
if (totalActive >= this.config.maxConnections) {
this.requestQueue.unshift(request);
break;
}
// Execute the request
this.executeRequest(request);
} catch (error) {
request.reject(error);
}
}
this.isProcessingQueue = false;
}
private async executeRequest(request: QueuedRequest): Promise<void> {
const { host, config } = request;
// Increment active connections
this.activeConnections.set(host, (this.activeConnections.get(host) || 0) + 1);
this.stats.activeRequests++;
const startTime = performance.now();
try {
// Build the full URL
const url = this.buildUrl(config.url, config);
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = config.timeout ? setTimeout(() => {
controller.abort();
}, config.timeout) : undefined;
// Make the fetch request
const response = await fetch(url, {
method: config.method || 'GET',
headers: this.buildHeaders(config.headers),
body: this.buildBody(config.body),
signal: controller.signal,
// Bun-specific optimizations
keepalive: this.config.keepAlive,
});
if (timeoutId) {
clearTimeout(timeoutId);
}
// Check if response is considered successful
const isSuccess = config.validateStatus
? config.validateStatus(response.status)
: response.status < 400;
if (!isSuccess) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Parse response data
const data = await this.parseResponse(response);
const endTime = performance.now();
const duration = endTime - startTime;
// Update stats
this.updateStats(true, duration);
// Build response object
const httpResponse = {
data,
status: response.status,
statusText: response.statusText,
headers: this.parseHeaders(response.headers),
config,
timing: {
start: startTime,
end: endTime,
duration
}
};
this.emit('response', { host, response: httpResponse });
request.resolve(httpResponse);
} catch (error: any) {
const endTime = performance.now();
const duration = endTime - startTime;
this.updateStats(false, duration);
this.emit('error', { host, error, config });
request.reject(error);
} finally {
// Decrement active connections
this.activeConnections.set(host, Math.max(0, (this.activeConnections.get(host) || 0) - 1));
this.stats.activeRequests = Math.max(0, this.stats.activeRequests - 1);
}
}
private buildUrl(url: string, config: RequestConfig): string {
// If URL is already absolute, return as-is
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// If no base URL in config, assume it's a relative URL that needs a protocol
if (!url.startsWith('/')) {
url = '/' + url;
}
return url;
}
private buildHeaders(headers?: Record<string, string>): HeadersInit {
return {
'User-Agent': 'StockBot-HttpClient/1.0',
'Accept': 'application/json',
'Content-Type': 'application/json',
...headers
};
}
private buildBody(body: any): BodyInit | undefined {
if (!body) return undefined;
if (typeof body === 'string') return body;
if (body instanceof FormData || body instanceof Blob) return body;
if (body instanceof ArrayBuffer || body instanceof Uint8Array) return body;
return JSON.stringify(body);
}
private async parseResponse(response: Response): Promise<any> {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return await response.json();
}
if (contentType.includes('text/')) {
return await response.text();
}
return await response.arrayBuffer();
}
private parseHeaders(headers: Headers): Record<string, string> {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
private extractHost(url: string): string {
try {
if (url.startsWith('http://') || url.startsWith('https://')) {
const urlObj = new URL(url);
return urlObj.host;
}
return 'default';
} catch {
return 'default';
}
}
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private updateStats(success: boolean, responseTime: number): void {
this.stats.requestCount++;
this.stats.totalResponseTime += responseTime;
if (success) {
this.stats.successfulRequests++;
} else {
this.stats.failedRequests++;
}
}
private startQueueProcessor(): void {
this.queueProcessor = setInterval(() => {
if (this.requestQueue.length > 0) {
this.processQueue();
}
}, 10); // Process queue every 10ms for better responsiveness
}
getStats(): ConnectionStats {
const totalActive = Array.from(this.activeConnections.values())
.reduce((sum, count) => sum + count, 0);
const averageResponseTime = this.stats.requestCount > 0
? this.stats.totalResponseTime / this.stats.requestCount
: 0;
const utilization = this.config.maxConnections > 0
? totalActive / this.config.maxConnections
: 0;
const elapsedTimeSeconds = (Date.now() - this.stats.startTime) / 1000;
const requestsPerSecond = elapsedTimeSeconds > 0
? this.stats.requestCount / elapsedTimeSeconds
: 0;
return {
activeConnections: totalActive,
totalConnections: this.stats.totalConnections,
successfulRequests: this.stats.successfulRequests,
failedRequests: this.stats.failedRequests,
averageResponseTime,
connectionPoolUtilization: utilization,
requestsPerSecond
};
}
async close(): Promise<void> {
// Stop queue processor
if (this.queueProcessor) {
clearInterval(this.queueProcessor);
this.queueProcessor = undefined;
}
// Wait for pending requests to complete (with timeout)
const timeout = 30000; // 30 seconds
const startTime = Date.now();
while (this.requestQueue.length > 0 && Date.now() - startTime < timeout) {
await new Promise(resolve => setTimeout(resolve, 100));
}
// Reject remaining requests
while (this.requestQueue.length > 0) {
const request = this.requestQueue.shift()!;
request.reject(new Error('Connection pool closing'));
}
// Clear connections
this.activeConnections.clear();
this.removeAllListeners();
this.emit('closed');
}
async healthCheck(): Promise<{ healthy: boolean; details: any }> {
const stats = this.getStats();
const queueSize = this.requestQueue.length;
const healthy =
stats.connectionPoolUtilization < 0.9 && // Less than 90% utilization
queueSize < 100 && // Queue not too large
stats.averageResponseTime < 5000; // Average response time under 5 seconds
return {
healthy,
details: {
stats,
queueSize,
activeHosts: Array.from(this.activeConnections.keys()),
config: this.config
},
};
}
}

View file

@ -1,130 +0,0 @@
import { EventEmitter } from 'eventemitter3';
import {
RetryConfig,
RequestConfig,
HttpResponse,
RetryExhaustedError
} from './types';
import { TimeoutError } from './types';
export class RetryHandler extends EventEmitter {
private config: Required<RetryConfig>;
constructor(config: Partial<RetryConfig> = {}) {
super();
this.config = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
exponentialBackoff: true,
retryCondition: this.defaultRetryCondition,
...config
};
}
async execute<T>(
operation: () => Promise<HttpResponse<T>>,
requestConfig: RequestConfig
): Promise<HttpResponse<T>> {
let lastError: any;
let attempt = 0;
while (attempt <= this.config.maxRetries) {
try {
const result = await operation();
if (attempt > 0) {
this.emit('retrySuccess', {
requestConfig,
attempt,
result
});
}
return result;
} catch (error) {
lastError = error;
attempt++;
// Check if we should retry
if (
attempt > this.config.maxRetries ||
!this.config.retryCondition(error)
) {
break;
}
// Calculate delay
const delay = this.calculateDelay(attempt);
this.emit('retryAttempt', {
requestConfig,
attempt,
error,
delay
});
// Wait before retry
await this.delay(delay);
}
}
// All retries exhausted
const finalError = new RetryExhaustedError(requestConfig, attempt, lastError);
this.emit('retryExhausted', {
requestConfig,
attempts: attempt,
finalError
});
throw finalError;
}
private calculateDelay(attempt: number): number {
if (!this.config.exponentialBackoff) {
return this.config.baseDelay;
}
// Exponential backoff with jitter
const exponentialDelay = this.config.baseDelay * Math.pow(2, attempt - 1);
const jitter = Math.random() * 0.1 * exponentialDelay; // 10% jitter
const totalDelay = Math.min(exponentialDelay + jitter, this.config.maxDelay);
return Math.floor(totalDelay);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private defaultRetryCondition(error: any): boolean {
// Network errors
if (error.code === 'ECONNRESET' ||
error.code === 'ECONNREFUSED' ||
error.code === 'ETIMEDOUT' ||
error.code === 'ENOTFOUND') {
return true;
}
// HTTP status codes that should be retried
const retryableStatuses = [408, 429, 500, 502, 503, 504];
if (error.status && retryableStatuses.includes(error.status)) {
return true;
}
// Timeout errors
if (error instanceof TimeoutError) {
return true;
}
return false;
}
updateConfig(config: Partial<RetryConfig>): void {
this.config = { ...this.config, ...config };
}
getConfig(): RetryConfig {
return { ...this.config };
}
}

View file

@ -1,25 +0,0 @@
// Main exports
export { BunHttpClient } from './BunHttpClient';
export { ConnectionPool } from './ConnectionPool';
export { RetryHandler } from './RetryHandler';
// Type exports
export type {
HttpClientConfig,
RequestConfig,
HttpResponse,
ConnectionPoolConfig,
ConnectionStats,
QueuedRequest,
RetryConfig
} from './types';
// Error exports
export {
HttpClientError,
TimeoutError,
RetryExhaustedError
} from './types';
// Default export for convenience
export { BunHttpClient as default } from './BunHttpClient';

View file

@ -1,104 +0,0 @@
// Type definitions for the HTTP client
export interface HttpClientConfig {
baseURL?: string;
timeout?: number;
headers?: Record<string, string>;
retries?: number;
retryDelay?: number;
maxConcurrency?: number;
keepAlive?: boolean;
validateStatus?: (status: number) => boolean;
}
export interface RequestConfig {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
headers?: Record<string, string>;
body?: any;
timeout?: number;
retries?: number;
metadata?: Record<string, any>;
validateStatus?: (status: number) => boolean;
}
export interface HttpResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: Record<string, string>;
config: RequestConfig;
timing: {
start: number;
end: number;
duration: number;
};
}
export interface ConnectionPoolConfig {
maxConnections: number;
maxConnectionsPerHost: number;
keepAlive: boolean;
maxIdleTime: number;
connectionTimeout: number;
}
export interface ConnectionStats {
activeConnections: number;
totalConnections: number;
successfulRequests: number;
failedRequests: number;
averageResponseTime: number;
connectionPoolUtilization: number;
requestsPerSecond: number;
}
export interface QueuedRequest {
id: string;
config: RequestConfig;
resolve: (value: HttpResponse) => void;
reject: (error: any) => void;
timestamp: number;
retryCount: number;
host: string;
}
export interface RetryConfig {
maxRetries: number;
baseDelay: number;
maxDelay: number;
exponentialBackoff: boolean;
retryCondition?: (error: any) => boolean;
}
export class HttpClientError extends Error {
constructor(
message: string,
public code?: string,
public status?: number,
public response?: any,
public config?: RequestConfig
) {
super(message);
this.name = 'HttpClientError';
}
}
export class TimeoutError extends HttpClientError {
constructor(config: RequestConfig, timeout: number) {
super(`Request timeout after ${timeout}ms`, 'TIMEOUT', undefined, undefined, config);
this.name = 'TimeoutError';
}
}
export class RetryExhaustedError extends HttpClientError {
constructor(config: RequestConfig, attempts: number, lastError: any) {
super(
`Request failed after ${attempts} attempts: ${lastError.message}`,
'RETRY_EXHAUSTED',
lastError.status,
lastError.response,
config
);
this.name = 'RetryExhaustedError';
}
}

View file

@ -1,18 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
"references": [
{ "path": "../api-client" },
{ "path": "../config" },
{ "path": "../event-bus" },
{ "path": "../types" },
{ "path": "../utils" },
]
}