import { API_BASE_URL } from '../api'; import { getComParams } from './CommonParams'; import { ApiEndpoint, ApiEnvelope, ApiParams, HttpAbortedError, HttpError, HttpFailedError, HttpMethod, HttpParseError, HttpTimeoutError, } from './RequestTypes'; import LZString from 'lz-string'; export interface ApiCodec { encode(params: ApiParams): string; decode(payload: string): string; } export interface HttpClientOptions { baseUrl: string; timeoutMs?: number; retryCount?: number; retryDelayMs?: number; compress?: boolean; codec?: ApiCodec; } export class HttpClient { private readonly timeoutMs: number; private readonly retryCount: number; private readonly retryDelayMs: number; constructor(private readonly options: HttpClientOptions) { this.timeoutMs = options.timeoutMs ?? 10_000; this.retryCount = options.retryCount ?? 2; this.retryDelayMs = options.retryDelayMs ?? 500; } get(path: string, query?: ApiParams | any): Promise> { return this.request('GET', path, query); } post(path: string, body?: ApiParams | any): Promise> { return this.request('POST', path, body); } call(endpoint: ApiEndpoint, params?: ApiParams | any): Promise> { return this.request(endpoint.method, endpoint.path, params); } async request(method: HttpMethod, path: string, params: ApiParams = {}): Promise> { for (let attempt = 0; attempt <= this.retryCount; attempt += 1) { try { return await this.send(method, path, params); } catch (error) { console.error(error); if (attempt >= this.retryCount) { return { code: -1, data: null, }; } await this.delay(this.retryDelayMs); } } return null; } private send(method: HttpMethod, path: string, params: ApiParams): Promise> { const comParams = getComParams(); Object.assign(params, comParams); const url = this.resolveUrl(path); const useCompression = Boolean(this.options.compress && this.options.codec); const payload = useCompression ? { lz: this.options.codec!.encode(params) } : params; const requestUrl = method === 'GET' ? this.appendQuery(url, payload) : url; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(method, requestUrl, true); xhr.timeout = this.timeoutMs; xhr.setRequestHeader('Accept', 'application/json, text/plain, */*'); if (method === 'POST') { xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); } xhr.onload = () => { if (xhr.status < 200 || xhr.status >= 300) { reject(new HttpError(`HTTP ${xhr.status}`, xhr.status, requestUrl)); return; } try { let text = xhr.responseText; if (text && this.options.codec && !text.replace(/^\s+/, '').startsWith('{')) { text = this.options.codec.decode(text); } resolve(JSON.parse(text) as ApiEnvelope); } catch (error) { reject(new HttpParseError( `Invalid API response: ${error instanceof Error ? error.message : String(error)}`, xhr.status, requestUrl, )); } }; xhr.onerror = () => reject(new HttpFailedError('Network request failed', xhr.status, requestUrl)); xhr.ontimeout = () => reject(new HttpTimeoutError('Network request timed out', 0, requestUrl)); xhr.onabort = () => reject(new HttpAbortedError('Network request aborted', 0, requestUrl)); xhr.send(method === 'POST' ? JSON.stringify(payload) : null); }); } private resolveUrl(path: string): string { return `${this.options.baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`; } private appendQuery(url: string, params: ApiParams): string { const query: string[] = []; for (const key of Object.keys(params)) { const value = params[key]; if (value === undefined || value === null || value === '') continue; const encoded = typeof value === 'object' ? JSON.stringify(value) : String(value); query.push(`${encodeURIComponent(key)}=${encodeURIComponent(encoded)}`); } return query.length > 0 ? `${url}?${query.join('&')}` : url; } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } } /** * 全局 HTTP 请求对象 */ export const Http = new HttpClient({ baseUrl: API_BASE_URL, timeoutMs: 10_000, retryCount: 2, retryDelayMs: 1_000, compress: false, codec: { encode: function (params: ApiParams): string { return LZString.compressToBase64(JSON.stringify(params)); }, decode: function (payload: string): string { try { return LZString.decompressFromBase64(payload); } catch (error) { console.error(`api response decompress error: `, error); return payload; } } } });