WebSockets
app.ws() registers a WebSocket route, served by µWS itself. The upgrade never reaches node, so server.on("upgrade") and the libraries built on it have nothing to hear; this is the replacement.
app.ws("/room/:id", {
upgrade(req, res) {
// runs before the handshake, with a real request and response.
// Answering the response declines the socket:
if (!req.query.token) return res.sendStatus(401);
// and anything left on the request is there for the socket's whole life:
req.room = req.params.id;
},
open(ws) {
ws.subscribe(ws.req.room);
},
message(ws, message, isBinary) {
ws.publish(ws.req.room, message, isBinary);
},
close(ws, code, message) {}
});- The behavior object is µWS's, settings included:
maxPayloadLength,idleTimeout,compression,maxBackpressure,sendPingsAutomaticallyand the rest are passed through untouched, as are theopen,message,drain,close,ping,pong,droppedandsubscriptionhandlers. The socket is µWS's too, sosend,subscribe,publish,corkandgetBufferedAmountbehave exactly as its documentation describes. upgrade(req, res)is this project's addition. It runs before the handshake with the sameRequestandResponseyour routes get, so a session, a token or a header decides whether the socket opens. Answering the response, withres.sendStatus(401)or any other write, declines the upgrade. Returning a promise holds the handshake until it settles, which is what an authentication lookup needs.ws.reqis that request, and it outlives the response: the client's address, headers, query and params are readable from any handler for as long as the socket is open. Hanging your own values on it inupgradeis how per-connection state gets tomessage.- A hook that awaits can be left holding a dead request. The client may go while a token is being checked, and µWS frees the response when it does, so
res.abortedsays whether there is still anybody to answer. Writing to a response that was aborted does nothing rather than throwing. - Routers work.
router.ws("/lobby", ...)mounted withapp.use("/chat", router)serves/chat/lobby. - Paths are the ones µWS matches: literal, or with parameters that are a whole segment such as
/room/:id. Anything else throws where it is written rather than failing to match later. - Broadcasting from outside a socket:
app.publish(topic, message)andapp.numSubscribers(topic).
A WebSocket route and an ordinary route can share a path: the upgrade goes to the WebSocket route, a plain GET goes through normal routing. Runnable, with a page that opens the socket: examples/websocket.js.
If you would rather use the ws module's API, Ultimate WS is a drop-in replacement for it written against Ultimate Express, and Fulmine still exposes the mechanism it hooks into, but that combination is not covered by this project's tests. app.uwsApp also remains available for anything µWS offers that this does not.
socket.io
socket.io normally takes over the upgrade on a node http.Server. The upgrade here never reaches node, so hand it the µWS app instead, which socket.io supports natively through attachApp():
const express = require("fulmine.js");
const { Server } = require("socket.io");
const app = express();
const io = new Server();
app.listen(3000);
io.attachApp(app.uwsApp);
io.on("connection", (socket) => {
socket.on("message", (data) => socket.emit("reply", data));
});attachApp() works before or after app.listen(). What does not work is new Server(app) on the app itself, or on what app.listen() returns, which is the same object: socket.io refuses it with "You are trying to attach socket.io to an express request handler function", because it checks for a function before it checks for a server, and an app here is callable. That refusal is the useful answer. Even if it accepted the object, there is no node socket behind it to take an upgrade over, so it would have failed later and more quietly. Plain HTTP keeps serving either way. This is covered by tests/tests/middlewares/socket-io.js, which runs the same file against Express and against Fulmine and compares the output. Runnable: examples/socket-io.js.