Lin / frontend /src /services /cacheService.js
Zelyanoth's picture
fff
25f22bf
raw
history blame
11 kB
import localforage from 'localforage';
class CacheService {
constructor() {
this.storage = localforage.createInstance({
name: 'LinAppCache',
storeName: 'authCache'
});
this.cachePrefix = 'lin_cache_';
this.defaultTTL = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
}
/**
* Generate a cache key with prefix
* @param {string} key - The cache key
* @returns {string} - Full cache key
*/
_generateKey(key) {
return `${this.cachePrefix}${key}`;
}
/**
* Set data in cache with TTL
* @param {string} key - Cache key
* @param {any} data - Data to cache
* @param {number} ttl - Time to live in milliseconds (optional)
* @returns {Promise<void>}
*/
async set(key, data, ttl = this.defaultTTL) {
const cacheKey = this._generateKey(key);
const expiry = Date.now() + ttl;
const cacheData = {
data,
expiry,
metadata: {
createdAt: Date.now(),
ttl,
key
}
};
try {
await this.storage.setItem(cacheKey, cacheData);
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.log(`πŸ—ƒοΈ [Cache] Set cache for key: ${key}`);
}
} catch (error) {
console.error(`πŸ—ƒοΈ [Cache] Error setting cache for key: ${key}`, error);
throw error;
}
}
/**
* Get data from cache
* @param {string} key - Cache key
* @returns {Promise<any|null>} - Cached data or null if not found/expired
*/
async get(key) {
const cacheKey = this._generateKey(key);
try {
const cacheData = await this.storage.getItem(cacheKey);
if (!cacheData) {
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.log(`πŸ—ƒοΈ [Cache] Cache miss for key: ${key}`);
}
return null;
}
// Check if cache has expired
if (Date.now() > cacheData.expiry) {
await this.remove(key);
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.log(`πŸ—ƒοΈ [Cache] Cache expired for key: ${key}`);
}
return null;
}
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.log(`πŸ—ƒοΈ [Cache] Cache hit for key: ${key}`);
}
return cacheData.data;
} catch (error) {
console.error(`πŸ—ƒοΈ [Cache] Error getting cache for key: ${key}`, error);
return null;
}
}
/**
* Remove data from cache
* @param {string} key - Cache key
* @returns {Promise<void>}
*/
async remove(key) {
const cacheKey = this._generateKey(key);
try {
await this.storage.removeItem(cacheKey);
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.log(`πŸ—ƒοΈ [Cache] Removed cache for key: ${key}`);
}
} catch (error) {
console.error(`πŸ—ƒοΈ [Cache] Error removing cache for key: ${key}`, error);
throw error;
}
}
/**
* Clear all cached data
* @returns {Promise<void>}
*/
async clear() {
try {
await this.storage.clear();
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.log('πŸ—ƒοΈ [Cache] Cleared all cache');
}
} catch (error) {
console.error('πŸ—ƒοΈ [Cache] Error clearing cache', error);
throw error;
}
}
/**
* Check if cache exists and is valid
* @param {string} key - Cache key
* @returns {Promise<boolean>} - True if cache exists and is valid
*/
async exists(key) {
const cacheKey = this._generateKey(key);
try {
const cacheData = await this.storage.getItem(cacheKey);
if (!cacheData) {
return false;
}
// Check if cache has expired
if (Date.now() > cacheData.expiry) {
await this.remove(key);
return false;
}
return true;
} catch (error) {
console.error(`πŸ—ƒοΈ [Cache] Error checking cache existence for key: ${key}`, error);
return false;
}
}
/**
* Get cache metadata
* @param {string} key - Cache key
* @returns {Promise<Object|null>} - Cache metadata or null
*/
async getMetadata(key) {
const cacheKey = this._generateKey(key);
try {
const cacheData = await this.storage.getItem(cacheKey);
if (!cacheData) {
return null;
}
return cacheData.metadata || null;
} catch (error) {
console.error(`πŸ—ƒοΈ [Cache] Error getting cache metadata for key: ${key}`, error);
return null;
}
}
/**
* Set user authentication cache
* @param {Object} authData - Authentication data
* @param {boolean} rememberMe - Remember me flag
* @returns {Promise<void>}
*/
async setAuthCache(authData, rememberMe = false) {
const ttl = rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session
await this.set('auth_token', authData.token, ttl);
await this.set('user_data', authData.user, ttl);
await this.set('remember_me', rememberMe, ttl);
await this.set('auth_expiry', Date.now() + ttl, ttl);
// Store device fingerprint for security
const deviceFingerprint = this.generateDeviceFingerprint();
await this.set('device_fingerprint', deviceFingerprint, ttl);
}
/**
* Get user authentication cache
* @returns {Promise<Object|null>} - Authentication cache data or null
*/
async getAuthCache() {
try {
const [token, userData, rememberMe, expiry, deviceFingerprint] = await Promise.all([
this.get('auth_token'),
this.get('user_data'),
this.get('remember_me'),
this.get('auth_expiry'),
this.get('device_fingerprint')
]);
if (!token || !userData || !expiry) {
return null;
}
// Check if cache has expired
if (Date.now() > expiry) {
await this.clearAuthCache();
return null;
}
// Validate device fingerprint for security
const currentFingerprint = this.generateDeviceFingerprint();
if (deviceFingerprint && deviceFingerprint !== currentFingerprint) {
await this.clearAuthCache();
return null;
}
return {
token,
user: userData,
rememberMe: rememberMe || false,
expiresAt: expiry,
deviceFingerprint
};
} catch (error) {
console.error('πŸ—ƒοΈ [Cache] Error getting auth cache', error);
return null;
}
}
/**
* Clear authentication cache
* @returns {Promise<void>}
*/
async clearAuthCache() {
const keys = ['auth_token', 'user_data', 'remember_me', 'auth_expiry', 'device_fingerprint'];
await Promise.all(keys.map(key => this.remove(key)));
// Also clear localStorage for consistency
localStorage.removeItem('token');
localStorage.removeItem('rememberMePreference');
}
/**
* Generate device fingerprint for security
* @returns {string} - Device fingerprint
*/
generateDeviceFingerprint() {
const userAgent = navigator.userAgent;
const screenResolution = `${screen.width}x${screen.height}`;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const language = navigator.language;
// Create a simple hash-like string
const fingerprint = `${userAgent}-${screenResolution}-${timezone}-${language}`;
return btoa(fingerprint).replace(/[^a-zA-Z0-9]/g, '').substring(0, 32);
}
/**
* Cache user preferences
* @param {Object} preferences - User preferences
* @returns {Promise<void>}
*/
async setUserPreferences(preferences) {
await this.set('user_preferences', preferences, 30 * 24 * 60 * 60 * 1000); // 30 days
}
/**
* Get user preferences
* @returns {Promise<Object|null>} - User preferences or null
*/
async getUserPreferences() {
return await this.get('user_preferences');
}
/**
* Cache API responses to reduce network requests
* @param {string} endpoint - API endpoint
* @param {Object} data - Response data
* @param {number} ttl - Cache TTL in milliseconds
* @returns {Promise<void>}
*/
async cacheApiResponse(endpoint, data, ttl = 5 * 60 * 1000) { // 5 minutes default
await this.set(`api_${endpoint}`, data, ttl);
}
/**
* Get cached API response
* @param {string} endpoint - API endpoint
* @returns {Promise<Object|null>} - Cached response or null
*/
async getCachedApiResponse(endpoint) {
return await this.get(`api_${endpoint}`);
}
/**
* Set authentication token
* @param {string} token - JWT token
* @param {boolean} rememberMe - Remember me flag
* @returns {Promise<void>}
*/
async setAuthToken(token, rememberMe = false) {
const ttl = rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session
await this.set('auth_token', token, ttl);
await this.set('remember_me', rememberMe, ttl);
await this.set('auth_expiry', Date.now() + ttl, ttl);
}
/**
* Get authentication token
* @returns {Promise<Object|null>} - Auth token data or null
*/
async getAuthToken() {
try {
const [token, rememberMe, expiry] = await Promise.all([
this.get('auth_token'),
this.get('remember_me'),
this.get('auth_expiry')
]);
if (!token || !expiry) {
return null;
}
// Check if cache has expired
if (Date.now() > expiry) {
await this.clearAuthToken();
return null;
}
return {
token,
rememberMe: rememberMe || false,
expiresAt: expiry
};
} catch (error) {
console.error('πŸ—ƒοΈ [Cache] Error getting auth token', error);
return null;
}
}
/**
* Clear authentication token
* @returns {Promise<void>}
*/
async clearAuthToken() {
const keys = ['auth_token', 'remember_me', 'auth_expiry'];
await Promise.all(keys.map(key => this.remove(key)));
localStorage.removeItem('token');
localStorage.removeItem('rememberMePreference');
}
/**
* Set user data
* @param {Object} userData - User data object
* @param {boolean} rememberMe - Remember me flag
* @returns {Promise<void>}
*/
async setUserData(userData, rememberMe = false) {
const ttl = rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session
await this.set('user_data', userData, ttl);
}
/**
* Get user data
* @returns {Promise<Object|null>} - User data or null
*/
async getUserData() {
return await this.get('user_data');
}
/**
* Set session information
* @param {Object} sessionData - Session data
* @returns {Promise<void>}
*/
async setSessionInfo(sessionData) {
const ttl = sessionData.rememberMe ? this.defaultTTL : 60 * 60 * 1000; // 1 hour for normal session
await this.set('session_info', sessionData, ttl);
}
/**
* Get session information
* @returns {Promise<Object|null>} - Session data or null
*/
async getSessionInfo() {
return await this.get('session_info');
}
}
// Export singleton instance
export default new CacheService();