Fox Events Fox Events

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

  • 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 com Fox.channel<T>(name).
  • Publisher — Quem publica um evento no canal, chamando emit(payload) ou emitAsync(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) ou once(). Sempre que alguém publicar nesse canal, o listener recebe o payload (no callback de on ou na Promise de once). 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:

ts
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-1

Um 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. Retorna void. Use para eventos de UI, navegação, notificações em memória.
  • emitAsync(payload) — Retorna uma Promise que 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ção off() 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 uma Promise que 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).

ts
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

ononce
Quantos eventosTodos (enquanto estiver inscrito)Só o próximo
RetornoFunção off() para desinscreverPromise<payload>
DesinscriçãoManual (off())Automática após o primeiro evento
Uso típicoUI reativa, logs, sincronização“Esperar uma resposta”, confirmação única

Exemplo com on: inscrever uma vez e reagir a cada clique.

ts
const off = channel.on((payload) => setCount((c) => c + 1));
// mais tarde: off() para parar de receber

Exemplo com once: esperar o próximo evento (ex.: usuário confirmar).

ts
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):

ts
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 do useEffect no React).
ts
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 emit ou emitAsync.
  • Listener: componente React (em useEffect), módulo de analytics, logger, store derivado, modal que “espera uma resposta”, etc. Qualquer código que chame on ou once no 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):

ts
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):

ts
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):

ts
const ch = Fox.channel<{ confirmed: boolean }>("modal:confirm");
const result = await ch.once();
if (result.confirmed) { /* ... */ }

once com timeout:

ts
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 que emit e os callbacks de on / once sejam tipados; menos bugs e melhor autocomplete.
  • Desinscreva quando não precisar mais — Em React, chame off() no return do useEffect; 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.