/***
|
* A simpler cache, using localStorage
|
* Mainly used to store settings locally
|
* example:
|
* -> dark/light mode switch
|
* -> view mode selection
|
* -> tab navigation direction (for table views)
|
**/
|
class SimpleCache {
|
/**
|
*
|
* @param {string} base
|
* @param {object} config
|
* @param {string} config.namespace
|
* @param {number} config.TTL
|
* @param {number} config.maxSize
|
*/
|
constructor(base, config = {}) {
|
this.base = base;
|
this.config = {
|
namespace: `${jvbBase.base}cache`,
|
TTL: 3600000,
|
maxSize: 100,
|
...config
|
};
|
|
|
// Initialize memory cache
|
this._cache = new Map();
|
this.subscribers = new Set();
|
}
|
|
/**
|
* Clear all memory cache
|
* @returns {number} Number of items cleared
|
*/
|
clearMemoryCache() {
|
const count = this._cache.size;
|
this._cache.clear();
|
|
console.log(`Cleared ${count} items from memory cache`);
|
return count;
|
}
|
|
/**
|
* Get a setting value
|
*/
|
get(key) {
|
//Check memory cache first
|
if (this._cache.has(key)) {
|
return this._cache.get(key);
|
}
|
|
let cacheKey = `${this.base}_${key}`;
|
let item;
|
try {
|
item = localStorage.getItem(cacheKey);
|
if (!item) {
|
return null;
|
}
|
item = JSON.parse(item);
|
} catch (error) {
|
console.warn('Error getting from localStorage:', error);
|
return null;
|
}
|
|
if (item) {
|
this._cache.set(key, item);
|
}
|
return item;
|
}
|
|
/**
|
* Set a setting value
|
*/
|
set(key, value) {
|
this._cache.set(key, value);
|
let cacheKey = `${this.base}_${key}`;
|
|
try {
|
localStorage.setItem(cacheKey, JSON.stringify(value));
|
} catch (error) {
|
// Handle quota exceeded
|
if (error instanceof DOMException && error.code === 22) {
|
this.clearOldestLocalStorageItems();
|
try {
|
localStorage.setItem(key, JSON.stringify(item));
|
} catch (retryError) {
|
console.warn('Still failed to set localStorage item after cleanup:', retryError);
|
}
|
} else {
|
console.warn('Error setting localStorage item:', error);
|
}
|
}
|
|
// Notify subscribers
|
this.notify('cache-saved', { key, value });
|
}
|
|
remove(key) {
|
let cacheKey = `${this.base}_${key}`;
|
try {
|
localStorage.removeItem(cacheKey);
|
} catch (error) {
|
console.warn('Error removing localStorage item:', error);
|
}
|
}
|
|
/**
|
* Clear oldest items from localStorage when quota is exceeded
|
*/
|
clearOldestLocalStorageItems() {
|
try {
|
const keysToRemove = [];
|
|
// Find all our cache keys
|
for (let i = 0; i < localStorage.length; i++) {
|
const key = localStorage.key(i);
|
if (key.startsWith(this.config.namespace)) {
|
try {
|
const item = JSON.parse(localStorage.getItem(key));
|
keysToRemove.push({ key, timestamp: item.timestamp || 0 });
|
} catch (e) {
|
// If it's not valid JSON or doesn't have a timestamp, prioritize for removal
|
keysToRemove.push({ key, timestamp: 0 });
|
}
|
}
|
}
|
|
// Sort by timestamp (oldest first)
|
keysToRemove.sort((a, b) => a.timestamp - b.timestamp);
|
|
// Remove the oldest 20% of items
|
const removeCount = Math.max(1, Math.ceil(keysToRemove.length * 0.2));
|
for (let i = 0; i < removeCount; i++) {
|
if (keysToRemove[i]) {
|
localStorage.removeItem(keysToRemove[i].key);
|
}
|
}
|
} catch (error) {
|
console.warn('Error cleaning up localStorage:', error);
|
}
|
}
|
|
async loadFromCache() {
|
for (let i = 0; i < localStorage.length; i++) {
|
const key = localStorage.key(i);
|
// Check if key starts with this cache's base prefix
|
if (key.startsWith(`${this.base}_`)) {
|
let cleanKey = key.replace(`${this.base}_`, '');
|
try {
|
// Parse the JSON value before caching
|
const value = JSON.parse(localStorage.getItem(key));
|
this._cache.set(cleanKey, value);
|
} catch (error) {
|
console.warn(`Failed to parse cached value for ${key}:`, error);
|
}
|
}
|
}
|
}
|
|
/**
|
* Clear all cache
|
*
|
* @returns {Promise<void>}
|
*/
|
async clear() {
|
this._cache.clear();
|
|
try {
|
for (let i = localStorage.length - 1; i >= 0; i--) {
|
const key = localStorage.key(i);
|
if (key && key.startsWith(this.config.namespace)) {
|
localStorage.removeItem(key);
|
}
|
}
|
} catch (error) {
|
console.warn('Error clearing localStorage cache:', error);
|
}
|
}
|
|
/**
|
* Subscribe to setting changes
|
*/
|
subscribe(callback) {
|
this.subscribers.add(callback);
|
return () => this.subscribers.delete(callback);
|
}
|
|
/**
|
* Notify subscribers
|
*/
|
notify(event, data) {
|
this.subscribers.forEach(cb => cb(event, data));
|
}
|
}
|
|
window.jvbCache = SimpleCache;
|