/** * Sync Service * ERP Transportistas * Sprint S8 - TASK-007 * * Background synchronization service for offline operations. */ import NetInfo from '@react-native-community/netinfo'; import { offlineStorage } from './OfflineStorage'; import { api } from './api'; import { TipoOperacionOffline, PrioridadSync } from '../types'; const MAX_RETRIES = 5; const SYNC_BATCH_SIZE = 10; const GPS_BATCH_SIZE = 50; interface SyncResult { total: number; exitosos: number; fallidos: number; errores: string[]; } class SyncService { private isSyncing = false; private listeners: Set<(syncing: boolean) => void> = new Set(); // ==================== Connection Monitoring ==================== async isOnline(): Promise { const state = await NetInfo.fetch(); return state.isConnected === true && state.isInternetReachable === true; } subscribeToSyncStatus(listener: (syncing: boolean) => void): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } private notifyListeners(syncing: boolean): void { this.listeners.forEach((listener) => listener(syncing)); } // ==================== Main Sync ==================== async sincronizar(): Promise { if (this.isSyncing) { return { total: 0, exitosos: 0, fallidos: 0, errores: ['Sincronización en progreso'] }; } const isOnline = await this.isOnline(); if (!isOnline) { return { total: 0, exitosos: 0, fallidos: 0, errores: ['Sin conexión'] }; } this.isSyncing = true; this.notifyListeners(true); const result: SyncResult = { total: 0, exitosos: 0, fallidos: 0, errores: [], }; try { // 1. Sync GPS positions first (high volume, low priority per item) const gpsResult = await this.sincronizarPosicionesGPS(); result.total += gpsResult.total; result.exitosos += gpsResult.exitosos; result.fallidos += gpsResult.fallidos; result.errores.push(...gpsResult.errores); // 2. Sync queued operations by priority const opsResult = await this.sincronizarOperaciones(); result.total += opsResult.total; result.exitosos += opsResult.exitosos; result.fallidos += opsResult.fallidos; result.errores.push(...opsResult.errores); // 3. Clean up synced data await offlineStorage.limpiarPosicionesSincronizadas(); } catch (error) { result.errores.push(`Error general: ${(error as Error).message}`); } finally { this.isSyncing = false; this.notifyListeners(false); } return result; } // ==================== GPS Sync ==================== private async sincronizarPosicionesGPS(): Promise { const result: SyncResult = { total: 0, exitosos: 0, fallidos: 0, errores: [], }; try { const posiciones = await offlineStorage.obtenerPosicionesNoSincronizadas(GPS_BATCH_SIZE); result.total = posiciones.length; if (posiciones.length === 0) { return result; } // Send batch to server const response = await api.post('/api/v1/gps/posiciones/batch', { posiciones: posiciones.map((p) => ({ latitud: p.latitud, longitud: p.longitud, velocidad: p.velocidad, rumbo: p.rumbo, precision: p.precision, timestamp: p.timestamp, })), }); if (response.data.success) { // Mark as synced const ids = posiciones.map((p) => p.id); await offlineStorage.marcarPosicionesSincronizadas(ids); result.exitosos = posiciones.length; } else { result.fallidos = posiciones.length; result.errores.push('Error al sincronizar posiciones GPS'); } } catch (error) { result.errores.push(`GPS: ${(error as Error).message}`); } return result; } // ==================== Operations Sync ==================== private async sincronizarOperaciones(): Promise { const result: SyncResult = { total: 0, exitosos: 0, fallidos: 0, errores: [], }; try { const operaciones = await offlineStorage.obtenerOperacionesPendientes(SYNC_BATCH_SIZE); result.total = operaciones.length; for (const op of operaciones) { try { // Skip if max retries exceeded if (op.intentos >= MAX_RETRIES) { result.fallidos++; result.errores.push(`${op.tipo}: Máximo de reintentos alcanzado`); continue; } const payload = JSON.parse(op.payload); const success = await this.procesarOperacion(op.tipo, payload); if (success) { await offlineStorage.marcarOperacionSincronizada(op.id); result.exitosos++; } else { await offlineStorage.incrementarIntentos(op.id); result.fallidos++; } } catch (error) { await offlineStorage.incrementarIntentos(op.id); result.fallidos++; result.errores.push(`${op.tipo}: ${(error as Error).message}`); } } } catch (error) { result.errores.push(`Operaciones: ${(error as Error).message}`); } return result; } private async procesarOperacion(tipo: TipoOperacionOffline, payload: object): Promise { switch (tipo) { case TipoOperacionOffline.EVENTO: return this.sincronizarEvento(payload); case TipoOperacionOffline.POD: return this.sincronizarPOD(payload); case TipoOperacionOffline.CHECKLIST: return this.sincronizarChecklist(payload); default: console.warn(`Tipo de operación no manejado: ${tipo}`); return false; } } private async sincronizarEvento(payload: object): Promise { const response = await api.post('/api/v1/tracking/eventos', payload); return response.data.success === true; } private async sincronizarPOD(payload: object): Promise { const response = await api.post('/api/v1/viajes/pod', payload); return response.data.success === true; } private async sincronizarChecklist(payload: object): Promise { const response = await api.post('/api/v1/viajes/checklist', payload); return response.data.success === true; } // ==================== Enqueue Operations ==================== async encolarEvento(evento: object): Promise { await offlineStorage.encolarOperacion( TipoOperacionOffline.EVENTO, evento, PrioridadSync.ALTA ); // Try immediate sync if online if (await this.isOnline()) { this.sincronizar(); } } async encolarPOD(pod: object): Promise { await offlineStorage.encolarOperacion( TipoOperacionOffline.POD, pod, PrioridadSync.ALTA ); if (await this.isOnline()) { this.sincronizar(); } } async encolarChecklist(checklist: object): Promise { await offlineStorage.encolarOperacion( TipoOperacionOffline.CHECKLIST, checklist, PrioridadSync.MEDIA ); if (await this.isOnline()) { this.sincronizar(); } } async guardarPosicionGPS( latitud: number, longitud: number, velocidad: number, rumbo: number, precision: number ): Promise { await offlineStorage.guardarPosicion(latitud, longitud, velocidad, rumbo, precision); } // ==================== Status ==================== async obtenerEstadoSync(): Promise<{ pendientes: number; posicionesGPS: number; ultimaSync?: string; enProgreso: boolean; }> { const stats = await offlineStorage.getEstadisticas(); return { pendientes: stats.operacionesPendientes, posicionesGPS: stats.posicionesNoSincronizadas, enProgreso: this.isSyncing, }; } } export const syncService = new SyncService();