@glandjs/events@2.0.0
Quick start
The shortest path from an empty file to a running event broker.
# pnpm pnpm add @glandjs/events# npm npm install @glandjs/events# yarn yarn add @glandjs/events# bun bun add @glandjs/events| Package | @glandjs/events |
| Version | 2.0.0 |
| Repository | glandjs/events |
| License | MIT |
| Bun | >=1.0.0 |
| TypeScript | >=5 |
| Dependencies | @glandjs/emitter ^1.1.4 |
This guide will help you get started with @glandjs/events quickly. Follow these steps to set up a basic event system in your application.
For the complete reference, see the API Reference.
Installation
Section titled “Installation”First, install the package using your preferred package manager:
# Using npmnpm install @glandjs/events
# Using pnpmpnpm add @glandjs/events
# Using bunbun add @glandjs/eventsBasic Usage
Section titled “Basic Usage”Here’s a simple example to demonstrate the core functionality:
import { EventBroker } from '@glandjs/events';
// Define your event types (optional but recommended with TypeScript)type AppEvents = { 'user:login': { id: string; username: string }; 'notification:new': { id: string; message: string };};
// Create an event brokerconst events = new EventBroker<AppEvents>({ name: 'app-broker' });
// Subscribe to eventsevents.on('user:login', (data) => { console.log(`User logged in: ${data.username} (${data.id})`);});
// Emit eventsevents.emit('user:login', { id: 'user123', username: 'john_doe' });Use a
typealias, notinterface AppEvents extends EventRecord. >EventRecordisRecord<string, any>, and an interface extending it inherits a string index signature — which silently turns off all checking of event names and payloads. A plaininterface AppEvents { … }with no base is fine too.
Waiting for Events
Section titled “Waiting for Events”You can wait for events to occur using the watch method:
// Wait for a user login eventasync function waitForLogin() { console.log('Waiting for user login...'); try { const userData = await events.watch('user:login', 5000); // 5 second timeout console.log(`User ${userData.username} logged in!`); return userData; } catch (error) { console.error('Login timeout:', (error as Error).message); return null; }}
// Call the functionwaitForLogin().then((user) => { if (user) { // Process the logged in user }});
// Later, when a user logs in, it will resolve the promisesetTimeout(() => { events.emit('user:login', { id: 'user456', username: 'jane_smith' });}, 2000);watch is one-shot: it settles on the next matching emit and unsubscribes. It
also works as an option on on/once, which is handy when you want a fallback
value instead of a rejection:
// Resolves with `{ id: 'default', message: 'No notifications' }` if nothing arrives.const notification = await events.on('notification:new', null, { watch: true, timeout: 1000, defaultValue: { id: 'default', message: 'No notifications' },});Requesting a Value with call
Section titled “Requesting a Value with call”Some events are questions, not notifications. Declare them with IOEvent and
call returns an answer:
import { EventBroker, type IOEvent } from '@glandjs/events';
type AppEvents = { 'user:login': { id: string; username: string }; 'validate:form': IOEvent<{ name: string; email: string; age: number }, boolean>;};
const events = new EventBroker<AppEvents>({ name: 'app-broker' });
// Each listener is one check.events.on('validate:form', (d) => d.name.length > 0);events.on('validate:form', (d) => d.email.includes('@'));events.on('validate:form', (d) => d.age >= 18);
const formData = { name: 'John', email: 'john@example.com', age: 25 };
// No strategy: the first listener's result, typed `boolean`.const isValid = events.call('validate:form', formData);
// 'all': every result, in registration order.const results = events.call('validate:form', formData, 'all');
console.log(isValid); // trueconsole.log(results); // [true, true, true]Both return [] when nothing is listening, so an unanswered request is a result
you can branch on rather than an undefined that looks like a handler returned
nothing.
Using Channels
Section titled “Using Channels”Channels let a module own a namespace without ever writing the prefix:
// Create a user channelconst userChannel = events.channel('user');
// Subscribe to channel events — 'login', not 'user:login'userChannel.on('login', (data) => { console.log(`Channel: User logged in: ${data.username}`);});
// Emit channel events — reaches 'user:login'userChannel.emit('login', { id: 'user789', username: 'bob_jackson' });The type system narrows the channel to the events under its prefix and re-labels
them, so userChannel.on('loginn') does not compile and the payload type is
already correct without an annotation.
A channel can be nested one level deeper:
const profile = events.channel('user').channel('profile');profile.on('viewed', (data) => track(data.id)); // 'user:profile:viewed'Connecting Multiple Brokers
Section titled “Connecting Multiple Brokers”You can connect multiple brokers to create a mesh network:
// Create brokersconst broker1 = new EventBroker<AppEvents>({ name: 'broker-1' });const broker2 = new EventBroker<AppEvents>({ name: 'broker-2' });const broker3 = new EventBroker<AppEvents>({ name: 'broker-3' });
// Connect thembroker1.connectTo(broker2);broker1.connectTo(broker3);broker2.connectTo(broker3);
// Or use createConnections for a complete mesh — it also connects the entries// to each other, so every broker reaches every other.broker1.createConnections([broker2, broker3]);
// Set up event listeners on each brokerbroker1.on('notification:new', (message) => { console.log(`[${broker1.id}] Received: ${message.message}`);});
// Broadcast a message to all connected brokers.// Every broker in the connected component receives it exactly once.broker1.broadcast('notification:new', { id: 'n1', message: 'Hello everyone!' });
// Send a direct message to a specific connected brokerbroker2.emitTo(broker3.id, 'notification:new', { id: 'n2', message: 'Just for you' });Other mesh operations:
broker1.getConnections(); // ['broker-2', 'broker-3']broker1.isConnected('broker-2'); // truebroker1.callTo('broker-2', 'validate:form', formData); // ask a peerbroker1.disconnect('broker-3'); // removes the link both waysTo forward selected events to a peer automatically, without per-message work:
broker1.connectTo(broker2, { events: ['notification:new'] });
// Every emit of 'notification:new' on broker1 now also reaches broker2.broker1.emit('notification:new', { id: 'n3', message: 'auto-forwarded' });Forwarding is routing state rather than a listener: it shares the original
correlation id so it cannot loop, and it stops on disconnect.
Advanced Event Handling
Section titled “Advanced Event Handling”One-time Listeners
Section titled “One-time Listeners”Subscribe to an event for one occurrence only:
events.once('notification:new', (data) => { console.log(`One-time notification: ${data.message}`);});Removing Listeners
Section titled “Removing Listeners”const handler = (data) => console.log(data.message);
events.on('notification:new', handler);events.off('notification:new', handler); // one listenerevents.off('notification:new'); // all of themRemoval is exact-name only: off('user:login') leaves a 'user:*' subscription
in place.
Wildcards
Section titled “Wildcards”The underlying emitter resolves * within a single segment:
events.on('user:*', (data) => console.log(data)); // every 'user:…' eventWildcards are matched by emit, and deliberately excluded from getListener —
which is what keeps call deterministic.
Listener Budget
Section titled “Listener Budget”Each event name has a listener budget, so a subscription leak fails loudly instead of accumulating quietly:
const events = new EventBroker<AppEvents>({ name: 'app', maxListeners: 5 });
// On the 6th listener for one event:// Error: Maximum listeners (5) exceeded for event 'user:login'off frees the budget again.
Configuration
Section titled “Configuration”const events = new EventBroker<AppEvents>({ name: 'app-broker', // required — the broker's id in a mesh cacheSize: 6, // event names the emitter keeps resolved delimiter: ':', // separator for namespaced names ignoreErrors: false, // swallow errors thrown by *peers* defaultTimeout: 1000, // default watcher timeout in ms maxListeners: 5, // listener budget per event});Cleaning Up
Section titled “Cleaning Up”events.shutdown();shutdown() releases every listener, disconnects every peer (telling them, so
none is left holding a dead link), and rejects any pending watcher rather than
leaving a timer armed. It is idempotent, and the broker stays usable afterwards.
TypeScript Integration
Section titled “TypeScript Integration”For full type safety, define your event types:
import { EventBroker, type IOEvent } from '@glandjs/events';
type AppEvents = { 'user:login': { id: string; username: string }; 'user:logout': { id: string; timestamp: number }; 'notification:new': { id: string; message: string; type: 'info' | 'warning' | 'error' }; 'data:loaded': { source: string; items: unknown[] }; 'user:validate': IOEvent<{ name: string }, boolean>;};
const events = new EventBroker<AppEvents>({ name: 'typed-broker' });
// TypeScript enforces correct event names and payload typesevents.on('user:login', (data) => { // data is correctly typed as { id: string; username: string } console.log(data.username);});
// These would all cause a TypeScript error:// events.emit('user:logon', { id: '1', username: 'a' }); // unknown event name// events.emit('user:login', { id: 123, username: 'a' }); // id should be a string// events.on('user:loginn', (d) => {}); // misspelled eventNext Steps
Section titled “Next Steps”- Read the API Reference for every method, option and type
- Look at the Examples directory for complete, runnable programs
- Learn about Event-Driven Architecture principles