Event System
The Event System provides a simple pub/sub mechanism for decoupling application logic. When something happens - a user registers, an order ships, a file uploads - you fire an event. Listeners react to it independently, keeping business logic out of your controllers and services.
How It Works
- Define an Event class that carries data about what happened
- Define one or more Listener classes that react to it
- Register the mapping in
routes/events.ts - Dispatch the event from anywhere in your code
routes/events.ts is loaded automatically during bootstrap - no manual wiring needed.
Creating Events
npx strux make:event UserRegisteredGenerated file (app/Events/UserRegistered.ts):
import { Event } from "struxjs";
export class UserRegistered extends Event {
constructor(
public readonly user: User
) {
super();
}
}Every event has a firedAt timestamp (ms since epoch) set automatically at instantiation.
Creating Listeners
npx strux make:listener SendWelcomeEmail --event UserRegisteredGenerated file (app/Listeners/SendWelcomeEmail.ts):
import { Listener } from "struxjs";
import { UserRegistered } from "../Events/UserRegistered.js";
export class SendWelcomeEmail extends Listener {
public async handle(event: UserRegistered): Promise<void> {
// TODO: implement listener logic
}
public async failed(event: UserRegistered, error: Error): Promise<void> {
// Optional: handle permanent failure (queued listeners only)
}
}Listener properties
| Property | Type | Default | Description |
|---|---|---|---|
shouldQueue | boolean | false | When true, listener runs in the background via the queue |
queue | string | "default" | Queue name to use when shouldQueue = true |
tries | number | 3 | Max retry attempts for queued listeners |
Registering Listeners
Open routes/events.ts and register your event-to-listener mappings:
import { EventDispatcher } from "struxjs";
import { UserRegistered } from "../app/Events/UserRegistered.js";
import { SendWelcomeEmail } from "../app/Listeners/SendWelcomeEmail.js";
import { LogUserActivity } from "../app/Listeners/LogUserActivity.js";
export default function (): void {
EventDispatcher.listen(UserRegistered, [
SendWelcomeEmail,
LogUserActivity,
]);
}EventDispatcher.listen() accepts an array - multiple listeners per event are fully supported and run in registration order.
Dispatching Events
Global event() helper
The recommended way to dispatch from anywhere in your code:
import { event } from "struxjs";
import { UserRegistered } from "../Events/UserRegistered.js";
await event(new UserRegistered(user));Event.dispatch() static method
await Event.dispatch(new UserRegistered(user));Instance dispatch() method
const e = new UserRegistered(user);
await e.dispatch();EventDispatcher.dispatch() directly
import { EventDispatcher } from "struxjs";
await EventDispatcher.dispatch(new UserRegistered(user));All four forms are equivalent. event() is the most concise.
dispatchNow() - fire and forget
Dispatches the event but suppresses all listener errors. Useful for side-effects that should never block the main request flow:
await EventDispatcher.dispatchNow(new UserLoggedIn(user));Listener Types
1. Class-based listeners (recommended)
The standard approach. Register the class constructor - StruxJS instantiates it per dispatch:
EventDispatcher.listen(UserRegistered, [SendWelcomeEmail]);2. Inline callback listeners
Register a plain async function instead of a class. Useful for quick one-off reactions:
EventDispatcher.on(UserRegistered, async (event) => {
console.log(`New user: ${event.user.email}`);
});EventDispatcher.on() is shorthand for EventDispatcher.listen() with a single callback.
3. Mixed listeners
You can mix class-based and callback listeners for the same event:
EventDispatcher.listen(UserRegistered, [
SendWelcomeEmail, // class
LogUserActivity, // class
async (event) => metrics.increment("users.registered"), // callback
]);Wildcard Listeners
Register a listener that fires for every event dispatched in the application:
// Using listen()
EventDispatcher.listen("*", [
async (event) => {
console.log(`Event fired: ${event.constructor.name} at ${event.firedAt}`);
},
]);
// Using on()
EventDispatcher.on("*", async (event) => {
auditLog.record(event.constructor.name, event);
});Wildcard listeners run after the specific listeners for the event.
Queued Listeners
Set shouldQueue = true on a listener to run it in the background via the job queue instead of synchronously. This is useful for slow operations - sending emails, generating PDFs, calling external APIs - that should not delay the HTTP response.
import { Listener } from "struxjs";
import { UserRegistered } from "../Events/UserRegistered.js";
export class SendWelcomeEmail extends Listener {
public shouldQueue = true;
public queue = "emails";
public tries = 3;
public async handle(event: UserRegistered): Promise<void> {
await mailer.send(event.user.email, "Welcome to the platform!");
}
public async failed(event: UserRegistered, error: Error): Promise<void> {
// Called when all retry attempts are exhausted
console.error(`Failed to send welcome email to ${event.user.email}:`, error.message);
}
}Generate a queued listener directly with the --queued flag:
npx strux make:listener SendWelcomeEmail --event UserRegistered --queuedQueued listeners require a running worker:
npx strux queue:work --queue emailsPractical Examples
User registration flow
// routes/events.ts
import { EventDispatcher } from "struxjs";
import { UserRegistered } from "../app/Events/UserRegistered.js";
import { SendWelcomeEmail } from "../app/Listeners/SendWelcomeEmail.js";
import { CreateUserProfile } from "../app/Listeners/CreateUserProfile.js";
import { NotifyAdmins } from "../app/Listeners/NotifyAdmins.js";
export default function (): void {
EventDispatcher.listen(UserRegistered, [
CreateUserProfile, // sync - must complete before response
SendWelcomeEmail, // queued - email in background
NotifyAdmins, // queued - non-critical notification
]);
}// app/Controllers/AuthController.ts
import { event } from "struxjs";
import { UserRegistered } from "../Events/UserRegistered.js";
export class AuthController {
public async register(request: Request, response: Response) {
const user = await User.create(request.validated());
await event(new UserRegistered(user));
return response.json({ user });
}
}Audit logging with wildcard
// routes/events.ts
EventDispatcher.on("*", async (e) => {
await AuditLog.create({
event: e.constructor.name,
fired_at: new Date(e.firedAt),
});
});Conditional dispatch
const order = await Order.create(data);
if (order.total > 1000) {
await event(new HighValueOrderPlaced(order));
} else {
await event(new OrderPlaced(order));
}Managing Listeners
Check if an event has listeners
const hasAny = EventDispatcher.hasListeners(UserRegistered);Get all registered listeners
const map = EventDispatcher.getRegistered();
// { UserRegistered: 2, OrderPlaced: 1, "*": 1 }Remove listeners for a specific event
EventDispatcher.forget(UserRegistered);Remove all listeners
EventDispatcher.flush();CLI Reference
| Command | Description |
|---|---|
npx strux make:event <name> | Create a new Event class in app/Events/ |
npx strux make:listener <name> | Create a new Listener class in app/Listeners/ |
npx strux make:listener <name> --event <Event> | Create a listener typed to a specific event |
npx strux make:listener <name> --queued | Create a queued listener with shouldQueue = true |