Criando adaptadores de fonte para telemetria
Como adaptadores separados transformam respostas do Spotify e do GitHub em retratos compactos e tipados, cada um com suas regras de polling, privacidade, cache e limites de uso da API.
Não quero esconder um cliente de API dentro de um componente do painel. Um adaptador pequeno deve consultar cada fonte e decidir quanto tempo o dado continua atual, quais campos guardar, por quanto tempo mantê-los e o que publicar após uma falha.
Spotify e GitHub são exemplos contrastantes. A reprodução do Spotify pode se tornar enganosa em poucos segundos e pode nem existir. Os dados de contribuições do GitHub mudam devagar, podem ser derivados de uma resposta maior e valem um cache entre reinicializações do processo. Ainda assim, as duas fontes precisam chegar à mesma camada de estatísticas do navegador.
O adaptador conversa com o sistema externo. O painel renderiza o resultado. Se um provedor cair, seu adaptador decide o que publicar e o painel continua recebendo o mesmo contrato.
Comece pelo contrato de saída
A interface comum dos módulos é deliberadamente pequena:
export type StatModule<T> = {
start: (...args: any[]) => void;
getLatest: () => T;
getHistory: () => T[];
getVersion: () => number;
};
Cada módulo tem uma tarefa: publicar retratos quando sua fonte mudar. getLatest alimenta o estado atual, getHistory fornece a visão inicial e getVersion permite que a rota SSE detecte mudanças sem enviar o mesmo valor repetidamente.
A interface genérica não torna os dados intercambiáveis. Cada fonte tem seu próprio schema em shared/stats. O Valibot valida os dados persistidos do GitHub antes do uso, enquanto os tipos compartilhados mantêm o código do servidor, a serialização do transporte e os stores do Solid alinhados.
O retrato público também é uma fronteira de segurança. Ele deve conter exatamente o que o painel precisa. Credenciais do provedor, respostas brutas, metadados do dispositivo e métodos de controle ficam no servidor.
Dê a cada fonte o seu próprio relógio
O intervalo de polling deve acompanhar o que o visitante vê:
| Fonte | Atualidade útil | Política local | Persistência |
|---|---|---|---|
| Spotify | Poucos segundos durante a reprodução | 2,5 s ativo, 15 s parado | Apenas histórico em memória |
| GitHub | Vários minutos | 30 min após sucesso | Retrato validado em disco |
Um timer compartilhado de cinco minutos seria inadequado para as duas fontes. Ele deixaria o Spotify desatualizado e consultaria o GitHub com mais frequência do que a interface precisa.
Adaptador 1: reduza o Spotify a um retrato seguro
A integração com o Spotify tem duas etapas de autenticação. O refresh token fica nas variáveis de ambiente do servidor. server/stats/spotify.ts troca esse token por um access token de curta duração e o mantém em memória até um minuto antes do vencimento informado:
if (tokenCache && tokenCache.expiresAt > Date.now() + TOKEN_EXPIRY_SAFETY_MARGIN_MS) {
return tokenCache.accessToken;
}
const response = await fetch(SPOTIFY_TOKEN_ENDPOINT, {
method: "POST",
headers: {
authorization: createBasicAuthHeader(clientId, clientSecret),
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
});
O navegador nunca recebe o refresh token ou o client secret. Depois da autenticação, o adaptador transforma a resposta do provedor nos campos que o painel pode renderizar:
type SpotifyNowPlaying = {
isConfigured: boolean;
isPlaying: boolean;
trackId: string | null;
trackName: string | null;
artistNames: string[];
albumName: string | null;
trackUrl: string | null;
progressMs: number;
durationMs: number;
fetchedAt: number;
};
Essa normalização trata três casos diferentes de “vazio” sem obrigar a interface a conhecer o formato da resposta do Spotify:
- HTTP
204significa que não há reprodução atual. - Um item que não é uma faixa vira o mesmo retrato vazio.
- A falta de credenciais produz
isConfigured: falseem vez de uma exceção no fluxo de estatísticas.
O intervalo de polling acompanha o retrato atual em vez de permanecer fixo:
function getPollIntervalMs(isPlaying: boolean) {
return isPlaying ? 2_500 : 15_000;
}
Quando o Spotify responde com 429, o adaptador lê Retry-After e usa 30 segundos como fallback. Se o access token for rejeitado, o cache em memória é invalidado para que a próxima tentativa possa renová-lo. Outras falhas de requisição publicam um retrato configurado vazio e continuam no intervalo mais lento.
O histórico da reprodução fica na memória. O módulo mantém no máximo 84 retratos, o suficiente para o painel mostrar uma faixa anterior sem gravar a atividade de escuta em um banco de dados. Uma reinicialização apaga o histórico.
Adaptador 2: derive as métricas do GitHub uma vez
A integração com o GitHub usa a estratégia oposta. server/stats/github.ts solicita o calendário de contribuições desde o início do ano com uma consulta GraphQL e deriva localmente todos os valores do painel:
A derivação usa strings de data ISO como chaves. Isso fornece um formato estável para comparar os limites do ano e do mês, enquanto a série de 30 dias é criada a partir da janela de datas local para que dias sem contribuições apareçam como zero.
O retrato resultante contém apenas agregados prontos para a interface:
type GitHubCommitStats = {
isConfigured: boolean;
username: string;
lastCommitDate: string | null;
commitsToday: number;
commitsLast30Days: number[];
commitsLast30DayLabels: string[];
commitsThisMonth: number;
commitsThisYear: number;
fetchedAt: number;
};
O painel usa “commits” como atalho para o calendário de contribuições do GitHub. Não é um git log local nem uma auditoria de todos os commits.
Armazene apenas o estado normalizado
Os dados do GitHub são lentos o suficiente para sobreviver a uma reinicialização. Depois de uma consulta bem-sucedida, o adaptador grava o retrato normalizado em github-cache.json, no diretório de dados da aplicação. Durante a inicialização, ele:
- Verifica se o arquivo existe.
- Faz o parse com o schema compartilhado do Valibot.
- Usa o arquivo somente enquanto ele tiver menos de 30 minutos.
- Agenda a próxima requisição para o fim da janela de validade restante.
O cache guarda o objeto normalizado, então o restante da aplicação não precisa interpretar novamente a resposta do provedor. O Valibot transforma um esquema antigo ou um arquivo malformado em cache miss, em vez de deixá-lo entrar no fluxo.
A política de erro separa “tentar mais tarde” de “mostrar um novo estado”. Quando recebe um limite, o adaptador lê X-RateLimit-Reset e preserva o último retrato válido até aquele horário. Se o header estiver ausente, espera 15 minutos. Outras falhas publicam um retrato configurado vazio e repetem a tentativa no intervalo mais longo. O painel não apresenta dados antigos como atuais, e um limite temporário não apaga o último valor válido.
Mantenha o painel alheio ao provedor
Os painéis do Solid consomem os retratos normalizados através do transporte de estatísticas. Eles não renovam credenciais, interpretam Retry-After, leem arquivos de cache nem sabem com que frequência um provedor deve ser consultado.
Cada adaptador passa a concentrar as decisões específicas de sua fonte:
| Preocupação | Adaptador do Spotify | Adaptador do GitHub |
|---|---|---|
| Autenticação | Refresh token trocado em memória | Token do servidor na requisição GraphQL |
| Normalização | Faixa atual ou estado vazio explícito | Dias de contribuição em métricas agregadas |
| Atualidade | Polling ativo/parado adaptativo | Polling fixo de 30 minutos |
| Limite | Retry-After |
X-RateLimit-Reset |
| Retenção | 84 retratos em memória | 84 retratos em memória e um retrato fresco em disco |
| Payload do navegador | Metadados da faixa e URL pública | Valores agregados de contribuição |
Os dois provedores seguem relógios e regras de falha diferentes, mas publicam retratos tipados e pequenos que a interface consegue renderizar diretamente.
O endpoint de histórico carrega os dois módulos por esse contrato. A rota SSE emite apenas as versões alteradas, e os painéis renderizam o estado sem repetir a lógica das APIs dos provedores.