@glandjs/events@2.0.0
@glandjs/events
The namespaced event broker — channels, mesh networking, request/response calls, async watchers and loop-safe propagation.'
@glandjs/events is the event backbone of Gland. Where @glandjs/emitter is a local
pub/sub primitive, a broker is a stateful, connectable node: it owns a namespace,
exposes channels, can be linked into a mesh, and can turn events into promises.
pnpm add @glandjs/eventsCreating a broker
Section titled “Creating a broker”import { EventBroker } from '@glandjs/events'
const broker = new EventBroker<Events>({ name: 'orders' })BrokerOptions
Section titled “BrokerOptions”interface BrokerOptions { /** Unique name of this broker. Required — it is also the broker's id. */ name: string /** Namespace delimiter. @default ':' */ delimiter?: string /** Listener resolution cache size. @default 6 */ cacheSize?: number /** Default timeout for watchers, in ms. @default 1000 */ defaultTimeout?: number /** Maximum listeners per exact event name. @default 5 */ maxListeners?: number /** Swallow a failure raised while routing to a connected peer. @default false */ ignoreErrors?: boolean}broker.options is always fully populated: every default is resolved at construction,
so a test can assert the whole object.
const broker = new EventBroker({ name: 'orders', delimiter: ':', defaultTimeout: 5_000, maxListeners: 20,})The typed event map
Section titled “The typed event map”Every broker, channel and context is generic over one map of event names to payload types.
import type { IOEvent } from '@glandjs/events'
// ✓ a type alias: names and payloads are both enforcedtype Events = { 'order:placed': { orderId: string; total: number } 'order:paid': IOEvent<{ orderId: string }, boolean>}
// ✓ a plain interface also works — it has no index signatureinterface Events { 'order:placed': { orderId: string; total: number }}IOEvent<Payload, Return> declares both sides of a request/response event. A plain type
is a payload with a void return.
Subscribing
Section titled “Subscribing”on(event, listener, options?)
Section titled “on(event, listener, options?)”broker.on('order:placed', (payload) => { console.log(payload.orderId)})
// chained — most methods return the brokerbroker .on('order:placed', audit) .on('order:paid', charge)Pass null with watch: true and you get a promise instead of a subscription:
const order = await broker.on('order:placed', null, { watch: true, timeout: 2_000, defaultValue: null,})once(event, listener, options?)
Section titled “once(event, listener, options?)”Fires a single time, then unsubscribes — it detaches before invoking, so a re-entrant
emit from inside the listener cannot run it twice. It also accepts the same
{ watch: true } form as on.
broker.once('server:ready', () => startWorkers())off(event, listener?)
Section titled “off(event, listener?)”broker.off('order:placed', audit) // one listenerbroker.off('order:placed') // every listener for that exact namegetListener(event)
Section titled “getListener(event)”Returns a copy of the listeners registered for an exact event name, so mutating the result cannot corrupt the registry.
Emitting
Section titled “Emitting”emit(event, payload, options?)
Section titled “emit(event, payload, options?)”Synchronous dispatch, in registration order, returning the broker.
broker.emit('order:placed', { orderId: 'o-1', total: 42 })A listener may be async, but emit does not wait for it. If you need the result, use
call().
call(event, data) / call(event, data, 'all')
Section titled “call(event, data) / call(event, data, 'all')”Request/response without a wire.
const ok = broker.call('order:paid', { orderId: 'o-1' })// ^? boolean — straight from IOEvent
const results = broker.call('order:paid', { orderId: 'o-1' }, 'all')// ^? boolean[]'all' is the only strategy. Without it you get the first listener’s result; with it you
get every result in registration order. There is no 'first', 'last', 'every',
'some' or 'race'.
Waiting for events
Section titled “Waiting for events”watch(event, timeoutMs?)
Section titled “watch(event, timeoutMs?)”Turns an event into a one-shot promise.
const order = await broker.watch('order:placed', 5_000)Rejects after timeoutMs (defaulting to defaultTimeout) with
Event 'order:placed' timed out after 5000ms. watch() always rejects on timeout; only
the on/once watch form accepts defaultValue.
const order = await broker.on('order:placed', null, { watch: true, timeout: 1_000, defaultValue: { orderId: 'fallback', total: 0 },})defaultValue is honoured for any non-undefined value, so 0, '', false and
null all work.
Channels
Section titled “Channels”channel(name) returns a live channel scoped to a namespace. Instances are cached per
name, so calling it twice returns the same object, and nested channels are cached on the
broker under their joined name.
const orders = broker.channel('order')
orders.on('placed', (payload) => {}) // -> 'order:placed'orders.emit('placed', { orderId: 'o-1' })orders.call('paid', { orderId: 'o-1' })orders.watch('placed') // namespaced too
const refunds = orders.channel('refund') // -> 'order:refund:*'| Member | Type |
|---|---|
id |
string — the owning broker’s id |
name |
string — the fully joined channel name |
on / once / off / emit / call / getListener / watch |
as on the broker, namespaced |
channel(name) |
a nested channel |
broadcast |
send to the whole mesh |
A channel owns no bus of its own: every method rewrites the name and delegates.
broker.channel('order').emit('placed', p) and broker.emit('order:placed', p) are the
same call.
Mesh networking
Section titled “Mesh networking”Brokers can be connected into a network. An event emitted on one broker is relayed to its peers, which serve their own listeners and relay onward.
const a = new EventBroker({ name: 'service-a' })const b = new EventBroker({ name: 'service-b' })
a.connectTo(b) // bidirectional
a.on('event:name', (data) => console.log('on a', data))b.emit('event:name', { ok: true })| Method | Signature | Description |
|---|---|---|
connectTo |
(broker, options?) => this |
Connect bidirectionally. |
disconnect |
(brokerId) => boolean |
Drop one connection, both directions. |
disconnectAll |
() => this |
Drop every connection. |
isConnected |
(brokerId) => boolean |
Test a connection. |
getConnections |
() => BrokerId[] |
Connected broker ids, in insertion order. |
getConnection |
(brokerId) => EventBroker | undefined |
The connected broker. |
createConnections |
(brokers[], options?) => this |
Full mesh, peer to peer. |
findBroker |
(brokerId, maxDepth = 3) => EventBroker | undefined |
Multi-hop lookup. |
broadcast |
(event, payload, options?) => this |
Emit to the whole mesh. |
broadcastTo |
(brokerIds, event, payload, options?) => this |
Emit to a subset. |
emitTo |
(brokerId, event, payload, options?) => boolean |
Emit to one peer. |
send |
(event, target, payload, options?) => this |
Emit to one peer object. |
callTo |
(brokerId, event, data) => EventReturn |
Request/response across the mesh. |
send needs no connection
Section titled “send needs no connection”a.send('order:placed', b, { orderId: 'o-1' })send targets a broker instance, works whether or not the two are connected, and is
not relayed onward. emitTo is the id-addressed variant and does require a connection —
it returns false when the peer is unknown.
Restricting what is forwarded
Section titled “Restricting what is forwarded”a.connectTo(b, { events: ['order:placed', 'order:paid'] })Loop safety
Section titled “Loop safety”Every propagate carries a correlation id, and each broker remembers the ids it has already processed, so a fully connected mesh terminates and a broker never handles the same event twice. The trace is bounded (the 4096 most recent ids) with FIFO eviction.
EventTrace.visited is bookkeeping inside a broker; only the id travels on the
wire. Two brokers can therefore each process the same id once — which is exactly the
guarantee you want.
Shutdown
Section titled “Shutdown”broker.shutdown()Rejects pending watchers, tells peers, drops every connection, releases cached channels and clears the processed-id trace. It is idempotent and safe on a broker that never started, and the instance stays usable afterwards.
await http.close()broker.shutdown() // the Gland core's own busFull reference
Section titled “Full reference”class EventBroker<TEvents extends EventRecord> implements Broker<TEvents> { constructor(options: BrokerOptions) get id(): BrokerId readonly options: BrokerOptions
on(event, listener, options?): this on(event, null, { watch: true, timeout?, defaultValue? }): Promise<Payload> once(event, listener, options?): this off(event, listener?): this emit(event, payload, options?): this getListener(event): Listener[]
call(event, data): EventReturn call(event, data, 'all'): EventReturn[] watch(event, timeoutMs?): Promise<Payload> channel(name): Channel<ChannelEvents>
broadcast(event, payload, options?): this broadcastTo(brokerIds, event, payload, options?): this send(event, target, payload, options?): this emitTo(brokerId, event, payload, options?): boolean callTo(brokerId, event, data): EventReturn callTo(brokerId, event, data, 'all'): EventReturn[]
connectTo(broker, options?): this disconnect(brokerId): boolean disconnectAll(): this isConnected(brokerId): boolean getConnections(): BrokerId[] getConnection(brokerId): EventBroker | undefined createConnections(brokers, options?): this findBroker(brokerId, maxDepth?): EventBroker | undefined
shutdown(): void}Two lower-level pieces are exported as well, and both are usable on their own:
| Symbol | Description |
|---|---|
EventEmitter<TEvents> |
The engine: on, once, off, emit, getListener, watch, shutdown. Its emit returns void, not this. |
EventWatcher<TEvents> |
The promise layer: watch, onEmit, pending, shutdown. |
Options
Section titled “Options”interface EventOptions { watch?: boolean; timeout?: number; defaultValue?: any }
interface EmitOptions extends EventOptions { _eventId?: string // correlation id _sourceId?: BrokerId // originating broker _propagate?: boolean // keep forwarding to peers, which forward onward}broadcast() sets all three for you. Setting _eventId yourself is how you replay the
same event on purpose; setting one that the broker has already processed is a no-op.
Design characteristics
Section titled “Design characteristics”Synchronous dispatch. Listeners run immediately, in registration order, which makes event flows straightforward to reason about and to log.
No persistence. Events flow and are gone. If no listener is registered, the emit is a no-op.
No guaranteed delivery. There is no acknowledgement, retry or dead-lettering.
No built-in serialisation. The event map is compile-time only; nothing is encoded. To move events between processes, put a channel behind a real transport.
These are design choices, not gaps. If you need durability, do the work in a channel that writes to a database or a queue.
Errors
Section titled “Errors”| Condition | Behaviour |
|---|---|
on with a null listener and no watch: true |
throws Listener cannot be null unless 'watch: true' is explicitly set in options. |
More than maxListeners on one exact event name |
throws Maximum listeners (5) exceeded for event 'order:placed' |
watch timeout |
rejects with Event 'order:placed' timed out after 1000ms |
shutdown with watchers pending |
rejects with Watcher shutdown before '…' was emitted |
connectTo(self) |
throws Cannot connect broker "orders" to itself |
connectTo(null) |
throws Invalid broker |
| A local listener throws | propagates out of emit; the remaining listeners for that event are skipped |
| A peer throws while being routed to | rethrown, unless ignoreErrors: true |
See also
Section titled “See also”- API reference — generated from the repository
@glandjs/emitter— the primitive underneath- Changelog