PWA e offline¶
A camada PWA / offline-first / WebPush (Trilho P) torna seu app instalável e capaz de rodar sem rede. É compartilhada pelos dois modos — você escreve a casca PWA uma vez. 📱
Em construção (Trilho P)
Esta camada é o Trilho P do roadmap. As fases P0–P5 estão detalhadas no plano de design. Esta página descreve a superfície planejada e os fluxos principais.
As quatro peças¶
-
Instalável (P0)
Manifest + ícones + prompt de instalação. O app entra na tela inicial como um nativo.
-
Service worker (P1)
Precache do app-shell → offline após o 1º load + update lifecycle ("nova versão, recarregar").
-
Offline-first (P2)
Store IndexedDB + fila/replay de eventos no reconnect (Background Sync).
-
WebPush (P3)
Subscribe no browser; envio via
tempest-fastapi-sdk[webpush](VAPID).
P0 — App instalável¶
O primeiro passo é o manifest e a captura do prompt de instalação. O cliente
guarda o evento beforeinstallprompt e sua UI decide quando oferecer "Instalar".
from tempestweb.pwa import install
def view(app: object) -> object:
"""Show an Install button only when installation is available."""
if install.can_prompt():
return install_button(on_click=install.prompt)
return already_installed_banner()
Modo A também ganha com o PWA
No Modo A, o precache do service worker (P1) resolve o cold-start do bundle WASM — o segundo load abre instantâneo e offline.
P1 — Service worker: offline após o 1º load¶
O service worker faz precache do app-shell (assets com hash) e gerencia o
ciclo de atualização. Quando uma versão nova é publicada, a UI mostra "nova
versão, recarregar"; ao confirmar, skipWaiting ativa a nova versão.
from tempestweb.pwa import service_worker
async def setup_sw(app: object) -> None:
"""Register the service worker and wire the update lifecycle.
Args:
app: The running app handle.
"""
await service_worker.register(
url="/sw.js",
on_update=lambda: app.set_state(lambda s: setattr(s, "update_ready", True)),
on_error=lambda err: app.log.error("SW failed", error=err),
)
async def apply_update() -> None:
"""Activate the waiting service worker and reload."""
await service_worker.skip_waiting()
Feito quando
O segundo load offline abre o app. Publicar uma versão nova dispara o banner "recarregar" — e ao confirmar, o app já está na versão nova.
P2 — Offline-first em runtime¶
A store IndexedDB (owner-scoped por domínio) guarda dados e estado offline.
Mutations feitas offline entram numa fila durável com idempotency key (do
native.http) e reaplicam sozinhas ao voltar a rede (Background Sync).
from tempestweb.native import storage
async def save_draft(text: str) -> None:
"""Persist a draft to IndexedDB, surviving offline.
Args:
text: The draft body to store.
"""
await storage.put("drafts", {"id": "current", "text": text})
async def list_drafts() -> list[dict[str, object]]:
"""List stored drafts, newest first.
Returns:
The drafts ordered by creation time, most recent first.
"""
return await storage.list("drafts", order_by="created_at", reverse=True)
A divergência por modo é só de comportamento, não de API:
| Modo A | Modo B | |
|---|---|---|
| Dados offline | Vivem no browser; offline é pleno | Último estado em cache (read-only) |
| Mutations offline | Aplicadas localmente | Enfileiradas; o servidor reconcilia ao reconectar |
| Banner online/offline | Ligado ao status de rede | Ligado ao status da conexão WS/SSE |
Replay precisa de idempotency
Ao voltar a rede, a fila reenvia as mutations. Sem a idempotency_key do
native.http, um replay poderia duplicar efeito. Por isso a
fila offline depende da capacidade HTTP.
P3 — WebPush¶
O cliente faz subscribe (com a chave pública VAPID); o servidor envia via
tempest-fastapi-sdk[webpush] (pywebpush).
from tempestweb.pwa import webpush
async def enable_notifications(app: object) -> None:
"""Subscribe the browser to WebPush and persist the subscription.
Args:
app: The running app handle.
"""
sub = await webpush.subscribe(vapid_public_key=app.settings.VAPID_PUBLIC_KEY)
await app.native.http.request("POST", "/webpush/subscribe", json=sub.to_dict())
iOS/Safari exige PWA instalada
No iOS (16.4+), o WebPush só funciona com o PWA instalado na tela inicial. Em browsers desktop e Android funciona sem instalar. Teste em device real — veja Verificação manual.
P4 e P5 — Gate no CI e extras de manifest¶
- P4 — Gate PWA no CI. Um job roda Lighthouse PWA (headless) + testes de service worker; o CI reprova um PR que quebre "installable", o offline ou o push.
- P5 — Extras de manifest.
share_target(pareia comnative.share), shortcuts e file handlers.
Verificação manual¶
O que exige device/browser real
Algumas garantias de PWA não dá para automatizar 100%; o CI usa Lighthouse headless (P4), mas confirme à mão:
- Instalar o app a partir do prompt e abrir da tela inicial.
- Desligar a rede e confirmar que o 2º load abre o app (offline).
- Receber uma notificação WebPush — no iOS, com o PWA instalado.
Recap¶
- O Trilho P torna o app instalável (P0) e offline após o 1º load (P1).
- O runtime offline (P2) usa IndexedDB + fila/replay com idempotency key.
- WebPush (P3) faz subscribe no browser e envia via
tempest-fastapi-sdk. - O CI (P4) trava regressões com Lighthouse; alguns testes exigem device real.
A store offline é exposta ao Python como a capacidade storage — veja
Capacidades. Para a saúde em produção, veja
Observabilidade. 🚀