Spaces:
Runtime error
Runtime error
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * API RESOURCE LOADER | |
| * Loads and manages API resources from api-resources JSON files | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| */ | |
| class APIResourceLoader { | |
| constructor() { | |
| this.resources = { | |
| unified: null, | |
| ultimate: null, | |
| config: null | |
| }; | |
| this.cache = new Map(); | |
| this.initialized = false; | |
| this.failedResources = new Set(); // Track failed resources to prevent infinite retries | |
| this.initPromise = null; // Prevent multiple simultaneous init calls | |
| } | |
| /** | |
| * Initialize and load all API resource files | |
| */ | |
| async init() { | |
| // Return existing promise if already initializing | |
| if (this.initPromise) { | |
| return this.initPromise; | |
| } | |
| // Return immediately if already initialized | |
| if (this.initialized) { | |
| return this.resources; | |
| } | |
| // Create a promise that will be reused if init is called multiple times | |
| this.initPromise = (async () => { | |
| // Don't log initialization - only log if resources are successfully loaded | |
| try { | |
| // Load all resource files in parallel (gracefully handle failures silently) | |
| // Use Promise.allSettled to ensure all complete even if some fail | |
| const [unified, ultimate, config] = await Promise.allSettled([ | |
| this.loadResource('/api-resources/crypto_resources_unified_2025-11-11.json').catch(() => null), | |
| this.loadResource('/api-resources/ultimate_crypto_pipeline_2025_NZasinich.json').catch(() => null), | |
| this.loadResource('/api-resources/api-config-complete__1_.txt') | |
| .then(text => { | |
| // Handle both text and null responses | |
| if (typeof text === 'string' && text.trim()) { | |
| return this.parseConfigText(text); | |
| } | |
| return null; | |
| }) | |
| .catch(() => null) | |
| ]); | |
| // Only log if resources were successfully loaded | |
| if (unified.status === 'fulfilled' && unified.value) { | |
| this.resources.unified = unified.value; | |
| const count = this.resources.unified?.registry?.metadata?.total_entries || 0; | |
| if (count > 0) { | |
| console.log('[API Resource Loader] Unified resources loaded:', count, 'entries'); | |
| } | |
| } | |
| // Silently skip failures - resources are optional | |
| if (ultimate.status === 'fulfilled' && ultimate.value) { | |
| this.resources.ultimate = ultimate.value; | |
| const count = this.resources.ultimate?.total_sources || 0; | |
| if (count > 0) { | |
| console.log('[API Resource Loader] Ultimate resources loaded:', count, 'sources'); | |
| } | |
| } | |
| // Silently skip failures - resources are optional | |
| if (config.status === 'fulfilled' && config.value) { | |
| this.resources.config = config.value; | |
| // Config loaded silently (not critical enough to log) | |
| } | |
| // Silently skip failures - resources are optional | |
| this.initialized = true; | |
| // Only log success if resources were actually loaded | |
| const stats = this.getStats(); | |
| if (stats.unified.count > 0 || stats.ultimate.count > 0) { | |
| console.log('[API Resource Loader] Initialized successfully'); | |
| } | |
| return this.resources; | |
| } catch (error) { | |
| // Silently mark as initialized - resources are optional | |
| this.initialized = true; | |
| return this.resources; | |
| } finally { | |
| // Clear the promise so we can re-init if needed | |
| this.initPromise = null; | |
| } | |
| })(); | |
| return this.initPromise; | |
| } | |
| /** | |
| * Load a resource file (tries backend API first, then direct file) | |
| */ | |
| async loadResource(path) { | |
| const cacheKey = `resource_${path}`; | |
| // Check cache first | |
| if (this.cache.has(cacheKey)) { | |
| return this.cache.get(cacheKey); | |
| } | |
| // Don't retry if this resource has already failed | |
| if (this.failedResources && this.failedResources.has(path)) { | |
| return null; | |
| } | |
| try { | |
| // Try backend API endpoint first | |
| let endpoint = null; | |
| if (path.includes('crypto_resources_unified')) { | |
| endpoint = '/api/resources/unified'; | |
| } else if (path.includes('ultimate_crypto_pipeline')) { | |
| endpoint = '/api/resources/ultimate'; | |
| } | |
| if (endpoint) { | |
| try { | |
| // Use fetch with timeout and silent error handling | |
| // Suppress browser console errors by catching all errors | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 5000); | |
| let response = null; | |
| try { | |
| response = await fetch(endpoint, { | |
| signal: controller.signal | |
| }); | |
| } catch (fetchError) { | |
| // Completely suppress fetch errors - these are expected if server isn't running | |
| // Don't log, don't throw, just return null | |
| clearTimeout(timeoutId); | |
| return null; | |
| } | |
| clearTimeout(timeoutId); | |
| if (response && response.ok) { | |
| try { | |
| const result = await response.json(); | |
| if (result.success && result.data) { | |
| this.cache.set(cacheKey, result.data); | |
| return result.data; | |
| } | |
| } catch (jsonError) { | |
| // Silently handle JSON parse errors | |
| return null; | |
| } | |
| } | |
| // Silently fall through to direct file access if endpoint fails | |
| return null; | |
| } catch (apiError) { | |
| // Silently continue - resources are optional | |
| return null; | |
| } | |
| } | |
| // Fallback to direct file access | |
| try { | |
| // Suppress fetch errors for 404s - wrap in try-catch to prevent console errors | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 5000); | |
| let response = null; | |
| try { | |
| response = await fetch(path, { | |
| signal: controller.signal | |
| }); | |
| } catch (fetchError) { | |
| // Completely suppress browser console errors for optional resources | |
| clearTimeout(timeoutId); | |
| this.failedResources.add(path); | |
| return null; | |
| } | |
| clearTimeout(timeoutId); | |
| if (!response || !response.ok) { | |
| // File not found, try alternative paths | |
| if (response && response.status === 404) { | |
| // Try alternative paths silently | |
| const altPaths = [ | |
| path.replace('/api-resources/', '/static/api-resources/'), | |
| path.replace('/api-resources/', 'static/api-resources/'), | |
| path.replace('/api-resources/', 'api-resources/') | |
| ]; | |
| for (const altPath of altPaths) { | |
| try { | |
| const altResponse = await fetch(altPath).catch(() => null); | |
| if (altResponse && altResponse.ok) { | |
| // Check if it's a text file | |
| if (path.endsWith('.txt')) { | |
| return await altResponse.text(); | |
| } | |
| const data = await altResponse.json(); | |
| this.cache.set(cacheKey, data); | |
| return data; | |
| } | |
| } catch (e) { | |
| // Continue to next path | |
| } | |
| } | |
| } | |
| // Return null if all paths fail (not critical) | |
| return null; | |
| } | |
| // Check if it's a text file | |
| if (path.endsWith('.txt')) { | |
| return await response.text(); | |
| } | |
| const data = await response.json(); | |
| this.cache.set(cacheKey, data); | |
| return data; | |
| } catch (fileError) { | |
| // Last resort: try with /static/ prefix | |
| if (!path.startsWith('/static/') && !path.startsWith('static/')) { | |
| try { | |
| const staticPath = path.startsWith('/') ? `/static${path}` : `static/${path}`; | |
| const controller2 = new AbortController(); | |
| const timeoutId2 = setTimeout(() => controller2.abort(), 5000); | |
| const response = await fetch(staticPath, { | |
| signal: controller2.signal | |
| }).catch(() => null); | |
| clearTimeout(timeoutId2); | |
| if (response && response.ok) { | |
| if (path.endsWith('.txt')) { | |
| return await response.text(); | |
| } | |
| const data = await response.json(); | |
| this.cache.set(cacheKey, data); | |
| return data; | |
| } | |
| } catch (staticError) { | |
| // Ignore - will return null | |
| } | |
| } | |
| // Return null instead of throwing (not critical) | |
| // Mark as failed to prevent future retries | |
| this.failedResources.add(path); | |
| return null; | |
| } | |
| } catch (error) { | |
| // Mark as failed to prevent infinite retries | |
| this.failedResources.add(path); | |
| // Completely silent - resources are optional | |
| // Don't log anything - these are expected failures | |
| return null; | |
| } | |
| } | |
| /** | |
| * Parse config text file | |
| */ | |
| parseConfigText(text) { | |
| if (!text) return null; | |
| // Simple parsing - extract key-value pairs | |
| const config = {}; | |
| const lines = text.split('\n'); | |
| for (const line of lines) { | |
| const match = line.match(/^([^=]+)=(.*)$/); | |
| if (match) { | |
| config[match[1].trim()] = match[2].trim(); | |
| } | |
| } | |
| return config; | |
| } | |
| /** | |
| * Get all market data APIs | |
| */ | |
| getMarketDataAPIs() { | |
| const apis = []; | |
| if (this.resources.unified?.registry?.market_data_apis) { | |
| apis.push(...this.resources.unified.registry.market_data_apis); | |
| } | |
| if (this.resources.ultimate?.files?.[0]?.content?.resources) { | |
| const marketAPIs = this.resources.ultimate.files[0].content.resources.filter( | |
| r => r.category === 'Market Data' | |
| ); | |
| apis.push(...marketAPIs.map(r => ({ | |
| id: r.name.toLowerCase().replace(/\s+/g, '_'), | |
| name: r.name, | |
| base_url: r.url, | |
| auth: r.key ? { type: 'apiKeyQuery', key: r.key } : { type: 'none' }, | |
| rateLimit: r.rateLimit, | |
| notes: r.desc | |
| }))); | |
| } | |
| return apis; | |
| } | |
| /** | |
| * Get all news APIs | |
| */ | |
| getNewsAPIs() { | |
| const apis = []; | |
| if (this.resources.unified?.registry?.news_apis) { | |
| apis.push(...this.resources.unified.registry.news_apis); | |
| } | |
| if (this.resources.ultimate?.files?.[0]?.content?.resources) { | |
| const newsAPIs = this.resources.ultimate.files[0].content.resources.filter( | |
| r => r.category === 'News' | |
| ); | |
| apis.push(...newsAPIs.map(r => ({ | |
| id: r.name.toLowerCase().replace(/\s+/g, '_'), | |
| name: r.name, | |
| base_url: r.url, | |
| auth: r.key ? { type: 'apiKeyQuery', key: r.key } : { type: 'none' }, | |
| rateLimit: r.rateLimit, | |
| notes: r.desc | |
| }))); | |
| } | |
| return apis; | |
| } | |
| /** | |
| * Get all sentiment APIs | |
| */ | |
| getSentimentAPIs() { | |
| const apis = []; | |
| if (this.resources.unified?.registry?.sentiment_apis) { | |
| apis.push(...this.resources.unified.registry.sentiment_apis); | |
| } | |
| if (this.resources.ultimate?.files?.[0]?.content?.resources) { | |
| const sentimentAPIs = this.resources.ultimate.files[0].content.resources.filter( | |
| r => r.category === 'Sentiment' | |
| ); | |
| apis.push(...sentimentAPIs.map(r => ({ | |
| id: r.name.toLowerCase().replace(/\s+/g, '_'), | |
| name: r.name, | |
| base_url: r.url, | |
| auth: r.key ? { type: 'apiKeyQuery', key: r.key } : { type: 'none' }, | |
| rateLimit: r.rateLimit, | |
| notes: r.desc | |
| }))); | |
| } | |
| return apis; | |
| } | |
| /** | |
| * Get all RPC nodes | |
| */ | |
| getRPCNodes() { | |
| if (this.resources.unified?.registry?.rpc_nodes) { | |
| return this.resources.unified.registry.rpc_nodes; | |
| } | |
| return []; | |
| } | |
| /** | |
| * Get all block explorers | |
| */ | |
| getBlockExplorers() { | |
| if (this.resources.unified?.registry?.block_explorers) { | |
| return this.resources.unified.registry.block_explorers; | |
| } | |
| return []; | |
| } | |
| /** | |
| * Search APIs by keyword | |
| */ | |
| searchAPIs(keyword) { | |
| const results = []; | |
| const lowerKeyword = keyword.toLowerCase(); | |
| // Search in unified resources | |
| if (this.resources.unified?.registry) { | |
| const categories = ['market_data_apis', 'news_apis', 'sentiment_apis', 'rpc_nodes', 'block_explorers']; | |
| for (const category of categories) { | |
| const items = this.resources.unified.registry[category] || []; | |
| for (const item of items) { | |
| if (item.name?.toLowerCase().includes(lowerKeyword) || | |
| item.id?.toLowerCase().includes(lowerKeyword) || | |
| item.base_url?.toLowerCase().includes(lowerKeyword)) { | |
| results.push({ ...item, category }); | |
| } | |
| } | |
| } | |
| } | |
| // Search in ultimate resources | |
| if (this.resources.ultimate?.files?.[0]?.content?.resources) { | |
| for (const resource of this.resources.ultimate.files[0].content.resources) { | |
| if (resource.name?.toLowerCase().includes(lowerKeyword) || | |
| resource.desc?.toLowerCase().includes(lowerKeyword) || | |
| resource.url?.toLowerCase().includes(lowerKeyword)) { | |
| results.push({ | |
| id: resource.name.toLowerCase().replace(/\s+/g, '_'), | |
| name: resource.name, | |
| base_url: resource.url, | |
| category: resource.category, | |
| auth: resource.key ? { type: 'apiKeyQuery', key: resource.key } : { type: 'none' }, | |
| rateLimit: resource.rateLimit, | |
| notes: resource.desc | |
| }); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| /** | |
| * Get API by ID | |
| */ | |
| getAPIById(id) { | |
| // Search in unified resources | |
| if (this.resources.unified?.registry) { | |
| const categories = ['market_data_apis', 'news_apis', 'sentiment_apis', 'rpc_nodes', 'block_explorers']; | |
| for (const category of categories) { | |
| const items = this.resources.unified.registry[category] || []; | |
| const found = items.find(item => item.id === id); | |
| if (found) return { ...found, category }; | |
| } | |
| } | |
| // Search in ultimate resources | |
| if (this.resources.ultimate?.files?.[0]?.content?.resources) { | |
| const found = this.resources.ultimate.files[0].content.resources.find( | |
| r => r.name.toLowerCase().replace(/\s+/g, '_') === id | |
| ); | |
| if (found) { | |
| return { | |
| id: found.name.toLowerCase().replace(/\s+/g, '_'), | |
| name: found.name, | |
| base_url: found.url, | |
| category: found.category, | |
| auth: found.key ? { type: 'apiKeyQuery', key: found.key } : { type: 'none' }, | |
| rateLimit: found.rateLimit, | |
| notes: found.desc | |
| }; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Get statistics | |
| */ | |
| getStats() { | |
| return { | |
| unified: { | |
| count: this.resources.unified?.registry?.metadata?.total_entries || 0, | |
| market: this.resources.unified?.registry?.market_data_apis?.length || 0, | |
| news: this.resources.unified?.registry?.news_apis?.length || 0, | |
| sentiment: this.resources.unified?.registry?.sentiment_apis?.length || 0, | |
| rpc: this.resources.unified?.registry?.rpc_nodes?.length || 0, | |
| explorers: this.resources.unified?.registry?.block_explorers?.length || 0 | |
| }, | |
| ultimate: { | |
| count: this.resources.ultimate?.total_sources || 0, | |
| loaded: this.resources.ultimate?.files?.[0]?.content?.resources?.length || 0 | |
| }, | |
| initialized: this.initialized | |
| }; | |
| } | |
| } | |
| // Initialize global instance | |
| window.apiResourceLoader = new APIResourceLoader(); | |
| // Auto-initialize when DOM is ready (only once, prevent infinite retries) | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => { | |
| if (!window.apiResourceLoader.initialized && !window.apiResourceLoader.initPromise) { | |
| window.apiResourceLoader.init().then(() => { | |
| const stats = window.apiResourceLoader.getStats(); | |
| if (stats.unified.count > 0 || stats.ultimate.count > 0) { | |
| console.log('[API Resource Loader] Ready!', stats); | |
| } | |
| }).catch(() => { | |
| // Silent fail - resources are optional | |
| }); | |
| } | |
| }, { once: true }); | |
| } else { | |
| if (!window.apiResourceLoader.initialized && !window.apiResourceLoader.initPromise) { | |
| window.apiResourceLoader.init().then(() => { | |
| const stats = window.apiResourceLoader.getStats(); | |
| if (stats.unified.count > 0 || stats.ultimate.count > 0) { | |
| console.log('[API Resource Loader] Ready!', stats); | |
| } | |
| }).catch(() => { | |
| // Silent fail - resources are optional | |
| }); | |
| } | |
| } | |