@sschepis/vast-node
Version:
Node.js client for Vast.ai API - Rent GPUs for machine learning and AI workloads
325 lines (324 loc) • 12.3 kB
JavaScript
;
/**
* @file dynamic-api.ts
* @description A flexible framework for making HTTP requests to RESTful APIs with automatic
* retry, rate limiting, and request queuing capabilities.
* @author vast-node team
* @license MIT
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamicApi = exports.RequestQueue = exports.transformToCamelCase = exports.transformToSnakeCase = void 0;
const axios_1 = __importDefault(require("axios"));
const limiter_1 = require("limiter");
/**
* Utility functions for parameter name transformation
* VAST.ai API requires snake_case parameter names like the Python client uses,
* but our TypeScript/JavaScript API uses camelCase for better developer experience.
*/
/**
* Convert a camelCase string to snake_case
* Example: "diskSpace" -> "disk_space"
*/
function camelToSnakeCase(str) {
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}
/**
* Convert a snake_case string to camelCase
* Example: "disk_space" -> "diskSpace"
*/
function snakeToCamelCase(str) {
return str.replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', '').replace('_', ''));
}
/**
* Recursively transform all keys in an object from camelCase to snake_case
*/
function transformToSnakeCase(obj) {
if (obj === null || obj === undefined || typeof obj !== 'object' || Array.isArray(obj)) {
return obj;
}
const result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const snakeCaseKey = camelToSnakeCase(key);
const value = obj[key];
// Recursively transform nested objects
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
// Handle array of objects
result[snakeCaseKey] = value.map(item => typeof item === 'object' && item !== null
? transformToSnakeCase(item)
: item);
}
else {
// Handle nested object
result[snakeCaseKey] = transformToSnakeCase(value);
}
}
else {
// Handle primitive values
result[snakeCaseKey] = value;
}
}
}
return result;
}
exports.transformToSnakeCase = transformToSnakeCase;
/**
* Recursively transform all keys in an object from snake_case to camelCase
*/
function transformToCamelCase(obj) {
if (obj === null || obj === undefined || typeof obj !== 'object' || Array.isArray(obj)) {
return obj;
}
const result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const camelCaseKey = snakeToCamelCase(key);
const value = obj[key];
// Recursively transform nested objects
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
// Handle array of objects
result[camelCaseKey] = value.map(item => typeof item === 'object' && item !== null
? transformToCamelCase(item)
: item);
}
else {
// Handle nested object
result[camelCaseKey] = transformToCamelCase(value);
}
}
else {
// Handle primitive values
result[camelCaseKey] = value;
}
}
}
return result;
}
exports.transformToCamelCase = transformToCamelCase;
/**
* Queue that limits the number of concurrent requests
* @internal
*/
class RequestQueue {
constructor(concurrency) {
this.queue = [];
this.running = 0;
this.concurrency = concurrency;
}
enqueue(task) {
return new Promise((resolve, reject) => {
this.queue.push(() => task().then(resolve).catch(reject));
this.runNext();
});
}
runNext() {
if (this.running >= this.concurrency || this.queue.length === 0)
return;
this.running++;
const task = this.queue.shift();
if (task) {
task().finally(() => {
this.running--;
this.runNext();
});
}
}
}
exports.RequestQueue = RequestQueue;
/**
* Dynamic API client that provides a flexible way to define and interact with RESTful APIs
*
* Features:
* - Automatic request retry with customizable conditions
* - Rate limiting to prevent API throttling
* - Request queuing to limit concurrent requests
* - Path parameter interpolation
* - Global and per-endpoint configurations
*/
class DynamicApi {
/**
* Creates a new DynamicApi instance
*
* @param config - API configuration
*/
constructor(config) {
this.rateLimiters = new Map();
this.config = config;
this.axiosInstance = axios_1.default.create({
baseURL: config.baseUrl,
timeout: config.timeout || 10000,
headers: config.globalHeaders || {},
});
if (config.responseInterceptor) {
this.axiosInstance.interceptors.response.use(config.responseInterceptor);
}
if (config.errorInterceptor) {
this.axiosInstance.interceptors.response.use(undefined, config.errorInterceptor);
}
this.requestQueue = new RequestQueue(config.queueConcurrency || 5);
}
/**
* Creates API method functions from the endpoint configurations
*
* @template T - The type of endpoints configuration
* @returns An object with methods corresponding to each endpoint
*/
createApiMethods() {
const api = {};
for (const [name, endpoint] of Object.entries(this.config.endpoints)) {
api[name] = this.createMethod(name, endpoint);
if (endpoint.rateLimitPerSecond) {
this.rateLimiters.set(name, new limiter_1.RateLimiter({ tokensPerInterval: endpoint.rateLimitPerSecond, interval: 'second' }));
}
}
return api;
}
/**
* Creates a method function for a specific endpoint
*
* @template T - Parameter types for this endpoint
* @param name - The name of the endpoint
* @param endpoint - The endpoint configuration
* @returns A function that makes requests to this endpoint
* @internal
*/
createMethod(name, endpoint) {
return async (data = {}, config = {}) => {
const rateLimiter = this.rateLimiters.get(name);
if (rateLimiter) {
await rateLimiter.removeTokens(1);
}
return this.requestQueue.enqueue(() => this.makeRequest(endpoint, data, config));
};
}
/**
* Makes an HTTP request based on the endpoint configuration
*
* @template T - Parameter types for this endpoint
* @param endpoint - The endpoint configuration
* @param data - The request data
* @param config - Additional axios configuration
* @returns The response data
* @internal
*/
async makeRequest(endpoint, data, config) {
var _a, _b, _c;
const { method, path, params = [], headers = {}, responseType, retryConfig } = endpoint;
let url = path;
// Replace path parameters and collect query parameters
const queryParams = {};
for (const param of params) {
const paramKey = String(param);
if (data[paramKey] !== undefined) {
// If the parameter is for the path, replace it in the URL
if (url.includes(`:${paramKey}`)) {
url = url.replace(`:${paramKey}`, encodeURIComponent(data[paramKey]));
}
else {
// Otherwise, it's a query parameter, convert its key to snake_case
const snakeCaseParamKey = camelToSnakeCase(paramKey);
queryParams[snakeCaseParamKey] = data[paramKey];
}
// Delete the parameter from the original data object after processing
delete data[paramKey];
}
}
const axiosConfig = {
...config,
method,
url,
headers: { ...headers, ...config.headers },
responseType,
};
// Remove Authorization header if ignoreGlobalAuth is true for this endpoint
if (endpoint.ignoreGlobalAuth && axiosConfig.headers) {
console.log('Ignoring global Authorization header for this endpoint.');
delete axiosConfig.headers['Authorization'];
}
// Transform remaining data (not used for path or query params) from camelCase to snake_case
const transformedData = transformToSnakeCase(data);
// The remaining transformedData goes into the request body for appropriate methods
if (['POST', 'PUT', 'PATCH'].includes(method)) {
axiosConfig.data = transformedData;
}
// Assign the collected query parameters
axiosConfig.params = queryParams;
console.log('Request config before sending:', {
method: axiosConfig.method,
url: axiosConfig.url,
headers: axiosConfig.headers,
params: axiosConfig.params,
data: axiosConfig.data,
});
const finalRetryConfig = {
maxRetries: (retryConfig === null || retryConfig === void 0 ? void 0 : retryConfig.maxRetries) || ((_a = this.config.globalRetryConfig) === null || _a === void 0 ? void 0 : _a.maxRetries) || 3,
retryDelay: (retryConfig === null || retryConfig === void 0 ? void 0 : retryConfig.retryDelay) || ((_b = this.config.globalRetryConfig) === null || _b === void 0 ? void 0 : _b.retryDelay) || 1000,
retryCondition: (retryConfig === null || retryConfig === void 0 ? void 0 : retryConfig.retryCondition) || ((_c = this.config.globalRetryConfig) === null || _c === void 0 ? void 0 : _c.retryCondition)
};
return this.retryRequest(axiosConfig, finalRetryConfig);
}
/**
* Makes a request with automatic retry on failure
*
* @param config - The axios request configuration
* @param retryConfig - The retry configuration
* @returns The response data
* @internal
*/
async retryRequest(config, retryConfig) {
const { maxRetries, retryDelay, retryCondition } = retryConfig;
let retries = 0;
while (true) {
try {
const response = await this.axiosInstance.request(config); // response is now the data from the interceptor
return response; // Return the data directly (which is response.data due to interceptor)
}
catch (error) {
if (axios_1.default.isAxiosError(error) &&
retries < maxRetries &&
(!retryCondition || retryCondition(error))) {
retries++;
await new Promise(resolve => setTimeout(resolve, retryDelay));
continue;
}
throw error;
}
}
}
/**
* Updates global headers for all subsequent requests
*
* @param headers - The headers to add or update
*/
updateGlobalHeaders(headers) {
this.axiosInstance.defaults.headers.common = {
...this.axiosInstance.defaults.headers.common,
...headers,
};
}
/**
* Sets the authorization token for all subsequent requests
*
* @param token - The authentication token
*/
setAuthToken(token) {
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
/**
* Updates the retry configuration
*
* @param config - The new retry configuration
*/
updateRetryConfig(config) {
this.config.globalRetryConfig = {
...this.config.globalRetryConfig,
...config
};
}
}
exports.DynamicApi = DynamicApi;