Event Bus
NATS Event Bus
Lightweight distributed event bus using NATS JetStream for at-least-once delivery and durable subscriptions.
The @noddde/nats package uses the nats client with JetStream to provide at-least-once delivery, durable consumers, and natural backpressure.
Installation
yarn add @noddde/nats natsConfiguration
import { NatsEventBus } from "@noddde/nats";
const eventBus = new NatsEventBus({
servers: "localhost:4222",
consumerGroup: "my-service", // required — scopes durable consumer names
streamName: "noddde-events", // optional — enables JetStream durable subscriptions
subjectPrefix: "noddde.", // optional — subjects become "noddde.AccountCreated"
prefetchCount: 256, // optional — backpressure control
resilience: { maxAttempts: -1, initialDelayMs: 2000, maxRetries: 10 }, // optional — connection + delivery resilience
});| Option | Type | Required | Default | Description |
|---|---|---|---|---|
servers | string | string[] | Yes | — | NATS server URL(s) |
consumerGroup | string | Yes | — | Consumer group identity. Used as prefix for JetStream durable consumer names (${consumerGroup}_${eventName}). Two services with different values independently consume the same stream without stealing each other's messages. Analogous to Kafka's groupId |
streamName | string | No | — | JetStream stream name for durable subscriptions |
subjectPrefix | string | No | "" | Prefix prepended to event names to form subjects |
prefetchCount | number | No | 256 | Maximum unacknowledged messages per consumer. Maps to JetStream maxAckPending. Provides backpressure control |
resilience | BrokerResilience | No | — | Connection resilience. maxAttempts maps to maxReconnectAttempts (-1 = infinite). initialDelayMs maps to reconnectTimeWait (default: 2000ms). maxDelayMs is ignored (NATS uses fixed intervals). maxRetries maps to JetStream maxDeliver |
logger | Logger | No | NodddeLogger("warn", "noddde:nats") | Framework logger instance from @noddde/core. All internal logging (errors, warnings) goes through this logger with structured context data |
How It Works
- Publishing: Events are serialized as JSON, encoded as
Uint8Array, and published to a NATS subject (${subjectPrefix}${event.name}). When astreamNameis configured, JetStream publish acknowledgment is awaited. - Subscribing: JetStream consumers with durable subscriptions are created for each registered event name, using the durable name pattern
${consumerGroup}_${eventName}. Two services with differentconsumerGroupvalues get independent consumers on the same stream. Handlers registered beforeconnect()are buffered; subscriptions are activated duringconnect()— if any subscription fails,connect()rejects immediately (fail-fast). Handlers registered afterconnect()create subscriptions immediately (failures are logged, not thrown). Multiple handlers for the same event are invoked in parallel viaPromise.allSettled()— every handler runs to completion even when some fail, and each failure is logged individually with structured fields (see Handler Error Isolation). - Delivery: At-least-once with JetStream. Messages are acknowledged only after all handlers complete successfully. If any handler fails, the first rejection is re-thrown after all siblings have settled, and the consumer loop calls
msg.nak()for immediate redelivery (capped bymaxDeliver). Consumers must be idempotent — handlers that completed before a sibling failure will re-execute on redelivery. - Poison message protection: Malformed messages that fail JSON deserialization are permanently discarded via
msg.term(), preventing corrupt data from blocking the subscription via infinite redelivery. Ifresilience.maxRetriesis configured, it maps to the JetStreammaxDeliverconsumer option, limiting how many times NATS will redeliver a message before discarding it server-side. - Backpressure: The
prefetchCountoption controls how many unacknowledged messages the server delivers to each consumer (mapped to JetStreammaxAckPending), providing natural backpressure when handlers are slow. - Connection resilience: NATS native reconnection is enabled by default. The optional
resilienceconfig mapsmaxAttemptsto NATSmaxReconnectAttemptsandinitialDelayMstoreconnectTimeWaitfor automatic recovery on connection loss.maxDelayMsis ignored because NATS uses fixed-interval reconnection (no exponential backoff). - Logging: All internal logging uses the framework
Loggerinterface from@noddde/core. Pass a customloggerin the config or accept the default (NodddeLogger("warn", "noddde:nats")). All log calls include structured context data (event name, error details). - Shutdown:
close()callsnc.drain()to process in-flight messages before disconnecting.
Wiring with Domain
import { wireDomain } from "@noddde/engine";
import { NatsEventBus } from "@noddde/nats";
const domain = await wireDomain(myDomain, {
buses: () => ({
eventBus: new NatsEventBus({
servers: "localhost:4222",
consumerGroup: "my-service",
streamName: "noddde-events",
}),
}),
});