/** * IndexedDB Service for Calendar App * Handles local storage of events and sync queue */ export class IndexedDBService { constructor() { this.db = null; this.initialized = false; } /** * Initialize and open the database */ async initialize() { return new Promise((resolve, reject) => { const request = indexedDB.open(IndexedDBService.DB_NAME, IndexedDBService.DB_VERSION); request.onerror = () => { reject(new Error(`Failed to open IndexedDB: ${request.error}`)); }; request.onsuccess = () => { this.db = request.result; this.initialized = true; resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create events store if (!db.objectStoreNames.contains(IndexedDBService.EVENTS_STORE)) { const eventsStore = db.createObjectStore(IndexedDBService.EVENTS_STORE, { keyPath: 'id' }); eventsStore.createIndex('start', 'start', { unique: false }); eventsStore.createIndex('end', 'end', { unique: false }); eventsStore.createIndex('syncStatus', 'syncStatus', { unique: false }); } // Create operation queue store if (!db.objectStoreNames.contains(IndexedDBService.QUEUE_STORE)) { const queueStore = db.createObjectStore(IndexedDBService.QUEUE_STORE, { keyPath: 'id' }); queueStore.createIndex('timestamp', 'timestamp', { unique: false }); } // Create sync state store if (!db.objectStoreNames.contains(IndexedDBService.SYNC_STATE_STORE)) { db.createObjectStore(IndexedDBService.SYNC_STATE_STORE, { keyPath: 'key' }); } }; }); } /** * Check if database is initialized */ isInitialized() { return this.initialized; } /** * Ensure database is initialized */ ensureDB() { if (!this.db) { throw new Error('IndexedDB not initialized. Call initialize() first.'); } return this.db; } // ======================================== // Event CRUD Operations // ======================================== /** * Get a single event by ID */ async getEvent(id) { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly'); const store = transaction.objectStore(IndexedDBService.EVENTS_STORE); const request = store.get(id); request.onsuccess = () => { const event = request.result; resolve(event ? this.deserializeEvent(event) : null); }; request.onerror = () => { reject(new Error(`Failed to get event ${id}: ${request.error}`)); }; }); } /** * Get all events */ async getAllEvents() { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly'); const store = transaction.objectStore(IndexedDBService.EVENTS_STORE); const request = store.getAll(); request.onsuccess = () => { const events = request.result; resolve(events.map(e => this.deserializeEvent(e))); }; request.onerror = () => { reject(new Error(`Failed to get all events: ${request.error}`)); }; }); } /** * Save an event (create or update) */ async saveEvent(event) { const db = this.ensureDB(); const serialized = this.serializeEvent(event); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite'); const store = transaction.objectStore(IndexedDBService.EVENTS_STORE); const request = store.put(serialized); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to save event ${event.id}: ${request.error}`)); }; }); } /** * Delete an event */ async deleteEvent(id) { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite'); const store = transaction.objectStore(IndexedDBService.EVENTS_STORE); const request = store.delete(id); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to delete event ${id}: ${request.error}`)); }; }); } // ======================================== // Queue Operations // ======================================== /** * Add operation to queue */ async addToQueue(operation) { const db = this.ensureDB(); const queueItem = { ...operation, id: `${operation.type}-${operation.eventId}-${Date.now()}` }; return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite'); const store = transaction.objectStore(IndexedDBService.QUEUE_STORE); const request = store.put(queueItem); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to add to queue: ${request.error}`)); }; }); } /** * Get all queue operations (sorted by timestamp) */ async getQueue() { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readonly'); const store = transaction.objectStore(IndexedDBService.QUEUE_STORE); const index = store.index('timestamp'); const request = index.getAll(); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(new Error(`Failed to get queue: ${request.error}`)); }; }); } /** * Remove operation from queue */ async removeFromQueue(id) { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite'); const store = transaction.objectStore(IndexedDBService.QUEUE_STORE); const request = store.delete(id); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to remove from queue: ${request.error}`)); }; }); } /** * Clear entire queue */ async clearQueue() { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite'); const store = transaction.objectStore(IndexedDBService.QUEUE_STORE); const request = store.clear(); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to clear queue: ${request.error}`)); }; }); } // ======================================== // Sync State Operations // ======================================== /** * Save sync state value */ async setSyncState(key, value) { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.SYNC_STATE_STORE], 'readwrite'); const store = transaction.objectStore(IndexedDBService.SYNC_STATE_STORE); const request = store.put({ key, value }); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to set sync state ${key}: ${request.error}`)); }; }); } /** * Get sync state value */ async getSyncState(key) { const db = this.ensureDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([IndexedDBService.SYNC_STATE_STORE], 'readonly'); const store = transaction.objectStore(IndexedDBService.SYNC_STATE_STORE); const request = store.get(key); request.onsuccess = () => { const result = request.result; resolve(result ? result.value : null); }; request.onerror = () => { reject(new Error(`Failed to get sync state ${key}: ${request.error}`)); }; }); } // ======================================== // Serialization Helpers // ======================================== /** * Serialize event for IndexedDB storage (convert Dates to ISO strings) */ serializeEvent(event) { return { ...event, start: event.start instanceof Date ? event.start.toISOString() : event.start, end: event.end instanceof Date ? event.end.toISOString() : event.end }; } /** * Deserialize event from IndexedDB (convert ISO strings to Dates) */ deserializeEvent(event) { return { ...event, start: typeof event.start === 'string' ? new Date(event.start) : event.start, end: typeof event.end === 'string' ? new Date(event.end) : event.end }; } /** * Close database connection */ close() { if (this.db) { this.db.close(); this.db = null; } } /** * Delete entire database (for testing/reset) */ static async deleteDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(IndexedDBService.DB_NAME); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to delete database: ${request.error}`)); }; }); } /** * Seed IndexedDB with mock data if empty */ async seedIfEmpty(mockDataUrl = 'data/mock-events.json') { try { const existingEvents = await this.getAllEvents(); if (existingEvents.length > 0) { console.log(`IndexedDB already has ${existingEvents.length} events - skipping seed`); return; } console.log('IndexedDB is empty - seeding with mock data'); // Check if online to fetch mock data if (!navigator.onLine) { console.warn('Offline and IndexedDB empty - starting with no events'); return; } // Fetch mock events const response = await fetch(mockDataUrl); if (!response.ok) { throw new Error(`Failed to fetch mock events: ${response.statusText}`); } const mockEvents = await response.json(); // Convert and save to IndexedDB for (const event of mockEvents) { const calendarEvent = { ...event, start: new Date(event.start), end: new Date(event.end), allDay: event.allDay || false, syncStatus: 'synced' }; await this.saveEvent(calendarEvent); } console.log(`Seeded IndexedDB with ${mockEvents.length} mock events`); } catch (error) { console.error('Failed to seed IndexedDB:', error); // Don't throw - allow app to start with empty calendar } } } IndexedDBService.DB_NAME = 'CalendarDB'; IndexedDBService.DB_VERSION = 1; IndexedDBService.EVENTS_STORE = 'events'; IndexedDBService.QUEUE_STORE = 'operationQueue'; IndexedDBService.SYNC_STATE_STORE = 'syncState'; //# sourceMappingURL=IndexedDBService.js.map