Pular para o conteúdo principal

@cactus-agents/platform-cache

Engine de cache multi-camada para Cloudflare Workers. Abstrai memória + CF Cache API + KV em uma interface única orientada a recursos, com política configurável via variável de ambiente.

Instalação

pnpm add @cactus-agents/platform-cache

Visão geral

O platform-cache implementa um pipeline de cache em camadas:

Requisição

1. Memory (in-process, instantâneo)
↓ miss
2. CF Cache API (edge, compartilhado entre workers)
↓ miss
3. Origin (fetch real)
↓ erro de origem
4. KV Snapshot (fallback estável — se habilitado na policy)

A política é definida em front-ops (YAML por brand/env) e injetada em deploy-time como PLATFORM_CACHE_POLICY_JSON. Se a variável estiver ausente, o engine trabalha em modo bypass — toda requisição vai direto à origem, sem cache (ideal para dev local).

Uso básico

import {
createCacheEngine,
parseCachePolicy,
CacheApiStore,
KvSnapshotStore,
NoopSnapshotStore,
} from "@cactus-agents/platform-cache";

// Em um Cloudflare Worker loader:
const policy = parseCachePolicy(env.PLATFORM_CACHE_POLICY_JSON);

const engine = createCacheEngine({
policy,
primaryStore: new CacheApiStore(),
snapshotStore: env.PLATFORM_CACHE_KV
? new KvSnapshotStore(env.PLATFORM_CACHE_KV)
: new NoopSnapshotStore(),
domain: env.ORIGIN_DOMAIN,
});

// Buscar um recurso com cache:
const result = await engine.fetch(
"brandConfig",
() => createBrandFromClient(client, opts),
{ waitUntil: ctx.waitUntil }
);

if (result.data) {
// result.status: "hit" | "stale" | "miss" | "fallback" | "error"
// result.source: "memory" | "cache_api" | "kv" | "origin" | null
}

API

createCacheEngine(config)

Cria uma instância do engine. Aceita CacheEngineConfig:

interface CacheEngineConfig {
policy: CachePolicy | null; // null = bypass (dev local sem policy)
primaryStore: PrimaryStore; // CacheApiStore ou implementação custom
snapshotStore: SnapshotStore; // KvSnapshotStore ou NoopSnapshotStore
domain: string; // ORIGIN_DOMAIN (ex: "casateste.com")
}

Retorna CacheEngine:

MétodoDescrição
fetch(resource, fetcher, opts?)Busca via pipeline — usa a chave padrão do resource
fetchWithKey(resource, suffix, fetcher, opts?)Busca com chave dinâmica — útil para recursos com múltiplas entradas (ex: detalhe de jogo por slug)
purge(resource, suffix?)Remove um resource específico do primary cache
purgeByTag(tag)Remove todos os resources que declaram essa tag na policy. Retorna a lista de resources purgados.
purgeByTagGlob(pattern)Mesmo que purgeByTag, mas o tag é interpretado como glob (* casa qualquer sequência, ? casa um char). Ex: purgeByTagGlob("games:*")
purgeAll()Remove todos os entries deste domínio do memory tier (e KV snapshots de resources com fallbackEnabled)

FetchOptions

interface FetchOptions {
mode?: "normal" | "cache-only"; // default: "normal"
waitUntil?: (promise: Promise<unknown>) => void; // ctx.waitUntil do CF
}
  • "normal" — pipeline completo: memory → cache_api → origin → snapshot
  • "cache-only" — consulta apenas o cache primário, nunca chama a origem. Retorna null em miss.

O waitUntil é essencial para stale-while-revalidate: ao passar ctx.waitUntil, a revalidação em background é registrada no Worker sem bloquear a resposta.

parseCachePolicy(json)

Deserializa a policy do env var PLATFORM_CACHE_POLICY_JSON:

const policy = parseCachePolicy(env.PLATFORM_CACHE_POLICY_JSON);
// Retorna CachePolicy | null
// null quando a string está ausente, vazia ou inválida

buildCacheKey(domain, keyPrefix, suffix?)

Gera a chave de cache no formato esperado pela CF Cache API:

https://{domain}-cache/{keyPrefix}:{suffix}/v1

Normalmente não é necessário chamar diretamente — o engine resolve as chaves internamente via ResourcePolicy.storage.keyPrefix.

getResourcePolicy(policy, resource)

Retorna a ResourcePolicy para um recurso específico, ou null se a policy é nula, o recurso não existe ou está desabilitado:

const rp = getResourcePolicy(policy, "brandConfig");
// null = recurso não configurado ou desabilitado → bypass

findResourcesByTag(policy, tag)

Retorna todos os resource names cuja policy declara o tag (match exato):

findResourcesByTag(policy, "catalog")
// → ["homeRows", "casinoRows", "casinoLiveRows", "gamesBase", ...]

findResourcesByTagGlob(policy, pattern)

Como findResourcesByTag, mas o pattern aceita glob (* casa qualquer sequência, ? casa um char):

findResourcesByTagGlob(policy, "games:*")
// → resources com tags games:list, games:detail, games:wins, etc

findResourcesByTagGlob(policy, "*")
// → todos os resources tageados (escape hatch pra "tudo")

listAllTags(policy)

Retorna todas as tags declaradas na policy, ordenadas alfabeticamente. Útil pra endpoints de introspecção:

listAllTags(policy)
// → ["appearance", "brand", "casino", "catalog", "config", "games:base", ...]

Stores

CacheApiStore

Store primária (memory in-process + CF Cache API). Dois níveis de cache em uma store só:

  • Memory: Map em memória, resolvido instantaneamente
  • CF Cache API: Cache de edge compartilhado entre instâncias do Worker
const store = new CacheApiStore();

Não recebe parâmetros. Em dev local (fora do Worker), a CF Cache API não está disponível — o store opera apenas em memória.

KvSnapshotStore

Store de snapshot usando Cloudflare KV. Usado como fallback estável quando a origem está falhando:

import type { KVNamespaceLike } from "@cactus-agents/platform-cache";

const store = new KvSnapshotStore(env.PLATFORM_CACHE_KV);

O binding PLATFORM_CACHE_KV é provisionado automaticamente pelo front-ops em deploy-time. O tipo KVNamespaceLike é exportado para facilitar mocks em testes.

NoopSnapshotStore

Snapshot store noop — não salva nem lê nada. Use quando KV não estiver disponível (dev local, ou quando fallbackEnabled: false para todos os recursos):

const store = new NoopSnapshotStore();

MemoryStore

Store de memória simples para testes unitários. Não usa CF Cache API:

import { MemoryStore } from "@cactus-agents/platform-cache";
// Útil em testes onde CF Cache API não está disponível

Tipos

CacheResult<T>

Retornado por engine.fetch() e engine.fetchWithKey():

interface CacheResult<T> {
data: T | null;
status: CacheResultStatus; // "hit" | "stale" | "miss" | "fallback" | "error"
source: CacheResultSource; // "memory" | "cache_api" | "kv" | "origin" | null
revalidated: boolean; // true = revalidação em background foi disparada
resource: string; // nome do recurso
error?: unknown; // presente quando status === "error"
}

Quando status === "error", use extractApiError() do @cactus-agents/api-client para normalizar o erro.

ResourcePolicy

Política de um recurso específico (vem do YAML do front-ops):

interface ResourcePolicy {
enabled: boolean;
ttlSeconds: number; // tempo de cache "fresh"
staleWhileRevalidateSeconds: number; // extra período para retornar stale + revalidar em BG
staleIfErrorSeconds: number; // extra período para retornar stale se origem errar
fallbackEnabled: boolean; // habilita KV snapshot como last-resort
storage: ResourceStorageConfig;
/**
* Cache tags — usados por `purgeByTag` e `purgeByTagGlob` para
* invalidar grupos de resources sem enumerá-los por nome.
* Convenções:
* - palavra simples: "catalog", "brand", "stats"
* - colon-separated para hierarquias: "games:list", "games:detail"
* - glob aceita no purge: purgeByTagGlob("games:*")
* Default: [] (resource só purgavel por nome).
*/
tags?: string[];
}

CachePolicy

Mapa de nome do recurso → ResourcePolicy:

type CachePolicy = Record<string, ResourcePolicy>;

Env vars relacionadas

VariávelTipoDescrição
PLATFORM_CACHE_POLICY_JSONstring (JSON)Política serializada, injetada pelo front-ops em deploy-time. Ausente em dev = bypass.
PLATFORM_CACHE_KVKV bindingBinding KV para snapshots. Configurado no wrangler.toml gerado pelo front-ops.

Veja env-vars para o contexto completo.

Recursos padrão cacheaveis

Definidos em front-ops/config/cache/defaults.yml. Resumo:

Ceiling: nenhum ttlSeconds de endpoint pode exceder 3600 (1h). O parseCachePolicy clamp é estrutural — um valor maior no YAML é reduzido silenciosamente.

ResourcekeyPrefixTTLSWRKVTags
brandConfigbrandConfig60s300sSimbrand, config
homeRowshomeRows3600s600sSimcatalog, rows, games:list, home
casinoRowscasinoRows3600s600sSimcatalog, rows, games:list, casino
casinoLiveRowscasinoLiveRows3600s600sSimcatalog, rows, games:list, casino, live
gamesBasegamesBase3600s300sSimcatalog, games:base
allGamesallGames3600s600sSimcatalog, games:list, games:all
topGamestopGames3600s600sSimcatalog, stats, games:high-payers
gameStatsstats-dl1800s1800sNãocatalog, stats, games:stats
gameDetailgame3600s600sNãocatalog, games:detail
gameCategorycategory3600s600sNãocatalog, games:category
gameProviderprovider3600s600sNãocatalog, games:provider
gameListPagelistPage300s300sNãocatalog, games:list, games:listPage
legalTermslegalTerms3600s300sNãolegal, config

Brands/envs podem fazer override em config/brands/<brand>/cache.yml ou .../environments/<env>/cache.yml (full replace por nível, não deep merge).

Invalidação de cache

Via purge handler (HTTP)

O package exporta createPurgeHandler que monta uma rota HTTP de purge autenticada:

import { createPurgeHandler } from "@cactus-agents/platform-cache";

// app/routes/api/cache/purge.ts
export async function action({ request, context }) {
const cf = context?.cloudflare?.env;
const engine = createPlatformCacheEngine(cf);
const secret = cf?.CACHE_PURGE_SECRET ?? "";
return createPurgeHandler({ engine, secret }).handle(request);
}

O handler aceita:

// Por tags (preferido)
{ "tags": ["catalog", "brand"] }

// Por glob de tag
{ "glob": "games:*" }

// Por segments (legado — resource names diretamente)
{ "segments": ["homeRows", "casinoRows"] }

// Sem nada = purgeAll
{}

Auth: header X-Cache-Secret deve casar com CACHE_PURGE_SECRET.

Programático

// Por resource name
await engine.purge("brandConfig");
await engine.purge("gameDetail", "sweet-bonanza"); // com suffix

// Por tag
const purged = await engine.purgeByTag("brand");
// → ["brandConfig"]

// Por glob
const purged = await engine.purgeByTagGlob("games:*");
// → ["homeRows", "casinoRows", "gameDetail", ...]

// Tudo (também limpa KV snapshot de resources com fallbackEnabled)
await engine.purgeAll();

A CF Cache API não suporta purge global programático; o purgeByPrefix itera as entries que o CacheApiStore rastreou em activeKeys. Resources nunca tocados nesse isolate não são afetados (não importa — eles também não estão "warm").

Exemplos

Busca com chave dinâmica (game detail)

const result = await engine.fetchWithKey(
"gameDetail",
`${provider}/${game}`, // suffix único por jogo
() => gamesService.getGameDetail(provider, game),
{ waitUntil: ctx.waitUntil }
);

Cache-only (search pré-populado)

// Não chama a origem se não tiver no cache — retorna null em miss
const cached = await engine.fetch(
"gamesBase",
() => { throw new Error("not reached"); },
{ mode: "cache-only" }
);