DEV Community

Cover image for Criei um SDK para tornar apps React Native mais resilientes a conexões ruins
Rafael Góes
Rafael Góes

Posted on

Criei um SDK para tornar apps React Native mais resilientes a conexões ruins

Um aplicativo mobile não precisa estar completamente offline para se tornar praticamente inutilizável.

Às vezes o dispositivo está tecnicamente conectado à internet, mas a conexão está extremamente lenta, instável ou sofrendo perdas constantes.

E é aí que começam os problemas:

  • requisições entram em timeout;
  • telas ficam carregando indefinidamente;
  • usuários tentam executar a mesma ação várias vezes;
  • operações podem ser duplicadas;
  • o aplicativo não sabe se deve tentar novamente, esperar ou simplesmente falhar;
  • e, no final, o usuário fecha o app.

Grande parte das bibliotecas de conectividade responde muito bem a uma pergunta:

O dispositivo está online ou offline?

Mas, em aplicações mobile reais, isso nem sempre é suficiente.

Foi pensando nisso que comecei a desenvolver o NetResilience SDK.


O que é o NetResilience?

O NetResilience é um SDK open source para React Native focado em tornar a comunicação HTTP mais resiliente em condições de internet ruim.

Em vez de tratar a conectividade apenas como online ou offline, o SDK tenta entender a qualidade real da conexão.

Atualmente, a rede pode ser classificada como:

EXCELLENT
MODERATE
POOR
OFFLINE
Enter fullscreen mode Exit fullscreen mode

A partir disso, a aplicação pode decidir como cada requisição deve se comportar.

Uma requisição pode:

  • ser executada imediatamente;
  • ser armazenada localmente;
  • entrar em uma fila;
  • ser tentada novamente;
  • aguardar uma conexão melhor;
  • ou ser rejeitada conforme sua política de resiliência.

A ideia básica

Imagine que um usuário esteja criando um pedido enquanto utiliza uma conexão móvel instável.

Normalmente teríamos algo parecido com:

await api.post("/orders", order);
Enter fullscreen mode Exit fullscreen mode

Se a conexão falhar no momento errado, a aplicação precisa decidir o que fazer.

Com o NetResilience, esse comportamento pode ser definido explicitamente.

const result = await net.post("/orders", order, {
  resilience: {
    queue: true,
    queueOnNetworkError: true,
    safety: "IDEMPOTENCY_PROTECTED",
    priority: "HIGH",
  },
});
Enter fullscreen mode Exit fullscreen mode

Se a requisição não puder ser enviada com segurança naquele momento, ela pode ser persistida no dispositivo.

if (result.status === "queued") {
  showMessage(
    "Pedido salvo no dispositivo e aguardando conexão."
  );
}
Enter fullscreen mode Exit fullscreen mode

Quando a conectividade melhorar, o SDK pode tentar sincronizar automaticamente as operações pendentes.


Fila persistente de requisições

Uma das partes mais importantes do projeto é a fila de requisições.

O NetResilience possui suporte a armazenamento persistente utilizando expo-sqlite.

import * as SQLite from "expo-sqlite";

import {
  ExpoSQLiteQueueStorage,
  NetResilienceClient,
} from "@netresilience/react-native";

const db = await SQLite.openDatabaseAsync(
  "netresilience.db"
);

const net = new NetResilienceClient({
  baseUrl: "https://api.example.com",
  storage: new ExpoSQLiteQueueStorage(db),

  authProvider: async () => {
    return auth.getCurrentToken();
  },
});

await net.initialize();
Enter fullscreen mode Exit fullscreen mode

Isso permite que operações pendentes sobrevivam mesmo se o aplicativo for fechado ou reiniciado.

Ou seja, a fila não existe apenas em memória.


Autenticação também virou um problema interessante

Quando começamos a persistir requisições HTTP localmente, aparece uma preocupação importante:

segurança.

Não é uma boa ideia armazenar tokens de autenticação diretamente junto com as requisições pendentes.

Por isso, o NetResilience remove informações sensíveis antes de persistir as operações.

No momento da sincronização, o SDK solicita novamente um token atualizado através do authProvider.

authProvider: async () => auth.getCurrentToken()
Enter fullscreen mode Exit fullscreen mode

Isso também evita um problema comum:

uma requisição ficar horas na fila e depois tentar ser enviada utilizando um token expirado.


Estratégia de retry

Tentar novamente imediatamente várias vezes não é exatamente resiliência.

Na prática, isso pode apenas aumentar o problema.

Imagine centenas ou milhares de dispositivos tentando atingir uma API repetidamente durante uma instabilidade.

Por isso, o SDK utiliza uma estratégia de:

exponential backoff + jitter

De forma simplificada:

requisição falha

↓
espera

retry

↓
espera mais tempo

retry

↓
conexão melhora

sincronização acontece
Enter fullscreen mode Exit fullscreen mode

O jitter adiciona pequenas variações ao tempo de espera.

Isso ajuda a evitar que milhares de clientes tentem novamente exatamente no mesmo momento.

Esse problema é conhecido como:

Thundering Herd
Enter fullscreen mode Exit fullscreen mode

Idempotência é extremamente importante

Retries podem se tornar perigosos quando estamos lidando com operações como:

  • pagamentos;
  • criação de pedidos;
  • reservas;
  • transferências;
  • alteração de saldo.

Imagine o seguinte cenário:

POST /payments

↓

servidor processa o pagamento

↓

conexão cai antes da resposta chegar ao aplicativo

↓

aplicativo entende que ocorreu erro

↓

requisição é enviada novamente
Enter fullscreen mode Exit fullscreen mode

Resultado:

o pagamento pode ser processado duas vezes.

Por isso, o NetResilience permite marcar determinadas operações como:

safety: "IDEMPOTENCY_PROTECTED"
Enter fullscreen mode Exit fullscreen mode

Nessas operações, o SDK pode gerar e reutilizar uma:

Idempotency-Key
Enter fullscreen mode Exit fullscreen mode

durante os retries.

Mas existe uma regra importante:

O backend também precisa implementar idempotência.

O SDK não consegue transformar automaticamente uma API não idempotente em uma API segura.


Qualidade da conexão

Outro objetivo do projeto é não depender exclusivamente da informação fornecida pelo sistema operacional.

Ter Wi-Fi conectado não significa necessariamente ter uma boa conexão com a API.

Por isso, o SDK pode aprender com o tráfego HTTP real da aplicação.

Algumas informações podem ser observadas, como:

  • latência;
  • falhas;
  • timeouts;
  • quantidade de bytes transferidos;
  • estabilidade das requisições.

Também é possível realizar probes HTTP opcionais.

net.startProbing({
  url: "https://probe.example.com/health",
  stableIntervalMs: 60_000,
  poorIntervalMs: 15_000,
});
Enter fullscreen mode Exit fullscreen mode

A ideia é utilizar essas informações para construir uma visão mais realista da conectividade do dispositivo.


Nem toda requisição deve entrar em uma fila

Esse foi outro ponto importante durante o desenvolvimento.

Não faria sentido simplesmente armazenar qualquer requisição que falhasse.

Por exemplo:

GET /restaurants
Enter fullscreen mode Exit fullscreen mode

Provavelmente não precisa ser executada horas depois.

Mas algo como:

POST /orders
Enter fullscreen mode Exit fullscreen mode

pode precisar.

Por isso, o comportamento é definido por requisição.

resilience: {
  queue: true,
  queueOnNetworkError: true,
  priority: "HIGH"
}
Enter fullscreen mode Exit fullscreen mode

Isso permite que a aplicação tenha políticas diferentes dependendo da importância da operação.


Onde esse tipo de SDK pode ser útil?

A ideia do NetResilience não é simplesmente detectar se existe internet.

O objetivo é criar uma camada entre:

Aplicação
    ↓
NetResilience
    ↓
HTTP
    ↓
Backend
Enter fullscreen mode Exit fullscreen mode

capaz de tomar decisões melhores quando a rede estiver instável.

Esse tipo de abordagem pode ser útil principalmente em aplicações como:

  • mobilidade urbana;
  • delivery;
  • logística;
  • sistemas de campo;
  • aplicativos financeiros;
  • sistemas utilizados em regiões com cobertura móvel limitada;
  • aplicativos que precisam continuar funcionando mesmo com internet instável.

Em muitos desses cenários, o problema não é estar totalmente offline.

O problema é ter uma conexão que existe, mas que não é confiável.


Por que resolvi tornar o projeto open source?

O NetResilience ainda está em desenvolvimento.

Algumas decisões de arquitetura provavelmente vão evoluir conforme novos cenários forem testados.

E foi justamente por isso que resolvi deixar o projeto open source.

Acredito que esse tipo de problema pode ser melhor resolvido com a participação de outros desenvolvedores.

Existem vários cenários que uma única pessoa dificilmente consegue testar sozinha:

  • diferentes dispositivos;
  • diferentes versões do Android e iOS;
  • conexões móveis instáveis;
  • redes congestionadas;
  • APIs com comportamentos diferentes;
  • aplicações com milhares de requisições;
  • diferentes estratégias de sincronização.

Quanto mais cenários reais forem testados, mais robusto o SDK pode se tornar.


Quer contribuir?

Se você trabalha com React Native, redes, sistemas distribuídos, backend ou simplesmente achou a ideia interessante, contribuições são muito bem-vindas.

Você pode ajudar de várias formas:

  • sugerindo melhorias;
  • abrindo Issues;
  • reportando bugs;
  • propondo novas funcionalidades;
  • melhorando a documentação;
  • criando testes;
  • analisando a arquitetura;
  • enviando Pull Requests;
  • compartilhando experiências reais com conexões instáveis.

Também tenho interesse em receber feedback principalmente sobre:

  • arquitetura da fila;
  • estratégias de retry;
  • detecção da qualidade da rede;
  • idempotência;
  • sincronização;
  • performance;
  • experiência da API do SDK.

Repositório

O projeto está disponível no GitHub:

https://github.com/rafaelgoesti/netresilience-sdk

Se quiser contribuir com o desenvolvimento, fique à vontade para abrir uma Issue ou enviar um Pull Request.

E se você gostar da ideia, deixar uma ⭐ no repositório também ajuda bastante a dar visibilidade ao projeto.

A ideia é fazer o NetResilience crescer como um projeto realmente construído com a comunidade.

🚀

Top comments (0)