createEventBus — Event Bus
A pub/sub bus for communication between independent modules. Built on the same bricks as the
rest of the BLL: createSynapse + MemoryStorage + Dispatcher (see create-synapse-basic).
Supports wildcard patterns, priorities, TTL, and event history.
Where this fits our domain: the pokemon module (see pokemon-advanced) only
knows about itself — it loads the list, tracks favorites, holds the selected pokemon. If other
parts of the app should react to those actions (analytics, toasts, a header badge), there's no need
to wire them together tightly. Pokemon publishes domain events (POKEMON_SELECTED, FAVORITE_TOGGLED),
and anyone subscribes to them. This is the "pattern 3 / mediator" from dependencies,
just packaged as a ready-made utility.
The reference pokemon module does not bake the bus in — event-bus is an optional integration on top of it, so this page has no canonical pokemon file, only a runnable sandbox.
Imports
import { createEventBus } from 'synapse-storage/utils'Creating
const eventBusHandle = createEventBus({
name: 'pokemon-events', // name (for singleton/debugging)
autoCleanup: true, // auto-cleanup of old events
maxEvents: 1000, // max stored events (default 1000)
})
// createEventBus returns a SynapseModule handle (lazy, PromiseLike) —
// the factory runs on the first await/ready()
const eventBus = await eventBusHandle
// The result (Synapse<EventBusState, EventBusDispatcher, undefined>):
// {
// storage: IStorage<EventBusState> — the state storage
// actions: EventBusDispatcher — typed actions (alias of dispatcher)
// dispatcher: EventBusDispatcher — the same dispatcher instance
// selectors: undefined — the bus has no selectors
// state$: Observable<EventBusState> — the state stream (always present)
// destroy: () => Promise<void> — cleanup
// }
// EventBusState:
// {
// events: Record<string, EventBusEvent>
// subscriptions: Record<string, SubscriptionInfo>
// }actions and dispatcher are the same EventBusDispatcher instance; its fields (publish/subscribe/…)
are the dispatch functions. Everything below uses eventBus.actions.
actions.publish() — Publishing an event
const eventBus = await createEventBus({ name: 'pokemon-events' })
// Publishing an event
const result = await eventBus.actions.publish({
event: 'POKEMON_SELECTED', // event type (string)
data: { id: 25, name: 'pikachu' }, // arbitrary data
metadata: { // optional metadata
priority: 'high', // 'low' | 'normal' | 'high'
ttl: 60000, // event time-to-live (ms)
},
})
// The result:
// {
// eventId: string — the unique event ID
// event: string — the event type
// data: any — the data
// }
// EventBusEvent (stored in storage):
// {
// id: string
// event: string
// data: any
// metadata: { ttl?: number | null, priority?: 'low' | 'normal' | 'high' }
// timestamp: number
// }actions.subscribe() — Subscribing to events
// Subscribing to a specific event
const { subscriptionId, unsubscribe } = await eventBus.actions.subscribe({
eventPattern: 'POKEMON_SELECTED', // exact match
handler: (data, event) => {
// data — event.data (the payload)
// event — the full EventBusEvent object
console.log(data) // { id: 25, name: 'pikachu' }
console.log(event.event) // 'POKEMON_SELECTED'
console.log(event.timestamp) // 1716633600000
},
})
// Wildcard patterns
await eventBus.actions.subscribe({
eventPattern: 'POKEMON_*', // all events starting with POKEMON_
handler: (data, event) => { // POKEMON_SELECTED, POKEMON_LOADED, ...
console.log(event.event, data)
},
})
await eventBus.actions.subscribe({
eventPattern: '*', // ALL events
handler: (data, event) => {
console.log('Any event:', event.event)
},
})
// Filter by priority
await eventBus.actions.subscribe({
eventPattern: 'FAVORITE_*',
handler: (data, event) => { ... },
options: { priority: 'high' }, // only high-priority events
})
// Unsubscribe
unsubscribe()Internally subscribe listens to the state.events slice of storage: when a new event is published,
every subscriber whose pattern matches gets a handler call. An error in a handler does not bring the
bus down — it is logged through the internal handleCallbackError.
actions.getEventHistory() — Event history
// Get the history for an event type
const history = await eventBus.actions.getEventHistory({
eventType: 'POKEMON_SELECTED', // the event type
limit: 10, // max records (default 100)
})
// Returns EventBusEvent[] — sorted by timestamp (newest first)
// [
// { id: '...', event: 'POKEMON_SELECTED', data: {...}, timestamp: 1716633600000 },
// { id: '...', event: 'POKEMON_SELECTED', data: {...}, timestamp: 1716633500000 },
// ]actions.getActiveSubscriptions() — Active subscriptions
const subscriptions = await eventBus.actions.getActiveSubscriptions()
// Returns an array:
// [
// {
// id: string, — the subscription ID
// pattern: string, — the pattern ('POKEMON_*', '*', etc.)
// options: {...}, — options (priority etc.)
// createdAt: number, — creation time
// }
// ]actions.clearEvents() — Clearing events
// Clear old events
await eventBus.actions.clearEvents({
olderThan: 60000, // remove events older than 60 seconds
})
// Clear all events
await eventBus.actions.clearEvents({})With autoCleanup: true old events are trimmed automatically on every publish: as soon as their
count exceeds maxEvents, only the maxEvents freshest ones (by timestamp) are kept.
destroy()
// Full cleanup: active subscriptions, storage, dispatcher
await eventBus.destroy()destroy() first calls every accumulated unsubscribe, then tears the module down and resets the
handle's memoization (the next await eventBusHandle rebuilds the bus from scratch).
Example: pokemon publishes, other modules listen
// pokemon-events.ts — the shared domain bus
import { createEventBus } from 'synapse-storage/utils'
export const pokemonEventsHandle = createEventBus({ name: 'pokemon-events', autoCleanup: true })
// ─── pokemon-side: publish domain events ─────────────────────────────────────
// A handy place is a wrapper over dispatcher intents (see dispatcher-detailed)
// or an effect that already sees the module's action stream.
const bus = await pokemonEventsHandle
export async function selectAndAnnounce(store: PokemonSynapse, pokemon: PokemonBrief) {
store.actions.selectPokemon(pokemon.id)
await bus.actions.publish({
event: 'POKEMON_SELECTED',
data: { id: pokemon.id, name: pokemon.name },
metadata: { priority: 'high' },
})
}
// ─── analytics.ts — listens to all domain events ─────────────────────────────
const bus = await pokemonEventsHandle
bus.actions.subscribe({
eventPattern: 'POKEMON_*',
handler: (data, event) => {
analytics.track(event.event, data) // POKEMON_SELECTED, FAVORITE_TOGGLED, ...
},
})
// ─── toaster.ts — reacts only to favorites ───────────────────────────────────
bus.actions.subscribe({
eventPattern: 'FAVORITE_TOGGLED',
handler: (data) => {
showToast(`Pokemon ${data.name} ${data.added ? 'added to' : 'removed from'} favorites`)
},
})The analytics and toaster modules know nothing about the pokemon synapse and don't import it —
the only link is the event names. That decoupling is the whole point of the bus.
Relation to createSynapse: the bus as an externalDispatcher
If you need not just to listen to events from outside, but to feed them into another synapse's
action stream (so its effects react to bus events like regular actions), pass the bus through
externalDispatchers — this is "communication variant 3" from dependencies:
const bus = await pokemonEventsHandle
const mySynapse = createSynapse(() => ({
storage,
dispatcher: new MyDispatcher(storage),
effects: new MyEffects(),
externalDispatchers: { eventBus: bus.dispatcher }, // bus actions land in action$
}))See also
dependencies — patterns for module communication (the bus = mediator / externalDispatchers).
create-synapse-basic — what the bus itself is built from (storage + dispatcher).
pokemon-advanced — the reference module whose events the bus publishes.