| |
| |
| |
| |
| |
|
|
|
|
| class APIResourceLoader {
|
| constructor() {
|
| this.resources = {
|
| unified: null,
|
| ultimate: null,
|
| config: null
|
| };
|
| this.cache = new Map();
|
| this.initialized = false;
|
| this.failedResources = new Set();
|
| this.initPromise = null;
|
| }
|
|
|
| |
| |
|
|
| async init() {
|
|
|
| if (this.initPromise) {
|
| return this.initPromise;
|
| }
|
|
|
|
|
| if (this.initialized) {
|
| return this.resources;
|
| }
|
|
|
|
|
| this.initPromise = (async () => {
|
|
|
| try {
|
|
|
|
|
| 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 => {
|
|
|
| if (typeof text === 'string' && text.trim()) {
|
| return this.parseConfigText(text);
|
| }
|
| return null;
|
| })
|
| .catch(() => null)
|
| ]);
|
|
|
|
|
| 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');
|
| }
|
| }
|
|
|
|
|
| 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');
|
| }
|
| }
|
|
|
|
|
| if (config.status === 'fulfilled' && config.value) {
|
| this.resources.config = config.value;
|
|
|
| }
|
|
|
|
|
| this.initialized = true;
|
|
|
|
|
| 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) {
|
|
|
| this.initialized = true;
|
| return this.resources;
|
| } finally {
|
|
|
| this.initPromise = null;
|
| }
|
| })();
|
|
|
| return this.initPromise;
|
| }
|
|
|
| |
| |
|
|
| async loadResource(path) {
|
| const cacheKey = `resource_${path}`;
|
|
|
|
|
| if (this.cache.has(cacheKey)) {
|
| return this.cache.get(cacheKey);
|
| }
|
|
|
|
|
| if (this.failedResources && this.failedResources.has(path)) {
|
| return null;
|
| }
|
|
|
| try {
|
|
|
| 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 {
|
|
|
|
|
| const controller = new AbortController();
|
| const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
|
| let response = null;
|
| try {
|
| response = await fetch(endpoint, {
|
| signal: controller.signal
|
| });
|
| } catch (fetchError) {
|
|
|
|
|
| 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) {
|
|
|
| return null;
|
| }
|
| }
|
|
|
| return null;
|
| } catch (apiError) {
|
|
|
| return null;
|
| }
|
| }
|
|
|
|
|
| try {
|
|
|
| const controller = new AbortController();
|
| const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
|
| let response = null;
|
| try {
|
| response = await fetch(path, {
|
| signal: controller.signal
|
| });
|
| } catch (fetchError) {
|
|
|
| clearTimeout(timeoutId);
|
| this.failedResources.add(path);
|
| return null;
|
| }
|
| clearTimeout(timeoutId);
|
| if (!response || !response.ok) {
|
|
|
| if (response && response.status === 404) {
|
|
|
| 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) {
|
|
|
| if (path.endsWith('.txt')) {
|
| return await altResponse.text();
|
| }
|
| const data = await altResponse.json();
|
| this.cache.set(cacheKey, data);
|
| return data;
|
| }
|
| } catch (e) {
|
|
|
| }
|
| }
|
| }
|
|
|
| return null;
|
| }
|
|
|
|
|
| if (path.endsWith('.txt')) {
|
| return await response.text();
|
| }
|
|
|
| const data = await response.json();
|
| this.cache.set(cacheKey, data);
|
| return data;
|
| } catch (fileError) {
|
|
|
| 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) {
|
|
|
| }
|
| }
|
|
|
|
|
| this.failedResources.add(path);
|
| return null;
|
| }
|
| } catch (error) {
|
|
|
| this.failedResources.add(path);
|
|
|
|
|
|
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
|
|
| parseConfigText(text) {
|
| if (!text) return null;
|
|
|
|
|
| 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;
|
| }
|
|
|
| |
| |
|
|
| 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;
|
| }
|
|
|
| |
| |
|
|
| 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;
|
| }
|
|
|
| |
| |
|
|
| 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;
|
| }
|
|
|
| |
| |
|
|
| getRPCNodes() {
|
| if (this.resources.unified?.registry?.rpc_nodes) {
|
| return this.resources.unified.registry.rpc_nodes;
|
| }
|
| return [];
|
| }
|
|
|
| |
| |
|
|
| getBlockExplorers() {
|
| if (this.resources.unified?.registry?.block_explorers) {
|
| return this.resources.unified.registry.block_explorers;
|
| }
|
| return [];
|
| }
|
|
|
| |
| |
|
|
| searchAPIs(keyword) {
|
| const results = [];
|
| const lowerKeyword = keyword.toLowerCase();
|
|
|
|
|
| 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 });
|
| }
|
| }
|
| }
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
| |
| |
|
|
| getAPIById(id) {
|
|
|
| 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 };
|
| }
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
| |
| |
|
|
| 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
|
| };
|
| }
|
| }
|
|
|
|
|
| window.apiResourceLoader = new APIResourceLoader();
|
|
|
|
|
| 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(() => {
|
|
|
| });
|
| }
|
| }, { 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(() => {
|
|
|
| });
|
| }
|
| }
|
|
|
|
|