diff --git a/client/js/libs/socket.js b/client/js/libs/socket.js new file mode 100644 index 0000000..5c756e8 --- /dev/null +++ b/client/js/libs/socket.js @@ -0,0 +1,430 @@ + +/** + * Module dependencies. + */ + +var Emitter = require('events').EventEmitter; +var parser = require('socket.io-parser'); +var url = require('url'); +var debug = require('debug')('socket.io:socket'); +var hasBin = require('has-binary-data'); + +/** + * Module exports. + */ + +module.exports = exports = Socket; + +/** + * Blacklisted events. + * + * @api public + */ + +exports.events = [ + 'error', + 'connect', + 'disconnect', + 'newListener', + 'removeListener' +]; + +/** + * Flags. + * + * @api private + */ + +var flags = [ + 'json', + 'volatile', + 'broadcast' +]; + +/** + * `EventEmitter#emit` reference. + */ + +var emit = Emitter.prototype.emit; + +/** + * Interface to a `Client` for a given `Namespace`. + * + * @param {Namespace} nsp + * @param {Client} client + * @api public + */ + +function Socket(nsp, client){ + this.nsp = nsp; + this.server = nsp.server; + this.adapter = this.nsp.adapter; + this.id = client.id; + this.request = client.request; + this.client = client; + this.conn = client.conn; + this.rooms = []; + this.acks = {}; + this.connected = true; + this.disconnected = false; + this.handshake = this.buildHandshake(); +} + +/** + * Inherits from `EventEmitter`. + */ + +Socket.prototype.__proto__ = Emitter.prototype; + +/** + * Apply flags from `Socket`. + */ + +flags.forEach(function(flag){ + Socket.prototype.__defineGetter__(flag, function(){ + this.flags = this.flags || {}; + this.flags[flag] = true; + return this; + }); +}); + +/** + * `request` engine.io shorcut. + * + * @api public + */ + +Socket.prototype.__defineGetter__('request', function(){ + return this.conn.request; +}); + +/** + * Builds the `handshake` BC object + * + * @api private + */ + +Socket.prototype.buildHandshake = function(){ + return { + headers: this.request.headers, + time: (new Date) + '', + address: this.conn.remoteAddress, + xdomain: !!this.request.headers.origin, + secure: !!this.request.connection.encrypted, + issued: +(new Date), + url: this.request.url, + query: url.parse(this.request.url, true).query || {} + }; +}; + +/** + * Emits to this client. + * + * @return {Socket} self + * @api public + */ + +Socket.prototype.emit = function(ev){ + if (~exports.events.indexOf(ev)) { + emit.apply(this, arguments); + } else { + var args = Array.prototype.slice.call(arguments); + var packet = {}; + packet.type = hasBin(args) ? parser.BINARY_EVENT : parser.EVENT; + packet.data = args; + + // access last argument to see if it's an ACK callback + if ('function' == typeof args[args.length - 1]) { + if (this._rooms || (this.flags && this.flags.broadcast)) { + throw new Error('Callbacks are not supported when broadcasting'); + } + + debug('emitting packet with ack id %d', this.nsp.ids); + this.acks[this.nsp.ids] = args.pop(); + packet.id = this.nsp.ids++; + } + + if (this._rooms || (this.flags && this.flags.broadcast)) { + this.adapter.broadcast(packet, { + except: [this.id], + rooms: this._rooms, + flags: this.flags + }); + } else { + // dispatch packet + this.packet(packet); + } + + // reset flags + delete this._rooms; + delete this.flags; + } + return this; +}; + +/** + * Targets a room when broadcasting. + * + * @param {String} name + * @return {Socket} self + * @api public + */ + +Socket.prototype.to = +Socket.prototype.in = function(name){ + this._rooms = this._rooms || []; + if (!~this._rooms.indexOf(name)) this._rooms.push(name); + return this; +}; + +/** + * Sends a `message` event. + * + * @return {Socket} self + * @api public + */ + +Socket.prototype.send = +Socket.prototype.write = function(){ + var args = Array.prototype.slice.call(arguments); + args.unshift('message'); + this.emit.apply(this, args); + return this; +}; + +/** + * Writes a packet. + * + * @param {Object} packet object + * @api private + */ + +Socket.prototype.packet = function(packet, preEncoded){ + packet.nsp = this.nsp.name; + var volatile = this.flags && this.flags.volatile; + this.client.packet(packet, preEncoded, volatile); +}; + +/** + * Joins a room. + * + * @param {String} room + * @param {Function} optional, callback + * @return {Socket} self + * @api private + */ + +Socket.prototype.join = function(room, fn){ + debug('joining room %s', room); + var self = this; + if (~this.rooms.indexOf(room)) return this; + this.adapter.add(this.id, room, function(err){ + if (err) return fn && fn(err); + debug('joined room %s', room); + self.rooms.push(room); + fn && fn(null); + }); + return this; +}; + +/** + * Leaves a room. + * + * @param {String} room + * @param {Function} optional, callback + * @return {Socket} self + * @api private + */ + +Socket.prototype.leave = function(room, fn){ + debug('leave room %s', room); + var self = this; + this.adapter.del(this.id, room, function(err){ + if (err) return fn && fn(err); + debug('left room %s', room); + self.rooms.splice(self.rooms.indexOf(room), 1); + fn && fn(null); + }); + return this; +}; + +/** + * Leave all rooms. + * + * @api private + */ + +Socket.prototype.leaveAll = function(){ + this.adapter.delAll(this.id); + this.rooms = []; +}; + +/** + * Called by `Namespace` upon succesful + * middleware execution (ie: authorization). + * + * @api private + */ + +Socket.prototype.onconnect = function(){ + debug('socket connected - writing packet'); + this.join(this.id); + this.packet({ type: parser.CONNECT }); + this.nsp.connected[this.id] = this; +}; + +/** + * Called with each packet. Called by `Client`. + * + * @param {Object} packet + * @api private + */ + +Socket.prototype.onpacket = function(packet){ + debug('got packet %j', packet); + switch (packet.type) { + case parser.EVENT: + this.onevent(packet); + break; + + case parser.BINARY_EVENT: + this.onevent(packet); + break; + + case parser.ACK: + this.onack(packet); + break; + + case parser.BINARY_ACK: + this.onack(packet); + break; + + case parser.DISCONNECT: + this.ondisconnect(); + break; + + case parser.ERROR: + this.emit('error', packet.data); + } +}; + +/** + * Called upon event packet. + * + * @param {Object} packet object + * @api private + */ + +Socket.prototype.onevent = function(packet){ + var args = packet.data || []; + debug('emitting event %j', args); + + if (null != packet.id) { + debug('attaching ack callback to event'); + args.push(this.ack(packet.id)); + } + + emit.apply(this, args); +}; + +/** + * Produces an ack callback to emit with an event. + * + * @param {Number} packet id + * @api private + */ + +Socket.prototype.ack = function(id){ + var self = this; + var sent = false; + return function(){ + // prevent double callbacks + if (sent) return; + var args = Array.prototype.slice.call(arguments); + debug('sending ack %j', args); + + var type = hasBin(args) ? parser.BINARY_ACK : parser.ACK; + self.packet({ + id: id, + type: type, + data: args + }); + }; +}; + +/** + * Called upon ack packet. + * + * @api private + */ + +Socket.prototype.onack = function(packet){ + var ack = this.acks[packet.id]; + if ('function' == typeof ack) { + debug('calling ack %s with %j', packet.id, packet.data); + ack.apply(this, packet.data); + delete this.acks[packet.id]; + } else { + debug('bad ack %s', packet.id); + } +}; + +/** + * Called upon client disconnect packet. + * + * @api private + */ + +Socket.prototype.ondisconnect = function(){ + debug('got disconnect packet'); + this.onclose('client namespace disconnect'); +}; + +/** + * Called upon closing. Called by `Client`. + * + * @param {String} reason + * @api private + */ + +Socket.prototype.onclose = function(reason){ + if (!this.connected) return this; + debug('closing socket - reason %s', reason); + this.leaveAll(); + this.nsp.remove(this); + this.client.remove(this); + this.connected = false; + this.disconnected = true; + delete this.nsp.connected[this.id]; + this.emit('disconnect', reason); +}; + +/** + * Produces an `error` packet. + * + * @param {Object} error object + * @api private + */ + +Socket.prototype.error = function(err){ + this.packet({ type: parser.ERROR, data: err }); +}; + +/** + * Disconnects this client. + * + * @param {Boolean} if `true`, closes the underlying connection + * @return {Socket} self + * @api public + */ + +Socket.prototype.disconnect = function(close){ + if (!this.connected) return this; + if (close) { + this.client.disconnect(); + } else { + this.packet({ type: parser.DISCONNECT }); + this.onclose('server namespace disconnect'); + } + return this; +};