@nestjs/microservices
Version:
Nest - modern, fast, powerful node.js web framework (@microservices)
340 lines (339 loc) • 14.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServerGrpc = void 0;
const shared_utils_1 = require("@nestjs/common/utils/shared.utils");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const constants_1 = require("../constants");
const decorators_1 = require("../decorators");
const enums_1 = require("../enums");
const invalid_grpc_package_exception_1 = require("../errors/invalid-grpc-package.exception");
const invalid_proto_definition_exception_1 = require("../errors/invalid-proto-definition.exception");
const server_1 = require("./server");
let grpcPackage = {};
let grpcProtoLoaderPackage = {};
class ServerGrpc extends server_1.Server {
constructor(options) {
super();
this.options = options;
this.transportId = enums_1.Transport.GRPC;
this.url = this.getOptionsProp(options, 'url') || constants_1.GRPC_DEFAULT_URL;
const protoLoader = this.getOptionsProp(options, 'protoLoader') || constants_1.GRPC_DEFAULT_PROTO_LOADER;
grpcPackage = this.loadPackage('@grpc/grpc-js', ServerGrpc.name, () => require('@grpc/grpc-js'));
grpcProtoLoaderPackage = this.loadPackage(protoLoader, ServerGrpc.name, () => protoLoader === constants_1.GRPC_DEFAULT_PROTO_LOADER
? require('@grpc/proto-loader')
: require(protoLoader));
}
async listen(callback) {
try {
this.grpcClient = await this.createClient();
await this.start(callback);
}
catch (err) {
callback(err);
}
}
async start(callback) {
await this.bindEvents();
this.grpcClient.start();
callback();
}
async bindEvents() {
const grpcContext = this.loadProto();
const packageOption = this.getOptionsProp(this.options, 'package');
const packageNames = Array.isArray(packageOption)
? packageOption
: [packageOption];
for (const packageName of packageNames) {
const grpcPkg = this.lookupPackage(grpcContext, packageName);
await this.createServices(grpcPkg);
}
}
/**
* Will return all of the services along with their fully namespaced
* names as an array of objects.
* This method initiates recursive scan of grpcPkg object
*/
getServiceNames(grpcPkg) {
// Define accumulator to collect all of the services available to load
const services = [];
// Initiate recursive services collector starting with empty name
this.collectDeepServices('', grpcPkg, services);
return services;
}
/**
* Will create service mapping from gRPC generated Object to handlers
* defined with @GrpcMethod or @GrpcStreamMethod annotations
*
* @param grpcService
* @param name
*/
async createService(grpcService, name) {
const service = {};
for (const methodName in grpcService.prototype) {
let pattern = '';
let methodHandler = null;
let streamingType = decorators_1.GrpcMethodStreamingType.NO_STREAMING;
const methodFunction = grpcService.prototype[methodName];
const methodReqStreaming = methodFunction.requestStream;
if (!(0, shared_utils_1.isUndefined)(methodReqStreaming) && methodReqStreaming) {
// Try first pattern to be presented, RX streaming pattern would be
// a preferable pattern to select among a few defined
pattern = this.createPattern(name, methodName, decorators_1.GrpcMethodStreamingType.RX_STREAMING);
methodHandler = this.messageHandlers.get(pattern);
streamingType = decorators_1.GrpcMethodStreamingType.RX_STREAMING;
// If first pattern didn't match to any of handlers then try
// pass-through handler to be presented
if (!methodHandler) {
pattern = this.createPattern(name, methodName, decorators_1.GrpcMethodStreamingType.PT_STREAMING);
methodHandler = this.messageHandlers.get(pattern);
streamingType = decorators_1.GrpcMethodStreamingType.PT_STREAMING;
}
}
else {
pattern = this.createPattern(name, methodName, decorators_1.GrpcMethodStreamingType.NO_STREAMING);
// Select handler if any presented for No-Streaming pattern
methodHandler = this.messageHandlers.get(pattern);
streamingType = decorators_1.GrpcMethodStreamingType.NO_STREAMING;
}
if (!methodHandler) {
continue;
}
service[methodName] = await this.createServiceMethod(methodHandler, grpcService.prototype[methodName], streamingType);
}
return service;
}
/**
* Will create a string of a JSON serialized format
*
* @param service name of the service which should be a match to gRPC service definition name
* @param methodName name of the method which is coming after rpc keyword
* @param streaming GrpcMethodStreamingType parameter which should correspond to
* stream keyword in gRPC service request part
*/
createPattern(service, methodName, streaming) {
return JSON.stringify({
service,
rpc: methodName,
streaming,
});
}
/**
* Will return async function which will handle gRPC call
* with Rx streams or as a direct call passthrough
*
* @param methodHandler
* @param protoNativeHandler
*/
createServiceMethod(methodHandler, protoNativeHandler, streamType) {
// If proto handler has request stream as "true" then we expect it to have
// streaming from the side of requester
if (protoNativeHandler.requestStream) {
// If any handlers were defined with GrpcStreamMethod annotation use RX
if (streamType === decorators_1.GrpcMethodStreamingType.RX_STREAMING) {
return this.createRequestStreamMethod(methodHandler, protoNativeHandler.responseStream);
}
// If any handlers were defined with GrpcStreamCall annotation
else if (streamType === decorators_1.GrpcMethodStreamingType.PT_STREAMING) {
return this.createStreamCallMethod(methodHandler, protoNativeHandler.responseStream);
}
}
return protoNativeHandler.responseStream
? this.createStreamServiceMethod(methodHandler)
: this.createUnaryServiceMethod(methodHandler);
}
createUnaryServiceMethod(methodHandler) {
return async (call, callback) => {
const handler = methodHandler(call.request, call.metadata, call);
this.transformToObservable(await handler).subscribe({
next: data => callback(null, data),
error: (err) => callback(err),
});
};
}
createStreamServiceMethod(methodHandler) {
return async (call, callback) => {
const handler = methodHandler(call.request, call.metadata, call);
const result$ = this.transformToObservable(await handler);
await result$
.pipe((0, operators_1.takeUntil)((0, rxjs_1.fromEvent)(call, constants_1.CANCEL_EVENT)), (0, operators_1.catchError)(err => {
call.emit('error', err);
return rxjs_1.EMPTY;
}))
.forEach(data => call.write(data));
call.end();
};
}
createRequestStreamMethod(methodHandler, isResponseStream) {
return async (call, callback) => {
const req = new rxjs_1.Subject();
call.on('data', (m) => req.next(m));
call.on('error', (e) => {
// Check if error means that stream ended on other end
const isCancelledError = String(e).toLowerCase().indexOf('cancelled');
if (isCancelledError) {
call.end();
return;
}
// If another error then just pass it along
req.error(e);
});
call.on('end', () => req.complete());
const handler = methodHandler(req.asObservable(), call.metadata, call);
const res = this.transformToObservable(await handler);
if (isResponseStream) {
await res
.pipe((0, operators_1.takeUntil)((0, rxjs_1.fromEvent)(call, constants_1.CANCEL_EVENT)), (0, operators_1.catchError)(err => {
call.emit('error', err);
return rxjs_1.EMPTY;
}))
.forEach(m => call.write(m));
call.end();
}
else {
const response = await (0, rxjs_1.lastValueFrom)(res.pipe((0, operators_1.takeUntil)((0, rxjs_1.fromEvent)(call, constants_1.CANCEL_EVENT)), (0, operators_1.catchError)(err => {
callback(err, null);
return rxjs_1.EMPTY;
})));
if (!(0, shared_utils_1.isUndefined)(response)) {
callback(null, response);
}
}
};
}
createStreamCallMethod(methodHandler, isResponseStream) {
return async (call, callback) => {
if (isResponseStream) {
methodHandler(call);
}
else {
methodHandler(call, callback);
}
};
}
close() {
this.grpcClient && this.grpcClient.forceShutdown();
this.grpcClient = null;
}
deserialize(obj) {
try {
return JSON.parse(obj);
}
catch (e) {
return obj;
}
}
addHandler(pattern, callback, isEventHandler = false) {
const route = (0, shared_utils_1.isString)(pattern) ? pattern : JSON.stringify(pattern);
callback.isEventHandler = isEventHandler;
this.messageHandlers.set(route, callback);
}
async createClient() {
const channelOptions = this.options && this.options.channelOptions
? this.options.channelOptions
: {};
if (this.options && this.options.maxSendMessageLength) {
channelOptions['grpc.max_send_message_length'] =
this.options.maxSendMessageLength;
}
if (this.options && this.options.maxReceiveMessageLength) {
channelOptions['grpc.max_receive_message_length'] =
this.options.maxReceiveMessageLength;
}
if (this.options && this.options.maxMetadataSize) {
channelOptions['grpc.max_metadata_size'] = this.options.maxMetadataSize;
}
const server = new grpcPackage.Server(channelOptions);
const credentials = this.getOptionsProp(this.options, 'credentials');
await new Promise((resolve, reject) => {
server.bindAsync(this.url, credentials || grpcPackage.ServerCredentials.createInsecure(), (error, port) => error ? reject(error) : resolve(port));
});
return server;
}
lookupPackage(root, packageName) {
/** Reference: https://github.com/kondi/rxjs-grpc */
let pkg = root;
for (const name of packageName.split(/\./)) {
pkg = pkg[name];
}
return pkg;
}
loadProto() {
try {
const file = this.getOptionsProp(this.options, 'protoPath');
const loader = this.getOptionsProp(this.options, 'loader');
const packageDefinition = grpcProtoLoaderPackage.loadSync(file, loader);
const packageObject = grpcPackage.loadPackageDefinition(packageDefinition);
return packageObject;
}
catch (err) {
const invalidProtoError = new invalid_proto_definition_exception_1.InvalidProtoDefinitionException();
const message = err && err.message ? err.message : invalidProtoError.message;
this.logger.error(message, invalidProtoError.stack);
throw err;
}
}
/**
* Recursively fetch all of the service methods available on loaded
* protobuf descriptor object, and collect those as an objects with
* dot-syntax full-path names.
*
* Example:
* for proto package Bundle.FirstService with service Events { rpc...
* will be resolved to object of (while loaded for Bundle package):
* {
* name: "FirstService.Events",
* service: {Object}
* }
*/
collectDeepServices(name, grpcDefinition, accumulator) {
if (!(0, shared_utils_1.isObject)(grpcDefinition)) {
return;
}
const keysToTraverse = Object.keys(grpcDefinition);
// Traverse definitions or namespace extensions
for (const key of keysToTraverse) {
const nameExtended = this.parseDeepServiceName(name, key);
const deepDefinition = grpcDefinition[key];
const isServiceDefined = deepDefinition && !(0, shared_utils_1.isUndefined)(deepDefinition.service);
const isServiceBoolean = isServiceDefined
? deepDefinition.service !== false
: false;
if (isServiceDefined && isServiceBoolean) {
accumulator.push({
name: nameExtended,
service: deepDefinition,
});
}
// Continue recursion until objects end or service definition found
else {
this.collectDeepServices(nameExtended, deepDefinition, accumulator);
}
}
}
parseDeepServiceName(name, key) {
// If depth is zero then just return key
if (name.length === 0) {
return key;
}
// Otherwise add next through dot syntax
return name + '.' + key;
}
async createServices(grpcPkg) {
if (!grpcPkg) {
const invalidPackageError = new invalid_grpc_package_exception_1.InvalidGrpcPackageException();
this.logger.error(invalidPackageError.message, invalidPackageError.stack);
throw invalidPackageError;
}
// Take all of the services defined in grpcPkg and assign them to
// method handlers defined in Controllers
for (const definition of this.getServiceNames(grpcPkg)) {
this.grpcClient.addService(
// First parameter requires exact service definition from proto
definition.service.service,
// Here full proto definition required along with namespaced pattern name
await this.createService(definition.service, definition.name));
}
}
}
exports.ServerGrpc = ServerGrpc;