mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
270 lines
9.9 KiB
JavaScript
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');
|
|
|
|
})();
|