Skip to content

Commit

Permalink
[feat] Add support for dynamic namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
darrachequesne committed Mar 29, 2018
1 parent e241fd0 commit 9cead1e
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 9 deletions.
30 changes: 29 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,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 @@ -301,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 Down
31 changes: 26 additions & 5 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,38 @@ Client.prototype.setup = function(){
* Connects a client to a namespace.
*
* @param {String} name namespace
* @param {Object} query the query parameters
* @api private
*/

Client.prototype.connect = function(name, query){
debug('connecting to namespace %s', name);
var nsp = this.server.nsps[name];
if (!nsp) {
this.packet({ type: parser.ERROR, nsp: name, data : 'Invalid namespace'});
return;
if (this.server.nsps[name]) {
debug('connecting to namespace %s', name);
return this.doConnect(name, query);
}

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);
this.packet({ type: parser.ERROR, nsp: name, data: 'Invalid namespace' });
}
});
};

/**
* Connects a client to a namespace.
*
* @param {String} name namespace
* @param {String} query the query parameters
* @api private
*/

Client.prototype.doConnect = function(name, query){
var nsp = this.server.of(name);

if ('/' != name && !this.nsps['/']) {
this.connectBuffer.push(name);
return;
Expand Down
48 changes: 47 additions & 1 deletion 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,6 +48,7 @@ function Server(srv, opts){
}
opts = opts || {};
this.nsps = {};
this.parentNsps = new Map();
this.path(opts.path || '/socket.io');
this.serveClient(false !== opts.serveClient);
this.parser = opts.parser || parser;
Expand Down Expand Up @@ -159,6 +162,37 @@ Server.prototype.set = function(key, val){
return this;
};

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

Server.prototype.checkNamespace = function(name, query, fn){
if (this.parentNsps.size === 0) return fn(false);

const keysIterator = this.parentNsps.keys();

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();
};

/**
* Sets the client serving path.
*
Expand Down Expand Up @@ -404,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;
61 changes: 59 additions & 2 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 @@ -889,6 +891,61 @@ describe('socket.io', function(){
});
});
});

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(){
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();
});
socket.on('error', function(err) {
expect().fail();
});
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 allow connections to dynamic namespaces with a function', function(done){
const srv = http();
const sio = io(srv);
srv.listen(function(){
const socket = client(srv, '/dynamic-101');
sio.of((name, query, next) => next(null, '/dynamic-101' === name));
socket.on('connect', done);
});
});

it('should disallow connections when no dynamic namespace matches', function(done){
const srv = http();
const sio = io(srv);
srv.listen(function(){
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();
});
});
});
});
});

describe('socket', function(){
Expand Down Expand Up @@ -1684,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');
socket.io.engine.write('5woooot');
setTimeout(function(){
done();
Expand All @@ -1701,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 9cead1e

Please sign in to comment.