@imqueue/rpc
Version:
RPC-like client-service implementation over messaging queue
324 lines • 12.8 kB
JavaScript
"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