UNPKG

@imqueue/rpc

Version:

RPC-like client-service implementation over messaging queue

324 lines 12.8 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IMQService = exports.Description = void 0; exports.send = send; /*! * IMQService implementation * * I'm Queue Software Project * Copyright (C) 2025 imqueue.com <support@imqueue.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * If you want to use this code in a closed source (commercial) project, you can * purchase a proprietary commercial license. Please contact us at * <support@imqueue.com> to get commercial licensing options. */ const core_1 = require("@imqueue/core"); const _1 = require("."); const os = require("os"); const http = require("node:http"); const cluster = require('cluster'); class Description { } exports.Description = Description; const serviceDescriptions = new Map(); /** * Returns collection of class methods metadata even those are inherited * from a chain of parent classes * * @param {string} className * @return {MethodsCollectionDescription} */ function getClassMethods(className) { let methods = {}; let classInfo = _1.IMQRPCDescription.serviceDescription[className]; if (classInfo.inherits && _1.IMQRPCDescription.serviceDescription[classInfo.inherits]) { Object.assign(methods, getClassMethods(classInfo.inherits)); } Object.assign(methods, classInfo.methods); return methods; } /** * Checks if given args match the given args description at least by args count * * @param {ArgDescription[]} argsInfo * @param {any[]} args * @returns {boolean} */ function isValidArgsCount(argsInfo, args) { // istanbul ignore next return (argsInfo.some(argInfo => argInfo.isOptional) ? argsInfo.length >= args.length : argsInfo.length === args.length); } /** * Class IMQService * Basic abstract service (server-side) implementation */ class IMQService { // noinspection TypeScriptAbstractClassConstructorCanBeMadeProtected /** * Class constructor * * @constructor * @param {Partial<IMQServiceOptions>} options * @param {string} [name] */ constructor(options, name) { this.name = name || this.constructor.name; if (this.name === 'IMQService') { throw new TypeError('IMQService class is abstract and cannot ' + 'be instantiated directly!'); } this.options = Object.assign(Object.assign(Object.assign({}, _1.DEFAULT_IMQ_SERVICE_OPTIONS), options), { metricsServer: Object.assign(Object.assign({}, _1.DEFAULT_IMQ_METRICS_SERVER_OPTIONS), ((options === null || options === void 0 ? void 0 : options.metricsServer) || {})) }); this.logger = this.options.logger || /* istanbul ignore next */ console; this.imq = core_1.default.create(this.name, this.options); this.handleRequest = this.handleRequest.bind(this); _1.SIGNALS.forEach((signal) => process.on(signal, async () => { this.destroy().catch(this.logger.error); if (this.metricsServer) { this.metricsServer.close(); } // istanbul ignore next setTimeout(() => process.exit(0), core_1.IMQ_SHUTDOWN_TIMEOUT); })); this.imq.on('message', this.handleRequest); } /** * Handles incoming request and produces corresponding response * * @access private * @param {IMQRPCRequest} request - request message * @param {string} id - message unique identifier * @return {Promise<string>} */ async handleRequest(request, id) { const logger = this.options.logger || console; const method = request.method; const description = await this.describe(); const args = request.args; let response = { to: id, data: null, error: null, request: request, }; if (typeof this.options.beforeCall === 'function') { const beforeCall = this.options.beforeCall.bind(this); try { await beforeCall(request, response); } catch (err) { logger.warn(_1.BEFORE_HOOK_ERROR, err); } } if (!this[method]) { response.error = (0, _1.IMQError)('IMQ_RPC_NO_METHOD', `Method ${this.name}.${method}() does not exist.`, new Error().stack, method, args); } else if (!description.service.methods[method]) { // Allow calling runtime-attached methods (own props) even if // they are not present in the exposed service description. // Deny access for prototype (class) methods not decorated with @expose. const isOwn = Object.prototype.hasOwnProperty.call(this, method); const value = this[method]; const proto = Object.getPrototypeOf(this); const protoValue = proto && proto[method]; const isSameAsProto = typeof protoValue === 'function' && value === protoValue; // Allow only truly dynamic own-instance functions (not the same as prototype) if (!(isOwn && typeof value === 'function' && !isSameAsProto)) { response.error = (0, _1.IMQError)('IMQ_RPC_NO_ACCESS', `Access to ${this.name}.${method}() denied!`, new Error().stack, method, args); } } else if (!isValidArgsCount(description.service.methods[method].arguments, args)) { response.error = (0, _1.IMQError)('IMQ_RPC_INVALID_ARGS_COUNT', `Invalid args count for ${this.name}.${method}().`, new Error().stack, method, args); } if (response.error) { this.logger.warn(response.error); return await send(request, response, this); } try { response.data = this[method].apply(this, args); // istanbul ignore next if (response.data && response.data.then) { response.data = await response.data; } } catch (err) { response.error = (0, _1.IMQError)(err.code || 'IMQ_RPC_CALL_ERROR', err.message, err.stack, method, args, err); } return await send(request, response, this); } /** * Initializes this instance of service and starts handling request * messages. * * @return {Promise<IMessageQueue | undefined>} */ async start() { if (!this.options.multiProcess) { this.logger.info('%s: starting single-worker, pid %s', this.name, process.pid); this.describe(); return this.startWithMetricsServer(); } if (cluster.isMaster) { const numCpus = os.cpus().length; const numWorkers = numCpus * this.options.childrenPerCore; for (let i = 0; i < numWorkers; i++) { this.logger.info('%s: starting worker #%s ...', this.name, i); cluster.fork({ workerId: i }); } // istanbul ignore next cluster.on('exit', (worker) => { this.logger.info('%s: worker pid %s died, exiting', this.name, worker.process.pid); process.exit(1); }); } else { this.logger.info('%s: worker #%s started, pid %s', this.name, process.env['workerId'], process.pid); this.describe(); return this.startWithMetricsServer(); } return this.startWithMetricsServer(); } // noinspection JSUnusedGlobalSymbols /** * Sends given data to service subscription channel * * @param {JsonObject} data */ async publish(data) { await this.imq.publish(data); } /** * Stops service from handling messages * * @return {Promise<void>} */ async stop() { await this.imq.stop(); } /** * Destroys this instance of service * * @return {Promise<void>} */ async destroy() { await this.imq.unsubscribe(); await this.imq.destroy(); } /** * Returns service description metadata. * * @returns {Promise<Description>} */ describe() { let description = serviceDescriptions.get(this.name) || null; if (!description) { description = { service: { name: this.name, methods: getClassMethods(this.constructor.name) }, types: _1.IMQRPCDescription.typesDescription }; serviceDescriptions.set(this.name, description); } return description; } async startWithMetricsServer() { const service = this.imq.start(); const metricServerOptions = this.options.metricsServer; if (!(metricServerOptions && metricServerOptions.enabled)) { return service; } this.logger.log('Starting metrics server...'); this.metricsServer = http.createServer(async (req, res) => { var _a; if (req.url === '/metrics') { const length = await this.imq.queueLength(); const content = ((_a = metricServerOptions.queueLengthFormatter) === null || _a === void 0 ? void 0 : _a.call(metricServerOptions, length, 'queue_length')) || String(length); res.setHeader('Content-Type', 'plain/text'); res.setHeader('Content-Length', Buffer.byteLength(content)); res.writeHead(200); res.end(content); return; } res.writeHead(404); res.end(); }); this.metricsServer.listen(metricServerOptions.port, '0.0.0.0', () => { this.logger.info('%s: metrics server started on port %s', this.name, metricServerOptions.port); }); return service; } } exports.IMQService = IMQService; __decorate([ (0, core_1.profile)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], IMQService.prototype, "start", null); __decorate([ (0, core_1.profile)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], IMQService.prototype, "stop", null); __decorate([ (0, core_1.profile)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], IMQService.prototype, "destroy", null); __decorate([ (0, _1.expose)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Description) ], IMQService.prototype, "describe", null); /** * Sends IMQ response with support of after call optional hook * * @param {IMQRPCRequest} request - from message identifier * @param {IMQRPCResponse} response - response to send * @param {IMQService} service - imq service to bind * @return {Promise<string>} - send result message identifier */ async function send(request, response, service) { const logger = service.options.logger || console; const id = await service.imq.send(request.from, response); if (typeof service.options.afterCall === 'function') { const afterCall = service.options.afterCall.bind(service); try { await afterCall(request, response); } catch (err) { logger.warn(_1.AFTER_HOOK_ERROR, err); } } return id; } //# sourceMappingURL=IMQService.js.map