Connection state
The state machine, the reconnect policy, and how to tell a flaky network apart from a refused credential.
The states
initialized → connecting → connected
│ │
▼ ▼
failed disconnected → (backoff) → connecting
│
▼
closedrealtime.connection.on('state', (state, reason) => {
console.log(state, reason?.message);
});
realtime.connection.off('state', listener);
realtime.connection.state; // current
realtime.connection.errorReason; // BlackevinError | nullreason is a second argument, so a listener written with one parameter keeps
working.
What moves it
| event | state |
|---|---|
connect() | connecting |
Connected frame | connected — restores attaches and subscriptions, clears errorReason |
| socket closed, not by you | disconnected, then reconnect with backoff |
close() | closed, no reconnect |
| auth Nack 401 / 403 | failed — stops retrying, rejects the queue |
| quota Nack 429 | disconnected — keeps retrying, sets errorReason |
Reconnect is exponential: disconnectedRetryTimeout (1s) doubled per attempt,
capped at 30s. Dynamic credentials (authCallback, authUrl) are cleared
before each attempt so a fresh token is fetched; a static key or token is
kept.
errorReason
Why the connection is not up, when the server said why. A BlackevinError with a
statusCode, an optional reason slug, and the server's own message.
It exists because a refused handshake used to reach nobody. The server sends a
Nack on id: "auth", nothing is awaiting that id, and then the socket closes —
leaving the client disconnected, which looks exactly like a flaky network. An
application hitting its plan's connection ceiling could not tell its user
anything true.
realtime.connection.on('state', (state, reason) => {
if (reason?.reason === 'connection_limit') {
showUpgradePrompt(reason.message);
}
});Branch on reason.reason, never on reason.message. The slugs are a contract
and are never renamed; the messages are prose and will be reworded.
A 429 is not terminal
A bad credential will never work, so a 401 or 403 sends the client to failed:
it stops retrying and rejects everything queued. Retrying a wrong key forever
only produces load.
A connection ceiling is the opposite. A slot frees the moment another client disconnects, and an upgrade takes effect on the next handshake. So a 429 keeps retrying, and queued publishes are held rather than rejected — throwing them away would discard exactly the work the offline queue exists to keep.
What it must not do is retry silently, which is what errorReason fixes.
Closing
realtime.close();closed is final. autoReconnect is switched off permanently, anything queued
is rejected, and the client will not come back until you call connect()
yourself.
That distinction matters on logout: dropping the token is not enough if the client can still reconnect with a cached one. Close it.
Forcing a client off
A backend can drop every live session for a clientId under its account:
POST /api/clients/{clientId}/connections/close{ "closed": 2 }closed counts the sockets on the node that answered; peers close their own
independently and are not summed.
This is a revoke for stuck or zombie connections — a browser whose
beforeunload never reached the server. It is not exclusive-login: a client
that still holds a valid token and has autoReconnect on will simply come back.
A logout flow needs to invalidate the token too.
Only connections with a bound clientId are targets, which in practice means
connections authenticated by a JWT carrying x-blackevin-client-id. A plain API-key
connection has no client identity to match on.