@wearemothership/socket.io-stream
Version:
288 lines (240 loc) • 6.5 kB
JavaScript
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var bind = require('component-bind');
var IOStream = require('./iostream');
var parser = require('./parser');
var debug = require('debug')('socket.io-stream:socket');
var emit = EventEmitter.prototype.emit;
var on = EventEmitter.prototype.on;
var slice = Array.prototype.slice;
exports = module.exports = Socket;
/**
* Base event name for messaging.
*
* @api public
*/
exports.event = '$stream';
exports.events = [
'error',
'newListener',
'removeListener'
];
util.inherits(Socket, EventEmitter);
/**
* Bidirectional stream socket which wraps Socket.IO.
*
* @param {socket.io#Socket} socket.io
* @api public
*/
function Socket(sio, options) {
if (!(this instanceof Socket)) {
return new Socket(sio, options);
}
EventEmitter.call(this);
options = options || {};
this.sio = sio;
this.forceBase64 = !!options.forceBase64;
this.streams = {};
this.encoder = new parser.Encoder();
this.decoder = new parser.Decoder();
var eventName = exports.event;
sio.on(eventName, bind(this, emit));
sio.on(eventName + '-read', bind(this, '_onread'));
sio.on(eventName + '-write', bind(this, '_onwrite'));
sio.on(eventName + '-end', bind(this, '_onend'));
sio.on(eventName + '-error', bind(this, '_onerror'));
sio.on('error', bind(this, emit, 'error'));
sio.on('disconnect', bind(this, '_ondisconnect'));
this.encoder.on('stream', bind(this, '_onencode'));
this.decoder.on('stream', bind(this, '_ondecode'));
}
/**
* Original emit function.
*
* @api private
*/
Socket.prototype.$emit = emit;
/**
* Emits streams to this corresponding server/client.
*
* @return {Socket} self
* @api public
*/
Socket.prototype.emit = function(type) {
if (~exports.events.indexOf(type)) {
return emit.apply(this, arguments);
}
this._stream.apply(this, arguments);
return this;
};
Socket.prototype.on = function(type, listener) {
if (~exports.events.indexOf(type)) {
return on.apply(this, arguments);
}
this._onstream(type, listener);
return this;
};
/**
* Sends a new stream request.
*
* @param {String} event type
* @api private
*/
Socket.prototype._stream = function(type) {
debug('sending new streams');
var self = this;
var args = slice.call(arguments, 1);
var ack = args[args.length - 1];
if ('function' == typeof ack) {
args[args.length - 1] = function() {
var args = slice.call(arguments);
args = self.decoder.decode(args);
ack.apply(this, args);
};
}
args = this.encoder.encode(args);
var sio = this.sio;
sio.emit.apply(sio, [exports.event, type].concat(args));
};
/**
* Notifies the read event.
*
* @api private
*/
Socket.prototype._read = function(id, size) {
this.sio.emit(exports.event + '-read', id, size);
};
/**
* Requests to write a chunk.
*
* @api private
*/
Socket.prototype._write = function(id, chunk, encoding, callback) {
if (Buffer.isBuffer(chunk)) {
if (this.forceBase64) {
encoding = 'base64';
chunk = chunk.toString(encoding);
} else if (!global.Buffer) {
// socket.io can't handle Buffer when using browserify.
if (chunk.toArrayBuffer) {
chunk = chunk.toArrayBuffer();
} else {
chunk = chunk.buffer;
}
}
}
this.sio.emit(exports.event + '-write', id, chunk, encoding, callback);
};
Socket.prototype._end = function(id) {
this.sio.emit(exports.event + '-end', id);
};
Socket.prototype._error = function(id, err) {
this.sio.emit(exports.event + '-error', id, err.message || err);
};
/**
* Handles a new stream request.
*
* @param {String} event type
* @param {Function} listener
*
* @api private
*/
Socket.prototype._onstream = function(type, listener) {
if ('function' != typeof listener) {
throw TypeError('listener must be a function');
}
function onstream() {
debug('new streams');
var self = this;
var args = slice.call(arguments);
var ack = args[args.length - 1];
if ('function' == typeof ack) {
args[args.length - 1] = function() {
var args = slice.call(arguments);
args = self.encoder.encode(args);
ack.apply(this, args);
};
}
args = this.decoder.decode(args);
listener.apply(this, args);
}
// for removeListener
onstream.listener = listener;
on.call(this, type, onstream);
};
Socket.prototype._onread = function(id, size) {
debug('read: "%s"', id);
var stream = this.streams[id];
if (stream) {
stream._onread(size);
} else {
debug('ignore invalid stream id');
}
};
Socket.prototype._onwrite = function(id, chunk, encoding, callback) {
debug('write: "%s"', id);
var stream = this.streams[id];
if (!stream) {
callback('invalid stream id: ' + id);
return;
}
if (global.ArrayBuffer && chunk instanceof ArrayBuffer) {
// make sure that chunk is a buffer for stream
chunk = new Buffer(new Uint8Array(chunk));
}
stream._onwrite(chunk, encoding, callback);
};
Socket.prototype._onend = function(id) {
debug('end: "%s"', id);
var stream = this.streams[id];
if (!stream) {
debug('ignore non-existent stream id: "%s"', id);
return;
}
stream._end();
};
Socket.prototype._onerror = function(id, message) {
debug('error: "%s", "%s"', id, message);
var stream = this.streams[id];
if (!stream) {
debug('invalid stream id: "%s"', id);
return;
}
var err = new Error(message);
err.remote = true;
stream.emit('error', err);
};
Socket.prototype._ondisconnect = function() {
var stream;
for (var id in this.streams) {
stream = this.streams[id];
stream.destroy();
// Close streams when the underlaying
// socket.io connection is closed (regardless why)
stream.emit('close');
stream.emit('error', new Error('Connection aborted'));
}
};
Socket.prototype._onencode = function(stream) {
if (stream.socket || stream.destroyed) {
throw new Error('stream has already been sent.');
}
var id = stream.id;
if (this.streams[id]) {
throw new Error('Encoded stream already exists: ' + id);
}
this.streams[id] = stream;
stream.socket = this;
};
Socket.prototype._ondecode = function(stream) {
var id = stream.id;
if (this.streams[id]) {
this._error(id, new Error('Decoded stream already exists: ' + id));
return;
}
this.streams[id] = stream;
stream.socket = this;
};
Socket.prototype.cleanup = function(id) {
delete this.streams[id];
};