erp-transportistas-v2/apps/frontend-mobile/src/services/SyncService.ts
Adrian Flores Cortes 04c2db7bbc [TASK-2026-02-06-ESTANDARIZACION-ESTRUCTURA-PROYECTOS] refactor: Migrate to canonical apps/ structure (ADR-0011)
- backend/ → apps/backend/, frontend/ → apps/frontend-web/, database/ → apps/database/
- mobile/ → apps/frontend-mobile/
- Updated .gitmodules, CLAUDE.md v2.0.0
- Added apps/_MAP.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 10:30:14 -06:00

283 lines
7.7 KiB
TypeScript

/**
* 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<boolean> {
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<SyncResult> {
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<SyncResult> {
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<SyncResult> {
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<boolean> {
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<boolean> {
const response = await api.post('/api/v1/tracking/eventos', payload);
return response.data.success === true;
}
private async sincronizarPOD(payload: object): Promise<boolean> {
const response = await api.post('/api/v1/viajes/pod', payload);
return response.data.success === true;
}
private async sincronizarChecklist(payload: object): Promise<boolean> {
const response = await api.post('/api/v1/viajes/checklist', payload);
return response.data.success === true;
}
// ==================== Enqueue Operations ====================
async encolarEvento(evento: object): Promise<void> {
await offlineStorage.encolarOperacion(
TipoOperacionOffline.EVENTO,
evento,
PrioridadSync.ALTA
);
// Try immediate sync if online
if (await this.isOnline()) {
this.sincronizar();
}
}
async encolarPOD(pod: object): Promise<void> {
await offlineStorage.encolarOperacion(
TipoOperacionOffline.POD,
pod,
PrioridadSync.ALTA
);
if (await this.isOnline()) {
this.sincronizar();
}
}
async encolarChecklist(checklist: object): Promise<void> {
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<void> {
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();