Publishers e listeners
O Fox Events é um sistema de eventos baseado em canais: quem publica (publisher) emite um payload; quem escuta (listener) recebe esse payload. Esta página explica o modelo mental e o uso de publishers e listeners na prática.
---
Índice
- 1. O modelo: canal, publisher, listener
- 2. Fluxo em uma frase
- 3. Publisher
- 4. Listener
- 5. Um publisher, vários listeners
- 6. on vs once
- 7. once com timeout
- 8. Desinscrever (off)
- 9. Quem pode ser publisher ou listener
- 10. O que o Fox não faz
- 11. Exemplos rápidos
- 12. Boas práticas
---
1. O modelo: canal, publisher, listener
- Canal — Um “assunto” com nome (ex.:
"user:login") e tipo de payload (ex.:{ userId: string }). O canal é o ponto de encontro: só quem conhece o nome do canal pode publicar ou escutar nele. Você obtém um canal comFox.channel<T>(name).
- Publisher — Quem publica um evento no canal, chamando
emit(payload)ouemitAsync(payload). O publisher não sabe quantos listeners existem nem quem são; só envia um dado (o payload) para o canal.
- Listener — Quem se inscreve no canal com
on(callback)ouonce(). Sempre que alguém publicar nesse canal, o listener recebe o payload (no callback deonou na Promise deonce). Pode haver zero, um ou muitos listeners no mesmo canal.
O Fox Events não guarda “quem publicou”: só garante que todo listener inscrito no canal receba o payload. O payload é um valor tipado (objeto, string, etc.); o canal define o tipo.
---
2. Fluxo em uma frase
Publisher chama channel.emit(payload) → o Fox entrega o mesmo payload a todos os listeners desse canal (quem está em on recebe no callback; quem está em once recebe na Promise e é desinscrito logo em seguida).
Em código:
const channel = Fox.channel<{ userId: string }>("user:login");
channel.on((payload) => console.log("Listener A:", payload.userId));
channel.on((payload) => console.log("Listener B:", payload.userId));
channel.emit({ userId: "u-1" });
// Listener A: u-1
// Listener B: u-1Um emit, dois listeners: ambos recebem o mesmo evento.
---
3. Publisher
Publisher é qualquer código que chama emit ou emitAsync em um canal.
emit(payload)— Dispara na hora, de forma síncrona. Os listeners são chamados imediatamente; o publisher não espera. Retornavoid. Use para eventos de UI, navegação, notificações em memória.
emitAsync(payload)— Retorna umaPromiseque resolve depois que todos os listeners (e qualquer middleware, ex.: persistência) forem executados. Use quando o evento passar por middleware (ex.: IndexedDB, bridge para React Native) e você precisar esperar o processamento antes de seguir (ex.: “salvar e depois redirecionar”).
O publisher não precisa saber quem está ouvindo; pode ser um botão, um serviço, uma lib externa ou outro módulo do app. O importante é usar o mesmo canal (mesmo nome) que os listeners.
---
4. Listener
Listener é qualquer código que se inscreve no canal com on ou once.
on(callback)— Inscreve um callback que será chamado toda vez que alguém publicar no canal. Retorna uma funçãooff()para desinscrever. Use quando quiser reagir a todo evento (ex.: atualizar UI a cada clique, sincronizar estado).
once()— Inscreve para apenas o próximo evento. Retorna umaPromiseque resolve com o payload desse evento; depois do primeiro evento, o listener é removido automaticamente. Use quando quiser “esperar a primeira ocorrência” (ex.: primeira resposta do servidor, primeiro clique em “Confirmar”).
Vários listeners podem estar no mesmo canal; todos recebem o mesmo payload em cada emit. A ordem de chamada dos listeners é a ordem em que foram inscritos.
---
5. Um publisher, vários listeners
É comum ter um publisher (ex.: um botão ou um serviço) e vários listeners (ex.: atualizar header, sidebar, log e analytics).
const loginChannel = Fox.channel<{ userId: string }>("user:login");
loginChannel.on((p) => updateHeader(p.userId));
loginChannel.on((p) => updateSidebar(p.userId));
loginChannel.on((p) => analytics.track("login", p));
document.getElementById("login-btn")?.addEventListener("click", () => {
loginChannel.emit({ userId: "u-42" });
});Um único emit dispara os três listeners. O publisher não acopla com nenhum deles; cada listener cuida da sua parte.
---
6. on vs once
| on | once | |
|---|---|---|
| Quantos eventos | Todos (enquanto estiver inscrito) | Só o próximo |
| Retorno | Função off() para desinscrever | Promise<payload> |
| Desinscrição | Manual (off()) | Automática após o primeiro evento |
| Uso típico | UI reativa, logs, sincronização | “Esperar uma resposta”, confirmação única |
Exemplo com on: inscrever uma vez e reagir a cada clique.
const off = channel.on((payload) => setCount((c) => c + 1));
// mais tarde: off() para parar de receberExemplo com once: esperar o próximo evento (ex.: usuário confirmar).
const payload = await channel.once();
console.log("Usuário confirmou:", payload);---
7. once com timeout
Se nenhum evento acontecer dentro de um tempo limite, once() pode rejeitar a Promise em vez de ficar esperando para sempre. Use a opção timeout (em milissegundos):
try {
const payload = await channel.once({ timeout: 5000 });
console.log("Recebido em até 5s:", payload);
} catch (err) {
console.error("Nenhum evento em 5s", err);
// err.message: "once: timeout after 5000ms"
}Útil para “esperar resposta do servidor em X segundos” ou “esperar clique de confirmação em X segundos”; se passar o tempo, você trata o erro (ex.: mostrar mensagem, voltar ao estado anterior).
---
8. Desinscrever (off)
- on:
on(callback)retorna uma função. Chame essa função para parar de receber eventos (ex.: no cleanup douseEffectno React).
const off = channel.on((payload) => { /* ... */ });
// quando não precisar mais:
off();- once: não precisa desinscrever; após o primeiro evento a inscrição é removida automaticamente. Se você usar
once({ timeout })e der timeout, a inscrição também é removida.
Em componentes React, inscreva em useEffect e chame off() no return para evitar memory leaks e atualizações em componente desmontado.
---
9. Quem pode ser publisher ou listener
- Publisher: handler de clique, serviço de API, worker, outra lib, store global, bridge React Native, etc. Qualquer código que tenha acesso ao canal e chame
emitouemitAsync.
- Listener: componente React (em
useEffect), módulo de analytics, logger, store derivado, modal que “espera uma resposta”, etc. Qualquer código que chameonouonceno canal.
Publisher e listener podem estar em arquivos, camadas ou bundles diferentes; o vínculo é só o nome do canal e o tipo do payload. Isso facilita desacoplamento e testes (você pode emitir de um teste e afirmar que o listener foi chamado).
---
10. O que o Fox não faz
- Não armazena “quem publicou” — O payload é entregue como está; não há metadado de “origem” do evento. Se precisar, inclua um campo no próprio payload (ex.:
{ userId, emittedBy: "header" }).
- Não garante ordem entre canais — Dentro de um canal, os listeners são chamados na ordem de inscrição. Entre canais diferentes, não há ordem definida.
- Não persiste eventos por padrão — Cada emit é entregue na hora aos listeners atuais. Para persistência (ex.: IndexedDB), use o pacote opcional e middleware; veja Storage.
- Não faz fila de retry — Se um listener lançar erro, o Fox não repete o evento. Trate erros dentro do listener ou use um padrão de dead-letter se precisar.
---
11. Exemplos rápidos
Um listener, um publisher (botão):
const ch = Fox.channel<{ value: string }>("app:input");
ch.on((p) => console.log(p.value));
document.querySelector("button")?.addEventListener("click", () => {
ch.emit({ value: "clicked" });
});Vários listeners, um publisher (serviço):
const ch = Fox.channel<{ id: string }>("api:user-loaded");
ch.on((p) => setUser(p));
ch.on((p) => analytics.identify(p.id));
getUser().then((user) => ch.emit(user));Listener que espera só o próximo evento (once):
const ch = Fox.channel<{ confirmed: boolean }>("modal:confirm");
const result = await ch.once();
if (result.confirmed) { /* ... */ }once com timeout:
const ch = Fox.channel<{ token: string }>("auth:token");
try {
const { token } = await ch.once({ timeout: 10000 });
saveToken(token);
} catch {
showError("Login expirou. Tente de novo.");
}---
12. Boas práticas
- Nomeie canais de forma clara — Use convenções como
domínio:ação(ex.:user:login,cart:item-added) para facilitar busca e evitar colisões.
- Tipagem do payload — Use
Fox.channel<SeuTipo>(name)para queemite os callbacks deon/oncesejam tipados; menos bugs e melhor autocomplete.
- Desinscreva quando não precisar mais — Em React, chame
off()no return douseEffect; em outros contextos, chame quando o listener não fizer mais sentido (ex.: modal fechado).
- Preferir once quando “uma vez basta” — Confirmações, “esperar primeira resposta”, etc. Evita esquecer de chamar
off()e deixa a intenção clara.
- Timeout em once para fluxos com limite de tempo — Evita Promises penduradas quando o evento pode nunca acontecer (rede, usuário abandonou a tela).
Para a API completa (trail, middleware, escopos), veja Começando e os outros módulos da documentação.