@stoplight/moleculer
Version:
Fast & powerful microservices framework for Node.JS
492 lines (422 loc) • 11.5 kB
JavaScript
/*
* moleculer
* Copyright (c) 2018 MoleculerJS (https://github.com/moleculerjs/moleculer)
* MIT Licensed
*/
"use strict";
const util = require("util");
const { isString } = require("./utils");
const _ = require("lodash");
const { RequestSkippedError, MaxCallLevelError } = require("./errors");
/**
* Merge metadata
*
* @param {Object} newMeta
*
* @private
* @memberof Context
*/
function mergeMeta(ctx, newMeta) {
if (newMeta) Object.assign(ctx.meta, newMeta);
return ctx.meta;
}
/**
* Context class for action calls
*
* @property {String} id - Context ID
* @property {ServiceBroker} broker - Broker instance
* @property {Action} action - Action definition
* @property {String} [nodeID=null] - Node ID
* @property {String} parentID - Parent Context ID
* @property {Boolean} tracing - Enable tracing
* @property {Number} [level=1] - Level of context
*
* @class Context
*/
class Context {
/**
* Creates an instance of Context.
*
* @param {ServiceBroker} broker - Broker instance
* @param {Endpoint} endpoint
*
* @memberof Context
*/
constructor(broker, endpoint) {
this.broker = broker;
if (this.broker) {
this.nodeID = this.broker.nodeID;
this.id = this.broker.generateUid();
} else {
this.nodeID = null;
}
if (endpoint) {
this.setEndpoint(endpoint);
} else {
this.endpoint = null;
this.service = null;
this.action = null;
this.event = null;
}
// The emitted event "user.created" because `ctx.event.name` can be "user.**"
this.eventName = null;
// Type of event ("emit" or "broadcast")
this.eventType = null;
// The groups of event
this.eventGroups = null;
this.options = {
timeout: null,
retries: null
};
this.parentID = null;
this.caller = null;
this.level = 1;
this.params = null;
this.meta = {};
this.locals = {};
this.requestID = this.id;
this.tracing = null;
this.span = null;
this._spanStack = [];
this.needAck = null;
this.ackID = null;
//this.startTime = null;
//his.startHrTime = null;
//this.stopTime = null;
//this.duration = null;
//this.error = null;
this.cachedResult = false;
}
/**
* Create a new Context instance
*
* @param {ServiceBroker} broker
* @param {Endpoint} endpoint
* @param {Object?} params
* @param {Object} opts
* @returns {Context}
*
* @static
* @memberof Context
*/
static create(broker, endpoint, params, opts = {}) {
const ctx = new broker.ContextFactory(broker, endpoint);
if (endpoint != null) ctx.setEndpoint(endpoint);
if (params != null) {
let cloning = broker ? broker.options.contextParamsCloning : false;
if (opts.paramsCloning != null) cloning = opts.paramsCloning;
ctx.setParams(params, cloning);
}
//Object.assign(ctx.options, opts);
ctx.options = opts;
// RequestID
if (opts.requestID != null) ctx.requestID = opts.requestID;
else if (opts.parentCtx != null && opts.parentCtx.requestID != null)
ctx.requestID = opts.parentCtx.requestID;
// Meta
if (opts.parentCtx != null && opts.parentCtx.meta != null)
ctx.meta = Object.assign({}, opts.parentCtx.meta || {}, opts.meta || {});
else if (opts.meta != null) ctx.meta = opts.meta;
// ParentID, Level, Caller, Tracing
if (opts.parentCtx != null) {
ctx.tracing = opts.parentCtx.tracing;
ctx.level = opts.parentCtx.level + 1;
if (opts.parentCtx.span) ctx.parentID = opts.parentCtx.span.id;
else ctx.parentID = opts.parentCtx.id;
if (opts.parentCtx.service) ctx.caller = opts.parentCtx.service.fullName;
}
// caller
if (opts.caller) {
ctx.caller = opts.caller;
}
// Parent span
if (opts.parentSpan != null) {
ctx.parentID = opts.parentSpan.id;
ctx.requestID = opts.parentSpan.traceID;
ctx.tracing = opts.parentSpan.sampled;
}
// Event acknowledgement
if (opts.needAck) {
ctx.needAck = opts.needAck;
}
return ctx;
}
/**
* Copy itself without ID.
* @param {Endpoint} ep
* @returns {Context}
*/
copy(ep) {
const newCtx = new this.constructor(this.broker);
newCtx.nodeID = this.nodeID;
newCtx.setEndpoint(ep || this.endpoint);
newCtx.options = this.options;
newCtx.parentID = this.parentID;
newCtx.caller = this.caller;
newCtx.level = this.level;
newCtx.params = this.params;
newCtx.meta = this.meta;
newCtx.locals = this.locals;
newCtx.requestID = this.requestID;
newCtx.tracing = this.tracing;
newCtx.span = this.span;
newCtx.needAck = this.needAck;
newCtx.ackID = this.ackID;
newCtx.eventName = this.eventName;
newCtx.eventType = this.eventType;
newCtx.eventGroups = this.eventGroups;
newCtx.cachedResult = this.cachedResult;
return newCtx;
}
/**
* Set endpoint of context
*
* @param {Endpoint} endpoint
* @memberof Context
*/
setEndpoint(endpoint) {
this.endpoint = endpoint;
if (endpoint) {
this.nodeID = endpoint.id;
if (endpoint.action) {
this.action = endpoint.action;
this.service = this.action.service;
this.event = null;
} else if (endpoint.event) {
this.event = endpoint.event;
this.service = this.event.service;
this.action = null;
}
}
}
/**
* Set params of context
*
* @param {Object} newParams
* @param {Boolean} cloning
*
* @memberof Context
*/
setParams(newParams, cloning = false) {
if (cloning && newParams) this.params = Object.assign({}, newParams);
else this.params = newParams;
}
/**
* Call an other action. It creates a sub-context.
*
* @param {String} actionName
* @param {Object?} params
* @param {Object?} opts
* @returns {Promise}
*
* @example <caption>Call an other service with params & options</caption>
* ctx.call("posts.get", { id: 12 }, { timeout: 1000 });
*
* @memberof Context
*/
call(actionName, params, _opts) {
const opts = Object.assign(
{
parentCtx: this
},
_opts
);
if (this.options.timeout > 0 && this.startHrTime) {
// Distributed timeout handling. Decrementing the timeout value with the elapsed time.
// If the timeout below 0, skip the call.
const diff = process.hrtime(this.startHrTime);
const duration = diff[0] * 1e3 + diff[1] / 1e6;
const distTimeout = this.options.timeout - duration;
if (distTimeout <= 0) {
return this.broker.Promise.reject(
new RequestSkippedError({ action: actionName, nodeID: this.broker.nodeID })
);
}
if (!opts.timeout || distTimeout < opts.timeout) opts.timeout = distTimeout;
}
// Max calling level check to avoid calling loops
if (
this.broker.options.maxCallLevel > 0 &&
this.level >= this.broker.options.maxCallLevel
) {
return this.broker.Promise.reject(
new MaxCallLevelError({ nodeID: this.broker.nodeID, level: this.level })
);
}
let p = this.broker.call(actionName, params, opts);
// Merge metadata with sub context metadata
return p
.then(res => {
if (p.ctx) mergeMeta(this, p.ctx.meta);
return res;
})
.catch(err => {
if (p.ctx) mergeMeta(this, p.ctx.meta);
return this.broker.Promise.reject(err);
});
}
mcall(def, _opts) {
const opts = Object.assign(
{
parentCtx: this
},
_opts
);
if (this.options.timeout > 0 && this.startHrTime) {
// Distributed timeout handling. Decrementing the timeout value with the elapsed time.
// If the timeout below 0, skip the call.
const diff = process.hrtime(this.startHrTime);
const duration = diff[0] * 1e3 + diff[1] / 1e6;
const distTimeout = this.options.timeout - duration;
if (distTimeout <= 0) {
const action = (Array.isArray(def) ? def : Object.values(def))
.map(d => d.action)
.join(", ");
return this.broker.Promise.reject(
new RequestSkippedError({ action, nodeID: this.broker.nodeID })
);
}
if (!opts.timeout || distTimeout < opts.timeout) opts.timeout = distTimeout;
}
// Max calling level check to avoid calling loops
if (
this.broker.options.maxCallLevel > 0 &&
this.level >= this.broker.options.maxCallLevel
) {
return this.broker.Promise.reject(
new MaxCallLevelError({ nodeID: this.broker.nodeID, level: this.level })
);
}
let p = this.broker.mcall(def, opts);
// Merge metadata with sub context metadata
return p
.then(res => {
if (Array.isArray(p.ctx) && p.ctx.length)
p.ctx.forEach(ctx => mergeMeta(this, ctx.meta));
return res;
})
.catch(err => {
if (Array.isArray(p.ctx) && p.ctx.length)
p.ctx.forEach(ctx => mergeMeta(this, ctx.meta));
return this.broker.Promise.reject(err);
});
}
/**
* Emit an event (grouped & balanced global event)
*
* @param {string} eventName
* @param {any?} payload
* @param {Object?} opts
* @returns {Promise}
*
* @example
* ctx.emit("user.created", { entity: user, creator: ctx.meta.user });
*
* @memberof Context
*/
emit(eventName, data, opts) {
if (Array.isArray(opts) || isString(opts)) opts = { groups: opts };
else if (opts == null) opts = {};
if (opts.groups && !Array.isArray(opts.groups)) opts.groups = [opts.groups];
opts.parentCtx = this;
return this.broker.emit(eventName, data, opts);
}
/**
* Emit an event for all local & remote services
*
* @param {string} eventName
* @param {any?} payload
* @param {Object?} opts
* @returns {Promise}
*
* @example
* ctx.broadcast("user.created", { entity: user, creator: ctx.meta.user });
*
* @memberof Context
*/
broadcast(eventName, data, opts) {
if (Array.isArray(opts) || isString(opts)) opts = { groups: opts };
else if (opts == null) opts = {};
if (opts.groups && !Array.isArray(opts.groups)) opts.groups = [opts.groups];
opts.parentCtx = this;
return this.broker.broadcast(eventName, data, opts);
}
/**
* Start a new child tracing span.
*
* @param {String} name
* @param {Object?} opts
* @returns {Span}
* @memberof Context
*/
startSpan(name, opts) {
let span;
if (this.span) {
span = this.span.startSpan(name, Object.assign({ ctx: this }, opts));
} else {
span = this.broker.tracer.startSpan(name, Object.assign({ ctx: this }, opts));
}
this._spanStack.push(span);
this.span = span;
return span;
}
/**
* Finish an active span.
*
* @param {Span} span
* @param {Number?} time
*/
finishSpan(span, time) {
if (!span.isActive()) return;
span.finish(time);
const idx = this._spanStack.findIndex(sp => sp == span);
if (idx !== -1) {
this._spanStack.splice(idx, 1);
this.span = this._spanStack[this._spanStack.length - 1];
} else {
/* istanbul ignore next */
this.service.logger.warn("This span is not assigned to this context", span);
}
}
/**
* Convert Context to a printable POJO object.
*/
toJSON() {
const res = _.pick(this, [
"id",
"nodeID",
"action.name",
"event.name",
"service.name",
"service.version",
"service.fullName",
"options",
"parentID",
"caller",
"level",
"params",
"meta",
//"locals",
"requestID",
"tracing",
"span",
"needAck",
"ackID",
"eventName",
"eventType",
"eventGroups",
"cachedResult"
]);
return res;
}
/* istanbul ignore next */
[util.inspect.custom](depth, options) {
// https://nodejs.org/docs/latest-v8.x/api/util.html#util_custom_inspection_functions_on_objects
if (depth < 0) {
return options.stylize("[Context]", "special");
}
const inner = util.inspect(this.toJSON(), options);
return `${options.stylize("Context", "special")}< ${inner} >`;
}
}
module.exports = Context;