UNPKG

xcraft-core-bus

Version:
379 lines (324 loc) 9.91 kB
'use strict'; const moduleName = 'bus/commander'; const watt = require('gigawatts'); const activity = require('xcraft-core-activity'); const xLog = require('xcraft-core-log')(moduleName, null); const {Router} = require('xcraft-core-transport'); const {isGenerator} = require('xcraft-core-utils').js; class Commander extends Router { constructor(id, acceptIncoming) { super(id, 'pull', xLog, acceptIncoming); this._onMessage = this._onMessage.bind(this); this.hook('message', this._onMessage); this._token = 'invalid'; this._registry = {}; this._modules = {}; } get busClient() { if (this._busClient) { return this._busClient; } this._busClient = require('xcraft-core-busclient').getGlobal(); return this._busClient; } static #unavailableCmdError(cmd) { const err = new Error(`the command "${cmd}" is not available`); err.code = 'CMD_NOT_AVAILABLE'; return err; } getRoutingKeyFromId(cmd, id, isRPC = false) { if (!cmd) { throw new Error('The `cmd` is mandatory'); } let isCmdAvailable = !isRPC && !!this._registry[cmd]; let xHorde; if (this.isModuleRegistered('horde')) { xHorde = require('xcraft-core-horde'); } let dstTribe = 0; if (isCmdAvailable) { const coreGoblin = this._registry[cmd].registrar === 'xcraft-core-goblin'; if ( coreGoblin && xHorde && xHorde.isTribeDispatcher && !cmd.startsWith('goblin.') && !cmd.startsWith('goblin-cache.') && !cmd.startsWith('warehouse.') ) { const hasId = !!id; const isDistributable = !!(hasId && id.indexOf('@') !== -1); const xHost = require('xcraft-core-host'); const {tribe} = xHost.appArgs(); if (isDistributable) { dstTribe = xHost.getTribeFromId(id); if (tribe !== dstTribe) { isCmdAvailable = false; } } else if (tribe !== 0) { isCmdAvailable = false; } } } if (isCmdAvailable) { return null; } /* Look for the command in the horde */ if (xHorde) { const slaves = xHorde.commands; let _horde; for (const horde in slaves) { const _tribe = xHorde.getTribe(horde); if (dstTribe > 0 && _tribe !== dstTribe) { continue; } const _cmd = slaves[horde][cmd]; if (_cmd) { _horde = horde; /* Prefer the shorter path */ if (!_cmd.noForwarding || !_cmd.noForwarding[horde]) { break; } } } if (_horde) { return xHorde.getSlave(_horde).routingKey; } } throw Commander.#unavailableCmdError(cmd); } _onMessage(cmd, msg) { if (!(msg.token === this._token || cmd === 'autoconnect')) { xLog.info('invalid token, command discarded'); return; } let isCmdAvailable = !!this._registry[cmd]; if (cmd === 'autoconnect') { msg.orcName = msg.data.autoConnectToken; } else if ( !isCmdAvailable && (cmd.endsWith('._postload') || cmd.endsWith('._preunload')) ) { xLog.verb(`skip ${cmd} because handler not registered`); this.busClient.events.send(`${msg.orcName}::${cmd}.${msg.id}.finished`); return; } try { /* _xcraftRPC is for the case where we want to send the command to the horde */ const isRPC = !!msg?.data?._xcraftRPC; const routingKey = this.getRoutingKeyFromId(cmd, msg?.data?.id, isRPC); if (routingKey) { const xHorde = require('xcraft-core-horde'); /* Disable commands forwarding in passive mode, without xcraftRPC */ const isPassive = xHorde.getSlave(routingKey).isPassive; const isForwarded = !isPassive || isRPC; if (isForwarded) { if (msg.orcName === 'greathall') { throw new Error( 'forbidden use of busClient with a command sent via xHorde with the "greathall" orcName' ); } xHorde.busClient.command.send(routingKey, cmd, msg); return; } if (!isCmdAvailable) { throw Commander.#unavailableCmdError(cmd); } } } catch (ex) { if (ex.code !== 'CMD_NOT_AVAILABLE') { throw ex; } const errorMessage = {}; errorMessage.data = msg; errorMessage.cmd = cmd; errorMessage.desc = ex; cmd = 'error'; msg = errorMessage; } /* activity is always true */ if ( this._registry[cmd] && this._registry[cmd].activity && msg.isNested === false ) { xLog.verb( 'Creating new activity for ', JSON.stringify(this._registry[cmd]) ); activity.execute( cmd, msg, (cmd, msg) => this._runCommand(cmd, msg, true), this._registry[cmd].parallel ); } else { if (!this._registry[cmd]) { /* FIXME: it seems that it's possible to arrive here * ('error' command not registered). How is possible? */ xLog.err(`the command "${cmd}" is not registered`); return; } /* We can always execute a nested command because the main command is blocked. */ this._runCommand(cmd, msg, false); } } _runCommand(cmd, msg, isActivity) { const routing = { router: msg.router, originRouter: msg.originRouter, activity: isActivity, }; if (msg.forwarding) { routing.forwarding = {...msg.forwarding}; routing.forwarding.route = msg.route; } const resp = this.busClient.newResponse( cmd, msg.orcName, routing, msg.context ); try { xLog.verb('Running command: %s', cmd); this._registry[cmd].handler(msg, resp); } catch (ex) { xLog.err( `error with the command "${cmd}":\n${ex.stack || ex.message || ex}` ); } } _registerHandler(cmdName, cmdHandler) { if (isGenerator(cmdHandler.handler)) { xLog.verb(`-> convert '${cmdName}' to a watt generator`); cmdHandler.handler = watt(cmdHandler.handler); } this._registry[cmdName] = cmdHandler; const modName = Commander._getModuleFromCmd(cmdName); if (!this._modules[modName]) { this._modules[modName] = { cnt: 0, info: cmdHandler.info, }; } ++this._modules[modName].cnt; } getRegistry() { return this._registry; } getFullRegistry() { const registry = {}; const ownRegistry = this.getRegistry(); if (this.isModuleRegistered('horde')) { const {appId, appArgs} = require('xcraft-core-host'); const {tribe} = appArgs(); const routingKey = tribe ? `${appId}-${tribe}` : appId; const xHorde = require('xcraft-core-horde'); Object.keys(xHorde.commands).forEach((horde) => { const noForwarding = xHorde.isNoForwarding(horde); if (noForwarding) { Object.values(xHorde.commands[horde]).forEach((cmd) => { if (!cmd.noForwarding) { cmd.noForwarding = {}; } cmd.noForwarding = {...cmd.noForwarding, [routingKey]: true}; }); } Object.assign(registry, xHorde.commands[horde]); }); } Object.assign(registry, ownRegistry); return registry; } start(host, port, unixSocketId, options, busToken, callback) { this._token = busToken; super.start({host, port, unixSocketId, ...options}, callback); } /** * Check if a command is registered. * @param {string} cmd command's name. * @returns {boolean} true if the property exists. */ isCommandRegistered(cmd) { return Object.prototype.hasOwnProperty.call(this._registry, cmd); } /** * Extract a module's name from a command's name. * @param {string} cmd command's name. * @returns {string} module's name. */ static _getModuleFromCmd(cmd) { return cmd.replace(/(.*?)\..*/, '$1'); } /** * Check if this module is already registered. * * If it's the case, at least one command is using the same namespace. * @param {string} name module's name. * @returns {boolean} true if registered. */ isModuleRegistered(name) { return ( Object.prototype.hasOwnProperty.call(this._modules, name) && this._modules[name].cnt > 0 ); } /** * Retrieve some useful informations about a specific module. * @param {*} name - module's name. * @returns {object} the module's info. */ getModuleInfo(name) { return ( (Object.prototype.hasOwnProperty.call(this._modules, name) && this._modules[name].info) || {} ); } registerCommandHandler(name, location, info, rc, handler) { xLog.verb("Command '%s' registered", name); const command = Object.assign( { handler, name, location, info, }, rc ); this._registerHandler(name, command); } _registerBuiltinHandler(name, handler, desc) { xLog.verb(`${name} handler registered`); const command = { handler, desc /* null for private commands */, name, }; this._registerHandler(name, command); } registerErrorHandler(handler) { this._registerBuiltinHandler('error', handler); } registerAutoconnectHandler(handler) { this._registerBuiltinHandler('autoconnect', handler); } registerDisconnectHandler(handler) { this._registerBuiltinHandler('disconnect', handler); } registerShutdownHandler(handler) { this._registerBuiltinHandler( 'shutdown', handler, 'disconnect all clients and shutdown the Xcraft server' ); } registerMotdHandler(handler) { this._registerBuiltinHandler('motd', handler); } registerBroadcastHandler(handler) { this._registerBuiltinHandler('broadcast', handler); } } module.exports = Commander;