STOMP messaging over WebSockets¶
Diana speaks STOMP 1.2 on top of its WebSocket support:
an in-memory topic broker, @MessageMapping handlers for inbound messages,
and an injectable publisher for server-side sends. It is the Spring
SimpMessagingTemplate / simple-broker model, built into package:diana
(no extra dependency).
The endpoint¶
A STOMP endpoint is a singleton @WebSocketController extending
StompEndpoint. It delegates every WebSocket callback to the framework's
broker, so it inherits all the WebSocket wiring — including handshake
authentication:
@WebSocketController('/stomp')
@UseMiddleware([AppAuth]) // authenticate the handshake
@Secured() // anonymous upgrades get 401
class ChatGateway extends StompEndpoint {
ChatGateway(super.broker);
}
Clients connect a WebSocket to /stomp and speak STOMP frames over it. The
authenticated principal (from the handshake) is available to handlers.
Handling messages: @MessageMapping / @SendTo¶
A client SENDs to a destination under the app prefix (stomp.appPrefix,
default /app). @MessageMapping('/chat') handles SENDs to /app/chat:
@Service()
class ChatMessaging {
@MessageMapping('/chat') // client SENDs to /app/chat
@SendTo('/topic/chat') // the return value is published here
ChatOut chat(ChatIn payload, StompMessage message) {
final from = message.principal is AuthPrincipal
? (message.principal as AuthPrincipal).name
: 'anonymous';
return ChatOut(from: from, text: payload.text);
}
}
- Payload: the first parameter is either a
String(the raw body) or an@Jsonclass (decoded from the JSON body). - Context: an optional second
StompMessageparameter carries the destination, headers, body, session id and principal. - Return / @SendTo: a non-void return (a
Stringor an@Jsonclass, optionally wrapped inFuture) is published to the@SendTodestination. A void handler consumes the message without replying. Returning a value without@SendTo— or@SendToon a void method — is a build error.
Handlers must live on singleton providers; every rule is checked at build time with an exact message.
Publishing from server code¶
Inject StompPublisher anywhere to push to a destination — every
subscriber, on any isolate, receives a MESSAGE:
@Service()
class Alerts {
Alerts(this.stomp);
final StompPublisher stomp;
Future<void> broadcast(String text) =>
stomp.publish('/topic/alerts', text, contentType: 'text/plain');
}
Subscriptions and acknowledgements¶
Clients SUBSCRIBE with an id, an ack mode (auto — the default — /
client / client-individual) and a destination (exact match, no
wildcards). In client/client-individual mode each MESSAGE carries an
ack header; the client ACKs (or NACKs) to release it:
client— an ACK is cumulative: it releases that message and every earlier un-acked one in the subscription.client-individual— an ACK releases only that message.
NACK drops the message and logs it — there is no redelivery. The broker is in-memory with no persistent queue, so NACK is spec-legal but purely informational.
Transactions¶
BEGIN/COMMIT/ABORT buffer SENDs and ACKs per transaction: COMMIT
applies them in arrival order, ABORT discards them. (App-destined SENDs
inside a transaction are dispatched on commit too.)
Heartbeats¶
The client's CONNECT heart-beat:cx,cy negotiates against the server's
stomp.heartbeatMs (default 10000) per direction (max of the two, 0
disables). The server sends a bare LF at the outgoing interval and closes a
connection whose client goes silent for twice the incoming interval.
Configuration¶
| Key | Default | Meaning |
|---|---|---|
stomp.appPrefix |
/app |
Destinations under this prefix route to @MessageMapping handlers |
stomp.heartbeatMs |
10000 |
Server heartbeat interval offered at CONNECT (both directions) |
Cross-isolate delivery¶
With server.isolates > 1, a publish to a topic reaches subscribers on
every isolate exactly once: the broker relays through the same
event-bus broadcast machinery Diana uses for application
events (hub-and-spoke, drained on shutdown).
Limitations¶
- No
Sec-WebSocket-Protocolnegotiation. The underlying relic upgrade does not echo thev12.stompsubprotocol; lenient clients (STOMP.js over a raw WebSocket) tolerate this, strict ones may not. - Exact-match destinations — no wildcards or destination hierarchies (the Spring simple broker's model).
login/passcodeon CONNECT are ignored — authenticate on the handshake with@UseMiddleware/@Securedinstead.- A SEND to an unmapped
/app/...destination is dropped and logged (Spring's behavior). - Pending (un-acked) messages are held per connection with no upper bound —
a client that never ACKs in
clientmode grows the set until it disconnects.