# Broadcasting
- [Introduction](#introduction)
- [Configuration](#configuration)
- [Connection Options](#connection-options)
- [Environment Variables](#environment-variables)
- [Echo Facade](#echo-facade)
- [API Reference](#api-reference)
- [Channels](#channels)
- [Public Channels](#public-channels)
- [Private Channels](#private-channels)
- [Presence Channels](#presence-channels)
- [Interceptors](#interceptors)
- [Creating a Custom Interceptor](#creating-a-custom-interceptor)
- [Registering Interceptors](#registering-interceptors)
- [Custom Drivers](#custom-drivers)
- [Implementing a Custom Driver](#implementing-a-custom-driver)
- [Registering the Custom Driver](#registering-the-custom-driver)
- [Testing](#testing)
- [Using FakeBroadcastManager](#using-fakebroadcastmanager)
- [Assertion Helpers](#assertion-helpers)
- [Connection](#connection)
- [Connection Lifecycle](#connection-lifecycle)
- [Reconnection and Heartbeat](#reconnection-and-heartbeat)
- [Deduplication](#deduplication)
## Introduction
Magic provides a Laravel Echo-equivalent broadcasting system that lets your Flutter app receive real-time events over WebSocket connections. The `Echo` facade mirrors the Laravel Echo JavaScript client API — if you know how to use Laravel Echo, you already know how to use this.
The broadcasting system is:
- **Pusher-compatible**: Works with Laravel Reverb, Soketi, and any Pusher-protocol server out of the box.
- **Resilient**: Automatic reconnection with exponential backoff, application-level heartbeat, and event deduplication.
- **Extensible**: Register custom drivers via `BroadcastManager.extend()`.
- **Testable**: `Echo.fake()` swaps the real driver for an in-memory fake with assertion helpers.
The `BroadcastServiceProvider` is **not** auto-registered. You must add it explicitly to your providers list.
## Configuration
Copy `defaultBroadcastingConfig` into your application config and register `BroadcastServiceProvider`:
```dart
// lib/config/broadcasting.dart
import 'package:magic/magic.dart';
final Map broadcastingConfig = {
'broadcasting': {
'default': Env.get('BROADCAST_CONNECTION', 'null'),
'connections': {
'reverb': {
'driver': 'reverb',
'host': Env.get('REVERB_HOST', 'localhost'),
'port': int.parse(Env.get('REVERB_PORT', '8080')!),
'scheme': Env.get('REVERB_SCHEME', 'ws'),
'app_key': Env.get('REVERB_APP_KEY', ''),
'auth_endpoint': '/broadcasting/auth',
'reconnect': true,
'max_reconnect_delay': 30000,
'activity_timeout': 120,
'dedup_buffer_size': 100,
},
'null': {'driver': 'null'},
},
},
};
```
Register the provider in `Magic.init()`:
```dart
await Magic.init(
configFactories: [
() => appConfig,
() => broadcastingConfig,
],
configs: [
{
'app': {
'providers': [
(app) => BroadcastServiceProvider(app),
],
},
},
],
);
```
### Connection Options
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `driver` | `String` | — | Driver name (`reverb`, `null`, or custom) |
| `host` | `String` | `'localhost'` | WebSocket server hostname |
| `port` | `int` | `8080` | WebSocket server port |
| `scheme` | `String` | `'ws'` | Connection scheme (`ws` or `wss`) |
| `app_key` | `String` | `''` | Reverb/Pusher application key |
| `auth_endpoint` | `String` | `'/broadcasting/auth'` | HTTP endpoint for private/presence channel auth |
| `reconnect` | `bool` | `true` | Whether to auto-reconnect on unexpected disconnect |
| `max_reconnect_delay` | `int` | `30000` | Maximum backoff delay in milliseconds |
| `activity_timeout` | `int` | `120` | Seconds before a heartbeat ping is expected |
| `dedup_buffer_size` | `int` | `100` | Number of recent event fingerprints kept for deduplication |
### Environment Variables
```dotenv
BROADCAST_CONNECTION=reverb
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=ws
REVERB_APP_KEY=your-app-key
```
## Echo Facade
The `Echo` facade provides static access to the broadcasting system, proxying all calls to the bound `BroadcastManager`.
### API Reference
| Method / Property | Returns | Description |
|:------------------|:--------|:------------|
| `Echo.channel(name)` | `BroadcastChannel` | Subscribe to a public channel |
| `Echo.private(name)` | `BroadcastChannel` | Subscribe to a private channel (auth required) |
| `Echo.join(name)` | `BroadcastPresenceChannel` | Join a presence channel (auth + member tracking) |
| `Echo.listen(channel, event, callback)` | `BroadcastChannel` | Shorthand: subscribe + listen in one call |
| `Echo.leave(name)` | `void` | Unsubscribe from a channel |
| `Echo.connect()` | `Future` | Establish the WebSocket connection |
| `Echo.disconnect()` | `Future` | Close the connection and release resources |
| `Echo.connection` | `BroadcastDriver` | The resolved default driver instance |
| `Echo.socketId` | `String?` | Server-assigned socket identifier, or `null` when disconnected |
| `Echo.connectionState` | `Stream` | Stream of connection lifecycle state changes |
| `Echo.onReconnect` | `Stream` | Emits once each time the driver successfully reconnects |
| `Echo.addInterceptor(interceptor)` | `void` | Register an interceptor on the default connection |
| `Echo.manager` | `BroadcastManager` | The underlying manager (for `extend()` and advanced use) |
| `Echo.fake()` | `FakeBroadcastManager` | Swap to in-memory fake for testing |
| `Echo.unfake()` | `void` | Restore the real manager binding |
## Channels
### Public Channels
Public channels require no authentication. Any connected client may subscribe.
```dart
// Subscribe and listen for a specific event
Echo.channel('orders').listen('OrderShipped', (event) {
final orderId = event.data['id'];
print('Order $orderId has shipped!');
});
// Fluent chaining for multiple events on the same channel
Echo.channel('orders')
.listen('OrderShipped', onShipped)
.listen('OrderCancelled', onCancelled);
// Stop listening to a specific event
Echo.channel('orders').stopListening('OrderShipped');
// Leave the channel entirely
Echo.leave('orders');
```
The `BroadcastEvent` envelope provides full context for each received message:
| Property | Type | Description |
|:---------|:-----|:------------|
| `event` | `String` | Event name (e.g. `'App\\Events\\OrderShipped'`) |
| `channel` | `String` | Channel name the event arrived on |
| `data` | `Map` | Decoded JSON payload |
| `receivedAt` | `DateTime` | Local timestamp of receipt |
### Private Channels
Private channels perform an HTTP auth handshake at `auth_endpoint` before subscribing. The driver sends the `socket_id` and `channel_name` to your server, which validates the request and returns an auth token.
```dart
// Subscribe to a private channel (driver adds 'private-' prefix automatically)
Echo.private('user.${Auth.user()!.id}')
.listen('ProfileUpdated', (event) {
print('Profile updated: ${event.data}');
});
// Convenience shorthand
Echo.listen('user.1', 'ProfileUpdated', (event) {
print(event.data);
});
```
On the Laravel server side, the channel authorization lives in `routes/channels.php`:
```php
Broadcast::channel('user.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
```
### Presence Channels
Presence channels extend private channels with real-time member tracking. The server returns member data on subscription success, and the driver emits `onJoin`/`onLeave` streams as membership changes.
```dart
final channel = Echo.join('room.1');
// Members currently in the channel
print('Online: ${channel.members.length}');
// React to member join/leave
channel.onJoin.listen((member) {
print('${member['name']} joined the room');
});
channel.onLeave.listen((member) {
print('${member['name']} left the room');
});
// Presence channels also support event listening
channel.listen('MessagePosted', (event) {
print('New message: ${event.data['body']}');
});
```
`BroadcastPresenceChannel` API:
| Property | Type | Description |
|:---------|:-----|:------------|
| `members` | `List