1134 lines
33 KiB
Markdown
1134 lines
33 KiB
Markdown
# Estratégia de Sincronização Offline - Implementação Completa
|
|
|
|
## Visão Geral
|
|
|
|
Este documento detalha a estratégia completa para implementar sincronização offline no aplicativo de entregas, permitindo que o aplicativo funcione sem dependência de internet após uma sincronização inicial completa.
|
|
|
|
## Arquitetura de Sincronização
|
|
|
|
### 1. Fluxo de Sincronização
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ Aplicativo │ │ Servidor │ │ Banco Local │
|
|
│ │ │ │ │ (SQLite) │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
│ │ │
|
|
│ 1. Login │ │
|
|
├──────────────────────►│ │
|
|
│◄──────────────────────┤ │
|
|
│ │ │
|
|
│ 2. Sync Inicial │ │
|
|
├──────────────────────►│ │
|
|
│◄──────────────────────┤ │
|
|
│ │ │
|
|
│ 3. Salvar Local │ │
|
|
├─────────────────────────────────────────────►│
|
|
│ │ │
|
|
│ 4. Modo Offline │ │
|
|
│ (Usar dados locais)│ │
|
|
├─────────────────────────────────────────────►│
|
|
│ │ │
|
|
│ 5. Sync Incremental │ │
|
|
│ (Quando online) │ │
|
|
├──────────────────────►│ │
|
|
│◄──────────────────────┤ │
|
|
│ │ │
|
|
│ 6. Atualizar Local │ │
|
|
├─────────────────────────────────────────────►│
|
|
```
|
|
|
|
### 2. Estados de Sincronização
|
|
|
|
```typescript
|
|
enum SyncStatus {
|
|
SYNCED = 'synced', // Sincronizado com servidor
|
|
PENDING = 'pending', // Aguardando sincronização
|
|
CONFLICT = 'conflict', // Conflito detectado
|
|
ERROR = 'error', // Erro na sincronização
|
|
OFFLINE = 'offline' // Modo offline ativo
|
|
}
|
|
|
|
enum SyncType {
|
|
FULL = 'full', // Sincronização completa
|
|
INCREMENTAL = 'incremental', // Sincronização incremental
|
|
SELECTIVE = 'selective' // Sincronização seletiva
|
|
}
|
|
```
|
|
|
|
## Implementação do Sistema de Sincronização
|
|
|
|
### 1. Contexto de Sincronização Expandido
|
|
|
|
**Arquivo**: `src/contexts/InitialSyncContext.tsx`
|
|
|
|
```typescript
|
|
interface InitialSyncContextData {
|
|
// Estados
|
|
isInitialSyncComplete: boolean;
|
|
syncProgress: number;
|
|
syncStatus: SyncStatus;
|
|
lastSyncTime: number | null;
|
|
pendingChanges: number;
|
|
|
|
// Métodos
|
|
startInitialSync: () => Promise<void>;
|
|
retryInitialSync: () => Promise<void>;
|
|
performIncrementalSync: () => Promise<void>;
|
|
performSelectiveSync: (ids: string[]) => Promise<void>;
|
|
resolveConflict: (conflictId: string, resolution: ConflictResolution) => Promise<void>;
|
|
|
|
// Utilitários
|
|
getSyncStats: () => Promise<SyncStats>;
|
|
clearSyncData: () => Promise<void>;
|
|
}
|
|
|
|
interface SyncStats {
|
|
totalRecords: number;
|
|
syncedRecords: number;
|
|
pendingRecords: number;
|
|
conflictedRecords: number;
|
|
errorRecords: number;
|
|
lastSyncDuration: number;
|
|
averageSyncTime: number;
|
|
}
|
|
|
|
interface ConflictResolution {
|
|
type: 'server_wins' | 'client_wins' | 'merge';
|
|
data?: any;
|
|
}
|
|
```
|
|
|
|
### 2. Serviço de Sincronização Principal
|
|
|
|
**Arquivo**: `src/services/syncService.ts`
|
|
|
|
```typescript
|
|
class SyncService {
|
|
private api: ApiService;
|
|
private database: DatabaseService;
|
|
private offlineStorage: OfflineStorageService;
|
|
private syncQueue: SyncQueue;
|
|
private conflictResolver: ConflictResolver;
|
|
|
|
constructor() {
|
|
this.api = new ApiService();
|
|
this.database = new DatabaseService();
|
|
this.offlineStorage = new OfflineStorageService();
|
|
this.syncQueue = new SyncQueue();
|
|
this.conflictResolver = new ConflictResolver();
|
|
}
|
|
|
|
// Sincronização inicial completa
|
|
async performInitialSync(): Promise<SyncResult> {
|
|
try {
|
|
console.log('=== INICIANDO SINCRONIZAÇÃO INICIAL ===');
|
|
|
|
const syncResult: SyncResult = {
|
|
success: true,
|
|
totalRecords: 0,
|
|
syncedRecords: 0,
|
|
errors: [],
|
|
duration: 0
|
|
};
|
|
|
|
const startTime = Date.now();
|
|
|
|
// 1. Sincronizar dados de usuário
|
|
await this.syncUserData();
|
|
syncResult.syncedRecords += 1;
|
|
|
|
// 2. Sincronizar entregas
|
|
const deliveriesResult = await this.syncDeliveries();
|
|
syncResult.syncedRecords += deliveriesResult.count;
|
|
|
|
// 3. Sincronizar configurações
|
|
await this.syncSettings();
|
|
syncResult.syncedRecords += 1;
|
|
|
|
// 4. Sincronizar dados de referência
|
|
await this.syncReferenceData();
|
|
syncResult.syncedRecords += 5; // Aproximadamente
|
|
|
|
// 5. Marcar sincronização como completa
|
|
await this.database.saveSetting('initial_sync_complete', 'true');
|
|
await this.database.saveSetting('last_sync_time', Date.now().toString());
|
|
|
|
syncResult.duration = Date.now() - startTime;
|
|
syncResult.totalRecords = syncResult.syncedRecords;
|
|
|
|
console.log('=== SINCRONIZAÇÃO INICIAL CONCLUÍDA ===');
|
|
console.log(`Total de registros: ${syncResult.totalRecords}`);
|
|
console.log(`Duração: ${syncResult.duration}ms`);
|
|
|
|
return syncResult;
|
|
|
|
} catch (error) {
|
|
console.error('Erro na sincronização inicial:', error);
|
|
throw new SyncError('Falha na sincronização inicial', error);
|
|
}
|
|
}
|
|
|
|
// Sincronização incremental
|
|
async performIncrementalSync(): Promise<SyncResult> {
|
|
try {
|
|
console.log('=== INICIANDO SINCRONIZAÇÃO INCREMENTAL ===');
|
|
|
|
const lastSyncTime = await this.database.getSetting('last_sync_time');
|
|
const syncResult: SyncResult = {
|
|
success: true,
|
|
totalRecords: 0,
|
|
syncedRecords: 0,
|
|
errors: [],
|
|
duration: 0
|
|
};
|
|
|
|
const startTime = Date.now();
|
|
|
|
// 1. Buscar mudanças do servidor desde última sincronização
|
|
const serverChanges = await this.api.getChangesSince(lastSyncTime);
|
|
|
|
// 2. Buscar mudanças locais não sincronizadas
|
|
const localChanges = await this.database.getUnsyncedRecords();
|
|
|
|
// 3. Resolver conflitos
|
|
const conflicts = await this.detectConflicts(serverChanges, localChanges);
|
|
await this.resolveConflicts(conflicts);
|
|
|
|
// 4. Aplicar mudanças do servidor
|
|
await this.applyServerChanges(serverChanges);
|
|
|
|
// 5. Enviar mudanças locais
|
|
await this.sendLocalChanges(localChanges);
|
|
|
|
// 6. Atualizar timestamp de sincronização
|
|
await this.database.saveSetting('last_sync_time', Date.now().toString());
|
|
|
|
syncResult.duration = Date.now() - startTime;
|
|
console.log('=== SINCRONIZAÇÃO INCREMENTAL CONCLUÍDA ===');
|
|
|
|
return syncResult;
|
|
|
|
} catch (error) {
|
|
console.error('Erro na sincronização incremental:', error);
|
|
throw new SyncError('Falha na sincronização incremental', error);
|
|
}
|
|
}
|
|
|
|
// Sincronização seletiva
|
|
async performSelectiveSync(recordIds: string[]): Promise<SyncResult> {
|
|
try {
|
|
console.log(`=== INICIANDO SINCRONIZAÇÃO SELETIVA (${recordIds.length} registros) ===`);
|
|
|
|
const syncResult: SyncResult = {
|
|
success: true,
|
|
totalRecords: recordIds.length,
|
|
syncedRecords: 0,
|
|
errors: [],
|
|
duration: 0
|
|
};
|
|
|
|
const startTime = Date.now();
|
|
|
|
for (const recordId of recordIds) {
|
|
try {
|
|
await this.syncSingleRecord(recordId);
|
|
syncResult.syncedRecords += 1;
|
|
} catch (error) {
|
|
syncResult.errors.push({
|
|
recordId,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
syncResult.duration = Date.now() - startTime;
|
|
console.log('=== SINCRONIZAÇÃO SELETIVA CONCLUÍDA ===');
|
|
|
|
return syncResult;
|
|
|
|
} catch (error) {
|
|
console.error('Erro na sincronização seletiva:', error);
|
|
throw new SyncError('Falha na sincronização seletiva', error);
|
|
}
|
|
}
|
|
|
|
// Métodos auxiliares
|
|
private async syncUserData(): Promise<void> {
|
|
const userData = await this.api.getCurrentUser();
|
|
await this.database.saveUser(userData);
|
|
}
|
|
|
|
private async syncDeliveries(): Promise<{ count: number }> {
|
|
const deliveries = await this.api.getDeliveries();
|
|
let count = 0;
|
|
|
|
for (const delivery of deliveries) {
|
|
await this.database.saveDelivery(delivery);
|
|
count += 1;
|
|
}
|
|
|
|
return { count };
|
|
}
|
|
|
|
private async syncSettings(): Promise<void> {
|
|
const settings = await this.api.getSettings();
|
|
for (const [key, value] of Object.entries(settings)) {
|
|
await this.database.saveSetting(key, value);
|
|
}
|
|
}
|
|
|
|
private async syncReferenceData(): Promise<void> {
|
|
// Sincronizar dados de referência como:
|
|
// - Lista de produtos
|
|
// - Configurações de entrega
|
|
// - Dados de clientes
|
|
// - Configurações de rota
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Fila de Sincronização
|
|
|
|
**Arquivo**: `src/services/syncQueue.ts`
|
|
|
|
```typescript
|
|
interface SyncQueueItem {
|
|
id: string;
|
|
type: 'create' | 'update' | 'delete';
|
|
table: string;
|
|
recordId: string;
|
|
data: any;
|
|
timestamp: number;
|
|
retryCount: number;
|
|
maxRetries: number;
|
|
priority: 'high' | 'normal' | 'low';
|
|
}
|
|
|
|
class SyncQueue {
|
|
private queue: SyncQueueItem[] = [];
|
|
private processing: boolean = false;
|
|
|
|
// Adicionar item à fila
|
|
async addItem(item: Omit<SyncQueueItem, 'id' | 'timestamp' | 'retryCount'>): Promise<string> {
|
|
const queueItem: SyncQueueItem = {
|
|
...item,
|
|
id: `sync_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
timestamp: Date.now(),
|
|
retryCount: 0,
|
|
maxRetries: item.maxRetries || 3
|
|
};
|
|
|
|
this.queue.push(queueItem);
|
|
this.queue.sort((a, b) => this.getPriorityValue(b.priority) - this.getPriorityValue(a.priority));
|
|
|
|
console.log(`Item adicionado à fila: ${queueItem.id}`);
|
|
return queueItem.id;
|
|
}
|
|
|
|
// Processar fila
|
|
async processQueue(): Promise<void> {
|
|
if (this.processing || this.queue.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.processing = true;
|
|
console.log(`Processando fila com ${this.queue.length} itens`);
|
|
|
|
while (this.queue.length > 0) {
|
|
const item = this.queue.shift();
|
|
if (!item) break;
|
|
|
|
try {
|
|
await this.processItem(item);
|
|
console.log(`Item processado com sucesso: ${item.id}`);
|
|
} catch (error) {
|
|
console.error(`Erro ao processar item ${item.id}:`, error);
|
|
|
|
item.retryCount += 1;
|
|
|
|
if (item.retryCount < item.maxRetries) {
|
|
// Reagendar item
|
|
this.queue.push(item);
|
|
console.log(`Item reagendado: ${item.id} (tentativa ${item.retryCount})`);
|
|
} else {
|
|
console.error(`Item falhou após ${item.maxRetries} tentativas: ${item.id}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.processing = false;
|
|
console.log('Processamento da fila concluído');
|
|
}
|
|
|
|
private async processItem(item: SyncQueueItem): Promise<void> {
|
|
switch (item.type) {
|
|
case 'create':
|
|
await this.api.createRecord(item.table, item.data);
|
|
break;
|
|
case 'update':
|
|
await this.api.updateRecord(item.table, item.recordId, item.data);
|
|
break;
|
|
case 'delete':
|
|
await this.api.deleteRecord(item.table, item.recordId);
|
|
break;
|
|
}
|
|
|
|
// Marcar como sincronizado no banco local
|
|
await this.database.markAsSynced(item.table, item.recordId);
|
|
}
|
|
|
|
private getPriorityValue(priority: string): number {
|
|
switch (priority) {
|
|
case 'high': return 3;
|
|
case 'normal': return 2;
|
|
case 'low': return 1;
|
|
default: return 2;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Resolução de Conflitos
|
|
|
|
**Arquivo**: `src/services/conflictResolver.ts`
|
|
|
|
```typescript
|
|
interface Conflict {
|
|
id: string;
|
|
table: string;
|
|
recordId: string;
|
|
localData: any;
|
|
serverData: any;
|
|
conflictFields: string[];
|
|
timestamp: number;
|
|
}
|
|
|
|
class ConflictResolver {
|
|
// Detectar conflitos
|
|
async detectConflicts(serverChanges: any[], localChanges: any[]): Promise<Conflict[]> {
|
|
const conflicts: Conflict[] = [];
|
|
|
|
for (const serverChange of serverChanges) {
|
|
const localChange = localChanges.find(lc => lc.id === serverChange.id);
|
|
|
|
if (localChange) {
|
|
const conflictFields = this.findConflictFields(serverChange, localChange);
|
|
|
|
if (conflictFields.length > 0) {
|
|
conflicts.push({
|
|
id: `conflict_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
table: serverChange.table,
|
|
recordId: serverChange.id,
|
|
localData: localChange,
|
|
serverData: serverChange,
|
|
conflictFields,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return conflicts;
|
|
}
|
|
|
|
// Resolver conflito
|
|
async resolveConflict(conflict: Conflict, resolution: ConflictResolution): Promise<void> {
|
|
let resolvedData: any;
|
|
|
|
switch (resolution.type) {
|
|
case 'server_wins':
|
|
resolvedData = conflict.serverData;
|
|
break;
|
|
case 'client_wins':
|
|
resolvedData = conflict.localData;
|
|
break;
|
|
case 'merge':
|
|
resolvedData = this.mergeData(conflict.localData, conflict.serverData, resolution.data);
|
|
break;
|
|
}
|
|
|
|
// Atualizar banco local com dados resolvidos
|
|
await this.database.updateRecord(conflict.table, conflict.recordId, resolvedData);
|
|
|
|
// Marcar conflito como resolvido
|
|
await this.database.markConflictResolved(conflict.id);
|
|
}
|
|
|
|
private findConflictFields(serverData: any, localData: any): string[] {
|
|
const conflictFields: string[] = [];
|
|
const fieldsToCheck = ['status', 'notes', 'photos', 'signature', 'completed_time'];
|
|
|
|
for (const field of fieldsToCheck) {
|
|
if (serverData[field] !== localData[field]) {
|
|
conflictFields.push(field);
|
|
}
|
|
}
|
|
|
|
return conflictFields;
|
|
}
|
|
|
|
private mergeData(localData: any, serverData: any, mergeRules?: any): any {
|
|
const merged = { ...localData };
|
|
|
|
// Aplicar regras de merge específicas
|
|
if (mergeRules) {
|
|
for (const [field, rule] of Object.entries(mergeRules)) {
|
|
switch (rule) {
|
|
case 'latest':
|
|
merged[field] = serverData[field];
|
|
break;
|
|
case 'append':
|
|
if (Array.isArray(merged[field]) && Array.isArray(serverData[field])) {
|
|
merged[field] = [...merged[field], ...serverData[field]];
|
|
}
|
|
break;
|
|
case 'combine':
|
|
merged[field] = `${merged[field]} ${serverData[field]}`.trim();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Estrutura de Banco de Dados para Sincronização
|
|
|
|
### 1. Tabelas Adicionais
|
|
|
|
```sql
|
|
-- Tabela de controle de sincronização
|
|
CREATE TABLE IF NOT EXISTS sync_control (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
table_name TEXT NOT NULL,
|
|
last_sync_timestamp INTEGER,
|
|
sync_status TEXT DEFAULT 'pending',
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
);
|
|
|
|
-- Tabela de conflitos
|
|
CREATE TABLE IF NOT EXISTS sync_conflicts (
|
|
id TEXT PRIMARY KEY,
|
|
table_name TEXT NOT NULL,
|
|
record_id TEXT NOT NULL,
|
|
local_data TEXT,
|
|
server_data TEXT,
|
|
conflict_fields TEXT,
|
|
resolution TEXT,
|
|
resolved_at INTEGER,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
);
|
|
|
|
-- Tabela de log de sincronização
|
|
CREATE TABLE IF NOT EXISTS sync_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sync_type TEXT NOT NULL,
|
|
table_name TEXT,
|
|
record_id TEXT,
|
|
action TEXT,
|
|
success INTEGER DEFAULT 1,
|
|
error_message TEXT,
|
|
duration INTEGER,
|
|
timestamp INTEGER DEFAULT (strftime('%s', 'now'))
|
|
);
|
|
|
|
-- Adicionar campos de controle às tabelas existentes
|
|
ALTER TABLE deliveries ADD COLUMN version INTEGER DEFAULT 1;
|
|
ALTER TABLE deliveries ADD COLUMN last_modified INTEGER DEFAULT (strftime('%s', 'now'));
|
|
ALTER TABLE deliveries ADD COLUMN sync_timestamp INTEGER;
|
|
ALTER TABLE deliveries ADD COLUMN conflict_resolution TEXT;
|
|
```
|
|
|
|
### 2. Índices para Performance
|
|
|
|
```sql
|
|
-- Índices para sincronização
|
|
CREATE INDEX IF NOT EXISTS idx_deliveries_sync_timestamp ON deliveries(sync_timestamp);
|
|
CREATE INDEX IF NOT EXISTS idx_deliveries_last_modified ON deliveries(last_modified);
|
|
CREATE INDEX IF NOT EXISTS idx_deliveries_version ON deliveries(version);
|
|
CREATE INDEX IF NOT EXISTS idx_sync_log_timestamp ON sync_log(timestamp);
|
|
CREATE INDEX IF NOT EXISTS idx_sync_conflicts_resolved ON sync_conflicts(resolved_at);
|
|
```
|
|
|
|
## Implementação de Endpoints para Sincronização
|
|
|
|
### 1. Endpoints do Servidor
|
|
|
|
```typescript
|
|
// Endpoints necessários no servidor
|
|
interface SyncEndpoints {
|
|
// Sincronização inicial
|
|
'GET /v1/sync/initial': () => Promise<InitialSyncData>;
|
|
|
|
// Sincronização incremental
|
|
'GET /v1/sync/changes': (since: number) => Promise<ChangeSet>;
|
|
'POST /v1/sync/changes': (changes: ChangeSet) => Promise<SyncResult>;
|
|
|
|
// Sincronização seletiva
|
|
'POST /v1/sync/selective': (ids: string[]) => Promise<SelectiveSyncData>;
|
|
|
|
// Resolução de conflitos
|
|
'POST /v1/sync/conflicts/resolve': (conflicts: ConflictResolution[]) => Promise<void>;
|
|
|
|
// Status de sincronização
|
|
'GET /v1/sync/status': () => Promise<SyncStatus>;
|
|
}
|
|
```
|
|
|
|
### 2. Implementação no Cliente
|
|
|
|
```typescript
|
|
// src/services/api.ts - Métodos adicionais
|
|
class ApiService {
|
|
// Obter dados para sincronização inicial
|
|
async getInitialSyncData(): Promise<InitialSyncData> {
|
|
const response = await this.request('/v1/sync/initial');
|
|
return response.data;
|
|
}
|
|
|
|
// Obter mudanças desde timestamp
|
|
async getChangesSince(timestamp: number): Promise<ChangeSet> {
|
|
const response = await this.request(`/v1/sync/changes?since=${timestamp}`);
|
|
return response.data;
|
|
}
|
|
|
|
// Enviar mudanças locais
|
|
async sendLocalChanges(changes: ChangeSet): Promise<SyncResult> {
|
|
const response = await this.request('/v1/sync/changes', {
|
|
method: 'POST',
|
|
body: JSON.stringify(changes)
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
// Sincronização seletiva
|
|
async performSelectiveSync(ids: string[]): Promise<SelectiveSyncData> {
|
|
const response = await this.request('/v1/sync/selective', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ids })
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
// Resolver conflitos
|
|
async resolveConflicts(resolutions: ConflictResolution[]): Promise<void> {
|
|
await this.request('/v1/sync/conflicts/resolve', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ resolutions })
|
|
});
|
|
}
|
|
|
|
// Obter status de sincronização
|
|
async getSyncStatus(): Promise<SyncStatus> {
|
|
const response = await this.request('/v1/sync/status');
|
|
return response.data;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Interface de Usuário para Sincronização
|
|
|
|
### 1. Tela de Sincronização Inicial
|
|
|
|
```typescript
|
|
// src/screens/sync/InitialSyncScreen.tsx
|
|
const InitialSyncScreen: React.FC = () => {
|
|
const { startInitialSync, syncProgress, syncStatus } = useInitialSync();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const handleStartSync = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
await startInitialSync();
|
|
// Navegar para tela principal após sincronização
|
|
navigation.replace('Main');
|
|
} catch (error) {
|
|
Alert.alert('Erro', 'Falha na sincronização inicial');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<LinearGradient colors={[COLORS.primary, "#3B82F6"]} style={styles.header}>
|
|
<Text style={styles.title}>Sincronização Inicial</Text>
|
|
<Text style={styles.subtitle}>
|
|
Baixando dados necessários para funcionamento offline
|
|
</Text>
|
|
</LinearGradient>
|
|
|
|
<View style={styles.content}>
|
|
<View style={styles.progressContainer}>
|
|
<ProgressBar progress={syncProgress} />
|
|
<Text style={styles.progressText}>
|
|
{Math.round(syncProgress * 100)}% concluído
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.statusContainer}>
|
|
<Text style={styles.statusText}>
|
|
Status: {getStatusText(syncStatus)}
|
|
</Text>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.syncButton, isLoading && styles.syncButtonDisabled]}
|
|
onPress={handleStartSync}
|
|
disabled={isLoading}
|
|
>
|
|
<Ionicons name="cloud-download" size={24} color="white" />
|
|
<Text style={styles.syncButtonText}>
|
|
{isLoading ? 'Sincronizando...' : 'Iniciar Sincronização'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 2. Componente de Status de Sincronização
|
|
|
|
```typescript
|
|
// src/components/SyncStatusIndicator.tsx
|
|
const SyncStatusIndicator: React.FC = () => {
|
|
const { syncStatus, lastSyncTime, pendingChanges } = useSync();
|
|
const [showDetails, setShowDetails] = useState(false);
|
|
|
|
const getStatusColor = (status: SyncStatus) => {
|
|
switch (status) {
|
|
case SyncStatus.SYNCED: return COLORS.success;
|
|
case SyncStatus.PENDING: return COLORS.warning;
|
|
case SyncStatus.CONFLICT: return COLORS.danger;
|
|
case SyncStatus.ERROR: return COLORS.danger;
|
|
case SyncStatus.OFFLINE: return COLORS.textLight;
|
|
default: return COLORS.textLight;
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status: SyncStatus) => {
|
|
switch (status) {
|
|
case SyncStatus.SYNCED: return 'checkmark-circle';
|
|
case SyncStatus.PENDING: return 'time';
|
|
case SyncStatus.CONFLICT: return 'warning';
|
|
case SyncStatus.ERROR: return 'close-circle';
|
|
case SyncStatus.OFFLINE: return 'cloud-offline';
|
|
default: return 'help-circle';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.container}
|
|
onPress={() => setShowDetails(!showDetails)}
|
|
>
|
|
<View style={styles.statusRow}>
|
|
<Ionicons
|
|
name={getStatusIcon(syncStatus)}
|
|
size={16}
|
|
color={getStatusColor(syncStatus)}
|
|
/>
|
|
<Text style={[styles.statusText, { color: getStatusColor(syncStatus) }]}>
|
|
{getStatusText(syncStatus)}
|
|
</Text>
|
|
{pendingChanges > 0 && (
|
|
<View style={styles.badge}>
|
|
<Text style={styles.badgeText}>{pendingChanges}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{showDetails && (
|
|
<View style={styles.detailsContainer}>
|
|
<Text style={styles.detailText}>
|
|
Última sincronização: {formatDate(lastSyncTime)}
|
|
</Text>
|
|
<Text style={styles.detailText}>
|
|
Mudanças pendentes: {pendingChanges}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 3. Tela de Resolução de Conflitos
|
|
|
|
```typescript
|
|
// src/screens/sync/ConflictResolutionScreen.tsx
|
|
const ConflictResolutionScreen: React.FC = () => {
|
|
const { conflicts, resolveConflict } = useSync();
|
|
const [selectedConflict, setSelectedConflict] = useState<Conflict | null>(null);
|
|
|
|
const handleResolveConflict = async (resolution: ConflictResolution) => {
|
|
if (!selectedConflict) return;
|
|
|
|
try {
|
|
await resolveConflict(selectedConflict.id, resolution);
|
|
setSelectedConflict(null);
|
|
Alert.alert('Sucesso', 'Conflito resolvido com sucesso');
|
|
} catch (error) {
|
|
Alert.alert('Erro', 'Falha ao resolver conflito');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.title}>Resolução de Conflitos</Text>
|
|
|
|
<FlatList
|
|
data={conflicts}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={({ item }) => (
|
|
<TouchableOpacity
|
|
style={styles.conflictItem}
|
|
onPress={() => setSelectedConflict(item)}
|
|
>
|
|
<Text style={styles.conflictTitle}>
|
|
Conflito em {item.table} - {item.recordId}
|
|
</Text>
|
|
<Text style={styles.conflictFields}>
|
|
Campos: {item.conflictFields.join(', ')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
/>
|
|
|
|
{selectedConflict && (
|
|
<ConflictResolutionModal
|
|
conflict={selectedConflict}
|
|
onResolve={handleResolveConflict}
|
|
onClose={() => setSelectedConflict(null)}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
```
|
|
|
|
## Estratégias de Otimização
|
|
|
|
### 1. Compressão de Dados
|
|
|
|
```typescript
|
|
// src/utils/compression.ts
|
|
import { compress, decompress } from 'lz-string';
|
|
|
|
export class DataCompression {
|
|
static compress(data: any): string {
|
|
const jsonString = JSON.stringify(data);
|
|
return compress(jsonString);
|
|
}
|
|
|
|
static decompress(compressedData: string): any {
|
|
const jsonString = decompress(compressedData);
|
|
return JSON.parse(jsonString);
|
|
}
|
|
|
|
static async compressFile(filePath: string): Promise<string> {
|
|
const fileContent = await FileSystem.readAsStringAsync(filePath);
|
|
return this.compress(fileContent);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Cache Inteligente
|
|
|
|
```typescript
|
|
// src/services/cacheService.ts
|
|
class CacheService {
|
|
private cache = new Map<string, CacheEntry>();
|
|
private maxSize = 100; // MB
|
|
private currentSize = 0;
|
|
|
|
async set(key: string, data: any, ttl: number = 3600000): Promise<void> {
|
|
const compressedData = DataCompression.compress(data);
|
|
const size = compressedData.length;
|
|
|
|
// Verificar se há espaço suficiente
|
|
if (this.currentSize + size > this.maxSize * 1024 * 1024) {
|
|
await this.evictOldEntries();
|
|
}
|
|
|
|
this.cache.set(key, {
|
|
data: compressedData,
|
|
timestamp: Date.now(),
|
|
ttl,
|
|
size
|
|
});
|
|
|
|
this.currentSize += size;
|
|
}
|
|
|
|
async get(key: string): Promise<any | null> {
|
|
const entry = this.cache.get(key);
|
|
|
|
if (!entry) return null;
|
|
|
|
// Verificar se expirou
|
|
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
this.cache.delete(key);
|
|
this.currentSize -= entry.size;
|
|
return null;
|
|
}
|
|
|
|
return DataCompression.decompress(entry.data);
|
|
}
|
|
|
|
private async evictOldEntries(): Promise<void> {
|
|
const entries = Array.from(this.cache.entries())
|
|
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
|
|
// Remover 20% das entradas mais antigas
|
|
const toRemove = Math.floor(entries.length * 0.2);
|
|
|
|
for (let i = 0; i < toRemove; i++) {
|
|
const [key, entry] = entries[i];
|
|
this.cache.delete(key);
|
|
this.currentSize -= entry.size;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Sincronização em Background
|
|
|
|
```typescript
|
|
// src/services/backgroundSync.ts
|
|
import * as BackgroundFetch from 'expo-background-fetch';
|
|
import * as TaskManager from 'expo-task-manager';
|
|
|
|
const BACKGROUND_SYNC_TASK = 'background-sync';
|
|
|
|
TaskManager.defineTask(BACKGROUND_SYNC_TASK, async () => {
|
|
try {
|
|
const syncService = new SyncService();
|
|
await syncService.performIncrementalSync();
|
|
|
|
return BackgroundFetch.BackgroundFetchResult.NewData;
|
|
} catch (error) {
|
|
console.error('Erro na sincronização em background:', error);
|
|
return BackgroundFetch.BackgroundFetchResult.Failed;
|
|
}
|
|
});
|
|
|
|
export class BackgroundSyncService {
|
|
static async registerBackgroundSync(): Promise<void> {
|
|
try {
|
|
await BackgroundFetch.registerTaskAsync(BACKGROUND_SYNC_TASK, {
|
|
minimumInterval: 15 * 60, // 15 minutos
|
|
stopOnTerminate: false,
|
|
startOnBoot: true,
|
|
});
|
|
} catch (error) {
|
|
console.error('Erro ao registrar sincronização em background:', error);
|
|
}
|
|
}
|
|
|
|
static async unregisterBackgroundSync(): Promise<void> {
|
|
try {
|
|
await BackgroundFetch.unregisterTaskAsync(BACKGROUND_SYNC_TASK);
|
|
} catch (error) {
|
|
console.error('Erro ao desregistrar sincronização em background:', error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Monitoramento e Logs
|
|
|
|
### 1. Sistema de Logs
|
|
|
|
```typescript
|
|
// src/services/logger.ts
|
|
interface LogEntry {
|
|
level: 'debug' | 'info' | 'warn' | 'error';
|
|
message: string;
|
|
timestamp: number;
|
|
context?: any;
|
|
userId?: string;
|
|
}
|
|
|
|
class Logger {
|
|
private logs: LogEntry[] = [];
|
|
private maxLogs = 1000;
|
|
|
|
log(level: LogEntry['level'], message: string, context?: any): void {
|
|
const logEntry: LogEntry = {
|
|
level,
|
|
message,
|
|
timestamp: Date.now(),
|
|
context,
|
|
userId: this.getCurrentUserId()
|
|
};
|
|
|
|
this.logs.push(logEntry);
|
|
|
|
// Manter apenas os logs mais recentes
|
|
if (this.logs.length > this.maxLogs) {
|
|
this.logs = this.logs.slice(-this.maxLogs);
|
|
}
|
|
|
|
// Log no console para desenvolvimento
|
|
if (__DEV__) {
|
|
console[level](`[${new Date().toISOString()}] ${message}`, context);
|
|
}
|
|
}
|
|
|
|
async exportLogs(): Promise<string> {
|
|
return JSON.stringify(this.logs, null, 2);
|
|
}
|
|
|
|
async clearLogs(): Promise<void> {
|
|
this.logs = [];
|
|
}
|
|
|
|
private getCurrentUserId(): string | undefined {
|
|
// Implementar obtenção do ID do usuário atual
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export const logger = new Logger();
|
|
```
|
|
|
|
### 2. Métricas de Performance
|
|
|
|
```typescript
|
|
// src/services/metrics.ts
|
|
interface SyncMetrics {
|
|
totalSyncs: number;
|
|
successfulSyncs: number;
|
|
failedSyncs: number;
|
|
averageSyncTime: number;
|
|
totalDataTransferred: number;
|
|
conflictsResolved: number;
|
|
}
|
|
|
|
class MetricsService {
|
|
private metrics: SyncMetrics = {
|
|
totalSyncs: 0,
|
|
successfulSyncs: 0,
|
|
failedSyncs: 0,
|
|
averageSyncTime: 0,
|
|
totalDataTransferred: 0,
|
|
conflictsResolved: 0
|
|
};
|
|
|
|
recordSync(success: boolean, duration: number, dataSize: number): void {
|
|
this.metrics.totalSyncs += 1;
|
|
|
|
if (success) {
|
|
this.metrics.successfulSyncs += 1;
|
|
} else {
|
|
this.metrics.failedSyncs += 1;
|
|
}
|
|
|
|
this.metrics.totalDataTransferred += dataSize;
|
|
|
|
// Calcular tempo médio
|
|
const totalTime = this.metrics.averageSyncTime * (this.metrics.totalSyncs - 1) + duration;
|
|
this.metrics.averageSyncTime = totalTime / this.metrics.totalSyncs;
|
|
}
|
|
|
|
recordConflictResolution(): void {
|
|
this.metrics.conflictsResolved += 1;
|
|
}
|
|
|
|
getMetrics(): SyncMetrics {
|
|
return { ...this.metrics };
|
|
}
|
|
|
|
async exportMetrics(): Promise<string> {
|
|
return JSON.stringify(this.metrics, null, 2);
|
|
}
|
|
}
|
|
|
|
export const metrics = new MetricsService();
|
|
```
|
|
|
|
## Considerações de Segurança
|
|
|
|
### 1. Criptografia de Dados Sensíveis
|
|
|
|
```typescript
|
|
// src/utils/encryption.ts
|
|
import CryptoJS from 'crypto-js';
|
|
|
|
export class DataEncryption {
|
|
private static readonly SECRET_KEY = 'your-secret-key'; // Em produção, usar variável de ambiente
|
|
|
|
static encrypt(data: string): string {
|
|
return CryptoJS.AES.encrypt(data, this.SECRET_KEY).toString();
|
|
}
|
|
|
|
static decrypt(encryptedData: string): string {
|
|
const bytes = CryptoJS.AES.decrypt(encryptedData, this.SECRET_KEY);
|
|
return bytes.toString(CryptoJS.enc.Utf8);
|
|
}
|
|
|
|
static encryptObject(obj: any): string {
|
|
return this.encrypt(JSON.stringify(obj));
|
|
}
|
|
|
|
static decryptObject<T>(encryptedData: string): T {
|
|
const decrypted = this.decrypt(encryptedData);
|
|
return JSON.parse(decrypted);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Validação de Dados
|
|
|
|
```typescript
|
|
// src/utils/validation.ts
|
|
export class DataValidation {
|
|
static validateDelivery(delivery: any): boolean {
|
|
const requiredFields = ['id', 'outId', 'customerName', 'status'];
|
|
|
|
for (const field of requiredFields) {
|
|
if (!delivery[field]) {
|
|
throw new Error(`Campo obrigatório ausente: ${field}`);
|
|
}
|
|
}
|
|
|
|
// Validar status
|
|
const validStatuses = ['pending', 'in_progress', 'delivered', 'failed'];
|
|
if (!validStatuses.includes(delivery.status)) {
|
|
throw new Error(`Status inválido: ${delivery.status}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static sanitizeData(data: any): any {
|
|
// Remover campos desnecessários
|
|
const sanitized = { ...data };
|
|
delete sanitized.internalId;
|
|
delete sanitized.tempData;
|
|
|
|
return sanitized;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Conclusão
|
|
|
|
Esta estratégia de sincronização offline fornece uma solução completa e robusta para permitir que o aplicativo funcione sem dependência de internet. A implementação inclui:
|
|
|
|
1. **Sincronização inicial completa** para carregar todos os dados necessários
|
|
2. **Sincronização incremental** para manter dados atualizados
|
|
3. **Resolução automática de conflitos** com interface para resolução manual
|
|
4. **Fila de sincronização** com retry automático
|
|
5. **Monitoramento e logs** para debugging e análise
|
|
6. **Otimizações de performance** com compressão e cache
|
|
7. **Segurança** com criptografia e validação
|
|
|
|
A implementação pode ser feita de forma incremental, começando com a sincronização inicial e expandindo gradualmente para incluir todas as funcionalidades avançadas.
|