import { ISync, EntityType, SyncStatus, IEventBus, IEntitySavedPayload, IEntityDeletedPayload } from '../types/CalendarTypes'; import { IEntityService } from './IEntityService'; import { SyncPlugin } from './SyncPlugin'; import { IndexedDBContext } from './IndexedDBContext'; import { CoreEvents } from '../constants/CoreEvents'; import { diff } from 'json-diff-ts'; /** * BaseEntityService - Abstract base class for all entity services * * PROVIDES: * - Generic CRUD operations (get, getAll, save, delete) * - Sync status management (delegates to SyncPlugin) * - Serialization hooks (override in subclass if needed) */ export abstract class BaseEntityService implements IEntityService { abstract readonly storeName: string; abstract readonly entityType: EntityType; private syncPlugin: SyncPlugin; private context: IndexedDBContext; protected eventBus: IEventBus; constructor(context: IndexedDBContext, eventBus: IEventBus) { this.context = context; this.eventBus = eventBus; this.syncPlugin = new SyncPlugin(this); } protected get db(): IDBDatabase { return this.context.getDatabase(); } /** * Serialize entity before storing in IndexedDB */ protected serialize(entity: T): unknown { return entity; } /** * Deserialize data from IndexedDB back to entity */ protected deserialize(data: unknown): T { return data as T; } /** * Get a single entity by ID */ 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; resolve(data ? this.deserialize(data) : null); }; request.onerror = () => { reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`)); }; }); } /** * Get 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 unknown[]; 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) * Emits ENTITY_SAVED event with operation type and changes (diff for updates) * @param entity - Entity to save * @param silent - If true, skip event emission (used for seeding) */ async save(entity: T, silent = false): Promise { const entityId = (entity as unknown as { id: string }).id; const existingEntity = await this.get(entityId); const isCreate = existingEntity === null; // Calculate changes: full entity for create, diff for update let changes: unknown; if (isCreate) { changes = entity; } else { const existingSerialized = this.serialize(existingEntity); const newSerialized = this.serialize(entity); changes = diff(existingSerialized, newSerialized); } 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 = () => { // Only emit event if not silent (silent used for seeding) if (!silent) { const payload: IEntitySavedPayload = { entityType: this.entityType, entityId, operation: isCreate ? 'create' : 'update', changes, timestamp: Date.now() }; this.eventBus.emit(CoreEvents.ENTITY_SAVED, payload); } resolve(); }; request.onerror = () => { reject(new Error(`Failed to save ${this.entityType} ${entityId}: ${request.error}`)); }; }); } /** * Delete an entity * Emits ENTITY_DELETED event */ 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 = () => { const payload: IEntityDeletedPayload = { entityType: this.entityType, entityId: id, operation: 'delete', timestamp: Date.now() }; this.eventBus.emit(CoreEvents.ENTITY_DELETED, payload); resolve(); }; request.onerror = () => { reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`)); }; }); } // Sync methods - delegate to SyncPlugin async markAsSynced(id: string): Promise { return this.syncPlugin.markAsSynced(id); } async markAsError(id: string): Promise { return this.syncPlugin.markAsError(id); } async getSyncStatus(id: string): Promise { return this.syncPlugin.getSyncStatus(id); } async getBySyncStatus(syncStatus: string): Promise { return this.syncPlugin.getBySyncStatus(syncStatus); } }