/** * Design System Server (DSS) - IndexedDB Storage * * Browser-side storage for offline-first operation: * - Projects, components, tokens (synced from backend) * - UI state persistence * - Request queue for offline operations * * Uses IndexedDB for structured data with good performance. */ const DB_NAME = 'dss'; const DB_VERSION = 2; class DssDB { constructor() { this.db = null; this.ready = this.init(); } // === Initialization === async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; console.log('[DssDB] Database ready'); resolve(this.db); }; request.onupgradeneeded = (event) => { const db = event.target.result; this.createStores(db); }; }); } createStores(db) { // Projects if (!db.objectStoreNames.contains('projects')) { const store = db.createObjectStore('projects', { keyPath: 'id' }); store.createIndex('status', 'status', { unique: false }); store.createIndex('updated_at', 'updated_at', { unique: false }); } // Components if (!db.objectStoreNames.contains('components')) { const store = db.createObjectStore('components', { keyPath: 'id' }); store.createIndex('project_id', 'project_id', { unique: false }); store.createIndex('name', 'name', { unique: false }); } // Tokens if (!db.objectStoreNames.contains('tokens')) { const store = db.createObjectStore('tokens', { keyPath: 'id' }); store.createIndex('project_id', 'project_id', { unique: false }); store.createIndex('category', 'category', { unique: false }); } // Styles if (!db.objectStoreNames.contains('styles')) { const store = db.createObjectStore('styles', { keyPath: 'id' }); store.createIndex('project_id', 'project_id', { unique: false }); store.createIndex('type', 'type', { unique: false }); } // Sync Queue (offline operations) if (!db.objectStoreNames.contains('sync_queue')) { const store = db.createObjectStore('sync_queue', { keyPath: 'id', autoIncrement: true }); store.createIndex('status', 'status', { unique: false }); store.createIndex('created_at', 'created_at', { unique: false }); } // Activity Log if (!db.objectStoreNames.contains('activity')) { const store = db.createObjectStore('activity', { keyPath: 'id', autoIncrement: true }); store.createIndex('project_id', 'project_id', { unique: false }); store.createIndex('created_at', 'created_at', { unique: false }); } // Cache (with TTL) if (!db.objectStoreNames.contains('cache')) { const store = db.createObjectStore('cache', { keyPath: 'key' }); store.createIndex('expires_at', 'expires_at', { unique: false }); } // UI State if (!db.objectStoreNames.contains('ui_state')) { db.createObjectStore('ui_state', { keyPath: 'key' }); } // Notifications if (!db.objectStoreNames.contains('notifications')) { const store = db.createObjectStore('notifications', { keyPath: 'id' }); store.createIndex('read', 'read', { unique: false }); store.createIndex('timestamp', 'timestamp', { unique: false }); store.createIndex('type', 'type', { unique: false }); } console.log('[DssDB] Stores created'); } // === Generic Operations === async transaction(storeName, mode = 'readonly') { await this.ready; return this.db.transaction(storeName, mode).objectStore(storeName); } async put(storeName, data) { await this.ready; return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, 'readwrite'); const store = tx.objectStore(storeName); const request = store.put(data); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async get(storeName, key) { await this.ready; return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const request = store.get(key); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async getAll(storeName) { await this.ready; return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async getAllByIndex(storeName, indexName, value) { await this.ready; return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const index = store.index(indexName); const request = index.getAll(value); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async delete(storeName, key) { await this.ready; return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, 'readwrite'); const store = tx.objectStore(storeName); const request = store.delete(key); request.onsuccess = () => resolve(true); request.onerror = () => reject(request.error); }); } async clear(storeName) { await this.ready; return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, 'readwrite'); const store = tx.objectStore(storeName); const request = store.clear(); request.onsuccess = () => resolve(true); request.onerror = () => reject(request.error); }); } async count(storeName) { await this.ready; return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const request = store.count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // === Cache with TTL === async cacheSet(key, value, ttlSeconds = 300) { const data = { key, value, created_at: Date.now(), expires_at: Date.now() + (ttlSeconds * 1000) }; return this.put('cache', data); } async cacheGet(key) { const data = await this.get('cache', key); if (!data) return null; // Check expiration if (Date.now() > data.expires_at) { await this.delete('cache', key); return null; } return data.value; } async cacheClear() { return this.clear('cache'); } async cacheCleanExpired() { await this.ready; const now = Date.now(); return new Promise((resolve, reject) => { const tx = this.db.transaction('cache', 'readwrite'); const store = tx.objectStore('cache'); const index = store.index('expires_at'); const range = IDBKeyRange.upperBound(now); let count = 0; const request = index.openCursor(range); request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { store.delete(cursor.primaryKey); count++; cursor.continue(); } else { resolve(count); } }; request.onerror = () => reject(request.error); }); } // === Projects === async saveProject(project) { return this.put('projects', { ...project, updated_at: new Date().toISOString() }); } async getProject(id) { return this.get('projects', id); } async getProjects() { return this.getAll('projects'); } // === Components === async saveComponents(projectId, components) { for (const comp of components) { await this.put('components', { id: comp.id || `${projectId}-${comp.name}`, project_id: projectId, ...comp }); } return components.length; } async getComponents(projectId) { return this.getAllByIndex('components', 'project_id', projectId); } // === Tokens === async saveTokens(projectId, tokens) { for (const token of tokens) { await this.put('tokens', { id: `${projectId}-${token.name}`, project_id: projectId, ...token }); } return tokens.length; } async getTokens(projectId) { return this.getAllByIndex('tokens', 'project_id', projectId); } async getTokensByCategory(projectId, category) { const tokens = await this.getTokens(projectId); return tokens.filter(t => t.category === category); } // === Sync Queue (Offline Operations) === async queueOperation(operation) { return this.put('sync_queue', { ...operation, status: 'pending', created_at: new Date().toISOString(), retries: 0 }); } async getPendingOperations() { return this.getAllByIndex('sync_queue', 'status', 'pending'); } async markOperationComplete(id) { const op = await this.get('sync_queue', id); if (op) { op.status = 'complete'; op.completed_at = new Date().toISOString(); await this.put('sync_queue', op); } } async markOperationFailed(id, error) { const op = await this.get('sync_queue', id); if (op) { op.status = 'failed'; op.error = error; op.retries = (op.retries || 0) + 1; await this.put('sync_queue', op); } } // === Activity Log === async logActivity(action, details = {}) { return this.put('activity', { action, ...details, created_at: new Date().toISOString() }); } async getRecentActivity(limit = 50) { const all = await this.getAll('activity'); return all .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) .slice(0, limit); } // === UI State === async saveUIState(key, value) { return this.put('ui_state', { key, value }); } async getUIState(key) { const data = await this.get('ui_state', key); return data?.value; } // === Stats === async getStats() { const stores = ['projects', 'components', 'tokens', 'styles', 'activity', 'sync_queue', 'cache']; const stats = {}; for (const store of stores) { stats[store] = await this.count(store); } // Storage estimate if (navigator.storage && navigator.storage.estimate) { const estimate = await navigator.storage.estimate(); stats.storage_used_mb = Math.round(estimate.usage / (1024 * 1024) * 100) / 100; stats.storage_quota_mb = Math.round(estimate.quota / (1024 * 1024)); } return stats; } // === Sync with Backend === async syncFromBackend(storeName, data, keyField = 'id') { // Clear existing and bulk insert await this.clear(storeName); for (const item of data) { await this.put(storeName, item); } return data.length; } } // Singleton instance const dssDB = new DssDB(); // Export export { DssDB }; export default dssDB;