UNPKG

moleculer-api

Version:

A dynamic API Gateway for MoleculerJS which updates REST endpoints and aggregated GraphQL schema, access control policy for each action calls from metadata of remote services schema without restart or deployment.

309 lines 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServiceBroker = void 0; const tslib_1 = require("tslib"); const kleur = tslib_1.__importStar(require("kleur")); const _ = tslib_1.__importStar(require("lodash")); const registry_1 = require("./registry"); const reporter_1 = require("./reporter"); const pubsub_1 = require("./pubsub"); const params_1 = require("./params"); const batching_1 = require("./batching"); const function_1 = require("./function"); const delegator_1 = require("./delegator"); class ServiceBroker { constructor(props, opts) { this.props = props; /* lifecycle */ this.working = false; // save options this.opts = _.defaultsDeep(opts || {}, { log: { event: true, call: true, }, }); // create delegator from options const delegatorKeys = Object.keys(delegator_1.ServiceBrokerDelegatorConstructors); let delegatorKey = delegatorKeys.find(type => !!this.opts[type]); if (!delegatorKey) { delegatorKey = delegatorKeys[0]; this.props.logger.info(`available delegator options are not specified: use ${delegatorKey} delegator`); } const key = delegatorKey; this.delegator = new (delegator_1.ServiceBrokerDelegatorConstructors[key])({ logger: this.props.logger.getChild(key), emitEvent: this.emitEvent.bind(this), emitServiceConnected: this.emitServiceConnected.bind(this), emitServiceDisconnected: this.emitServiceDisconnected.bind(this), }, this.opts[key] || {}); this.delegatorSymbol = Symbol(`BrokerDelegator:${this.props.id}`); // create event buses this.eventPubSub = new pubsub_1.EventPubSub({ onError: error => this.props.logger.error(error), // eventNamePatternResolver: this.delegator.eventNameResolver, maxListeners: Infinity, }); this.discoveryPubSub = new pubsub_1.DiscoveryPubSub({ onError: error => this.props.logger.error(error), }); // create registry this.registry = new registry_1.ServiceRegistry({ logger: this.props.logger.getChild("registry"), }, this.opts.registry); } get delegatedBroker() { return this.delegator.broker; } start(listeners) { return tslib_1.__awaiter(this, void 0, void 0, function* () { this.working = true; yield this.discoveryPubSub.subscribeAll(listeners); yield this.registry.start(); yield this.delegator.start(); this.props.logger.info(`service broker has been started: ${kleur.yellow(this.delegator.key)}`); }); } stop() { return tslib_1.__awaiter(this, void 0, void 0, function* () { this.working = false; yield this.registry.stop(); this.discoveryPubSub.unsubscribeAll(); this.eventPubSub.unsubscribeAll(); yield this.delegator.stop(); this.props.logger.info(`service broker has been stopped`); }); } /* protected methods for brokering discovery and events by delegator */ emitEvent(packet) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (!this.working) { return; } // publish and store yield this.eventPubSub.publish(packet.event, packet); this.registry.addEventExample([packet.event], packet); // log this.props.logger[this.opts.log.event ? "info" : "debug"](`received ${kleur.green(packet.event)} ${packet.broadcast ? "broadcast " : ""}event from ${kleur.yellow(packet.from || "unknown")}`); }); } emitServiceConnected(service) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (!this.working) { return; } if (!(service instanceof registry_1.Service) || !service.id) { // unrecognized service this.props.logger.error(`service broker discovered a unrecognized service: ${service}`); return; } const foundService = this.registry.findServiceByHash(service.hash); if (foundService) { // node pool updated for (const node of service.nodeIdMap.values()) { foundService.addNode(node); } yield this.discoveryPubSub.publish("nodePoolUpdated", service); } else { // new service connected service.setBroker(this); this.registry.addService(service); yield this.discoveryPubSub.publish("connected", service); } }); } emitServiceDisconnected(service, nodeId) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (!this.working) { return; } const foundService = this.registry.findServiceByHash(service.hash); if (!foundService || !foundService.nodeIdMap.delete(nodeId)) { // unknown service this.props.logger.error(`service broker has been disconnected from non-registered service node: ${service} (${nodeId})`); } else if (foundService.nodeIdMap.size === 0) { // service disconnected this.registry.removeServiceByHash(foundService.hash); yield this.discoveryPubSub.publish("disconnected", foundService); } else { // node pool updated yield this.discoveryPubSub.publish("nodePoolUpdated", foundService); } }); } /* context resource management */ getDelegatorContext(context) { let delegatorContext = context.get(this.delegatorSymbol); if (!delegatorContext) { delegatorContext = this.delegator.createContext(context); context.set(this.delegatorSymbol, delegatorContext, ctx => this.delegator.clearContext(ctx)); } return delegatorContext; } getEventSubscriptions(context) { let subscriptions = context.get(ServiceBroker.EventSubscriptionSymbol); if (!subscriptions) { subscriptions = []; context.set(ServiceBroker.EventSubscriptionSymbol, subscriptions, subs => { for (const sub of subs) { this.unsubscribeEvent(sub); } }); } return subscriptions; } getBatchingPool(context) { let batchingPool = context.get(ServiceBroker.BatchingPoolSymbol); if (!batchingPool) { batchingPool = new batching_1.BatchingPool(this.opts.batching); context.set(ServiceBroker.BatchingPoolSymbol, batchingPool, pool => { pool.clear(); }); } return batchingPool; } /* action call */ call(context, args) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const ctx = this.getDelegatorContext(context); const { action, params, batchingParams, disableCache } = args; const node = this.delegator.selectActionTargetNode(ctx, action); // console.assert(node && action, "there are no available nodes to call the action"); // do batching if (batchingParams) { const batchingPool = this.getBatchingPool(context); const batchingParamNames = Object.keys(batchingParams); const batchingKey = batchingPool.getBatchingKey({ action: action.id, params, batchingParamNames }); // set batching handler for this call if (!batchingPool.hasBatchingHandler(batchingKey)) { // or register job batchingPool.setBatchingHandler(batchingKey, (batchingParamsList) => tslib_1.__awaiter(this, void 0, void 0, function* () { // merge common params with batching params const mergedParams = (params || {}); for (const batchingParamName of batchingParamNames) { mergedParams[batchingParamName] = []; } for (const bParams of batchingParamsList) { for (const [k, v] of Object.entries(bParams)) { mergedParams[k].push(v); } } // log in advance this.props.logger[this.opts.log.call ? "info" : "debug"](`call ${action}${kleur.cyan("@")}${node} ${kleur.cyan(batchingParamsList.length)} times in a batch from ${kleur.yellow((context.id || "unknown") + "@" + (context.ip || "unknown"))}`); // do batching call const response = yield this.delegator.call(ctx, { action, node, params: mergedParams, disableCache, batchedParamsLength: batchingParamsList.length }); this.registry.addActionExample({ action, params: mergedParams, response }); return response; })); } // add batch entry and wait response return batchingPool.batch(batchingKey, batchingParams); } else { // log in advance this.props.logger[this.opts.log.call ? "info" : "debug"](`call ${action}${kleur.cyan("@")}${node} from ${kleur.yellow((context.id || "unknown") + "@" + (context.ip || "unknown"))}`); // normal request const response = yield this.delegator.call(ctx, { action, node, params, disableCache }); this.registry.addActionExample({ action, params, response }); return response; } }); } /* action call cache */ clearActionCache(action) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (yield this.delegator.clearActionCache(action)) { this.props.logger.info(`${action} action caches have been cleared`); } }); } clearServiceCache(service) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (yield this.delegator.clearServiceCache(service)) { this.props.logger.info(`${service} service caches have been cleared`); } }); } clearAllCache() { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (yield this.delegator.clearAllCache()) { this.props.logger.info(`all caches have been cleared`); } }); } /* event pub/sub */ subscribeEvent(context, eventNamePattern, listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const subscriptions = this.getEventSubscriptions(context); if (listener) { subscriptions.push(...(yield this.eventPubSub.subscribe(eventNamePattern, listener))); return undefined; } // returns event packet async iterator when no listener given const iterator = this.eventPubSub.asyncIterator(eventNamePattern); subscriptions.push(iterator); return iterator; }); } unsubscribeEvent(subscription) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (typeof subscription === "number") { this.eventPubSub.unsubscribe(subscription); } else if (subscription.return) { yield subscription.return(); } }); } publishEvent(context, args) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const ctx = this.getDelegatorContext(context); yield this.delegator.publish(ctx, args); // add from information to original packet and store as example const packet = args; packet.from = `${context.id || "unknown"}@${context.ip || "unknown"}`; this.registry.addEventExample([args.event], packet); // log this.props.logger[this.opts.log.event ? "info" : "debug"](`published ${kleur.green(packet.event)} ${packet.broadcast ? "broadcast " : ""}event from ${kleur.yellow(packet.from)}`); }); } /* params mapper */ createParamsMapper(opts) { return new params_1.ParamsMapper(opts); } /* compile inline function string */ createInlineFunction(props) { return function_1.createInlineFunction(props, this.opts.function); } /* service reporter */ createReporter(service) { return new reporter_1.Reporter({ logger: this.props.logger.getChild(`${service}\n`), service, props: null, send: (messages, table) => this.delegator.report(service, messages, table), }, this.opts.reporter); } /* pattern matching for action and event names */ matchActionName(name, namePattern) { return this.delegator.matchActionName(name, namePattern); } matchEventName(name, namePattern) { return this.delegator.matchEventName(name, namePattern); } /* health check */ healthCheckService(service) { return this.delegator.healthCheckService(service); } healthCheckCall(action) { return this.delegator.healthCheckCall(action); } healthCheckPublish(args) { return this.delegator.healthCheckPublish(args); } healthCheckSubscribe() { return this.delegator.healthCheckSubscribe(); } } exports.ServiceBroker = ServiceBroker; ServiceBroker.EventSubscriptionSymbol = Symbol("BrokerEventSubscriptions"); ServiceBroker.BatchingPoolSymbol = Symbol("BrokerBatchingPool"); //# sourceMappingURL=broker.js.map