Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bind routes to express-ws instance to make it easier to broadcast #122

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ app.ws('/echo', function(ws, req) {
It works with routers, too, this time at `/ws-stuff/echo`:

```javascript
var router = express.Router();
const router = express.Router();

router.ws('/echo', function(ws, req) {
ws.on('message', function(msg) {
Expand All @@ -45,9 +45,9 @@ app.use("/ws-stuff", router);
## Full example

```javascript
var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
const express = require('express');
const app = express();
const expressWs = require('express-ws')(app);

app.use(function (req, res, next) {
console.log('middleware');
Expand Down Expand Up @@ -92,7 +92,31 @@ This property contains the `app` that `express-ws` was set up on.

Returns the underlying WebSocket server/handler. You can use `wsInstance.getWss().clients` to obtain a list of all the connected WebSocket clients for this server.

Note that this list will include *all* clients, not just those for a specific route - this means that it's often *not* a good idea to use this for broadcasts, for example.
Note that this list will include *all* clients, not just those for a specific route - this means that it's often *not* a good idea to use this for broadcasts, for example. For broadcasts, use the `setRoom()` and `broadcast()` methods as shown in the [chat example](examples/chat.js):

```javascript
const express = require('express');
const expressWs = require('express-ws')(express());

const app = expressWs.app;

function roomHandler(client, request) {
client.room = this.setRoom(request);
console.log(`New client connected to ${client.room}`);

client.on('message', (message) => {
const numberOfRecipients = this.broadcast(client, message);
console.log(`${client.room} message broadcast to ${numberOfRecipients} recipient${numberOfRecipients === 1 ? '' : 's'}.`);
});
}

app.ws('/room1', roomHandler);
app.ws('/room2', roomHandler);

app.listen(3000, () => {
console.log('\nChat server running on http://localhost:3000\n\nFor Room 1, connect to http://localhost:3000/room1\nFor Room 2, connect to http://localhost:3000/room2\n');
});
```

### wsInstance.applyTo(router)

Expand All @@ -103,6 +127,10 @@ Sets up `express-ws` on the given `router` (or other Router-like object). You wi

In most cases, you won't need this at all.

### A note on route scope

Routes are bound to the wsInstance so you can access `.getWss()`, `.setRoom()`, `.broadcast()` and `.app` via `this` in your routes even if the original wsInstance is not in scope (e.g., if you have your routes defined in external files).

## Development

This module is written in ES6 and uses ESM.
22 changes: 9 additions & 13 deletions examples/broadcast.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
var express = require('express');
var expressWs = require('..')
const express = require('express');
const expressWs = require('..')(express());

var expressWs = expressWs(express());
var app = expressWs.app;
const app = expressWs.app;

app.ws('/a', function(ws, req) {
});
var aWss = expressWs.getWss('/a');
app.ws('/broadcast');
const wss = expressWs.getWss();

app.ws('/b', function(ws, req) {
});

setInterval(function () {
aWss.clients.forEach(function (client) {
setInterval(() => {
// Note that these messages will be sent to all clients.
wss.clients.forEach((client) => {
client.send('hello');
});
}, 5000);

app.listen(3000)
app.listen(3000);
21 changes: 21 additions & 0 deletions examples/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const express = require('express');
const expressWs = require('..')(express());

const app = expressWs.app;

function roomHandler(client, request) {
client.room = this.setRoom(request);
console.log(`New client connected to ${client.room}`); // eslint-disable-line no-console

client.on('message', (message) => {
const numberOfRecipients = this.broadcast(client, message);
console.log(`${client.room} message broadcast to ${numberOfRecipients} recipient${numberOfRecipients === 1 ? '' : 's'}.`); // eslint-disable-line no-console, max-len
});
}

app.ws('/room1', roomHandler);
app.ws('/room2', roomHandler);

app.listen(3000, () => {
console.log('\nChat server running on http://localhost:3000\n\nFor Room 1, connect to http://localhost:3000/room1\nFor Room 2, connect to http://localhost:3000/room2\n'); // eslint-disable-line no-console
});
39 changes: 22 additions & 17 deletions examples/https.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
var https = require('https');
var fs = require('fs');
const https = require('https');
const fs = require('fs');

var express = require('express');
var expressWs = require('..');
const express = require('express');
const expressWs = require('..');

var options = {
// Note: you will need the following two files in the examples/ folder for
// ===== this example to work. To generate locally-trusted TLS certificates,
// you can use mkcert (https://github.com/FiloSottile/mkcert) and then
// copy your certificates here (e.g., for localhost, copy localhost.pem
// to ./cert.pem and localhost-key.pem to ./key.pem.)
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
var app = express();
var server = https.createServer(options, app);
var expressWs = expressWs(app, server);
const app = express();
const server = https.createServer(options, app);
expressWs(app, server);

app.use(function (req, res, next) {
console.log('middleware');
app.use((req, res, next) => {
console.log('middleware'); // eslint-disable-line no-console
req.testing = 'testing';
return next();
});

app.get('/', function(req, res, next){
console.log('get route', req.testing);
app.get('/', (req, res) => {
console.log('get route', req.testing); // eslint-disable-line no-console
res.end();
});

app.ws('/', function(ws, req) {
ws.on('message', function(msg) {
console.log(msg);
app.ws('/', (ws, req) => {
ws.on('message', (msg) => {
console.log(msg); // eslint-disable-line no-console
});
console.log('socket', req.testing);
console.log('socket', req.testing); // eslint-disable-line no-console
});

server.listen(3000)
server.listen(3000);
23 changes: 11 additions & 12 deletions examples/params.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
var express = require('express');
var expressWs = require('..');
const express = require('express');
const expressWs = require('..')(express());

var expressWs = expressWs(express());
var app = expressWs.app;
const app = expressWs.app;

app.param('world', function (req, res, next, world) {
app.param('world', (req, res, next, world) => {
req.world = world || 'world';
return next();
});

app.get('/hello/:world', function(req, res, next){
console.log('hello', req.world);
app.get('/hello/:world', (req, res, next) => {
console.log('hello', req.world); // eslint-disable-line no-console
res.end();
next();
});

app.ws('/hello/:world', function(ws, req, next) {
ws.on('message', function(msg) {
console.log(msg);
app.ws('/hello/:world', (ws, req, next) => {
ws.on('message', (msg) => {
console.log(msg); // eslint-disable-line no-console
});
console.log('socket hello', req.world);
console.log('socket hello', req.world); // eslint-disable-line no-console
next();
});

app.listen(3000)
app.listen(3000);
25 changes: 12 additions & 13 deletions examples/simple.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
var express = require('express');
var expressWs = require('..');
const express = require('express');
const expressWs = require('..')(express());

var expressWs = expressWs(express());
var app = expressWs.app;
const app = expressWs.app;

app.use(function (req, res, next) {
console.log('middleware');
app.use((req, res, next) => {
console.log('middleware'); // eslint-disable-line no-console
req.testing = 'testing';
return next();
});

app.get('/', function(req, res, next){
console.log('get route', req.testing);
app.get('/', (req, res) => {
console.log('get route', req.testing); // eslint-disable-line no-console
res.end();
});

app.ws('/', function(ws, req) {
ws.on('message', function(msg) {
console.log(msg);
app.ws('/', (ws, req) => {
ws.on('message', (msg) => {
console.log(msg); // eslint-disable-line no-console
});
console.log('socket', req.testing);
console.log('socket', req.testing); // eslint-disable-line no-console
});

app.listen(3000)
app.listen(3000);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index",
"module": "src/index",
"scripts": {
"lint": "eslint src/"
"lint": "eslint src/ && eslint examples/"
},
"author": "Henning Morud <[email protected]>",
"contributors": [
Expand Down
4 changes: 2 additions & 2 deletions src/add-ws-method.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import wrapMiddleware from './wrap-middleware';
import websocketUrl from './websocket-url';

export default function addWsMethod(target) {
export default function addWsMethod(target, self) {
/* This prevents conflict with other things setting `.ws`. */
if (target.ws === null || target.ws === undefined) {
target.ws = function addWsRoute(route, ...middlewares) {
const wrappedMiddlewares = middlewares.map(wrapMiddleware);
const wrappedMiddlewares = middlewares.map(fn => fn.bind(self)).map(wrapMiddleware);

/* We append `/.websocket` to the route path here. Why? To prevent conflicts when
* a non-WebSocket request is made to the same GET route - after all, we are only
Expand Down
68 changes: 52 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,58 @@ export default function expressWs(app, httpServer, options = {}) {
};
}

// allow caller to pass in options to WebSocketServer constructor
const wsOptions = options.wsOptions || {};
wsOptions.server = server;
const wsServer = new ws.Server(wsOptions);

/* Create the Express Web Socket object (we do this here so we can bind any routes to it
so that they can easily access the WebSocket Server clients to broadcast to them) */
const me = {
app,
getWss: function getWss() {
return wsServer;
},
applyTo: function applyTo(router) {
addWsMethod(router, this);
},
setRoom: function setRoom(request) {
return request.url.replace('/.websocket', '');
},
broadcast: function broadcast(
currentClient,
message,
options = { skipSelf: true, allClients: false } // eslint-disable-line no-shadow
) {
let recipients = 0;

wsServer.clients.forEach((client) => {
// Ensure that messages are only sent to clients connected to this route (/chat).
let shouldBroadcastToThisClient = true;

if (options.skipSelf) {
shouldBroadcastToThisClient = shouldBroadcastToThisClient && client !== currentClient;
}

if (currentClient.room !== undefined && !options.allClients) {
shouldBroadcastToThisClient = shouldBroadcastToThisClient
&& client.room === currentClient.room;
}

const theSocketIsOpen = currentClient.readyState === 1; /* WebSocket.OPEN */

if (shouldBroadcastToThisClient && theSocketIsOpen) {
client.send(message);
recipients += 1;
}
});
return recipients;
}
};

/* Make our custom `.ws` method available directly on the Express application. You should
* really be using Routers, though. */
addWsMethod(app);
addWsMethod(app, me);

/* Monkeypatch our custom `.ws` method into Express' Router prototype. This makes it possible,
* when using the standard Express Router, to use the `.ws` method without any further calls
Expand All @@ -34,14 +83,9 @@ export default function expressWs(app, httpServer, options = {}) {
* function is simultaneously the prototype that gets assigned to the resulting Router
* object. */
if (!options.leaveRouterUntouched) {
addWsMethod(express.Router);
addWsMethod(express.Router, me);
}

// allow caller to pass in options to WebSocketServer constructor
const wsOptions = options.wsOptions || {};
wsOptions.server = server;
const wsServer = new ws.Server(wsOptions);

wsServer.on('connection', (socket, request) => {
if ('upgradeReq' in socket) {
request = socket.upgradeReq;
Expand Down Expand Up @@ -74,13 +118,5 @@ export default function expressWs(app, httpServer, options = {}) {
});
});

return {
app,
getWss: function getWss() {
return wsServer;
},
applyTo: function applyTo(router) {
addWsMethod(router);
}
};
return me;
}