@dima_aryze/reforge
Version:
TypeScript/JavaScript SDK for Reforge - Cross-chain token operations
253 lines • 8.08 kB
JavaScript
/**
* Base HTTP client implementation for the Reforge SDK
*/
import axios from 'axios';
import { ReforgeError, ReforgeValidationError } from '../errors';
import { createLogger, deepMerge, generateRequestId, sanitizeUrl } from '../utils';
import { ErrorTransformer } from './errors';
import { RequestInterceptor } from './interceptors';
import { RetryHandler } from './retry';
/**
* Default configuration for HTTP requests
*/
const DEFAULT_CONFIG = {
timeout: 30000,
headers: {},
debug: false,
retry: {
attempts: 3,
delay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
},
};
/**
* Base HTTP client with comprehensive error handling and retry logic
*/
export class BaseHttpClient {
constructor(options) {
Object.defineProperty(this, "httpClient", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "config", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "logger", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "eventListeners", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
Object.defineProperty(this, "errorTransformer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "retryHandler", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "apiKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.validateOptions(options);
// Merge configuration with defaults
this.config = deepMerge({}, DEFAULT_CONFIG, {
baseUrl: options.baseUrl,
timeout: options.timeout,
headers: options.headers,
debug: options.debug,
retry: options.retry,
});
this.apiKey = options.apiKey;
this.logger = createLogger(this.config.debug ? 'debug' : 'warn');
this.errorTransformer = new ErrorTransformer();
this.retryHandler = new RetryHandler(this.config.retry, this.logger);
// Initialize HTTP client
this.httpClient = options.httpClient || this.createHttpClient();
this.logger.info('HTTP client initialized', {
baseUrl: this.config.baseUrl,
hasApiKey: !!this.apiKey,
debug: this.config.debug,
});
}
/**
* Validate constructor options
*/
validateOptions(options) {
if (!options.apiKey || typeof options.apiKey !== 'string') {
throw new ReforgeValidationError('API key is required and must be a string');
}
if (!options.baseUrl || typeof options.baseUrl !== 'string') {
throw new ReforgeValidationError('Base URL is required and must be a string');
}
try {
new URL(options.baseUrl);
}
catch {
throw new ReforgeValidationError('Base URL must be a valid URL');
}
}
/**
* Create and configure the Axios instance
*/
createHttpClient() {
const client = axios.create({
baseURL: sanitizeUrl(this.config.baseUrl),
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'reforge-sdk/1.0.0',
...this.config.headers,
},
});
// Setup interceptors
const requestInterceptor = new RequestInterceptor(this.apiKey, this.logger);
requestInterceptor.setup(client);
// Response interceptor
client.interceptors.response.use(response => {
this.logger.debug('Response received', {
status: response.status,
url: response.config.url,
});
return response;
}, error => {
const transformedError = this.errorTransformer.transform(error);
this.logger.error('Response error', transformedError.message);
return Promise.reject(transformedError);
});
return client;
}
/**
* Emit an event to all registered listeners
*/
emitEvent(type, data) {
const listeners = this.eventListeners.get(type) || [];
const eventData = {
type,
timestamp: new Date(),
data,
};
listeners.forEach(callback => {
try {
callback(eventData);
}
catch (error) {
this.logger.error('Event listener error', error);
}
});
}
/**
* Make HTTP request with comprehensive error handling and retry logic
*/
async makeRequest(method, path, data, options = {}) {
return this.retryHandler.execute(async (attempt) => {
this.emitEvent('request:start', { method, path, attempt });
try {
const axiosConfig = {
method,
url: path,
timeout: options.timeout || this.config.timeout,
headers: {
...options.headers,
...(options.skipAuth ? { 'skip-auth': 'true' } : {}),
'X-Request-ID': generateRequestId(),
},
};
if (data) {
if (method === 'GET') {
axiosConfig.params = data;
}
else {
axiosConfig.data = data;
}
}
const response = await this.httpClient.request(axiosConfig);
this.emitEvent('request:success', {
method,
path,
attempt,
status: response.status,
});
return response.data;
}
catch (error) {
const transformedError = error instanceof ReforgeError ? error : this.errorTransformer.transform(error);
this.emitEvent('request:error', {
method,
path,
attempt,
error: transformedError,
});
throw transformedError;
}
}, options.retry);
}
/**
* Update the API key
*/
setApiKey(apiKey) {
if (typeof apiKey !== 'string' || !apiKey.trim()) {
throw new ReforgeValidationError('API key must be a non-empty string');
}
this.apiKey = apiKey.trim();
this.logger.info('API key updated');
}
/**
* Get the current API key
*/
getApiKey() {
return this.apiKey;
}
/**
* Add event listener
*/
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
/**
* Remove event listener
*/
off(event, callback) {
const listeners = this.eventListeners.get(event);
if (listeners) {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
/**
* Remove all event listeners
*/
removeAllListeners(event) {
if (event) {
this.eventListeners.delete(event);
}
else {
this.eventListeners.clear();
}
}
}
//# sourceMappingURL=base.js.map