Skip to content

Module-level bus

Problem

Multiple modules need to communicate through typed events without passing a bus instance through function arguments or a DI container. A module-level singleton gives every importer access to the same bus.

Solution

Define a shared bus as a module singleton. Any module can import it to publish or subscribe.

ts
// src/events/app-bus.ts
import { createBus } from '@vielzeug/eventit';

type AppEvents = {
  'user:login': { userId: string; email: string };
  'user:logout': void;
  'cart:updated': { items: CartItem[]; total: number };
  'theme:change': 'light' | 'dark';
};

export const appBus = createBus<AppEvents>({
  onError: (err, event, payload) => console.error(`[bus] error in "${event}"`, err, payload),
});

// src/cart/cart-module.ts
import { appBus } from '../events/app-bus';

appBus.on('user:login', ({ userId }) => loadCart(userId));
appBus.on('user:logout', clearCart);

Pitfalls

  • A module-level bus is a singleton. Calling dispose() in a component's teardown disposes it for all modules that imported it. Only dispose when the application shuts down.
  • Circular imports between modules that both import the same bus can cause the bus to be undefined during initialization. Keep the bus in a leaf module with no dependencies on the importing modules.
  • The TypeScript event map is erased at runtime. Emitting a misspelled event name does not throw — it simply fires with no listeners. Use the type parameter to catch these at compile time.