Skip to content

Modo Offline e Sincronia

O Neemias adota uma arquitetura backend-primary, offline-resilient: o backend é a fonte da verdade definitiva, mas o frontend funciona plenamente offline usando o SQLite WASM (OPFS) como cache operacional. Desde v0.53.0, quando o backend está disponível, getStore() retorna um OnlineProxy que delega leituras/escritas via API, com hot-swap automático ao ficar offline.

Arquitetura

┌───────────────┐     online       ┌──────────────┐
│  OnlineProxy  │ ←──────────────→│  D1 (Cloudflare) │
│  (v0.53.0+)   │   fetch+JWT     │   Workers API    │
└───────┬───────┘                 └──────────────┘
        │ offline (hot-swap)
┌───────▼───────┐
│  SQLite WASM  │
│  (OPFS)       │
│  useSqlQuery  │
└───────┬───────┘

     React UI

Quando offline, todas as mutações são registradas localmente na syncQueue e enviadas ao backend quando a conectividade é restaurada. O resetStore() fecha OnlineProxy e cria um novo SQLiteStore lendo os mesmos dados OPFS.

OfflineContext

O OfflineContext (app/src/app/context/OfflineContext.tsx) é o coração do modo offline. Ele expõe:

PropriedadeTipoDescrição
isOnlinebooleanEstado atual da conectividade (navigator.onLine)
pendingCountnumberItens pendentes de sincronia (PENDING + RETRYING)
pendingEntriesSyncQueueEntry[]Lista completa de entradas pendentes
autoSyncingbooleanSe o dreno automático está em andamento
markAllSynced()() => Promise<void>Marca todos como sincronizados (uso quando não há backend)

Detecção de Conectividade

O provider reage aos eventos window online e offline:

typescript
window.addEventListener("online", () => setIsOnline(true));
window.addEventListener("offline", () => setIsOnline(false));

Ao detectar que o dispositivo voltou a ficar online, o contador de tentativas é resetado para zero.

Fila de Sincronia (syncQueueRepository)

O repositório gerencia a tabela syncQueue no Dexie com as seguintes operações:

  • listPending() — lista entradas com syncState: "PENDING" ou "RETRYING"
  • drainQueue(processor) — processa cada entrada pendente, chamando syncQueueEntryWithBackend()
  • markAllSynced() — marca todas como sincronizadas (usado quando BACKEND_URL não está configurado)
  • purgeSyncedEvents(days) — remove eventos sincronizados com mais de N dias (quota management)

Backoff Exponencial

Em caso de falha na sincronia, o sistema aplica backoff exponencial:

TentativaDelay
05 segundos
110 segundos
220 segundos
330 segundos
4+30 segundos (cap)

Após 6 tentativas (MAX_RETRY_ATTEMPTS), o sistema para de tentar automaticamente — o usuário pode forçar uma nova tentativa ao alternar o estado de conectividade.

Sessão Expirada Durante Sync

Se o backend retornar erro de autenticação (SyncAuthExpiredError), o contexto tenta renovar o token via refreshAccessToken(). Se a renovação falhar, a sessão é marcada como expirada e o sync é interrompido.

Sem Backend Configurado

Quando a variável de ambiente BACKEND_URL está vazia, o dreno automático simplesmente marca todos os itens como sincronizados localmente — útil para desenvolvimento e testes offline.

StatusIndicator

O componente StatusIndicator exibe o estado atual da sincronia na interface:

  • Online + fila vazia — indicador verde
  • Online + sincronizando — indicador animado com contagem de pendentes
  • Offline — indicador cinza/amarelo

Resolução de Conflitos

Durante a sincronia, conflitos entre eventos locais e remotos são resolvidos no servidor pela classe EventReplay com duas estratégias:

EstratégiaDescrição
ADMIN_PRECEDENCEEventos de roles com maior peso (ADMIN > CADASTRO > CHAMADOR > RELATORIOS) prevalecem
LAST_TIMESTAMP_WINSEm caso de empate de role, o evento com timestamp mais recente vence

O evento perdedor é marcado com isConflictLoser: true e o campo conflictSupersededBy aponta para o evento vencedor.

Ciclo de Vida Completo

  1. Usuário realiza ação offline → EventWriter grava evento + entrada na syncQueue (transação atômica)
  2. UI reage imediatamente via useLiveQuery
  3. Ao reconectar → OfflineContext detecta online e dispara drainQueue()
  4. Cada entrada é enviada via POST /api/v1/sync/events com header Idempotency-Key
  5. Backend processa, resolve conflitos e retorna confirmação
  6. Entrada marcada como SYNCED ou RETRYING (em caso de falha transitória)
  7. A cada 30 dias, entradas sincronizadas antigas são expurgadas

Fonte: app/CONTEXT.md + OfflineContext.tsx

Distribuído sob licença MIT.