import { CoreEvents } from '../constants/CoreEvents'; /** * 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 { constructor(eventBus, queue, indexedDB, apiRepository) { this.isOnline = navigator.onLine; this.isSyncing = false; this.syncInterval = 5000; // 5 seconds this.maxRetries = 5; this.intervalId = null; this.eventBus = eventBus; this.queue = queue; this.indexedDB = indexedDB; this.apiRepository = apiRepository; this.setupNetworkListeners(); this.startSync(); console.log('SyncManager initialized and started'); } /** * Setup online/offline event listeners */ setupNetworkListeners() { 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 */ startSync() { 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 */ stopSync() { if (this.intervalId) { window.clearInterval(this.intervalId); this.intervalId = null; console.log('SyncManager: Stopped background sync'); } } /** * Process operation queue * Sends pending operations to API */ async processQueue() { // 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 */ async processOperation(operation) { // 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); 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 */ async markEventAsSynced(eventId) { 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 */ async markEventAsError(eventId) { 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 */ calculateBackoff(retryCount) { // 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) */ async triggerManualSync() { console.log('SyncManager: Manual sync triggered'); await this.processQueue(); } /** * Get current sync status */ getSyncStatus() { return { isOnline: this.isOnline, isSyncing: this.isSyncing, isRunning: this.intervalId !== null }; } /** * Cleanup - stop sync and remove listeners */ destroy() { this.stopSync(); // Note: We don't remove window event listeners as they're global } } //# sourceMappingURL=SyncManager.js.map