@discoveryjs/cli
Version:
CLI tools to serve & build projects based on Discovery.js
205 lines (166 loc) • 6.61 kB
JavaScript
const { Readable } = require('stream');
function isReadableStream(value) {
return (
typeof value.pipe === 'function' &&
typeof value._read === 'function' &&
typeof value._readableState === 'object' && value._readableState !== null
);
}
module.exports = class ChunkedContent extends Readable {
constructor(content) {
super({
autoDestroy: true
});
this.values = new Map();
this.content = content;
this.contentChunks = null;
this.error = null;
this._processing = false;
this._ended = false;
this._readSize = 0;
this._buffer = '';
}
replace(pattern, fn) {
this.contentChunks = null;
this.content = this.content.replace(pattern, (...args) => {
let chunks = fn(...args);
if (!Array.isArray(chunks)) {
chunks = [chunks];
}
return chunks.map(chunk => {
if (chunk === null || typeof chunk !== 'object') {
return chunk;
}
const key = '{{__discovery_placeholder_' + this.values.size + '}}';
this.values.set(key, chunk);
return key;
}).join('');
});
return this;
}
get currentChunk() {
return this.contentChunks && this.contentChunks[0] || null;
}
processChunks() {
if (this._processing || this._ended) {
return;
}
try {
this._processing = true;
while (this.currentChunk !== null && !this.currentChunk.awaiting) {
const current = this.currentChunk;
const value = current.value;
switch (true) {
default:
this.push(value);
this.contentChunks.shift();
break;
case value && typeof value.then === 'function':
this.currentChunk.awaiting = true;
Promise.resolve(value)
.then(resolved => {
this.contentChunks[0] = {
value: resolved
};
this.processChunks();
})
.catch(error => {
this.destroy(error);
});
break;
case isReadableStream(value): {
if (!current.readStream) {
if (value.readableEnded) {
return this.destroy(new Error('Readable Stream has ended before it was serialized. All stream data have been lost'));
}
if (value.readableFlowing) {
return this.destroy(new Error('Readable Stream is in flowing mode, data may have been lost. Trying to pause stream.'));
}
const continueProcessing = () => {
if (current.awaiting) {
current.awaiting = false;
this.processChunks();
}
};
value.once('error', error => this.destroy(error));
value.once('end', continueProcessing);
value.on('readable', continueProcessing);
current.first = false;
current.awaiting = !value.readable || value.readableLength === 0;
current.readStream = () => {
const data = value.read(this._readSize);
if (data !== null) {
current.first = false;
this.push(data);
} else {
if (current.first && !value._readableState.reading) {
this.contentChunks.shift();
} else {
current.first = true;
current.awaiting = true;
}
}
};
} else if (!current.awaiting) {
current.readStream();
}
break;
}
}
if (!this._processing) {
return;
}
}
this._processing = false;
} catch (error) {
this.destroy(error);
return;
}
if (this.currentChunk === null && !this._ended) {
this._finish();
this.push(null);
}
}
push(data) {
if (data !== null) {
this._buffer += data;
// check buffer overflow
if (this._buffer.length < this._readSize) {
return;
}
// flush buffer
data = this._buffer;
this._buffer = '';
this._processing = false;
}
super.push(data);
}
_read(size) {
if (this.contentChunks === null) {
this.contentChunks = this.content
.split(/(\{\{__discovery_placeholder_\d+\}\})/)
.map(chunk => ({
value: this.values.has(chunk) ? this.values.get(chunk) : chunk
}));
}
// start processing
this._readSize = size || this.readableHighWaterMark;
this.processChunks();
}
_finish() {
this._ended = true;
if (this._buffer && this._buffer.length) {
super.push(this._buffer); // flush buffer
}
this._buffer = '';
}
_destroy(error, cb) {
this.error = this.error || error;
this._finish();
cb(error);
}
};
// const x = new (module.exports)('asdXXfsa234adadXXasd33')
// .replace(/\d+/g, (m) => Promise.resolve(m).then(x => '[[!' + x + ']]'))
// .replace(/XX/g, () => ['>', Readable.from(['(a)', Promise.resolve('(c)'), new Promise(resolve => setTimeout(() => resolve('!!'), 1000))], {objectMode: true}), '<']);
// x.pipe(process.stdout);