@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
JavaScript
/**
* 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