@glandjs/events
A pragmatic, battle-ready broker layer for event-driven systems.
@glandjs/events packages the pieces you need to build reliable, high-performance event brokers and broker meshes — not as a feature-gluttonous framework, but as a clear, composable foundation you can use inside Gland or drop into any project that needs deterministic, namespaced event routing.
Introduction
@glandjs/events sits at the core of the Gland architecture—a sophisticated, zero-dependency event broker system implemented in TypeScript. While @glandjs/emitter provides the atomic primitive for event emission, @glandjs/events builds upon that foundation to offer a complete, production-ready messaging system designed for complex, multi-broker environments.
The primary distinction lies in scale and scope. Where an emitter is a local, single-instance primitive, an event broker is a network-aware entity. Brokers can connect to one another, forming mesh networks where events flow seamlessly across isolated parts of a distributed system. They introduce concepts like channels for organizational hierarchy, watchers for asynchronous event waiting, and mesh connectivity for peer-to-peer communication.
In Gland’s vision of modular architecture, the event broker is more than a communication tool—it’s the architectural backbone. Every service, every module, every isolated component communicates through brokers, creating a system where message flow defines behavior and event patterns define relationships. This is not tight coupling masquerading as modularity; this is true decoupling through semantic abstraction.
Think of it as the glue that turns many independent emitters into a coherent messaging substrate: explicit propagation rules, channel boundaries, pluggable transports, lifecycle semantics, and patterns for composing brokers into a mesh.
Philosophy
Event-driven architecture solves a fundamental problem in modern software design: how do you build systems where components are independent, interchangeable, and yet perfectly coordinated? The answer lies in treating all communication as events—messages that propagate through the system, triggering responses without requiring knowledge of the sender or the downstream consequences.
@glandjs/events is built on this principle, but extends it with an additional layer of abstraction. It is protocol-agnostic and transport-agnostic, meaning it doesn’t care whether events are transmitted via HTTP, WebSocket, message queues, or any other mechanism. What matters is the message itself—the event. The broker abstracts away all concerns about delivery, routing, and acknowledgment.
The philosophy can be summarized in a few core tenets:
Semantic Decoupling: Events carry meaning, not just data. A broker doesn’t route events based on predefined paths; it simply broadcasts them, allowing any interested component to listen and react. This creates a system where behavior emerges from the interaction of independent listeners, not from rigid predetermined flows.
Protocol Independence: Whether your infrastructure uses REST, gRPC, WebSockets, or Redis, the event broker remains unchanged. This is achieved through adapters and connectors that translate between transport layers and the broker’s internal model. Your application code remains untouched as infrastructure evolves.
Composability: Brokers can be composed into larger systems. A single broker handles local events; multiple connected brokers create a mesh where events traverse network boundaries seamlessly. This composability allows systems to grow from simple single-broker applications to complex, distributed networks without architectural changes.
Minimal Constraints: Like its foundation emitter, @glandjs/events refuses to impose rigid structures. There are no mandatory message formats, no required delivery semantics, no predefined lifecycles. The broker provides the mechanism; developers define the meaning.
Core Concepts
EventBroker: The Central Node
At the heart of @glandjs/events is the EventBroker—a stateful entity that manages event subscriptions, emissions, and routing. Each broker maintains its own internal event tree (powered by the core emitter) and can connect to other brokers to form a network.
Creating a broker is straightforward, though the implications are profound. A broker has an identity, a name that distinguishes it in logs and mesh networks. It maintains all listeners for local events and acts as the gateway for events flowing into and out of its scope.
The broker is not a passive container; it’s an active participant in the system. When an event is emitted, the broker ensures all local listeners are invoked. When the broker is connected to peers, it acts as a relay, forwarding events across network boundaries. When events arrive from external sources, it integrates them seamlessly into its local event space.
Channels: Organizational Hierarchy
While event names provide semantic meaning (like user:login or system:error), channels provide organizational structure. A channel is a scoped namespace within a broker, allowing related events to be grouped and managed together.
Imagine a large application where the user domain manages authentication, profile updates, and preferences. Rather than registering listeners directly on the broker with names like user:auth:login, you can create a user channel and register listeners on scoped events like login. This separation offers clarity, maintainability, and the ability to manage channel-level concerns independently.
Channels themselves can be connected to other channels, creating hierarchical event flows within or across brokers. This architectural flexibility allows for both top-down and bottom-up event organization.
Mesh Networking: Distributed Communication
Where single brokers handle local event routing, mesh networks enable communication across service boundaries. In a mesh, multiple brokers are connected through peer links. Events emitted on one broker can propagate to connected peers, creating a system where services remain logically independent yet remain perfectly synchronized.
The mesh operates on a simple principle: when a broker emits an event, it first serves all local listeners, then broadcasts to all connected peers. Each peer, upon receiving the event, repeats the process—serving its local listeners and broadcasting to its own connections. This decentralized approach eliminates the need for a central broker or message bus, reducing architectural bottlenecks and improving resilience.
Mesh networks can be configured with different topologies. A star topology, where one broker connects to many. A linear topology, where brokers form a chain. A full mesh, where every broker connects to every other. Or custom topologies designed for specific system characteristics. The broker API makes all of these patterns straightforward to implement.
Adapters: Transport Independence
An adapter is a specialized component that translates between a broker’s internal event model and an external transport mechanism. An HTTP adapter might receive events via REST endpoints and emit them internally. A Redis adapter might subscribe to pub/sub channels and relay events into the broker. A WebSocket adapter might maintain persistent connections to other brokers.
Adapters allow @glandjs/events to integrate seamlessly with any infrastructure. They’re not built-in—instead, they’re implemented by developers or provided by the ecosystem. This keeps the core broker lightweight while enabling unlimited extensibility.
Event Watching: Asynchronous Waiting
While event listeners are inherently reactive—they respond to events as they arrive—sometimes you need to wait for an event to occur before proceeding. The watch method transforms events into promises, allowing you to write code in an imperative style when needed.
This is particularly useful for orchestration logic, integration tests, or scenarios where code flow depends on external events. You can watch for a single event with a timeout, defaulting to a specific value if the event doesn’t arrive in time. This flexibility bridges the gap between event-driven and imperative programming styles.
Why @glandjs/events Exists
The development of @glandjs/events emerged from practical needs observed while building the larger Gland architecture. While @glandjs/emitter solved the problem of local event emission with minimal overhead, real-world applications required more. Services needed to communicate across process boundaries. Events needed to flow through multiple tiers of infrastructure. Systems needed to scale without architectural changes.
Existing solutions were either too heavy (introducing significant performance overhead and complexity) or too simplistic (providing only basic pub/sub without the sophistication required for production systems). @glandjs/events was built to fill this gap—to provide a complete, production-ready event brokering system that remained as lightweight and predictable as the core emitter.
The result is a system that weighs only a few kilobytes, introduces zero external dependencies, and yet handles complex scenarios like mesh networking, channel hierarchies, and asynchronous event waiting with elegance and efficiency.
Architecture
The architecture of @glandjs/events is layered, with each layer building upon the previous one while maintaining clear separation of concerns.
At the foundation lies the @glandjs/emitter, providing the high-speed, tree-based event routing for local listeners. Above this sits the EventBroker, adding state management, channel support, and mesh networking capabilities. At the topmost layer sit adapters and integration points, allowing brokers to connect to external systems and protocols.
The broker’s internal structure maintains a registry of connected peers, a collection of active channels, and a set of pending watchers. When an event is emitted, the broker first resolves it through the internal emitter (serving local listeners), then broadcasts it through the mesh. This two-stage process ensures local events are handled with minimal latency while still maintaining network awareness.
Mesh connections are maintained through a simple peer registry. Each broker tracks which other brokers it’s connected to and maintains the necessary connection state to forward events. Broadcasts are idempotent—receiving the same event multiple times through different paths results in the same final state. A message ID system prevents infinite loops in highly connected meshes.
Channels are implemented as lightweight wrappers around a scoped event namespace. When you emit an event through a channel, it’s transformed into a fully qualified event name and emitted through the broker. This abstraction allows channels to be managed and composed independently without introducing additional complexity.
Use Cases
@glandjs/events is purpose-built for scenarios where systems must remain loosely coupled, scalable, and maintainable. While it can be used for simple applications (where a basic emitter might suffice), its true value emerges at scale.
Microservices Coordination: In a microservices architecture, services need to coordinate without direct API calls. Events flow between services—user created events trigger welcome emails, order events trigger inventory updates. @glandjs/events provides the substrate for this coordination, with each service as a broker or connected to a broker mesh.
Real-time Applications: Applications requiring live updates—collaborative editing, live notifications, multiplayer games—benefit from the event-driven model. All clients and servers are brokers in the same mesh, with events flowing in real-time across the network.
Complex Domain Logic: Large applications with intricate business logic benefit from event-driven decomposition. Rather than monolithic classes with numerous responsibilities, components emit domain events that trigger appropriate responses. Order service emits order:placed, payment service emits payment:processed, fulfillment service emits shipment:initiated. Each component operates independently while the system remains coordinated.
Asynchronous Workflows: Long-running workflows that span multiple services naturally decompose into event sequences. Approval workflows, data processing pipelines, and system orchestration become readable event sequences: this event triggers this action, which emits this event, which triggers the next action.
Testing and Instrumentation: Event brokers are excellent testing primitives. You can emit test events, watch for expected events to be emitted, verify event sequences. Similarly, brokers naturally support telemetry and observability—instrumenting event flow reveals system behavior with minimal code.
Integration
@glandjs/events is designed to integrate cleanly into existing systems. Whether you’re building a new application from scratch or integrating into established infrastructure, the path is straightforward.
In new applications, you typically create a broker instance at your application’s entry point, register domain event handlers, and let the event-driven model guide your architecture. In integrated scenarios, you might create a broker for a specific subdomain or module, letting it handle internal coordination while existing code remains unchanged.
Integration with external systems happens through adapters. An HTTP adapter allows REST endpoints to emit events into the broker. A WebSocket adapter enables real-time browser communication. A Redis adapter connects the broker to a message bus shared with other services. Building adapters is straightforward—they’re simply components that listen to an external source and emit events into the broker, or listen to the broker and send events to an external destination.
The type safety provided by TypeScript integration ensures that event contracts remain consistent across your system. Define your event types once, use them everywhere, and TypeScript prevents mismatched event names or incorrect payloads at compile time. This is particularly valuable in large teams where clarity and correctness matter.
Design Characteristics
Several characteristics define how @glandjs/events behaves and why it’s suited to its intended use cases.
Synchronous Dispatch: Events are dispatched synchronously by default. Listeners are invoked immediately when an event is emitted, in the order they were registered. This predictability makes event flows easier to reason about and debug. Asynchronous work can be performed within listeners (through async functions), but the dispatch mechanism itself remains synchronous. This is intentional—it prevents hidden complexity and ensures observable event ordering.
No Message Persistence: Events are ephemeral. They flow through the system and are gone. If a broker goes offline or a listener isn’t registered when an event is emitted, that event is lost. This is not a limitation but a feature—it encourages systems to be resilient to temporary unavailability and prevents accumulation of stale events.
No Guaranteed Delivery: In a mesh network, there’s no guarantee that an event emitted on one broker will reach all connected peers. Network partitions, timing issues, or process crashes might prevent delivery. This reflects reality—in distributed systems, guarantees are expensive and often unnecessary. Applications should be designed to handle missing events gracefully.
No Built-in Persistence or Queuing: The broker doesn’t persist events to disk or queue them for later delivery. For applications requiring durability, external systems (databases, message queues) can store events outside the broker. The broker’s job is real-time coordination, not data storage.
These characteristics might sound like limitations, but they reflect a deliberate design choice: keep the system simple, predictable, and fast. Add persistence, queuing, or guarantees externally where specific use cases require them.
Performance Characteristics
Performance is foundational to @glandjs/events design, just as it was for @glandjs/emitter. The broker inherits the emitter’s tree-based routing and caching, adding only minimal overhead for mesh operations.
Local event emission is extremely fast—event dispatch through the emitter occurs in near-constant time, with results cached for frequently accessed events. Emitting to a single broker with no mesh connections adds negligible overhead beyond the core emitter’s performance.
Mesh operations scale linearly with the number of connected peers. Broadcasting an event to N connected brokers requires N emit operations on the broker’s internal emitter, each triggered by the mesh relay logic. In practice, mesh sizes are typically small (a handful to dozens of brokers), so this overhead remains acceptable.
Channels add no perceptible overhead—they’re essentially scoped event name prefixes. The underlying event tree naturally optimizes repeated channel operations.
The result is a system capable of handling thousands of events per second on modest hardware, even with multiple mesh connections active. For applications with higher throughput requirements, external tools (message queues, distributed event streams) can supplement the broker for specific high-volume event flows.
Getting Started
Begin by creating a broker instance and registering event listeners. The simplest application looks like this:
import { EventBroker } from '@glandjs/events'
interface AppEvents { 'user:login': { id: string; username: string } 'order:placed': { orderId: string; total: number }}
const broker = new EventBroker<AppEvents>({ name: 'main' })
broker.on('user:login', (data) => { console.log(`User ${data.username} logged in`)})
broker.emit('user:login', { id: '123', username: 'alice' })For multi-broker scenarios, connect brokers into a mesh and events flow automatically:
const broker1 = new EventBroker({ name: 'service-a' })const broker2 = new EventBroker({ name: 'service-b' })
broker1.connectTo(broker2)
broker1.on('event:name', (data) => { console.log('Received on broker1:', data)})
broker2.emit('event:name', { /* data */ })From here, the documentation guides you through channels, mesh patterns, adapters, and advanced scenarios. The philosophy is consistent throughout: minimal constraints, maximum flexibility, production-ready performance.
Next Steps
Explore the following sections to deepen your understanding and apply @glandjs/events effectively:
Architecture and Concepts: Understand the internal design decisions, how the tree-based routing works, and why certain choices were made.
EventBroker API: Complete reference for all broker methods, parameters, and options.
Channels and Scoping: How to organize events hierarchically and manage complex event namespaces.
Mesh Networking: Patterns for connecting multiple brokers, building resilient networks, and handling different topologies.
Adapters and Integration: Extending the broker to communicate with external systems and protocols.
Performance Optimization: Tuning brokers for specific workloads and scaling considerations.
Recipes and Patterns: Real-world examples demonstrating common scenarios and best practices.