UNPKG

@simpleapps-com/augur-api

Version:

TypeScript client library for Augur microservices API endpoints

212 lines 8.38 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HTTPClient = void 0; const axios_1 = __importDefault(require("axios")); const config_1 = require("./config"); const errors_1 = require("./errors"); /** * Generates a unique key for request deduplication * @param method HTTP method * @param url Request URL * @param params Query parameters or request body * @returns Unique string key for this request */ const generateRequestKey = (method, url, params) => { const paramsStr = params ? JSON.stringify(params, Object.keys(params).sort()) : ''; return `${method}:${url}:${paramsStr}`; }; /** * Calculates delay for exponential backoff with jitter * @param attempt Current attempt number (0-indexed) * @param baseDelay Base delay in milliseconds * @returns Delay in milliseconds with jitter */ const calculateBackoffDelay = (attempt, baseDelay) => { const exponentialDelay = baseDelay * Math.pow(2, attempt); const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter return Math.min(exponentialDelay + jitter, 30000); // Cap at 30 seconds }; /** * Determines if an error is retryable * @param error The error to check * @returns True if the request should be retried */ const isRetryableError = (error) => { const axiosError = error; // Network errors (no response) are retryable if (!axiosError.response) { return true; } const status = axiosError.response.status; // Rate limit (429) and server errors (5xx) are retryable return status === 429 || (status >= 500 && status < 600); }; /** * Delays execution for specified milliseconds * @param ms Milliseconds to delay */ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); class HTTPClient { constructor(serviceName, config) { this.inflightRequests = new Map(); this.serviceName = serviceName; this.config = { ...config_1.DEFAULT_CONFIG, ...config }; this.axios = axios_1.default.create({ timeout: this.config.timeout, }); this.setupInterceptors(); } setupInterceptors() { // Request interceptor this.axios.interceptors.request.use(async (config) => { // Always add x-site-id header config.headers['x-site-id'] = this.config.siteId; // Add bearer token for non-health-check endpoints // Use endsWith() for precise matching - includes() would false-positive on URLs like '/health-check-status' const isPublicEndpoint = config.url?.endsWith('/health-check') || config.url?.endsWith('/ping'); if (!isPublicEndpoint && this.config.bearerToken) { config.headers['Authorization'] = `Bearer ${this.config.bearerToken}`; } // User-defined request interceptor if (this.config.onRequest) { const modifiedConfig = await this.config.onRequest(config); // Ensure we return the internal config format return modifiedConfig; } return config; }, error => Promise.reject(error)); // Response interceptor this.axios.interceptors.response.use(async (response) => { if (this.config.onResponse) { return this.config.onResponse(response); } return response; }, async (error) => { const originalRequest = error.config; const endpoint = originalRequest?.url || 'unknown'; if (error.response) { this.handleHttpError(error.response, endpoint); } this.handleNetworkError(error, endpoint); }); } handleHttpError(response, endpoint) { const { status, data } = response; switch (status) { case 401: throw new errors_1.AuthenticationError({ message: data?.message || 'Authentication failed', service: this.serviceName, endpoint, }); case 404: throw new errors_1.NotFoundError({ message: data?.message || 'Resource not found', service: this.serviceName, endpoint, }); case 429: throw new errors_1.RateLimitError({ message: data?.message || 'Rate limit exceeded', service: this.serviceName, endpoint, }); default: throw new errors_1.AugurError({ message: data?.message || `Request failed with status ${status}`, code: data?.code || 'API_ERROR', statusCode: status, service: this.serviceName, endpoint, requestId: data?.requestId, }); } } handleNetworkError(error, endpoint) { throw new errors_1.AugurError({ message: error.message || 'Network error', code: 'NETWORK_ERROR', statusCode: 0, service: this.serviceName, endpoint, }); } /** * Executes a single request attempt * @param requestFn Function that performs the actual request * @param attempt Current attempt number * @param maxRetries Maximum number of retries * @param baseDelay Base delay for exponential backoff * @returns Promise resolving to the response */ async executeAttempt(requestFn, attempt, maxRetries, baseDelay) { try { return await requestFn(); } catch (error) { // Don't retry if we've exhausted attempts or error is not retryable if (attempt >= maxRetries || !isRetryableError(error)) { throw error; } // Wait before retrying with exponential backoff const backoffDelay = calculateBackoffDelay(attempt, baseDelay); await delay(backoffDelay); // Recursive call for next attempt return this.executeAttempt(requestFn, attempt + 1, maxRetries, baseDelay); } } /** * Executes a request with retry logic * @param requestFn Function that performs the actual request * @returns Promise resolving to the response */ async executeWithRetry(requestFn) { const maxRetries = this.config.retries ?? config_1.DEFAULT_CONFIG.retries; const baseDelay = this.config.retryDelay ?? config_1.DEFAULT_CONFIG.retryDelay; return this.executeAttempt(requestFn, 0, maxRetries, baseDelay); } async get(url, params, config) { const requestKey = generateRequestKey('GET', url, params); // Check if identical request is already in-flight const inflight = this.inflightRequests.get(requestKey); if (inflight) { return inflight; } // Create the request promise with retry logic const requestPromise = this.executeWithRetry(() => this.axios.get(url, { params, ...config, })) .then(response => response.data) .finally(() => { // Clean up after request completes (success or failure) this.inflightRequests.delete(requestKey); }); // Store the promise for deduplication this.inflightRequests.set(requestKey, requestPromise); return requestPromise; } async post(url, data, config) { const response = await this.executeWithRetry(() => this.axios.post(url, data, config)); return response.data; } async put(url, data, config) { const response = await this.executeWithRetry(() => this.axios.put(url, data, config)); return response.data; } async delete(url, config) { const response = await this.executeWithRetry(() => this.axios.delete(url, config)); return response.data; } setBearerToken(token) { this.config.bearerToken = token; } setSiteId(siteId) { this.config.siteId = siteId; } } exports.HTTPClient = HTTPClient; //# sourceMappingURL=client.js.map