Skip to main content
Glama
ComposedRequestManager.ts9.73 kB
/** * Composed Request Manager * Uses composition instead of inheritance for better testability and flexibility */ import type { HTTPMethod, RequestOptions, ClientStats, WordPressClientConfig } from "@/types/client.js"; import { WordPressAPIError, RateLimitError } from "@/types/client.js"; import { config } from "@/config/Config.js"; import { startTimer } from "@/utils/debug.js"; import { getUserAgent } from "@/utils/version.js"; import type { ConfigurationProvider, ErrorHandler, ParameterValidator, AuthenticationProvider, RequestHandler, } from "./interfaces/ManagerInterfaces.js"; import { ConfigurationProviderImpl } from "./implementations/ConfigurationProviderImpl.js"; import { ErrorHandlerImpl } from "./implementations/ErrorHandlerImpl.js"; import { ParameterValidatorImpl } from "./implementations/ParameterValidatorImpl.js"; interface ComposedRequestManagerDependencies { configProvider: ConfigurationProvider; errorHandler: ErrorHandler; validator: ParameterValidator; authProvider: AuthenticationProvider; } export class ComposedRequestManager implements RequestHandler { private stats: ClientStats; private lastRequestTime: number = 0; private requestInterval: number; private initialized: boolean = false; constructor(private dependencies: ComposedRequestManagerDependencies) { this.requestInterval = 60000 / config().security.rateLimit; this.stats = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, rateLimitHits: 0, authFailures: 0, errors: 0, }; } /** * Factory method to create ComposedRequestManager with default implementations */ static create(clientConfig: WordPressClientConfig, authProvider: AuthenticationProvider): ComposedRequestManager { const configProvider = new ConfigurationProviderImpl(clientConfig); const errorHandler = new ErrorHandlerImpl(configProvider); const validator = new ParameterValidatorImpl(); return new ComposedRequestManager({ configProvider, errorHandler, validator, authProvider, }); } /** * Initialize the manager */ async initialize(): Promise<void> { if (this.initialized) { return; } try { // Validate configuration if (!this.dependencies.configProvider.config.baseUrl) { throw new Error("Base URL is required"); } // Ensure authentication is ready await this.dependencies.authProvider.authenticate(); this.initialized = true; } catch (error) { this.dependencies.errorHandler.handleError(error, "initialize request manager"); } } /** * Make HTTP request with retry logic and rate limiting */ async request<T>(method: HTTPMethod, endpoint: string, data?: unknown, options: RequestOptions = {}): Promise<T> { this.ensureInitialized(); const timer = startTimer(); this.stats.totalRequests++; try { // Validate inputs this.dependencies.validator.validateString(method, "method", { required: true }); this.dependencies.validator.validateString(endpoint, "endpoint", { required: true }); await this.enforceRateLimit(); const response = await this.makeRequestWithRetry(method, endpoint, data, options); this.stats.successfulRequests++; this.updateAverageResponseTime(timer.end()); this.dependencies.errorHandler.logSuccess(`${method} ${endpoint}`, { responseTime: timer.end() }); return response as T; } catch (error) { this.stats.failedRequests++; this.stats.errors++; if (error instanceof RateLimitError) { this.stats.rateLimitHits++; } this.dependencies.errorHandler.handleError(error, `${method} ${endpoint}`); } } /** * Get request statistics */ getStats(): ClientStats { return { ...this.stats }; } /** * Reset statistics */ resetStats(): void { this.stats = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, rateLimitHits: 0, authFailures: 0, errors: 0, }; } /** * Enforce rate limiting */ private async enforceRateLimit(): Promise<void> { const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.requestInterval) { const delay = this.requestInterval - timeSinceLastRequest; await new Promise((resolve) => setTimeout(resolve, delay)); } this.lastRequestTime = Date.now(); } /** * Make request with retry logic */ private async makeRequestWithRetry<T>( method: HTTPMethod, endpoint: string, data?: unknown, options: RequestOptions = {}, attempt: number = 1, ): Promise<T> { const maxRetries = 3; // Use default retries for composed manager try { return await this.makeRequest<T>(method, endpoint, data, options); } catch (error) { if (attempt <= maxRetries && this.shouldRetry(error)) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff await new Promise((resolve) => setTimeout(resolve, delay)); return this.makeRequestWithRetry<T>(method, endpoint, data, options, attempt + 1); } throw error; } } /** * Make actual HTTP request */ private async makeRequest<T>( method: HTTPMethod, endpoint: string, data?: unknown, options: RequestOptions = {}, ): Promise<T> { const url = this.buildUrl(endpoint); const requestOptions = await this.buildRequestOptions(method, data, options); const response = await fetch(url, requestOptions); if (!response.ok) { await this.handleHttpError(response); } const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { return (await response.json()) as T; } return (await response.text()) as unknown as T; } /** * Build full URL for request */ private buildUrl(endpoint: string): string { const baseUrl = this.dependencies.configProvider.config.baseUrl; const cleanBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; return `${cleanBase}/wp-json${cleanEndpoint}`; } /** * Build request options with authentication */ private async buildRequestOptions( method: HTTPMethod, data?: unknown, options: RequestOptions = {}, ): Promise<RequestInit> { const authHeaders = this.dependencies.authProvider.getAuthHeaders(); const timeout = this.dependencies.configProvider.config.timeout || 30000; const headers = { "Content-Type": "application/json", Accept: "application/json", "User-Agent": getUserAgent(), ...authHeaders, ...options.headers, }; const requestOptions: RequestInit = { method, headers, signal: typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(timeout) : null, }; if (data && (method === "POST" || method === "PUT" || method === "PATCH")) { requestOptions.body = JSON.stringify(data); } return requestOptions; } /** * Handle HTTP error responses */ private async handleHttpError(response: Response): Promise<never> { const statusCode = response.status; if (statusCode === 401) { this.stats.authFailures++; // Try to refresh authentication try { await this.dependencies.authProvider.handleAuthFailure(new Error(`HTTP ${statusCode}`)); // If refresh succeeds, the caller should retry throw new WordPressAPIError("Authentication refreshed, retry needed", 401, "auth_refreshed"); } catch (_authError) { throw new WordPressAPIError("Authentication failed", 401, "auth_failed"); } } if (statusCode === 429) { const retryAfter = response.headers.get("retry-after"); const delay = retryAfter ? parseInt(retryAfter, 10) : 60; throw new RateLimitError("Rate limit exceeded", delay); } let errorMessage = `HTTP ${statusCode}: ${response.statusText}`; try { const errorBody = (await response.json()) as { message?: string }; if (errorBody.message) { errorMessage = errorBody.message; } } catch { // Ignore JSON parsing errors for non-JSON error responses } throw new WordPressAPIError(errorMessage, statusCode, "http_error"); } /** * Determine if error is retryable */ private shouldRetry(error: unknown): boolean { if (error instanceof WordPressAPIError) { const nonRetryableCodes = [400, 401, 403, 404]; return !nonRetryableCodes.includes(error.statusCode || 500); } if (error instanceof Error) { const retryableErrors = ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN"]; return retryableErrors.some((code) => error.message.includes(code)); } return false; } /** * Update average response time with new measurement */ private updateAverageResponseTime(newTime: number): void { const total = this.stats.averageResponseTime * (this.stats.successfulRequests - 1) + newTime; this.stats.averageResponseTime = Math.round(total / this.stats.successfulRequests); } /** * Ensure manager is initialized before operations */ private ensureInitialized(): void { if (!this.initialized) { throw new Error("ComposedRequestManager not initialized. Call initialize() first."); } } /** * Cleanup resources */ dispose(): void { // Cleanup any resources, timers, etc. this.initialized = false; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server