stream-flow-control
Version:
Stream Flow Control
171 lines (157 loc) • 6 kB
JavaScript
const {Writable, Readable} = require('stream');
const {Manager} = require('../manager/manager.js');
const uuid = require('uuid');
/**
* Check all piped streams, and flow data to the first that matches criteria.
*
* @extends Writable
*/
class FlowOne extends Writable {
/**
* Create a FlowOne stream
* @param {object} [options] Global options
* @param {string} [options.name] Name for this stream
*/
constructor(options) {
options = {...options, objectMode: true};
super(options);
this.options = options;
this._shuffle = options.shuffle;
this._readers = [];
this._corking_readers={};
this._messages = {};
this._none_messages = [];
this._none_reader = null;
this.on('finish', () => {
this._readers.forEach(rdr=>rdr.reader.push(null));
if(this._none_reader) this._none_reader.push(null);
});
this.type = 'FlowOne';
if(this.options.name) Manager.set(this.type, this);
}
/**
* Set stream(s) to pipe if criteria is match
* @param {Condition} cond
* @param {(Writable|Writable[])} dst
* @returns {(Writable|FlowAll)} if dst is an array, then returns "this", else dst is returned
*/
when(cond, dst) {
if(typeof cond != 'function') this.emit('error', '"when" first argument must be a function');
const id = uuid.v4();
const self = this;
const reader = new Readable({
objectMode: true,
read() {
if(self._corking_readers[id]) {
self.uncork();
delete self._corking_readers[id];
}
}
});
reader._readableState.sync = false;
const pipes = Array.isArray(dst)?dst:[dst];
pipes.forEach(dst=>reader.pipe(dst));
const pos = this._readers.push({
cond,
reader,
id
});
reader.options = {...reader.options||{}, name: this.options.name, port: 'when'+(pos-1)};
return Array.isArray(dst)?this:dst;
}
/**
* Set stream(s) to pipe unconditionally
* @param {(Writable|Writable[])} dst
* @returns {(Writable|FlowAll)} if dst is an array, then returns "this", else dst is returned
*/
pipe(dst) {
const id = uuid.v4();
const self = this;
const reader = new Readable({
objectMode: true,
read() {
if(self._writableState.corked && self._corking_readers[id]) {
self.uncork();
delete self._corking_readers[id];
}
}
});
reader._readableState.sync = false;
const pipes = Array.isArray(dst)?dst:[dst];
pipes.forEach(dst=>reader.pipe(dst));
this._readers.push({
reader,
id
});
reader.options = {...reader.options||{}, name: this.options.name};
return Array.isArray(dst)?this:dst;
}
/**
* Set stream(s) to pipe when no other stream matched criteria. If unconditional piping was set, this will never be used.
* @param {(Writable|Writable[])} dst Destination stream
* @returns {(Writable|FlowAll)} if dst is an array, then returns "this", else dst is returned
*/
none(dst) {
if(!this._none_reader) {
const self = this;
this._none_reader = new Readable({
objectMode: true,
read() {
if(self._writableState.corked && self._corking_readers['none']) {
self.uncork();
delete self._corking_readers['none'];
}
}
});
this._none_reader._readableState.sync = false;
this._none_reader.options = {...this._none_reader.options||{}, name: this.options.name, port: 'none'};
this._none_reader.on('resume', ()=>this.uncork());
}
const pipes = Array.isArray(dst)?dst:[dst];
pipes.forEach(dst=>this._none_reader.pipe(dst));
return Array.isArray(dst)?this:dst;
}
/**
* Internal _write method. Do not call directly.
* Do not override unless you are sure of what you are doing
* @param {(DataWrapper|*)} payload chunk of data to be written
* @param {string} encoding encoding of data when objectMode=false. Never used.
* @param {WriteCallback} cb Internal Writable callback
*/
_write(payload, encoding, cb) {
var delivered = false;
if(this.goal) {
// Is goal player
payload = payload.getChild(this);
}
if(this._shuffle) {
for (let i = this._readers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this._readers[i], this._readers[j]] = [this._readers[j], this._readers[i]];
}
}
for (let i = 0; i < this._readers.length; i++) {
const rdr = this._readers[i];
if(!rdr.hasOwnProperty('cond') || rdr.cond(payload)) {
delivered = true;
if(!rdr.reader.push(payload) && !this._corking_readers[rdr.id]) {
this.cork();
this._corking_readers[rdr.id] = true;
}
break;
}
}
if(!delivered && this._none_reader) {
if(this.options.none_reason) {
let data = typeof this.options.none_reason == 'function'?this.options.none_reason(payload):this.options.none_reason;
payload = (this.goal)?payload.getChild(this).setData(data):data;
}
if(!this._none_reader.push(payload) && !this._corking_readers['none']) {
this.cork();
this._corking_readers['none'] = true;
}
}
cb();
}
}
module.exports.FlowOne = FlowOne;