Skip to content

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

  1. Define an Event class that carries data about what happened
  2. Define one or more Listener classes that react to it
  3. Register the mapping in routes/events.ts
  4. Dispatch the event from anywhere in your code

routes/events.ts is loaded automatically during bootstrap - no manual wiring needed.


Creating Events

bash
npx strux make:event UserRegistered

Generated file (app/Events/UserRegistered.ts):

typescript
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

bash
npx strux make:listener SendWelcomeEmail --event UserRegistered

Generated file (app/Listeners/SendWelcomeEmail.ts):

typescript
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

PropertyTypeDefaultDescription
shouldQueuebooleanfalseWhen true, listener runs in the background via the queue
queuestring"default"Queue name to use when shouldQueue = true
triesnumber3Max retry attempts for queued listeners

Registering Listeners

Open routes/events.ts and register your event-to-listener mappings:

typescript
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:

typescript
import { event } from "struxjs";
import { UserRegistered } from "../Events/UserRegistered.js";

await event(new UserRegistered(user));

Event.dispatch() static method

typescript
await Event.dispatch(new UserRegistered(user));

Instance dispatch() method

typescript
const e = new UserRegistered(user);
await e.dispatch();

EventDispatcher.dispatch() directly

typescript
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:

typescript
await EventDispatcher.dispatchNow(new UserLoggedIn(user));

Listener Types

The standard approach. Register the class constructor - StruxJS instantiates it per dispatch:

typescript
EventDispatcher.listen(UserRegistered, [SendWelcomeEmail]);

2. Inline callback listeners

Register a plain async function instead of a class. Useful for quick one-off reactions:

typescript
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:

typescript
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:

typescript
// 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.

typescript
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:

bash
npx strux make:listener SendWelcomeEmail --event UserRegistered --queued

Queued listeners require a running worker:

bash
npx strux queue:work --queue emails

Practical Examples

User registration flow

typescript
// 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
    ]);
}
typescript
// 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

typescript
// routes/events.ts
EventDispatcher.on("*", async (e) => {
    await AuditLog.create({
        event: e.constructor.name,
        fired_at: new Date(e.firedAt),
    });
});

Conditional dispatch

typescript
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

typescript
const hasAny = EventDispatcher.hasListeners(UserRegistered);

Get all registered listeners

typescript
const map = EventDispatcher.getRegistered();
// { UserRegistered: 2, OrderPlaced: 1, "*": 1 }

Remove listeners for a specific event

typescript
EventDispatcher.forget(UserRegistered);

Remove all listeners

typescript
EventDispatcher.flush();

CLI Reference

CommandDescription
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> --queuedCreate a queued listener with shouldQueue = true

Released under the MIT License.