@betit/orion-node-sdk
Version:
SDK for orion
414 lines • 15.8 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const codec_1 = require("../codec");
const transport_1 = require("../transport");
const tracer_1 = require("../tracer/tracer");
const logger_1 = require("../logger/logger");
const LOGGER_LEVELS = require("../logger/levels");
const request_1 = require("../request/request");
const response_1 = require("../response/response");
const error_1 = require("../error/error");
const utils_1 = require("../utils");
const asyncArray_1 = require("../utils/asyncArray");
const health_1 = require("../health/health");
const messages_1 = require("../health/messages");
function noop() { }
const DEBUG = utils_1.debugLog('orion:service');
function uniqueName(name, uniqueID) {
return name + '@' + uniqueID;
}
exports.uniqueName = uniqueName;
/**
* Checks if a environment variable is true or 1.
* @param envName The name of the environment variable.
*/
function trueEnv(envName) {
return envName in process.env &&
process.env[envName] in {
'true': 0,
'1': 0,
};
}
/**
* Checks if the object is thenable (a promise).
*
* @param object Anything that can be thenable.
*/
function isThenable(object) {
return object && typeof object.then === 'function';
}
/**
* Provides an interface to create your service and register request handlers.
*/
class Service {
/**
* Create new service.
*/
constructor(name, options = {}) {
this.name = name;
this.options = options;
this.id = utils_1.generateId();
this.options = Object.assign({
timeout: 200,
timeouts: {},
}, options);
this._codec = new codec_1.DefaultBinaryCodec();
this._transport = this.options.transport || new transport_1.DefaultTransport();
this._tracer = this.options.tracer || new tracer_1.Tracer(name);
this.logger = this.options.logger || new logger_1.Logger(name, trueEnv('VERBOSE'));
this._enableStatusEndpoints = ('enableStatusEndpoints' in options) ? options.enableStatusEndpoints : trueEnv('WATCHDOG');
this._registerToWatchdog = ('registerToWatchdog' in options) ? options.registerToWatchdog : trueEnv('WATCHDOG');
this._healthChecks = {};
this._watchdogServiceName = health_1.DefaultWatchdogServiceName();
}
commsWithWatchdog() {
const endpoints = Object.keys(this._healthChecks).map((k) => this._healthChecks[k]);
const { killTheLoop, responseArray } = health_1.WatchdogRegisterLoop(this._watchdogServiceName, this.name, this.id, endpoints, this);
let closed = false;
const killLoop = () => { closed = true; killTheLoop(); };
process.nextTick(() => __awaiter(this, void 0, void 0, function* () {
while (!closed) {
const res = yield responseArray.consume();
const err = res.error;
if (!!err) {
this.logger
.createMessage('Health/Watchdog')
.setLevel(LOGGER_LEVELS.ERROR)
.setParams({
description: 'Error trying to register or ping the Watchdog \'' + this._watchdogServiceName + '\' service.',
})
.send();
}
}
}));
}
listenToHealthChecks() {
for (const name in this._healthChecks) {
const check = this._healthChecks[name];
this.handleHealthCheck('status.' + name, messages_1.DependencyHandleGenerator(check));
}
this.handleHealthCheck('status.am-i-up', messages_1.AmIUpHandle);
this.handleHealthCheck('status.aggregate', messages_1.AggregateHandleGenerator(this._healthChecks));
}
handleHealthCheck(healthCheckName, handler) {
const ROUTE = `${uniqueName(this.name, this.id)}.${healthCheckName}`;
const logging = true;
DEBUG('register handler:', ROUTE);
this._transport.handle(ROUTE, this.name, (data, send) => {
const DATA = this._codec.decode(data);
let req = new request_1.Request(DATA.path, DATA.params);
req.meta = DATA.meta || {};
req.tracerData = DATA.tracerData;
if (logging) {
this.logger.createMessage(healthCheckName)
.setLevel(LOGGER_LEVELS.INFO)
.setId(req.getId())
.setParams({
params: req.params,
meta: req.meta
})
.send();
}
DEBUG('incoming request:', req);
let onlyOnce = false;
const afterCallback = (res) => {
if (onlyOnce) {
throw new Error('The handler\'s callback was called more than once');
}
onlyOnce = true;
this._checkResponse(res);
if (res.error && logging) {
this.logger.createMessage(healthCheckName)
.setLevel(LOGGER_LEVELS.ERROR)
.setLOC(res.error)
.setId(req.getId())
.setParams(utils_1.StringifyError(res.error))
.send();
}
send(this._codec.encode(res));
};
const promise = handler(req, afterCallback);
if (isThenable(promise)) {
promise
.then(afterCallback)
.catch((err) => {
if (err instanceof error_1.OrionError) {
afterCallback(new response_1.Response(null, err));
}
else {
throw err;
}
});
}
});
}
checkHealthOrTimeout(name, timeout, check) {
return __awaiter(this, void 0, void 0, function* () {
const result = yield Promise.race([check(), utils_1.sleep(timeout)]);
if (!result) {
const err = new error_1.OrionError(messages_1.HealthCheckResult.HC_CRIT);
return ['The health check ' + name + ' did timeout for ' + timeout / health_1.SECOND + ' seconds', err];
}
else {
return result;
}
});
}
/**
* Subscribe to a topic.
* @param {string} topic
* @param {Function} callback
* @param {boolean} disableGroup
*/
on(topic, callback, disableGroup = false) {
const SUBJECT = `${this.name}:${topic}`;
DEBUG('on:', SUBJECT);
return this._transport.subscribe(SUBJECT, disableGroup ? null : this.name, message => {
callback(this._codec.decode(message));
});
}
/**
* Subscribe to a topic, getting a producer/consumer array of promises.
* @param {string} topic
* @param {boolean} disableGroup
* @returns An async array following the producer/consumer pattern.
*/
onAsync(topic, disableGroup = false) {
const SUBJECT = `${this.name}:${topic}`;
const ARRAY = new asyncArray_1.AsyncArray();
DEBUG('onAsync:', SUBJECT);
this._transport.subscribe(SUBJECT, disableGroup ? null : this.name, message => {
ARRAY.produce(this._codec.decode(message));
});
return ARRAY;
}
/**
* Register request handler method with enabled logging.
* @param {string} method
* @param {Function} callback
* @param {string} [prefix]
*/
handle(path, callback, prefix) {
const LOGGING = true;
this._handle(path, callback, LOGGING, prefix);
}
/**
* Register request handler method with disabled logging.
* @param {string} method
* @param {Function} callback
* @param {string} [prefix]
*/
handleWithoutLogging(path, callback, prefix) {
const LOGGING = false;
this._handle(path, callback, LOGGING, prefix);
}
/**
* Start listenning on the underlying transport connection.
* @param {Function} callback
* @returns {Promise} returns promise if called with no callback.
*/
listen(callback) {
DEBUG('listen');
if (this._enableStatusEndpoints) {
this.listenToHealthChecks();
}
if (this._registerToWatchdog) {
this.commsWithWatchdog();
}
if (callback) {
this._transport.listen(callback);
}
else {
return new Promise((res, rej) => {
this._transport.listen(res);
});
}
}
/**
* Close underlying transport connection.
*/
close() {
DEBUG('close');
this._transport.close();
}
/**
* Connection closed handler
* @param {Function} callback
*/
onClose(callback) {
this._transport.onClose(() => {
DEBUG('on close');
callback();
});
}
/**
* Service name and id.
*/
toString() {
return `${this.name}-${this.id}`;
}
/**
* Publish to a topic.
* @param {any} topic
* @param {Object} message
*/
emit(topic, message) {
DEBUG('emit:', topic);
this._transport.publish(topic, this._codec.encode(message));
}
registerHealthCheck(check) {
const originalCheck = check.checkIsWorking;
this._healthChecks[check.name] = Object.assign({}, check, {
checkIsWorking: () => __awaiter(this, void 0, void 0, function* () { return this.checkHealthOrTimeout(check.name, check.timeout, originalCheck); }),
});
}
/**
* Call service method.
* @param {Request} request
* @param {Object} [params]
* @param {Function} callback
* @returns {Promise} returns promise if called with no callback.
*/
call(req, callback) {
if (req instanceof request_1.Request === false) {
throw new Error('Request must be instance of Orion.Request');
}
if (!callback) {
return new Promise((res, rej) => {
this._serializeRequest(req, (d) => {
if (d.error instanceof Error) {
rej(d.error);
}
else {
res(d);
}
});
});
}
else {
this._serializeRequest(req, callback);
}
}
_getCallTimeout(path, timeout) {
let options = this.options;
let specificCallTimeout = options.timeout && options.timeouts[path];
return timeout || specificCallTimeout || options.timeout;
}
_call(route, req, callback) {
const CLOSE_TRACER = this._tracer.trace(req);
this._transport.request(route, this._codec.encode(req), res => {
// handle transport error
if (res instanceof Error) {
callback(new response_1.Response(null, new error_1.OrionError('ORION_TRANSPORT', res.message)));
}
else {
const RESULT = this._codec.decode(res);
if (RESULT.error) {
RESULT.error = error_1.OrionError.decode(RESULT.error);
}
let response = new response_1.Response(RESULT.payload, RESULT.error);
DEBUG('got response:', response);
CLOSE_TRACER();
callback(response);
}
}, this._getCallTimeout(req.path, req.timeout));
}
_handle(path, callback, logging, prefix) {
const ROUTE = `${prefix || this.name}.${path}`;
DEBUG('register handler:', ROUTE);
this._transport.handle(ROUTE, this.name, (data, send) => {
const DATA = this._codec.decode(data);
let req = new request_1.Request(DATA.path, DATA.params);
req.meta = DATA.meta || {};
req.tracerData = DATA.tracerData;
if (logging) {
this.logger.createMessage(path)
.setLevel(LOGGER_LEVELS.INFO)
.setId(req.getId())
.setMap({
meta: req.meta,
})
.setParams(req.params)
.send();
}
DEBUG('incoming request:', req);
let onlyOnce = false;
const afterCallback = (res) => {
if (onlyOnce) {
throw new Error('The handler\'s callback was called more than once');
}
onlyOnce = true;
this._checkResponse(res);
if (res.error && logging) {
this.logger.createMessage(path)
.setLevel(LOGGER_LEVELS.ERROR)
.setLOC(res.error)
.setId(req.getId())
.setParams(utils_1.StringifyError(res.error))
.send();
}
send(this._codec.encode(res));
};
const promise = callback(req, afterCallback);
if (isThenable(promise)) {
promise
.then(afterCallback)
.catch((err) => {
if (err instanceof error_1.OrionError) {
afterCallback(new response_1.Response(null, err));
}
else {
throw err;
}
});
}
});
}
_serializeRequest(request, callback) {
let route = request.path;
// services registered with names like `service.method`
// but we support `/service/method` style calls as well
// hence we need to "normalize" the route here.
route = route.replace(/\//gi, '.');
if (route.startsWith('.')) {
route = route.substring(1);
}
if (!callback) {
callback = noop;
}
if (!route) {
return callback(new response_1.Response(null, new error_1.OrionError('Invalid request path')));
}
if (this.options.service) {
route = `${this.options.service}.${route}`;
}
let req = new request_1.Request(request.path, request.params);
req.timeout = request.timeout;
req.tracerData = request.tracerData;
req.meta = request.meta || {};
DEBUG('calling:', route);
DEBUG('sending request:', request);
this._call(route, req, callback);
}
_checkResponse(res) {
if (res instanceof response_1.Response === false) {
throw new Error('The response must instance of Orion.Response');
}
if (res.error !== undefined &&
res.error instanceof error_1.OrionError === false) {
throw new Error('The passed error must instance of Orion.Error');
}
return true;
}
}
exports.Service = Service;
//# sourceMappingURL=index.js.map