import { ISync, EntityType, SyncStatus } from '../types/CalendarTypes'; import { IEntityService } from './IEntityService'; import { SyncPlugin } from './SyncPlugin'; /** * BaseEntityService - Abstract base class for all entity services * * HYBRID PATTERN: Inheritance + Composition * - Services EXTEND this base class (inheritance for structure) * - Sync logic is COMPOSED via SyncPlugin (pluggable) * * PROVIDES: * - Generic CRUD operations (get, getAll, save, delete) * - Sync status management (delegates to SyncPlugin) * - Serialization hooks (override in subclass if needed) * * SUBCLASSES MUST IMPLEMENT: * - storeName: string (IndexedDB object store name) * - entityType: EntityType (for runtime routing) * * SUBCLASSES MAY OVERRIDE: * - serialize(entity: T): any (default: no serialization) * - deserialize(data: any): T (default: no deserialization) * * BENEFITS: * - DRY: Single source of truth for CRUD logic * - Type safety: Generic T ensures compile-time checking * - Pluggable: SyncPlugin can be swapped for testing/different implementations * - Open/Closed: New entities just extend this class */ export abstract class BaseEntityService implements IEntityService { // Abstract properties - must be implemented by subclasses abstract readonly storeName: string; abstract readonly entityType: EntityType; // Internal composition - sync functionality private syncPlugin: SyncPlugin; // Protected database instance - accessible to subclasses protected db: IDBDatabase; /** * @param db - IDBDatabase instance (injected dependency) */ constructor(db: IDBDatabase) { this.db = db; this.syncPlugin = new SyncPlugin(this); } /** * Serialize entity before storing in IndexedDB * Override in subclass if entity has Date fields or needs transformation * * @param entity - Entity to serialize * @returns Serialized data (default: entity itself) */ protected serialize(entity: T): any { return entity; // Default: no serialization } /** * Deserialize data from IndexedDB back to entity * Override in subclass if entity has Date fields or needs transformation * * @param data - Raw data from IndexedDB * @returns Deserialized entity (default: data itself) */ protected deserialize(data: any): T { return data as T; // Default: no deserialization } /** * Get a single entity by ID * * @param id - Entity ID * @returns Entity or null if not found */ async get(id: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); const request = store.get(id); request.onsuccess = () => { const data = request.result; if (data) { resolve(this.deserialize(data)); } else { resolve(null); } }; request.onerror = () => { reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`)); }; }); } /** * Get all entities * * @returns Array of all entities */ async getAll(): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); const request = store.getAll(); request.onsuccess = () => { const data = request.result as any[]; const entities = data.map(item => this.deserialize(item)); resolve(entities); }; request.onerror = () => { reject(new Error(`Failed to get all ${this.entityType}s: ${request.error}`)); }; }); } /** * Save an entity (create or update) * * @param entity - Entity to save */ async save(entity: T): Promise { const serialized = this.serialize(entity); return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); const request = store.put(serialized); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to save ${this.entityType} ${(entity as any).id}: ${request.error}`)); }; }); } /** * Delete an entity * * @param id - Entity ID to delete */ async delete(id: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); const request = store.delete(id); request.onsuccess = () => { resolve(); }; request.onerror = () => { reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`)); }; }); } // ============================================================================ // SYNC METHODS (IEntityService implementation) - Delegates to SyncPlugin // ============================================================================ /** * Mark entity as successfully synced (IEntityService implementation) * Delegates to SyncPlugin * * @param id - Entity ID */ async markAsSynced(id: string): Promise { return this.syncPlugin.markAsSynced(id); } /** * Mark entity as sync error (IEntityService implementation) * Delegates to SyncPlugin * * @param id - Entity ID */ async markAsError(id: string): Promise { return this.syncPlugin.markAsError(id); } /** * Get sync status for an entity (IEntityService implementation) * Delegates to SyncPlugin * * @param id - Entity ID * @returns SyncStatus or null if entity not found */ async getSyncStatus(id: string): Promise { return this.syncPlugin.getSyncStatus(id); } /** * Get entities by sync status * Delegates to SyncPlugin - uses IndexedDB syncStatus index * * @param syncStatus - Sync status ('synced', 'pending', 'error') * @returns Array of entities with this sync status */ async getBySyncStatus(syncStatus: string): Promise { return this.syncPlugin.getBySyncStatus(syncStatus); } }