Skip to main content

Events

Say a pet gets adopted, and three unrelated things need to happen: email the new owner, update a search index, and let every other running instance of your service know that pet's status changed. PetService shouldn't have to know about all three. Hard-coding them in means editing PetService again for the next thing that needs to react, and coupling a service that just records adoptions to email and search infrastructure it has nothing to do with.

The event system exists to break that coupling: any class can announce that something happened, once, without knowing or caring who's listening, and any other class can listen for it, without knowing or caring who announced it. Concretely, that means letting a class listen for named events published on a Redis pub/sub channel. Redis is what makes this cross-instance rather than just cross-class: publish an event from one horizontally-scaled instance of your service, and every listener on every instance hears it, not just the one that happened to publish it, the way a plain in-process EventEmitter would be limited to.

Not the same as Telemetry

This is for real-time, in-process reactions to something happening, across service instances. Permanently recording that something happened, for an external analytics or observability service, is a separate, unrelated system, see Telemetry, despite both using an Event type imported from the same place.

Listening with @OnEvent

Decorate a method with @OnEvent(type?) to have it called whenever a matching event arrives:

import {EventDecorators} from '@rapidrest/service-core';
import type {Event} from '@rapidrest/core';
const {EventListener, OnEvent} = EventDecorators;

@EventListener()
export class PetEventHandler {
@OnEvent('pet.created')
onPetCreated(evt: Event) {
// ...
}

@OnEvent(['pet.updated', 'pet.deleted'])
onPetChanged(evt: Event) {
// ...
}

@OnEvent()
onAnyEvent(evt: Event) {
// called for every event received, regardless of type
}
}
DecoratorWhat it does
@EventListener()Class decorator. Marks a class (which must have a no-argument constructor) to be automatically instantiated and registered as an event listener at startup.
@OnEvent(type?)Method decorator. type can be a single event type, an array of types, or omitted entirely to receive every event. Types are matched as case-insensitive regular expressions, not exact strings.

As with routes and models, there's no manual registration step for a class decorated with @EventListener(). Export it anywhere under src/ and it's picked up automatically (see Auto-Discovery).

Configuring the event bus

Events flow over a Redis connection named events, and EventListenerManager subscribes to whichever channels you list under events:channels:

conf.defaults({
datastores: {
events: {type: 'redis', host: 'localhost', port: 6379},
},
events: {
channels: ['pets'],
},
});

The event system only starts if a datastore named events is configured. If it isn't, @OnEvent handlers are simply never registered, with no error.

Publishing an event

There isn't a dedicated "publish an event" helper. An event is just a JSON message with a type field, published to one of the configured channels over the same events Redis connection:

import {DatabaseDecorators} from '@rapidrest/service-core';
import type {RedisClientType} from 'redis';
const {Redis} = DatabaseDecorators;

export class PetService {
@Redis('events')
private redis?: RedisClientType;

async create(pet: Pet) {
const created = await this.repo.create(pet);
void this.redis?.publish('pets', JSON.stringify({type: 'pet.created', data: created}));
return created;
}
}

Any listener subscribed to the pets channel with a matching @OnEvent type receives it. The channel is just the transport; dispatch to handlers is based on the event's type field, not which channel it arrived on.