UNPKG

solver-sdk

Version:

SDK for WorkAI API - AI-powered code analysis with WorkCoins billing system

355 lines 15.2 kB
import axios from 'axios'; import { ErrorMapper } from './error-mapper.js'; import { BaseSDKError } from '../errors/sdk-errors.js'; /** * Определение типа среды выполнения * @returns 'browser' | 'node' */ function getEnvironment() { return (typeof window !== 'undefined' && typeof window.document !== 'undefined') ? 'browser' : 'node'; } /** * HTTP клиент для выполнения запросов к API * * Предоставляет методы для работы с REST API, включая обработку ошибок, * повторные попытки и таймауты. */ export class HttpClient { /** * Создает новый HTTP клиент * @param {string} baseURL Базовый URL для запросов * @param {HttpClientOptions} [options] Опции для HTTP клиента */ constructor(baseURL, options = {}) { this.baseURL = baseURL; this.options = { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...(options.headers || {}) }, timeout: options.timeout || 30000, retry: options.retry || { maxRetries: 3, retryDelay: 1000, maxRetryDelay: 10000, retryStatusCodes: [408, 429, 500, 502, 503, 504] }, httpsAgent: options.httpsAgent }; this.environment = getEnvironment(); // Создаем Axios инстанс с базовыми настройками this.axiosInstance = axios.create({ baseURL: this.baseURL, timeout: this.options.timeout, headers: this.options.headers, ...(this.environment === 'node' && this.options.httpsAgent ? { httpsAgent: this.options.httpsAgent } : {}) }); // Добавляем интерцептор для обработки ошибок и повторных попыток this.setupInterceptors(); } /** * Настраивает интерцепторы Axios */ setupInterceptors() { // Интерцептор для ответов this.axiosInstance.interceptors.response.use((response) => response.data, async (error) => { const { config, response } = error; // ✅ ЦЕНТРАЛИЗОВАННЫЙ МАППИНГ через ErrorMapper const mappedError = ErrorMapper.mapError(error); // RETRY LOGIC: Если нет конфига запроса или это уже повторный запрос, или skipRetry установлен if (!config || config._retryCount || config.skipRetry) { throw mappedError; } config._retryCount = config._retryCount || 0; const retryConfig = this.options.retry; // Проверяем, нужно ли делать повторную попытку для данного статуса const shouldRetry = response && retryConfig.retryStatusCodes?.includes(response.status) && config._retryCount < (retryConfig.maxRetries || 3); if (shouldRetry) { config._retryCount += 1; // Вычисляем время задержки перед повторной попыткой const delay = Math.min((retryConfig.retryDelay || 1000) * Math.pow(2, config._retryCount - 1), retryConfig.maxRetryDelay || 10000); // Ждем перед повторной попыткой await new Promise(resolve => setTimeout(resolve, delay)); // Делаем повторную попытку return this.axiosInstance(config); } // Если не нужно делать повторную попытку, выбрасываем типизированную ошибку throw mappedError; }); } /** * Выполняет HTTP запрос * @param {RequestOptions} options Опции запроса * @returns {Promise<T>} Ответ от API */ async request(options) { // 🔑 Динамическое получение токена перед запросом let authHeaders = {}; if (this.options.getAuthToken) { try { const token = await Promise.resolve(this.options.getAuthToken()); if (token) { authHeaders['Authorization'] = `Bearer ${token}`; } } catch (error) { // Если не удалось получить токен, продолжаем без него console.warn('⚠️ Failed to get auth token:', error); } } const axiosConfig = { url: options.url, method: options.method, data: options.data, params: options.params, headers: { ...this.options.headers, ...authHeaders, // ← Динамический токен имеет приоритет ...(options.headers || {}) }, timeout: options.timeout || this.options.timeout }; // Если указано не делать повторные попытки, добавляем специальный флаг if (options.noRetry) { axiosConfig.skipRetry = true; } try { return await this.axiosInstance.request(axiosConfig); } catch (error) { // ✅ Если это уже SDK ошибка от interceptor - пробрасываем как есть if (error instanceof BaseSDKError) { throw error; } // ✅ Иначе маппим через ErrorMapper (для ошибок вне interceptor) throw ErrorMapper.mapError(error); } } /** * Выполняет GET запрос * @param {string} url URL для запроса * @param {Record<string, any>} [params] Query параметры * @param {Record<string, string>} [headers] HTTP заголовки * @returns {Promise<T>} Ответ от API */ async get(url, params, headers) { return this.request({ url, method: 'GET', params, headers }); } /** * Выполняет POST запрос * @param {string} url URL для запроса * @param {any} [data] Данные для отправки * @param {Record<string, string>} [headers] HTTP заголовки * @returns {Promise<T>} Ответ от API */ async post(url, data, headers) { return this.request({ url, method: 'POST', data, headers }); } /** * Выполняет PUT запрос * @param {string} url URL для запроса * @param {any} [data] Данные для отправки * @param {Record<string, string>} [headers] HTTP заголовки * @returns {Promise<T>} Ответ от API */ async put(url, data, headers) { return this.request({ url, method: 'PUT', data, headers }); } /** * Выполняет DELETE запрос * @param {string} url URL для запроса * @param {Record<string, any>} [params] Query параметры * @param {Record<string, string>} [headers] HTTP заголовки * @returns {Promise<T>} Ответ от API */ async delete(url, params, headers) { return this.request({ url, method: 'DELETE', params, headers }); } /** * Выполняет PATCH запрос * @param {string} url URL для запроса * @param {any} [data] Данные для отправки * @param {Record<string, string>} [headers] HTTP заголовки * @returns {Promise<T>} Ответ от API */ async patch(url, data, headers) { return this.request({ url, method: 'PATCH', data, headers }); } /** * Выполняет POST запрос с потоковым ответом * @param {string} url URL для запроса * @param {any} [data] Данные для отправки * @param {Record<string, string>} [headers] HTTP заголовки * @returns {Promise<Response>} Объект Response с потоком данных */ async postStream(url, data, headers) { // Формируем полный URL правильно const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`; const requestHeaders = { 'Content-Type': 'application/json', ...this.options.headers, ...(headers || {}) }; // Для Node.js среды используем нативный fetch если доступен, иначе node-fetch if (this.environment === 'node') { // Проверяем доступность нативного fetch (Node.js 18+) if (typeof globalThis.fetch !== 'undefined') { return globalThis.fetch(fullUrl, { method: 'POST', headers: requestHeaders, body: JSON.stringify(data) }); } try { // Fallback на node-fetch для старых версий Node.js const { default: nodeFetch } = await import('node-fetch'); return nodeFetch(fullUrl, { method: 'POST', headers: requestHeaders, body: JSON.stringify(data) }); } catch (error) { console.error('Ошибка при загрузке node-fetch:', error); throw new Error('Для использования потоковой передачи в Node.js необходимо установить node-fetch или использовать Node.js 18+'); } } // Для браузера используем нативный fetch return fetch(fullUrl, { method: 'POST', headers: requestHeaders, body: JSON.stringify(data) }); } /** * Обрабатывает ошибки от Axios * @param {any} error Ошибка от Axios * @returns {Error} Обработанная ошибка */ handleError(error) { let enhancedError; let message; if (error.response) { // Ошибка от сервера с кодом ответа const { status, data } = error.response; // Улучшенное извлечение сообщения ошибки if (typeof data === 'string') { message = data; } else if (data && typeof data === 'object') { // Пробуем извлечь сообщение из разных возможных полей const rawMessage = data.message || data.error || data.errorMessage || data.msg; if (rawMessage) { // Если нашли сообщение, проверяем его тип if (typeof rawMessage === 'string') { message = rawMessage; } else if (typeof rawMessage === 'object') { // Если это объект, сериализуем его try { message = JSON.stringify(rawMessage); } catch (e) { message = String(rawMessage); } } else { // Для других типов просто конвертируем в строку message = String(rawMessage); } } else { // Если сообщение не найдено, сериализуем весь объект data try { message = JSON.stringify(data); } catch (e) { message = `[Ошибка сериализации ответа]`; } } } else { message = `HTTP ошибка ${status}`; } enhancedError = new Error(`[HTTP ${status}] ${message}`); enhancedError.status = status; enhancedError.data = data; enhancedError.isApiError = true; } else if (error.request) { // Запрос был сделан, но ответ не получен message = 'Нет ответа от сервера. Проверьте подключение к интернету.'; enhancedError = new Error(message); enhancedError.request = error.request; enhancedError.isNetworkError = true; } else { // Произошла ошибка при настройке запроса message = error.message || error.toString() || 'Произошла неизвестная ошибка'; enhancedError = new Error(message); enhancedError.isUnknownError = true; } // Сохраняем оригинальную ошибку для отладки enhancedError.originalError = error; // Переопределяем toString для лучшего логирования enhancedError.toString = function () { return this.message; }; // Пытаемся обработать ошибку через глобальный обработчик try { // eslint-disable-next-line @typescript-eslint/no-var-requires const { CodeSolverSDK } = require('../code-solver-sdk.js'); if (typeof CodeSolverSDK.handleError === 'function') { CodeSolverSDK.handleError(enhancedError); } } catch (e) { // Игнорируем ошибки при импорте или вызове обработчика } return enhancedError; } /** * Получает экземпляр Axios для тестирования */ get axiosConfig() { return this.axiosInstance; } /** * Получает базовый URL API * @returns {string} Базовый URL */ getBaseURL() { return this.baseURL; } } //# sourceMappingURL=http-client.js.map