@wearemothership/socket.io-stream
Version:
266 lines (220 loc) • 5.27 kB
JavaScript
var util = require('util');
var Duplex = require('stream').Duplex;
var bind = require('component-bind');
var uuid = require('./uuid');
var debug = require('debug')('socket.io-stream:iostream');
module.exports = IOStream;
util.inherits(IOStream, Duplex);
/**
* Duplex
*
* @param {Object} options
* @api private
*/
function IOStream(options) {
if (!(this instanceof IOStream)) {
return new IOStream(options);
}
IOStream.super_.call(this, options);
this.options = options;
this.id = uuid();
this.socket = null;
// Buffers
this.pushBuffer = [];
this.writeBuffer = [];
// Op states
this._readable = false;
this._writable = false;
this.destroyed = false;
// default to *not* allowing half open sockets
this.allowHalfOpen = options && options.allowHalfOpen || false;
this.on('finish', this._onfinish);
this.on('end', this._onend);
this.on('error', this._onerror);
}
/**
* Ensures that no more I/O activity happens on this stream.
* Not necessary in the usual case.
*
* @api public
*/
IOStream.prototype.destroy = function() {
debug('destroy');
if (this.destroyed) {
debug('already destroyed');
return;
}
this.readable = this.writable = false;
if (this.socket) {
debug('clean up');
this.socket.cleanup(this.id);
this.socket = null;
}
this.destroyed = true;
};
/**
* Local read
*
* @api private
*/
IOStream.prototype._read = function(size) {
var push;
// We can not read from the socket if it's destroyed obviously ...
if (this.destroyed) return;
if (this.pushBuffer.length) {
// flush buffer and end if it exists.
while (push = this.pushBuffer.shift()) {
if (!push()) break;
}
return;
}
this._readable = true;
// Go get data from remote stream
// Calls
// ._onread remotely
// then
// ._onwrite locally
this.socket._read(this.id, size);
};
/**
* Read from remote stream
*
* @api private
*/
IOStream.prototype._onread = function(size) {
var write = this.writeBuffer.shift();
if (write) return write();
this._writable = true;
};
/**
* Write local data to remote stream
* Calls
* remtote ._onwrite
*
* @api private
*/
IOStream.prototype._write = function(chunk, encoding, callback) {
var self = this;
function write() {
// We can not write to the socket if it's destroyed obviously ...
if (self.destroyed) return;
self._writable = false;
self.socket._write(self.id, chunk, encoding, callback);
}
if (this._writable) {
write();
} else {
this.writeBuffer.push(write);
}
};
/**
* Write the data fetched remotely
* so that we can now read locally
*
* @api private
*/
IOStream.prototype._onwrite = function(chunk, encoding, callback) {
var self = this;
function push() {
self._readable = false;
var ret = self.push(chunk || '', encoding);
callback();
return ret;
}
if (this._readable) {
push();
} else {
this.pushBuffer.push(push);
}
};
/**
* When ending send 'end' event to remote stream
*
* @api private
*/
IOStream.prototype._end = function() {
if (this.pushBuffer.length) {
// end after flushing buffer.
this.pushBuffer.push(bind(this, '_done'));
} else {
this._done();
}
};
/**
* Remote stream just ended
*
* @api private
*/
IOStream.prototype._done = function() {
this._readable = false;
// signal the end of the data.
return this.push(null);
};
/**
* the user has called .end(), and all the bytes have been
* sent out to the other side.
* If allowHalfOpen is false, or if the readable side has
* ended already, then destroy.
* If allowHalfOpen is true, then we need to set writable false,
* so that only the writable side will be cleaned up.
*
* @api private
*/
IOStream.prototype._onfinish = function() {
debug('_onfinish');
// Local socket just finished
// send 'end' event to remote
if (this.socket) {
this.socket._end(this.id);
}
this.writable = false;
this._writableState.ended = true;
if (!this.readable || this._readableState.ended) {
debug('_onfinish: ended, destroy %s', this._readableState);
return this.destroy();
}
debug('_onfinish: not ended');
if (!this.allowHalfOpen) {
this.push(null);
// just in case we're waiting for an EOF.
if (this.readable && !this._readableState.endEmitted) {
this.read(0);
}
}
};
/**
* the EOF has been received, and no more bytes are coming.
* if the writable side has ended already, then clean everything
* up.
*
* @api private
*/
IOStream.prototype._onend = function() {
debug('_onend');
this.readable = false;
this._readableState.ended = true;
if (!this.writable || this._writableState.finished) {
debug('_onend: %s', this._writableState);
return this.destroy();
}
debug('_onend: not finished');
if (!this.allowHalfOpen) {
this.end();
}
};
/**
* When error in local stream
* notyify remote
* if err.remote = true
* then error happened on remote stream
*
* @api private
*/
IOStream.prototype._onerror = function(err) {
// check if the error came from remote stream.
if (!err.remote && this.socket) {
// notify the error to the corresponding remote stream.
this.socket._error(this.id, err);
}
this.destroy();
};