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 UIQuando 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:
| Propriedade | Tipo | Descrição |
|---|---|---|
isOnline | boolean | Estado atual da conectividade (navigator.onLine) |
pendingCount | number | Itens pendentes de sincronia (PENDING + RETRYING) |
pendingEntries | SyncQueueEntry[] | Lista completa de entradas pendentes |
autoSyncing | boolean | Se 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:
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 comsyncState: "PENDING"ou"RETRYING"drainQueue(processor)— processa cada entrada pendente, chamandosyncQueueEntryWithBackend()markAllSynced()— marca todas como sincronizadas (usado quandoBACKEND_URLnã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:
| Tentativa | Delay |
|---|---|
| 0 | 5 segundos |
| 1 | 10 segundos |
| 2 | 20 segundos |
| 3 | 30 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égia | Descrição |
|---|---|
| ADMIN_PRECEDENCE | Eventos de roles com maior peso (ADMIN > CADASTRO > CHAMADOR > RELATORIOS) prevalecem |
| LAST_TIMESTAMP_WINS | Em 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
- Usuário realiza ação offline →
EventWritergrava evento + entrada nasyncQueue(transação atômica) - UI reage imediatamente via
useLiveQuery - Ao reconectar →
OfflineContextdetectaonlinee disparadrainQueue() - Cada entrada é enviada via
POST /api/v1/sync/eventscom headerIdempotency-Key - Backend processa, resolve conflitos e retorna confirmação
- Entrada marcada como
SYNCEDouRETRYING(em caso de falha transitória) - A cada 30 dias, entradas sincronizadas antigas são expurgadas
Fonte: app/CONTEXT.md + OfflineContext.tsx