darto_ws 1.1.0
darto_ws: ^1.1.0 copied to clipboard
WebSocket plugin for Darto — route-integrated, same port, Hono-style. Rooms, broadcast and Redis pub/sub for multi-instance fanout.
darto_ws #
WebSocket support for Darto — route-integrated, same port, Hono-style.
WebSocket routes live alongside HTTP routes on the same server and port. Middleware runs before the upgrade, so auth, param extraction, and state set via c.set() are all available inside the callbacks.
Install #
dependencies:
darto: ^1.0.0
darto_ws: ^1.0.0
Quick start #
import 'package:darto/darto.dart';
import 'package:darto_ws/darto_ws.dart';
void main() {
final app = Darto();
app.get('/ws', [], upgradeWebSocket((c) => WSHandler(
onOpen: (ws) => ws.send('connected'),
onMessage: (event, ws) => ws.send('echo: ${event.text}'),
onClose: () => print('client disconnected'),
)));
app.listen(3000);
}
Connect from a browser:
const ws = new WebSocket('ws://localhost:3000/ws');
ws.onmessage = (e) => console.log(e.data);
ws.send('hello');
upgradeWebSocket(factory) #
Returns a Handler that upgrades the HTTP request to a WebSocket.
The factory receives the full Context — path params, headers, and any middleware state — before the upgrade happens.
Pass route-level middlewares in the second argument as usual:
// Auth middleware runs before the WebSocket upgrade
app.get('/ws', [bearerAuth(token: env.token)], upgradeWebSocket((c) => WSHandler(
onOpen: (ws) => ws.send('authenticated'),
)));
Path params and middleware state #
app.get('/chat/:room', [jwt(secret: env.secret)], upgradeWebSocket((c) {
final room = c.req.param('room')!; // path param
final userId = c.get<String>('userId'); // set by auth middleware
return WSHandler(
onOpen: (ws) => ws.send('$userId joined $room'),
onMessage: (event, ws) => ws.send('[$room] ${event.text}'),
);
}));
WSHandler #
All callbacks are optional.
| Callback | Signature | When called |
|---|---|---|
onOpen |
(DartoWebSocket ws) |
Handshake complete |
onMessage |
(WSEvent event, DartoWebSocket ws) |
Message received |
onClose |
(DartoWebSocket ws) |
Connection closed (read ws.id / ws.rooms here) |
onError |
(Object error, DartoWebSocket ws) |
Protocol error |
Breaking in 1.1.0:
onCloseandonErrornow receive the closing socket. Old callers likeonClose: () => …becomeonClose: (_) => ….
Rooms and broadcast — WsHub #
A WsHub is the connection registry — track sockets, group them in rooms and
fan messages out from anywhere. Install one per app via middleware so every
upgradeWebSocket factory picks it up automatically.
import 'package:darto/darto.dart';
import 'package:darto_ws/darto_ws.dart';
void main() {
final hub = WsHub();
final app = Darto()..use(hub.middleware());
app.get('/chat/:room', [], upgradeWebSocket((c) {
final room = c.req.param('room')!;
return WSHandler(
onOpen: (ws) {
ws.join(room);
ws.to(room).except(ws).send('${ws.id} joined');
},
onMessage: (event, ws) =>
ws.to(room).sendJson({'from': ws.id, 'text': event.text}),
onClose: (ws) {
// ws.leave(room) is automatic on close
},
);
}));
// Server-initiated broadcast — works from any HTTP route, cron, etc.
app.post('/announce', [], (c) async {
final body = await c.req.json();
hub.to('lobby').sendJson(body);
return c.noContent();
});
app.listen(3000);
}
| Member | Description |
|---|---|
hub.middleware() |
Darto middleware — exposes the hub to factories via wsHub(c) |
hub.to(room) / hub.broadcast() |
Fluent fanout — chain .except(ws), then send / sendJson / sendBytes |
hub.connections / roomSize / rooms |
Membership stats |
ws.id / ws.rooms |
Per-connection UUID + the rooms it is in |
ws.join(room) / ws.leave(room) |
Mutate room membership (auto-leaves all rooms on close) |
ws.to(room) / ws.broadcast() |
Shortcuts to the hub — same as hub.to(...) |
Fanout patterns #
// Send to everyone in the room, including the sender
ws.to(room).send('hello everyone');
// Send to everyone in the room, except the sender
ws.to(room).except(ws).send('someone else joined');
// Send to ALL connected sockets across every room, including the sender
ws.broadcast().send('server announcement');
// Send to ALL connected sockets, except the sender
ws.broadcast().except(ws).send('someone connected');
The same methods are available on hub from outside a WS callback (e.g. an HTTP route):
hub.to('lobby').send('hello lobby');
hub.broadcast().sendJson({'event': 'shutdown'});
Multi-instance fanout — RedisWsAdapter #
When you run multiple replicas behind a load balancer, sockets connected to
one instance can't see broadcasts emitted by another. The Redis adapter
solves it: each to(room).send(...) is published to a Redis pub/sub channel,
peers re-fanout to their local sockets, and an origin id suppresses
self-echo.
final hub = WsHub();
await hub.attachAdapter(await RedisWsAdapter.connect(
host: 'localhost',
port: 6379,
));
final app = Darto()..use(hub.middleware());
// … routes as above; broadcasts now cross instances.
Run the adapter tests with:
dart test --tags redis
(requires Docker — the suite boots a disposable redis:latest on a random
host port.)
DartoWebSocket #
| Method | Description |
|---|---|
send(String) |
Send a text frame to this client |
sendJson(Map) |
Encode as JSON and send to this client |
sendBytes(List<int>) |
Send a binary frame to this client |
close([code, reason]) |
Close the connection |
closeCode |
Close code from the peer (null while open) |
id |
Unique per-connection UUID v4 |
rooms |
Rooms this socket is in (mutable set) |
join(room) / leave(room) |
Mutate room membership |
to(room) / broadcast() |
Hub fanout helpers (require hub.middleware()) |
WSEvent #
| Member | Type | Description |
|---|---|---|
data |
dynamic |
Raw frame data (String or List<int>) |
text |
String |
UTF-8 decoded text |
json |
Map<String, dynamic> |
JSON-decoded object |
JSON round-trip #
app.get('/ws/json', [], upgradeWebSocket((c) => WSHandler(
onMessage: (event, ws) {
final payload = event.json; // Map<String, dynamic>
ws.sendJson({'echo': payload});
},
)));
See also #
- darto — core framework
- examples/example_websocket — working example
Support 💖 #
If you find Darto WS useful, please consider supporting its development 🌟Buy Me a Coffee.🌟 Your support helps us improve the package and make it even better!