UNPKG

@kimsungwhee/apple-docs-mcp

Version:

MCP server for Apple Developer Documentation - Search iOS/macOS/SwiftUI/UIKit docs, WWDC videos, Swift/Objective-C APIs & code examples in Claude, Cursor & AI assistants

276 lines 10.4 kB
/** * HTTP client with timeout, retry, and rate limiting */ import { REQUEST_CONFIG, ERROR_MESSAGES, PROCESSING_LIMITS } from './constants.js'; import { handleFetchError } from './error-handler.js'; import { globalRateLimiter } from './rate-limiter.js'; class HttpClient { requestQueue = []; activeRequests = 0; maxConcurrentRequests = REQUEST_CONFIG.MAX_CONCURRENT_REQUESTS; // Performance monitoring stats = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, totalResponseTime: 0, averageResponseTime: 0, successRate: 0, requestsByStatus: {}, requestsByDomain: {}, }; /** * Make a GET request with timeout and retry logic */ async get(url, options = {}) { const { timeout = REQUEST_CONFIG.TIMEOUT, retries = REQUEST_CONFIG.MAX_RETRIES, retryDelay = REQUEST_CONFIG.RETRY_DELAY, headers = {}, } = options; const defaultHeaders = { 'User-Agent': REQUEST_CONFIG.USER_AGENT, 'Accept': 'application/json', ...headers, }; return this.executeWithQueue(async () => { // Check rate limit if (!globalRateLimiter.canMakeRequest()) { throw new Error('Rate limit exceeded. Please try again later.'); } return this.fetchWithRetry(url, { method: 'GET', headers: defaultHeaders, signal: AbortSignal.timeout(timeout), }, retries, retryDelay); }); } /** * Execute request with concurrency control */ async executeWithQueue(requestFn) { return new Promise((resolve, reject) => { const execute = async () => { if (this.activeRequests >= this.maxConcurrentRequests) { // Add to queue this.requestQueue.push(execute); return; } this.activeRequests++; try { const result = await requestFn(); resolve(result); } catch (error) { reject(error); } finally { this.activeRequests--; // Process next request in queue const nextRequest = this.requestQueue.shift(); if (nextRequest) { void nextRequest(); } } }; void execute(); }); } /** * Fetch with retry logic and performance monitoring */ async fetchWithRetry(url, options, retries, retryDelay) { const startTime = Date.now(); const domain = new URL(url).hostname; let lastError = null; // Update domain stats this.stats.requestsByDomain[domain] = (this.stats.requestsByDomain[domain] || 0) + 1; this.stats.totalRequests++; for (let attempt = 0; attempt <= retries; attempt++) { try { const response = await fetch(url, options); const responseTime = Date.now() - startTime; // Update performance stats only if response is valid if (response) { this.updateStats(response.status, responseTime, true); } if (!response?.ok) { if (response && response.status === 404) { throw new Error(`${ERROR_MESSAGES.NOT_FOUND} (${response.status})`); } if (response && response.status >= 500 && attempt < retries) { // Retry on server errors throw new Error(`Server error: ${response.status}`); } const status = response ? response.status : 'unknown'; const statusText = response ? response.statusText : 'No response'; throw new Error(`HTTP ${status}: ${statusText}`); } return response; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Don't retry on certain errors if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error(ERROR_MESSAGES.TIMEOUT); } if (error.message.includes('404')) { throw error; // Don't retry 404s } } // Wait before retry (except on last attempt) if (attempt < retries) { await this.delay(retryDelay * Math.pow(2, attempt)); // Exponential backoff } } } // Update stats for failed request const responseTime = Date.now() - startTime; this.updateStats(0, responseTime, false); throw lastError ?? new Error('Request failed after retries'); } /** * Update performance statistics */ updateStats(statusCode, responseTime, success) { if (success) { this.stats.successfulRequests++; } else { this.stats.failedRequests++; } this.stats.totalResponseTime += responseTime; this.stats.averageResponseTime = this.stats.totalResponseTime / this.stats.totalRequests; this.stats.successRate = (this.stats.successfulRequests / this.stats.totalRequests) * 100; if (statusCode > 0) { this.stats.requestsByStatus[statusCode] = (this.stats.requestsByStatus[statusCode] || 0) + 1; } } /** * Delay utility */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get JSON response with error handling */ async getJson(url, options = {}) { try { const response = await this.get(url, options); return await response.json(); } catch (error) { const appError = handleFetchError(error, url); throw appError; } } /** * Get text response with error handling */ async getText(url, options = {}) { try { const response = await this.get(url, { ...options, headers: { ...options.headers, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, }); return await response.text(); } catch (error) { const appError = handleFetchError(error, url); throw appError; } } /** * Get current queue status */ getStatus() { return { activeRequests: this.activeRequests, queuedRequests: this.requestQueue.length, maxConcurrent: this.maxConcurrentRequests, }; } /** * Get performance statistics */ getPerformanceStats() { return { ...this.stats }; } /** * Reset performance statistics */ resetStats() { this.stats = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, totalResponseTime: 0, averageResponseTime: 0, successRate: 0, requestsByStatus: {}, requestsByDomain: {}, }; } /** * Get formatted performance report */ getPerformanceReport() { const stats = this.getPerformanceStats(); let report = '# HTTP Client Performance Report\n\n'; // Overall Statistics report += '## Overall Statistics\n\n'; report += `- **Total Requests:** ${stats.totalRequests}\n`; report += `- **Successful Requests:** ${stats.successfulRequests}\n`; report += `- **Failed Requests:** ${stats.failedRequests}\n`; report += `- **Success Rate:** ${stats.successRate.toFixed(2)}%\n`; report += `- **Average Response Time:** ${stats.averageResponseTime.toFixed(0)}ms\n\n`; // Status Code Distribution if (Object.keys(stats.requestsByStatus).length > 0) { report += '## Response Status Codes\n\n'; Object.entries(stats.requestsByStatus) .sort(([a], [b]) => parseInt(a) - parseInt(b)) .forEach(([status, count]) => { const percentage = ((count / stats.totalRequests) * 100).toFixed(1); report += `- **${status}:** ${count} requests (${percentage}%)\n`; }); report += '\n'; } // Domain Distribution if (Object.keys(stats.requestsByDomain).length > 0) { report += '## Requests by Domain\n\n'; Object.entries(stats.requestsByDomain) .sort(([, a], [, b]) => b - a) .forEach(([domain, count]) => { const percentage = ((count / stats.totalRequests) * 100).toFixed(1); report += `- **${domain}:** ${count} requests (${percentage}%)\n`; }); report += '\n'; } // Performance Insights report += '## Performance Insights\n\n'; if (stats.successRate >= 95) { report += '✅ **Excellent reliability** - Success rate above 95%\n'; } else if (stats.successRate >= 90) { report += '⚠️ **Good reliability** - Success rate above 90%\n'; } else { report += '❌ **Poor reliability** - Success rate below 90%\n'; } if (stats.averageResponseTime < PROCESSING_LIMITS.RESPONSE_TIME_GOOD_THRESHOLD) { report += '✅ **Fast response times** - Average under 1 second\n'; } else if (stats.averageResponseTime < PROCESSING_LIMITS.RESPONSE_TIME_MODERATE_THRESHOLD) { report += '⚠️ **Moderate response times** - Average under 3 seconds\n'; } else { report += '❌ **Slow response times** - Average over 3 seconds\n'; } report += `\n*Report generated at ${new Date().toISOString()}*`; return report; } } // Export singleton instance export const httpClient = new HttpClient(); //# sourceMappingURL=http-client.js.map