@numaris-microfront/event-bus
v0.2.1
Published
Event Bus pub/sub con retencion del ultimo valor por tipo de evento, agnostico de framework y de dominio.
Maintainers
Readme
@numaris-microfront/event-bus
Event Bus pub/sub agnostico de framework y de dominio: no sabe que es React, no sabe que
es un trackable ni un comando. Su unico trabajo es entregar eventos entre partes de una
aplicacion que no se importan entre si — el caso tipico es una arquitectura de
micro-frontends, donde el host y cada remoto son bundles separados y nunca deberian
importarse en el codigo.
Si estas construyendo un widget para la plataforma Numaris, probablemente no instales este
paquete directo: usa @numaris-microfront/contracts, que expone numarisBus — la misma
instancia de este bus, ya tipada con los eventos de dominio de la plataforma (trackable.selected,
trackable.list.updated, etc.). Instala este paquete directo solo si necesitas el bus crudo,
sin esos tipos.
Por que "retiene el ultimo valor"
La diferencia con un EventEmitter comun: cuando alguien se suscribe a un tipo de evento,
recibe inmediatamente el ultimo valor emitido de ese tipo, si existe — no solo los que se
emitan a partir de ahora. Eso resuelve un problema concreto de micro-frontends: si el widget A
emite trackable.selected y el widget B se monta un segundo despues, B igual tiene que saber
cual esta seleccionado. Sin retencion, el orden de montaje de los widgets cambiaria lo que cada
uno ve — un bug dificil de reproducir porque depende de timing de red, no de codigo.
Instalacion
npm install @numaris-microfront/event-busSin dependencias de runtime. type: module — es ESM puro.
Uso basico
import { createEventBus } from '@numaris-microfront/event-bus';
interface MyEvents {
'user.selected': { userId: string | null };
'cart.updated': { itemCount: number };
}
const bus = createEventBus<MyEvents>({ label: 'my-app' });
// Suscribirse. Por default, si ya hay un valor emitido de este tipo, se entrega de inmediato.
const unsubscribe = bus.on('user.selected', (payload, meta) => {
console.log(payload.userId, meta.retained); // meta.retained: true si vino por replay
});
// Emitir. Todo suscriptor activo (y futuro, mientras dure el valor) lo recibe.
bus.emit('user.selected', { userId: 'u-123' });
// Cortar la suscripcion.
unsubscribe();API
createEventBus<TMap>(options?)
Crea una instancia. TMap es un Record<string, unknown> — el mapa de tipo de evento a forma
del payload. Cada instancia es independiente: dos createEventBus() no comparten estado.
interface EventBusOptions {
/** Se invoca si un handler lanza una excepcion. El bus nunca la deja escapar
* hacia quien emitio: un suscriptor que falla no puede tumbar a los demas. */
onHandlerError?: (error: unknown, meta: EventMeta) => void;
/** Etiqueta para diagnostico, aparece en `instanceId`. */
label?: string;
}Metodos de la instancia
| Metodo | Que hace |
|---|---|
| emit(type, payload) | Publica un valor. Se guarda como "ultimo valor" del tipo y se entrega a los suscriptores activos. |
| on(type, handler, options?) | Se suscribe. Devuelve una funcion para desuscribirse. Por default entrega el ultimo valor retenido de inmediato (options.replayLast, default true). |
| once(type, handler, options?) | Como on, pero se desuscribe solo despues de la primera entrega. |
| onAny(handler) | Observa TODOS los eventos, sin replay. Pensado para diagnostico/logging, no para logica de negocio. |
| getLast(type) | Lee el ultimo valor sin suscribirse. Devuelve { payload, emittedAt } o undefined. |
| snapshot() | Devuelve todos los valores retenidos, de todos los tipos. Para paneles de diagnostico. |
| clearRetained(type?) | Olvida el ultimo valor de un tipo (o de todos, sin argumento). No cancela suscripciones. Util al cerrar sesion o cambiar de tenant, para que el proximo widget que monte no reciba por replay datos de un contexto anterior. |
| listenerCount(type) | Cantidad de suscriptores activos de un tipo. Para tests/diagnostico. |
| reset() | Borra suscripciones y valores retenidos. Para tests. |
| instanceId | Identifica la instancia en memoria. Si dos partes de tu app ven un instanceId distinto para lo que debia ser el mismo bus compartido, tu bundler esta duplicando el paquete en vez de compartirlo (ver seccion siguiente). |
EventMeta (segundo argumento de cada handler)
interface EventMeta {
type: string;
emittedAt: number; // epoch ms de la emision original, no de la entrega
retained: boolean; // true si llego por replay al suscribirse, no por una emision en vivo
}El punto critico si lo usas en Module Federation (o cualquier arquitectura multi-bundle)
Este bus solo funciona como mecanismo de comunicacion entre widgets si todos comparten la misma instancia en memoria. Si tu host y cada remoto cargan su propia copia del paquete (porque no esta declarado como dependencia compartida singleton en tu configuracion de Module Federation, Webpack o Vite), vas a tener N instancias distintas que nunca se hablan entre si — y el sintoma no es un error: la app carga, cada widget renderiza, y simplemente nunca reciben los eventos del otro. Nada rompe visiblemente.
Para detectarlo en desarrollo, compara instanceId entre dos partes que deberian compartir
bus: si difiere, hay una copia duplicada del paquete en el bundle.
Si usas Vite + @originjs/vite-plugin-federation (u otro plugin de Module Federation),
declara este paquete como singleton: true en el bloque shared de cada remoto y del host.
