Event Patterns
Recurring ways to put events to work: broadcasting from a service to many consumers, reacting to one event in several components, fire-and-forget notifications, and latest-value or state broadcasting with replay and behavior subjects. Every pattern uses only fire, sub, and once on PostboyGenericMessage — see Events for the basics.
Broadcasting from a service
A data-producing service registers the event once and fires on incoming data. Consumers depend only on the message class and the bus — never on the producing service.
import {ConnectMessage, PostboyGenericMessage, PostboyService} from '@artstesh/postboy';
import {Subject} from 'rxjs';
export class CallStartedEvent extends PostboyGenericMessage {
static readonly ID = 'call.started';
constructor(public readonly caller: string) {
super();
}
}
export class CallWebSocketAdapter {
constructor(private postboy: PostboyService) {
this.postboy.exec(new ConnectMessage(CallStartedEvent, new Subject<CallStartedEvent>()));
}
onIncomingCall(caller: string): void {
this.postboy.fire(new CallStartedEvent(caller));
}
}
Consumers subscribe without importing the adapter:
postboy.sub(CallStartedEvent).subscribe((msg) => showBanner(msg.caller));
postboy.sub(CallStartedEvent).subscribe((msg) => logCall(msg.caller));
Subscribing in multiple components
One fire updates every part of the UI that cares. Each component subscribes independently; none knows about the others.
// header badge
postboy.sub(CallStartedEvent).subscribe(() => (this.hasActiveCall = true));
// customer profile widget
postboy.sub(CallStartedEvent).subscribe((msg) => (this.callerName = msg.caller));
// call status panel
postboy.sub(CallStartedEvent).subscribe(() => this.startTimer());
Adding a fourth subscriber is a new subscription and nothing else — no @Input chains, no shared state service.
Fire-and-forget
Use one event as the app-wide channel for transient notifications. The publisher does not wait and cannot know who reacted.
export class ToastNotificationEvent extends PostboyGenericMessage {
static readonly ID = 'ui.toast';
constructor(public readonly text: string, public readonly severity: 'info' | 'error') {
super();
}
}
// anywhere in the app
postboy.fire(new ToastNotificationEvent('Item added to cart', 'info'));
// the only place that renders toasts
postboy.sub(ToastNotificationEvent).subscribe((msg) => renderToast(msg.text, msg.severity));
A registered event with zero subscribers is discarded without an error, so optional events (telemetry, debug) need no guards.
Latest-value and state broadcasting
When late subscribers must catch up, register with recordReplay:
export class QuoteUpdatedEvent extends PostboyGenericMessage {
static readonly ID = 'trading.quote.updated';
constructor(public readonly symbol: string, public readonly price: number) {
super();
}
}
protected _up(): void {
this.recordReplay(QuoteUpdatedEvent); // new subscribers get the last quote
}
When the event models state, register with recordBehavior; the initial value is a message instance:
export class SessionStateEvent extends PostboyGenericMessage {
static readonly ID = 'app.session.state';
constructor(public readonly online: boolean) {
super();
}
}
protected _up(): void {
this.recordBehavior(SessionStateEvent, new SessionStateEvent(false));
}
Every subscriber — early or late — sees the current state immediately.
Do and don't
Do | Don't |
|---|
Register once in a registrator | Re-register the same type ad hoc (silent overwrite) |
Publish with fire | Call .next() on the observable returned by sub |
Use recordReplay/recordBehavior for latest-value or state | Hand-roll "current value" caches next to the bus |
Unsubscribe on component teardown | Leak subscriptions in long-lived apps |
Keep events result-free | Smuggle responses into events; use callbacks instead |
07 September 2026