@roostjs/events Guides
Task-oriented instructions for defining events, registering listeners, using subscribers, deferring listeners to the queue, and testing dispatches.
How to define an event
Extend Event with a typed constructor. Store all event data as public properties.
import { Event } from '@roostjs/events';
export class UserRegistered extends Event {
constructor(
public readonly userId: string,
public readonly email: string,
public readonly plan: 'free' | 'pro'
) {
super();
}
}How to define a listener
Implement the Listener<T> interface. The handle() method receives the
dispatched event instance.
import type { Listener } from '@roostjs/events';
import type { UserRegistered } from '../events/UserRegistered';
export class SendWelcomeEmailListener implements Listener<UserRegistered> {
async handle(event: UserRegistered): Promise<void> {
await sendEmail({
to: event.email,
subject: 'Welcome!',
template: 'welcome',
data: { userId: event.userId, plan: event.plan },
});
}
}How to register events and listeners
Extend EventServiceProvider and override listen() to map event classes to
listener classes.
import { EventServiceProvider } from '@roostjs/events';
import { UserRegistered } from '../events/UserRegistered';
import { SendWelcomeEmailListener } from '../listeners/SendWelcomeEmailListener';
import { CreateTrialSubscriptionListener } from '../listeners/CreateTrialSubscriptionListener';
export class AppEventServiceProvider extends EventServiceProvider {
protected listen() {
return new Map([
[UserRegistered, [SendWelcomeEmailListener, CreateTrialSubscriptionListener]],
]);
}
}Register the provider in your app:
import { createApp } from '@roostjs/core';
import { AppEventServiceProvider } from './providers/AppEventServiceProvider';
export const app = createApp({
providers: [new AppEventServiceProvider()],
});How to dispatch an event
Call the static dispatch() method on the event class.
import { UserRegistered } from '../events/UserRegistered';
// After creating a user record
await UserRegistered.dispatch(new UserRegistered(user.id, user.email, 'free'));All registered listeners for UserRegistered execute in parallel before
dispatch() resolves.
How to use a subscriber for multiple events
Use Subscriber to group related event handlers in one class instead of
defining a separate listener class per event.
import { Subscriber } from '@roostjs/events';
import type { EventClass, Event } from '@roostjs/events';
import { UserRegistered } from '../events/UserRegistered';
import { OrderPlaced } from '../events/OrderPlaced';
export class NotificationSubscriber extends Subscriber {
subscribe(): Map<EventClass<Event>, string> {
return new Map([
[UserRegistered, 'onUserRegistered'],
[OrderPlaced, 'onOrderPlaced'],
]);
}
async onUserRegistered(event: UserRegistered): Promise<void> {
await notifyAdmins(`New user: ${event.email}`);
}
async onOrderPlaced(event: OrderPlaced): Promise<void> {
await notifyFulfillment(event.orderId);
}
}Register the subscriber in the service provider:
import { EventServiceProvider } from '@roostjs/events';
import { NotificationSubscriber } from '../subscribers/NotificationSubscriber';
export class AppEventServiceProvider extends EventServiceProvider {
protected subscribers() {
return [NotificationSubscriber];
}
}How to defer a listener to the queue
Add the ShouldQueue interface to a listener that also extends Job<TEvent>
from @roostjs/queue. The event dispatcher will dispatch the listener as a job
rather than calling handle() inline.
import { Job } from '@roostjs/queue';
import type { Listener, ShouldQueue } from '@roostjs/events';
import type { OrderPlaced } from '../events/OrderPlaced';
export class GenerateInvoiceListener
extends Job<OrderPlaced>
implements Listener<OrderPlaced>, ShouldQueue
{
readonly shouldQueue = true as const;
async handle(event: OrderPlaced): Promise<void> {
await generatePdfInvoice(event.orderId);
await emailInvoice(event.customerEmail);
}
}Requires @roostjs/queue to be installed and configured.
How to test event dispatches
Call Event.fake() to intercept dispatches without running listeners.
Use assertDispatched() and assertNotDispatched() to make assertions.
import { describe, it, afterEach } from 'bun:test';
import { UserRegistered } from '../../src/events/UserRegistered';
import { registerUser } from '../../src/handlers/auth';
describe('UserRegistered', () => {
afterEach(() => UserRegistered.restore());
it('is dispatched after successful registration', async () => {
UserRegistered.fake();
await registerUser({ email: 'alice@example.com', plan: 'free' });
UserRegistered.assertDispatched();
});
it('is dispatched with correct email', async () => {
UserRegistered.fake();
await registerUser({ email: 'bob@example.com', plan: 'pro' });
UserRegistered.assertDispatched((event) => event.email === 'bob@example.com');
});
it('is not dispatched when registration fails', async () => {
UserRegistered.fake();
await registerUser({ email: 'invalid', plan: 'free' }).catch(() => {});
UserRegistered.assertNotDispatched();
});
});