mitlist/fe/src/utils/cache.ts
mohamad 5a2e80eeee feat: Enhance WebSocket connection handling and introduce skeleton components
This commit includes several improvements and new features:

- Updated the WebSocket connection logic in `websocket.py` to include connection status messages and periodic pings for maintaining the connection.
- Introduced new skeleton components (`Skeleton.vue`, `SkeletonDashboard.vue`, `SkeletonList.vue`) for improved loading states in the UI, enhancing user experience during data fetching.
- Refactored the Vite configuration to support advanced code splitting and caching strategies, optimizing the build process.
- Enhanced ESLint configuration for better compatibility with project structure.

These changes aim to improve real-time communication, user interface responsiveness, and overall application performance.
2025-06-28 23:02:23 +02:00

369 lines
9.1 KiB
TypeScript

/**
* Advanced caching utility for Vue 3 applications
* Supports TTL, memory limits, persistence, and cache invalidation
*/
import { ref, computed, onUnmounted } from 'vue';
export interface CacheOptions {
ttl?: number; // Time to live in milliseconds
maxSize?: number; // Maximum cache size
persistent?: boolean; // Store in localStorage
keyPrefix?: string; // Prefix for localStorage keys
}
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl?: number;
hits: number;
lastAccessed: number;
}
class CacheManager<T = any> {
private cache = new Map<string, CacheEntry<T>>();
private options: Required<CacheOptions>;
private cleanupInterval?: number;
constructor(options: CacheOptions = {}) {
this.options = {
ttl: 5 * 60 * 1000, // 5 minutes default
maxSize: 100,
persistent: false,
keyPrefix: 'app_cache_',
...options
};
// Setup cleanup interval for expired entries
this.cleanupInterval = window.setInterval(() => {
this.cleanup();
}, 60000); // Cleanup every minute
// Load from localStorage if persistent
if (this.options.persistent) {
this.loadFromStorage();
}
}
/**
* Store data in cache
*/
set(key: string, data: T, customTtl?: number): void {
const now = Date.now();
const ttl = customTtl || this.options.ttl;
// Remove oldest entries if cache is full
if (this.cache.size >= this.options.maxSize) {
this.evictLRU();
}
const entry: CacheEntry<T> = {
data,
timestamp: now,
ttl,
hits: 0,
lastAccessed: now
};
this.cache.set(key, entry);
// Persist to localStorage if enabled
if (this.options.persistent) {
this.saveToStorage(key, entry);
}
}
/**
* Get data from cache
*/
get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
const now = Date.now();
// Check if entry has expired
if (entry.ttl && (now - entry.timestamp) > entry.ttl) {
this.delete(key);
return null;
}
// Update access statistics
entry.hits++;
entry.lastAccessed = now;
return entry.data;
}
/**
* Check if key exists and is not expired
*/
has(key: string): boolean {
return this.get(key) !== null;
}
/**
* Delete specific key
*/
delete(key: string): boolean {
const deleted = this.cache.delete(key);
if (deleted && this.options.persistent) {
localStorage.removeItem(this.options.keyPrefix + key);
}
return deleted;
}
/**
* Clear all cache entries
*/
clear(): void {
if (this.options.persistent) {
// Remove all persistent entries
Object.keys(localStorage).forEach(key => {
if (key.startsWith(this.options.keyPrefix)) {
localStorage.removeItem(key);
}
});
}
this.cache.clear();
}
/**
* Get cache statistics
*/
getStats() {
const entries = Array.from(this.cache.values());
const now = Date.now();
return {
size: this.cache.size,
maxSize: this.options.maxSize,
expired: entries.filter(e => e.ttl && (now - e.timestamp) > e.ttl).length,
totalHits: entries.reduce((sum, e) => sum + e.hits, 0),
averageAge: entries.length > 0
? entries.reduce((sum, e) => sum + (now - e.timestamp), 0) / entries.length
: 0
};
}
/**
* Remove expired entries
*/
private cleanup(): void {
const now = Date.now();
const keysToDelete: string[] = [];
this.cache.forEach((entry, key) => {
if (entry.ttl && (now - entry.timestamp) > entry.ttl) {
keysToDelete.push(key);
}
});
keysToDelete.forEach(key => this.delete(key));
}
/**
* Evict least recently used entry
*/
private evictLRU(): void {
let oldestKey = '';
let oldestTime = Date.now();
this.cache.forEach((entry, key) => {
if (entry.lastAccessed < oldestTime) {
oldestTime = entry.lastAccessed;
oldestKey = key;
}
});
if (oldestKey) {
this.delete(oldestKey);
}
}
/**
* Save entry to localStorage
*/
private saveToStorage(key: string, entry: CacheEntry<T>): void {
try {
const storageKey = this.options.keyPrefix + key;
localStorage.setItem(storageKey, JSON.stringify(entry));
} catch (error) {
console.warn('Failed to save cache entry to localStorage:', error);
}
}
/**
* Load cache from localStorage
*/
private loadFromStorage(): void {
try {
Object.keys(localStorage).forEach(storageKey => {
if (storageKey.startsWith(this.options.keyPrefix)) {
const key = storageKey.replace(this.options.keyPrefix, '');
const entryData = localStorage.getItem(storageKey);
if (entryData) {
const entry: CacheEntry<T> = JSON.parse(entryData);
const now = Date.now();
// Check if entry is still valid
if (!entry.ttl || (now - entry.timestamp) <= entry.ttl) {
this.cache.set(key, entry);
} else {
// Remove expired entry from storage
localStorage.removeItem(storageKey);
}
}
}
});
} catch (error) {
console.warn('Failed to load cache from localStorage:', error);
}
}
/**
* Cleanup resources
*/
destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
}
}
// Global cache instances
const apiCache = new CacheManager({
ttl: 5 * 60 * 1000, // 5 minutes
maxSize: 50,
persistent: true,
keyPrefix: 'api_'
});
const uiCache = new CacheManager({
ttl: 30 * 60 * 1000, // 30 minutes
maxSize: 100,
persistent: false,
keyPrefix: 'ui_'
});
/**
* Composable for reactive caching
*/
export function useCache<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions = {}
) {
const cache = options.persistent !== false ? apiCache : uiCache;
const data = ref<T | null>(cache.get(key));
const isLoading = ref(false);
const error = ref<Error | null>(null);
const isStale = computed(() => {
return data.value === null;
});
const refresh = async (force = false): Promise<T | null> => {
if (isLoading.value) return data.value;
if (!force && !isStale.value) {
return data.value;
}
isLoading.value = true;
error.value = null;
try {
const result = await fetcher();
cache.set(key, result, options.ttl);
data.value = result;
return result;
} catch (err) {
error.value = err instanceof Error ? err : new Error(String(err));
throw err;
} finally {
isLoading.value = false;
}
};
const invalidate = () => {
cache.delete(key);
data.value = null;
};
// Auto-fetch if data is stale
if (isStale.value) {
refresh();
}
// Cleanup on unmount
onUnmounted(() => {
// Cache persists beyond component lifecycle
});
return {
data: computed(() => data.value),
isLoading: computed(() => isLoading.value),
error: computed(() => error.value),
isStale,
refresh,
invalidate
};
}
/**
* Preload data into cache
*/
export async function preloadCache<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions = {}
): Promise<void> {
const cache = options.persistent !== false ? apiCache : uiCache;
if (!cache.has(key)) {
try {
const data = await fetcher();
cache.set(key, data, options.ttl);
} catch (error) {
console.warn(`Failed to preload cache for key: ${key}`, error);
}
}
}
/**
* Cache invalidation utilities
*/
export const cacheUtils = {
invalidatePattern: (pattern: string) => {
[apiCache, uiCache].forEach(cache => {
Array.from(cache['cache'].keys()).forEach(key => {
if (key.includes(pattern)) {
cache.delete(key);
}
});
});
},
invalidateAll: () => {
apiCache.clear();
uiCache.clear();
},
getStats: () => ({
api: apiCache.getStats(),
ui: uiCache.getStats()
})
};
// Export instances for direct use
export { apiCache, uiCache };
export default CacheManager;