Skip to main content

EventBus usage

The eridu-tech/event-bus component provides a way for dispatching and listening to events independent of underlying technology.

Initial configuration​

To begin using the EventBus class, you'll need to create and configure an instance:

./samples/event-bus-initial-config.ts
import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter";
import type { IEventBus } from "eridu-tech/event-bus/contracts";
import { EventBus } from "eridu-tech/event-bus";

export const eventBus: IEventBus = new EventBus({
// You can choose the adapter to use
adapter: new MemoryEventBusAdapter(),
});
info

Here is a complete list of settings for the EventBus class.

Event handling basics​

Registering Listeners and Dispatching Events​

Event listeners can be added to respond to specific events:

./samples/event-bus-listeners.ts
import { eventBus } from "./event-bus-initial-config.js";

await eventBus.addListener("add", (event) => {
console.log(event);
});

await eventBus.dispatch("add", {
a: 5,
b: 5,
});

Listener management​

To properly remove a listener, you must use a named function:

./samples/event-bus-listener-management.ts
import { eventBus } from "./event-bus-initial-config.js";
import type { BaseEvent } from "eridu-tech/event-bus/contracts";

const listener = (event: BaseEvent) => {
console.log(event);
};

await eventBus.addListener("add", listener);

await eventBus.removeListener("add", listener);

// The listener is removed before dispatch and won't be triggered.
await eventBus.dispatch("add", {
a: 5,
b: 5,
});

Patterns​

Compile time type safety​

An event map can be used to strictly type the events:

./samples/event-bus-type-safety.ts
import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter";
import { EventBus } from "eridu-tech/event-bus";

type AddEvent = {
a: number;
b: number;
};

type EventMap = {
add: AddEvent;
};

const eventBus = new EventBus<EventMap>({
adapter: new MemoryEventBusAdapter(),
});

// A typescript error will show up because the event name doesnt exist.
await eventBus.dispatch("addd", {
a: 2,
b: 2,
});

// A typescript error will show up because the event fields doesnt match
await eventBus.dispatch("add", {
nbr1: 1,
nbr2: 2,
});

// A typescript error will show up because the event name doesnt exist.
await eventBus.addListener("addd", (event) => {
console.log(event);
});

Runtime type safety​

You can validate event data against standard-schema-compliant schemas by providing the eventMapSchema setting on the EventBus. This works with any library that implements the StandardSchemaV1 specification, such as Zod, ArkType and Valibot.

When a schema map is provided, event data is validated:

  • On dispatch — event data is validated against the schema for the event name before it is dispatched.
  • On listener delivery — when shouldValidateListeners is true (the default), event data is validated before it is delivered to listeners. This ensures listeners only receive data that conforms to the schema.

If no schema is defined for a particular event name, that event is passed through without validation. If validation fails, a ValidationError is thrown.

./samples/event-bus-runtime-validation.ts
import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter";
import { EventBus } from "eridu-tech/event-bus";
import { z } from "zod";

type UserCreatedEvent = {
userId: string;
name: string;
};
// The value type stays permissive so schema validation is what enforces the
// full shape at runtime (compile-time safety is shown in event_bus_type_safety).
type EventMap = {
"user.created": UserCreatedEvent;
};

const eventBus = new EventBus<EventMap>({
adapter: new MemoryEventBusAdapter(),
eventMapSchema: {
"user.created": z.object({
userId: z.string(),
name: z.string(),
}),
},
});

await eventBus.dispatch("user.created", {
userId: "123",
name: "John",
});

// Throws a ValidationError because userId is missing
await eventBus.dispatch("user.created", {
name: "Jane",
});

Disabling listener validation​

If you only want to validate event data on dispatch and skip validation when delivering to listeners, set shouldValidateListeners to false:

./samples/event-bus-disable-listener-validation.ts
import { EventBus } from "eridu-tech/event-bus";
import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter";
import { z } from "zod";

type UserCreatedEvent = {
userId: string;
name: string;
};
type EventMap = {
"user.created": UserCreatedEvent;
};

const eventBus = new EventBus<EventMap>({
adapter: new MemoryEventBusAdapter(),
eventMapSchema: {
"user.created": z.object({
userId: z.string(),
name: z.string(),
}),
},
shouldValidateListeners: false,
});

Subscribe method​

The subscription pattern provides automatic cleanup through an unsubscribe function:

./samples/event-bus-subscribe.ts
import { eventBus } from "./event-bus-initial-config.js";

const unsubscribe = await eventBus.subscribe("add", (event) => {
console.log(event);
});
await eventBus.dispatch("add", {
a: 20,
b: 5,
});
await unsubscribe();

One-Time event handling​

For listeners that should only trigger once:

./samples/event-bus-listen-once.ts
import { eventBus } from "./event-bus-initial-config.js";

await eventBus.listenOnce("add", (event) => {
console.log(event);
});

// Listener will be only triggered here
await eventBus.dispatch("add", {
a: 5,
b: 5,
});

// Listener will not be triggered because it removed after the first dispatch.
await eventBus.dispatch("add", {
a: 3,
b: 3,
});

You can also cancel one-time listeners before they trigger:

./samples/event-bus-cancel-listen-once.ts
import { eventBus } from "./event-bus-initial-config.js";
import type { BaseEvent } from "eridu-tech/event-bus/contracts";

const listener = (event: BaseEvent) => {
console.log(event);
};

await eventBus.listenOnce("add", listener);

await eventBus.removeListener("add", listener);

// The listener is removed before dispatch and won't be triggered.
await eventBus.dispatch("add", {
a: 5,
b: 5,
});

The subscribeOnce method creates a one-time listener and returns an unsubscribe function:

./samples/event-bus-subscribe-once.ts
import { eventBus } from "./event-bus-initial-config.js";

const unsubscribe = await eventBus.subscribeOnce("add", (event) => {
console.log(event);
});

await unsubscribe();

await eventBus.dispatch("add", {
a: 5,
b: 5,
});

Promise-based event handling​

Wait for events using promises:

./samples/event-bus-as-promise.ts
import { eventBus } from "./event-bus-initial-config.js";
import { delay } from "eridu-tech/utilities";
import { TimeSpan } from "eridu-tech/time-span";

// Register the promise before dispatching the event.
const eventPromise = eventBus.asPromise("add");

await delay(TimeSpan.fromSeconds(1));
await eventBus.dispatch("add", {
a: 30,
b: 20,
});

const event = await eventPromise;

Listening to multiple events​

The addListener, removeListener, and subscribe methods all accept either a single event name or an array of event names, allowing you to register one listener for multiple events at once:

./samples/event-bus-multi-events.ts
import { EventBus } from "eridu-tech/event-bus";
import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter";

type AddEvent = {
a: number;
b: number;
};
type RemoveEvent = {
id: number;
};
type EventMap = {
add: AddEvent;
remove: RemoveEvent;
};

const eventBus = new EventBus<EventMap>({
adapter: new MemoryEventBusAdapter(),
});

// The same listener handles both "add" and "remove" events
await eventBus.addListener(["add", "remove"], (event) => {
console.log("EVENT:", event);
// event.type will be "add" or "remove" depending on which was dispatched
});

await eventBus.dispatch("add", { a: 1, b: 2 });
await eventBus.dispatch("remove", { id: 42 });

You can also use subscribe to get a single cleanup function that unsubscribes from all listed events at once:

./samples/event-bus-subscribe-multi.ts
import { eventBus } from "./event-bus-initial-config.js";

const unsubscribe = await eventBus.subscribe(["add", "remove"], (event) => {
console.log("EVENT:", event);
});

await eventBus.dispatch("add", { a: 1, b: 2 });
await eventBus.dispatch("remove", { id: 42 });

// Unsubscribes from both "add" and "remove" in one call
await unsubscribe();

Separating dispatching and listening​

The library includes two additional contracts:

This separation makes it easy to visually distinguish the two contracts, making it immediately obvious that they serve different purposes.

./samples/event-bus-contracts.ts
import type {
IEventListenable,
IEventDispatcher,
} from "eridu-tech/event-bus/contracts";
import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter";
import { EventBus } from "eridu-tech/event-bus";

type AddEvent = {
a: number;
b: number;
};
type EventMap = {
add: AddEvent;
};

async function listenerFunc(
eventListenable: IEventListenable<EventMap>,
): Promise<void> {
// You cannot access the dispatch method
// You will get typescript error if you try

await eventListenable.addListener("add", (event) => {
console.log("EVENT:", event);
});
}

async function dispatchingFunc(
eventDispatcher: IEventDispatcher<EventMap>,
): Promise<void> {
// You cannot access the listener methods
// You will get typescript error if you try

await eventDispatcher.dispatch("add", {
a: 20,
b: 5,
});
}

const eventBus = new EventBus<EventMap>({
// You can choose the adapter to use
adapter: new MemoryEventBusAdapter(),
});

await listenerFunc(eventBus);
await dispatchingFunc(eventBus);

Invocable listeners​

An event listener is Invocable meaning you can also pass in an object (class instance or object literal) as listener:

info

For further information refer the Invocable docs.

./samples/event-bus-invocable-listener.ts
import { EventBus } from "eridu-tech/event-bus";
import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter";
import type { IEventListenerObject } from "eridu-tech/event-bus/contracts";

type AddEvent = {
a: number;
b: number;
};
type EventMap = {
add: AddEvent;
};

class Listener implements IEventListenerObject<AddEvent> {
private count = 0;

invoke(event: AddEvent): void {
console.log("EVENT:", event);
console.log("COUNT:", this.count);
this.count++;
}
}

const eventBus = new EventBus<EventMap>({
adapter: new MemoryEventBusAdapter(),
});

await eventBus.addListener("add", new Listener());
await eventBus.dispatch("add", {
a: 1,
b: 2,
});
await eventBus.dispatch("add", {
a: 3,
b: -1,
});

Further information​

For further information refer to eridu-tech/event-bus API docs.