| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | 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(() => {
|
| |
|
| | });
|
| | }
|
| | }
|
| |
|
| |
|