UNPKG

@betit/orion-node-sdk

Version:
414 lines 15.8 kB
"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