@simpleapps-com/augur-api
Version:
TypeScript client library for Augur microservices API endpoints
212 lines • 8.38 kB
JavaScript
"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