AVISO: Este documento é especulativo. O Recorder ainda não existe no código no momento em que foi escrito, e a
Engineainda não tem os métodosonStreamStarted/onStreamEndedmencionados aqui. Registra o desenho combinado pra não perder o raciocínio. Quando as peças forem construídas, revisitar este documento e ajustar contra a realidade concreta.
Quando o Monitor detectar que um canal entrou ao vivo, o daemon precisa:
A questão era: quem faz cada passo? Uma leitura ingênua colocaria o Monitor fazendo os três (detectar + persistir + disparar Recorder), o que reverteria todo o desacoplamento construído (Emitter injetado, Monitor puro emissor, Engine como orquestrador).
Esta nota registra a divisão que preserva o desacoplamento sem virar over-engineering.
1. Monitor.checkOnLiveChannels()
├→ Detecta transição de canal offline → live
└→ Emite ChannelLiveEvent (com metadados completos do stream)
2. Composition root (main.ts) — wire feito uma vez:
monitor.on(async event => {
if (event.type === 'live') await engine.onStreamStarted(event)
else if (event.type === 'offline') await engine.onStreamEnded(event)
})
3. Engine.onStreamStarted(event) — chamado pelo listener:
├→ streamRepository.create({ streamId, channelName, startedAt, title, category })
│ (INVARIANTE — chamada síncrona direta, não é event handler)
└→ recorder.start(streamRow)
(INVARIANTE — chamada síncrona direta, não é event handler)
4. Recorder.start(streamRow) — assume o processo longo:
├→ recordingRepository.create({ streamId, status: 'starting', ... })
├→ Spawna streamlink como child process supervisionado
├→ Escuta exit code / erros
├→ Atualiza recording row conforme progresso (status, endedAt, bytes)
└→ Emite RecorderEvent (started/finished/failed) via Emitter próprio
pra reações externas (rewrap, webhook, retention, etc.)liveoffline (finalizar, ou deixar o Recorder detectar pelo próprio
streamlink caindo — a decidir quando implementar)monitor.on(engine.onStreamStarted) no main.ts; Engine não recebe
Monitor injetado, não chama métodos dele)Nenhuma peça tem referência direta a mais componentes do que precisa. Ampliar
o comportamento (adicionar webhook, métrica, log estruturado que reagem a live)
é sempre uma nova linha no main.ts, nunca mudança em Monitor/Engine/Recorder.
Monitor já bate na Twitch e recebe todos os metadados do stream (streamId, title, game/category, createdAt). Não descartar essa informação — enriquece o evento pra Engine não precisar re-request:
Custo zero (Monitor já tem os campos), Engine ganha payload completo pra persistir sem viagem extra à Twitch.
Uma leitura tentadora seria fazer Monitor orquestrar tudo:
Problemas:
streams (concern que não é dele)Se você se pegar querendo Monitor "chamando alguém direto", pare e volte pro fluxo com Engine no meio. É o que preserva a arquitetura.
A persistência da stream row e a chamada ao Recorder são invariantes (se
falharem, o sistema fica inconsistente — arquivo eventual sem rastro no DB, ou
gravação nunca iniciada). Por isso essas duas ações estão dentro de
Engine.onStreamStarted como chamadas síncronas diretas, NÃO como novos
event handlers.
Reações externas (webhook Discord "canal X ficou live", métrica
monitor.transitions_total, log estruturado) SIM entram como listeners
adicionais no main.ts:
Ordem entre listeners não é garantida — invariante ganha caminho síncrono
próprio, reações se acumulam no on(). Ver
speculation-early-recorder-invariants-vs-reactions.md.
offlineMais aberto ainda — a decidir quando o Recorder existir. Duas opções:
offline e chama recorder.stop(username) explicitamentenotes/recording-twitch-streams.md)Provavelmente os dois — Engine avisa o Recorder da transição observada, e Recorder também tem seu próprio senso de "streamlink morreu, agora eu penso o que fazer". Redundância intencional pra robustez.
ChannelLiveEvent já carrega username + title + startedAt; streamId
e category ainda são placeholders (Monitor não passa stream.id no evento)onStreamStarted e onStreamEnded implementados; disparam
streamRepository.createStream + recorder.recordTwitchStream / recorder.stopStream// Antes (só o mínimo):
{ type: 'live'; username: string; startedAt: Date }
// Depois (todos os metadados que já temos):
{
type: 'live'
username: string
startedAt: Date
streamId: string
title: string
category?: string
}// ❌ NÃO fazer isso
Monitor.checkOnLiveChannels():
├→ detecta live
├→ streamRepo.create(...) // Monitor virou persistidor
├→ recorder.start(...) // Monitor virou orquestrador
└→ terminamonitor.on(async e => await engine.onStreamStarted(e)) // invariante
monitor.on(async e => await discord.notifyStreamStart(e)) // reação
monitor.on(async e => metrics.increment('monitor.live')) // reação