@future-agi/sdk
Version:
We help GenAI teams maintain high-accuracy for their Models in production.
259 lines • 9.24 kB
JavaScript
import axios, { AxiosError } from 'axios';
import { BoundedExecutor } from '../utils/executor.js';
import { MissingAuthError, DatasetNotFoundError, InvalidAuthError, RateLimitError, ServerError, ServiceUnavailableError } from '../utils/errors.js';
import { AUTH_ENVVAR_NAME, DEFAULT_SETTINGS, get_base_url } from '../utils/constants.js';
/**
* Generic response handler for parsing and validating HTTP responses
*/
export class ResponseHandler {
/**
* Parse the response into the expected type
*/
static parse(response, handlerClass) {
if (!response || response.status !== 200) {
handlerClass._handleError(response);
}
return handlerClass._parseSuccess(response);
}
/**
* Parse successful response - to be implemented by subclasses
*/
static _parseSuccess(response) {
throw new Error("Method '_parseSuccess' must be implemented by subclass.");
}
/**
* Handle error responses - to be implemented by subclasses
*/
static _handleError(response) {
const status = response?.status || 500;
let message;
if (response?.data) {
const d = response.data;
message = d.message || d.detail || d.result;
// If nothing explicit, stringify the whole payload so callers see something meaningful
if (!message) {
try {
message = JSON.stringify(d);
}
catch {
/* ignore */
}
}
}
if (!message) {
const url = response?.config?.url ? ` – ${response.config.url}` : '';
message = (response?.statusText && response.statusText.trim().length > 0)
? response.statusText
: `HTTP ${status}${url}`;
}
switch (status) {
case 401:
case 403:
throw new InvalidAuthError(message);
case 404:
throw new DatasetNotFoundError(message);
case 429:
throw new RateLimitError(message);
case 503:
throw new ServiceUnavailableError(message);
case 500:
case 502:
case 504:
throw new ServerError(message);
default:
throw new Error(`HTTP ${status}: ${message}`);
}
}
}
/**
* Base HTTP client with improved request handling, connection pooling, and async execution
*/
export class HttpClient {
constructor(config = {}) {
this._baseUrl = (config.baseUrl || get_base_url()).replace(/\/$/, '');
this._defaultTimeout = config.timeout || DEFAULT_SETTINGS.TIMEOUT;
this._defaultRetryAttempts = config.retryAttempts || 3;
this._defaultRetryDelay = config.retryDelay || 1000;
// Create axios instance with default configuration
this._axiosInstance = axios.create({
baseURL: this._baseUrl,
timeout: this._defaultTimeout,
headers: {
'Content-Type': 'application/json',
'User-Agent': '@future-agi/sdk',
...config.defaultHeaders,
},
// Enable connection pooling
maxRedirects: 5,
validateStatus: () => true, // Handle all status codes manually
});
// Create bounded executor for managing concurrent requests
this._executor = new BoundedExecutor(config.maxQueue || DEFAULT_SETTINGS.MAX_QUEUE, config.maxWorkers || DEFAULT_SETTINGS.MAX_WORKERS);
this._setupInterceptors();
}
/**
* Setup request and response interceptors
*/
_setupInterceptors() {
// Request interceptor for logging and debugging
this._axiosInstance.interceptors.request.use((config) => {
// Add request timestamp for performance monitoring
config.startTime = Date.now();
return config;
}, (error) => Promise.reject(error));
// Response interceptor for logging and performance monitoring
this._axiosInstance.interceptors.response.use((response) => {
const duration = Date.now() - (response.config.startTime || 0);
// Could add logging here for production monitoring
return response;
}, (error) => Promise.reject(error));
}
/**
* Make an HTTP request with retries and response handling
*/
async request(config, responseHandler) {
const requestConfig = {
method: config.method,
url: config.url,
headers: config.headers,
params: config.params,
data: config.json || config.data,
timeout: config.timeout || this._defaultTimeout,
};
// Handle file uploads
if (config.files && Object.keys(config.files).length > 0) {
const formData = new FormData();
Object.entries(config.files).forEach(([key, file]) => {
formData.append(key, file);
});
if (config.data) {
Object.entries(config.data).forEach(([key, value]) => {
formData.append(key, value);
});
}
requestConfig.data = formData;
requestConfig.headers = {
...requestConfig.headers,
'Content-Type': 'multipart/form-data',
};
}
const retryAttempts = config.retry_attempts || this._defaultRetryAttempts;
const retryDelay = config.retry_delay || this._defaultRetryDelay;
// Execute request with bounded concurrency
return this._executor.submit(async () => {
for (let attempt = 0; attempt < retryAttempts; attempt++) {
try {
const response = await this._axiosInstance.request(requestConfig);
if (responseHandler) {
return ResponseHandler.parse(response, responseHandler);
}
// Handle errors if no custom handler
if (response.status >= 400) {
ResponseHandler._handleError(response);
}
return response;
}
catch (error) {
// Don't retry certain errors
if (error instanceof DatasetNotFoundError ||
error instanceof InvalidAuthError ||
(error instanceof AxiosError && error.response?.status === 401)) {
throw error;
}
// Last attempt - throw the error
if (attempt === retryAttempts - 1) {
if (error instanceof AxiosError) {
ResponseHandler._handleError(error.response);
}
throw error;
}
// Wait before retry
await this._sleep(retryDelay * Math.pow(2, attempt)); // Exponential backoff
}
}
throw new Error('Unexpected end of retry loop');
});
}
/**
* Helper method for sleep/delay
*/
async _sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Close the client and cleanup resources
*/
async close() {
await this._executor.shutdown(true);
}
/**
* Get the base URL
*/
get baseUrl() {
return this._baseUrl;
}
/**
* Get the default timeout
*/
get defaultTimeout() {
return this._defaultTimeout;
}
}
/**
* HTTP client with API key authentication
*/
export class APIKeyAuth extends HttpClient {
constructor(config = {}) {
const fiApiKey = config.fiApiKey || process.env[AUTH_ENVVAR_NAME.API_KEY];
const fiSecretKey = config.fiSecretKey || process.env[AUTH_ENVVAR_NAME.SECRET_KEY];
if (!fiApiKey || !fiSecretKey) {
throw new MissingAuthError(fiApiKey, fiSecretKey);
}
super({
...config,
baseUrl: config.fiBaseUrl || config.baseUrl,
defaultHeaders: {
'X-Api-Key': fiApiKey,
'X-Secret-Key': fiSecretKey,
...config.defaultHeaders,
},
});
// Set class-level credentials
this._fiApiKey = fiApiKey;
this._fiSecretKey = fiSecretKey;
}
/**
* Get the current API key
*/
get fiApiKey() {
return this._fiApiKey;
}
/**
* Get the current secret key
*/
get fiSecretKey() {
return this._fiSecretKey;
}
/**
* Get authentication headers
*/
get headers() {
return {
'X-Api-Key': this._fiApiKey,
'X-Secret-Key': this._fiSecretKey,
};
}
}
/**
* Factory function to create authenticated HTTP client
*/
export function createAuthenticatedClient(config) {
return new APIKeyAuth(config);
}
export default {
ResponseHandler,
HttpClient,
APIKeyAuth,
createAuthenticatedClient,
};
//# sourceMappingURL=auth.js.map