
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.
369 lines
9.1 KiB
TypeScript
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;
|