UNPKG

xcraft-core-busclient

Version:
219 lines (190 loc) 6.32 kB
'use strict'; const moduleName = 'busclient/command'; const xLog = require('xcraft-core-log')(moduleName, null); class Command { static #unsubList = new Map(); /* Only for _xcraftRPC messages */ _routingKey; constructor(busClient, pushSocket) { this._busClient = busClient; this._push = pushSocket; const xEtc = require('xcraft-core-etc')(); if (xEtc) { const {appId, appArgs} = require('xcraft-core-host'); const {tribe} = appArgs(); this._routingKey = tribe ? `${appId}-${tribe}` : appId; } this._push .on('close', (err) => this.#onNetworkChange(err)) .on('error', (err) => this.#onNetworkChange(err)) .on('connect', (err) => this.#onNetworkChange(err)) .on('reconnect attempt', (err) => this.#onNetworkChange(err)); } #onNetworkChange(err) { Command.abortAll(err); } static abort(commandId) { const context = Command.#unsubList.get(commandId); if (!context) { return; } Command.#unsubList.delete(commandId); const {cmd, unsub, callback} = context; unsub(); if (callback) { callback(`The command ${cmd} ${commandId} is aborted`); } } static abortAll(err = null) { for (const {cmd, unsub, callback} of Command.#unsubList.values()) { unsub(); if (callback) { callback(`The command ${cmd} is aborted${err ? `: ${err}` : ''}`); } } Command.#unsubList.clear(); } connectedWith() { return this._push.connectedWith(); } newMessage(cmd, which) { if (!which) { which = this._busClient.getOrcName() || 'greathall'; } return this._busClient.newMessage(cmd, which); } retry(msg) { return this.send(msg.topic, msg); } /** * Send a command on the bus. * * If a callback is specified, the finished topic is automatically * subscribed to the events bus. Then when the callback is called, the * topic is unsubscribed. * * @param {string} cmd - Command's name. * @param {object} data - Message or map of arguments passed to the command. * @param {string} which - Orc name or greathall. * @param {function()} [finishHandler] - Callback. * @param {object} options - Options like forceNested. * @param {object} msgContext - Message context. */ send(cmd, data, which, finishHandler, options = {}, msgContext) { if (!which) { which = this._busClient.getOrcName() || 'greathall'; } let busMessage = data; if (!data || !data._xcraftMessage) { busMessage = this.newMessage(cmd, which); busMessage.data = data; busMessage.originRouter = this._push.connectedWith(); } else if (data._xcraftMessage) { this._busClient.patchMessage(busMessage); } if (msgContext) { busMessage.context = msgContext; } if (!busMessage.arp) { let token = require('xcraft-core-etc')() ? require('xcraft-core-bus').getToken() : null; let nodeName = this._routingKey; let {orcName} = busMessage; let nice = 0; let noForwarding = false; if (cmd === 'autoconnect') { orcName = busMessage.data.autoConnectToken; nice = busMessage.data.nice; noForwarding = busMessage.data.noForwarding; /* `token` can be empty, because it's provided by the autoconnect */ } else if (!token) { token = orcName.split('@')[1]; nodeName = 'zog'; if (!token) { throw new Error( `this ARP entry without token is forbidden; it's seems that your client is using a forbidden orcName or you are not connected properly to the server` ); } } busMessage.arp = { [orcName]: {token, nice, noForwarding, nodeName}, }; busMessage.router = this.connectedWith(); } if (options && options.forceNested) { busMessage.isNested = true; } const isRPC = !!busMessage?.data?._xcraftRPC; if (finishHandler) { /* Subscribe to end and error command notification. */ const unsubscribe = this._busClient.events.subscribe( `${which}::${cmd}.${busMessage.id}.(error|finished)`, (msg) => { if (isRPC) { Command.#unsubList.delete(busMessage.id); } unsubscribe(); /* On success */ if (!msg.isError) { finishHandler(null, msg); return; } // Generator exception handling, // see https://gist.github.com/deepak/e325fa2d1a39984fd3e9 setTimeout(() => { try { let err = {}; if (msg.data && msg.data.message) { err.message = msg.data.message; if (msg.data.id) { err.id = msg.data.id; } if (msg.data.code) { err.code = msg.data.code; } if (msg.data.name) { err.name = msg.data.name; } if (msg.data.stack) { err.stack = msg.data.stack; } if (msg.data.info) { err.info = msg.data.info; } } else { err.message = msg.data; } if (!err.topic) { err.topic = msg.topic; } if (!err.code) { err.code = 'XCRAFT_CMD_ERROR'; } finishHandler(err); } catch (ex) { console.error( `Unexpected error: ${ex.stack || ex.message || ex}` ); } }, 0); }, this._push.connectedWith() ); /* In the case of passive server, we must ensure to throw all exceptions * by calling the callbacks where appropriate like it was an .error * command event. Of course, the subscriptions must be removed by the way. */ if (isRPC) { Command.#unsubList.set(busMessage.id, { cmd, unsub: unsubscribe, callback: finishHandler, }); } xLog.verb(() => 'finish handler registered for cmd: ' + cmd); } xLog.verb(() => `client send "${cmd}" command`); this._push.send(cmd, busMessage); } } module.exports = Command;