Files
KefinTweaks/localStorageCache.js
T

270 lines
9.9 KiB
JavaScript

// KefinTweaks localStorage Cache Manager
// Provides 24-hour TTL caching with manual refresh functionality
// Usage: Integrates with existing cache system to add persistence
(function() {
'use strict';
// Common logging function
const LOG = (...args) => console.log('[LocalStorageCache]', ...args);
const WARN = (...args) => console.warn('[LocalStorageCache]', ...args);
const ERR = (...args) => console.error('[LocalStorageCache]', ...args);
// Generic localStorage cache manager with chunked storage support
class LocalStorageCache {
constructor(prefix = 'kefinTweaks_', ttl = 24 * 60 * 60 * 1000, chunkSize = 10) { // 24 hours, 10 items per chunk
this.prefix = prefix;
this.ttl = ttl;
this.chunkSize = chunkSize;
}
// Generate cache key with user context
getCacheKey(cacheName, userId = null) {
const user = userId || window.ApiClient?.getCurrentUserId() || 'anonymous';
return `${this.prefix}${cacheName}_${user}`;
}
// Check if cache is valid (not expired)
isCacheValid(cacheName, userId = null) {
const key = this.getCacheKey(cacheName, userId);
const cached = localStorage.getItem(key);
if (!cached) return false;
try {
const data = JSON.parse(cached);
const now = Date.now();
const ttl = data.ttl || this.ttl; // Use stored TTL or default
return (now - data.timestamp) < ttl;
} catch {
return false;
}
}
// Get cached data if valid
get(cacheName, userId = null) {
if (!this.isCacheValid(cacheName, userId)) {
return null;
}
const key = this.getCacheKey(cacheName, userId);
try {
const cached = localStorage.getItem(key);
const data = JSON.parse(cached);
return data.payload;
} catch {
return null;
}
}
// Store data with timestamp
set(cacheName, data, userId = null, customTtl = null) {
const key = this.getCacheKey(cacheName, userId);
const ttl = customTtl || this.ttl;
const payload = {
timestamp: Date.now(),
ttl: ttl,
payload: data
};
try {
localStorage.setItem(key, JSON.stringify(payload));
LOG(`Cached data for ${cacheName}: ${Array.isArray(data) ? data.length : 'object'} items (TTL: ${ttl / (1000 * 60 * 60)}h)`);
return true;
} catch (error) {
WARN('Failed to store data:', error);
return false;
}
}
// Clear specific cache
clear(cacheName, userId = null) {
const key = this.getCacheKey(cacheName, userId);
localStorage.removeItem(key);
LOG(`Cleared cache for ${cacheName}`);
}
// Clear all caches for current user
clearAll(userId = null) {
const user = userId || window.ApiClient?.getCurrentUserId() || 'anonymous';
const keys = Object.keys(localStorage).filter(key =>
key.startsWith(this.prefix) && key.endsWith(`_${user}`)
);
keys.forEach(key => localStorage.removeItem(key));
LOG(`Cleared all caches for user ${user}: ${keys.length} entries`);
}
// Get cache age in hours
getCacheAge(cacheName, userId = null) {
const key = this.getCacheKey(cacheName, userId);
const cached = localStorage.getItem(key);
if (!cached) return null;
try {
const data = JSON.parse(cached);
return (Date.now() - data.timestamp) / (1000 * 60 * 60); // hours
} catch {
return null;
}
}
// Get cache size in bytes (approximate)
getCacheSize(cacheName, userId = null) {
const key = this.getCacheKey(cacheName, userId);
const cached = localStorage.getItem(key);
return cached ? cached.length : 0;
}
// Get all cache names for current user
getAllCacheNames(userId = null) {
const user = userId || window.ApiClient?.getCurrentUserId() || 'anonymous';
const keys = Object.keys(localStorage).filter(key =>
key.startsWith(this.prefix) && key.endsWith(`_${user}`)
);
return keys.map(key => key.replace(this.prefix, '').replace(`_${user}`, ''));
}
// Chunked storage methods for large datasets
setChunked(cacheName, data, userId = null) {
if (!Array.isArray(data)) {
WARN('Chunked storage requires array data');
return false;
}
if (data.length === 0) {
LOG(`No data to chunk for ${cacheName}`);
return true;
}
try {
const chunks = this.chunkArray(data, this.chunkSize);
const chunkCount = chunks.length;
LOG(`Storing ${data.length} items in ${chunkCount} chunks for ${cacheName}`);
// Store each chunk
for (let i = 0; i < chunks.length; i++) {
const chunkKey = `${cacheName}_chunk_${i}`;
const chunkData = {
chunkIndex: i,
totalChunks: chunkCount,
data: chunks[i],
timestamp: Date.now()
};
if (!this.set(chunkKey, chunkData, userId)) {
WARN(`Failed to store chunk ${i} for ${cacheName}`);
return false;
}
}
// Store metadata
const metadata = {
totalChunks: chunkCount,
totalItems: data.length,
timestamp: Date.now(),
chunkSize: this.chunkSize
};
if (!this.set(`${cacheName}_meta`, metadata, userId)) {
WARN(`Failed to store metadata for ${cacheName}`);
return false;
}
LOG(`Successfully stored ${data.length} items in ${chunkCount} chunks for ${cacheName}`);
return true;
} catch (error) {
ERR(`Error storing chunked data for ${cacheName}:`, error);
return false;
}
}
// Load chunked data
getChunked(cacheName, userId = null) {
try {
const meta = this.get(`${cacheName}_meta`, userId);
if (!meta) {
LOG(`No metadata found for chunked cache ${cacheName}`);
return null;
}
if (!this.isCacheValid(`${cacheName}_meta`, userId)) {
LOG(`Chunked cache ${cacheName} has expired`);
return null;
}
const allData = [];
let missingChunks = 0;
for (let i = 0; i < meta.totalChunks; i++) {
const chunk = this.get(`${cacheName}_chunk_${i}`, userId);
if (chunk && chunk.data) {
allData.push(...chunk.data);
} else {
missingChunks++;
WARN(`Missing chunk ${i} for ${cacheName}`);
}
}
if (missingChunks > 0) {
WARN(`Missing ${missingChunks} chunks for ${cacheName}, returning null`);
return null;
}
if (allData.length !== meta.totalItems) {
WARN(`Data length mismatch for ${cacheName}: expected ${meta.totalItems}, got ${allData.length}`);
return null;
}
LOG(`Successfully loaded ${allData.length} items from ${meta.totalChunks} chunks for ${cacheName}`);
return allData;
} catch (error) {
ERR(`Error loading chunked data for ${cacheName}:`, error);
return null;
}
}
// Clear chunked cache
clearChunked(cacheName, userId = null) {
try {
const meta = this.get(`${cacheName}_meta`, userId);
if (meta) {
// Clear all chunks
for (let i = 0; i < meta.totalChunks; i++) {
this.clear(`${cacheName}_chunk_${i}`, userId);
}
// Clear metadata
this.clear(`${cacheName}_meta`, userId);
LOG(`Cleared chunked cache ${cacheName} (${meta.totalChunks} chunks)`);
} else {
LOG(`No chunked cache found for ${cacheName}`);
}
} catch (error) {
ERR(`Error clearing chunked cache ${cacheName}:`, error);
}
}
// Utility method to chunk arrays
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// Check if a cache is chunked
isChunked(cacheName, userId = null) {
const meta = this.get(`${cacheName}_meta`, userId);
return meta && meta.totalChunks > 1;
}
}
// Create global instance
window.LocalStorageCache = LocalStorageCache;
LOG('LocalStorageCache manager loaded');
})();