roar
Lightweight distributed pub/sub for Gleam on Erlang with automatic cluster synchronization.
Roar enables process-local and cluster-wide message broadcasting with automatic scope isolation, supporting both streaming subscriptions and callback-based handlers.
Features
- 🌍 Distributed by default - Messages automatically sync across all nodes in your cluster
- 🎯 Scope isolation - Multiple independent pub/sub systems in the same cluster
- 📦 Simple API - Subscribe with callbacks or buffered streams
- 🛡️ Memory safe - Configurable buffer capacity prevents runaway memory growth
- ⚡ BEAM native - Built on Erlang’s distribution primitives
Installation
Add roar to your Gleam project:
gleam add roar
Quick Start
import roar
pub fn main() {
// Create a router with buffer capacity and scope
let router = roar.new(capacity: 100, scope: "my_app")
// Subscribe to a topic
let sub = roar.subscribe(router, "events")
// Publish a message (reaches ALL nodes in "my_app" scope)
roar.publish(router, "events", "Hello, cluster!")
// Receive messages
case sub.receive() {
Ok(message) -> io.println(message)
Error(Nil) -> io.println("No messages")
}
sub.cancel()
}
Usage
Callback-based subscriptions
let cancel = roar.on(router, "notifications", fn(msg) {
io.println("Received: " <> msg)
})
roar.publish(router, "notifications", "New user signed up!")
cancel()
Buffered subscriptions
let sub = roar.subscribe(router, "events")
roar.publish(router, "events", "Event 1")
roar.publish(router, "events", "Event 2")
// Messages are buffered, read at your own pace
let assert Ok("Event 1") = sub.receive()
let assert Ok("Event 2") = sub.receive()
sub.cancel()
Multiple topics
let router = roar.new(capacity: 50, scope: "chat")
let messages = roar.subscribe(router, "messages")
let alerts = roar.subscribe(router, "alerts")
roar.publish(router, "messages", "Hello!")
roar.publish(router, "alerts", "Server restarting")
// Each subscription only receives its topic's messages
Scope isolation
// Different scopes don't interfere with each other
let app_router = roar.new(capacity: 100, scope: "app")
let admin_router = roar.new(capacity: 100, scope: "admin")
roar.subscribe(app_router, "events") // Only sees "app" scope
roar.subscribe(admin_router, "events") // Only sees "admin" scope
How It Works
Roar uses Whisper for local pub/sub and Erlang’s pg (process groups) for cluster-wide distribution. When you publish a message:
- Local delivery - Delivered immediately to subscribers on the same node
- Remote delivery - Forwarded to all nodes in the same scope via process groups
- No duplicates - Each subscriber receives exactly one copy
The capacity parameter protects against memory issues by limiting buffered messages per subscription. If a subscriber is too slow, old messages are dropped to prevent memory exhaustion.