@cygnus-wealth/asset-valuator
Version:
Asset valuation library for retrieving and converting cryptocurrency prices
213 lines (212 loc) • 8.41 kB
JavaScript
import { hasIndexedDB, hasLocalStorage } from './browser-detect.js';
export class EdgeCache {
constructor(options = {}) {
this.memoryCache = new Map();
this.dbName = 'asset-valuator-cache';
this.storeName = 'prices';
this.db = null;
this.options = {
defaultTTL: options.defaultTTL || 60000, // 60 seconds
maxSize: options.maxSize || 1000,
storage: options.storage || 'memory'
};
if (this.options.storage === 'indexedDB' && hasIndexedDB()) {
this.initIndexedDB();
}
}
async initIndexedDB() {
try {
const request = globalThis.window.indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'key' });
}
};
this.db = await new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
catch (error) {
console.warn('IndexedDB initialization failed, falling back to memory cache', error);
this.options.storage = 'memory';
}
}
async get(key) {
const now = Date.now();
switch (this.options.storage) {
case 'localStorage':
if (hasLocalStorage()) {
try {
const stored = localStorage.getItem(`cache_${key}`);
if (stored) {
const entry = JSON.parse(stored);
if (now - entry.timestamp < entry.ttl) {
return entry.data;
}
localStorage.removeItem(`cache_${key}`);
}
}
catch (error) {
console.warn('localStorage read failed', error);
}
}
break;
case 'indexedDB':
if (this.db) {
try {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(key);
const result = await new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (result && now - result.timestamp < result.ttl) {
return result.data;
}
// Clean up expired entry
if (result) {
this.delete(key);
}
}
catch (error) {
console.warn('IndexedDB read failed', error);
}
}
break;
default: // memory
const entry = this.memoryCache.get(key);
if (entry && now - entry.timestamp < entry.ttl) {
return entry.data;
}
if (entry) {
this.memoryCache.delete(key);
}
}
return null;
}
async set(key, data, ttl) {
const entry = {
data,
timestamp: Date.now(),
ttl: ttl || this.options.defaultTTL
};
switch (this.options.storage) {
case 'localStorage':
if (hasLocalStorage()) {
try {
localStorage.setItem(`cache_${key}`, JSON.stringify(entry));
// Enforce max size for localStorage
if (localStorage.length > this.options.maxSize) {
this.evictOldest();
}
}
catch (error) {
console.warn('localStorage write failed', error);
// Fall back to memory cache
this.memoryCache.set(key, entry);
}
}
break;
case 'indexedDB':
if (this.db) {
try {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await new Promise((resolve, reject) => {
const request = store.put({ key, ...entry });
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
catch (error) {
console.warn('IndexedDB write failed', error);
// Fall back to memory cache
this.memoryCache.set(key, entry);
}
}
break;
default: // memory
this.memoryCache.set(key, entry);
// Enforce max size for memory cache
if (this.memoryCache.size > this.options.maxSize) {
const firstKey = this.memoryCache.keys().next().value;
this.memoryCache.delete(firstKey);
}
}
}
async delete(key) {
switch (this.options.storage) {
case 'localStorage':
if (hasLocalStorage()) {
localStorage.removeItem(`cache_${key}`);
}
break;
case 'indexedDB':
if (this.db) {
try {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
store.delete(key);
}
catch (error) {
console.warn('IndexedDB delete failed', error);
}
}
break;
default:
this.memoryCache.delete(key);
}
}
async clear() {
switch (this.options.storage) {
case 'localStorage':
if (hasLocalStorage()) {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith('cache_')) {
localStorage.removeItem(key);
}
});
}
break;
case 'indexedDB':
if (this.db) {
try {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
store.clear();
}
catch (error) {
console.warn('IndexedDB clear failed', error);
}
}
break;
default:
this.memoryCache.clear();
}
}
evictOldest() {
if (hasLocalStorage()) {
const cacheKeys = Object.keys(localStorage)
.filter(key => key.startsWith('cache_'))
.map(key => {
try {
const entry = JSON.parse(localStorage.getItem(key) || '{}');
return { key, timestamp: entry.timestamp || 0 };
}
catch {
return { key, timestamp: 0 };
}
})
.sort((a, b) => a.timestamp - b.timestamp);
// Remove oldest 10% of entries
const toRemove = Math.ceil(cacheKeys.length * 0.1);
cacheKeys.slice(0, toRemove).forEach(({ key }) => {
localStorage.removeItem(key);
});
}
}
}