// Pinia plugin for persisted state with performance optimizations import { debounce } from '@/utils/performance' const STORAGE_KEY_PREFIX = 'marketing_agent_' /** * Create persisted state plugin for Pinia */ export function createPersistedState(options = {}) { const { storage = localStorage, paths = [], debounceTime = 1000, serializer = JSON, filter = () => true } = options return (context) => { const { store } = context const storeId = store.$id const storageKey = STORAGE_KEY_PREFIX + storeId // Load initial state try { const savedState = storage.getItem(storageKey) if (savedState) { const parsed = serializer.parse(savedState) // Only restore specified paths or all if paths is empty if (paths.length === 0) { store.$patch(parsed) } else { const filtered = {} paths.forEach(path => { const keys = path.split('.') let source = parsed let target = filtered keys.forEach((key, index) => { if (index === keys.length - 1) { if (source && key in source) { target[key] = source[key] } } else { if (!target[key]) target[key] = {} target = target[key] source = source ? source[key] : undefined } }) }) store.$patch(filtered) } } } catch (error) { console.error(`Failed to load persisted state for ${storeId}:`, error) } // Save state changes with debouncing const saveState = debounce(() => { try { const state = store.$state let toSave = state // Filter state if paths are specified if (paths.length > 0) { toSave = {} paths.forEach(path => { const keys = path.split('.') let source = state let target = toSave keys.forEach((key, index) => { if (index === keys.length - 1) { if (source && key in source) { target[key] = source[key] } } else { if (!target[key]) target[key] = {} target = target[key] source = source ? source[key] : undefined } }) }) } // Apply custom filter if (filter(toSave)) { storage.setItem(storageKey, serializer.stringify(toSave)) } } catch (error) { console.error(`Failed to save state for ${storeId}:`, error) } }, debounceTime) // Subscribe to state changes store.$subscribe((mutation, state) => { saveState() }) // Subscribe to actions for immediate save on specific actions store.$onAction(({ name, after }) => { // Save immediately after logout or critical actions if (['logout', 'clearData', 'resetStore'].includes(name)) { after(() => { saveState.cancel() try { if (name === 'logout' || name === 'clearData') { storage.removeItem(storageKey) } else { storage.setItem(storageKey, serializer.stringify(store.$state)) } } catch (error) { console.error(`Failed to handle action ${name}:`, error) } }) } }) } } /** * Secure storage wrapper with encryption */ export class SecureStorage { constructor(secretKey) { this.secretKey = secretKey } async encrypt(data) { const encoder = new TextEncoder() const dataBuffer = encoder.encode(JSON.stringify(data)) const key = await crypto.subtle.importKey( 'raw', encoder.encode(this.secretKey), { name: 'AES-GCM', length: 256 }, false, ['encrypt'] ) const iv = crypto.getRandomValues(new Uint8Array(12)) const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, dataBuffer ) const combined = new Uint8Array(iv.length + encrypted.byteLength) combined.set(iv) combined.set(new Uint8Array(encrypted), iv.length) return btoa(String.fromCharCode(...combined)) } async decrypt(encryptedData) { const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0)) const iv = combined.slice(0, 12) const encrypted = combined.slice(12) const encoder = new TextEncoder() const key = await crypto.subtle.importKey( 'raw', encoder.encode(this.secretKey), { name: 'AES-GCM', length: 256 }, false, ['decrypt'] ) const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, encrypted ) const decoder = new TextDecoder() return JSON.parse(decoder.decode(decrypted)) } setItem(key, value) { return this.encrypt(value).then(encrypted => { localStorage.setItem(key, encrypted) }) } getItem(key) { const encrypted = localStorage.getItem(key) if (!encrypted) return Promise.resolve(null) return this.decrypt(encrypted).catch(error => { console.error('Decryption failed:', error) localStorage.removeItem(key) return null }) } removeItem(key) { localStorage.removeItem(key) } } /** * IndexedDB storage for large data */ export class IndexedDBStorage { constructor(dbName = 'marketing_agent_store', storeName = 'state') { this.dbName = dbName this.storeName = storeName this.db = null } async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1) request.onerror = () => reject(request.error) request.onsuccess = () => { this.db = request.result resolve() } request.onupgradeneeded = (event) => { const db = event.target.result if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName) } } }) } async setItem(key, value) { if (!this.db) await this.init() return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite') const store = transaction.objectStore(this.storeName) const request = store.put(value, key) request.onsuccess = () => resolve() request.onerror = () => reject(request.error) }) } async getItem(key) { if (!this.db) await this.init() return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readonly') const store = transaction.objectStore(this.storeName) const request = store.get(key) request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } async removeItem(key) { if (!this.db) await this.init() return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite') const store = transaction.objectStore(this.storeName) const request = store.delete(key) request.onsuccess = () => resolve() request.onerror = () => reject(request.error) }) } }