| | const DEFAULT_TTL = 60 * 1000; |
| |
|
| | class ApiClient { |
| | constructor() { |
| | const origin = window?.location?.origin ?? ''; |
| | this.baseURL = origin.replace(/\/$/, ''); |
| | this.cache = new Map(); |
| | this.requestLogs = []; |
| | this.errorLogs = []; |
| | this.logSubscribers = new Set(); |
| | this.errorSubscribers = new Set(); |
| | } |
| |
|
| | buildUrl(endpoint) { |
| | if (!endpoint.startsWith('/')) { |
| | return `${this.baseURL}/${endpoint}`; |
| | } |
| | return `${this.baseURL}${endpoint}`; |
| | } |
| |
|
| | notifyLog(entry) { |
| | this.requestLogs.push(entry); |
| | this.requestLogs = this.requestLogs.slice(-100); |
| | this.logSubscribers.forEach((cb) => cb(entry)); |
| | } |
| |
|
| | notifyError(entry) { |
| | this.errorLogs.push(entry); |
| | this.errorLogs = this.errorLogs.slice(-100); |
| | this.errorSubscribers.forEach((cb) => cb(entry)); |
| | } |
| |
|
| | onLog(callback) { |
| | this.logSubscribers.add(callback); |
| | return () => this.logSubscribers.delete(callback); |
| | } |
| |
|
| | onError(callback) { |
| | this.errorSubscribers.add(callback); |
| | return () => this.errorSubscribers.delete(callback); |
| | } |
| |
|
| | getLogs() { |
| | return [...this.requestLogs]; |
| | } |
| |
|
| | getErrors() { |
| | return [...this.errorLogs]; |
| | } |
| |
|
| | async request(method, endpoint, { body, cache = true, ttl = DEFAULT_TTL } = {}) { |
| | const url = this.buildUrl(endpoint); |
| | const cacheKey = `${method}:${url}`; |
| |
|
| | if (method === 'GET' && cache && this.cache.has(cacheKey)) { |
| | const cached = this.cache.get(cacheKey); |
| | if (Date.now() - cached.timestamp < ttl) { |
| | return { ok: true, data: cached.data, cached: true }; |
| | } |
| | } |
| |
|
| | const started = performance.now(); |
| | const randomId = (window.crypto && window.crypto.randomUUID && window.crypto.randomUUID()) |
| | || `${Date.now()}-${Math.random()}`; |
| | const entry = { |
| | id: randomId, |
| | method, |
| | endpoint, |
| | status: 'pending', |
| | duration: 0, |
| | time: new Date().toISOString(), |
| | }; |
| |
|
| | try { |
| | const response = await fetch(url, { |
| | method, |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: body ? JSON.stringify(body) : undefined, |
| | }); |
| |
|
| | const duration = performance.now() - started; |
| | entry.duration = Math.round(duration); |
| | entry.status = response.status; |
| |
|
| | const contentType = response.headers.get('content-type') || ''; |
| | let data = null; |
| | if (contentType.includes('application/json')) { |
| | data = await response.json(); |
| | } else if (contentType.includes('text')) { |
| | data = await response.text(); |
| | } |
| |
|
| | if (!response.ok) { |
| | const error = new Error((data && data.message) || response.statusText || 'Unknown error'); |
| | error.status = response.status; |
| | throw error; |
| | } |
| |
|
| | if (method === 'GET' && cache) { |
| | this.cache.set(cacheKey, { timestamp: Date.now(), data }); |
| | } |
| |
|
| | this.notifyLog({ ...entry, success: true }); |
| | return { ok: true, data }; |
| | } catch (error) { |
| | const duration = performance.now() - started; |
| | entry.duration = Math.round(duration); |
| | entry.status = error.status || 'error'; |
| | this.notifyLog({ ...entry, success: false, error: error.message }); |
| | this.notifyError({ |
| | message: error.message, |
| | endpoint, |
| | method, |
| | time: new Date().toISOString(), |
| | }); |
| | return { ok: false, error: error.message }; |
| | } |
| | } |
| |
|
| | get(endpoint, options) { |
| | return this.request('GET', endpoint, options); |
| | } |
| |
|
| | post(endpoint, body, options = {}) { |
| | return this.request('POST', endpoint, { ...options, body }); |
| | } |
| |
|
| | |
| | getHealth() { |
| | return this.get('/api/health'); |
| | } |
| |
|
| | getTopCoins(limit = 10) { |
| | return this.get(`/api/coins/top?limit=${limit}`); |
| | } |
| |
|
| | getCoinDetails(symbol) { |
| | return this.get(`/api/coins/${symbol}`); |
| | } |
| |
|
| | getMarketStats() { |
| | return this.get('/api/market/stats'); |
| | } |
| | |
| | async getLatestNews(limit = 20) { |
| | try { |
| | |
| | return await this.get(`/api/news/latest?limit=${limit}`); |
| | } catch (error) { |
| | console.warn('[APIClient] /api/news/latest failed, falling back to /news/latest', error); |
| | |
| | return await this.get(`/news/latest?limit=${limit}`); |
| | } |
| | } |
| |
|
| | getProviders() { |
| | return this.get('/api/providers'); |
| | } |
| |
|
| | getPriceChart(symbol, timeframe = '7d') { |
| | return this.get(`/api/charts/price/${symbol}?timeframe=${timeframe}`); |
| | } |
| |
|
| | analyzeChart(symbol, timeframe = '7d', indicators = []) { |
| | return this.post('/api/charts/analyze', { symbol, timeframe, indicators }); |
| | } |
| |
|
| | runQuery(payload) { |
| | return this.post('/api/query', payload); |
| | } |
| |
|
| | analyzeSentiment(payload) { |
| | return this.post('/api/sentiment/analyze', payload); |
| | } |
| |
|
| | summarizeNews(item) { |
| | return this.post('/api/news/summarize', item); |
| | } |
| |
|
| | getDatasetsList() { |
| | return this.get('/api/datasets/list'); |
| | } |
| |
|
| | getDatasetSample(name) { |
| | return this.get(`/api/datasets/sample?name=${encodeURIComponent(name)}`); |
| | } |
| |
|
| | getModelsList() { |
| | return this.get('/api/models/list'); |
| | } |
| |
|
| | testModel(payload) { |
| | return this.post('/api/models/test', payload); |
| | } |
| | } |
| |
|
| | const apiClient = new ApiClient(); |
| | export default apiClient; |
| |
|