tailorx
Version:
Tailor assembles a web page from multiple fragments
140 lines (117 loc) • 3.7 kB
JavaScript
'use strict';
const stream = require('stream');
const BotDetector = require('device-detector-js/dist/parsers/bot');
const botDetector = new BotDetector();
/**
* The goals of this stream is to change behaviour of the Tailor when dealing with SEO/SM bots.
* While it's ok to not to throw an error if non-primary fragment fails when dealing with browser -
* it's not acceptable to bots. This is because browser can attempt to render missing page
* parts at he client while bots can't do so due to the limited JS support.
*
* @type {module.StringifierStream}
*/
module.exports = class StringifierStream extends stream.Transform {
#response;
#isBot = false;
#fragmentsCounter = 0;
#fragmentFound = false;
#batch = [];
#erroredState = false;
#writeHeadArgs = null;
set statusCode(v) {
this.statusCodeVal = v;
if (this.#isBot) {
return;
}
this.#response.statusCode = v;
}
get statusCode() {
return this.statusCodeVal;
}
/**
* @param {boolean} enabled
* @param {http.IncomingHttpHeaders} reqHeaders
* @param {http.ServerResponse} res
*/
constructor(enabled, reqHeaders, res) {
super({ objectMode: true, autoDestroy: true });
this.#isBot = enabled
? botDetector.parse(reqHeaders['user-agent']) !== null
: false;
this.#response = res;
}
_transform(chunk, encoding, done) {
if (!this.#isBot) {
this.push(chunk, encoding);
return done();
}
// Stop accepting new data in errored state.
// Default error handler should generate response
if (this.#erroredState) {
return done();
}
this.#batch.push(chunk);
if (this.#fragmentsCounter === 0 && this.#fragmentFound) {
this.#flushBuffer();
}
done();
}
_flush(done) {
//Not allowing to send broken response to Bot
if (this.#erroredState) {
this.#batch = [];
this.end();
return;
}
this.#flushBuffer();
done();
}
addFragment(fragment) {
if (!this.#isBot) {
return;
}
this.#fragmentsCounter++;
this.#fragmentFound = true;
fragment.once('response', () => this.#fragmentsCounter--);
fragment.prependOnceListener('error', this.#handleFragmentError);
}
/**
* Not allowing to write headers before all fragments will respond if dealing with Bot
* Handling case when we received them from primary fragment & non-primary one fails afterwards.
* @param args
* @returns {*}
*/
writeHead(...args) {
if (!this.#isBot) {
return this.#response.writeHead(...args);
}
this.#writeHeadArgs = args;
return this.#response;
}
#flushBuffer = () => {
this.#flushHead();
this.#batch.forEach(v => this.push(v));
this.#batch = [];
};
#flushHead = () => {
if (this.statusCode) {
this.#response.statusCode = this.statusCode;
}
if (this.#writeHeadArgs !== null) {
this.#response.writeHead(...this.#writeHeadArgs);
this.#writeHeadArgs = null;
}
};
#handleFragmentError = () => {
if (!this.#erroredState) {
this.emit(
'error',
new Error(
'Fragment error while processing request from SEO/SM bot. See adjacent messages for real cause.'
)
);
}
this.#erroredState = true;
this.#batch = [];
};
};