Datasourceforcryptocurrency / static /js /api-resource-loader.js
Really-amin's picture
Upload 303 files
b068b76 verified
/**
* ═══════════════════════════════════════════════════════════════════
* 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
});
}
}