starty
Version:
a lightweight framework which helps building modular and configurable Node.js applications
282 lines (215 loc) • 9.05 kB
JavaScript
const crypto = require('crypto');
const {loggerBuilder, logLevels} = require("../../../logging");
const {messageBuilder} = require('./message');
const {requestsCacheBuilder} = require('./requests-cache');
const {unpackRequest, clearUrlFromQuery, checkAggregateEnvelope} = require('./prepare');
const requestsCache = requestsCacheBuilder().build();
const finishRequestLogger = loggerBuilder()
.name('output')
.level(logLevels.INFO)
.build();
function finishRequest(envelope){
let origRequest = requestsCache.pop(envelope.msgId);
if (origRequest !== undefined){
let {error, message, receiver} = origRequest;
if (error !== undefined){
finishRequestLogger.error(`Request ${envelope.msgId} couldn't be found in cache. It may be finished before.`);
} else {
let {response} = message;
response.statusCode = envelope.response.statusCode;
for (let header in envelope.headers){
response.setHeader(header, envelope.headers[header]);
}
try{
response.write(envelope.payload);
response.end();
} catch (ex){
requestsCache.put(envelope.msgId, origRequest.message, `error_${envelope.msgId}`);
finishRequestLogger.error(`Error writing the response: ${ex}`);
fail(500, envelope, ex);
}
let status = envelope.response.statusCode == 200 ? 'OK' : 'ERROR';
finishRequestLogger.info(`[${receiver}] Request ${envelope.msgId} completed with status ${status}`);
}
}
}
function fail(statusCode, envelope, error){
if (envelope.response !== undefined){
envelope.response.statusCode = statusCode;
if (envelope.response.headers === undefined){
envelope.response.headers = {};
}
let rsHeaders = envelope.response.headers;
if (!('content-type' in rsHeaders)){
rsHeaders['content-type'] = 'application/json';
}
envelope.payload = JSON.stringify(error);
finishRequest(envelope);
}
// when the response property is undefined then probably we have an envelope provided by an internal source
// thus there's no need to look for a response recipient outside
//TODO: implement a default error sink for internal error handling
}
function ok(envelope){
if (envelope.response !== undefined){
if (envelope.response.statusCode === undefined){
envelope.response.statusCode = 200;
}
// TODO: detect if it is needed to convert msg.payload to string
// and set the content-type header properly
finishRequest(envelope);
}
// when the response property is undefined then probably we have an envelope provided by an internal source
// thus there's no need to look for a response recipient outside
}
function flightRecorder(){
let orangeBox = {};
function logEvent(msg, call){
if (orangeBox[msg.msgId] == undefined){
orangeBox[msg.msgId] = [];
}
orangeBox[msg.msgId].push(call.name);
}
return {
orangeBox,
logEvent
}
}
const recorder = new flightRecorder();
function messageRouterImpl(){
const internalModules = {};
const log = loggerBuilder()
.name(`internal router`)
.level(logLevels.INFO)
.build();
/**
*
* @param {string} name
* @param {Function} call
*/
function addInternal(name, call){
internalModules[name] = call;
}
/**
*
* @param {function} messageHandler
*/
function processMessage(messageHandler){
const handler = messageHandler;
/**
*
* @param {{request, payload, response, msgId}} message
*/
function process(message){
try {
recorder.logEvent(message, handler);
let callResult = handler(message);
if (callResult !== undefined){
let {payload} = callResult;
if (payload !== undefined){
callResult = checkAggregateEnvelope(callResult, message);
// finalize response here
ok(callResult);
} else {
// find an intersection between the message fields
// and internal modules names
let receiverNames = Object.keys(callResult).filter(key => Object.keys(internalModules).includes(key));
//route to internal services
for (let sinkName of receiverNames){
// very questionable, but it is very likely to use payload as a one known field for data
// before giving it into a custom function call
callResult.payload = callResult[sinkName];
// remove custom field
delete callResult[sinkName];
// TODO: implement the multiple subscribers to one source of messages
let {call} = internalModules[sinkName];
if (call !== undefined && call instanceof Function){
call(callResult);
}
}
}
} else {
log.info(`non-routable call result`);
}
} catch (ex){
fail(500, message, ex);
}
}
return {
process,
}
}
return {
processMessage,
addInternal
}
}
const messageRouter = messageRouterImpl();
function prepareEnvelope(requestId, rq, rs, requestBody, handlerName) {
let originalRequest = messageBuilder()
.msgId(requestId)
.request(rq)
.response(rs)
.payload(requestBody)
.build();
requestsCache.put(requestId, originalRequest, handlerName);
let requestEnvelope = messageBuilder()
.msgId(requestId)
.request(unpackRequest(rq))
.payload(requestBody)
.build();
return requestEnvelope;
}
/**
*
* @param {function} routerFunction
*/
function routedRequestHandler(name, routerFunction){
const handlerName = name;
const findRoute = routerFunction;
const log = loggerBuilder()
.name(handlerName)
.level(logLevels.INFO)
.build();
/**
* Handles an incoming API request.
* @param {IncomingMessage} [rq]
* @param {ServerResponse} [rs]
* @type {(rq:IncomingMessage, rs: ServerResponse) => RequestWrapper}
* @returns {RequestWrapper}
*/
function handleRequest (rq, rs){
let requestBodyChunks = [];
let requestBody;
const requestId = crypto.randomUUID();
let {url, method} = rq;
rq.on('error', err => {
log.error(`Error when processing ${method} request ${requestId} to ${url}`);
let failEnvelope = prepareEnvelope(requestId, rq, rs, "", handlerName);
fail(500, failEnvelope, err);
}).on('data', chunk => {
requestBodyChunks.push(chunk);
}).on('end', () => {
requestBody = Buffer.concat(requestBodyChunks).toString();
log.info(`Received ${method} request ${requestId} to ${url}`);
let urlWithoutQuery = clearUrlFromQuery(url);
let {error, handler} = findRoute(urlWithoutQuery, method);
if (error !== undefined){
let failEnvelope = prepareEnvelope(requestId, rq, rs, requestBody, handlerName);
fail(404, failEnvelope, error);
log.info(`Request ${requestId} failed: ${error}`);
} else {
let requestEnvelope = prepareEnvelope(requestId, rq, rs, requestBody, handlerName);
messageRouter.processMessage(handler).process(requestEnvelope);
}
})
}
return {
handleRequest
}
}
module.exports = {
routedRequestHandler,
messageRouter
}