Skip to content

Commit ac945d1

Browse files
authoredMar 29, 2018
[feat] Add support for dynamic namespaces (#3195)
This follows #3187, with a slightly different API. A dynamic namespace can be created with: ```js io.of(/^\/dynamic-\d+$/).on('connect', (socket) => { /* ... */ }); ```
1 parent ad0c052 commit ac945d1

File tree

5 files changed

+147
-117
lines changed

5 files changed

+147
-117
lines changed
 

‎docs/API.md

+29-18
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
- [server.onconnection(socket)](#serveronconnectionsocket)
2121
- [server.of(nsp)](#serverofnsp)
2222
- [server.close([callback])](#serverclosecallback)
23-
- [server.useNamespaceValidator(fn)](#serverusenamespacevalidatorfn)
2423
- [Class: Namespace](#namespace)
2524
- [namespace.name](#namespacename)
2625
- [namespace.connected](#namespaceconnected)
@@ -293,7 +292,7 @@ Advanced use only. Creates a new `socket.io` client from the incoming engine.io
293292

294293
#### server.of(nsp)
295294

296-
- `nsp` _(String)_
295+
- `nsp` _(String|RegExp|Function)_
297296
- **Returns** `Namespace`
298297

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

304+
A regex or a function can also be provided, in order to create namespace in a dynamic way:
305+
306+
```js
307+
const dynamicNsp = io.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
308+
const newNamespace = socket.nsp; // newNamespace.name === '/dynamic-101'
309+
310+
// broadcast to all clients in the given sub-namespace
311+
newNamespace.emit('hello');
312+
});
313+
314+
// client-side
315+
const socket = io('/dynamic-101');
316+
317+
// broadcast to all clients in each sub-namespace
318+
dynamicNsp.emit('hello');
319+
320+
// use a middleware for each sub-namespace
321+
dynamicNsp.use((socket, next) => { /* ... */ });
322+
```
323+
324+
With a function:
325+
326+
```js
327+
io.of((name, query, next) => {
328+
next(null, checkToken(query.token));
329+
}).on('connect', (socket) => { /* ... */ });
330+
```
331+
305332
#### server.close([callback])
306333

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

325-
#### server.useNamespaceValidator(fn)
326-
327-
- `fn` _(Function)_
328-
329-
Sets up server middleware to validate whether a new namespace should be created.
330-
331-
```js
332-
io.useNamespaceValidator((nsp, next) => {
333-
if (nsp === 'dynamic') {
334-
next(null, true);
335-
} else {
336-
next(new Error('Invalid namespace'));
337-
}
338-
});
339-
```
340-
341352
#### server.engine.generateId
342353

343354
Overwrites the default method to generate your custom socket id.

‎lib/client.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Client.prototype.setup = function(){
5656
* Connects a client to a namespace.
5757
*
5858
* @param {String} name namespace
59-
* @param {String} query the query parameters
59+
* @param {Object} query the query parameters
6060
* @api private
6161
*/
6262

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

69-
this.server.checkNamespace(name, (allow) => {
70-
if (allow) {
71-
debug('creating namespace %s', name);
69+
this.server.checkNamespace(name, query, (dynamicNsp) => {
70+
if (dynamicNsp) {
71+
debug('dynamic namespace %s was created', dynamicNsp.name);
7272
this.doConnect(name, query);
7373
} else {
7474
debug('creation of namespace %s was denied', name);

‎lib/index.js

+35-37
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
'use strict';
12

23
/**
34
* Module dependencies.
@@ -12,6 +13,7 @@ var clientVersion = require('socket.io-client/package.json').version;
1213
var Client = require('./client');
1314
var Emitter = require('events').EventEmitter;
1415
var Namespace = require('./namespace');
16+
var ParentNamespace = require('./parent-namespace');
1517
var Adapter = require('socket.io-adapter');
1618
var parser = require('socket.io-parser');
1719
var debug = require('debug')('socket.io:server');
@@ -46,7 +48,7 @@ function Server(srv, opts){
4648
}
4749
opts = opts || {};
4850
this.nsps = {};
49-
this.nspValidators = [];
51+
this.parentNsps = new Map();
5052
this.path(opts.path || '/socket.io');
5153
this.serveClient(false !== opts.serveClient);
5254
this.parser = opts.parser || parser;
@@ -160,51 +162,35 @@ Server.prototype.set = function(key, val){
160162
return this;
161163
};
162164

163-
/**
164-
* Sets up server middleware to validate incoming namespaces not already created on the server.
165-
*
166-
* @return {Server} self
167-
* @api public
168-
*/
169-
170-
Server.prototype.useNamespaceValidator = function(fn){
171-
this.nspValidators.push(fn);
172-
return this;
173-
};
174-
175165
/**
176166
* Executes the middleware for an incoming namespace not already created on the server.
177167
*
178-
* @param name of incomming namespace
179-
* @param {Function} last fn call in the middleware
168+
* @param {String} name name of incoming namespace
169+
* @param {Object} query the query parameters
170+
* @param {Function} fn callback
180171
* @api private
181172
*/
182173

183-
Server.prototype.checkNamespace = function(name, fn){
184-
var fns = this.nspValidators.slice(0);
185-
if (!fns.length) return fn(false);
186-
187-
var namespaceAllowed = false; // Deny unknown namespaces by default
188-
189-
function run(i){
190-
fns[i](name, function(err, allow){
191-
// upon error, short-circuit
192-
if (err) return fn(false);
174+
Server.prototype.checkNamespace = function(name, query, fn){
175+
if (this.parentNsps.size === 0) return fn(false);
193176

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

197-
namespaceAllowed = namespaceAllowed || allow === true;
198-
199-
// if no middleware left, summon callback
200-
if (!fns[i + 1]) return fn(namespaceAllowed);
201-
202-
// go on to next
203-
run(i + 1);
179+
const run = () => {
180+
let nextFn = keysIterator.next();
181+
if (nextFn.done) {
182+
return fn(false);
183+
}
184+
nextFn.value(name, query, (err, allow) => {
185+
if (err || !allow) {
186+
run();
187+
} else {
188+
fn(this.parentNsps.get(nextFn.value).createChild(name));
189+
}
204190
});
205-
}
191+
};
206192

207-
run(0);
193+
run();
208194
};
209195

210196
/**
@@ -452,12 +438,24 @@ Server.prototype.onconnection = function(conn){
452438
/**
453439
* Looks up a namespace.
454440
*
455-
* @param {String} name nsp name
441+
* @param {String|RegExp|Function} name nsp name
456442
* @param {Function} [fn] optional, nsp `connection` ev handler
457443
* @api public
458444
*/
459445

460446
Server.prototype.of = function(name, fn){
447+
if (typeof name === 'function' || name instanceof RegExp) {
448+
const parentNsp = new ParentNamespace(this);
449+
debug('initializing parent namespace %s', parentNsp.name);
450+
if (typeof name === 'function') {
451+
this.parentNsps.set(name, parentNsp);
452+
} else {
453+
this.parentNsps.set((nsp, conn, next) => next(null, name.test(nsp)), parentNsp);
454+
}
455+
if (fn) parentNsp.on('connect', fn);
456+
return parentNsp;
457+
}
458+
461459
if (String(name)[0] !== '/') name = '/' + name;
462460

463461
var nsp = this.nsps[name];

‎lib/parent-namespace.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
const Namespace = require('./namespace');
4+
5+
let count = 0;
6+
7+
class ParentNamespace extends Namespace {
8+
9+
constructor(server) {
10+
super(server, '/_' + (count++));
11+
this.children = new Set();
12+
}
13+
14+
initAdapter() {}
15+
16+
emit() {
17+
const args = Array.prototype.slice.call(arguments);
18+
19+
this.children.forEach(nsp => {
20+
nsp.rooms = this.rooms;
21+
nsp.flags = this.flags;
22+
nsp.emit.apply(nsp, args);
23+
});
24+
this.rooms = [];
25+
this.flags = {};
26+
}
27+
28+
createChild(name) {
29+
const namespace = new Namespace(this.server, name);
30+
namespace.fns = this.fns.slice(0);
31+
this.listeners('connect').forEach(listener => namespace.on('connect', listener));
32+
this.listeners('connection').forEach(listener => namespace.on('connection', listener));
33+
this.children.add(namespace);
34+
this.server.nsps[name] = namespace;
35+
return namespace;
36+
}
37+
}
38+
39+
module.exports = ParentNamespace;

‎test/socket.io.js

+40-58
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use strict';
2+
13
var http = require('http').Server;
24
var io = require('../lib');
35
var fs = require('fs');
@@ -890,75 +892,55 @@ describe('socket.io', function(){
890892
});
891893
});
892894

893-
describe('dynamic', function () {
894-
it('should allow connections to dynamic namespaces', function(done){
895-
var srv = http();
896-
var sio = io(srv);
895+
describe('dynamic namespaces', function () {
896+
it('should allow connections to dynamic namespaces with a regex', function(done){
897+
const srv = http();
898+
const sio = io(srv);
899+
let count = 0;
897900
srv.listen(function(){
898-
var namespace = '/dynamic';
899-
var dynamic = client(srv, namespace);
900-
sio.useNamespaceValidator(function(nsp, next) {
901-
expect(nsp).to.be(namespace);
902-
next(null, true);
901+
const socket = client(srv, '/dynamic-101');
902+
let dynamicNsp = sio.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
903+
expect(socket.nsp.name).to.be('/dynamic-101');
904+
dynamicNsp.emit('hello', 1, '2', { 3: '4'});
905+
if (++count === 4) done();
906+
}).use((socket, next) => {
907+
next();
908+
if (++count === 4) done();
903909
});
904-
dynamic.on('error', function(err) {
910+
socket.on('error', function(err) {
905911
expect().fail();
906912
});
907-
dynamic.on('connect', function() {
908-
expect(sio.nsps[namespace]).to.be.a(Namespace);
909-
expect(Object.keys(sio.nsps[namespace].sockets).length).to.be(1);
910-
done();
913+
socket.on('connect', () => {
914+
if (++count === 4) done();
915+
});
916+
socket.on('hello', (a, b, c) => {
917+
expect(a).to.eql(1);
918+
expect(b).to.eql('2');
919+
expect(c).to.eql({ 3: '4' });
920+
if (++count === 4) done();
911921
});
912922
});
913923
});
914924

915-
it('should not allow connections to dynamic namespaces if not supported', function(done){
916-
var srv = http();
917-
var sio = io(srv);
925+
it('should allow connections to dynamic namespaces with a function', function(done){
926+
const srv = http();
927+
const sio = io(srv);
918928
srv.listen(function(){
919-
var namespace = '/dynamic';
920-
sio.useNamespaceValidator(function(nsp, next) {
921-
expect(nsp).to.be(namespace);
922-
next(null, false);
923-
});
924-
sio.on('connect', function(socket) {
925-
if (socket.nsp.name === namespace) {
926-
expect().fail();
927-
}
928-
});
929-
930-
var dynamic = client(srv,namespace);
931-
dynamic.on('connect', function(){
932-
expect().fail();
933-
});
934-
dynamic.on('error', function(err) {
935-
expect(err).to.be("Invalid namespace");
936-
done();
937-
});
929+
const socket = client(srv, '/dynamic-101');
930+
sio.of((name, query, next) => next(null, '/dynamic-101' === name));
931+
socket.on('connect', done);
938932
});
939933
});
940934

941-
it('should not allow connections to dynamic namespaces if there is an error', function(done){
942-
var srv = http();
943-
var sio = io(srv);
935+
it('should disallow connections when no dynamic namespace matches', function(done){
936+
const srv = http();
937+
const sio = io(srv);
944938
srv.listen(function(){
945-
var namespace = '/dynamic';
946-
sio.useNamespaceValidator(function(nsp, next) {
947-
expect(nsp).to.be(namespace);
948-
next(new Error(), true);
949-
});
950-
sio.on('connect', function(socket) {
951-
if (socket.nsp.name === namespace) {
952-
expect().fail();
953-
}
954-
});
955-
956-
var dynamic = client(srv,namespace);
957-
dynamic.on('connect', function(){
958-
expect().fail();
959-
});
960-
dynamic.on('error', function(err) {
961-
expect(err).to.be("Invalid namespace");
939+
const socket = client(srv, '/abc');
940+
sio.of(/^\/dynamic-\d+$/);
941+
sio.of((name, query, next) => next(null, '/dynamic-101' === name));
942+
socket.on('error', (err) => {
943+
expect(err).to.be('Invalid namespace');
962944
done();
963945
});
964946
});
@@ -1759,7 +1741,7 @@ describe('socket.io', function(){
17591741
var socket = client(srv, { reconnection: false });
17601742
sio.on('connection', function(s){
17611743
s.conn.on('upgrade', function(){
1762-
console.log('\033[96mNote: warning expected and normal in test.\033[39m');
1744+
console.log('\u001b[96mNote: warning expected and normal in test.\u001b[39m');
Has a conversation. Original line has a conversation.
17631745
socket.io.engine.write('5woooot');
17641746
setTimeout(function(){
17651747
done();
@@ -1776,7 +1758,7 @@ describe('socket.io', function(){
17761758
var socket = client(srv, { reconnection: false });
17771759
sio.on('connection', function(s){
17781760
s.conn.on('upgrade', function(){
1779-
console.log('\033[96mNote: warning expected and normal in test.\033[39m');
1761+
console.log('\u001b[96mNote: warning expected and normal in test.\u001b[39m');
17801762
socket.io.engine.write('44["handle me please"]');
17811763
setTimeout(function(){
17821764
done();

0 commit comments

Comments
 (0)
Please sign in to comment.