439 lines
8.1 KiB
JavaScript
439 lines
8.1 KiB
JavaScript
|
|
||
|
/**
|
||
|
* Module dependencies.
|
||
|
*/
|
||
|
|
||
|
var parser = require('socket.io-parser');
|
||
|
var Emitter = require('component-emitter');
|
||
|
var toArray = require('to-array');
|
||
|
var on = require('./on');
|
||
|
var bind = require('component-bind');
|
||
|
var debug = require('debug')('socket.io-client:socket');
|
||
|
var parseqs = require('parseqs');
|
||
|
var hasBin = require('has-binary2');
|
||
|
|
||
|
/**
|
||
|
* Module exports.
|
||
|
*/
|
||
|
|
||
|
module.exports = exports = Socket;
|
||
|
|
||
|
/**
|
||
|
* Internal events (blacklisted).
|
||
|
* These events can't be emitted by the user.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
var events = {
|
||
|
connect: 1,
|
||
|
connect_error: 1,
|
||
|
connect_timeout: 1,
|
||
|
connecting: 1,
|
||
|
disconnect: 1,
|
||
|
error: 1,
|
||
|
reconnect: 1,
|
||
|
reconnect_attempt: 1,
|
||
|
reconnect_failed: 1,
|
||
|
reconnect_error: 1,
|
||
|
reconnecting: 1,
|
||
|
ping: 1,
|
||
|
pong: 1
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Shortcut to `Emitter#emit`.
|
||
|
*/
|
||
|
|
||
|
var emit = Emitter.prototype.emit;
|
||
|
|
||
|
/**
|
||
|
* `Socket` constructor.
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
function Socket (io, nsp, opts) {
|
||
|
this.io = io;
|
||
|
this.nsp = nsp;
|
||
|
this.json = this; // compat
|
||
|
this.ids = 0;
|
||
|
this.acks = {};
|
||
|
this.receiveBuffer = [];
|
||
|
this.sendBuffer = [];
|
||
|
this.connected = false;
|
||
|
this.disconnected = true;
|
||
|
this.flags = {};
|
||
|
if (opts && opts.query) {
|
||
|
this.query = opts.query;
|
||
|
}
|
||
|
if (this.io.autoConnect) this.open();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Mix in `Emitter`.
|
||
|
*/
|
||
|
|
||
|
Emitter(Socket.prototype);
|
||
|
|
||
|
/**
|
||
|
* Subscribe to open, close and packet events
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.subEvents = function () {
|
||
|
if (this.subs) return;
|
||
|
|
||
|
var io = this.io;
|
||
|
this.subs = [
|
||
|
on(io, 'open', bind(this, 'onopen')),
|
||
|
on(io, 'packet', bind(this, 'onpacket')),
|
||
|
on(io, 'close', bind(this, 'onclose'))
|
||
|
];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* "Opens" the socket.
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.open =
|
||
|
Socket.prototype.connect = function () {
|
||
|
if (this.connected) return this;
|
||
|
|
||
|
this.subEvents();
|
||
|
this.io.open(); // ensure open
|
||
|
if ('open' === this.io.readyState) this.onopen();
|
||
|
this.emit('connecting');
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sends a `message` event.
|
||
|
*
|
||
|
* @return {Socket} self
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.send = function () {
|
||
|
var args = toArray(arguments);
|
||
|
args.unshift('message');
|
||
|
this.emit.apply(this, args);
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Override `emit`.
|
||
|
* If the event is in `events`, it's emitted normally.
|
||
|
*
|
||
|
* @param {String} event name
|
||
|
* @return {Socket} self
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.emit = function (ev) {
|
||
|
if (events.hasOwnProperty(ev)) {
|
||
|
emit.apply(this, arguments);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
var args = toArray(arguments);
|
||
|
var packet = {
|
||
|
type: (this.flags.binary !== undefined ? this.flags.binary : hasBin(args)) ? parser.BINARY_EVENT : parser.EVENT,
|
||
|
data: args
|
||
|
};
|
||
|
|
||
|
packet.options = {};
|
||
|
packet.options.compress = !this.flags || false !== this.flags.compress;
|
||
|
|
||
|
// event ack callback
|
||
|
if ('function' === typeof args[args.length - 1]) {
|
||
|
debug('emitting packet with ack id %d', this.ids);
|
||
|
this.acks[this.ids] = args.pop();
|
||
|
packet.id = this.ids++;
|
||
|
}
|
||
|
|
||
|
if (this.connected) {
|
||
|
this.packet(packet);
|
||
|
} else {
|
||
|
this.sendBuffer.push(packet);
|
||
|
}
|
||
|
|
||
|
this.flags = {};
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sends a packet.
|
||
|
*
|
||
|
* @param {Object} packet
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.packet = function (packet) {
|
||
|
packet.nsp = this.nsp;
|
||
|
this.io.packet(packet);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called upon engine `open`.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.onopen = function () {
|
||
|
debug('transport is open - connecting');
|
||
|
|
||
|
// write connect packet if necessary
|
||
|
if ('/' !== this.nsp) {
|
||
|
if (this.query) {
|
||
|
var query = typeof this.query === 'object' ? parseqs.encode(this.query) : this.query;
|
||
|
debug('sending connect packet with query %s', query);
|
||
|
this.packet({type: parser.CONNECT, query: query});
|
||
|
} else {
|
||
|
this.packet({type: parser.CONNECT});
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called upon engine `close`.
|
||
|
*
|
||
|
* @param {String} reason
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.onclose = function (reason) {
|
||
|
debug('close (%s)', reason);
|
||
|
this.connected = false;
|
||
|
this.disconnected = true;
|
||
|
delete this.id;
|
||
|
this.emit('disconnect', reason);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called with socket packet.
|
||
|
*
|
||
|
* @param {Object} packet
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.onpacket = function (packet) {
|
||
|
var sameNamespace = packet.nsp === this.nsp;
|
||
|
var rootNamespaceError = packet.type === parser.ERROR && packet.nsp === '/';
|
||
|
|
||
|
if (!sameNamespace && !rootNamespaceError) return;
|
||
|
|
||
|
switch (packet.type) {
|
||
|
case parser.CONNECT:
|
||
|
this.onconnect();
|
||
|
break;
|
||
|
|
||
|
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);
|
||
|
break;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called upon a server event.
|
||
|
*
|
||
|
* @param {Object} packet
|
||
|
* @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));
|
||
|
}
|
||
|
|
||
|
if (this.connected) {
|
||
|
emit.apply(this, args);
|
||
|
} else {
|
||
|
this.receiveBuffer.push(args);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Produces an ack callback to emit with an event.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.ack = function (id) {
|
||
|
var self = this;
|
||
|
var sent = false;
|
||
|
return function () {
|
||
|
// prevent double callbacks
|
||
|
if (sent) return;
|
||
|
sent = true;
|
||
|
var args = toArray(arguments);
|
||
|
debug('sending ack %j', args);
|
||
|
|
||
|
self.packet({
|
||
|
type: hasBin(args) ? parser.BINARY_ACK : parser.ACK,
|
||
|
id: id,
|
||
|
data: args
|
||
|
});
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called upon a server acknowlegement.
|
||
|
*
|
||
|
* @param {Object} 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 server connect.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.onconnect = function () {
|
||
|
this.connected = true;
|
||
|
this.disconnected = false;
|
||
|
this.emit('connect');
|
||
|
this.emitBuffered();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Emit buffered events (received and emitted).
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.emitBuffered = function () {
|
||
|
var i;
|
||
|
for (i = 0; i < this.receiveBuffer.length; i++) {
|
||
|
emit.apply(this, this.receiveBuffer[i]);
|
||
|
}
|
||
|
this.receiveBuffer = [];
|
||
|
|
||
|
for (i = 0; i < this.sendBuffer.length; i++) {
|
||
|
this.packet(this.sendBuffer[i]);
|
||
|
}
|
||
|
this.sendBuffer = [];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called upon server disconnect.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.ondisconnect = function () {
|
||
|
debug('server disconnect (%s)', this.nsp);
|
||
|
this.destroy();
|
||
|
this.onclose('io server disconnect');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called upon forced client/server side disconnections,
|
||
|
* this method ensures the manager stops tracking us and
|
||
|
* that reconnections don't get triggered for this.
|
||
|
*
|
||
|
* @api private.
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.destroy = function () {
|
||
|
if (this.subs) {
|
||
|
// clean subscriptions to avoid reconnections
|
||
|
for (var i = 0; i < this.subs.length; i++) {
|
||
|
this.subs[i].destroy();
|
||
|
}
|
||
|
this.subs = null;
|
||
|
}
|
||
|
|
||
|
this.io.destroy(this);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Disconnects the socket manually.
|
||
|
*
|
||
|
* @return {Socket} self
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.close =
|
||
|
Socket.prototype.disconnect = function () {
|
||
|
if (this.connected) {
|
||
|
debug('performing disconnect (%s)', this.nsp);
|
||
|
this.packet({ type: parser.DISCONNECT });
|
||
|
}
|
||
|
|
||
|
// remove socket from pool
|
||
|
this.destroy();
|
||
|
|
||
|
if (this.connected) {
|
||
|
// fire events
|
||
|
this.onclose('io client disconnect');
|
||
|
}
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sets the compress flag.
|
||
|
*
|
||
|
* @param {Boolean} if `true`, compresses the sending data
|
||
|
* @return {Socket} self
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.compress = function (compress) {
|
||
|
this.flags.compress = compress;
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sets the binary flag
|
||
|
*
|
||
|
* @param {Boolean} whether the emitted data contains binary
|
||
|
* @return {Socket} self
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Socket.prototype.binary = function (binary) {
|
||
|
this.flags.binary = binary;
|
||
|
return this;
|
||
|
};
|