import { EncryptionUtils } from '@repo/utils'; import type { IStorageService } from '../types/storage.interface'; import type { StorageOptions } from '../local-storage/local-storage.service'; // ─── Types ────────────────────────────────────────────────────── export interface IndexedDBConfig extends StorageOptions { /** Database name. @default 'app_db' */ dbName?: string; /** Object store name. @default 'kv_store' */ storeName?: string; /** Database version. @default 1 */ version?: number; } // ─── Helpers ──────────────────────────────────────────────────── /** * Open (or create) an IndexedDB database with a simple key-value store. * Returns a Promise that resolves with the IDBDatabase instance. */ function openDatabase(dbName: string, storeName: string, version: number): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, version); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } /** * Execute a single IndexedDB transaction and return the result. * Handles open → transaction → request → close lifecycle cleanly. */ function withTransaction( db: IDBDatabase, storeName: string, mode: IDBTransactionMode, operation: (store: IDBObjectStore) => IDBRequest, ): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(storeName, mode); const store = tx.objectStore(storeName); const request = operation(store); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // ─── Service ──────────────────────────────────────────────────── /** * Enterprise-grade IndexedDB wrapper with optional AES encryption. */ export class IndexedDBService implements IStorageService { private readonly encryption: EncryptionUtils; private readonly dbName: string; private readonly storeName: string; private readonly version: number; private readonly encryptedKeys: Set; private readonly plainTextKeys: Set; private dbPromise: Promise | null = null; constructor(config?: IndexedDBConfig, encryptionUtils?: EncryptionUtils) { this.encryption = encryptionUtils ?? EncryptionUtils.getInstance(); this.dbName = config?.dbName ?? 'app_db'; this.storeName = config?.storeName ?? 'kv_store'; this.version = config?.version ?? 1; this.encryptedKeys = config?.encryptedKeys ?? new Set(); this.plainTextKeys = config?.plainTextKeys ?? new Set(); } /** Lazy-open the database connection (cached). */ private getDB(): Promise { if (!this.dbPromise) { this.dbPromise = openDatabase(this.dbName, this.storeName, this.version); } return this.dbPromise; } private validateKey(key: TKey): void { if (!this.encryptedKeys.has(key) && !this.plainTextKeys.has(key)) { throw new Error(`[Storage Engine] Security Exception: Key '${key}' is not registered and cannot be accessed.`); } } private shouldEncrypt(key: TKey): boolean { return this.encryptedKeys.has(key); } async setItem(key: TKey, value: T): Promise { this.validateKey(key); const db = await this.getDB(); const serialized = JSON.stringify(value); const payload = this.shouldEncrypt(key) ? this.encryption.encrypt(serialized) : serialized; await withTransaction(db, this.storeName, 'readwrite', (store) => store.put(payload, key as string)); } async getItem(key: TKey): Promise { this.validateKey(key); const db = await this.getDB(); const raw = await withTransaction( db, this.storeName, 'readonly', (store) => store.get(key as string) as IDBRequest, ); if (raw === undefined || raw === null) return null; try { if (this.shouldEncrypt(key)) { const decrypted = this.encryption.decrypt(raw); if (!decrypted) return null; return JSON.parse(decrypted) as T; } return JSON.parse(raw) as T; } catch { console.warn(`[core-storage] Failed to parse IndexedDB key "${key}". Removing corrupt entry.`); await this.removeItem(key); return null; } } async removeItem(key: TKey): Promise { this.validateKey(key); const db = await this.getDB(); await withTransaction(db, this.storeName, 'readwrite', (store) => store.delete(key as string)); } async clear(): Promise { const db = await this.getDB(); await withTransaction(db, this.storeName, 'readwrite', (store) => store.clear()); } async hasItem(key: TKey): Promise { const value = await this.getItem(key); return value !== null; } async keys(): Promise { const db = await this.getDB(); const allKeys = await withTransaction( db, this.storeName, 'readonly', (store) => store.getAllKeys() as IDBRequest, ); return allKeys as TKey[]; } } export function createIndexedDB(config: IndexedDBConfig): IStorageService { return new IndexedDBService(config); }