Connection
How the Reverb driver connects, reconnects, authorises channels and detects a dead socket, and which settings change that.
On this page
Lifecycle#
BroadcastServiceProvider.boot() starts the default connection and does not wait for it, so a server that is down never fails the app launch. The failure is logged as a warning and the driver keeps retrying. Code that needs the socket awaits Echo.connect(), which is idempotent: it returns at once when connected, joins an attempt already in flight, and when a retry is armed it cancels the timer and reconnects now. It never opens a second socket.
import 'package:magic_socket/magic_socket.dart';
Future<void> main() async {
await Echo.connect();
print(Echo.socketId);
await Echo.disconnect();
}
Connection State#
Echo.connectionState is a broadcast stream of BroadcastConnectionState.
| State | Meaning |
|---|---|
connecting |
Establishing the connection. |
connected |
Active and healthy. |
disconnected |
Not connected and not retrying. |
reconnecting |
Lost the connection and re-establishing it. |
import 'package:magic_socket/magic_socket.dart';
void main() {
Echo.connectionState.listen((state) {
switch (state) {
case BroadcastConnectionState.connected:
print('online');
case BroadcastConnectionState.reconnecting:
print('reconnecting');
case BroadcastConnectionState.disconnected:
print('offline');
case BroadcastConnectionState.connecting:
break;
}
});
}
Resubscription#
Every handshake subscribes every channel the app holds, public, private and presence, with the new socket id, as pusher-js does. That covers a timer retry, an explicit Echo.connect() after a drop and the first connect. A channel created while the socket is down is subscribed once, by the next handshake. Private and presence channels authorise again, because the signature covers the socket id.
Echo.onReconnect emits after each reconnect handshake, once every channel was sent its subscribe, and never for the first connection. It does not mean the server confirmed anything. Refetch on the channel's own confirmation instead:
import 'package:magic_socket/magic_socket.dart';
void main() {
Echo.private('teams.1').onSubscriptionChanged.where((subscribed) => subscribed).listen((_) {
print('events flow again; fetch what was missed');
});
}
Auth Failures#
The default auth call throws a BroadcastAuthException when the auth_endpoint answers outside 2xx.
| Property | Meaning |
|---|---|
channelName |
The fully-qualified channel the call was for. |
statusCode |
The HTTP status, or 0 when no response arrived. |
isRetryable |
true for status 0 and any 5xx. |
A retryable failure is retried with the reconnect backoff while the socket lasts and logged as a warning. A 401 or 403 is logged as an error and not retried, because retrying cannot change the answer. Every failure goes through the interceptor onError chain, and one channel's failure does not block the rest. An answer that arrives after the socket changed, or after the channel was left, is dropped.
Reconnect Backoff#
base = min(500 ms * 2^attempt, max_reconnect_delay)
delay = base + random(0 .. base * 0.3)
The base starts at 500 ms and doubles per attempt up to max_reconnect_delay (30 000 ms by default). The jitter, up to 30% of the base, keeps clients from reconnecting in the same instant after a server restart. Set reconnect to false to disable automatic reconnects; a later Echo.connect() still opens a fresh socket.
Close Codes#
The websocket close code decides the strategy, following the Pusher protocol.
| Close code | Action |
|---|---|
| 4000 to 4099 | Fatal: no reconnect, the state goes to disconnected. |
| 4100 to 4199 | Reconnect with backoff. |
| 4200 to 4299 | Reconnect at once, no backoff. |
| Any other, or none | Reconnect with backoff. |
A close without a code is the common case: a restarting Reverb drops connections without a close frame. A pusher:error frame on an open socket never reconnects; it only reaches the interceptor onError chain, because Reverb sends one for any server-side exception and keeps the socket open.
Activity Monitor#
The driver detects silent connection loss, such as a crashed server or a network partition, without a close frame.
- After the handshake it reads
activity_timeoutfrom the server payload, falling back to the config key (120 seconds). - If no message arrives within that time, it sends
pusher:ping. - If no
pusher:pongarrives within 30 seconds, the connection is declared dead and dropped as a close without a code: the state becomesreconnecting, every channel reports unsubscribed and the backoff starts. - Any inbound message resets the timer, not only a pong.
The driver also answers a server pusher:ping with pusher:pong by itself.
Connection Timeout#
connection_timeout (15 seconds by default) is how long the driver waits for pusher:connection_established after the websocket upgrades. On expiry the socket is closed, a reconnect is scheduled (subject to reconnect) and Echo.connect() throws a TimeoutException, so a caller can show an error state.
Perf Events#
BroadcastListeners reports each received message to core's perf seam. When MagicPerfHooks.sink is set, every fanned-out event emits a BroadcastReceived(event) and its handlers run under the MagicNotifyCause.broadcast root cause, so a diagnostic tool can attribute rebuilds to a realtime message. Both types live in package:magic/magic.dart; with no sink installed the check is a single null test and nothing is allocated.
Related Documentation#
- Configuration: the keys named here.
- Channels: subscription state per channel.
- Listeners:
AuthChannelSubscriptionandonReconnect. - Extending: interceptors see every error.