-
Notifications
You must be signed in to change notification settings - Fork 0
Broadcasting & WebSocket support (Laravel Echo equivalent) #37
Description
Problem
Magic framework has no broadcasting/WebSocket abstraction. Apps must reimplement the entire Pusher-compatible WebSocket client from scratch — connection management, channel subscriptions, private channel authentication, event parsing, reconnection logic, deduplication, and heartbeat.
This is a core piece of the Laravel ecosystem that's missing on the Flutter side. Laravel has Broadcasting + Echo; magic should have its equivalent.
Current State (Kodizm app-level implementation)
We built a full Pusher-compatible client at the app level (~350 lines). Here's what it handles:
Connection Lifecycle
- WebSocket connect to
ws://{host}/app/{key}(Reverb/Pusher protocol) - Ping/pong heartbeat from server's
activity_timeout - Exponential backoff reconnect (2^attempt, capped 30s)
- Connection state tracking (connecting, connected, disconnected, reconnecting)
onReconnectstream for catch-up after reconnection
Channel Management
subscribe(channel)/unsubscribe(channel)- Private channel auth via HTTP POST to
/broadcasting/auth(sendssocket_id+channel_name, receives auth signature) - Presence channel support (not yet implemented but protocol supports it)
- Subscription queuing during connection
Event Handling
- Pusher envelope parsing:
{"event":"App\\Events\\Foo","channel":"private-x","data":"{...}"} - Double-JSON decode (Reverb encodes payload as JSON string inside JSON)
- Per-channel event listeners with
Stream<WebSocketEvent> - Event deduplication via ring buffer (channel:event:data hash, max 100)
What we have to wire manually every time
// Service registration
Magic.app.singleton('websocket', () => WebSocketService());
// Boot — connect after auth
final ws = Magic.make<WebSocketService>('websocket');
await ws.connect(
host: Config.get('websocket.host'),
port: Config.get('websocket.port'),
appKey: Config.get('websocket.app_key'),
authEndpoint: Config.get('websocket.auth_endpoint'),
authHeaders: {'Authorization': 'Bearer ${Auth.token()}'},
);
// Subscribe in state classes
final stream = ws.channel('private-conversation.$id');
stream.listen((event) => _handleEvent(event));Proposed Architecture
1. Config (config/broadcasting.dart)
Mirror Laravel's broadcasting config:
Map<String, dynamic> get broadcastingConfig => {
'broadcasting': {
'default': 'reverb',
'connections': {
'reverb': {
'driver': 'reverb',
'host': env('REVERB_HOST', 'localhost'),
'port': int.tryParse(env('REVERB_PORT', '8080')) ?? 8080,
'scheme': env('REVERB_SCHEME', 'ws'),
'app_key': env('REVERB_APP_KEY', ''),
'auth_endpoint': '/broadcasting/auth',
'reconnect': true,
'max_reconnect_attempts': null, // infinite
'activity_timeout': 30,
},
'pusher': {
'driver': 'pusher',
'key': env('PUSHER_APP_KEY', ''),
'cluster': env('PUSHER_APP_CLUSTER', 'mt1'),
'encrypted': true,
},
'null': {
'driver': 'null',
},
},
},
};2. Facades
// Echo facade — primary API (mirrors Laravel Echo)
Echo.channel('team.5'); // public channel
Echo.private('conversation.uuid'); // private channel
Echo.join('session.uuid'); // presence channel
Echo.listen('conversation.uuid', '.message', (event) => ...);
Echo.leave('conversation.uuid');
Echo.disconnect();
// Low-level access
Echo.connection; // BroadcastConnection instance
Echo.socketId; // for excluding self from broadcasts3. Contracts
/// Connection driver contract
abstract class BroadcastDriver {
Future<void> connect();
Future<void> disconnect();
String? get socketId;
bool get isConnected;
Stream<BroadcastConnectionState> get connectionState;
Stream<void> get onReconnect;
BroadcastChannel channel(String name);
BroadcastChannel private(String name);
BroadcastPresenceChannel join(String name);
void leave(String name);
}
/// Channel contract
abstract class BroadcastChannel {
String get name;
Stream<BroadcastEvent> get events;
BroadcastChannel listen(String event, void Function(BroadcastEvent) callback);
void stopListening(String event);
}
/// Presence channel extends with member tracking
abstract class BroadcastPresenceChannel extends BroadcastChannel {
List<Map<String, dynamic>> get members;
Stream<Map<String, dynamic>> get onJoin;
Stream<Map<String, dynamic>> get onLeave;
}
/// Parsed event
class BroadcastEvent {
final String event;
final String channel;
final Map<String, dynamic> data;
final DateTime receivedAt;
}4. Drivers
| Driver | Use Case |
|---|---|
ReverbDriver |
Laravel Reverb (Pusher-compatible WebSocket) |
PusherDriver |
Pusher Channels (cloud) |
NullDriver |
Local dev / testing (no-op) |
5. BroadcastManager
Same pattern as LogManager — resolves driver from config, caches connection:
class BroadcastManager {
static final Map<String, BroadcastDriver Function(Map<String, dynamic>)> _customDrivers = {};
/// Register a custom driver factory (same as LogManager.extend from #33).
static void extend(String name, BroadcastDriver Function(Map<String, dynamic>) factory) {
_customDrivers[name] = factory;
}
BroadcastDriver connection([String? name]) { ... }
}6. Auth Integration
Private/presence channel auth should automatically use Magic's Auth.token():
// Internal — BroadcastManager handles this
Future<Map<String, String>> _authenticate(String channel, String socketId) async {
final response = await Http.post(authEndpoint, data: {
'socket_id': socketId,
'channel_name': channel,
});
return response.data;
}7. Service Provider Lifecycle
// register() — bind BroadcastManager singleton
Magic.app.singleton('broadcasting', () => BroadcastManager());
// boot() — auto-connect if auth'd and driver configured
if (Auth.check() && Config.get('broadcasting.default') != 'null') {
await Echo.connect();
}8. Built-in Resilience (from our production learnings)
These should be framework-level, not app-level:
- Exponential backoff reconnect with configurable cap
- Event deduplication (Reverb sometimes double-delivers)
- Subscription queue during reconnection (auto-resubscribe on reconnect)
- Heartbeat/ping-pong from server's
activity_timeout - Connection state stream for UI indicators
- onReconnect stream for app-level catch-up logic
Prior Art
| Framework | Flutter Equivalent |
|---|---|
| Laravel Broadcasting | This proposal |
| Laravel Echo (JS) | Echo facade |
| Pusher Channels Flutter | pusher_channels_flutter (heavy, native deps) |
| Laravel Reverb | Server-side, no client changes needed |
Related Issues
- feat: add observer support to MagicRouter for analytics/monitoring #31 — GoRouter navigation observer hook
- feat: add plugin hook for network driver customization #32 — Dio interceptor/plugin hook
- Support custom LoggerDriver registration in LogManager #33 — Custom LoggerDriver registration
All four issues (#31–#34) together complete the "Laravel parity" story for magic framework — routing observers, HTTP plugins, logging drivers, and now real-time broadcasting.