nntp-server
Version:
NNTP server implementation.
172 lines (133 loc) • 4.41 kB
JavaScript
// Transform stream that concatenates and unfolds all strings in input
//
// Each input element could be either:
//
// - String
// - Stream of strings in object mode (strings only; null and arrays are not allowed)
// - null (ends the stream)
// - Array with any combinations of the above
//
// This stream inserts CRLF after each string/buffer, each array element and
// each chunk in the nested object stream.
//
'use strict';
const Denque = require('denque');
const stream = require('stream');
const STATE_IDLE = 0; // no data in queue
const STATE_WRITE = 1; // writing a string to the output
const STATE_FLOWING = 2; // piping one of input streams to the output
const STATE_PAUSED = 3; // output stream does not accept more data
class FlattenStream extends stream.Duplex {
constructor(options) {
super(Object.assign({}, options, {
writableObjectMode: true,
readableObjectMode: false,
allowHalfOpen: false
}));
this.queue = new Denque();
this.state = STATE_IDLE;
this.top_chunk_stream = null;
this.top_chunk_callback = null;
this.top_chunk_read_fn = null;
this.stream_ended = false;
}
// Recursive function to add data to internal queue
//
_add_data(data, fn) {
if (Array.isArray(data)) {
// Flatten any arrays, callback is called when last element is processed
data.forEach((el, idx) => this._add_data(el, (idx === data.length - 1 ? fn : null)));
return;
}
this.queue.push([ data, fn ]);
}
_write(data, encoding, callback) {
this._add_data(data, callback);
if (this.state === STATE_IDLE) this._read();
}
destroy() {
if (this.stream_ended) return;
this.stream_ended = true;
this.push(null);
if (this.top_chunk_stream && typeof this.top_chunk_stream.destroy === 'function') {
this.top_chunk_stream.destroy();
}
while (!this.queue.isEmpty()) {
let data = this.queue.shift()[0];
if (data && typeof data.destroy === 'function') data.destroy();
}
}
_read() {
for (;;) {
if (this.state === STATE_WRITE) {
if (this.top_chunk_callback) {
this.top_chunk_callback();
}
this.state = STATE_IDLE;
this.top_chunk_callback = null;
} else if (this.state === STATE_FLOWING) {
this.top_chunk_read_fn();
return;
} else if (this.state === STATE_PAUSED) {
this.state = STATE_FLOWING;
this.top_chunk_read_fn();
return;
}
if (this.queue.isEmpty()) break;
let [ data, callback ] = this.queue.shift();
if (data && typeof data.on === 'function') {
// looks like data is a stream
this.state = STATE_FLOWING;
this.top_chunk_stream = data;
this.top_chunk_callback = callback;
this.top_chunk_read_fn = () => {
if (this.state !== STATE_FLOWING) return;
for (;;) {
let chunk = data.read();
if (chunk === null) {
// no more data is available yet
break;
}
if (this.stream_ended) break;
if (!this.push(String(chunk) + '\r\n')) {
this.state = STATE_PAUSED;
break;
}
}
};
data.on('readable', this.top_chunk_read_fn);
this.top_chunk_read_fn();
stream.finished(data, err => {
data.removeListener('readable', this.top_chunk_read_fn);
if (err) {
this.destroy();
return;
}
if (this.top_chunk_callback) {
this.top_chunk_callback();
}
this.state = STATE_IDLE;
this.top_chunk_stream = null;
this.top_chunk_callback = null;
this.top_chunk_read_fn = null;
this._read();
});
break;
} else {
// regular data chunk (null, string, buffer)
this.state = STATE_WRITE;
this.top_chunk_callback = callback;
if (data === null) {
// signal to end this stream
this.destroy();
break;
} else {
/* eslint-disable no-lonely-if */
// string (or mistakenly pushed numbers and such)
if (!this.push(String(data) + '\r\n')) break;
}
}
}
}
}
module.exports = (...args) => new FlattenStream(...args);