Pular para o conteúdo principal

Service Worker (public/sw.js)

O front-web-base registra um Service Worker de ~456 linhas que cacheia documentos de navegação, não só assets. É a camada de cache mais próxima do usuário e a única que vive dentro do browser dele — fora do alcance de qualquer purge do Cloudflare.

:::tip Se alguém reporta "HTML velho depois do deploy" Comece por aqui. Um purge de zona, um manual-cache-purge.yml e um bump de CACHE_GENERATION não alcançam o cache do Service Worker. O box do SW é rotacionado por BUILD_ID, então um deploy normal resolve — mas se o /sw.js do usuário ficou preso, ou se a página serviu stale dentro da janela de frescor, o sintoma é exatamente "HTML velho" com todas as camadas de servidor já limpas. Ver Diagnóstico. :::

O que ele cacheia

CaminhoEstratégiaChave
Navegação (documento HTML)SWR + gate de frescorquery colapsada (ver abaixo)
/api/games/* e *.dataSWR + gate de frescorrequest completa
/assets/*cache-firstrequest completa
request.destination === "image"cache-first, limite de 1000 entradasrequest completa
/offline.htmlpré-cacheado no install

/clever (landing de afiliado) é a exceção explícita: sempre vai à rede, porque o loader dela processa os params de afiliado a cada carga.

A chave de navegação colapsa a query

Esta é a parte que mais surpreende. cacheKeyUrl():

// public/sw.js
function cacheKeyUrl(rawUrl) {
const url = new URL(rawUrl);
const version = url.searchParams.get("version");
const versionSuffix = version ? `&version=${encodeURIComponent(version)}` : "";
return `${url.origin}${url.pathname}?__build-id=${BUILD_ID}${versionSuffix}`;
}
  • Toda a query é descartada/, /?a=1 e /?utm_source=x colapsam na mesma entrada. Sem bloat, e o F5 de uma landing com UTM bate na mesma chave que já está cacheada.
  • ?version=X é o BYPASS DE EMERGÊNCIA. É o único param preservado na chave: /?version=2 pega uma entrada distinta de /. Use quando precisar forçar um usuário específico a sair de um documento cacheado.
  • __build-id é um param SINTÉTICO, só de chave — nunca vai pra rede. Build novo → chave nova → entrada velha inalcançável.

O colapso vale pra navegação. /api/* e .data (dataSWR) keiam pela request completa, então params de conteúdo (q, category, paginação) continuam preservados lá.

Na primeira carga (MISS) o SW busca a request original, com query, pra o loader root capturar o tracking, e grava sob a chave limpa. Redirects (redirected) não são cacheados, então a atribuição de clean-URL é preservada.

Gate de frescor + SWR

navigationSWR não é um SWR puro:

  • HIT dentro do TTL da config (idade < freshnessTtl): serve o HTML do cache, zero rede. O cache fica "impregnado" por até o valor de SSR_CACHE_HTML_TTL que a origem carimbou no Cache-Control.
  • HIT passado o TTL: serve o stale (0ms) e revalida em background com If-None-Match: <ETag> — 304 barato mantém a cópia, 200 substitui pra próxima.
  • MISS: rede, cacheia; rede falha → /offline.html.

A idade vem do header Date da resposta cacheada; a janela de frescor vem do Cache-Control (max-age tem precedência sobre s-maxage; sem nenhum → 0, sempre revalida). Ou seja: o TTL do documento no SW é o mesmo TTL que a borda declarou — não há número hardcoded aqui.

Há uma eviction deliberada no caminho de revalidação: um 200 autoritativo que não é mais text/html (ex: /robots.txt que passou a sair no-store da borda) evicta a entrada. Sem isso a cópia stale era servida em toda navegação até o box do build morrer — caso real de bet7k.cl servindo Disallow: / antigo indefinidamente no Ctrl+R.

Não existe bypass por auth no fetch handler, de propósito: um Service Worker não consegue ler o header Cookie (é forbidden-header — request.headers.get("Cookie") vem sempre vazio), então checar is_authenticated/jwt_token na request seria no-op. Foi um bug histórico, já removido.

A isolação depende das respostas: a borda (workers/middleware.ts) marca todo documento / .data / api autenticado como private, no-store, e isCacheableNavigation / isCacheableData respeitam isso → conteúdo per-usuário nunca entra no cache do SW.

Os shells públicos são auth-agnósticos (o SSR não carrega dado de usuário — ver Fluxo de autenticação), então servir um shell cacheado a um usuário logado é seguro: ele hidrata o auth client-side. Logados podem usar o cache do SW — é justamente o objetivo do cache instantâneo.

Guardas de isCacheableNavigation: só 200/301/308, só text/html, só same-origin não-redirecionado (type === "basic", !redirected), e nunca com Cache-Control: no-store.

Versionamento e limpeza dos boxes

const CACHE_NAME = "app-apcsrasj-v6-__SW_VERSION__";

__SW_VERSION__ é substituído por BUILD_ID em build time pelo swVersionPlugin (vite.config.ts, hook writeBundlebuild/client/sw.js).

O token apcsrasj-v6 é um literal: um Service Worker roda no browser sem acesso ao env do wrangler, então ele não acompanha o CACHE_GENERATION do front-ops/config/cache/generation.yml. Não confie nele como indicador de geração — a invalidação real é o BUILD_ID, que rotaciona em todo deploy.

Cada build é um "box" (um Cache do CacheStorage). A limpeza roda em três momentos:

  1. activate — deleta todos os caches cujo nome difere do atual, e purga entradas corrompidas (status non-2xx) do cache corrente.
  2. No fetch, throttled — no máximo 1× por hora (BOX_CLEANUP_INTERVAL_MS).
  3. Sob demanda — mensagem cleanup-old-boxes da página (ignora o throttle).

O SW chama self.skipWaiting() no install e self.clients.claim() no activate: todas as abas trocam pro SW novo em segundos, sem exigir reload.

Registro

// app/entry.client.tsx
navigator.serviceWorker.register(`/sw.js?build=${BUILD_ID}`).catch(() => {});

Roda depois da hidratação (via requestIdleCallback, fallback 4s) pra não competir com o React por CPU. O ?build=<BUILD_ID> faz o browser tratar o script como atualizado quando o build muda — garante a rotação do box mesmo se o revalidate do script travar. A query não muda o scope (continua /) nem quebra TWA/PWA.

/sw.js nunca é cacheado

workers/middleware.ts intercepta o pathname /sw.js e força Cache-Control: no-cache, must-revalidate — nem no cache assets:, nem com immutable. O browser precisa re-checar o script a cada visita pra pegar o build novo; um sw.js velho preso no HTTP cache travaria a atualização e a limpeza dos boxes antigos.

Prefetch de documento sob demanda

A página pode mandar uma mensagem cache-document (ver app/components/perf/PrefetchCurrentDocument.tsx) pedindo ao SW pra buscar o HTML da rota atual sem os params de tracking e gravar sob a mesma chave normalizada que a navegação usa. O React Router só cacheia o .data na navegação SPA; isto cobre o HTML que só entraria num F5. Se já estiver cacheado, não refaz.

Diagnóstico e escape hatches

FerramentaO que faz
app/utils/version-info.client.tscollectVersionInfo() reporta serviceWorker: "controlado" | "registrado" | "nenhum", versão do servidor (fetchServerVersion) e probes de HTML/.data (probeCurrent)
app/utils/force-clear-cache.client.tsforceClearCache() deleta todos os caches + desregistra os SWs; prepareFreshNextLoad() faz o mesmo preparando a próxima carga
app/utils/cache-purge.client.tspurgeAllClientCaches() — chamado no logout (via useAuthLogoutCleanup)

Roteiro pra um report de "vejo conteúdo velho":

  1. Confirme o estado do SWcollectVersionInfo() (serviceWorker + BUILD_ID do cliente vs. do servidor). BUILD_ID divergente = o usuário está num box antigo.
  2. Ainda divergente depois de reload? É o /sw.js preso ou o box não rotacionado — forceClearCache() resolve no cliente.
  3. BUILD_ID igual mas conteúdo velho? O SW está servindo dentro da janela de frescor que a borda declarou. O lever é SSR_CACHE_HTML_TTL do ambiente, não o SW. Ver Cache architecture.
  4. Precisa desviar um usuário específico agora? Mande ele abrir com ?version=2 — bypass de chave, sem tocar em nada no servidor.

Docs relacionadas