UNPKG

moleculer

Version:

Fast & powerful microservices framework for Node.JS

548 lines (472 loc) 13 kB
/* * moleculer * Copyright (c) 2023 MoleculerJS (https://github.com/moleculerjs/moleculer) * MIT Licensed */ "use strict"; const util = require("util"); const { pick } = require("lodash"); const { RequestSkippedError, MaxCallLevelError } = require("./errors"); /** * @typedef {import("./context")} ContextClass * @typedef {import("./service-broker").MCallDefinition} MCallDefinition * @typedef {import("./service-broker").MCallCallingOptions} MCallCallingOptions * @typedef {import("./service-broker")} ServiceBroker Moleculer Service Broker instance * @typedef {import("./service-broker").CallingOptions} CallingOptions Calling Options * @typedef {import("./registry/endpoint")} Endpoint Registry Endpoint * @typedef {import("./registry/endpoint-action")} ActionEndpoint Registry Action Endpoint * @typedef {import("./registry/endpoint-event")} EventEndpoint Registry Event Endpoint * @typedef {import("./tracing/span")} Span Tracing Span */ /** * Merge metadata * * @param {Context} ctx * @param {Object} newMeta */ function mergeMeta(ctx, newMeta) { if (newMeta) Object.assign(ctx.meta, newMeta); return ctx.meta; } /** * Context class for action calls * * @class Context * @implements {ContextClass} */ class Context { /** * Creates an instance of Context. * * @param {ServiceBroker} broker - Broker instance * @param {ActionEndpoint|EventEndpoint=} 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; /** @type {CallingOptions} */ this.options = { timeout: null, retries: null }; this.parentID = null; this.caller = null; this.level = 1; this.params = null; this.meta = {}; this.headers = {}; this.responseHeaders = {}; this.locals = {}; this.stream = null; this.requestID = this.id; this.tracing = null; this.span = null; this._spanStack = []; this.needAck = null; this.ackID = null; this.startHrTime = null; this.cachedResult = false; } /** * Create a new Context instance * * @param {ServiceBroker} broker * @param {ActionEndpoint|EventEndpoint} endpoint * @param {Object?} params * @param {CallingOptions} 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; // Headers if (opts.headers) { ctx.headers = opts.headers; } // 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 {ActionEndpoint|EventEndpoint} ep * @returns {Context} */ copy(ep) { /** @type {any} */ const ctor = this.constructor; /** @type {Context} */ const newCtx = new ctor(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.headers = this.headers; newCtx.responseHeaders = this.responseHeaders; 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.stream = this.stream; newCtx.cachedResult = this.cachedResult; return newCtx; } /** * * @param {ActionEndpoint|EventEndpoint} ep * @returns {ep is ActionEndpoint} */ isActionEndpoint(ep) { // @ts-ignore return ep?.action != null; } /** * * @param {ActionEndpoint|EventEndpoint} ep * @returns {ep is EventEndpoint} */ isEventEndpoint(ep) { // @ts-ignore return ep?.event != null; } /** * Set endpoint of context * * @param {ActionEndpoint|EventEndpoint} endpoint * @memberof Context */ setEndpoint(endpoint) { this.endpoint = endpoint; if (endpoint) { this.nodeID = endpoint.id; if (this.isActionEndpoint(endpoint)) { this.action = endpoint.action; this.service = this.action.service; this.event = null; } else if (this.isEventEndpoint(endpoint)) { 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 = structuredClone(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); }); } /** * @overload * @param {Record<string, MCallDefinition>} def * @param {MCallCallingOptions=} _opts * @returns {Promise<Record<string, TResult>>} */ /** * @overload * @param {MCallDefinition[]} def * @param {MCallCallingOptions=} _opts * @returns {Promise<TResult[]>} */ /** * Multiple action calls. * * @template TResult * @param {Record<string, MCallDefinition>|MCallDefinition[]} def * @param {MCallCallingOptions=} _opts * @returns {Promise<Record<string, TResult> | TResult[]>} */ 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(/** @type {MCallDefinition[]} */ (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=} data * @param {Object=} opts * @returns {Promise} * * @example * ctx.emit("user.created", { entity: user, creator: ctx.meta.user }); * * @memberof Context */ emit(eventName, data, opts) { opts = opts ?? {}; opts.parentCtx = this; if (opts.groups && !Array.isArray(opts.groups)) opts.groups = [opts.groups]; return this.broker.emit(eventName, data, opts); } /** * Emit an event for all local & remote services * * @param {string} eventName * @param {any=} data * @param {Object=} opts * @returns {Promise} * * @example * ctx.broadcast("user.created", { entity: user, creator: ctx.meta.user }); * * @memberof Context */ broadcast(eventName, data, opts) { opts = opts ?? {}; opts.parentCtx = this; if (opts.groups && !Array.isArray(opts.groups)) opts.groups = [opts.groups]; 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", "headers", "responseHeaders", //"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;