| 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');
|
| }
|
|
|
| getLatestNews(limit = 20) {
|
| return this.get(`/api/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;
|
|
|