node-tailor
Version:
Tailor assembles a web page from multiple fragments
149 lines (127 loc) • 5.45 kB
JavaScript
;
const AsyncStream = require('./streams/async-stream');
const Fragment = require('./fragment');
const StringifierStream = require('./streams/stringifier-stream');
const ContentLengthStream = require('./streams/content-length-stream');
const FRAGMENT_EVENTS = ['start', 'response', 'end', 'error', 'timeout', 'fallback', 'warn'];
module.exports = function processRequest (options, request, response) {
this.emit('start', request);
const fetchContext = options.fetchContext;
const fetchTemplate = options.fetchTemplate;
const handleTag = options.handleTag;
const parseTemplate = options.parseTemplate;
const requestFragment = options.requestFragment;
const pipeInstanceName = options.pipeInstanceName();
const pipeDefinition = options.pipeDefinition(pipeInstanceName);
const asyncStream = new AsyncStream();
const contextPromise = fetchContext(request).catch((err) => {
this.emit('context:error', request, err);
return {};
});
const templatePromise = fetchTemplate(request, parseTemplate);
const responseHeaders = {
// Disable cache in browsers and proxies
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Content-Type': 'text/html'
};
let shouldWriteHead = true;
let index = 0;
contextPromise.then((context) => {
const contentLengthStream = new ContentLengthStream((contentLength) => {
this.emit('end', request, contentLength);
});
const resultStream = new StringifierStream((tag) => {
if (tag.placeholder === 'pipe') {
return pipeDefinition;
}
if (tag.placeholder === 'async') {
// end of body tag
return asyncStream;
}
if (tag.name === options.fragmentTag) {
const fragment = new Fragment(
tag,
context,
index++,
requestFragment,
pipeInstanceName
);
FRAGMENT_EVENTS.forEach((eventName) => {
fragment.on(eventName, (function () {
// this has to be a function, because
// arrow functions don't have `arguments`
const prefixedName = 'fragment:' + eventName;
const prefixedArgs = [prefixedName, request, fragment.attributes].concat(...arguments);
this.emit.apply(this, prefixedArgs);
}).bind(this));
});
if (fragment.attributes.async) {
asyncStream.write(fragment.stream);
}
if (fragment.attributes.primary && shouldWriteHead) {
shouldWriteHead = false;
fragment.on('response', (statusCode, headers) => {
if (headers.location) {
responseHeaders['Location'] = headers.location;
}
this.emit('response', request, statusCode, responseHeaders);
response.writeHead(statusCode, responseHeaders);
resultStream
.pipe(contentLengthStream)
.pipe(response);
});
fragment.on('fallback', (err) => {
this.emit('error', request, err);
response.writeHead(500, responseHeaders);
resultStream
.pipe(contentLengthStream)
.pipe(response);
});
fragment.on('error', (err) => {
this.emit('error', request, err);
response.writeHead(500, responseHeaders);
response.end();
});
}
return fragment.fetch(request, false);
}
return handleTag(request, tag);
});
resultStream.on('finish', () => {
asyncStream.end();
const statusCode = response.statusCode || 200;
if (shouldWriteHead) {
shouldWriteHead = false;
this.emit('response', request, statusCode, responseHeaders);
response.writeHead(statusCode, responseHeaders);
resultStream
.pipe(contentLengthStream)
.pipe(response);
}
});
resultStream.on('error', (err) => {
this.emit('error', request, err);
if (shouldWriteHead) {
shouldWriteHead = false;
response.writeHead(500, responseHeaders);
response.end();
} else {
contentLengthStream.end();
}
});
templatePromise
.then((template) => {
if (template.statusCode) {
response.statusCode = template.statusCode;
}
template.on('error', (err) => {
resultStream.emit('error', err);
});
template.pipe(resultStream);
})
.catch((err) => {
resultStream.emit('error', err);
});
});
};