/** * Module dependencies. */ var Socket = require( './socket' ) , EventEmitter = process.EventEmitter , parser = require( './parser' ) , util = require( './util' ); /** * Exports the constructor. */ exports = module.exports = SocketNamespace; /** * Constructor. * * @api public. */ function SocketNamespace( mgr, name ) { this.manager = mgr; this.name = name || ''; this.sockets = {}; this.auth = false; this.setFlags(); } ; /** * Inherits from EventEmitter. */ SocketNamespace.prototype.__proto__ = EventEmitter.prototype; /** * Copies emit since we override it. * * @api private */ SocketNamespace.prototype.$emit = EventEmitter.prototype.emit; /** * Retrieves all clients as Socket instances as an array. * * @api public */ SocketNamespace.prototype.clients = function ( room ) { var room = this.name + (room !== undefined ? '/' + room : ''); if ( !this.manager.rooms[room] ) { return []; } return this.manager.rooms[room].map( function ( id ) { return this.socket( id ); }, this ); }; /** * Access logger interface. * * @api public */ SocketNamespace.prototype.__defineGetter__( 'log', function () { return this.manager.log; } ); /** * Access store. * * @api public */ SocketNamespace.prototype.__defineGetter__( 'store', function () { return this.manager.store; } ); /** * JSON message flag. * * @api public */ SocketNamespace.prototype.__defineGetter__( 'json', function () { this.flags.json = true; return this; } ); /** * Volatile message flag. * * @api public */ SocketNamespace.prototype.__defineGetter__( 'volatile', function () { this.flags.volatile = true; return this; } ); /** * Overrides the room to relay messages to (flag). * * @api public */ SocketNamespace.prototype.in = SocketNamespace.prototype.to = function ( room ) { this.flags.endpoint = this.name + (room ? '/' + room : ''); return this; }; /** * Adds a session id we should prevent relaying messages to (flag). * * @api public */ SocketNamespace.prototype.except = function ( id ) { this.flags.exceptions.push( id ); return this; }; /** * Sets the default flags. * * @api private */ SocketNamespace.prototype.setFlags = function () { this.flags = { endpoint : this.name, exceptions : [] }; return this; }; /** * Sends out a packet. * * @api private */ SocketNamespace.prototype.packet = function ( packet ) { packet.endpoint = this.name; var store = this.store , log = this.log , volatile = this.flags.volatile , exceptions = this.flags.exceptions , packet = parser.encodePacket( packet ); this.manager.onDispatch( this.flags.endpoint, packet, volatile, exceptions ); this.store.publish( 'dispatch', this.flags.endpoint, packet, volatile, exceptions ); this.setFlags(); return this; }; /** * Sends to everyone. * * @api public */ SocketNamespace.prototype.send = function ( data ) { return this.packet( { type : this.flags.json ? 'json' : 'message', data : data } ); }; /** * Emits to everyone (override). * * @api public */ SocketNamespace.prototype.emit = function ( name ) { if ( name == 'newListener' ) { return this.$emit.apply( this, arguments ); } return this.packet( { type : 'event', name : name, args : util.toArray( arguments ).slice( 1 ) } ); }; /** * Retrieves or creates a write-only socket for a client, unless specified. * * @param {Boolean} whether the socket will be readable when initialized * @api public */ SocketNamespace.prototype.socket = function ( sid, readable ) { if ( !this.sockets[sid] ) { this.sockets[sid] = new Socket( this.manager, sid, this, readable ); } return this.sockets[sid]; }; /** * Sets authorization for this namespace. * * @api public */ SocketNamespace.prototype.authorization = function ( fn ) { this.auth = fn; return this; }; /** * Called when a socket disconnects entirely. * * @api private */ SocketNamespace.prototype.handleDisconnect = function ( sid, reason, raiseOnDisconnect ) { if ( this.sockets[sid] && this.sockets[sid].readable ) { if ( raiseOnDisconnect ) { this.sockets[sid].onDisconnect( reason ); } delete this.sockets[sid]; } }; /** * Performs authentication. * * @param Object client request data * @api private */ SocketNamespace.prototype.authorize = function ( data, fn ) { if ( this.auth ) { var self = this; this.auth.call( this, data, function ( err, authorized ) { self.log.debug( 'client ' + (authorized ? '' : 'un') + 'authorized for ' + self.name ); fn( err, authorized ); } ); } else { this.log.debug( 'client authorized for ' + this.name ); fn( null, true ); } return this; }; /** * Handles a packet. * * @api private */ SocketNamespace.prototype.handlePacket = function ( sessid, packet ) { var socket = this.socket( sessid ) , dataAck = packet.ack == 'data' , manager = this.manager , self = this; function ack() { self.log.debug( 'sending data ack packet' ); socket.packet( { type : 'ack', args : util.toArray( arguments ), ackId : packet.id } ); } ; function error( err ) { self.log.warn( 'handshake error ' + err + ' for ' + self.name ); socket.packet( { type : 'error', reason : err } ); } ; function connect() { self.manager.onJoin( sessid, self.name ); self.store.publish( 'join', sessid, self.name ); // packet echo socket.packet( { type : 'connect' } ); // emit connection event self.$emit( 'connection', socket ); } ; switch ( packet.type ) { case 'connect': if ( packet.endpoint == '' ) { connect(); } else { var handshakeData = manager.handshaken[sessid]; this.authorize( handshakeData, function ( err, authorized, newData ) { if ( err ) { return error( err ); } if ( authorized ) { manager.onHandshake( sessid, newData || handshakeData ); self.store.publish( 'handshake', sessid, newData || handshakeData ); connect(); } else { error( 'unauthorized' ); } } ); } break; case 'ack': if ( socket.acks[packet.ackId] ) { socket.acks[packet.ackId].apply( socket, packet.args ); } else { this.log.info( 'unknown ack packet' ); } break; case 'event': // check if the emitted event is not blacklisted if ( -~manager.get( 'blacklist' ).indexOf( packet.name ) ) { this.log.debug( 'ignoring blacklisted event `' + packet.name + '`' ); } else { var params = [packet.name].concat( packet.args ); if ( dataAck ) { params.push( ack ); } socket.$emit.apply( socket, params ); } break; case 'disconnect': this.manager.onLeave( sessid, this.name ); this.store.publish( 'leave', sessid, this.name ); socket.$emit( 'disconnect', packet.reason || 'packet' ); break; case 'json': case 'message': var params = ['message', packet.data]; if ( dataAck ) { params.push( ack ); } socket.$emit.apply( socket, params ); } ; };