stream-flow-control
Version:
Stream Flow Control
220 lines (200 loc) • 7.69 kB
JavaScript
const {Writable, Readable} = require('stream');
const {Manager} = require('../manager/manager.js');
const uuid = require('uuid');
// /*
// * Example of yaml representation:
// *
// * ````yaml
// * stream_name:
// * type: FlowAll
// * options:
// * none_message: 'some message' #name of the option
// * methods:
// * write: #Override default write (not recomended)
// * code: [array of strings]
// * params: [array of param names]
// * when: #Conditional piping
// * cond: [array of strings] #Conditional piping (always receives payload as parameter)
// * dst: #Where to pipe (can be an array of destinations)
// * type: {destination type}
// * name: {destination name}
// * pipe: #unconditional piping
// * type: {destination type}
// * name: {destination name}
// * none: #Where to pipe when no matches where found
// * type: {destination type}
// * name: {destination name}
// * on: #add listener
// * finish: #event name
// * code: [array of strings]
// * params: [array of param names]
// * pon: #prepend listener
// * prefinish: #event name
// * code: [array of strings]
// * params: [array of param names]
// * once: #add once listener
// * finish: #event name
// * code: [array of strings]
// * params: [array of param names]
// * ponce: #prepend once listener
// * prefinish: #event name
// * code: [array of strings]
// * params: [array of param names]
// * ````
// *
// */
/**
* Check all piped streams, and flow data to all which matches criteria.
*
* @extends Writable
*/
class FlowAll extends Writable {
/**
* Create a FlowAll 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._cnd_readers = [];
this._uncnd_reader = null;
this._none_reader = null;
this.on('finish', () => {
this._cnd_readers.forEach(rdr=>rdr.reader.push(null));
if(this._uncnd_reader) this._uncnd_reader.push(null);
if(this._none_reader) this._none_reader.push(null);
});
this._corking_readers={};
this.type = 'FlowAll';
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 self = this;
const id = uuid.v4();
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._cnd_readers.push({
cond,
id,
reader
});
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) {
if(!this._uncnd_reader) {
const self = this;
this._uncnd_reader = new Readable({
objectMode: true,
read() {
if(self._writableState.corked && self._corking_readers['uncnd']) {
self.uncork();
delete self._corking_readers['uncnd'];
}
}
});
this._uncnd_reader._readableState.sync = false;
this._uncnd_reader.options = {...this._uncnd_reader.options||{}, name: this.options.name};
this._uncnd_reader.on('resume', ()=>this.uncork());
}
const pipes = Array.isArray(dst)?dst:[dst];
pipes.forEach(dst=>this._uncnd_reader.pipe(dst));
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.options = {...this._none_reader.options||{}, name: this.options.name, port: 'none'};
this._none_reader._readableState.sync = false;
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);
}
this._cnd_readers.forEach(cr => {
if(cr.cond(payload)) {
delivered = true;
if(!cr.reader.push(payload) && !this._corking_readers[cr.id]) {
this.cork();
this._corking_readers[cr.id] = true;
}
}
});
if(this._uncnd_reader) {
delivered = true;
if(!this._uncnd_reader.push(payload) && !this._corking_readers['uncnd']) {
this.cork();
this._corking_readers['uncnd'] = true;
}
}
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();
}
end() {
this._cnd_readers.forEach(cnd => cnd.reader.push(null));
if(this._uncnd_reader) this._uncnd_reader.push(null);
if(this._none_reader) this._none_reader.push(null);
}
}
module.exports.FlowAll = FlowAll;