JAEB - Just Another Event Bus
In-process, actor-based event bus for Tokio applications.
JAEB provides:
- sync + async listeners via a unified
subscribeAPI - automatic dispatch-mode selection based on handler trait
- explicit listener unsubscription via
Subscriptionhandles or RAIISubscriptionGuard - dependency injection via handler structs
- retry policies with configurable strategy for async handlers
- dead-letter stream for terminal failures
- explicit
Result-based error handling (no panics in the public API) - graceful shutdown with queue draining and in-flight task completion
- idempotent shutdown (safe to call multiple times)
- optional Prometheus-compatible metrics via the
metricscrate - structured tracing with per-handler spans
#![forbid(unsafe_code)]
Installation
[]
= { = "0.2.3" }
= { = "1", = ["macros", "rt-multi-thread"] }
To enable metrics instrumentation:
[]
= { = "0.2.3", = ["metrics"] }
Quick Start
use Duration;
use ;
;
;
;
async
API Overview
EventBus
EventBus::new(buffer) -> Result<EventBus, EventBusError>-- create a new bus with the given channel capacityEventBus::builder()-- builder for fine-grained configuration (buffer size, timeouts, concurrency limits)subscribe(handler) -> Result<Subscription, EventBusError>-- dispatch mode inferred from traitsubscribe_with_policy(handler, policy) -> Result<Subscription, EventBusError>-- policy type is compile-time checked (FailurePolicyfor async,NoRetryPolicyfor sync)subscribe_dead_letters(handler) -> Result<Subscription, EventBusError>publish(event) -> Result<(), EventBusError>try_publish(event) -> Result<(), EventBusError>-- non-blocking, returnsChannelFullwhen queue is fullunsubscribe(subscription_id) -> Result<bool, EventBusError>shutdown() -> Result<(), EventBusError>-- idempotent, drains queue + in-flight tasksis_healthy() -> bool-- async, checks if the internal actor is still running
EventBus is Clone -- all clones share the same underlying actor.
Handler Traits
EventHandler<E>-- async handler, dispatched on a spawned task (requiresE: Clone)SyncEventHandler<E>-- sync handler, awaited inline during dispatch
The dispatch mode is selected automatically based on which trait is implemented.
The IntoHandler<E, Mode> trait performs the conversion; the Mode parameter
(AsyncMode / SyncMode) is inferred, so callers simply write bus.subscribe(handler).
Subscription & SubscriptionGuard
Subscription holds a SubscriptionId and a bus handle. Call subscription.unsubscribe()
to remove the handler, or use bus.unsubscribe(id) directly.
SubscriptionGuard is an RAII wrapper that automatically unsubscribes the listener when
dropped. Convert a Subscription via subscription.into_guard(). Call guard.disarm()
to prevent the automatic unsubscribe.
Types
Event-- blanket trait implemented for allT: Send + Sync + 'staticHandlerResult = Result<(), Box<dyn Error + Send + Sync>>FailurePolicy { max_retries, retry_strategy, dead_letter }-- for async handlersNoRetryPolicy { dead_letter }-- for sync handlers (or async handlers that don't need retries)IntoFailurePolicy<M>-- sealed trait enforcing compile-time policy/handler compatibilityDeadLetter { event_name, subscription_id, attempts, error }SubscriptionId-- opaque handler ID (wrapsu64)
Feature Flags
| Flag | Default | Description |
|---|---|---|
metrics |
off | Enables Prometheus-compatible instrumentation via the metrics crate |
When the metrics feature is enabled, the bus records:
eventbus.publish(counter, per event type)eventbus.handler.duration(histogram, per event type)eventbus.handler.error(counter, per event type)eventbus.handler.join_error(counter, per event type)
Observability
JAEB uses the tracing crate throughout. Key spans and events:
event_bus.publish/event_bus.subscribe/event_bus.shutdown-- top-level operationseventbus.handlerspan -- per-handler execution withevent,mode, andlistener_idfieldshandler.retry(warn) -- logged on each retry attempthandler.failed(error) -- logged when retries are exhaustedhandler.join_error(error) -- logged when a spawned task panics
Semantics
publishwaits for dispatch and sync listeners to finish.publishdoes not wait for async listeners to finish.- async handler failures can be retried based on
FailurePolicy. - sync handlers execute exactly once -- retries are not supported; passing a
FailurePolicywith retries to a sync handler is a compile error viaIntoFailurePolicy<M>. - after retries are exhausted (async) or on first failure (sync), dead-letter events are emitted when
dead_letter: true. - dead-letter handlers themselves cannot trigger further dead letters (recursion guard).
shutdowndrains queued publish messages and waits for in-flight async listeners.- after shutdown, all operations return
EventBusError::ActorStopped.
Error Variants
EventBusError::ActorStopped-- the actor has shut down or the channel is closedEventBusError::ChannelFull-- the internal queue is full (try_publishonly)EventBusError::InvalidConfig(ConfigError)-- invalid builder/constructor configuration (zero buffer size, zero concurrency)EventBusError::MiddlewareRejected(String)-- a middleware rejected the event before it reached any listenerEventBusError::ShutdownTimeout-- the configuredshutdown_timeoutexpired and in-flight async tasks were forcibly aborted
Examples
See examples/jaeb-demo for a working demo that includes:
- async and sync handlers with retry policies
- dead-letter logging
- Prometheus metrics exporter
- structured tracing setup
Run it with:
RUST_LOG=info,jaeb=trace
Notes
- JAEB requires a running Tokio runtime.
- Events must be
Send + Sync + 'static. Async handlers additionally require events to beClone. - Events are in-process only (no persistence, replay, or broker integration).
- The crate enforces
#![forbid(unsafe_code)].
License
jaeb is distributed under the MIT License.
Copyright (c) 2025-2026 Linke Thomas
This project uses third-party libraries. See THIRD-PARTY-LICENSES for the full list of dependencies, their versions, and their respective license terms.