WebSocket
WebSocket allows the server and browser to maintain a persistent, two-way connection. Unlike regular HTTP where the browser always initiates requests, WebSocket lets the server push messages to clients at any time.
Common use cases: real-time notifications, live chat, dashboards that update automatically, multiplayer games.
Finch's WebSocket support is built around three classes:
SocketManager— manages all active WebSocket connections and dispatches messages to the correct route handler.SocketEvent— defines callbacks foronConnect,onMessage,onDisconnect, andonErroron a specific path.SocketClient— represents one connected client; passed into every callback, it's what you call.send()on.
A regular Finch Controller is what upgrades the incoming HTTP request to a WebSocket connection — there is no separate SocketController base class to extend.
Setup
1. Define a SocketManager
Create the SocketManager in app.dart. It takes the app instance, a root SocketEvent (for lifecycle events), and a map of named route handlers:
final socketManager = SocketManager(
app,
event: SocketEvent(
onConnect: (socket) {
// Called when a client connects
// Notify all other clients about the new connection
app.socketManager?.sendToAll(
'A user connected. Total: ${app.socketManager?.countClients}',
path: 'output',
);
// Send a confirmation to the newly connected client
socket.send(
{'message': 'Successfully connected to socket!'},
path: 'connected',
);
},
onMessage: (socket, data) {
// Called for messages not matched by any named route
},
onDisconnect: (socket) {
// Called when a client disconnects
var count = app.socketManager?.countClients ?? 0;
app.socketManager?.sendToAll(
'A user disconnected. Total: ${count - 1}',
path: 'output',
);
},
onError: (socket, data) {
// Called when the incoming message can't be decoded as JSON,
// or another error occurs while handling it
},
),
routes: _getSocketRoutes(),
);
Only the root event gets onConnect/onDisconnect/onError calls — see the note under "Define Named Routes" below.
2. Define Named Routes
Socket routes are a Map<String, SocketEvent>. Each key is a "path" (a logical channel name). When a client sends a message to that path, the corresponding onMessage callback fires:
Map<String, SocketEvent> _getSocketRoutes() {
return {
// Client sends to path 'test' — server replies with request headers
'test': SocketEvent(
onMessage: (socket, data) {
socket.send([socket.rq.headers], path: 'test');
},
),
// Client sends to path 'time' — server replies with current time
'time': SocketEvent(
onMessage: (socket, data) {
socket.send(DateTime.now().toString(), path: 'output');
},
),
};
}
Only
onMessageis used from a named route'sSocketEvent—onConnect/onDisconnect/onErrorset on a route entry are never called; those only fire from the rooteventpassed toSocketManager. A message whosepathdoesn't match any named route falls through to the rootevent.onMessageinstead.
3. Upgrade the Request in a Controller
The controller's job is to hand the HTTP request over to socketManager so the upgrade can happen — it's a plain Controller, nothing special:
class WebSocketController extends Controller {
Future<String> socket() async {
// Transfer the request to the SocketManager for WebSocket upgrade
await socketManager.requestHandle(rq);
return rq.renderSocket(); // returns 'Socket is requested!'
}
}
4. Register the WebSocket Route
The WebSocket route must accept Methods.ALL because the upgrade handshake uses a GET request with special headers:
FinchRoute(
key: 'root.ws',
path: '/ws',
methods: Methods.ALL,
index: webSocketController.socket,
),
Sending Messages from the Server
Every callback receives a SocketClient (referred to as socket in the examples above). It — and SocketManager itself — expose methods to send messages:
// Send to the specific client that triggered the event
socket.send(data, path: 'channelName');
// Send to all connected clients
app.socketManager?.sendToAll(data, path: 'channelName');
// Send to one specific client by connection ID
// (note the method's real name has a typo: "Clinet", not "Client")
app.socketManager?.sendToClinet(clientId, data, path: 'channelName');
// Send to every client associated with a given user ID
// (requires that userId was passed to requestHandle(rq, userId: ...) when they connected)
app.socketManager?.sendToUser(userId, data, path: 'channelName');
There is no built-in "send to all except this one" method — if you need that, filter app.socketManager?.getAllClientsKeys() yourself and call sendToClinet for each remaining ID.
The path in a send call determines which handler on the client side receives it. On the JavaScript side, listen for messages on the same path name.
Client-Side JavaScript Example
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = () => console.log('Connected');
// Listen for messages on the 'connected' path
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.path === 'connected') {
console.log('Server says:', msg.data.message);
}
if (msg.path === 'output') {
console.log('Output:', msg.data);
}
};
// Send a message to the 'time' route on the server
ws.send(JSON.stringify({ path: 'time', data: {} }));
How Many Clients Are Connected?
int count = app.socketManager?.countClients ?? 0;
int users = app.socketManager?.countUsers ?? 0; // unique users, if you pass userId on connect