Pular para o conteúdo principal

Services

O template separa serviços server-side (.server.ts) e client-side (.client.ts).

api.server.ts — ApiClient SSR

Cria um ApiClient configurado para uso em loaders e actions no server.

function createClient(
cloudflareEnv?: Record<string, string>,
options?: {
request?: Request;
cookies?: Record<string, string>;
} & Partial<Pick<ApiClientConfig, 'getAccessToken'>>
): ApiClient

Resolução de config:

  1. Tenta ler variáveis de cloudflareEnv primeiro
  2. Fallback para process.env (dev local)

Headers injetados automaticamente:

  • User-Agent — extraído do request (fallback "CactusWorker/1.0")
  • city, country, country_alpha3 — geolocalização
  • currency, jurisdiction — config da marca
  • Cookie — repassado do request original (quando cookies fornecido)

Nota: sempre usa OriginAccess.Desktop como origin access.

// Uso típico em loader:
export async function loader({ context, request }: Route.LoaderArgs) {
const client = createClient(context.cloudflare?.env, { request });
// ...
}

brand.server.ts — Brand Loading

Carrega a configuração de marca no server com cache:

function loadBrandConfig(
cloudflareEnv?: Record<string, string>
): Promise<BrandResult>

Retorno — discriminated union:

type BrandResult =
| { ok: true; brand: BrandConfig; fromCache: boolean }
| { ok: false; error: BrandError }

type BrandError = {
message: string;
status: number;
detail?: string;
}

Estratégia de cache:

  • Cache-first via getBrandCache(ttl), onde TTL vem de BRAND_CACHE_TTL (default: 3600 segundos)
  • No cache miss: cria ApiClient via createClient, chama createBrandFromClient(client, { country, language })
  • No erro de API: retorna { ok: false, error: { message, status, detail? } }
// Uso no loader:
const result = await loadBrandConfig(context.cloudflare?.env);
if (!result.ok) {
// renderiza BrandErrorPage
}
const { brand } = result;

sports.client.ts — SportsService Singleton

Singleton para integração com provedores de esportes no client-side:

function initSportsService(env: ClientEnv): SportsService
function getSportsService(): SportsService

Inicializado uma vez, retorna a mesma instância nas chamadas subsequentes.

sports.server.ts — SportsService Factory (SSR)

Factory que cria um SportsService por request no server:

function createSportsService(cloudflareEnv?: Record<string, string>): SportsService

Diferente do singleton client-side, o server cria uma instância nova a cada request para evitar estado compartilhado entre requests.

logger.server.ts — Structured Logging

Logging estruturado para uso em loaders e actions:

import { logger } from '~/services/logger.server';

logger.info('Brand loaded', { brand: brand.settings.name });
logger.error('Failed to load brand', { error });

auth.client.ts — AuthService Singleton

Singleton que inicializa o AuthService com um ApiClient client-side:

let service: AuthService | null = null;

export function initAuthService(env: ClientEnv): AuthService
export function getAuthService(): AuthService

user.client.ts — UserService Singleton

Singleton que inicializa o UserService com um ApiClient client-side. Deve ser inicializado após o usuário estar autenticado:

export function initUserService(env: ClientEnv): UserService
export function getUserService(): UserService

Os componentes da área do usuário fazem import('~/services/user.client') via dynamic import para garantir code-splitting — o bundle do UserService só é carregado quando o usuário está logado e acessa as páginas da conta.

cache.server.ts — ServerCache genérico

Classe genérica ServerCache com duas camadas de cache:

  1. In-memoryMap no Worker isolate (hot path, ~microsegundos)
  2. CF Cache API — Cache por datacenter (warm path, ~milisegundos)

Estratégia stale-while-revalidate: serve dados do cache imediatamente e revalida em background com ctx.waitUntil().

import { ServerCache } from '~/services/cache.server';

const cache = new ServerCache({ ttl: 300 });
const data = await cache.get('key', fetchFn, ctx);

games.server.ts — Factory GamesService (SSR)

Cria um GamesService para uso em loaders SSR. Wrapper fino sobre createGamesFromClient:

import { getGamesCacheService } from '~/services/games.server';

export async function loader({ context }: Route.LoaderArgs) {
const svc = getGamesCacheService(context);
const home = await svc.getHome();
return { home };
}

games.cache.server.ts — GamesCacheService

Encapsula GamesService + ServerCache para caching automático de todos os dados de jogos:

  • getHome() — Homepage rows com jogos (filtra rows vazias)
  • getBase() — Categorias + providers
  • getAllGames() — Lista completa paginada
  • getByCategory(slug) — Jogos de uma categoria específica
  • getByProvider(slug) — Jogos de um provider específico
  • getDetail(slug) — Detalhe de jogo
  • getTopWins() / getLastWins() — Wins recentes
  • getStats(slug) — Estatísticas
  • search(term) — Busca server-side contra dados em cache (nome, tags, provider)
  • purge(key) / purgeAll() — Invalidação manual de cache

games.client.ts — GamesService Singleton (client-side)

Singleton que inicializa o GamesService com um ApiClient client-side. Usado para chamadas diretas que não passam por cache (ex: start-game, votos):

import { getGamesClientService } from '~/services/games.client';

const games = getGamesClientService();
const vote = await games.vote(casinoGameId, true);

smartico.server.ts — Smartico Hash Generation

Gera o hash MD5 para identificação do usuário no Smartico. Executado apenas no server (loader do _layout.tsx):

import { generateSmarticoUserHash } from '~/services/smartico.server';

// No loader:
const cf = context?.cloudflare?.env;
const smarticoHash = generateSmarticoUserHash(
String(auth.user.id),
cf,
);

Internamente resolve a saltKey com prioridade: env var SMARTICO_SALT_KEY (CF secret → process.env) → smarticoHashConfig.saltKey (config file fallback).

O hash é passado ao client via loaderData e consumido pelo SmarticoInitializer para identificar o usuário no SDK Smartico. O saltKey nunca é exposto ao browser.

Separação de serviços

ArquivoContextoTokenUso
api.server.tsServer (loader/action)Sim (via Request cookie quando aplicável)Brand loading, auth/profile, ações protegidas
brand.server.tsServer (loader)NãoBrand config com cache (BrandResult)
cache.server.tsServer (loader)NãoCache genérico (in-memory + CF Cache API)
sports.server.tsServer (loader)NãoFactory SportsService por request
logger.server.tsServer (loader/action)NãoLogging estruturado
games.server.tsServer (loader)NãoFactory GamesService para SSR
games.cache.server.tsServer (loader)NãoGamesCacheService (cache + stale-while-revalidate)
games.client.tsClient (browser)NãoGamesService singleton (start-game, votos)
sports.client.tsClient (browser)NãoSportsService singleton
auth.client.tsClient (browser)Não (cookie HttpOnly)Login/logout e integração com UI
user.client.tsClient (browser)Sim (via ApiClient)Perfil, segurança, limites, IRPF, wallet
authProfile.client.tsClient (browser)Não (cookie HttpOnly)Busca profile atualizado (GET /api/auth/profile)
validation.client.tsClient (browser)Não (cookie HttpOnly)Validações: send email/SMS, verify, submit address/docs, accept terms
userLimits.client.tsClient (browser)Não (cookie HttpOnly)Atualiza limites prudenciais (POST /api/user/update-limits)
kyc.client.tsClient (browser)Não (cookie HttpOnly)Inicia KYC e consulta status via API routes
kyc.server.tsServer (action)Sim (via Request cookie)Factory KycService para server actions
smartico.server.tsServer (loader)NãoGeração de hash MD5 para Smartico

Essa separação é intencional: o token HttpOnly é lido no server (loaders/actions) e o client mantém apenas estado de UI/sessão derivado do loader.