README.md

# roar

[![Package Version](https://img.shields.io/hexpm/v/roar)](https://hex.pm/packages/roar)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/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:

```sh
gleam add roar
```

## Quick Start

```gleam
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

```gleam
let cancel = roar.on(router, "notifications", fn(msg) {
  io.println("Received: " <> msg)
})

roar.publish(router, "notifications", "New user signed up!")

cancel()
```

### Buffered subscriptions

```gleam
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

```gleam
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

```gleam
// 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](https://hexdocs.pm/whisper/) for local pub/sub and Erlang's `pg` (process groups) for cluster-wide distribution. When you publish a message:

1. **Local delivery** - Delivered immediately to subscribers on the same node
2. **Remote delivery** - Forwarded to all nodes in the same scope via process groups
3. **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.