mirror of
https://github.com/basicswap/basicswap.git
synced 2026-05-09 07:52:13 +02:00
Refactoring + various fixes. (#285)
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
const ApiManager = (function() {
|
||||
|
||||
const state = {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
const config = {
|
||||
requestTimeout: 60000,
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200
|
||||
},
|
||||
cryptocompare: {
|
||||
requestsPerMinute: 30,
|
||||
minInterval: 2000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rateLimiter = {
|
||||
lastRequestTime: {},
|
||||
minRequestInterval: {
|
||||
coingecko: 1200,
|
||||
cryptocompare: 2000
|
||||
},
|
||||
requestQueue: {},
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
|
||||
canMakeRequest: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return (now - lastRequest) >= this.minRequestInterval[apiName];
|
||||
},
|
||||
|
||||
updateLastRequestTime: function(apiName) {
|
||||
this.lastRequestTime[apiName] = Date.now();
|
||||
},
|
||||
|
||||
getWaitTime: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
|
||||
},
|
||||
|
||||
queueRequest: async function(apiName, requestFn, retryCount = 0) {
|
||||
if (!this.requestQueue[apiName]) {
|
||||
this.requestQueue[apiName] = Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.requestQueue[apiName];
|
||||
|
||||
const executeRequest = async () => {
|
||||
const waitTime = this.getWaitTime(apiName);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
}
|
||||
|
||||
try {
|
||||
this.updateLastRequestTime(apiName);
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
|
||||
retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
|
||||
apiName,
|
||||
retryCount,
|
||||
error: error.message
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
this.requestQueue[apiName] = executeRequest();
|
||||
return await this.requestQueue[apiName];
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') ||
|
||||
error.message.includes('timeout') ||
|
||||
error.name === 'NetworkError') {
|
||||
const cacheKey = `coinData_${apiName}`;
|
||||
try {
|
||||
const cachedData = JSON.parse(localStorage.getItem(cacheKey));
|
||||
if (cachedData && cachedData.value) {
|
||||
return cachedData.value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error accessing cached data:', e);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
config,
|
||||
rateLimiter,
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (state.isInitialized) {
|
||||
console.warn('[ApiManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (options.config) {
|
||||
Object.assign(config, options.config);
|
||||
}
|
||||
|
||||
if (config.rateLimits) {
|
||||
Object.keys(config.rateLimits).forEach(api => {
|
||||
if (config.rateLimits[api].minInterval) {
|
||||
rateLimiter.minRequestInterval[api] = config.rateLimits[api].minInterval;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (config.retryDelays) {
|
||||
rateLimiter.retryDelays = [...config.retryDelays];
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('apiManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
state.isInitialized = true;
|
||||
console.log('ApiManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
signal: AbortSignal.timeout(config.requestTimeout)
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Request failed for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
makePostRequest: async function(url, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/json/readurl', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
headers: headers
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.Error) {
|
||||
reject(new Error(data.Error));
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Request failed for ${url}:`, error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) {
|
||||
if (!Array.isArray(coins)) {
|
||||
coins = [coins];
|
||||
}
|
||||
|
||||
return this.makeRequest('/json/coinprices', 'POST', {}, {
|
||||
coins: Array.isArray(coins) ? coins.join(',') : coins,
|
||||
source: source,
|
||||
ttl: ttl
|
||||
});
|
||||
},
|
||||
|
||||
fetchCoinGeckoData: async function() {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
const coins = (window.config && window.config.coins) ?
|
||||
window.config.coins
|
||||
.filter(coin => coin.usesCoinGecko)
|
||||
.map(coin => coin.name)
|
||||
.join(',') :
|
||||
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred';
|
||||
|
||||
//console.log('Fetching coin prices for:', coins);
|
||||
const response = await this.fetchCoinPrices(coins);
|
||||
|
||||
//console.log('Full API response:', response);
|
||||
|
||||
if (!response || typeof response !== 'object') {
|
||||
throw new Error('Invalid response type');
|
||||
}
|
||||
|
||||
if (!response.rates || typeof response.rates !== 'object' || Object.keys(response.rates).length === 0) {
|
||||
throw new Error('No valid rates found in response');
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchCoinGeckoData:', {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchVolumeData: async function() {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
const coins = (window.config && window.config.coins) ?
|
||||
window.config.coins
|
||||
.filter(coin => coin.usesCoinGecko)
|
||||
.map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name)
|
||||
.join(',') :
|
||||
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred';
|
||||
|
||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
|
||||
|
||||
const response = await this.makePostRequest(url, {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Accept': 'application/json'
|
||||
});
|
||||
|
||||
const volumeData = {};
|
||||
Object.entries(response).forEach(([coinId, data]) => {
|
||||
if (data && data.usd_24h_vol) {
|
||||
volumeData[coinId] = {
|
||||
total_volume: data.usd_24h_vol,
|
||||
price_change_percentage_24h: data.usd_24h_change || 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return volumeData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching volume data:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchCryptoCompareData: function(coin) {
|
||||
return this.rateLimiter.queueRequest('cryptocompare', async () => {
|
||||
try {
|
||||
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
|
||||
const url = `https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${apiKey}`;
|
||||
const headers = {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
return await this.makePostRequest(url, headers);
|
||||
} catch (error) {
|
||||
console.error(`CryptoCompare request failed for ${coin}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
|
||||
if (!Array.isArray(coinSymbols)) {
|
||||
coinSymbols = [coinSymbols];
|
||||
}
|
||||
|
||||
const results = {};
|
||||
const fetchPromises = coinSymbols.map(async coin => {
|
||||
if (coin === 'WOW') {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
const url = `https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1`;
|
||||
try {
|
||||
const response = await this.makePostRequest(url);
|
||||
if (response && response.prices) {
|
||||
results[coin] = response.prices;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching CoinGecko data for WOW:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return this.rateLimiter.queueRequest('cryptocompare', async () => {
|
||||
try {
|
||||
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
|
||||
let url;
|
||||
|
||||
if (resolution === 'day') {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
|
||||
} else if (resolution === 'year') {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
|
||||
} else {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=180&api_key=${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await this.makePostRequest(url);
|
||||
if (response.Response === "Error") {
|
||||
console.error(`API Error for ${coin}:`, response.Message);
|
||||
throw new Error(response.Message);
|
||||
} else if (response.Data && response.Data.Data) {
|
||||
results[coin] = response.Data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(fetchPromises);
|
||||
return results;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
// Clear any pending requests or resources
|
||||
rateLimiter.requestQueue = {};
|
||||
rateLimiter.lastRequestTime = {};
|
||||
state.isInitialized = false;
|
||||
console.log('ApiManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
function getCoinBackendId(coinName) {
|
||||
const nameMap = {
|
||||
'bitcoin-cash': 'bitcoincash',
|
||||
'bitcoin cash': 'bitcoincash',
|
||||
'firo': 'zcoin',
|
||||
'zcoin': 'zcoin',
|
||||
'bitcoincash': 'bitcoin-cash'
|
||||
};
|
||||
return nameMap[coinName.toLowerCase()] || coinName.toLowerCase();
|
||||
}
|
||||
|
||||
window.Api = ApiManager;
|
||||
window.ApiManager = ApiManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.apiManagerInitialized) {
|
||||
ApiManager.initialize();
|
||||
window.apiManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
|
||||
console.log('ApiManager initialized');
|
||||
@@ -0,0 +1,535 @@
|
||||
const CacheManager = (function() {
|
||||
const defaults = window.config?.cacheConfig?.storage || {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200,
|
||||
defaultTTL: 5 * 60 * 1000
|
||||
};
|
||||
|
||||
const PRICES_CACHE_KEY = 'crypto_prices_unified';
|
||||
|
||||
const CACHE_KEY_PATTERNS = [
|
||||
'coinData_',
|
||||
'chartData_',
|
||||
'historical_',
|
||||
'rates_',
|
||||
'prices_',
|
||||
'offers_',
|
||||
'fallback_',
|
||||
'volumeData'
|
||||
];
|
||||
|
||||
const isCacheKey = (key) => {
|
||||
return CACHE_KEY_PATTERNS.some(pattern => key.startsWith(pattern)) ||
|
||||
key === 'coinGeckoOneLiner' ||
|
||||
key === PRICES_CACHE_KEY;
|
||||
};
|
||||
|
||||
const isLocalStorageAvailable = () => {
|
||||
try {
|
||||
const testKey = '__storage_test__';
|
||||
localStorage.setItem(testKey, testKey);
|
||||
localStorage.removeItem(testKey);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let storageAvailable = isLocalStorageAvailable();
|
||||
|
||||
const memoryCache = new Map();
|
||||
|
||||
if (!storageAvailable) {
|
||||
console.warn('localStorage is not available. Using in-memory cache instead.');
|
||||
}
|
||||
|
||||
const cacheAPI = {
|
||||
getTTL: function(resourceType) {
|
||||
const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
|
||||
return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
|
||||
},
|
||||
|
||||
set: function(key, value, resourceTypeOrCustomTtl = null) {
|
||||
try {
|
||||
this.cleanup();
|
||||
|
||||
if (!value) {
|
||||
console.warn('Attempted to cache null/undefined value for key:', key);
|
||||
return false;
|
||||
}
|
||||
|
||||
let ttl;
|
||||
if (typeof resourceTypeOrCustomTtl === 'string') {
|
||||
ttl = this.getTTL(resourceTypeOrCustomTtl);
|
||||
} else if (typeof resourceTypeOrCustomTtl === 'number') {
|
||||
ttl = resourceTypeOrCustomTtl;
|
||||
} else {
|
||||
ttl = window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
|
||||
}
|
||||
|
||||
const item = {
|
||||
value: value,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + ttl
|
||||
};
|
||||
|
||||
let serializedItem;
|
||||
try {
|
||||
serializedItem = JSON.stringify(item);
|
||||
} catch (e) {
|
||||
console.error('Failed to serialize cache item:', e);
|
||||
return false;
|
||||
}
|
||||
|
||||
const itemSize = new Blob([serializedItem]).size;
|
||||
if (itemSize > defaults.maxSizeBytes) {
|
||||
console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
localStorage.setItem(key, serializedItem);
|
||||
return true;
|
||||
} catch (storageError) {
|
||||
if (storageError.name === 'QuotaExceededError') {
|
||||
this.cleanup(true);
|
||||
try {
|
||||
localStorage.setItem(key, serializedItem);
|
||||
return true;
|
||||
} catch (retryError) {
|
||||
console.error('Storage quota exceeded even after cleanup:', retryError);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to quota issues');
|
||||
memoryCache.set(key, item);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
console.error('localStorage error:', storageError);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
memoryCache.set(key, item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memoryCache.set(key, item);
|
||||
if (memoryCache.size > defaults.maxItems) {
|
||||
const keysToDelete = Array.from(memoryCache.keys())
|
||||
.filter(k => isCacheKey(k))
|
||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
||||
.slice(0, Math.floor(memoryCache.size * 0.2)); // Remove oldest 20%
|
||||
|
||||
keysToDelete.forEach(k => memoryCache.delete(k));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Cache set error:', error);
|
||||
try {
|
||||
memoryCache.set(key, {
|
||||
value: value,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + (window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL)
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Memory cache set error:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
try {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
if (itemStr) {
|
||||
let item;
|
||||
try {
|
||||
item = JSON.parse(itemStr);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse cached item:', parseError);
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!item || typeof item.expiresAt !== 'number' || !Object.prototype.hasOwnProperty.call(item, 'value')) {
|
||||
console.warn('Invalid cache item structure for key:', key);
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now < item.expiresAt) {
|
||||
return {
|
||||
value: item.value,
|
||||
remainingTime: item.expiresAt - now
|
||||
};
|
||||
}
|
||||
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("localStorage access error:", error);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
}
|
||||
}
|
||||
|
||||
if (memoryCache.has(key)) {
|
||||
const item = memoryCache.get(key);
|
||||
const now = Date.now();
|
||||
|
||||
if (now < item.expiresAt) {
|
||||
return {
|
||||
value: item.value,
|
||||
remainingTime: item.expiresAt - now
|
||||
};
|
||||
} else {
|
||||
|
||||
memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Cache retrieval error:", error);
|
||||
try {
|
||||
if (storageAvailable) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
memoryCache.delete(key);
|
||||
} catch (removeError) {
|
||||
console.error("Failed to remove invalid cache entry:", removeError);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
isValid: function(key) {
|
||||
return this.get(key) !== null;
|
||||
},
|
||||
|
||||
cleanup: function(aggressive = false) {
|
||||
const now = Date.now();
|
||||
let totalSize = 0;
|
||||
let itemCount = 0;
|
||||
const items = [];
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!isCacheKey(key)) continue;
|
||||
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
const size = new Blob([itemStr]).size;
|
||||
const item = JSON.parse(itemStr);
|
||||
|
||||
if (now >= item.expiresAt) {
|
||||
localStorage.removeItem(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
key,
|
||||
size,
|
||||
expiresAt: item.expiresAt,
|
||||
timestamp: item.timestamp
|
||||
});
|
||||
|
||||
totalSize += size;
|
||||
itemCount++;
|
||||
} catch (error) {
|
||||
console.error("Error processing cache item:", error);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (aggressive || totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) {
|
||||
items.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
while ((totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) && items.length > 0) {
|
||||
const item = items.pop();
|
||||
try {
|
||||
localStorage.removeItem(item.key);
|
||||
totalSize -= item.size;
|
||||
itemCount--;
|
||||
} catch (error) {
|
||||
console.error("Error removing cache item:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during localStorage cleanup:", error);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
}
|
||||
}
|
||||
|
||||
const expiredKeys = [];
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (now >= item.expiresAt) {
|
||||
expiredKeys.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
expiredKeys.forEach(key => memoryCache.delete(key));
|
||||
|
||||
if (aggressive && memoryCache.size > defaults.maxItems / 2) {
|
||||
const keysToDelete = Array.from(memoryCache.keys())
|
||||
.filter(key => isCacheKey(key))
|
||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
||||
.slice(0, Math.floor(memoryCache.size * 0.3)); // Remove oldest 30% during aggressive cleanup
|
||||
|
||||
keysToDelete.forEach(key => memoryCache.delete(key));
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize,
|
||||
itemCount,
|
||||
memoryCacheSize: memoryCache.size,
|
||||
cleaned: items.length,
|
||||
storageAvailable
|
||||
};
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
const keys = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (isCacheKey(key)) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache item:", error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error clearing localStorage cache:", error);
|
||||
storageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
Array.from(memoryCache.keys())
|
||||
.filter(key => isCacheKey(key))
|
||||
.forEach(key => memoryCache.delete(key));
|
||||
|
||||
console.log("Cache cleared successfully");
|
||||
return true;
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
let totalSize = 0;
|
||||
let itemCount = 0;
|
||||
let expiredCount = 0;
|
||||
const now = Date.now();
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!isCacheKey(key)) continue;
|
||||
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
const size = new Blob([itemStr]).size;
|
||||
const item = JSON.parse(itemStr);
|
||||
|
||||
totalSize += size;
|
||||
itemCount++;
|
||||
|
||||
if (now >= item.expiresAt) {
|
||||
expiredCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting cache stats:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting localStorage stats:", error);
|
||||
storageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
let memoryCacheSize = 0;
|
||||
let memoryCacheItems = 0;
|
||||
let memoryCacheExpired = 0;
|
||||
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (isCacheKey(key)) {
|
||||
memoryCacheItems++;
|
||||
if (now >= item.expiresAt) {
|
||||
memoryCacheExpired++;
|
||||
}
|
||||
try {
|
||||
memoryCacheSize += new Blob([JSON.stringify(item)]).size;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
|
||||
itemCount,
|
||||
expiredCount,
|
||||
utilization: ((totalSize / defaults.maxSizeBytes) * 100).toFixed(1) + '%',
|
||||
memoryCacheItems,
|
||||
memoryCacheExpired,
|
||||
memoryCacheSizeKB: (memoryCacheSize / 1024).toFixed(2),
|
||||
storageType: storageAvailable ? 'localStorage' : 'memory'
|
||||
};
|
||||
},
|
||||
|
||||
checkStorage: function() {
|
||||
const wasAvailable = storageAvailable;
|
||||
storageAvailable = isLocalStorageAvailable();
|
||||
|
||||
if (storageAvailable && !wasAvailable && memoryCache.size > 0) {
|
||||
console.log('localStorage is now available. Migrating memory cache...');
|
||||
let migratedCount = 0;
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (isCacheKey(key)) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(item));
|
||||
memoryCache.delete(key);
|
||||
migratedCount++;
|
||||
} catch (e) {
|
||||
if (e.name === 'QuotaExceededError') {
|
||||
console.warn('Storage quota exceeded during migration. Keeping items in memory cache.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Migrated ${migratedCount} items from memory cache to localStorage.`);
|
||||
}
|
||||
|
||||
return {
|
||||
available: storageAvailable,
|
||||
type: storageAvailable ? 'localStorage' : 'memory'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
...cacheAPI,
|
||||
|
||||
setPrices: function(priceData, customTtl = null) {
|
||||
return this.set(PRICES_CACHE_KEY, priceData,
|
||||
customTtl || (typeof customTtl === 'undefined' ? 'prices' : null));
|
||||
},
|
||||
|
||||
getPrices: function() {
|
||||
return this.get(PRICES_CACHE_KEY);
|
||||
},
|
||||
|
||||
getCoinPrice: function(symbol) {
|
||||
const prices = this.getPrices();
|
||||
if (!prices || !prices.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedSymbol = symbol.toLowerCase();
|
||||
return prices.value[normalizedSymbol] || null;
|
||||
},
|
||||
|
||||
getCompatiblePrices: function(format) {
|
||||
const prices = this.getPrices();
|
||||
if (!prices || !prices.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch(format) {
|
||||
case 'rates':
|
||||
const ratesFormat = {};
|
||||
Object.entries(prices.value).forEach(([coin, data]) => {
|
||||
const coinKey = coin.replace(/-/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.replace(' ', '-');
|
||||
|
||||
ratesFormat[coinKey] = {
|
||||
usd: data.price || data.usd,
|
||||
btc: data.price_btc || data.btc
|
||||
};
|
||||
});
|
||||
return {
|
||||
value: ratesFormat,
|
||||
remainingTime: prices.remainingTime
|
||||
};
|
||||
|
||||
case 'coinGecko':
|
||||
const geckoFormat = {};
|
||||
Object.entries(prices.value).forEach(([coin, data]) => {
|
||||
const symbol = this.getSymbolFromCoinId(coin);
|
||||
if (symbol) {
|
||||
geckoFormat[symbol.toLowerCase()] = {
|
||||
current_price: data.price || data.usd,
|
||||
price_btc: data.price_btc || data.btc,
|
||||
total_volume: data.total_volume,
|
||||
price_change_percentage_24h: data.price_change_percentage_24h,
|
||||
displayName: symbol
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
value: geckoFormat,
|
||||
remainingTime: prices.remainingTime
|
||||
};
|
||||
|
||||
default:
|
||||
return prices;
|
||||
}
|
||||
},
|
||||
|
||||
getSymbolFromCoinId: function(coinId) {
|
||||
const symbolMap = {
|
||||
'bitcoin': 'BTC',
|
||||
'litecoin': 'LTC',
|
||||
'monero': 'XMR',
|
||||
'particl': 'PART',
|
||||
'pivx': 'PIVX',
|
||||
'firo': 'FIRO',
|
||||
'zcoin': 'FIRO',
|
||||
'dash': 'DASH',
|
||||
'decred': 'DCR',
|
||||
'wownero': 'WOW',
|
||||
'bitcoin-cash': 'BCH',
|
||||
'dogecoin': 'DOGE'
|
||||
};
|
||||
|
||||
return symbolMap[coinId] || null;
|
||||
}
|
||||
};
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('cacheManager', publicAPI, (cm) => {
|
||||
cm.clear();
|
||||
});
|
||||
}
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.CacheManager = CacheManager;
|
||||
|
||||
|
||||
//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
|
||||
console.log('CacheManager initialized');
|
||||
@@ -0,0 +1,270 @@
|
||||
const CleanupManager = (function() {
|
||||
|
||||
const state = {
|
||||
eventListeners: [],
|
||||
timeouts: [],
|
||||
intervals: [],
|
||||
animationFrames: [],
|
||||
resources: new Map(),
|
||||
debug: false
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (state.debug) {
|
||||
console.log(`[CleanupManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
addListener: function(element, type, handler, options = false) {
|
||||
if (!element) {
|
||||
log('Warning: Attempted to add listener to null/undefined element');
|
||||
return handler;
|
||||
}
|
||||
|
||||
element.addEventListener(type, handler, options);
|
||||
state.eventListeners.push({ element, type, handler, options });
|
||||
log(`Added ${type} listener to`, element);
|
||||
return handler;
|
||||
},
|
||||
|
||||
setTimeout: function(callback, delay) {
|
||||
const id = window.setTimeout(callback, delay);
|
||||
state.timeouts.push(id);
|
||||
log(`Created timeout ${id} with ${delay}ms delay`);
|
||||
return id;
|
||||
},
|
||||
|
||||
setInterval: function(callback, delay) {
|
||||
const id = window.setInterval(callback, delay);
|
||||
state.intervals.push(id);
|
||||
log(`Created interval ${id} with ${delay}ms delay`);
|
||||
return id;
|
||||
},
|
||||
|
||||
requestAnimationFrame: function(callback) {
|
||||
const id = window.requestAnimationFrame(callback);
|
||||
state.animationFrames.push(id);
|
||||
log(`Requested animation frame ${id}`);
|
||||
return id;
|
||||
},
|
||||
|
||||
registerResource: function(type, resource, cleanupFn) {
|
||||
const id = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
state.resources.set(id, { resource, cleanupFn });
|
||||
log(`Registered custom resource ${id} of type ${type}`);
|
||||
return id;
|
||||
},
|
||||
|
||||
unregisterResource: function(id) {
|
||||
const resourceInfo = state.resources.get(id);
|
||||
if (resourceInfo) {
|
||||
try {
|
||||
resourceInfo.cleanupFn(resourceInfo.resource);
|
||||
state.resources.delete(id);
|
||||
log(`Unregistered and cleaned up resource ${id}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
log(`Resource ${id} not found`);
|
||||
return false;
|
||||
},
|
||||
|
||||
clearTimeout: function(id) {
|
||||
const index = state.timeouts.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.clearTimeout(id);
|
||||
state.timeouts.splice(index, 1);
|
||||
log(`Cleared timeout ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
clearInterval: function(id) {
|
||||
const index = state.intervals.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.clearInterval(id);
|
||||
state.intervals.splice(index, 1);
|
||||
log(`Cleared interval ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
cancelAnimationFrame: function(id) {
|
||||
const index = state.animationFrames.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.cancelAnimationFrame(id);
|
||||
state.animationFrames.splice(index, 1);
|
||||
log(`Cancelled animation frame ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
removeListener: function(element, type, handler, options = false) {
|
||||
if (!element) return;
|
||||
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
log(`Removed ${type} listener from`, element);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
|
||||
state.eventListeners = state.eventListeners.filter(
|
||||
listener => !(listener.element === element &&
|
||||
listener.type === type &&
|
||||
listener.handler === handler)
|
||||
);
|
||||
},
|
||||
|
||||
removeListenersByElement: function(element) {
|
||||
if (!element) return;
|
||||
|
||||
const listenersToRemove = state.eventListeners.filter(
|
||||
listener => listener.element === element
|
||||
);
|
||||
|
||||
listenersToRemove.forEach(({ element, type, handler, options }) => {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
log(`Removed ${type} listener from`, element);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
state.eventListeners = state.eventListeners.filter(
|
||||
listener => listener.element !== element
|
||||
);
|
||||
},
|
||||
|
||||
clearAllTimeouts: function() {
|
||||
state.timeouts.forEach(id => {
|
||||
window.clearTimeout(id);
|
||||
});
|
||||
const count = state.timeouts.length;
|
||||
state.timeouts = [];
|
||||
log(`Cleared all timeouts (${count})`);
|
||||
},
|
||||
|
||||
clearAllIntervals: function() {
|
||||
state.intervals.forEach(id => {
|
||||
window.clearInterval(id);
|
||||
});
|
||||
const count = state.intervals.length;
|
||||
state.intervals = [];
|
||||
log(`Cleared all intervals (${count})`);
|
||||
},
|
||||
|
||||
clearAllAnimationFrames: function() {
|
||||
state.animationFrames.forEach(id => {
|
||||
window.cancelAnimationFrame(id);
|
||||
});
|
||||
const count = state.animationFrames.length;
|
||||
state.animationFrames = [];
|
||||
log(`Cancelled all animation frames (${count})`);
|
||||
},
|
||||
|
||||
clearAllResources: function() {
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
state.resources.forEach((resourceInfo, id) => {
|
||||
try {
|
||||
resourceInfo.cleanupFn(resourceInfo.resource);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
|
||||
errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
state.resources.clear();
|
||||
log(`Cleared all custom resources (${successCount} success, ${errorCount} errors)`);
|
||||
},
|
||||
|
||||
clearAllListeners: function() {
|
||||
state.eventListeners.forEach(({ element, type, handler, options }) => {
|
||||
if (element) {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
const count = state.eventListeners.length;
|
||||
state.eventListeners = [];
|
||||
log(`Removed all event listeners (${count})`);
|
||||
},
|
||||
|
||||
clearAll: function() {
|
||||
const counts = {
|
||||
listeners: state.eventListeners.length,
|
||||
timeouts: state.timeouts.length,
|
||||
intervals: state.intervals.length,
|
||||
animationFrames: state.animationFrames.length,
|
||||
resources: state.resources.size
|
||||
};
|
||||
|
||||
this.clearAllListeners();
|
||||
this.clearAllTimeouts();
|
||||
this.clearAllIntervals();
|
||||
this.clearAllAnimationFrames();
|
||||
this.clearAllResources();
|
||||
|
||||
log(`All resources cleaned up:`, counts);
|
||||
return counts;
|
||||
},
|
||||
|
||||
getResourceCounts: function() {
|
||||
return {
|
||||
listeners: state.eventListeners.length,
|
||||
timeouts: state.timeouts.length,
|
||||
intervals: state.intervals.length,
|
||||
animationFrames: state.animationFrames.length,
|
||||
resources: state.resources.size,
|
||||
total: state.eventListeners.length +
|
||||
state.timeouts.length +
|
||||
state.intervals.length +
|
||||
state.animationFrames.length +
|
||||
state.resources.size
|
||||
};
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
state.debug = Boolean(enabled);
|
||||
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
|
||||
return state.debug;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
this.clearAll();
|
||||
log('CleanupManager disposed');
|
||||
},
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (options.debug !== undefined) {
|
||||
this.setDebugMode(options.debug);
|
||||
}
|
||||
log('CleanupManager initialized');
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
|
||||
window.CleanupManager = CleanupManager;
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.cleanupManagerInitialized) {
|
||||
CleanupManager.initialize();
|
||||
window.cleanupManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager));
|
||||
console.log('CleanupManager initialized');
|
||||
@@ -0,0 +1,414 @@
|
||||
const ConfigManager = (function() {
|
||||
const state = {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
function determineWebSocketPort() {
|
||||
const wsPort =
|
||||
window.ws_port ||
|
||||
(typeof getWebSocketConfig === 'function' ? getWebSocketConfig().port : null) ||
|
||||
'11700';
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
const selectedWsPort = determineWebSocketPort();
|
||||
|
||||
const defaultConfig = {
|
||||
cacheDuration: 10 * 60 * 1000,
|
||||
requestTimeout: 60000,
|
||||
wsPort: selectedWsPort,
|
||||
|
||||
cacheConfig: {
|
||||
defaultTTL: 10 * 60 * 1000,
|
||||
|
||||
ttlSettings: {
|
||||
prices: 5 * 60 * 1000,
|
||||
chart: 5 * 60 * 1000,
|
||||
historical: 60 * 60 * 1000,
|
||||
volume: 30 * 60 * 1000,
|
||||
offers: 2 * 60 * 1000,
|
||||
identity: 15 * 60 * 1000
|
||||
},
|
||||
|
||||
storage: {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200
|
||||
},
|
||||
|
||||
fallbackTTL: 24 * 60 * 60 * 1000
|
||||
},
|
||||
|
||||
itemsPerPage: 50,
|
||||
|
||||
apiEndpoints: {
|
||||
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
|
||||
coinGecko: 'https://api.coingecko.com/api/v3',
|
||||
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday',
|
||||
cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour',
|
||||
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
|
||||
},
|
||||
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200
|
||||
},
|
||||
cryptocompare: {
|
||||
requestsPerMinute: 30,
|
||||
minInterval: 2000
|
||||
}
|
||||
},
|
||||
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
|
||||
coins: [
|
||||
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
|
||||
],
|
||||
|
||||
coinMappings: {
|
||||
nameToSymbol: {
|
||||
'Bitcoin': 'BTC',
|
||||
'Litecoin': 'LTC',
|
||||
'Monero': 'XMR',
|
||||
'Particl': 'PART',
|
||||
'Particl Blind': 'PART',
|
||||
'Particl Anon': 'PART',
|
||||
'PIVX': 'PIVX',
|
||||
'Firo': 'FIRO',
|
||||
'Zcoin': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'Decred': 'DCR',
|
||||
'Wownero': 'WOW',
|
||||
'Bitcoin Cash': 'BCH',
|
||||
'Dogecoin': 'DOGE'
|
||||
},
|
||||
|
||||
nameToDisplayName: {
|
||||
'Bitcoin': 'Bitcoin',
|
||||
'Litecoin': 'Litecoin',
|
||||
'Monero': 'Monero',
|
||||
'Particl': 'Particl',
|
||||
'Particl Blind': 'Particl Blind',
|
||||
'Particl Anon': 'Particl Anon',
|
||||
'PIVX': 'PIVX',
|
||||
'Firo': 'Firo',
|
||||
'Zcoin': 'Firo',
|
||||
'Dash': 'Dash',
|
||||
'Decred': 'Decred',
|
||||
'Wownero': 'Wownero',
|
||||
'Bitcoin Cash': 'Bitcoin Cash',
|
||||
'Dogecoin': 'Dogecoin'
|
||||
},
|
||||
|
||||
idToName: {
|
||||
1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
|
||||
6: 'monero', 7: 'particl blind', 8: 'particl anon',
|
||||
9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash',
|
||||
18: 'dogecoin'
|
||||
},
|
||||
|
||||
nameToCoinGecko: {
|
||||
'bitcoin': 'bitcoin',
|
||||
'monero': 'monero',
|
||||
'particl': 'particl',
|
||||
'bitcoin cash': 'bitcoin-cash',
|
||||
'bitcoincash': 'bitcoin-cash',
|
||||
'pivx': 'pivx',
|
||||
'firo': 'firo',
|
||||
'zcoin': 'firo',
|
||||
'dash': 'dash',
|
||||
'litecoin': 'litecoin',
|
||||
'dogecoin': 'dogecoin',
|
||||
'decred': 'decred',
|
||||
'wownero': 'wownero'
|
||||
}
|
||||
},
|
||||
|
||||
chartConfig: {
|
||||
colors: {
|
||||
default: {
|
||||
lineColor: 'rgba(77, 132, 240, 1)',
|
||||
backgroundColor: 'rgba(77, 132, 240, 0.1)'
|
||||
}
|
||||
},
|
||||
showVolume: false,
|
||||
specialCoins: [''],
|
||||
resolutions: {
|
||||
year: { days: 365, interval: 'month' },
|
||||
sixMonths: { days: 180, interval: 'daily' },
|
||||
day: { days: 1, interval: 'hourly' }
|
||||
},
|
||||
currentResolution: 'year'
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
...defaultConfig,
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (state.isInitialized) {
|
||||
console.warn('[ConfigManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
Object.assign(this, options);
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
this.utils = utils;
|
||||
|
||||
state.isInitialized = true;
|
||||
console.log('ConfigManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
getAPIKeys: function() {
|
||||
if (typeof window.getAPIKeys === 'function') {
|
||||
const apiKeys = window.getAPIKeys();
|
||||
return {
|
||||
cryptoCompare: apiKeys.cryptoCompare || '',
|
||||
coinGecko: apiKeys.coinGecko || ''
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
cryptoCompare: '',
|
||||
coinGecko: ''
|
||||
};
|
||||
},
|
||||
|
||||
getCoinBackendId: function(coinName) {
|
||||
if (!coinName) return null;
|
||||
|
||||
const nameMap = {
|
||||
'bitcoin-cash': 'bitcoincash',
|
||||
'bitcoin cash': 'bitcoincash',
|
||||
'firo': 'firo',
|
||||
'zcoin': 'firo',
|
||||
'bitcoincash': 'bitcoin-cash'
|
||||
};
|
||||
|
||||
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
|
||||
return nameMap[lowerCoinName] || lowerCoinName;
|
||||
},
|
||||
|
||||
coinMatches: function(offerCoin, filterCoin) {
|
||||
if (!offerCoin || !filterCoin) return false;
|
||||
|
||||
offerCoin = offerCoin.toLowerCase();
|
||||
filterCoin = filterCoin.toLowerCase();
|
||||
|
||||
if (offerCoin === filterCoin) return true;
|
||||
|
||||
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
|
||||
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
|
||||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const particlVariants = ['particl', 'particl anon', 'particl blind'];
|
||||
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (particlVariants.includes(filterCoin)) {
|
||||
return offerCoin === filterCoin;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
update: function(path, value) {
|
||||
const parts = path.split('.');
|
||||
let current = this;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!current[parts[i]]) {
|
||||
current[parts[i]] = {};
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
|
||||
current[parts[parts.length - 1]] = value;
|
||||
return this;
|
||||
},
|
||||
|
||||
get: function(path, defaultValue = null) {
|
||||
const parts = path.split('.');
|
||||
let current = this;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (current === undefined || current === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
|
||||
return current !== undefined ? current : defaultValue;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
state.isInitialized = false;
|
||||
console.log('ConfigManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
const utils = {
|
||||
formatNumber: function(number, decimals = 2) {
|
||||
if (typeof number !== 'number' || isNaN(number)) {
|
||||
console.warn('formatNumber received a non-number value:', number);
|
||||
return '0';
|
||||
}
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(number);
|
||||
} catch (e) {
|
||||
return '0';
|
||||
}
|
||||
},
|
||||
|
||||
formatDate: function(timestamp, resolution) {
|
||||
const date = new Date(timestamp);
|
||||
const options = {
|
||||
day: { hour: '2-digit', minute: '2-digit', hour12: true },
|
||||
week: { month: 'short', day: 'numeric' },
|
||||
month: { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
};
|
||||
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
|
||||
},
|
||||
|
||||
debounce: function(func, delay) {
|
||||
let timeoutId;
|
||||
return function(...args) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
},
|
||||
|
||||
formatTimeLeft: function(timestamp) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (timestamp <= now) return "Expired";
|
||||
return this.formatTime(timestamp);
|
||||
},
|
||||
|
||||
formatTime: function(timestamp, addAgoSuffix = false) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = Math.abs(now - timestamp);
|
||||
|
||||
let timeString;
|
||||
if (diff < 60) {
|
||||
timeString = `${diff} seconds`;
|
||||
} else if (diff < 3600) {
|
||||
timeString = `${Math.floor(diff / 60)} minutes`;
|
||||
} else if (diff < 86400) {
|
||||
timeString = `${Math.floor(diff / 3600)} hours`;
|
||||
} else if (diff < 2592000) {
|
||||
timeString = `${Math.floor(diff / 86400)} days`;
|
||||
} else if (diff < 31536000) {
|
||||
timeString = `${Math.floor(diff / 2592000)} months`;
|
||||
} else {
|
||||
timeString = `${Math.floor(diff / 31536000)} years`;
|
||||
}
|
||||
|
||||
return addAgoSuffix ? `${timeString} ago` : timeString;
|
||||
},
|
||||
|
||||
escapeHtml: function(unsafe) {
|
||||
if (typeof unsafe !== 'string') {
|
||||
console.warn('escapeHtml received a non-string value:', unsafe);
|
||||
return '';
|
||||
}
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
|
||||
formatPrice: function(coin, price) {
|
||||
if (typeof price !== 'number' || isNaN(price)) {
|
||||
console.warn(`Invalid price for ${coin}:`, price);
|
||||
return 'N/A';
|
||||
}
|
||||
if (price < 0.000001) return price.toExponential(2);
|
||||
if (price < 0.001) return price.toFixed(8);
|
||||
if (price < 1) return price.toFixed(4);
|
||||
if (price < 10) return price.toFixed(3);
|
||||
if (price < 1000) return price.toFixed(2);
|
||||
if (price < 100000) return price.toFixed(1);
|
||||
return price.toFixed(0);
|
||||
},
|
||||
|
||||
getEmptyPriceData: function() {
|
||||
return {
|
||||
'bitcoin': { usd: null, btc: null },
|
||||
'bitcoin-cash': { usd: null, btc: null },
|
||||
'dash': { usd: null, btc: null },
|
||||
'dogecoin': { usd: null, btc: null },
|
||||
'decred': { usd: null, btc: null },
|
||||
'litecoin': { usd: null, btc: null },
|
||||
'particl': { usd: null, btc: null },
|
||||
'pivx': { usd: null, btc: null },
|
||||
'monero': { usd: null, btc: null },
|
||||
'zano': { usd: null, btc: null },
|
||||
'wownero': { usd: null, btc: null },
|
||||
'firo': { usd: null, btc: null }
|
||||
};
|
||||
},
|
||||
|
||||
getCoinSymbol: function(fullName) {
|
||||
return publicAPI.coinMappings?.nameToSymbol[fullName] || fullName;
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.logger = {
|
||||
log: function(message) {
|
||||
console.log(`[AppLog] ${new Date().toISOString()}: ${message}`);
|
||||
},
|
||||
warn: function(message) {
|
||||
console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`);
|
||||
},
|
||||
error: function(message) {
|
||||
console.error(`[AppError] ${new Date().toISOString()}: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.config = ConfigManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.configManagerInitialized) {
|
||||
ConfigManager.initialize();
|
||||
window.configManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = ConfigManager;
|
||||
}
|
||||
|
||||
//console.log('ConfigManager initialized with properties:', Object.keys(ConfigManager));
|
||||
console.log('ConfigManager initialized');
|
||||
@@ -0,0 +1,192 @@
|
||||
const IdentityManager = (function() {
|
||||
const state = {
|
||||
cache: new Map(),
|
||||
pendingRequests: new Map(),
|
||||
config: {
|
||||
retryDelay: 2000,
|
||||
maxRetries: 3,
|
||||
maxCacheSize: 100,
|
||||
cacheTimeout: window.config?.cacheConfig?.ttlSettings?.identity || 15 * 60 * 1000,
|
||||
debug: false
|
||||
}
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (state.config.debug) {
|
||||
console.log(`[IdentityManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
getIdentityData: async function(address) {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedData = this.getCachedIdentity(address);
|
||||
if (cachedData) {
|
||||
log(`Cache hit for ${address}`);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
if (state.pendingRequests.has(address)) {
|
||||
log(`Using pending request for ${address}`);
|
||||
return state.pendingRequests.get(address);
|
||||
}
|
||||
|
||||
log(`Fetching identity for ${address}`);
|
||||
const request = fetchWithRetry(address);
|
||||
state.pendingRequests.set(address, request);
|
||||
|
||||
try {
|
||||
const data = await request;
|
||||
this.setCachedIdentity(address, data);
|
||||
return data;
|
||||
} finally {
|
||||
state.pendingRequests.delete(address);
|
||||
}
|
||||
},
|
||||
|
||||
getCachedIdentity: function(address) {
|
||||
const cached = state.cache.get(address);
|
||||
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
|
||||
return cached.data;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
setCachedIdentity: function(address, data) {
|
||||
if (state.cache.size >= state.config.maxCacheSize) {
|
||||
const oldestEntries = [...state.cache.entries()]
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||
.slice(0, Math.floor(state.config.maxCacheSize * 0.2));
|
||||
|
||||
oldestEntries.forEach(([key]) => {
|
||||
state.cache.delete(key);
|
||||
log(`Pruned cache entry for ${key}`);
|
||||
});
|
||||
}
|
||||
|
||||
state.cache.set(address, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
log(`Cached identity for ${address}`);
|
||||
},
|
||||
|
||||
clearCache: function() {
|
||||
log(`Clearing identity cache (${state.cache.size} entries)`);
|
||||
state.cache.clear();
|
||||
state.pendingRequests.clear();
|
||||
},
|
||||
|
||||
limitCacheSize: function(maxSize = state.config.maxCacheSize) {
|
||||
if (state.cache.size <= maxSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const entriesToRemove = [...state.cache.entries()]
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||
.slice(0, state.cache.size - maxSize);
|
||||
|
||||
entriesToRemove.forEach(([key]) => state.cache.delete(key));
|
||||
log(`Limited cache size, removed ${entriesToRemove.length} entries`);
|
||||
|
||||
return entriesToRemove.length;
|
||||
},
|
||||
|
||||
getCacheSize: function() {
|
||||
return state.cache.size;
|
||||
},
|
||||
|
||||
configure: function(options = {}) {
|
||||
Object.assign(state.config, options);
|
||||
log(`Configuration updated:`, state.config);
|
||||
return state.config;
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
const now = Date.now();
|
||||
let expiredCount = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
state.cache.forEach((value, key) => {
|
||||
if (now - value.timestamp > state.config.cacheTimeout) {
|
||||
expiredCount++;
|
||||
}
|
||||
const keySize = key.length * 2;
|
||||
const dataSize = JSON.stringify(value.data).length * 2;
|
||||
totalSize += keySize + dataSize;
|
||||
});
|
||||
|
||||
return {
|
||||
cacheEntries: state.cache.size,
|
||||
pendingRequests: state.pendingRequests.size,
|
||||
expiredEntries: expiredCount,
|
||||
estimatedSizeKB: Math.round(totalSize / 1024),
|
||||
config: { ...state.config }
|
||||
};
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
state.config.debug = Boolean(enabled);
|
||||
return `Debug mode ${state.config.debug ? 'enabled' : 'disabled'}`;
|
||||
},
|
||||
|
||||
initialize: function(options = {}) {
|
||||
|
||||
if (options) {
|
||||
this.configure(options);
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('identityManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
log('IdentityManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
this.clearCache();
|
||||
log('IdentityManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchWithRetry(address, attempt = 1) {
|
||||
try {
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (attempt >= state.config.maxRetries) {
|
||||
console.error(`[IdentityManager] Error:`, error.message);
|
||||
console.warn(`[IdentityManager] Failed to fetch identity for ${address} after ${attempt} attempts`);
|
||||
return null;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
|
||||
return fetchWithRetry(address, attempt + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.IdentityManager = IdentityManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.identityManagerInitialized) {
|
||||
IdentityManager.initialize();
|
||||
window.identityManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
|
||||
console.log('IdentityManager initialized');
|
||||
@@ -0,0 +1,219 @@
|
||||
const MemoryManager = (function() {
|
||||
|
||||
const state = {
|
||||
isMonitoringEnabled: false,
|
||||
monitorInterval: null,
|
||||
cleanupInterval: null
|
||||
};
|
||||
|
||||
const config = {
|
||||
monitorInterval: 30000,
|
||||
cleanupInterval: 60000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[MemoryManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
enableMonitoring: function(interval = config.monitorInterval) {
|
||||
if (state.monitorInterval) {
|
||||
clearInterval(state.monitorInterval);
|
||||
}
|
||||
|
||||
state.isMonitoringEnabled = true;
|
||||
config.monitorInterval = interval;
|
||||
|
||||
this.logMemoryUsage();
|
||||
|
||||
state.monitorInterval = setInterval(() => {
|
||||
this.logMemoryUsage();
|
||||
}, interval);
|
||||
|
||||
console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
|
||||
return true;
|
||||
},
|
||||
|
||||
disableMonitoring: function() {
|
||||
if (state.monitorInterval) {
|
||||
clearInterval(state.monitorInterval);
|
||||
state.monitorInterval = null;
|
||||
}
|
||||
|
||||
state.isMonitoringEnabled = false;
|
||||
console.log('Memory monitoring disabled');
|
||||
return true;
|
||||
},
|
||||
|
||||
logMemoryUsage: function() {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
console.log(`=== Memory Monitor [${timestamp}] ===`);
|
||||
|
||||
if (window.performance && window.performance.memory) {
|
||||
console.log('Memory usage:', {
|
||||
usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
|
||||
totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB'
|
||||
});
|
||||
}
|
||||
|
||||
if (navigator.deviceMemory) {
|
||||
console.log('Device memory:', navigator.deviceMemory, 'GB');
|
||||
}
|
||||
|
||||
const nodeCount = document.querySelectorAll('*').length;
|
||||
console.log('DOM node count:', nodeCount);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
const counts = CleanupManager.getResourceCounts();
|
||||
console.log('Managed resources:', counts);
|
||||
}
|
||||
|
||||
if (window.TooltipManager) {
|
||||
const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length;
|
||||
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length;
|
||||
console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers);
|
||||
}
|
||||
|
||||
if (window.CacheManager && window.CacheManager.getStats) {
|
||||
const cacheStats = CacheManager.getStats();
|
||||
console.log('Cache stats:', cacheStats);
|
||||
}
|
||||
|
||||
if (window.IdentityManager && window.IdentityManager.getStats) {
|
||||
const identityStats = window.IdentityManager.getStats();
|
||||
console.log('Identity cache stats:', identityStats);
|
||||
}
|
||||
|
||||
console.log('==============================');
|
||||
},
|
||||
|
||||
enableAutoCleanup: function(interval = config.cleanupInterval) {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
}
|
||||
|
||||
config.cleanupInterval = interval;
|
||||
|
||||
this.forceCleanup();
|
||||
|
||||
state.cleanupInterval = setInterval(() => {
|
||||
this.forceCleanup();
|
||||
}, interval);
|
||||
|
||||
log('Auto-cleanup enabled every', interval/1000, 'seconds');
|
||||
return true;
|
||||
},
|
||||
|
||||
disableAutoCleanup: function() {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
state.cleanupInterval = null;
|
||||
}
|
||||
|
||||
console.log('Memory auto-cleanup disabled');
|
||||
return true;
|
||||
},
|
||||
|
||||
forceCleanup: function() {
|
||||
if (config.debug) {
|
||||
console.log('Running memory cleanup...', new Date().toLocaleTimeString());
|
||||
}
|
||||
|
||||
if (window.CacheManager && CacheManager.cleanup) {
|
||||
CacheManager.cleanup(true);
|
||||
}
|
||||
|
||||
if (window.TooltipManager && TooltipManager.cleanup) {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
|
||||
if (window.TooltipManager && TooltipManager.destroy) {
|
||||
window.TooltipManager.destroy(element);
|
||||
}
|
||||
});
|
||||
|
||||
if (window.chartModule && chartModule.cleanup) {
|
||||
chartModule.cleanup();
|
||||
}
|
||||
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
} else {
|
||||
const arr = new Array(1000);
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
arr[i] = new Array(10000).join('x');
|
||||
}
|
||||
}
|
||||
|
||||
if (config.debug) {
|
||||
console.log('Memory cleanup completed');
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
config.debug = Boolean(enabled);
|
||||
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
|
||||
},
|
||||
|
||||
getStatus: function() {
|
||||
return {
|
||||
monitoring: {
|
||||
enabled: Boolean(state.monitorInterval),
|
||||
interval: config.monitorInterval
|
||||
},
|
||||
autoCleanup: {
|
||||
enabled: Boolean(state.cleanupInterval),
|
||||
interval: config.cleanupInterval
|
||||
},
|
||||
debug: config.debug
|
||||
};
|
||||
},
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (options.debug !== undefined) {
|
||||
this.setDebugMode(options.debug);
|
||||
}
|
||||
|
||||
if (options.enableMonitoring) {
|
||||
this.enableMonitoring(options.monitorInterval || config.monitorInterval);
|
||||
}
|
||||
|
||||
if (options.enableAutoCleanup) {
|
||||
this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval);
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
log('MemoryManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
this.disableMonitoring();
|
||||
this.disableAutoCleanup();
|
||||
log('MemoryManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.MemoryManager = MemoryManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.memoryManagerInitialized) {
|
||||
MemoryManager.initialize();
|
||||
window.memoryManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager));
|
||||
console.log('MemoryManager initialized');
|
||||
@@ -0,0 +1,280 @@
|
||||
const NetworkManager = (function() {
|
||||
const state = {
|
||||
isOnline: navigator.onLine,
|
||||
reconnectAttempts: 0,
|
||||
reconnectTimer: null,
|
||||
lastNetworkError: null,
|
||||
eventHandlers: {},
|
||||
connectionTestInProgress: false
|
||||
};
|
||||
|
||||
const config = {
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectDelay: 5000,
|
||||
reconnectBackoff: 1.5,
|
||||
connectionTestEndpoint: '/json',
|
||||
connectionTestTimeout: 3000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[NetworkManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
function generateHandlerId() {
|
||||
return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
window.addEventListener('online', this.handleOnlineStatus.bind(this));
|
||||
window.addEventListener('offline', this.handleOfflineStatus.bind(this));
|
||||
|
||||
state.isOnline = navigator.onLine;
|
||||
log(`Network status initialized: ${state.isOnline ? 'online' : 'offline'}`);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('networkManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
isOnline: function() {
|
||||
return state.isOnline;
|
||||
},
|
||||
|
||||
getReconnectAttempts: function() {
|
||||
return state.reconnectAttempts;
|
||||
},
|
||||
|
||||
resetReconnectAttempts: function() {
|
||||
state.reconnectAttempts = 0;
|
||||
return this;
|
||||
},
|
||||
|
||||
handleOnlineStatus: function() {
|
||||
log('Browser reports online status');
|
||||
state.isOnline = true;
|
||||
this.notifyHandlers('online');
|
||||
|
||||
if (state.reconnectTimer) {
|
||||
this.scheduleReconnectRefresh();
|
||||
}
|
||||
},
|
||||
|
||||
handleOfflineStatus: function() {
|
||||
log('Browser reports offline status');
|
||||
state.isOnline = false;
|
||||
this.notifyHandlers('offline');
|
||||
},
|
||||
|
||||
handleNetworkError: function(error) {
|
||||
if (error && (
|
||||
(error.name === 'TypeError' && error.message.includes('NetworkError')) ||
|
||||
(error.name === 'AbortError') ||
|
||||
(error.message && error.message.includes('network')) ||
|
||||
(error.message && error.message.includes('timeout'))
|
||||
)) {
|
||||
log('Network error detected:', error.message);
|
||||
|
||||
if (state.isOnline) {
|
||||
state.isOnline = false;
|
||||
state.lastNetworkError = error;
|
||||
this.notifyHandlers('error', error);
|
||||
}
|
||||
|
||||
if (!state.reconnectTimer) {
|
||||
this.scheduleReconnectRefresh();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
scheduleReconnectRefresh: function() {
|
||||
if (state.reconnectTimer) {
|
||||
clearTimeout(state.reconnectTimer);
|
||||
state.reconnectTimer = null;
|
||||
}
|
||||
|
||||
const delay = config.reconnectDelay * Math.pow(config.reconnectBackoff,
|
||||
Math.min(state.reconnectAttempts, 5));
|
||||
|
||||
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
|
||||
|
||||
state.reconnectTimer = setTimeout(() => {
|
||||
state.reconnectTimer = null;
|
||||
this.attemptReconnect();
|
||||
}, delay);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
attemptReconnect: function() {
|
||||
if (!navigator.onLine) {
|
||||
log('Browser still reports offline, delaying reconnection attempt');
|
||||
this.scheduleReconnectRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.connectionTestInProgress) {
|
||||
log('Connection test already in progress');
|
||||
return;
|
||||
}
|
||||
|
||||
state.reconnectAttempts++;
|
||||
state.connectionTestInProgress = true;
|
||||
|
||||
log(`Attempting reconnect #${state.reconnectAttempts}`);
|
||||
|
||||
this.testBackendConnection()
|
||||
.then(isAvailable => {
|
||||
state.connectionTestInProgress = false;
|
||||
|
||||
if (isAvailable) {
|
||||
log('Backend connection confirmed');
|
||||
state.isOnline = true;
|
||||
state.reconnectAttempts = 0;
|
||||
state.lastNetworkError = null;
|
||||
this.notifyHandlers('reconnected');
|
||||
} else {
|
||||
log('Backend still unavailable');
|
||||
|
||||
if (state.reconnectAttempts < config.maxReconnectAttempts) {
|
||||
this.scheduleReconnectRefresh();
|
||||
} else {
|
||||
log('Maximum reconnect attempts reached');
|
||||
this.notifyHandlers('maxAttemptsReached');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
state.connectionTestInProgress = false;
|
||||
log('Error during connection test:', error);
|
||||
|
||||
if (state.reconnectAttempts < config.maxReconnectAttempts) {
|
||||
this.scheduleReconnectRefresh();
|
||||
} else {
|
||||
log('Maximum reconnect attempts reached');
|
||||
this.notifyHandlers('maxAttemptsReached');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
testBackendConnection: function() {
|
||||
return fetch(config.connectionTestEndpoint, {
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
},
|
||||
timeout: config.connectionTestTimeout,
|
||||
signal: AbortSignal.timeout(config.connectionTestTimeout)
|
||||
})
|
||||
.then(response => {
|
||||
return response.ok;
|
||||
})
|
||||
.catch(error => {
|
||||
log('Backend connection test failed:', error.message);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
manualReconnect: function() {
|
||||
log('Manual reconnection requested');
|
||||
|
||||
state.isOnline = navigator.onLine;
|
||||
state.reconnectAttempts = 0;
|
||||
|
||||
this.notifyHandlers('manualReconnect');
|
||||
|
||||
if (state.isOnline) {
|
||||
return this.attemptReconnect();
|
||||
} else {
|
||||
log('Cannot attempt manual reconnect while browser reports offline');
|
||||
this.notifyHandlers('offlineWarning');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addHandler: function(event, handler) {
|
||||
if (!state.eventHandlers[event]) {
|
||||
state.eventHandlers[event] = {};
|
||||
}
|
||||
|
||||
const handlerId = generateHandlerId();
|
||||
state.eventHandlers[event][handlerId] = handler;
|
||||
|
||||
return handlerId;
|
||||
},
|
||||
|
||||
removeHandler: function(event, handlerId) {
|
||||
if (state.eventHandlers[event] && state.eventHandlers[event][handlerId]) {
|
||||
delete state.eventHandlers[event][handlerId];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
notifyHandlers: function(event, data) {
|
||||
if (state.eventHandlers[event]) {
|
||||
Object.values(state.eventHandlers[event]).forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
} catch (error) {
|
||||
log(`Error in ${event} handler:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
config.debug = Boolean(enabled);
|
||||
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
return {
|
||||
isOnline: state.isOnline,
|
||||
reconnectAttempts: state.reconnectAttempts,
|
||||
hasReconnectTimer: Boolean(state.reconnectTimer),
|
||||
connectionTestInProgress: state.connectionTestInProgress
|
||||
};
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
if (state.reconnectTimer) {
|
||||
clearTimeout(state.reconnectTimer);
|
||||
state.reconnectTimer = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('online', this.handleOnlineStatus);
|
||||
window.removeEventListener('offline', this.handleOfflineStatus);
|
||||
|
||||
state.eventHandlers = {};
|
||||
|
||||
log('NetworkManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.NetworkManager = NetworkManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.networkManagerInitialized) {
|
||||
NetworkManager.initialize();
|
||||
window.networkManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
|
||||
console.log('NetworkManager initialized');
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
const NotificationManager = (function() {
|
||||
|
||||
const config = {
|
||||
showNewOffers: false,
|
||||
showNewBids: true,
|
||||
showBidAccepted: true
|
||||
};
|
||||
|
||||
function ensureToastContainer() {
|
||||
let container = document.getElementById('ul_updates');
|
||||
if (!container) {
|
||||
const floating_div = document.createElement('div');
|
||||
floating_div.classList.add('floatright');
|
||||
container = document.createElement('ul');
|
||||
container.setAttribute('id', 'ul_updates');
|
||||
floating_div.appendChild(container);
|
||||
document.body.appendChild(floating_div);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
|
||||
|
||||
console.log('NotificationManager disposed');
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
createToast: function(title, type = 'success') {
|
||||
const messages = ensureToastContainer();
|
||||
const message = document.createElement('li');
|
||||
message.innerHTML = `
|
||||
<div id="hide">
|
||||
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
|
||||
bg-white rounded-lg shadow" role="alert">
|
||||
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
|
||||
bg-blue-500 rounded-lg">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="#ffffff">
|
||||
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
|
||||
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
|
||||
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
|
||||
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
|
||||
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
|
||||
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
|
||||
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
messages.appendChild(message);
|
||||
},
|
||||
|
||||
handleWebSocketEvent: function(data) {
|
||||
if (!data || !data.event) return;
|
||||
let toastTitle;
|
||||
let shouldShowToast = false;
|
||||
|
||||
switch (data.event) {
|
||||
case 'new_offer':
|
||||
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
shouldShowToast = config.showNewOffers;
|
||||
break;
|
||||
case 'new_bid':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
|
||||
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
shouldShowToast = config.showNewBids;
|
||||
break;
|
||||
case 'bid_accepted':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
|
||||
shouldShowToast = config.showBidAccepted;
|
||||
break;
|
||||
}
|
||||
|
||||
if (toastTitle && shouldShowToast) {
|
||||
this.createToast(toastTitle);
|
||||
}
|
||||
},
|
||||
|
||||
updateConfig: function(newConfig) {
|
||||
Object.assign(config, newConfig);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
window.closeAlert = function(event) {
|
||||
let element = event.target;
|
||||
while (element.nodeName !== "BUTTON") {
|
||||
element = element.parentNode;
|
||||
}
|
||||
element.parentNode.parentNode.removeChild(element.parentNode);
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.NotificationManager = NotificationManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (!window.notificationManagerInitialized) {
|
||||
window.NotificationManager.initialize(window.notificationConfig || {});
|
||||
window.notificationManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
|
||||
console.log('NotificationManager initialized');
|
||||
@@ -0,0 +1,338 @@
|
||||
const SummaryManager = (function() {
|
||||
const config = {
|
||||
refreshInterval: window.config?.cacheDuration || 30000,
|
||||
summaryEndpoint: '/json',
|
||||
retryDelay: 5000,
|
||||
maxRetries: 3,
|
||||
requestTimeout: 15000
|
||||
};
|
||||
|
||||
let refreshTimer = null;
|
||||
let webSocket = null;
|
||||
let fetchRetryCount = 0;
|
||||
let lastSuccessfulData = null;
|
||||
|
||||
function updateElement(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return false;
|
||||
|
||||
const safeValue = (value !== undefined && value !== null)
|
||||
? value
|
||||
: (element.dataset.lastValue || 0);
|
||||
|
||||
element.dataset.lastValue = safeValue;
|
||||
|
||||
if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
|
||||
const svg = element.querySelector('svg');
|
||||
element.textContent = safeValue;
|
||||
if (svg) {
|
||||
element.insertBefore(svg, element.firstChild);
|
||||
}
|
||||
} else {
|
||||
element.textContent = safeValue;
|
||||
}
|
||||
|
||||
if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
|
||||
'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
|
||||
'watched-outputs-counter'].includes(elementId)) {
|
||||
element.classList.remove('bg-blue-500', 'bg-gray-400');
|
||||
element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
|
||||
}
|
||||
|
||||
if (elementId === 'swaps-counter') {
|
||||
const swapContainer = document.getElementById('swapContainer');
|
||||
if (swapContainer) {
|
||||
const isSwapping = safeValue > 0;
|
||||
if (isSwapping) {
|
||||
swapContainer.innerHTML = document.querySelector('#swap-in-progress-green-template').innerHTML || '';
|
||||
swapContainer.style.animation = 'spin 2s linear infinite';
|
||||
} else {
|
||||
swapContainer.innerHTML = document.querySelector('#swap-in-progress-template').innerHTML || '';
|
||||
swapContainer.style.animation = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateUIFromData(data) {
|
||||
if (!data) return;
|
||||
|
||||
updateElement('network-offers-counter', data.num_network_offers);
|
||||
updateElement('offers-counter', data.num_sent_active_offers);
|
||||
updateElement('sent-bids-counter', data.num_sent_active_bids);
|
||||
updateElement('recv-bids-counter', data.num_recv_active_bids);
|
||||
updateElement('bid-requests-counter', data.num_available_bids);
|
||||
updateElement('swaps-counter', data.num_swapping);
|
||||
updateElement('watched-outputs-counter', data.num_watched_outputs);
|
||||
|
||||
const shutdownButtons = document.querySelectorAll('.shutdown-button');
|
||||
shutdownButtons.forEach(button => {
|
||||
button.setAttribute('data-active-swaps', data.num_swapping);
|
||||
if (data.num_swapping > 0) {
|
||||
button.classList.add('shutdown-disabled');
|
||||
button.setAttribute('data-disabled', 'true');
|
||||
button.setAttribute('title', 'Caution: Swaps in progress');
|
||||
} else {
|
||||
button.classList.remove('shutdown-disabled');
|
||||
button.removeAttribute('data-disabled');
|
||||
button.removeAttribute('title');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cacheSummaryData(data) {
|
||||
if (!data) return;
|
||||
|
||||
localStorage.setItem('summary_data_cache', JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
data: data
|
||||
}));
|
||||
}
|
||||
|
||||
function getCachedSummaryData() {
|
||||
let cachedData = null;
|
||||
|
||||
cachedData = localStorage.getItem('summary_data_cache');
|
||||
if (!cachedData) return null;
|
||||
|
||||
const parsedCache = JSON.parse(cachedData);
|
||||
const maxAge = 24 * 60 * 60 * 1000;
|
||||
|
||||
if (Date.now() - parsedCache.timestamp < maxAge) {
|
||||
return parsedCache.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fetchSummaryDataWithTimeout() {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
|
||||
|
||||
return fetch(config.summaryEndpoint, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function setupWebSocket() {
|
||||
if (webSocket) {
|
||||
webSocket.close();
|
||||
}
|
||||
|
||||
const wsPort = window.config?.wsPort ||
|
||||
(typeof determineWebSocketPort === 'function' ? determineWebSocketPort() : '11700');
|
||||
|
||||
const wsUrl = "ws://" + window.location.hostname + ":" + wsPort;
|
||||
webSocket = new WebSocket(wsUrl);
|
||||
|
||||
webSocket.onopen = () => {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
webSocket.onmessage = (event) => {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
if (window.logger && window.logger.error) {
|
||||
window.logger.error('WebSocket message processing error: ' + error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.event) {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
webSocket.onclose = () => {
|
||||
setTimeout(setupWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSwapTemplates() {
|
||||
if (!document.getElementById('swap-in-progress-template')) {
|
||||
const template = document.createElement('template');
|
||||
template.id = 'swap-in-progress-template';
|
||||
template.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
|
||||
document.body.appendChild(template);
|
||||
}
|
||||
|
||||
if (!document.getElementById('swap-in-progress-green-template') &&
|
||||
document.querySelector('[id^="swapContainer"]')?.innerHTML) {
|
||||
const greenTemplate = document.createElement('template');
|
||||
greenTemplate.id = 'swap-in-progress-green-template';
|
||||
greenTemplate.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
|
||||
document.body.appendChild(greenTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
function startRefreshTimer() {
|
||||
stopRefreshTimer();
|
||||
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
refreshTimer = setInterval(() => {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}, config.refreshInterval);
|
||||
}
|
||||
|
||||
function stopRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
ensureSwapTemplates();
|
||||
|
||||
const cachedData = getCachedSummaryData();
|
||||
if (cachedData) {
|
||||
updateUIFromData(cachedData);
|
||||
}
|
||||
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
|
||||
const wsManager = window.WebSocketManager;
|
||||
|
||||
if (!wsManager.isConnected()) {
|
||||
wsManager.connect();
|
||||
}
|
||||
|
||||
wsManager.addMessageHandler('message', (data) => {
|
||||
if (data.event) {
|
||||
this.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setupWebSocket();
|
||||
}
|
||||
|
||||
startRefreshTimer();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('summaryManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
fetchSummaryData: function() {
|
||||
return fetchSummaryDataWithTimeout()
|
||||
.then(data => {
|
||||
lastSuccessfulData = data;
|
||||
cacheSummaryData(data);
|
||||
fetchRetryCount = 0;
|
||||
|
||||
updateUIFromData(data);
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (window.logger && window.logger.error) {
|
||||
window.logger.error('Summary data fetch error: ' + error.message);
|
||||
}
|
||||
|
||||
if (fetchRetryCount < config.maxRetries) {
|
||||
fetchRetryCount++;
|
||||
|
||||
if (window.logger && window.logger.warn) {
|
||||
window.logger.warn(`Retrying summary data fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(this.fetchSummaryData());
|
||||
}, config.retryDelay);
|
||||
});
|
||||
} else {
|
||||
const cachedData = lastSuccessfulData || getCachedSummaryData();
|
||||
|
||||
if (cachedData) {
|
||||
if (window.logger && window.logger.warn) {
|
||||
window.logger.warn('Using cached summary data after fetch failures');
|
||||
}
|
||||
updateUIFromData(cachedData);
|
||||
}
|
||||
|
||||
fetchRetryCount = 0;
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
startRefreshTimer: function() {
|
||||
startRefreshTimer();
|
||||
},
|
||||
|
||||
stopRefreshTimer: function() {
|
||||
stopRefreshTimer();
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
stopRefreshTimer();
|
||||
|
||||
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
|
||||
webSocket.close();
|
||||
}
|
||||
|
||||
webSocket = null;
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.SummaryManager = SummaryManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.summaryManagerInitialized) {
|
||||
window.SummaryManager = SummaryManager.initialize();
|
||||
window.summaryManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
|
||||
console.log('SummaryManager initialized');
|
||||
@@ -0,0 +1,588 @@
|
||||
const TooltipManager = (function() {
|
||||
let instance = null;
|
||||
|
||||
class TooltipManagerImpl {
|
||||
constructor() {
|
||||
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
this.activeTooltips = new WeakMap();
|
||||
this.tooltipIdCounter = 0;
|
||||
this.pendingAnimationFrames = new Set();
|
||||
this.tooltipElementsMap = new Map();
|
||||
this.maxTooltips = 300;
|
||||
this.cleanupThreshold = 1.3;
|
||||
this.disconnectedCheckInterval = null;
|
||||
|
||||
this.setupStyles();
|
||||
this.setupCleanupEvents();
|
||||
this.initializeMutationObserver();
|
||||
this.startDisconnectedElementsCheck();
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
create(element, content, options = {}) {
|
||||
if (!element) return null;
|
||||
|
||||
this.destroy(element);
|
||||
|
||||
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
|
||||
const oldestEntries = Array.from(this.tooltipElementsMap.entries())
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||
.slice(0, 20);
|
||||
|
||||
oldestEntries.forEach(([el]) => {
|
||||
this.destroy(el);
|
||||
});
|
||||
}
|
||||
|
||||
const originalContent = content;
|
||||
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
this.pendingAnimationFrames.delete(rafId);
|
||||
|
||||
if (!document.body.contains(element)) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
this.createTooltip(element, originalContent, options, rect);
|
||||
} else {
|
||||
let retryCount = 0;
|
||||
const retryCreate = () => {
|
||||
const newRect = element.getBoundingClientRect();
|
||||
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= 3) {
|
||||
if (newRect.width > 0 && newRect.height > 0) {
|
||||
this.createTooltip(element, originalContent, options, newRect);
|
||||
}
|
||||
} else {
|
||||
retryCount++;
|
||||
const newRafId = requestAnimationFrame(retryCreate);
|
||||
this.pendingAnimationFrames.add(newRafId);
|
||||
}
|
||||
};
|
||||
const initialRetryId = requestAnimationFrame(retryCreate);
|
||||
this.pendingAnimationFrames.add(initialRetryId);
|
||||
}
|
||||
});
|
||||
|
||||
this.pendingAnimationFrames.add(rafId);
|
||||
return null;
|
||||
}
|
||||
|
||||
createTooltip(element, content, options, rect) {
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
let bgClass = 'bg-gray-400';
|
||||
let arrowColor = 'rgb(156 163 175)';
|
||||
|
||||
if (targetId?.includes('tooltip-offer-') && window.jsonData) {
|
||||
try {
|
||||
const offerId = targetId.split('tooltip-offer-')[1];
|
||||
let actualOfferId = offerId;
|
||||
|
||||
if (offerId.includes('_')) {
|
||||
[actualOfferId] = offerId.split('_');
|
||||
}
|
||||
|
||||
let offer = null;
|
||||
if (Array.isArray(window.jsonData)) {
|
||||
for (let i = 0; i < window.jsonData.length; i++) {
|
||||
const o = window.jsonData[i];
|
||||
if (o && (o.unique_id === offerId || o.offer_id === actualOfferId)) {
|
||||
offer = o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (offer) {
|
||||
if (offer.is_revoked) {
|
||||
bgClass = 'bg-red-500';
|
||||
arrowColor = 'rgb(239 68 68)';
|
||||
} else if (offer.is_own_offer) {
|
||||
bgClass = 'bg-gray-300';
|
||||
arrowColor = 'rgb(209 213 219)';
|
||||
} else {
|
||||
bgClass = 'bg-green-700';
|
||||
arrowColor = 'rgb(21 128 61)';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error finding offer for tooltip:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
|
||||
|
||||
try {
|
||||
if (typeof tippy !== 'function') {
|
||||
console.error('Tippy.js is not loaded. Cannot create tooltip.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const instance = tippy(element, {
|
||||
content: content,
|
||||
allowHTML: true,
|
||||
placement: options.placement || 'top',
|
||||
appendTo: document.body,
|
||||
animation: false,
|
||||
duration: 0,
|
||||
delay: 0,
|
||||
interactive: true,
|
||||
arrow: false,
|
||||
theme: '',
|
||||
moveTransition: 'none',
|
||||
offset: [0, 10],
|
||||
onShow(instance) {
|
||||
if (!document.body.contains(element)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onMount(instance) {
|
||||
if (instance.popper && instance.popper.firstElementChild) {
|
||||
instance.popper.firstElementChild.classList.add(bgClass);
|
||||
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
|
||||
}
|
||||
const arrow = instance.popper.querySelector('.tippy-arrow');
|
||||
if (arrow) {
|
||||
arrow.style.setProperty('color', arrowColor, 'important');
|
||||
}
|
||||
},
|
||||
popperOptions: {
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: 'viewport',
|
||||
padding: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
padding: 10,
|
||||
fallbackPlacements: ['top', 'bottom', 'right', 'left']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
element.setAttribute('data-tooltip-trigger-id', tooltipId);
|
||||
this.activeTooltips.set(element, instance);
|
||||
|
||||
this.tooltipElementsMap.set(element, {
|
||||
timestamp: Date.now(),
|
||||
id: tooltipId
|
||||
});
|
||||
|
||||
return instance;
|
||||
} catch (e) {
|
||||
console.error('Error creating tooltip:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy(element) {
|
||||
if (!element) return;
|
||||
|
||||
const id = element.getAttribute('data-tooltip-trigger-id');
|
||||
if (!id) return;
|
||||
|
||||
const instance = this.activeTooltips.get(element);
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying tooltip:', e);
|
||||
|
||||
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
|
||||
if (tippyRoot && tippyRoot.parentNode) {
|
||||
tippyRoot.parentNode.removeChild(tippyRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.activeTooltips.delete(element);
|
||||
this.tooltipElementsMap.delete(element);
|
||||
|
||||
element.removeAttribute('data-tooltip-trigger-id');
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.pendingAnimationFrames.forEach(id => {
|
||||
cancelAnimationFrame(id);
|
||||
});
|
||||
this.pendingAnimationFrames.clear();
|
||||
|
||||
const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
|
||||
const batchSize = 20;
|
||||
|
||||
const processElementsBatch = (startIdx) => {
|
||||
const endIdx = Math.min(startIdx + batchSize, elements.length);
|
||||
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
this.destroy(elements[i]);
|
||||
}
|
||||
|
||||
if (endIdx < elements.length) {
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
this.pendingAnimationFrames.delete(rafId);
|
||||
processElementsBatch(endIdx);
|
||||
});
|
||||
this.pendingAnimationFrames.add(rafId);
|
||||
} else {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
};
|
||||
|
||||
if (elements.length > 0) {
|
||||
processElementsBatch(0);
|
||||
} else {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
|
||||
this.tooltipElementsMap.clear();
|
||||
}
|
||||
|
||||
cleanupOrphanedTippyElements() {
|
||||
const tippyElements = document.querySelectorAll('[data-tippy-root]');
|
||||
tippyElements.forEach(element => {
|
||||
if (element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupStyles() {
|
||||
if (document.getElementById('tooltip-styles')) return;
|
||||
|
||||
document.head.insertAdjacentHTML('beforeend', `
|
||||
<style id="tooltip-styles">
|
||||
[data-tippy-root] {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
position: relative !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.tippy-content {
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
|
||||
.tippy-box .bg-gray-400 {
|
||||
background-color: rgb(156 163 175);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-gray-400) .tippy-arrow {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.tippy-box .bg-red-500 {
|
||||
background-color: rgb(239 68 68);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-red-500) .tippy-arrow {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.tippy-box .bg-gray-300 {
|
||||
background-color: rgb(209 213 219);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-gray-300) .tippy-arrow {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
.tippy-box .bg-green-700 {
|
||||
background-color: rgb(21 128 61);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-green-700) .tippy-arrow {
|
||||
color: rgb(21 128 61);
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
|
||||
border-top-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
|
||||
border-bottom-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
|
||||
border-left-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
|
||||
border-right-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='top'] > .tippy-arrow {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='left'] > .tippy-arrow {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='right'] > .tippy-arrow {
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
}
|
||||
|
||||
setupCleanupEvents() {
|
||||
this.boundCleanup = this.cleanup.bind(this);
|
||||
this.handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
this.cleanup();
|
||||
|
||||
if (window.MemoryManager) {
|
||||
window.MemoryManager.forceCleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', this.boundCleanup);
|
||||
window.addEventListener('unload', this.boundCleanup);
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('tooltipManager', this, (tm) => tm.dispose());
|
||||
}
|
||||
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.performPeriodicCleanup();
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
startDisconnectedElementsCheck() {
|
||||
|
||||
if (this.disconnectedCheckInterval) {
|
||||
clearInterval(this.disconnectedCheckInterval);
|
||||
}
|
||||
|
||||
this.disconnectedCheckInterval = setInterval(() => {
|
||||
this.checkForDisconnectedElements();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
checkForDisconnectedElements() {
|
||||
if (this.tooltipElementsMap.size === 0) return;
|
||||
|
||||
const elementsToCheck = Array.from(this.tooltipElementsMap.keys());
|
||||
let removedCount = 0;
|
||||
|
||||
elementsToCheck.forEach(element => {
|
||||
|
||||
if (!document.body.contains(element)) {
|
||||
this.destroy(element);
|
||||
removedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (removedCount > 0) {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
}
|
||||
|
||||
performPeriodicCleanup() {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
this.checkForDisconnectedElements();
|
||||
|
||||
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
|
||||
const sortedTooltips = Array.from(this.tooltipElementsMap.entries())
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
|
||||
const tooltipsToRemove = sortedTooltips.slice(0, sortedTooltips.length - this.maxTooltips);
|
||||
tooltipsToRemove.forEach(([element]) => {
|
||||
this.destroy(element);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeCleanupEvents() {
|
||||
window.removeEventListener('beforeunload', this.boundCleanup);
|
||||
window.removeEventListener('unload', this.boundCleanup);
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.cleanupInterval = null;
|
||||
}
|
||||
|
||||
if (this.disconnectedCheckInterval) {
|
||||
clearInterval(this.disconnectedCheckInterval);
|
||||
this.disconnectedCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
initializeMutationObserver() {
|
||||
if (this.mutationObserver) return;
|
||||
|
||||
this.mutationObserver = new MutationObserver(mutations => {
|
||||
let needsCleanup = false;
|
||||
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.removedNodes.length) {
|
||||
Array.from(mutation.removedNodes).forEach(node => {
|
||||
if (node.nodeType === 1) {
|
||||
|
||||
if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
|
||||
this.destroy(node);
|
||||
needsCleanup = true;
|
||||
}
|
||||
|
||||
if (node.querySelectorAll) {
|
||||
const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]');
|
||||
if (tooltipTriggers.length > 0) {
|
||||
tooltipTriggers.forEach(el => {
|
||||
this.destroy(el);
|
||||
});
|
||||
needsCleanup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (needsCleanup) {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
});
|
||||
|
||||
this.mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
initializeTooltips(selector = '[data-tooltip-target]') {
|
||||
document.querySelectorAll(selector).forEach(element => {
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
const tooltipContent = document.getElementById(targetId);
|
||||
|
||||
if (tooltipContent) {
|
||||
this.create(element, tooltipContent.innerHTML, {
|
||||
placement: element.getAttribute('data-tooltip-placement') || 'top'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.cleanup();
|
||||
|
||||
this.pendingAnimationFrames.forEach(id => {
|
||||
cancelAnimationFrame(id);
|
||||
});
|
||||
this.pendingAnimationFrames.clear();
|
||||
|
||||
if (this.mutationObserver) {
|
||||
this.mutationObserver.disconnect();
|
||||
this.mutationObserver = null;
|
||||
}
|
||||
|
||||
this.removeCleanupEvents();
|
||||
|
||||
const styleElement = document.getElementById('tooltip-styles');
|
||||
if (styleElement && styleElement.parentNode) {
|
||||
styleElement.parentNode.removeChild(styleElement);
|
||||
}
|
||||
|
||||
this.activeTooltips = new WeakMap();
|
||||
this.tooltipElementsMap.clear();
|
||||
|
||||
instance = null;
|
||||
}
|
||||
|
||||
initialize(options = {}) {
|
||||
|
||||
if (options.maxTooltips) {
|
||||
this.maxTooltips = options.maxTooltips;
|
||||
}
|
||||
|
||||
console.log('TooltipManager initialized');
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
initialize: function(options = {}) {
|
||||
if (!instance) {
|
||||
const manager = new TooltipManagerImpl();
|
||||
manager.initialize(options);
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
getInstance: function() {
|
||||
if (!instance) {
|
||||
const manager = new TooltipManagerImpl();
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
create: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.create(...args);
|
||||
},
|
||||
|
||||
destroy: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.destroy(...args);
|
||||
},
|
||||
|
||||
cleanup: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.cleanup(...args);
|
||||
},
|
||||
|
||||
initializeTooltips: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.initializeTooltips(...args);
|
||||
},
|
||||
|
||||
dispose: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.dispose(...args);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.TooltipManager = TooltipManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.tooltipManagerInitialized) {
|
||||
TooltipManager.initialize();
|
||||
TooltipManager.initializeTooltips();
|
||||
window.tooltipManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TooltipManager;
|
||||
}
|
||||
|
||||
//console.log('TooltipManager initialized with methods:', Object.keys(TooltipManager));
|
||||
console.log('TooltipManager initialized');
|
||||
@@ -0,0 +1,655 @@
|
||||
const WalletManager = (function() {
|
||||
|
||||
const config = {
|
||||
maxRetries: 5,
|
||||
baseDelay: 500,
|
||||
cacheExpiration: 5 * 60 * 1000,
|
||||
priceUpdateInterval: 5 * 60 * 1000,
|
||||
apiTimeout: 30000,
|
||||
debounceDelay: 300,
|
||||
cacheMinInterval: 60 * 1000,
|
||||
defaultTTL: 300,
|
||||
priceSource: {
|
||||
primary: 'coingecko.com',
|
||||
fallback: 'cryptocompare.com',
|
||||
enabledSources: ['coingecko.com', 'cryptocompare.com']
|
||||
}
|
||||
};
|
||||
|
||||
const stateKeys = {
|
||||
lastUpdate: 'last-update-time',
|
||||
previousTotal: 'previous-total-usd',
|
||||
currentTotal: 'current-total-usd',
|
||||
balancesVisible: 'balancesVisible'
|
||||
};
|
||||
|
||||
const coinData = {
|
||||
symbols: {
|
||||
'Bitcoin': 'BTC',
|
||||
'Particl': 'PART',
|
||||
'Monero': 'XMR',
|
||||
'Wownero': 'WOW',
|
||||
'Litecoin': 'LTC',
|
||||
'Dogecoin': 'DOGE',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'PIVX': 'PIVX',
|
||||
'Decred': 'DCR',
|
||||
'Bitcoin Cash': 'BCH'
|
||||
},
|
||||
|
||||
coingeckoIds: {
|
||||
'BTC': 'btc',
|
||||
'PART': 'part',
|
||||
'XMR': 'xmr',
|
||||
'WOW': 'wownero',
|
||||
'LTC': 'ltc',
|
||||
'DOGE': 'doge',
|
||||
'FIRO': 'firo',
|
||||
'DASH': 'dash',
|
||||
'PIVX': 'pivx',
|
||||
'DCR': 'dcr',
|
||||
'BCH': 'bch'
|
||||
},
|
||||
|
||||
shortNames: {
|
||||
'Bitcoin': 'BTC',
|
||||
'Particl': 'PART',
|
||||
'Monero': 'XMR',
|
||||
'Wownero': 'WOW',
|
||||
'Litecoin': 'LTC',
|
||||
'Litecoin MWEB': 'LTC MWEB',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'PIVX': 'PIVX',
|
||||
'Decred': 'DCR',
|
||||
'Bitcoin Cash': 'BCH',
|
||||
'Dogecoin': 'DOGE'
|
||||
}
|
||||
};
|
||||
|
||||
const state = {
|
||||
lastFetchTime: 0,
|
||||
toggleInProgress: false,
|
||||
toggleDebounceTimer: null,
|
||||
priceUpdateInterval: null,
|
||||
lastUpdateTime: 0,
|
||||
isWalletsPage: false,
|
||||
initialized: false,
|
||||
cacheKey: 'rates_crypto_prices'
|
||||
};
|
||||
|
||||
function getShortName(fullName) {
|
||||
return coinData.shortNames[fullName] || fullName;
|
||||
}
|
||||
|
||||
async function fetchPrices(forceUpdate = false) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastFetch = now - state.lastFetchTime;
|
||||
|
||||
if (!forceUpdate && timeSinceLastFetch < config.cacheMinInterval) {
|
||||
const cachedData = CacheManager.get(state.cacheKey);
|
||||
if (cachedData) {
|
||||
return cachedData.value;
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
for (let attempt = 0; attempt < config.maxRetries; attempt++) {
|
||||
try {
|
||||
const processedData = {};
|
||||
const currentSource = config.priceSource.primary;
|
||||
|
||||
const shouldIncludeWow = currentSource === 'coingecko.com';
|
||||
|
||||
const coinsToFetch = Object.values(coinData.symbols)
|
||||
.filter(symbol => shouldIncludeWow || symbol !== 'WOW')
|
||||
.map(symbol => coinData.coingeckoIds[symbol] || symbol.toLowerCase())
|
||||
.join(',');
|
||||
|
||||
const mainResponse = await fetch("/json/coinprices", {
|
||||
method: "POST",
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
coins: coinsToFetch,
|
||||
source: currentSource,
|
||||
ttl: config.defaultTTL
|
||||
})
|
||||
});
|
||||
|
||||
if (!mainResponse.ok) {
|
||||
throw new Error(`HTTP error: ${mainResponse.status}`);
|
||||
}
|
||||
|
||||
const mainData = await mainResponse.json();
|
||||
|
||||
if (mainData && mainData.rates) {
|
||||
Object.entries(mainData.rates).forEach(([coinId, price]) => {
|
||||
const symbol = Object.entries(coinData.coingeckoIds).find(([sym, id]) => id.toLowerCase() === coinId.toLowerCase())?.[0];
|
||||
if (symbol) {
|
||||
const coinKey = Object.keys(coinData.symbols).find(key => coinData.symbols[key] === symbol);
|
||||
if (coinKey) {
|
||||
processedData[coinKey.toLowerCase().replace(' ', '-')] = {
|
||||
usd: price,
|
||||
btc: symbol === 'BTC' ? 1 : price / (mainData.rates.btc || 1)
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!shouldIncludeWow && !processedData['wownero']) {
|
||||
try {
|
||||
const wowResponse = await fetch("/json/coinprices", {
|
||||
method: "POST",
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
coins: "wownero",
|
||||
source: "coingecko.com",
|
||||
ttl: config.defaultTTL
|
||||
})
|
||||
});
|
||||
|
||||
if (wowResponse.ok) {
|
||||
const wowData = await wowResponse.json();
|
||||
if (wowData && wowData.rates && wowData.rates.wownero) {
|
||||
processedData['wownero'] = {
|
||||
usd: wowData.rates.wownero,
|
||||
btc: processedData.bitcoin ? wowData.rates.wownero / processedData.bitcoin.usd : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (wowError) {
|
||||
console.error('Error fetching WOW price:', wowError);
|
||||
}
|
||||
}
|
||||
|
||||
CacheManager.set(state.cacheKey, processedData, config.cacheExpiration);
|
||||
state.lastFetchTime = now;
|
||||
return processedData;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error(`Price fetch attempt ${attempt + 1} failed:`, error);
|
||||
|
||||
if (attempt === config.maxRetries - 1 &&
|
||||
config.priceSource.fallback &&
|
||||
config.priceSource.fallback !== config.priceSource.primary) {
|
||||
const temp = config.priceSource.primary;
|
||||
config.priceSource.primary = config.priceSource.fallback;
|
||||
config.priceSource.fallback = temp;
|
||||
|
||||
console.warn(`Switching to fallback source: ${config.priceSource.primary}`);
|
||||
attempt = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attempt < config.maxRetries - 1) {
|
||||
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cachedData = CacheManager.get(state.cacheKey);
|
||||
if (cachedData) {
|
||||
console.warn('Using cached data after fetch failures');
|
||||
return cachedData.value;
|
||||
}
|
||||
|
||||
throw lastError || new Error('Failed to fetch prices');
|
||||
}
|
||||
|
||||
// UI Management functions
|
||||
function storeOriginalValues() {
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const value = el.textContent?.trim() || '';
|
||||
|
||||
if (coinName) {
|
||||
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
|
||||
const coinId = coinData.symbols[coinName];
|
||||
const shortName = getShortName(coinName);
|
||||
|
||||
if (coinId) {
|
||||
if (coinName === 'Particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinName === 'Litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinId.toLowerCase()}-amount`, amount.toString());
|
||||
}
|
||||
|
||||
el.setAttribute('data-original-value', `${amount} ${shortName}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const text = el.textContent?.trim() || '';
|
||||
if (text === 'Loading...') {
|
||||
el.textContent = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updatePrices(forceUpdate = false) {
|
||||
try {
|
||||
const prices = await fetchPrices(forceUpdate);
|
||||
let newTotal = 0;
|
||||
|
||||
const currentTime = Date.now();
|
||||
localStorage.setItem(stateKeys.lastUpdate, currentTime.toString());
|
||||
state.lastUpdateTime = currentTime;
|
||||
|
||||
if (prices) {
|
||||
Object.entries(prices).forEach(([coinId, priceData]) => {
|
||||
if (priceData?.usd) {
|
||||
localStorage.setItem(`${coinId}-price`, priceData.usd.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
|
||||
|
||||
if (!coinName) return;
|
||||
|
||||
let amount = 0;
|
||||
if (amountStr) {
|
||||
const matches = amountStr.match(/([0-9]*[.])?[0-9]+/);
|
||||
if (matches && matches.length > 0) {
|
||||
amount = parseFloat(matches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const coinId = coinName.toLowerCase().replace(' ', '-');
|
||||
|
||||
if (!prices[coinId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
|
||||
if (!price) return;
|
||||
|
||||
const usdValue = (amount * price).toFixed(2);
|
||||
|
||||
if (coinName === 'Particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinName === 'Litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinId}-last-value`, usdValue);
|
||||
localStorage.setItem(`${coinId}-amount`, amount.toString());
|
||||
}
|
||||
|
||||
if (amount > 0) {
|
||||
newTotal += parseFloat(usdValue);
|
||||
}
|
||||
|
||||
let usdEl = null;
|
||||
|
||||
const flexContainer = el.closest('.flex');
|
||||
if (flexContainer) {
|
||||
const nextFlex = flexContainer.nextElementSibling;
|
||||
if (nextFlex) {
|
||||
const usdInNextFlex = nextFlex.querySelector('.usd-value');
|
||||
if (usdInNextFlex) {
|
||||
usdEl = usdInNextFlex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!usdEl) {
|
||||
const parentCell = el.closest('td');
|
||||
if (parentCell) {
|
||||
const usdInSameCell = parentCell.querySelector('.usd-value');
|
||||
if (usdInSameCell) {
|
||||
usdEl = usdInSameCell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!usdEl) {
|
||||
const sibling = el.nextElementSibling;
|
||||
if (sibling && sibling.classList.contains('usd-value')) {
|
||||
usdEl = sibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (!usdEl) {
|
||||
const parentElement = el.parentElement;
|
||||
if (parentElement) {
|
||||
const usdElNearby = parentElement.querySelector('.usd-value');
|
||||
if (usdElNearby) {
|
||||
usdEl = usdElNearby;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usdEl) {
|
||||
usdEl.textContent = `$${usdValue}`;
|
||||
usdEl.setAttribute('data-original-value', usdValue);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
|
||||
const parentCell = el.closest('td');
|
||||
if (!parentCell) return;
|
||||
|
||||
const coinValueEl = parentCell.querySelector('.coinname-value');
|
||||
if (!coinValueEl) return;
|
||||
|
||||
const coinName = coinValueEl.getAttribute('data-coinname');
|
||||
if (!coinName) return;
|
||||
|
||||
const amountStr = coinValueEl.textContent?.trim() || '0';
|
||||
const amount = parseFloat(amountStr) || 0;
|
||||
|
||||
const coinId = coinName.toLowerCase().replace(' ', '-');
|
||||
if (!prices[coinId]) return;
|
||||
|
||||
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
|
||||
if (!price) return;
|
||||
|
||||
const usdValue = (amount * price).toFixed(8);
|
||||
el.textContent = `$${usdValue}`;
|
||||
el.setAttribute('data-original-value', usdValue);
|
||||
}
|
||||
});
|
||||
|
||||
if (state.isWalletsPage) {
|
||||
updateTotalValues(newTotal, prices?.bitcoin?.usd);
|
||||
}
|
||||
|
||||
localStorage.setItem(stateKeys.previousTotal, localStorage.getItem(stateKeys.currentTotal) || '0');
|
||||
localStorage.setItem(stateKeys.currentTotal, newTotal.toString());
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Price update failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTotalValues(totalUsd, btcPrice) {
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
|
||||
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
|
||||
localStorage.setItem('total-usd', totalUsd.toString());
|
||||
}
|
||||
|
||||
if (btcPrice) {
|
||||
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
|
||||
const totalBtcEl = document.getElementById('total-btc-value');
|
||||
if (totalBtcEl) {
|
||||
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
|
||||
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBalances() {
|
||||
if (state.toggleInProgress) return;
|
||||
|
||||
try {
|
||||
state.toggleInProgress = true;
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
const newVisibility = !balancesVisible;
|
||||
|
||||
localStorage.setItem('balancesVisible', newVisibility.toString());
|
||||
updateVisibility(newVisibility);
|
||||
|
||||
if (state.toggleDebounceTimer) {
|
||||
clearTimeout(state.toggleDebounceTimer);
|
||||
}
|
||||
|
||||
state.toggleDebounceTimer = window.setTimeout(async () => {
|
||||
state.toggleInProgress = false;
|
||||
if (newVisibility) {
|
||||
await updatePrices(true);
|
||||
}
|
||||
}, config.debounceDelay);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle balances:', error);
|
||||
state.toggleInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisibility(isVisible) {
|
||||
if (isVisible) {
|
||||
showBalances();
|
||||
} else {
|
||||
hideBalances();
|
||||
}
|
||||
|
||||
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
|
||||
if (eyeIcon) {
|
||||
eyeIcon.innerHTML = isVisible ?
|
||||
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
|
||||
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
|
||||
}
|
||||
}
|
||||
|
||||
function showBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'inline';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const originalValue = el.getAttribute('data-original-value');
|
||||
if (originalValue) {
|
||||
el.textContent = originalValue;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const storedValue = el.getAttribute('data-original-value');
|
||||
if (storedValue !== null && storedValue !== undefined) {
|
||||
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
|
||||
el.textContent = `$${parseFloat(storedValue).toFixed(8)}`;
|
||||
} else {
|
||||
el.textContent = `$${parseFloat(storedValue).toFixed(2)}`;
|
||||
}
|
||||
} else {
|
||||
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
|
||||
el.textContent = '$0.00000000';
|
||||
} else {
|
||||
el.textContent = '$0.00';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (state.isWalletsPage) {
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
const originalValue = el?.getAttribute('data-original-value');
|
||||
if (el && originalValue) {
|
||||
if (id === 'total-usd-value') {
|
||||
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
|
||||
el.classList.add('font-extrabold');
|
||||
} else {
|
||||
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hideBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'none';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
el.textContent = '****';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
el.textContent = '****';
|
||||
});
|
||||
|
||||
if (state.isWalletsPage) {
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = '****';
|
||||
}
|
||||
});
|
||||
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.classList.remove('font-extrabold');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBalanceVisibility() {
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
updateVisibility(balancesVisible);
|
||||
|
||||
if (balancesVisible) {
|
||||
await updatePrices(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
const publicAPI = {
|
||||
initialize: async function(options) {
|
||||
if (state.initialized) {
|
||||
console.warn('[WalletManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
Object.assign(config, options);
|
||||
}
|
||||
|
||||
state.lastUpdateTime = parseInt(localStorage.getItem(stateKeys.lastUpdate) || '0');
|
||||
state.isWalletsPage = document.querySelector('.wallet-list') !== null ||
|
||||
window.location.pathname.includes('/wallets');
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const text = el.textContent?.trim() || '';
|
||||
if (text === 'Loading...') {
|
||||
el.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
storeOriginalValues();
|
||||
|
||||
if (localStorage.getItem('balancesVisible') === null) {
|
||||
localStorage.setItem('balancesVisible', 'true');
|
||||
}
|
||||
|
||||
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
|
||||
if (hideBalancesToggle) {
|
||||
hideBalancesToggle.addEventListener('click', toggleBalances);
|
||||
}
|
||||
|
||||
await loadBalanceVisibility();
|
||||
|
||||
if (state.priceUpdateInterval) {
|
||||
clearInterval(state.priceUpdateInterval);
|
||||
}
|
||||
|
||||
state.priceUpdateInterval = setInterval(() => {
|
||||
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
|
||||
updatePrices(false);
|
||||
}
|
||||
}, config.priceUpdateInterval);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('walletManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
state.initialized = true;
|
||||
console.log('WalletManager initialized');
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
updatePrices: function(forceUpdate = false) {
|
||||
return updatePrices(forceUpdate);
|
||||
},
|
||||
|
||||
toggleBalances: function() {
|
||||
return toggleBalances();
|
||||
},
|
||||
|
||||
setPriceSource: function(primarySource, fallbackSource = null) {
|
||||
if (!config.priceSource.enabledSources.includes(primarySource)) {
|
||||
throw new Error(`Invalid primary source: ${primarySource}`);
|
||||
}
|
||||
|
||||
if (fallbackSource && !config.priceSource.enabledSources.includes(fallbackSource)) {
|
||||
throw new Error(`Invalid fallback source: ${fallbackSource}`);
|
||||
}
|
||||
|
||||
config.priceSource.primary = primarySource;
|
||||
if (fallbackSource) {
|
||||
config.priceSource.fallback = fallbackSource;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getConfig: function() {
|
||||
return { ...config };
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
return {
|
||||
initialized: state.initialized,
|
||||
lastUpdateTime: state.lastUpdateTime,
|
||||
isWalletsPage: state.isWalletsPage,
|
||||
balancesVisible: localStorage.getItem('balancesVisible') === 'true'
|
||||
};
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
if (state.priceUpdateInterval) {
|
||||
clearInterval(state.priceUpdateInterval);
|
||||
state.priceUpdateInterval = null;
|
||||
}
|
||||
|
||||
if (state.toggleDebounceTimer) {
|
||||
clearTimeout(state.toggleDebounceTimer);
|
||||
state.toggleDebounceTimer = null;
|
||||
}
|
||||
|
||||
state.initialized = false;
|
||||
console.log('WalletManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.WalletManager = WalletManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.walletManagerInitialized) {
|
||||
WalletManager.initialize();
|
||||
window.walletManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
|
||||
console.log('WalletManager initialized');
|
||||
@@ -0,0 +1,444 @@
|
||||
const WebSocketManager = (function() {
|
||||
let ws = null;
|
||||
|
||||
const config = {
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectDelay: 5000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
const state = {
|
||||
isConnecting: false,
|
||||
isIntentionallyClosed: false,
|
||||
lastConnectAttempt: null,
|
||||
connectTimeout: null,
|
||||
lastHealthCheck: null,
|
||||
healthCheckInterval: null,
|
||||
isPageHidden: document.hidden,
|
||||
messageHandlers: {},
|
||||
listeners: {},
|
||||
reconnectTimeout: null
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[WebSocketManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
function generateHandlerId() {
|
||||
return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
function determineWebSocketPort() {
|
||||
let wsPort;
|
||||
|
||||
if (window.config && window.config.wsPort) {
|
||||
wsPort = window.config.wsPort;
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
if (window.ws_port) {
|
||||
wsPort = window.ws_port.toString();
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
if (typeof getWebSocketConfig === 'function') {
|
||||
const wsConfig = getWebSocketConfig();
|
||||
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
wsPort = '11700';
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
setupPageVisibilityHandler();
|
||||
this.connect();
|
||||
startHealthCheck();
|
||||
|
||||
log('WebSocketManager initialized with options:', options);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('webSocketManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
if (state.isConnecting || state.isIntentionallyClosed) {
|
||||
log('Connection attempt blocked - already connecting or intentionally closed');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
state.isConnecting = true;
|
||||
state.lastConnectAttempt = Date.now();
|
||||
|
||||
try {
|
||||
const wsPort = determineWebSocketPort();
|
||||
|
||||
if (!wsPort) {
|
||||
state.isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
setupEventHandlers();
|
||||
|
||||
state.connectTimeout = setTimeout(() => {
|
||||
if (state.isConnecting) {
|
||||
log('Connection timeout, cleaning up');
|
||||
cleanup();
|
||||
handleReconnect();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log('Error during connection attempt:', error);
|
||||
state.isConnecting = false;
|
||||
handleReconnect();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
log('Disconnecting WebSocket');
|
||||
state.isIntentionallyClosed = true;
|
||||
cleanup();
|
||||
stopHealthCheck();
|
||||
},
|
||||
|
||||
isConnected: function() {
|
||||
return ws && ws.readyState === WebSocket.OPEN;
|
||||
},
|
||||
|
||||
sendMessage: function(message) {
|
||||
if (!this.isConnected()) {
|
||||
log('Cannot send message - not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ws.send(JSON.stringify(message));
|
||||
return true;
|
||||
} catch (error) {
|
||||
log('Error sending message:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addMessageHandler: function(type, handler) {
|
||||
if (!state.messageHandlers[type]) {
|
||||
state.messageHandlers[type] = {};
|
||||
}
|
||||
|
||||
const handlerId = generateHandlerId();
|
||||
state.messageHandlers[type][handlerId] = handler;
|
||||
|
||||
return handlerId;
|
||||
},
|
||||
|
||||
removeMessageHandler: function(type, handlerId) {
|
||||
if (state.messageHandlers[type] && state.messageHandlers[type][handlerId]) {
|
||||
delete state.messageHandlers[type][handlerId];
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
log('Cleaning up WebSocket resources');
|
||||
|
||||
clearTimeout(state.connectTimeout);
|
||||
stopHealthCheck();
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
state.isConnecting = false;
|
||||
|
||||
if (ws) {
|
||||
ws.onopen = null;
|
||||
ws.onmessage = null;
|
||||
ws.onerror = null;
|
||||
ws.onclose = null;
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(1000, 'Cleanup');
|
||||
}
|
||||
|
||||
ws = null;
|
||||
window.ws = null;
|
||||
}
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
log('Disposing WebSocketManager');
|
||||
|
||||
this.disconnect();
|
||||
|
||||
if (state.listeners.visibilityChange) {
|
||||
document.removeEventListener('visibilitychange', state.listeners.visibilityChange);
|
||||
}
|
||||
|
||||
state.messageHandlers = {};
|
||||
state.listeners = {};
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
log('WebSocketManager paused');
|
||||
state.isIntentionallyClosed = true;
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(1000, 'WebSocketManager paused');
|
||||
}
|
||||
|
||||
stopHealthCheck();
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
log('WebSocketManager resumed');
|
||||
state.isIntentionallyClosed = false;
|
||||
|
||||
if (!this.isConnected()) {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
startHealthCheck();
|
||||
}
|
||||
};
|
||||
|
||||
function setupEventHandlers() {
|
||||
if (!ws) return;
|
||||
|
||||
ws.onopen = () => {
|
||||
state.isConnecting = false;
|
||||
config.reconnectAttempts = 0;
|
||||
clearTimeout(state.connectTimeout);
|
||||
state.lastHealthCheck = Date.now();
|
||||
window.ws = ws;
|
||||
|
||||
log('WebSocket connection established');
|
||||
|
||||
notifyHandlers('connect', { isConnected: true });
|
||||
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('connected');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
log('WebSocket message received:', message);
|
||||
notifyHandlers('message', message);
|
||||
} catch (error) {
|
||||
log('Error processing message:', error);
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
log('WebSocket error:', error);
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
notifyHandlers('error', error);
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
log('WebSocket closed:', event);
|
||||
state.isConnecting = false;
|
||||
window.ws = null;
|
||||
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('disconnected');
|
||||
}
|
||||
|
||||
notifyHandlers('disconnect', {
|
||||
code: event.code,
|
||||
reason: event.reason
|
||||
});
|
||||
|
||||
if (!state.isIntentionallyClosed) {
|
||||
handleReconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setupPageVisibilityHandler() {
|
||||
const visibilityChangeHandler = () => {
|
||||
if (document.hidden) {
|
||||
handlePageHidden();
|
||||
} else {
|
||||
handlePageVisible();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', visibilityChangeHandler);
|
||||
state.listeners.visibilityChange = visibilityChangeHandler;
|
||||
}
|
||||
|
||||
function handlePageHidden() {
|
||||
log('Page hidden');
|
||||
state.isPageHidden = true;
|
||||
stopHealthCheck();
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
state.isIntentionallyClosed = true;
|
||||
ws.close(1000, 'Page hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageVisible() {
|
||||
log('Page visible');
|
||||
state.isPageHidden = false;
|
||||
state.isIntentionallyClosed = false;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!publicAPI.isConnected()) {
|
||||
publicAPI.connect();
|
||||
}
|
||||
startHealthCheck();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function startHealthCheck() {
|
||||
stopHealthCheck();
|
||||
state.healthCheckInterval = setInterval(() => {
|
||||
performHealthCheck();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopHealthCheck() {
|
||||
if (state.healthCheckInterval) {
|
||||
clearInterval(state.healthCheckInterval);
|
||||
state.healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function performHealthCheck() {
|
||||
if (!publicAPI.isConnected()) {
|
||||
log('Health check failed - not connected');
|
||||
handleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const lastCheck = state.lastHealthCheck;
|
||||
|
||||
if (lastCheck && (now - lastCheck) > 60000) {
|
||||
log('Health check failed - too long since last check');
|
||||
handleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
state.lastHealthCheck = now;
|
||||
log('Health check passed');
|
||||
}
|
||||
|
||||
function handleReconnect() {
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
config.reconnectAttempts++;
|
||||
if (config.reconnectAttempts <= config.maxReconnectAttempts) {
|
||||
const delay = Math.min(
|
||||
config.reconnectDelay * Math.pow(1.5, config.reconnectAttempts - 1),
|
||||
30000
|
||||
);
|
||||
|
||||
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
|
||||
|
||||
state.reconnectTimeout = setTimeout(() => {
|
||||
state.reconnectTimeout = null;
|
||||
if (!state.isIntentionallyClosed) {
|
||||
publicAPI.connect();
|
||||
}
|
||||
}, delay);
|
||||
} else {
|
||||
log('Max reconnect attempts reached');
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
|
||||
state.reconnectTimeout = setTimeout(() => {
|
||||
state.reconnectTimeout = null;
|
||||
config.reconnectAttempts = 0;
|
||||
publicAPI.connect();
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyHandlers(type, data) {
|
||||
if (state.messageHandlers[type]) {
|
||||
Object.values(state.messageHandlers[type]).forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
} catch (error) {
|
||||
log(`Error in ${type} handler:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
log('Cleaning up WebSocket resources');
|
||||
|
||||
clearTimeout(state.connectTimeout);
|
||||
stopHealthCheck();
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
state.isConnecting = false;
|
||||
|
||||
if (ws) {
|
||||
ws.onopen = null;
|
||||
ws.onmessage = null;
|
||||
ws.onerror = null;
|
||||
ws.onclose = null;
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(1000, 'Cleanup');
|
||||
}
|
||||
|
||||
ws = null;
|
||||
window.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.WebSocketManager = WebSocketManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (!window.webSocketManagerInitialized) {
|
||||
window.WebSocketManager.initialize();
|
||||
window.webSocketManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
|
||||
console.log('WebSocketManager initialized');
|
||||
Reference in New Issue
Block a user