import { IStore } from './IStore'; /** * IndexedDBContext - Database connection manager and provider * * RESPONSIBILITY: * - Opens and manages IDBDatabase connection lifecycle * - Creates object stores via injected IStore implementations * - Provides shared IDBDatabase instance to all services * * SEPARATION OF CONCERNS: * - This class: Connection management ONLY * - OperationQueue: Queue and sync state operations * - Entity Services: CRUD operations for specific entities * * USAGE: * Services inject IndexedDBContext and call getDatabase() to access db. * This lazy access pattern ensures db is ready when requested. */ export class IndexedDBContext { private static readonly DB_NAME = 'CalendarDB'; private static readonly DB_VERSION = 2; static readonly QUEUE_STORE = 'operationQueue'; static readonly SYNC_STATE_STORE = 'syncState'; private db: IDBDatabase | null = null; private initialized: boolean = false; private stores: IStore[]; /** * @param stores - Array of IStore implementations injected via DI */ constructor(stores: IStore[]) { this.stores = stores; } /** * Initialize and open the database * Creates all entity stores, queue store, and sync state store */ async initialize(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(IndexedDBContext.DB_NAME, IndexedDBContext.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 as IDBOpenDBRequest).result; // Create all entity stores via injected IStore implementations // Open/Closed Principle: Adding new entity only requires DI registration this.stores.forEach(store => { if (!db.objectStoreNames.contains(store.storeName)) { store.create(db); } }); // Create operation queue store (sync infrastructure) if (!db.objectStoreNames.contains(IndexedDBContext.QUEUE_STORE)) { const queueStore = db.createObjectStore(IndexedDBContext.QUEUE_STORE, { keyPath: 'id' }); queueStore.createIndex('timestamp', 'timestamp', { unique: false }); } // Create sync state store (sync metadata) if (!db.objectStoreNames.contains(IndexedDBContext.SYNC_STATE_STORE)) { db.createObjectStore(IndexedDBContext.SYNC_STATE_STORE, { keyPath: 'key' }); } }; }); } /** * Check if database is initialized */ public isInitialized(): boolean { return this.initialized; } /** * Get IDBDatabase instance * Used by services to access the database * * @throws Error if database not initialized * @returns IDBDatabase instance */ public getDatabase(): IDBDatabase { if (!this.db) { throw new Error('IndexedDB not initialized. Call initialize() first.'); } return this.db; } /** * Close database connection */ close(): void { if (this.db) { this.db.close(); this.db = null; this.initialized = false; } } /** * Delete entire database (for testing/reset) */ static async deleteDatabase(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(IndexedDBContext.DB_NAME); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to delete database: ${request.error}`)); }; }); } }