import { IEventBus } from '../types/CalendarTypes'; import { CoreEvents } from '../constants/CoreEvents'; import { OperationQueue } from '../storage/OperationQueue'; import { IQueueOperation } from '../storage/IndexedDBService'; import { IndexedDBService } from '../storage/IndexedDBService'; import { ApiEventRepository } from '../repositories/ApiEventRepository'; /** * SyncManager - Background sync worker * Processes operation queue and syncs with API when online * * Features: * - Monitors online/offline status * - Processes queue with FIFO order * - Exponential backoff retry logic * - Updates syncStatus in IndexedDB after successful sync * - Emits sync events for UI feedback */ export class SyncManager { private eventBus: IEventBus; private queue: OperationQueue; private indexedDB: IndexedDBService; private apiRepository: ApiEventRepository; private isOnline: boolean = navigator.onLine; private isSyncing: boolean = false; private syncInterval: number = 5000; // 5 seconds private maxRetries: number = 5; private intervalId: number | null = null; constructor( eventBus: IEventBus, queue: OperationQueue, indexedDB: IndexedDBService, apiRepository: ApiEventRepository ) { this.eventBus = eventBus; this.queue = queue; this.indexedDB = indexedDB; this.apiRepository = apiRepository; this.setupNetworkListeners(); } /** * Setup online/offline event listeners */ private setupNetworkListeners(): void { window.addEventListener('online', () => { this.isOnline = true; this.eventBus.emit(CoreEvents.OFFLINE_MODE_CHANGED, { isOnline: true }); console.log('SyncManager: Network online - starting sync'); this.startSync(); }); window.addEventListener('offline', () => { this.isOnline = false; this.eventBus.emit(CoreEvents.OFFLINE_MODE_CHANGED, { isOnline: false }); console.log('SyncManager: Network offline - pausing sync'); this.stopSync(); }); } /** * Start background sync worker */ public startSync(): void { if (this.intervalId) { return; // Already running } console.log('SyncManager: Starting background sync'); // Process immediately this.processQueue(); // Then poll every syncInterval this.intervalId = window.setInterval(() => { this.processQueue(); }, this.syncInterval); } /** * Stop background sync worker */ public stopSync(): void { if (this.intervalId) { window.clearInterval(this.intervalId); this.intervalId = null; console.log('SyncManager: Stopped background sync'); } } /** * Process operation queue * Sends pending operations to API */ private async processQueue(): Promise { // Don't sync if offline if (!this.isOnline) { return; } // Don't start new sync if already syncing if (this.isSyncing) { return; } // Check if queue is empty if (await this.queue.isEmpty()) { return; } this.isSyncing = true; try { const operations = await this.queue.getAll(); this.eventBus.emit(CoreEvents.SYNC_STARTED, { operationCount: operations.length }); // Process operations one by one (FIFO) for (const operation of operations) { await this.processOperation(operation); } this.eventBus.emit(CoreEvents.SYNC_COMPLETED, { operationCount: operations.length }); } catch (error) { console.error('SyncManager: Queue processing error:', error); this.eventBus.emit(CoreEvents.SYNC_FAILED, { error: error instanceof Error ? error.message : 'Unknown error' }); } finally { this.isSyncing = false; } } /** * Process a single operation */ private async processOperation(operation: IQueueOperation): Promise { // Check if max retries exceeded if (operation.retryCount >= this.maxRetries) { console.error(`SyncManager: Max retries exceeded for operation ${operation.id}`, operation); await this.queue.remove(operation.id); await this.markEventAsError(operation.eventId); return; } try { // Send to API based on operation type switch (operation.type) { case 'create': await this.apiRepository.sendCreate(operation.data as any); break; case 'update': await this.apiRepository.sendUpdate(operation.eventId, operation.data); break; case 'delete': await this.apiRepository.sendDelete(operation.eventId); break; default: console.error(`SyncManager: Unknown operation type ${operation.type}`); await this.queue.remove(operation.id); return; } // Success - remove from queue and mark as synced await this.queue.remove(operation.id); await this.markEventAsSynced(operation.eventId); console.log(`SyncManager: Successfully synced operation ${operation.id}`); } catch (error) { console.error(`SyncManager: Failed to sync operation ${operation.id}:`, error); // Increment retry count await this.queue.incrementRetryCount(operation.id); // Calculate backoff delay const backoffDelay = this.calculateBackoff(operation.retryCount + 1); this.eventBus.emit(CoreEvents.SYNC_RETRY, { operationId: operation.id, retryCount: operation.retryCount + 1, nextRetryIn: backoffDelay }); } } /** * Mark event as synced in IndexedDB */ private async markEventAsSynced(eventId: string): Promise { try { const event = await this.indexedDB.getEvent(eventId); if (event) { event.syncStatus = 'synced'; await this.indexedDB.saveEvent(event); } } catch (error) { console.error(`SyncManager: Failed to mark event ${eventId} as synced:`, error); } } /** * Mark event as error in IndexedDB */ private async markEventAsError(eventId: string): Promise { try { const event = await this.indexedDB.getEvent(eventId); if (event) { event.syncStatus = 'error'; await this.indexedDB.saveEvent(event); } } catch (error) { console.error(`SyncManager: Failed to mark event ${eventId} as error:`, error); } } /** * Calculate exponential backoff delay * @param retryCount Current retry count * @returns Delay in milliseconds */ private calculateBackoff(retryCount: number): number { // Exponential backoff: 2^retryCount * 1000ms // Retry 1: 2s, Retry 2: 4s, Retry 3: 8s, Retry 4: 16s, Retry 5: 32s const baseDelay = 1000; const exponentialDelay = Math.pow(2, retryCount) * baseDelay; const maxDelay = 60000; // Max 1 minute return Math.min(exponentialDelay, maxDelay); } /** * Manually trigger sync (for testing or manual sync button) */ public async triggerManualSync(): Promise { console.log('SyncManager: Manual sync triggered'); await this.processQueue(); } /** * Get current sync status */ public getSyncStatus(): { isOnline: boolean; isSyncing: boolean; isRunning: boolean; } { return { isOnline: this.isOnline, isSyncing: this.isSyncing, isRunning: this.intervalId !== null }; } /** * Cleanup - stop sync and remove listeners */ public destroy(): void { this.stopSync(); // Note: We don't remove window event listeners as they're global } }