Skip to content

Commit

Permalink
[feat] Add support for dynamic namespaces (#3195)
Browse files Browse the repository at this point in the history
This follows #3187, with a slightly different API.

A dynamic namespace can be created with:

```js
io.of(/^\/dynamic-\d+$/).on('connect', (socket) => { /* ... */ });
```
  • Loading branch information
darrachequesne authored Mar 29, 2018
1 parent ad0c052 commit ac945d1
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 117 deletions.
47 changes: 29 additions & 18 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
- [server.onconnection(socket)](#serveronconnectionsocket)
- [server.of(nsp)](#serverofnsp)
- [server.close([callback])](#serverclosecallback)
- [server.useNamespaceValidator(fn)](#serverusenamespacevalidatorfn)
- [Class: Namespace](#namespace)
- [namespace.name](#namespacename)
- [namespace.connected](#namespaceconnected)
Expand Down Expand Up @@ -293,7 +292,7 @@ Advanced use only. Creates a new `socket.io` client from the incoming engine.io

#### server.of(nsp)

- `nsp` _(String)_
- `nsp` _(String|RegExp|Function)_
- **Returns** `Namespace`

Initializes and retrieves the given `Namespace` by its pathname identifier `nsp`. If the namespace was already initialized it returns it immediately.
Expand All @@ -302,6 +301,34 @@ Initializes and retrieves the given `Namespace` by its pathname identifier `nsp`
const adminNamespace = io.of('/admin');
```

A regex or a function can also be provided, in order to create namespace in a dynamic way:

```js
const dynamicNsp = io.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
const newNamespace = socket.nsp; // newNamespace.name === '/dynamic-101'

// broadcast to all clients in the given sub-namespace
newNamespace.emit('hello');
});

// client-side
const socket = io('/dynamic-101');

// broadcast to all clients in each sub-namespace
dynamicNsp.emit('hello');

// use a middleware for each sub-namespace
dynamicNsp.use((socket, next) => { /* ... */ });
```

With a function:

```js
io.of((name, query, next) => {
next(null, checkToken(query.token));
}).on('connect', (socket) => { /* ... */ });
```

#### server.close([callback])

- `callback` _(Function)_
Expand All @@ -322,22 +349,6 @@ server.listen(PORT); // PORT is free to use
io = Server(server);
```

#### server.useNamespaceValidator(fn)

- `fn` _(Function)_

Sets up server middleware to validate whether a new namespace should be created.

```js
io.useNamespaceValidator((nsp, next) => {
if (nsp === 'dynamic') {
next(null, true);
} else {
next(new Error('Invalid namespace'));
}
});
```

#### server.engine.generateId

Overwrites the default method to generate your custom socket id.
Expand Down
8 changes: 4 additions & 4 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Client.prototype.setup = function(){
* Connects a client to a namespace.
*
* @param {String} name namespace
* @param {String} query the query parameters
* @param {Object} query the query parameters
* @api private
*/

Expand All @@ -66,9 +66,9 @@ Client.prototype.connect = function(name, query){
return this.doConnect(name, query);
}

this.server.checkNamespace(name, (allow) => {
if (allow) {
debug('creating namespace %s', name);
this.server.checkNamespace(name, query, (dynamicNsp) => {
if (dynamicNsp) {
debug('dynamic namespace %s was created', dynamicNsp.name);
this.doConnect(name, query);
} else {
debug('creation of namespace %s was denied', name);
Expand Down
72 changes: 35 additions & 37 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use strict';

/**
* Module dependencies.
Expand All @@ -12,6 +13,7 @@ var clientVersion = require('socket.io-client/package.json').version;
var Client = require('./client');
var Emitter = require('events').EventEmitter;
var Namespace = require('./namespace');
var ParentNamespace = require('./parent-namespace');
var Adapter = require('socket.io-adapter');
var parser = require('socket.io-parser');
var debug = require('debug')('socket.io:server');
Expand Down Expand Up @@ -46,7 +48,7 @@ function Server(srv, opts){
}
opts = opts || {};
this.nsps = {};
this.nspValidators = [];
this.parentNsps = new Map();
this.path(opts.path || '/socket.io');
this.serveClient(false !== opts.serveClient);
this.parser = opts.parser || parser;
Expand Down Expand Up @@ -160,51 +162,35 @@ Server.prototype.set = function(key, val){
return this;
};

/**
* Sets up server middleware to validate incoming namespaces not already created on the server.
*
* @return {Server} self
* @api public
*/

Server.prototype.useNamespaceValidator = function(fn){
this.nspValidators.push(fn);
return this;
};

/**
* Executes the middleware for an incoming namespace not already created on the server.
*
* @param name of incomming namespace
* @param {Function} last fn call in the middleware
* @param {String} name name of incoming namespace
* @param {Object} query the query parameters
* @param {Function} fn callback
* @api private
*/

Server.prototype.checkNamespace = function(name, fn){
var fns = this.nspValidators.slice(0);
if (!fns.length) return fn(false);

var namespaceAllowed = false; // Deny unknown namespaces by default

function run(i){
fns[i](name, function(err, allow){
// upon error, short-circuit
if (err) return fn(false);
Server.prototype.checkNamespace = function(name, query, fn){
if (this.parentNsps.size === 0) return fn(false);

// if one piece of middleware explicitly denies namespace, short-circuit
if (allow === false) return fn(false);
const keysIterator = this.parentNsps.keys();

namespaceAllowed = namespaceAllowed || allow === true;

// if no middleware left, summon callback
if (!fns[i + 1]) return fn(namespaceAllowed);

// go on to next
run(i + 1);
const run = () => {
let nextFn = keysIterator.next();
if (nextFn.done) {
return fn(false);
}
nextFn.value(name, query, (err, allow) => {
if (err || !allow) {
run();
} else {
fn(this.parentNsps.get(nextFn.value).createChild(name));
}
});
}
};

run(0);
run();
};

/**
Expand Down Expand Up @@ -452,12 +438,24 @@ Server.prototype.onconnection = function(conn){
/**
* Looks up a namespace.
*
* @param {String} name nsp name
* @param {String|RegExp|Function} name nsp name
* @param {Function} [fn] optional, nsp `connection` ev handler
* @api public
*/

Server.prototype.of = function(name, fn){
if (typeof name === 'function' || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this);
debug('initializing parent namespace %s', parentNsp.name);
if (typeof name === 'function') {
this.parentNsps.set(name, parentNsp);
} else {
this.parentNsps.set((nsp, conn, next) => next(null, name.test(nsp)), parentNsp);
}
if (fn) parentNsp.on('connect', fn);
return parentNsp;
}

if (String(name)[0] !== '/') name = '/' + name;

var nsp = this.nsps[name];
Expand Down
39 changes: 39 additions & 0 deletions lib/parent-namespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

const Namespace = require('./namespace');

let count = 0;

class ParentNamespace extends Namespace {

constructor(server) {
super(server, '/_' + (count++));
this.children = new Set();
}

initAdapter() {}

emit() {
const args = Array.prototype.slice.call(arguments);

this.children.forEach(nsp => {
nsp.rooms = this.rooms;
nsp.flags = this.flags;
nsp.emit.apply(nsp, args);
});
this.rooms = [];
this.flags = {};
}

createChild(name) {
const namespace = new Namespace(this.server, name);
namespace.fns = this.fns.slice(0);
this.listeners('connect').forEach(listener => namespace.on('connect', listener));
this.listeners('connection').forEach(listener => namespace.on('connection', listener));
this.children.add(namespace);
this.server.nsps[name] = namespace;
return namespace;
}
}

module.exports = ParentNamespace;
98 changes: 40 additions & 58 deletions test/socket.io.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use strict';

var http = require('http').Server;
var io = require('../lib');
var fs = require('fs');
Expand Down Expand Up @@ -890,75 +892,55 @@ describe('socket.io', function(){
});
});

describe('dynamic', function () {
it('should allow connections to dynamic namespaces', function(done){
var srv = http();
var sio = io(srv);
describe('dynamic namespaces', function () {
it('should allow connections to dynamic namespaces with a regex', function(done){
const srv = http();
const sio = io(srv);
let count = 0;
srv.listen(function(){
var namespace = '/dynamic';
var dynamic = client(srv, namespace);
sio.useNamespaceValidator(function(nsp, next) {
expect(nsp).to.be(namespace);
next(null, true);
const socket = client(srv, '/dynamic-101');
let dynamicNsp = sio.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
expect(socket.nsp.name).to.be('/dynamic-101');
dynamicNsp.emit('hello', 1, '2', { 3: '4'});
if (++count === 4) done();
}).use((socket, next) => {
next();
if (++count === 4) done();
});
dynamic.on('error', function(err) {
socket.on('error', function(err) {
expect().fail();
});
dynamic.on('connect', function() {
expect(sio.nsps[namespace]).to.be.a(Namespace);
expect(Object.keys(sio.nsps[namespace].sockets).length).to.be(1);
done();
socket.on('connect', () => {
if (++count === 4) done();
});
socket.on('hello', (a, b, c) => {
expect(a).to.eql(1);
expect(b).to.eql('2');
expect(c).to.eql({ 3: '4' });
if (++count === 4) done();
});
});
});

it('should not allow connections to dynamic namespaces if not supported', function(done){
var srv = http();
var sio = io(srv);
it('should allow connections to dynamic namespaces with a function', function(done){
const srv = http();
const sio = io(srv);
srv.listen(function(){
var namespace = '/dynamic';
sio.useNamespaceValidator(function(nsp, next) {
expect(nsp).to.be(namespace);
next(null, false);
});
sio.on('connect', function(socket) {
if (socket.nsp.name === namespace) {
expect().fail();
}
});

var dynamic = client(srv,namespace);
dynamic.on('connect', function(){
expect().fail();
});
dynamic.on('error', function(err) {
expect(err).to.be("Invalid namespace");
done();
});
const socket = client(srv, '/dynamic-101');
sio.of((name, query, next) => next(null, '/dynamic-101' === name));
socket.on('connect', done);
});
});

it('should not allow connections to dynamic namespaces if there is an error', function(done){
var srv = http();
var sio = io(srv);
it('should disallow connections when no dynamic namespace matches', function(done){
const srv = http();
const sio = io(srv);
srv.listen(function(){
var namespace = '/dynamic';
sio.useNamespaceValidator(function(nsp, next) {
expect(nsp).to.be(namespace);
next(new Error(), true);
});
sio.on('connect', function(socket) {
if (socket.nsp.name === namespace) {
expect().fail();
}
});

var dynamic = client(srv,namespace);
dynamic.on('connect', function(){
expect().fail();
});
dynamic.on('error', function(err) {
expect(err).to.be("Invalid namespace");
const socket = client(srv, '/abc');
sio.of(/^\/dynamic-\d+$/);
sio.of((name, query, next) => next(null, '/dynamic-101' === name));
socket.on('error', (err) => {
expect(err).to.be('Invalid namespace');
done();
});
});
Expand Down Expand Up @@ -1759,7 +1741,7 @@ describe('socket.io', function(){
var socket = client(srv, { reconnection: false });
sio.on('connection', function(s){
s.conn.on('upgrade', function(){
console.log('\033[96mNote: warning expected and normal in test.\033[39m');
console.log('\u001b[96mNote: warning expected and normal in test.\u001b[39m');

This comment has been minimized.

Copy link
@yangfuhe

yangfuhe Nov 6, 2018

s

socket.io.engine.write('5woooot');
setTimeout(function(){
done();
Expand All @@ -1776,7 +1758,7 @@ describe('socket.io', function(){
var socket = client(srv, { reconnection: false });
sio.on('connection', function(s){
s.conn.on('upgrade', function(){
console.log('\033[96mNote: warning expected and normal in test.\033[39m');
console.log('\u001b[96mNote: warning expected and normal in test.\u001b[39m');
socket.io.engine.write('44["handle me please"]');
setTimeout(function(){
done();
Expand Down

0 comments on commit ac945d1

Please sign in to comment.