mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
feat(microservices): add status, unwrap, on, and other features
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { ExceptionFilter } from './exceptions/exception-filter.interface';
|
||||
import { CanActivate } from './features/can-activate.interface';
|
||||
import { NestInterceptor } from './features/nest-interceptor.interface';
|
||||
@@ -19,8 +20,8 @@ export interface INestMicroservice extends INestApplicationContext {
|
||||
listen(): Promise<any>;
|
||||
|
||||
/**
|
||||
* Register Ws Adapter which will be used inside Gateways.
|
||||
* Use when you want to override default `socket.io` library.
|
||||
* Registers a web socket adapter that will be used for Gateways.
|
||||
* Use to override the default `socket.io` library.
|
||||
*
|
||||
* @param {WebSocketAdapter} adapter
|
||||
* @returns {this}
|
||||
@@ -28,37 +29,64 @@ export interface INestMicroservice extends INestApplicationContext {
|
||||
useWebSocketAdapter(adapter: WebSocketAdapter): this;
|
||||
|
||||
/**
|
||||
* Registers exception filters as global filters (will be used within every message pattern handler)
|
||||
* Registers global exception filters (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...ExceptionFilter} filters
|
||||
*/
|
||||
useGlobalFilters(...filters: ExceptionFilter[]): this;
|
||||
|
||||
/**
|
||||
* Registers pipes as global pipes (will be used within every message pattern handler)
|
||||
* Registers global pipes (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...PipeTransform} pipes
|
||||
*/
|
||||
useGlobalPipes(...pipes: PipeTransform<any>[]): this;
|
||||
|
||||
/**
|
||||
* Registers interceptors as global interceptors (will be used within every message pattern handler)
|
||||
* Registers global interceptors (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...NestInterceptor} interceptors
|
||||
*/
|
||||
useGlobalInterceptors(...interceptors: NestInterceptor[]): this;
|
||||
|
||||
/**
|
||||
* Registers guards as global guards (will be used within every message pattern handler)
|
||||
* Registers global guards (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...CanActivate} guards
|
||||
*/
|
||||
useGlobalGuards(...guards: CanActivate[]): this;
|
||||
|
||||
/**
|
||||
* Terminates the application
|
||||
* Terminates the application.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns an observable that emits status changes.
|
||||
*
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
status: Observable<string>;
|
||||
|
||||
/**
|
||||
* Registers an event listener for the given event.
|
||||
* @param event Event name
|
||||
* @param callback Callback to be executed when the event is emitted
|
||||
*/
|
||||
on<
|
||||
EventsMap extends Record<string, Function> = Record<string, Function>,
|
||||
EventKey extends keyof EventsMap = keyof EventsMap,
|
||||
EventCallback extends EventsMap[EventKey] = EventsMap[EventKey],
|
||||
>(
|
||||
event: EventKey,
|
||||
callback: EventCallback,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Returns an instance of the underlying server/broker instance,
|
||||
* or a group of servers if there are more than one.
|
||||
*/
|
||||
unwrap<T>(): T;
|
||||
}
|
||||
|
||||
@@ -6,23 +6,42 @@ import { GRPC_DEFAULT_PROTO_LOADER, GRPC_DEFAULT_URL } from '../constants';
|
||||
import { InvalidGrpcPackageException } from '../errors/invalid-grpc-package.exception';
|
||||
import { InvalidGrpcServiceException } from '../errors/invalid-grpc-service.exception';
|
||||
import { InvalidProtoDefinitionException } from '../errors/invalid-proto-definition.exception';
|
||||
import { ClientGrpc, GrpcOptions } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
import { GRPC_CANCELLED } from './constants';
|
||||
import { ChannelOptions } from '../external/grpc-options.interface';
|
||||
import { getGrpcPackageDefinition } from '../helpers';
|
||||
import { ClientGrpc, GrpcOptions } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
|
||||
let grpcPackage: any = {};
|
||||
let grpcProtoLoaderPackage: any = {};
|
||||
const GRPC_CANCELLED = 'Cancelled';
|
||||
|
||||
// To enable type safety for gRPC. This cant be uncommented by default
|
||||
// because it would require the user to install the @grpc/grpc-js package even if they dont use gRPC
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type GrpcClient = import('@grpc/grpc-js').Client;
|
||||
// let grpcPackage = {} as typeof import('@grpc/grpc-js');
|
||||
// let grpcProtoLoaderPackage = {} as typeof import('@grpc/proto-loader');
|
||||
|
||||
type GrpcClient = any;
|
||||
let grpcPackage = {} as any;
|
||||
let grpcProtoLoaderPackage = {} as any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientGrpcProxy extends ClientProxy implements ClientGrpc {
|
||||
export class ClientGrpcProxy
|
||||
extends ClientProxy<never, never>
|
||||
implements ClientGrpc
|
||||
{
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected readonly clients = new Map<string, any>();
|
||||
protected readonly url: string;
|
||||
protected grpcClients = [];
|
||||
protected grpcClients: GrpcClient[] = [];
|
||||
|
||||
get status(): never {
|
||||
throw new Error(
|
||||
'The "status" attribute is not supported by the gRPC transport',
|
||||
);
|
||||
}
|
||||
|
||||
constructor(protected readonly options: GrpcOptions['options']) {
|
||||
super();
|
||||
@@ -367,4 +386,15 @@ export class ClientGrpcProxy extends ClientProxy implements ClientGrpc {
|
||||
'Method is not supported in gRPC mode. Use ClientGrpc instead (learn more in the documentation).',
|
||||
);
|
||||
}
|
||||
|
||||
public on<EventKey extends never = never, EventCallback = any>(
|
||||
event: EventKey,
|
||||
callback: EventCallback,
|
||||
) {
|
||||
throw new Error('Method is not supported in gRPC mode.');
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
throw new Error('Method is not supported in gRPC mode.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { KafkaResponseDeserializer } from '../deserializers/kafka-response.deserializer';
|
||||
import { KafkaHeaders } from '../enums';
|
||||
import { InvalidKafkaClientTopicException } from '../errors/invalid-kafka-client-topic.exception';
|
||||
import { KafkaStatus } from '../events';
|
||||
import {
|
||||
BrokersFunction,
|
||||
Consumer,
|
||||
@@ -27,7 +28,9 @@ import {
|
||||
KafkaReplyPartitionAssigner,
|
||||
} from '../helpers';
|
||||
import {
|
||||
ClientKafkaProxy,
|
||||
KafkaOptions,
|
||||
MsPattern,
|
||||
OutgoingEvent,
|
||||
ReadPacket,
|
||||
WritePacket,
|
||||
@@ -43,11 +46,12 @@ let kafkaPackage: any = {};
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientKafka extends ClientProxy {
|
||||
export class ClientKafka
|
||||
extends ClientProxy<never, KafkaStatus>
|
||||
implements ClientKafkaProxy
|
||||
{
|
||||
protected logger = new Logger(ClientKafka.name);
|
||||
protected client: Kafka | null = null;
|
||||
protected consumer: Consumer | null = null;
|
||||
protected producer: Producer | null = null;
|
||||
protected parser: KafkaParser | null = null;
|
||||
protected initialized: Promise<void> | null = null;
|
||||
protected responsePatterns: string[] = [];
|
||||
@@ -56,6 +60,26 @@ export class ClientKafka extends ClientProxy {
|
||||
protected clientId: string;
|
||||
protected groupId: string;
|
||||
protected producerOnlyMode: boolean;
|
||||
protected _consumer: Consumer | null = null;
|
||||
protected _producer: Producer | null = null;
|
||||
|
||||
get consumer(): Consumer {
|
||||
if (!this._consumer) {
|
||||
throw new Error(
|
||||
'No consumer initialized. Please, call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this._consumer;
|
||||
}
|
||||
|
||||
get producer(): Producer {
|
||||
if (!this._consumer) {
|
||||
throw new Error(
|
||||
'No producer initialized. Please, call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this._producer;
|
||||
}
|
||||
|
||||
constructor(protected readonly options: KafkaOptions['options']) {
|
||||
super();
|
||||
@@ -95,28 +119,27 @@ export class ClientKafka extends ClientProxy {
|
||||
this.initializeDeserializer(options);
|
||||
}
|
||||
|
||||
public subscribeToResponseOf(pattern: any): void {
|
||||
const request = this.normalizePattern(pattern);
|
||||
public subscribeToResponseOf(pattern: unknown): void {
|
||||
const request = this.normalizePattern(pattern as MsPattern);
|
||||
this.responsePatterns.push(this.getResponsePatternName(request));
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
this.producer && (await this.producer.disconnect());
|
||||
this.consumer && (await this.consumer.disconnect());
|
||||
this.producer = null;
|
||||
this.consumer = null;
|
||||
this._producer && (await this._producer.disconnect());
|
||||
this._consumer && (await this._consumer.disconnect());
|
||||
this._producer = null;
|
||||
this._consumer = null;
|
||||
this.initialized = null;
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
public async connect(): Promise<Producer> {
|
||||
if (this.initialized) {
|
||||
return this.initialized.then(() => this.producer);
|
||||
return this.initialized.then(() => this._producer);
|
||||
}
|
||||
this.initialized = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
this.client = this.createClient();
|
||||
|
||||
if (!this.producerOnlyMode) {
|
||||
const partitionAssigners = [
|
||||
(
|
||||
@@ -136,42 +159,45 @@ export class ClientKafka extends ClientProxy {
|
||||
},
|
||||
);
|
||||
|
||||
this.consumer = this.client.consumer(consumerOptions);
|
||||
// set member assignments on join and rebalance
|
||||
this.consumer.on(
|
||||
this.consumer.events.GROUP_JOIN,
|
||||
this._consumer = this.client.consumer(consumerOptions);
|
||||
this.registerConsumerEventListeners();
|
||||
|
||||
// Set member assignments on join and rebalance
|
||||
this._consumer.on(
|
||||
this._consumer.events.GROUP_JOIN,
|
||||
this.setConsumerAssignments.bind(this),
|
||||
);
|
||||
await this.consumer.connect();
|
||||
await this._consumer.connect();
|
||||
await this.bindTopics();
|
||||
}
|
||||
|
||||
this.producer = this.client.producer(this.options.producer || {});
|
||||
await this.producer.connect();
|
||||
this._producer = this.client.producer(this.options.producer || {});
|
||||
this.registerProducerEventListeners();
|
||||
await this._producer.connect();
|
||||
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
return this.initialized.then(() => this.producer);
|
||||
return this.initialized.then(() => this._producer);
|
||||
}
|
||||
|
||||
public async bindTopics(): Promise<void> {
|
||||
if (!this.consumer) {
|
||||
if (!this._consumer) {
|
||||
throw Error('No consumer initialized');
|
||||
}
|
||||
|
||||
const consumerSubscribeOptions = this.options.subscribe || {};
|
||||
|
||||
if (this.responsePatterns.length > 0) {
|
||||
await this.consumer.subscribe({
|
||||
await this._consumer.subscribe({
|
||||
...consumerSubscribeOptions,
|
||||
topics: this.responsePatterns,
|
||||
});
|
||||
}
|
||||
|
||||
await this.consumer.run(
|
||||
await this._consumer.run(
|
||||
Object.assign(this.options.run || {}, {
|
||||
eachMessage: this.createResponseCallback(),
|
||||
}),
|
||||
@@ -223,6 +249,52 @@ export class ClientKafka extends ClientProxy {
|
||||
return this.consumerAssignments;
|
||||
}
|
||||
|
||||
public commitOffsets(
|
||||
topicPartitions: TopicPartitionOffsetAndMetadata[],
|
||||
): Promise<void> {
|
||||
if (this._consumer) {
|
||||
return this._consumer.commitOffsets(topicPartitions);
|
||||
} else {
|
||||
throw new Error('No consumer initialized');
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.client) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.client as T;
|
||||
}
|
||||
|
||||
protected registerConsumerEventListeners() {
|
||||
this._consumer.on(this._consumer.events.CONNECT, () =>
|
||||
this._status$.next(KafkaStatus.CONNECTED),
|
||||
);
|
||||
this._consumer.on(this._consumer.events.DISCONNECT, () =>
|
||||
this._status$.next(KafkaStatus.DISCONNECTED),
|
||||
);
|
||||
this._consumer.on(this._consumer.events.REBALANCING, () =>
|
||||
this._status$.next(KafkaStatus.REBALANCING),
|
||||
);
|
||||
this._consumer.on(this._consumer.events.STOP, () =>
|
||||
this._status$.next(KafkaStatus.STOPPED),
|
||||
);
|
||||
this.consumer.on(this._consumer.events.CRASH, () =>
|
||||
this._status$.next(KafkaStatus.CRASHED),
|
||||
);
|
||||
}
|
||||
|
||||
protected registerProducerEventListeners() {
|
||||
this._producer.on(this._producer.events.CONNECT, () =>
|
||||
this._status$.next(KafkaStatus.CONNECTED),
|
||||
);
|
||||
this._producer.on(this._producer.events.DISCONNECT, () =>
|
||||
this._status$.next(KafkaStatus.DISCONNECTED),
|
||||
);
|
||||
}
|
||||
|
||||
protected async dispatchEvent(packet: OutgoingEvent): Promise<any> {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const outgoingEvent = await this.serializer.serialize(packet.data, {
|
||||
@@ -236,7 +308,7 @@ export class ClientKafka extends ClientProxy {
|
||||
this.options.send || {},
|
||||
);
|
||||
|
||||
return this.producer.send(message);
|
||||
return this._producer.send(message);
|
||||
}
|
||||
|
||||
protected getReplyTopicPartition(topic: string): string {
|
||||
@@ -245,7 +317,7 @@ export class ClientKafka extends ClientProxy {
|
||||
throw new InvalidKafkaClientTopicException(topic);
|
||||
}
|
||||
|
||||
// get the minimum partition
|
||||
// Get the minimum partition
|
||||
return minimumPartition.toString();
|
||||
}
|
||||
|
||||
@@ -282,7 +354,7 @@ export class ClientKafka extends ClientProxy {
|
||||
this.options.send || {},
|
||||
);
|
||||
|
||||
return this.producer.send(message);
|
||||
return this._producer.send(message);
|
||||
})
|
||||
.catch(err => errorCallback(err));
|
||||
|
||||
@@ -299,7 +371,7 @@ export class ClientKafka extends ClientProxy {
|
||||
protected setConsumerAssignments(data: ConsumerGroupJoinEvent): void {
|
||||
const consumerAssignments: { [key: string]: number } = {};
|
||||
|
||||
// only need to set the minimum
|
||||
// Only need to set the minimum
|
||||
Object.keys(data.payload.memberAssignment).forEach(topic => {
|
||||
const memberPartitions = data.payload.memberAssignment[topic];
|
||||
|
||||
@@ -321,13 +393,10 @@ export class ClientKafka extends ClientProxy {
|
||||
(options && options.deserializer) || new KafkaResponseDeserializer();
|
||||
}
|
||||
|
||||
public commitOffsets(
|
||||
topicPartitions: TopicPartitionOffsetAndMetadata[],
|
||||
): Promise<void> {
|
||||
if (this.consumer) {
|
||||
return this.consumer.commitOffsets(topicPartitions);
|
||||
} else {
|
||||
throw new Error('No consumer initialized');
|
||||
}
|
||||
public on<
|
||||
EventKey extends string | number | symbol = string | number | symbol,
|
||||
EventCallback = any,
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
throw new Error('Method is not supported for Kafka client');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,8 @@ import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { EmptyError, fromEvent, lastValueFrom, merge, Observable } from 'rxjs';
|
||||
import { first, map, share, tap } from 'rxjs/operators';
|
||||
import {
|
||||
CLOSE_EVENT,
|
||||
ECONNREFUSED,
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
MQTT_DEFAULT_URL,
|
||||
} from '../constants';
|
||||
import { MqttClient } from '../external/mqtt-client.interface';
|
||||
import { ECONNREFUSED, MQTT_DEFAULT_URL } from '../constants';
|
||||
import { MqttEvents, MqttEventsMap, MqttStatus } from '../events/mqtt.events';
|
||||
import { MqttOptions, ReadPacket, WritePacket } from '../interfaces';
|
||||
import {
|
||||
MqttRecord,
|
||||
@@ -20,19 +14,32 @@ import { ClientProxy } from './client-proxy';
|
||||
|
||||
let mqttPackage: any = {};
|
||||
|
||||
// To enable type safety for MQTT. This cant be uncommented by default
|
||||
// because it would require the user to install the mqtt package even if they dont use MQTT
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
type MqttClient = import('mqtt').MqttClient;
|
||||
// type MqttClient = any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientMqtt extends ClientProxy {
|
||||
export class ClientMqtt extends ClientProxy<MqttEvents, MqttStatus> {
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected readonly subscriptionsCount = new Map<string, number>();
|
||||
protected readonly url: string;
|
||||
protected mqttClient: MqttClient;
|
||||
protected connection: Promise<any>;
|
||||
protected connectionPromise: Promise<any>;
|
||||
protected isInitialConnection = false;
|
||||
protected isReconnecting = false;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof MqttEvents;
|
||||
callback: MqttEvents[keyof MqttEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(protected readonly options: MqttOptions['options']) {
|
||||
super();
|
||||
this.url = this.getOptionsProp(this.options, 'url') || MQTT_DEFAULT_URL;
|
||||
this.url = this.getOptionsProp(this.options, 'url') ?? MQTT_DEFAULT_URL;
|
||||
|
||||
mqttPackage = loadPackage('mqtt', ClientMqtt.name, () => require('mqtt'));
|
||||
|
||||
@@ -51,38 +58,49 @@ export class ClientMqtt extends ClientProxy {
|
||||
public close() {
|
||||
this.mqttClient && this.mqttClient.end();
|
||||
this.mqttClient = null;
|
||||
this.connection = null;
|
||||
this.connectionPromise = null;
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public connect(): Promise<any> {
|
||||
if (this.mqttClient) {
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.mqttClient = this.createClient();
|
||||
this.handleError(this.mqttClient);
|
||||
this.registerErrorListener(this.mqttClient);
|
||||
this.registerOfflineListener(this.mqttClient);
|
||||
this.registerReconnectListener(this.mqttClient);
|
||||
this.registerConnectListener(this.mqttClient);
|
||||
this.registerDisconnectListener(this.mqttClient);
|
||||
this.registerCloseListener(this.mqttClient);
|
||||
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.mqttClient.on(event, callback),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
const connect$ = this.connect$(this.mqttClient);
|
||||
this.connection = lastValueFrom(
|
||||
this.mergeCloseEvent(this.mqttClient, connect$).pipe(
|
||||
tap(() =>
|
||||
this.mqttClient.on(MESSAGE_EVENT, this.createResponseCallback()),
|
||||
),
|
||||
share(),
|
||||
),
|
||||
this.connectionPromise = lastValueFrom(
|
||||
this.mergeCloseEvent(this.mqttClient, connect$).pipe(share()),
|
||||
).catch(err => {
|
||||
if (err instanceof EmptyError) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public mergeCloseEvent<T = any>(
|
||||
instance: MqttClient,
|
||||
source$: Observable<T>,
|
||||
): Observable<T> {
|
||||
const close$ = fromEvent(instance, CLOSE_EVENT).pipe(
|
||||
const close$ = fromEvent(instance, MqttEventsMap.CLOSE).pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
this._status$.next(MqttStatus.CLOSED);
|
||||
},
|
||||
}),
|
||||
map((err: any) => {
|
||||
throw err;
|
||||
}),
|
||||
@@ -94,13 +112,81 @@ export class ClientMqtt extends ClientProxy {
|
||||
return mqttPackage.connect(this.url, this.options as MqttOptions);
|
||||
}
|
||||
|
||||
public handleError(client: MqttClient) {
|
||||
client.addListener(
|
||||
ERROR_EVENT,
|
||||
public registerErrorListener(client: MqttClient) {
|
||||
client.on(
|
||||
MqttEventsMap.ERROR,
|
||||
(err: any) => err.code !== ECONNREFUSED && this.logger.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
public registerOfflineListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.OFFLINE, () => {
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled rejections
|
||||
this.connectionPromise.catch(() => {});
|
||||
this.logger.error('MQTT broker went offline.');
|
||||
});
|
||||
}
|
||||
|
||||
public registerReconnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.RECONNECT, () => {
|
||||
this.isReconnecting = true;
|
||||
this._status$.next(MqttStatus.RECONNECTING);
|
||||
|
||||
this.logger.log('MQTT connection lost. Trying to reconnect...');
|
||||
});
|
||||
}
|
||||
|
||||
public registerDisconnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.DISCONNECT, () => {
|
||||
this._status$.next(MqttStatus.DISCONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerCloseListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.CLOSE, () => {
|
||||
this._status$.next(MqttStatus.CLOSED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerConnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.CONNECT, () => {
|
||||
this.isReconnecting = false;
|
||||
this._status$.next(MqttStatus.CONNECTED);
|
||||
|
||||
this.logger.log('Connected to MQTT broker');
|
||||
this.connectionPromise = Promise.resolve();
|
||||
|
||||
if (!this.isInitialConnection) {
|
||||
this.isInitialConnection = true;
|
||||
client.on('message', this.createResponseCallback());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof MqttEvents = keyof MqttEvents,
|
||||
EventCallback extends MqttEvents[EventKey] = MqttEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.mqttClient) {
|
||||
this.mqttClient.on(event, callback as any);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.mqttClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.mqttClient as T;
|
||||
}
|
||||
|
||||
public createResponseCallback(): (channel: string, buffer: Buffer) => any {
|
||||
return async (channel: string, buffer: Buffer) => {
|
||||
const packet = JSON.parse(buffer.toString());
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { isObject } from '@nestjs/common/utils/shared.utils';
|
||||
import { EventEmitter } from 'stream';
|
||||
import { NATS_DEFAULT_URL } from '../constants';
|
||||
import { NatsResponseJSONDeserializer } from '../deserializers/nats-response-json.deserializer';
|
||||
import { EmptyResponseException } from '../errors/empty-response.exception';
|
||||
import { Client, NatsMsg } from '../external/nats-client.interface';
|
||||
import { NatsEvents, NatsEventsMap, NatsStatus } from '../events/nats.events';
|
||||
import { NatsOptions, PacketId, ReadPacket, WritePacket } from '../interfaces';
|
||||
import { NatsRecord } from '../record-builders';
|
||||
import { NatsRecordSerializer } from '../serializers/nats-record.serializer';
|
||||
@@ -12,13 +13,27 @@ import { ClientProxy } from './client-proxy';
|
||||
|
||||
let natsPackage = {} as any;
|
||||
|
||||
// To enable type safety for Nats. This cant be uncommented by default
|
||||
// because it would require the user to install the nats package even if they dont use Nats
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type Client = import('nats').NatsConnection;
|
||||
// type NatsMsg = import('nats').Msg;
|
||||
|
||||
type Client = any;
|
||||
type NatsMsg = any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientNats extends ClientProxy {
|
||||
export class ClientNats extends ClientProxy<NatsEvents, NatsStatus> {
|
||||
protected readonly logger = new Logger(ClientNats.name);
|
||||
|
||||
protected natsClient: Client;
|
||||
protected clientConnectionPromise: Promise<Client>;
|
||||
protected connectionPromise: Promise<Client>;
|
||||
protected statusEventEmitter = new EventEmitter<{
|
||||
[key in keyof NatsEvents]: Parameters<NatsEvents[key]>;
|
||||
}>();
|
||||
|
||||
constructor(protected readonly options: NatsOptions['options']) {
|
||||
super();
|
||||
@@ -30,22 +45,29 @@ export class ClientNats extends ClientProxy {
|
||||
|
||||
public async close() {
|
||||
await this.natsClient?.close();
|
||||
this.statusEventEmitter.removeAllListeners();
|
||||
|
||||
this.natsClient = null;
|
||||
this.clientConnectionPromise = null;
|
||||
this.connectionPromise = null;
|
||||
}
|
||||
|
||||
public async connect(): Promise<any> {
|
||||
if (this.clientConnectionPromise) {
|
||||
return this.clientConnectionPromise;
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.clientConnectionPromise = this.createClient();
|
||||
this.natsClient = await this.clientConnectionPromise;
|
||||
this.connectionPromise = this.createClient();
|
||||
this.natsClient = await this.connectionPromise.catch(err => {
|
||||
this.connectionPromise = null;
|
||||
throw err;
|
||||
});
|
||||
|
||||
this._status$.next(NatsStatus.CONNECTED);
|
||||
this.handleStatusUpdates(this.natsClient);
|
||||
return this.natsClient;
|
||||
}
|
||||
|
||||
public createClient(): Promise<Client> {
|
||||
const options: any = this.options || ({} as NatsOptions);
|
||||
const options = this.options || ({} as NatsOptions);
|
||||
return natsPackage.connect({
|
||||
servers: NATS_DEFAULT_URL,
|
||||
...options,
|
||||
@@ -61,12 +83,46 @@ export class ClientNats extends ClientProxy {
|
||||
|
||||
switch (status.type) {
|
||||
case 'error':
|
||||
case 'disconnect':
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
// Prevent unhandled promise rejection
|
||||
this.connectionPromise.catch(() => {});
|
||||
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
|
||||
this._status$.next(NatsStatus.DISCONNECTED);
|
||||
this.statusEventEmitter.emit(
|
||||
NatsEventsMap.DISCONNECT,
|
||||
status.data as string,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'reconnecting':
|
||||
this._status$.next(NatsStatus.RECONNECTING);
|
||||
break;
|
||||
|
||||
case 'reconnect':
|
||||
this.connectionPromise = Promise.resolve(client);
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
|
||||
this._status$.next(NatsStatus.CONNECTED);
|
||||
this.statusEventEmitter.emit(
|
||||
NatsEventsMap.RECONNECT,
|
||||
status.data as string,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'pingTimer':
|
||||
if (this.options.debug) {
|
||||
this.logger.debug(
|
||||
@@ -75,6 +131,13 @@ export class ClientNats extends ClientProxy {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
this.statusEventEmitter.emit(NatsEventsMap.UPDATE, status.data);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
@@ -84,6 +147,22 @@ export class ClientNats extends ClientProxy {
|
||||
}
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof NatsEvents = keyof NatsEvents,
|
||||
EventCallback extends NatsEvents[EventKey] = NatsEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
this.statusEventEmitter.on(event, callback as any);
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.natsClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.natsClient as T;
|
||||
}
|
||||
|
||||
public createSubscriptionHandler(
|
||||
packet: ReadPacket & PacketId,
|
||||
callback: (packet: WritePacket) => any,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import { ClientKafkaProxy } from '../interfaces';
|
||||
import {
|
||||
ClientOptions,
|
||||
CustomClientOptions,
|
||||
TcpClientOptions,
|
||||
} from '../interfaces/client-metadata.interface';
|
||||
import { Closeable } from '../interfaces/closeable.interface';
|
||||
import {
|
||||
GrpcOptions,
|
||||
KafkaOptions,
|
||||
@@ -23,7 +23,7 @@ import { ClientRMQ } from './client-rmq';
|
||||
import { ClientTCP } from './client-tcp';
|
||||
|
||||
export interface IClientProxyFactory {
|
||||
create(clientOptions: ClientOptions): ClientProxy & Closeable;
|
||||
create(clientOptions: ClientOptions): ClientProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,33 +33,38 @@ export class ClientProxyFactory {
|
||||
public static create(
|
||||
clientOptions: { transport: Transport.GRPC } & ClientOptions,
|
||||
): ClientGrpcProxy;
|
||||
public static create(clientOptions: ClientOptions): ClientProxy & Closeable;
|
||||
public static create(
|
||||
clientOptions: CustomClientOptions,
|
||||
): ClientProxy & Closeable;
|
||||
clientOptions: { transport: Transport.KAFKA } & ClientOptions,
|
||||
): ClientKafkaProxy;
|
||||
public static create(clientOptions: ClientOptions): ClientProxy;
|
||||
public static create(clientOptions: CustomClientOptions): ClientProxy;
|
||||
public static create(
|
||||
clientOptions: ClientOptions | CustomClientOptions,
|
||||
): ClientProxy & Closeable {
|
||||
): ClientProxy | ClientGrpcProxy | ClientKafkaProxy {
|
||||
if (this.isCustomClientOptions(clientOptions)) {
|
||||
const { customClass, options } = clientOptions;
|
||||
return new customClass(options);
|
||||
}
|
||||
const { transport, options } = clientOptions || {};
|
||||
const { transport, options = {} } = clientOptions ?? { options: {} };
|
||||
switch (transport) {
|
||||
case Transport.REDIS:
|
||||
return new ClientRedis(options as RedisOptions['options']);
|
||||
return new ClientRedis(
|
||||
options as RedisOptions['options'],
|
||||
) as ClientProxy;
|
||||
case Transport.NATS:
|
||||
return new ClientNats(options as NatsOptions['options']);
|
||||
return new ClientNats(options as NatsOptions['options']) as ClientProxy;
|
||||
case Transport.MQTT:
|
||||
return new ClientMqtt(options as MqttOptions['options']);
|
||||
return new ClientMqtt(options as MqttOptions['options']) as ClientProxy;
|
||||
case Transport.GRPC:
|
||||
return new ClientGrpcProxy(options as GrpcOptions['options']);
|
||||
case Transport.RMQ:
|
||||
return new ClientRMQ(options as RmqOptions['options']);
|
||||
return new ClientRMQ(options as RmqOptions['options']) as ClientProxy;
|
||||
case Transport.KAFKA:
|
||||
return new ClientKafka(options as KafkaOptions['options']);
|
||||
default:
|
||||
return new ClientTCP(options as TcpClientOptions['options']);
|
||||
return new ClientTCP(
|
||||
options as TcpClientOptions['options'],
|
||||
) as ClientProxy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
|
||||
import { isNil } from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
throwError as _throw,
|
||||
connectable,
|
||||
defer,
|
||||
fromEvent,
|
||||
merge,
|
||||
Observable,
|
||||
Observer,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
throwError as _throw,
|
||||
} from 'rxjs';
|
||||
import { map, mergeMap, take } from 'rxjs/operators';
|
||||
import { CONNECT_EVENT, ERROR_EVENT } from '../constants';
|
||||
import { distinctUntilChanged, map, mergeMap, take } from 'rxjs/operators';
|
||||
import { IncomingResponseDeserializer } from '../deserializers/incoming-response.deserializer';
|
||||
import { InvalidMessageException } from '../errors/invalid-message.exception';
|
||||
import {
|
||||
@@ -32,14 +32,57 @@ import { ProducerSerializer } from '../interfaces/serializer.interface';
|
||||
import { IdentitySerializer } from '../serializers/identity.serializer';
|
||||
import { transformPatternToRoute } from '../utils';
|
||||
|
||||
export abstract class ClientProxy {
|
||||
public abstract connect(): Promise<any>;
|
||||
public abstract close(): any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export abstract class ClientProxy<
|
||||
EventsMap extends Record<string, Function> = Record<string, Function>,
|
||||
Status extends string = string,
|
||||
> {
|
||||
protected routingMap = new Map<string, Function>();
|
||||
protected serializer: ProducerSerializer;
|
||||
protected deserializer: ProducerDeserializer;
|
||||
protected _status$ = new ReplaySubject<Status>(1);
|
||||
|
||||
/**
|
||||
* Returns an observable that emits status changes.
|
||||
*/
|
||||
public get status(): Observable<Status> {
|
||||
return this._status$.asObservable().pipe(distinctUntilChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the connection to the underlying server/broker.
|
||||
*/
|
||||
public abstract connect(): Promise<any>;
|
||||
/**
|
||||
* Closes the underlying connection to the server/broker.
|
||||
*/
|
||||
public abstract close(): any;
|
||||
/**
|
||||
* Registers an event listener for the given event.
|
||||
* @param event Event name
|
||||
* @param callback Callback to be executed when the event is emitted
|
||||
*/
|
||||
public on<
|
||||
EventKey extends keyof EventsMap = keyof EventsMap,
|
||||
EventCallback extends EventsMap[EventKey] = EventsMap[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
/**
|
||||
* Returns an instance of the underlying server/broker instance,
|
||||
* or a group of servers if there are more than one.
|
||||
*/
|
||||
public abstract unwrap<T>(): T;
|
||||
|
||||
/**
|
||||
* Send a message to the server/broker.
|
||||
* Used for message-driven communication style between microservices.
|
||||
* @param pattern Pattern to identify the message
|
||||
* @param data Data to be sent
|
||||
* @returns Observable with the result
|
||||
*/
|
||||
public send<TResult = any, TInput = any>(
|
||||
pattern: any,
|
||||
data: TInput,
|
||||
@@ -58,6 +101,13 @@ export abstract class ClientProxy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to the server/broker.
|
||||
* Used for event-driven communication style between microservices.
|
||||
* @param pattern Pattern to identify the event
|
||||
* @param data Data to be sent
|
||||
* @returns Observable that completes when the event is successfully emitted
|
||||
*/
|
||||
public emit<TResult = any, TInput = any>(
|
||||
pattern: any,
|
||||
data: TInput,
|
||||
@@ -114,8 +164,8 @@ export abstract class ClientProxy {
|
||||
|
||||
protected connect$(
|
||||
instance: any,
|
||||
errorEvent = ERROR_EVENT,
|
||||
connectEvent = CONNECT_EVENT,
|
||||
errorEvent = 'error',
|
||||
connectEvent = 'connect',
|
||||
): Observable<any> {
|
||||
const error$ = fromEvent(instance, errorEvent).pipe(
|
||||
map((err: any) => {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { REDIS_DEFAULT_HOST, REDIS_DEFAULT_PORT } from '../constants';
|
||||
import {
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
REDIS_DEFAULT_HOST,
|
||||
REDIS_DEFAULT_PORT,
|
||||
} from '../constants';
|
||||
RedisEvents,
|
||||
RedisEventsMap,
|
||||
RedisStatus,
|
||||
} from '../events/redis.events';
|
||||
import { ReadPacket, RedisOptions, WritePacket } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
|
||||
// To enable type safety for Redis. This cant be uncommented by default
|
||||
// because it would require the user to install the ioredis package even if they dont use Redis
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type Redis = import('ioredis').Redis;
|
||||
type Redis = any;
|
||||
|
||||
let redisPackage = {} as any;
|
||||
@@ -16,13 +21,18 @@ let redisPackage = {} as any;
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientRedis extends ClientProxy {
|
||||
export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected readonly subscriptionsCount = new Map<string, number>();
|
||||
protected pubClient: Redis;
|
||||
protected subClient: Redis;
|
||||
protected connection: Promise<any>;
|
||||
protected isExplicitlyTerminated = false;
|
||||
protected connectionPromise: Promise<any>;
|
||||
protected isManuallyClosed = false;
|
||||
protected wasInitialConnectionSuccessful = false;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof RedisEvents;
|
||||
callback: RedisEvents[keyof RedisEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(protected readonly options: RedisOptions['options']) {
|
||||
super();
|
||||
@@ -47,26 +57,37 @@ export class ClientRedis extends ClientProxy {
|
||||
this.pubClient && this.pubClient.quit();
|
||||
this.subClient && this.subClient.quit();
|
||||
this.pubClient = this.subClient = null;
|
||||
this.isExplicitlyTerminated = true;
|
||||
this.isManuallyClosed = true;
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public async connect(): Promise<any> {
|
||||
if (this.pubClient && this.subClient) {
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.pubClient = this.createClient();
|
||||
this.subClient = this.createClient();
|
||||
this.handleError(this.pubClient);
|
||||
this.handleError(this.subClient);
|
||||
|
||||
this.connection = Promise.all([
|
||||
[this.pubClient, this.subClient].forEach((client, index) => {
|
||||
const type = index === 0 ? 'pub' : 'sub';
|
||||
this.registerErrorListener(client);
|
||||
this.registerReconnectListener(client);
|
||||
this.registerReadyListener(client);
|
||||
this.registerEndListener(client);
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
client.on(event, (...args: [any]) => callback(type, ...args)),
|
||||
);
|
||||
});
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
this.connectionPromise = Promise.all([
|
||||
this.subClient.connect(),
|
||||
this.pubClient.connect(),
|
||||
]);
|
||||
await this.connection;
|
||||
await this.connectionPromise;
|
||||
|
||||
this.subClient.on(MESSAGE_EVENT, this.createResponseCallback());
|
||||
return this.connection;
|
||||
this.subClient.on('message', this.createResponseCallback());
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public createClient(): Redis {
|
||||
@@ -78,8 +99,76 @@ export class ClientRedis extends ClientProxy {
|
||||
});
|
||||
}
|
||||
|
||||
public handleError(client: Redis) {
|
||||
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
|
||||
public registerErrorListener(client: Redis) {
|
||||
client.addListener(RedisEventsMap.ERROR, (err: any) =>
|
||||
this.logger.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
public registerReconnectListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on(RedisEventsMap.RECONNECTING, () => {
|
||||
if (this.isManuallyClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled rejections
|
||||
this.connectionPromise.catch(() => {});
|
||||
|
||||
this._status$.next(RedisStatus.RECONNECTING);
|
||||
|
||||
if (this.wasInitialConnectionSuccessful) {
|
||||
this.logger.log('Reconnecting to Redis...');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerReadyListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on(RedisEventsMap.READY, () => {
|
||||
this.connectionPromise = Promise.resolve();
|
||||
this._status$.next(RedisStatus.CONNECTED);
|
||||
|
||||
this.logger.log('Connected to Redis. Subscribing to channels...');
|
||||
|
||||
if (!this.wasInitialConnectionSuccessful) {
|
||||
this.wasInitialConnectionSuccessful = true;
|
||||
this.subClient.on('message', this.createResponseCallback());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerEndListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on('end', () => {
|
||||
if (this.isManuallyClosed) {
|
||||
return;
|
||||
}
|
||||
this._status$.next(RedisStatus.DISCONNECTED);
|
||||
|
||||
if (this.getOptionsProp(this.options, 'retryAttempts') === undefined) {
|
||||
// When retryAttempts is not specified, the connection will not be re-established
|
||||
this.logger.error('Disconnected from Redis.');
|
||||
|
||||
// Clean up client instances and just recreate them when connect is called
|
||||
this.pubClient = this.subClient = null;
|
||||
} else {
|
||||
this.logger.error('Disconnected from Redis.');
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled rejections
|
||||
this.connectionPromise.catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getClientOptions(): Partial<RedisOptions['options']> {
|
||||
@@ -91,18 +180,42 @@ export class ClientRedis extends ClientProxy {
|
||||
};
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof RedisEvents = keyof RedisEvents,
|
||||
EventCallback extends RedisEvents[EventKey] = RedisEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.subClient && this.pubClient) {
|
||||
this.subClient.on(event, (...args: [any]) => callback('sub', ...args));
|
||||
this.pubClient.on(event, (...args: [any]) => callback('pub', ...args));
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.pubClient || !this.subClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return [this.pubClient, this.subClient] as T;
|
||||
}
|
||||
|
||||
public createRetryStrategy(times: number): undefined | number {
|
||||
if (this.isExplicitlyTerminated) {
|
||||
if (this.isManuallyClosed) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!this.getOptionsProp(this.options, 'retryAttempts') ||
|
||||
times > this.getOptionsProp(this.options, 'retryAttempts')
|
||||
) {
|
||||
if (!this.getOptionsProp(this.options, 'retryAttempts')) {
|
||||
this.logger.error(
|
||||
'Redis connection closed and retry attempts not specified',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (times > this.getOptionsProp(this.options, 'retryAttempts')) {
|
||||
this.logger.error('Retry time exhausted');
|
||||
return;
|
||||
}
|
||||
return this.getOptionsProp(this.options, 'retryDelay') || 0;
|
||||
return this.getOptionsProp(this.options, 'retryDelay') ?? 5000;
|
||||
}
|
||||
|
||||
public createResponseCallback(): (
|
||||
|
||||
@@ -13,11 +13,7 @@ import {
|
||||
} from 'rxjs';
|
||||
import { first, map, retryWhen, scan, skip, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
CONNECT_EVENT,
|
||||
CONNECT_FAILED_EVENT,
|
||||
DISCONNECT_EVENT,
|
||||
DISCONNECTED_RMQ_MESSAGE,
|
||||
ERROR_EVENT,
|
||||
RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT,
|
||||
RQM_DEFAULT_NO_ASSERT,
|
||||
RQM_DEFAULT_NOACK,
|
||||
@@ -27,47 +23,53 @@ import {
|
||||
RQM_DEFAULT_QUEUE_OPTIONS,
|
||||
RQM_DEFAULT_URL,
|
||||
} from '../constants';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { RmqEvents, RmqEventsMap, RmqStatus } from '../events/rmq.events';
|
||||
import { ReadPacket, RmqOptions, WritePacket } from '../interfaces';
|
||||
import { RmqRecord } from '../record-builders';
|
||||
import { RmqRecordSerializer } from '../serializers/rmq-record.serializer';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
|
||||
// import type {
|
||||
// AmqpConnectionManager,
|
||||
// ChannelWrapper,
|
||||
// } from 'amqp-connection-manager';
|
||||
// import type { Channel, ConsumeMessage } from 'amqplib';
|
||||
// To enable type safety for RMQ. This cant be uncommented by default
|
||||
// because it would require the user to install the amqplib package even if they dont use RabbitMQ
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type AmqpConnectionManager =
|
||||
// import('amqp-connection-manager').AmqpConnectionManager;
|
||||
// type ChannelWrapper = import('amqp-connection-manager').ChannelWrapper;
|
||||
// type Channel = import('amqplib').Channel;
|
||||
// type ConsumeMessage = import('amqplib').ConsumeMessage;
|
||||
|
||||
type Channel = any;
|
||||
type ChannelWrapper = any;
|
||||
type ConsumeMessage = any;
|
||||
type AmqpConnectionManager = any;
|
||||
|
||||
let rmqPackage: any = {};
|
||||
let rmqPackage = {} as any; // typeof import('amqp-connection-manager');
|
||||
|
||||
const REPLY_QUEUE = 'amq.rabbitmq.reply-to';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientRMQ extends ClientProxy {
|
||||
export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected connection$: ReplaySubject<any>;
|
||||
protected connection: Promise<any>;
|
||||
protected connectionPromise: Promise<void>;
|
||||
protected client: AmqpConnectionManager = null;
|
||||
protected channel: ChannelWrapper = null;
|
||||
protected urls: string[] | RmqUrl[];
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof RmqEvents;
|
||||
callback: RmqEvents[keyof RmqEvents];
|
||||
}> = [];
|
||||
protected isInitialConnect = true;
|
||||
protected responseEmitter: EventEmitter;
|
||||
protected queue: string;
|
||||
protected queueOptions: Record<string, any>;
|
||||
protected responseEmitter: EventEmitter;
|
||||
protected replyQueue: string;
|
||||
protected persistent: boolean;
|
||||
protected noAssert: boolean;
|
||||
|
||||
constructor(protected readonly options: RmqOptions['options']) {
|
||||
super();
|
||||
this.urls = this.getOptionsProp(this.options, 'urls') || [RQM_DEFAULT_URL];
|
||||
this.queue =
|
||||
this.getOptionsProp(this.options, 'queue') || RQM_DEFAULT_QUEUE;
|
||||
this.queueOptions =
|
||||
@@ -75,8 +77,6 @@ export class ClientRMQ extends ClientProxy {
|
||||
RQM_DEFAULT_QUEUE_OPTIONS;
|
||||
this.replyQueue =
|
||||
this.getOptionsProp(this.options, 'replyQueue') || REPLY_QUEUE;
|
||||
this.persistent =
|
||||
this.getOptionsProp(this.options, 'persistent') || RQM_DEFAULT_PERSISTENT;
|
||||
this.noAssert =
|
||||
this.getOptionsProp(this.options, 'noAssert') ??
|
||||
this.queueOptions.noAssert ??
|
||||
@@ -96,15 +96,22 @@ export class ClientRMQ extends ClientProxy {
|
||||
this.client && this.client.close();
|
||||
this.channel = null;
|
||||
this.client = null;
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public connect(): Promise<any> {
|
||||
if (this.client) {
|
||||
return this.convertConnectionToPromise();
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.client = this.createClient();
|
||||
this.handleError(this.client);
|
||||
this.handleDisconnectError(this.client);
|
||||
|
||||
this.registerErrorListener(this.client);
|
||||
this.registerDisconnectListener(this.client);
|
||||
this.registerConnectListener(this.client);
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.client.on(event, callback),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
this.responseEmitter = new EventEmitter();
|
||||
this.responseEmitter.setMaxListeners(0);
|
||||
@@ -115,13 +122,16 @@ export class ClientRMQ extends ClientProxy {
|
||||
connect$,
|
||||
).pipe(switchMap(() => this.createChannel()));
|
||||
|
||||
const withReconnect$ = fromEvent(this.client, CONNECT_EVENT).pipe(skip(1));
|
||||
const withReconnect$ = fromEvent(this.client, RmqEventsMap.CONNECT).pipe(
|
||||
skip(1),
|
||||
);
|
||||
const source$ = merge(withDisconnect$, withReconnect$);
|
||||
|
||||
this.connection$ = new ReplaySubject(1);
|
||||
source$.subscribe(this.connection$);
|
||||
this.connectionPromise = this.convertConnectionToPromise();
|
||||
|
||||
return this.convertConnectionToPromise();
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public createChannel(): Promise<void> {
|
||||
@@ -135,9 +145,8 @@ export class ClientRMQ extends ClientProxy {
|
||||
|
||||
public createClient(): AmqpConnectionManager {
|
||||
const socketOptions = this.getOptionsProp(this.options, 'socketOptions');
|
||||
return rmqPackage.connect(this.urls, {
|
||||
connectionOptions: socketOptions?.connectionOptions,
|
||||
});
|
||||
const urls = this.getOptionsProp(this.options, 'urls') || [RQM_DEFAULT_URL];
|
||||
return rmqPackage.connect(urls, socketOptions);
|
||||
}
|
||||
|
||||
public mergeDisconnectEvent<T = any>(
|
||||
@@ -150,10 +159,11 @@ export class ClientRMQ extends ClientProxy {
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
const disconnect$ = eventToError(DISCONNECT_EVENT);
|
||||
const disconnect$ = eventToError(RmqEventsMap.DISCONNECT);
|
||||
|
||||
const urls = this.getOptionsProp(this.options, 'urls', []);
|
||||
const connectFailed$ = eventToError(CONNECT_FAILED_EVENT).pipe(
|
||||
const connectFailedEventKey = 'connectFailed';
|
||||
const connectFailed$ = eventToError(connectFailedEventKey).pipe(
|
||||
retryWhen(e =>
|
||||
e.pipe(
|
||||
scan((errorCount, error: any) => {
|
||||
@@ -209,31 +219,81 @@ export class ClientRMQ extends ClientProxy {
|
||||
);
|
||||
}
|
||||
|
||||
public handleError(client: AmqpConnectionManager): void {
|
||||
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
|
||||
public registerErrorListener(client: AmqpConnectionManager): void {
|
||||
client.addListener(RmqEventsMap.ERROR, (err: any) =>
|
||||
this.logger.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
public handleDisconnectError(client: AmqpConnectionManager): void {
|
||||
client.addListener(DISCONNECT_EVENT, (err: any) => {
|
||||
public registerDisconnectListener(client: AmqpConnectionManager): void {
|
||||
client.addListener(RmqEventsMap.DISCONNECT, (err: any) => {
|
||||
this._status$.next(RmqStatus.DISCONNECTED);
|
||||
|
||||
if (!this.isInitialConnect) {
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled promise rejection
|
||||
this.connectionPromise.catch(() => {});
|
||||
}
|
||||
|
||||
this.logger.error(DISCONNECTED_RMQ_MESSAGE);
|
||||
this.logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
private registerConnectListener(client: AmqpConnectionManager): void {
|
||||
client.addListener(RmqEventsMap.CONNECT, () => {
|
||||
this._status$.next(RmqStatus.CONNECTED);
|
||||
this.logger.log('Successfully connected to RMQ broker');
|
||||
|
||||
if (this.isInitialConnect) {
|
||||
this.isInitialConnect = false;
|
||||
|
||||
if (!this.channel) {
|
||||
this.connectionPromise = this.createChannel();
|
||||
}
|
||||
} else {
|
||||
this.connectionPromise = Promise.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof RmqEvents = keyof RmqEvents,
|
||||
EventCallback extends RmqEvents[EventKey] = RmqEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.client) {
|
||||
this.client.addListener(event, callback);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.client) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.client as T;
|
||||
}
|
||||
|
||||
public async handleMessage(
|
||||
packet: unknown,
|
||||
callback: (packet: WritePacket) => any,
|
||||
);
|
||||
): Promise<void>;
|
||||
public async handleMessage(
|
||||
packet: unknown,
|
||||
options: Record<string, unknown>,
|
||||
callback: (packet: WritePacket) => any,
|
||||
);
|
||||
): Promise<void>;
|
||||
public async handleMessage(
|
||||
packet: unknown,
|
||||
options: Record<string, unknown> | ((packet: WritePacket) => any),
|
||||
callback?: (packet: WritePacket) => any,
|
||||
) {
|
||||
): Promise<void> {
|
||||
if (isFunction(options)) {
|
||||
callback = options as (packet: WritePacket) => any;
|
||||
options = undefined;
|
||||
@@ -289,7 +349,11 @@ export class ClientRMQ extends ClientProxy {
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
replyTo: this.replyQueue,
|
||||
persistent: this.persistent,
|
||||
persistent: this.getOptionsProp(
|
||||
this.options,
|
||||
'persistent',
|
||||
RQM_DEFAULT_PERSISTENT,
|
||||
),
|
||||
...options,
|
||||
headers: this.mergeHeaders(options?.headers),
|
||||
correlationId,
|
||||
@@ -314,7 +378,11 @@ export class ClientRMQ extends ClientProxy {
|
||||
this.queue,
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
persistent: this.persistent,
|
||||
persistent: this.getOptionsProp(
|
||||
this.options,
|
||||
'persistent',
|
||||
RQM_DEFAULT_PERSISTENT,
|
||||
),
|
||||
...options,
|
||||
headers: this.mergeHeaders(options?.headers),
|
||||
},
|
||||
|
||||
@@ -2,17 +2,10 @@ import { Logger, Type } from '@nestjs/common';
|
||||
import * as net from 'net';
|
||||
import { EmptyError, lastValueFrom } from 'rxjs';
|
||||
import { share, tap } from 'rxjs/operators';
|
||||
import { ConnectionOptions } from 'tls';
|
||||
import {
|
||||
CLOSE_EVENT,
|
||||
ECONNREFUSED,
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
TCP_DEFAULT_HOST,
|
||||
TCP_DEFAULT_PORT,
|
||||
} from '../constants';
|
||||
import { ConnectionOptions, connect as tlsConnect, TLSSocket } from 'tls';
|
||||
import { ECONNREFUSED, TCP_DEFAULT_HOST, TCP_DEFAULT_PORT } from '../constants';
|
||||
import { TcpEvents, TcpEventsMap, TcpStatus } from '../events/tcp.events';
|
||||
import { JsonSocket, TcpSocket } from '../helpers';
|
||||
import { connect as tlsConnect, TLSSocket } from 'tls';
|
||||
import { PacketId, ReadPacket, WritePacket } from '../interfaces';
|
||||
import { TcpClientOptions } from '../interfaces/client-metadata.interface';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
@@ -20,15 +13,18 @@ import { ClientProxy } from './client-proxy';
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientTCP extends ClientProxy {
|
||||
protected connection: Promise<any>;
|
||||
private readonly logger = new Logger(ClientTCP.name);
|
||||
private readonly port: number;
|
||||
private readonly host: string;
|
||||
private readonly socketClass: Type<TcpSocket>;
|
||||
private isConnected = false;
|
||||
private socket: TcpSocket;
|
||||
public tlsOptions?: ConnectionOptions;
|
||||
export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
|
||||
protected readonly logger = new Logger(ClientTCP.name);
|
||||
protected readonly port: number;
|
||||
protected readonly host: string;
|
||||
protected readonly socketClass: Type<TcpSocket>;
|
||||
protected readonly tlsOptions?: ConnectionOptions;
|
||||
protected socket: TcpSocket;
|
||||
protected connectionPromise: Promise<any>;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof TcpEvents;
|
||||
callback: TcpEvents[keyof TcpEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(options: TcpClientOptions['options']) {
|
||||
super();
|
||||
@@ -43,16 +39,22 @@ export class ClientTCP extends ClientProxy {
|
||||
}
|
||||
|
||||
public connect(): Promise<any> {
|
||||
if (this.connection) {
|
||||
return this.connection;
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.socket = this.createSocket();
|
||||
this.bindEvents(this.socket);
|
||||
this.registerConnectListener(this.socket);
|
||||
this.registerCloseListener(this.socket);
|
||||
this.registerErrorListener(this.socket);
|
||||
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.socket.on(event, callback as any),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
const source$ = this.connect$(this.socket.netSocket).pipe(
|
||||
tap(() => {
|
||||
this.isConnected = true;
|
||||
this.socket.on(MESSAGE_EVENT, (buffer: WritePacket & PacketId) =>
|
||||
this.socket.on('message', (buffer: WritePacket & PacketId) =>
|
||||
this.handleResponse(buffer),
|
||||
);
|
||||
}),
|
||||
@@ -63,14 +65,14 @@ export class ClientTCP extends ClientProxy {
|
||||
if (!this.tlsOptions) {
|
||||
this.socket.connect(this.port, this.host);
|
||||
}
|
||||
this.connection = lastValueFrom(source$).catch(err => {
|
||||
this.connectionPromise = lastValueFrom(source$).catch(err => {
|
||||
if (err instanceof EmptyError) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public async handleResponse(buffer: unknown): Promise<void> {
|
||||
@@ -114,14 +116,30 @@ export class ClientTCP extends ClientProxy {
|
||||
public close() {
|
||||
this.socket && this.socket.end();
|
||||
this.handleClose();
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public bindEvents(socket: TcpSocket) {
|
||||
socket.on(
|
||||
ERROR_EVENT,
|
||||
(err: any) => err.code !== ECONNREFUSED && this.handleError(err),
|
||||
);
|
||||
socket.on(CLOSE_EVENT, () => this.handleClose());
|
||||
public registerConnectListener(socket: TcpSocket) {
|
||||
socket.on(TcpEventsMap.CONNECT, () => {
|
||||
this._status$.next(TcpStatus.CONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerErrorListener(socket: TcpSocket) {
|
||||
socket.on(TcpEventsMap.ERROR, err => {
|
||||
if (err.code !== ECONNREFUSED) {
|
||||
this.handleError(err);
|
||||
} else {
|
||||
this._status$.next(TcpStatus.DISCONNECTED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerCloseListener(socket: TcpSocket) {
|
||||
socket.on(TcpEventsMap.CLOSE, () => {
|
||||
this._status$.next(TcpStatus.DISCONNECTED);
|
||||
this.handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
public handleError(err: any) {
|
||||
@@ -129,9 +147,8 @@ export class ClientTCP extends ClientProxy {
|
||||
}
|
||||
|
||||
public handleClose() {
|
||||
this.isConnected = false;
|
||||
this.socket = null;
|
||||
this.connection = undefined;
|
||||
this.connectionPromise = undefined;
|
||||
|
||||
if (this.routingMap.size > 0) {
|
||||
const err = new Error('Connection closed');
|
||||
@@ -142,6 +159,26 @@ export class ClientTCP extends ClientProxy {
|
||||
}
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof TcpEvents = keyof TcpEvents,
|
||||
EventCallback extends TcpEvents[EventKey] = TcpEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.socket) {
|
||||
this.socket.on(event, callback as any);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.socket) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.socket.netSocket as T;
|
||||
}
|
||||
|
||||
protected publish(
|
||||
partialPacket: ReadPacket,
|
||||
callback: (packet: WritePacket) => any,
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const GRPC_CANCELLED = 'Cancelled';
|
||||
export const RABBITMQ_REPLY_QUEUE = 'amq.rabbitmq.reply-to';
|
||||
@@ -2,25 +2,29 @@ import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
|
||||
|
||||
export const TCP_DEFAULT_PORT = 3000;
|
||||
export const TCP_DEFAULT_HOST = 'localhost';
|
||||
|
||||
export const REDIS_DEFAULT_PORT = 6379;
|
||||
export const REDIS_DEFAULT_HOST = 'localhost';
|
||||
|
||||
export const NATS_DEFAULT_URL = 'nats://localhost:4222';
|
||||
export const MQTT_DEFAULT_URL = 'mqtt://localhost:1883';
|
||||
export const GRPC_DEFAULT_URL = 'localhost:5000';
|
||||
export const RQM_DEFAULT_URL = 'amqp://localhost';
|
||||
export const KAFKA_DEFAULT_BROKER = 'localhost:9092';
|
||||
export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer';
|
||||
export const KAFKA_DEFAULT_GROUP = 'nestjs-group';
|
||||
export const MQTT_SEPARATOR = '/';
|
||||
export const MQTT_WILDCARD_SINGLE = '+';
|
||||
export const MQTT_WILDCARD_ALL = '#';
|
||||
export const RQM_DEFAULT_QUEUE = 'default';
|
||||
export const RQM_DEFAULT_PREFETCH_COUNT = 0;
|
||||
export const RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT = false;
|
||||
export const RQM_DEFAULT_QUEUE_OPTIONS = {};
|
||||
export const RQM_DEFAULT_NOACK = true;
|
||||
export const RQM_DEFAULT_PERSISTENT = false;
|
||||
export const RQM_DEFAULT_NO_ASSERT = false;
|
||||
|
||||
export const CONNECT_EVENT = 'connect';
|
||||
export const DISCONNECT_EVENT = 'disconnect';
|
||||
export const CONNECT_FAILED_EVENT = 'connectFailed';
|
||||
export const MESSAGE_EVENT = 'message';
|
||||
export const DATA_EVENT = 'data';
|
||||
export const ERROR_EVENT = 'error';
|
||||
export const CLOSE_EVENT = 'close';
|
||||
export const SUBSCRIBE = 'subscribe';
|
||||
export const CANCEL_EVENT = 'cancelled';
|
||||
export const ECONNREFUSED = 'ECONNREFUSED';
|
||||
export const CONN_ERR = 'CONN_ERR';
|
||||
export const EADDRINUSE = 'EADDRINUSE';
|
||||
|
||||
export const PATTERN_METADATA = 'microservices:pattern';
|
||||
export const PATTERN_EXTRAS_METADATA = 'microservices:pattern_extras';
|
||||
@@ -29,17 +33,9 @@ export const CLIENT_CONFIGURATION_METADATA = 'microservices:client';
|
||||
export const PATTERN_HANDLER_METADATA = 'microservices:handler_type';
|
||||
export const CLIENT_METADATA = 'microservices:is_client_instance';
|
||||
export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA;
|
||||
|
||||
export const REQUEST_PATTERN_METADATA = 'microservices:request_pattern';
|
||||
export const REPLY_PATTERN_METADATA = 'microservices:reply_pattern';
|
||||
|
||||
export const RQM_DEFAULT_QUEUE = 'default';
|
||||
export const RQM_DEFAULT_PREFETCH_COUNT = 0;
|
||||
export const RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT = false;
|
||||
export const RQM_DEFAULT_QUEUE_OPTIONS = {};
|
||||
export const RQM_DEFAULT_NOACK = true;
|
||||
export const RQM_DEFAULT_PERSISTENT = false;
|
||||
export const RQM_DEFAULT_NO_ASSERT = false;
|
||||
export const RQM_NO_EVENT_HANDLER = (
|
||||
text: TemplateStringsArray,
|
||||
pattern: string,
|
||||
@@ -55,19 +51,6 @@ export const GRPC_DEFAULT_PROTO_LOADER = '@grpc/proto-loader';
|
||||
export const NO_EVENT_HANDLER = (text: TemplateStringsArray, pattern: string) =>
|
||||
`There is no matching event handler defined in the remote service. Event pattern: ${pattern}`;
|
||||
export const NO_MESSAGE_HANDLER = `There is no matching message handler defined in the remote service.`;
|
||||
|
||||
export const DISCONNECTED_RMQ_MESSAGE = `Disconnected from RMQ. Trying to reconnect.`;
|
||||
|
||||
export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer';
|
||||
export const KAFKA_DEFAULT_GROUP = 'nestjs-group';
|
||||
|
||||
export const MQTT_SEPARATOR = '/';
|
||||
export const MQTT_WILDCARD_SINGLE = '+';
|
||||
export const MQTT_WILDCARD_ALL = '#';
|
||||
|
||||
export const ECONNREFUSED = 'ECONNREFUSED';
|
||||
export const CONN_ERR = 'CONN_ERR';
|
||||
export const EADDRINUSE = 'EADDRINUSE';
|
||||
|
||||
export const CONNECTION_FAILED_MESSAGE =
|
||||
'Connection to transport failed. Trying to reconnect...';
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { ClientProxy } from './client/client-proxy';
|
||||
import { Closeable } from './interfaces/closeable.interface';
|
||||
|
||||
export type CloseableClient = Closeable & ClientProxy;
|
||||
|
||||
export class ClientsContainer {
|
||||
private clients: CloseableClient[] = [];
|
||||
private clients: ClientProxy[] = [];
|
||||
|
||||
public getAllClients(): CloseableClient[] {
|
||||
public getAllClients(): ClientProxy[] {
|
||||
return this.clients;
|
||||
}
|
||||
|
||||
public addClient(client: CloseableClient) {
|
||||
public addClient(client: ClientProxy) {
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
||||
|
||||
6
packages/microservices/events/index.ts
Normal file
6
packages/microservices/events/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { KafkaStatus } from './kafka.events';
|
||||
export { MqttEvents, MqttStatus } from './mqtt.events';
|
||||
export { NatsEvents, NatsStatus } from './nats.events';
|
||||
export { RedisEvents, RedisStatus } from './redis.events';
|
||||
export { RmqEvents, RmqStatus } from './rmq.events';
|
||||
export { TcpEvents, TcpStatus } from './tcp.events';
|
||||
7
packages/microservices/events/kafka.events.ts
Normal file
7
packages/microservices/events/kafka.events.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const enum KafkaStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTED = 'connected',
|
||||
CRASHED = 'crashed',
|
||||
STOPPED = 'stopped',
|
||||
REBALANCING = 'rebalancing',
|
||||
}
|
||||
39
packages/microservices/events/mqtt.events.ts
Normal file
39
packages/microservices/events/mqtt.events.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
type VoidCallback = () => void;
|
||||
type OnPacketCallback = (packet: any) => void;
|
||||
type OnErrorCallback = (error: Error) => void;
|
||||
|
||||
export const enum MqttStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
CONNECTED = 'connected',
|
||||
CLOSED = 'closed',
|
||||
}
|
||||
|
||||
export const enum MqttEventsMap {
|
||||
CONNECT = 'connect',
|
||||
RECONNECT = 'reconnect',
|
||||
DISCONNECT = 'disconnect',
|
||||
CLOSE = 'close',
|
||||
OFFLINE = 'offline',
|
||||
END = 'end',
|
||||
ERROR = 'error',
|
||||
PACKETRECEIVE = 'packetreceive',
|
||||
PACKETSEND = 'packetsend',
|
||||
}
|
||||
|
||||
/**
|
||||
* MQTT events map for the MQTT client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type MqttEvents = {
|
||||
connect: OnPacketCallback;
|
||||
reconnect: VoidCallback;
|
||||
disconnect: OnPacketCallback;
|
||||
close: VoidCallback;
|
||||
offline: VoidCallback;
|
||||
end: VoidCallback;
|
||||
error: OnErrorCallback;
|
||||
packetreceive: OnPacketCallback;
|
||||
packetsend: OnPacketCallback;
|
||||
};
|
||||
29
packages/microservices/events/nats.events.ts
Normal file
29
packages/microservices/events/nats.events.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
type DefaultCallback = (data?: string | number) => any;
|
||||
|
||||
export type ServersChangedEvent = {
|
||||
added: string[];
|
||||
deleted: string[];
|
||||
};
|
||||
|
||||
export const enum NatsStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum NatsEventsMap {
|
||||
DISCONNECT = 'disconnect',
|
||||
RECONNECT = 'reconnect',
|
||||
UPDATE = 'update',
|
||||
}
|
||||
|
||||
/**
|
||||
* Nats events map for the Nats client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type NatsEvents = {
|
||||
disconnect: DefaultCallback;
|
||||
reconnect: DefaultCallback;
|
||||
update: (data?: string | number | ServersChangedEvent) => any;
|
||||
};
|
||||
34
packages/microservices/events/redis.events.ts
Normal file
34
packages/microservices/events/redis.events.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
type VoidCallback = (client: 'pub' | 'sub') => void;
|
||||
type OnErrorCallback = (client: 'pub' | 'sub', error: Error) => void;
|
||||
type OnWarningCallback = (client: 'pub' | 'sub', warning: any) => void;
|
||||
|
||||
export const enum RedisStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum RedisEventsMap {
|
||||
CONNECT = 'connect',
|
||||
READY = 'ready',
|
||||
ERROR = 'error',
|
||||
CLOSE = 'close',
|
||||
RECONNECTING = 'reconnecting',
|
||||
END = 'end',
|
||||
WARNING = 'warning',
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis events map for the Redis client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type RedisEvents = {
|
||||
connect: VoidCallback;
|
||||
ready: VoidCallback;
|
||||
error: OnErrorCallback;
|
||||
close: VoidCallback;
|
||||
reconnecting: VoidCallback;
|
||||
end: VoidCallback;
|
||||
warning: OnWarningCallback;
|
||||
};
|
||||
24
packages/microservices/events/rmq.events.ts
Normal file
24
packages/microservices/events/rmq.events.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
type VoidCallback = () => void;
|
||||
type OnErrorCallback = (error: Error) => void;
|
||||
|
||||
export const enum RmqStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum RmqEventsMap {
|
||||
ERROR = 'error',
|
||||
DISCONNECT = 'disconnect',
|
||||
CONNECT = 'connect',
|
||||
}
|
||||
|
||||
/**
|
||||
* RabbitMQ events map for the ampqlip client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type RmqEvents = {
|
||||
error: OnErrorCallback;
|
||||
disconnect: VoidCallback;
|
||||
connect: VoidCallback;
|
||||
};
|
||||
39
packages/microservices/events/tcp.events.ts
Normal file
39
packages/microservices/events/tcp.events.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
type VoidCallback = () => void;
|
||||
type OnErrorCallback = (error: Error) => void;
|
||||
type OnLookupCallback = (
|
||||
err: Error,
|
||||
address: string,
|
||||
family: string,
|
||||
host: string,
|
||||
) => void;
|
||||
|
||||
export const enum TcpStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum TcpEventsMap {
|
||||
ERROR = 'error',
|
||||
CONNECT = 'connect',
|
||||
END = 'end',
|
||||
CLOSE = 'close',
|
||||
TIMEOUT = 'timeout',
|
||||
DRAIN = 'drain',
|
||||
LOOKUP = 'lookup',
|
||||
LISTENING = 'listening',
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP events map for the net TCP socket.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type TcpEvents = {
|
||||
error: OnErrorCallback;
|
||||
connect: VoidCallback;
|
||||
end: VoidCallback;
|
||||
close: VoidCallback;
|
||||
timeout: VoidCallback;
|
||||
drain: VoidCallback;
|
||||
lookup: OnLookupCallback;
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* @see https://github.com/mqttjs/MQTT.js/
|
||||
*
|
||||
* @publicApi
|
||||
*
|
||||
*/
|
||||
export declare class MqttClient extends EventEmitter {
|
||||
public connected: boolean;
|
||||
public disconnecting: boolean;
|
||||
public disconnected: boolean;
|
||||
public reconnecting: boolean;
|
||||
public incomingStore: any;
|
||||
public outgoingStore: any;
|
||||
public options: any;
|
||||
public queueQoSZero: boolean;
|
||||
|
||||
constructor(streamBuilder: (client: MqttClient) => any, options: any);
|
||||
|
||||
public on(event: 'message', cb: any): this;
|
||||
public on(event: 'packetsend' | 'packetreceive', cb: any): this;
|
||||
public on(event: 'error', cb: any): this;
|
||||
public on(event: string, cb: Function): this;
|
||||
|
||||
public once(event: 'message', cb: any): this;
|
||||
public once(event: 'packetsend' | 'packetreceive', cb: any): this;
|
||||
public once(event: 'error', cb: any): this;
|
||||
public once(event: string, cb: Function): this;
|
||||
|
||||
/**
|
||||
* publish - publish <message> to <topic>
|
||||
*
|
||||
* @param {String} topic - topic to publish to
|
||||
* @param {(String|Buffer)} message - message to publish
|
||||
*
|
||||
* @param {Object} [opts] - publish options, includes:
|
||||
* @param {Number} [opts.qos] - qos level to publish on
|
||||
* @param {Boolean} [opts.retain] - whether or not to retain the message
|
||||
*
|
||||
* @param {Function} [callback] - function(err){}
|
||||
* called when publish succeeds or fails
|
||||
* @returns {Client} this - for chaining
|
||||
* @api public
|
||||
*
|
||||
* @example client.publish('topic', 'message')
|
||||
* @example
|
||||
* client.publish('topic', 'message', {qos: 1, retain: true})
|
||||
* @example client.publish('topic', 'message', console.log)
|
||||
*/
|
||||
public publish(
|
||||
topic: string,
|
||||
message: string | Buffer,
|
||||
opts: any,
|
||||
callback?: any,
|
||||
): this;
|
||||
public publish(topic: string, message: string | Buffer, callback?: any): this;
|
||||
|
||||
/**
|
||||
* subscribe - subscribe to <topic>
|
||||
*
|
||||
* @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos}
|
||||
* @param {Object} [opts] - optional subscription options, includes:
|
||||
* @param {Number} [opts.qos] - subscribe qos level
|
||||
* @param {Function} [callback] - function(err, granted){} where:
|
||||
* {Error} err - subscription error (none at the moment!)
|
||||
* {Array} granted - array of {topic: 't', qos: 0}
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @api public
|
||||
* @example client.subscribe('topic')
|
||||
* @example client.subscribe('topic', {qos: 1})
|
||||
* @example client.subscribe({'topic': 0, 'topic2': 1}, console.log)
|
||||
* @example client.subscribe('topic', console.log)
|
||||
*/
|
||||
public subscribe(topic: string | string[], opts: any, callback?: any): this;
|
||||
public subscribe(topic: string | string[] | any, callback?: any): this;
|
||||
|
||||
/**
|
||||
* unsubscribe - unsubscribe from topic(s)
|
||||
*
|
||||
* @param {string|Array} topic - topics to unsubscribe from
|
||||
* @param {Function} [callback] - callback fired on unsuback
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @api public
|
||||
* @example client.unsubscribe('topic')
|
||||
* @example client.unsubscribe('topic', console.log)
|
||||
*/
|
||||
public unsubscribe(topic: string | string[], callback?: any): this;
|
||||
|
||||
/**
|
||||
* end - close connection
|
||||
*
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @param {Boolean} force - do not wait for all in-flight messages to be acked
|
||||
* @param {Function} cb - called when the client has been closed
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
public end(force?: boolean, cb?: any): this;
|
||||
|
||||
/**
|
||||
* removeOutgoingMessage - remove a message in outgoing store
|
||||
* the outgoing callback will be called withe Error('Message removed') if the message is removed
|
||||
*
|
||||
* @param {Number} mid - messageId to remove message
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @api public
|
||||
*
|
||||
* @example client.removeOutgoingMessage(client.getLastMessageId());
|
||||
*/
|
||||
public removeOutgoingMessage(mid: number): this;
|
||||
|
||||
/**
|
||||
* reconnect - connect again using the same options as connect()
|
||||
*
|
||||
* @param {Object} [opts] - optional reconnect options, includes:
|
||||
* {any} incomingStore - a store for the incoming packets
|
||||
* {any} outgoingStore - a store for the outgoing packets
|
||||
* if opts is not given, current stores are used
|
||||
*
|
||||
* @returns {MqttClient} this - for chaining
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
public reconnect(opts?: any): this;
|
||||
|
||||
/**
|
||||
* Handle messages with backpressure support, one at a time.
|
||||
* Override at will.
|
||||
*
|
||||
* @param packet packet the packet
|
||||
* @param callback callback call when finished
|
||||
* @api public
|
||||
*/
|
||||
public handleMessage(packet: any, callback: any): void;
|
||||
|
||||
/**
|
||||
* getLastMessageId
|
||||
*/
|
||||
public getLastMessageId(): number;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* @see https://github.com/nats-io/nats.js
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface NatsCodec<T> {
|
||||
encode(d: T): Uint8Array;
|
||||
decode(a: Uint8Array): T;
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
timeout: number;
|
||||
headers?: any;
|
||||
noMux?: boolean;
|
||||
reply?: string;
|
||||
}
|
||||
interface PublishOptions {
|
||||
reply?: string;
|
||||
headers?: any;
|
||||
}
|
||||
interface SubOpts<T> {
|
||||
queue?: string;
|
||||
max?: number;
|
||||
timeout?: number;
|
||||
callback?: (err: object | null, msg: T) => void;
|
||||
}
|
||||
|
||||
declare type SubscriptionOptions = SubOpts<NatsMsg>;
|
||||
|
||||
export interface NatsMsg {
|
||||
subject: string;
|
||||
sid: number;
|
||||
reply?: string;
|
||||
data: Uint8Array;
|
||||
headers?: any;
|
||||
respond(data?: Uint8Array, opts?: PublishOptions): boolean;
|
||||
}
|
||||
|
||||
interface Sub<T> extends AsyncIterable<T> {
|
||||
unsubscribe(max?: number): void;
|
||||
drain(): Promise<void>;
|
||||
isDraining(): boolean;
|
||||
isClosed(): boolean;
|
||||
callback(err: object | null, msg: NatsMsg): void;
|
||||
getSubject(): string;
|
||||
getReceived(): number;
|
||||
getProcessed(): number;
|
||||
getPending(): number;
|
||||
getID(): number;
|
||||
getMax(): number | undefined;
|
||||
}
|
||||
|
||||
declare type Subscription = Sub<NatsMsg>;
|
||||
|
||||
declare enum Events {
|
||||
Disconnect = 'disconnect',
|
||||
Reconnect = 'reconnect',
|
||||
Update = 'update',
|
||||
LDM = 'ldm',
|
||||
Error = 'error',
|
||||
}
|
||||
interface Status {
|
||||
type: Events | DebugEvents;
|
||||
data: string | number;
|
||||
}
|
||||
|
||||
declare enum DebugEvents {
|
||||
Reconnecting = 'reconnecting',
|
||||
PingTimer = 'pingTimer',
|
||||
StaleConnection = 'staleConnection',
|
||||
}
|
||||
|
||||
export declare class Client {
|
||||
info?: Record<string, any>;
|
||||
closed(): Promise<void | Error>;
|
||||
close(): Promise<void>;
|
||||
publish(subject: string, data?: Uint8Array, options?: PublishOptions): void;
|
||||
subscribe(subject: string, opts?: SubscriptionOptions): Subscription;
|
||||
request(
|
||||
subject: string,
|
||||
data?: Uint8Array,
|
||||
opts?: RequestOptions,
|
||||
): Promise<NatsMsg>;
|
||||
flush(): Promise<void>;
|
||||
drain(): Promise<void>;
|
||||
isClosed(): boolean;
|
||||
isDraining(): boolean;
|
||||
getServer(): string;
|
||||
status(): AsyncIterable<Status>;
|
||||
stats(): Record<string, any>;
|
||||
jetstreamManager(opts?: Record<string, any>): Promise<any>;
|
||||
jetstream(opts?: Record<string, any>): any;
|
||||
}
|
||||
9
packages/microservices/external/nats-codec.interface.ts
vendored
Normal file
9
packages/microservices/external/nats-codec.interface.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @see https://github.com/nats-io/nats.js
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface NatsCodec<T> {
|
||||
encode(d: T): Uint8Array;
|
||||
decode(a: Uint8Array): T;
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import { Socket } from 'net';
|
||||
import {
|
||||
CLOSE_EVENT,
|
||||
CONNECT_EVENT,
|
||||
DATA_EVENT,
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
} from '../constants';
|
||||
import { NetSocketClosedException } from '../errors/net-socket-closed.exception';
|
||||
import { InvalidJSONFormatException } from '../errors/invalid-json-format.exception';
|
||||
import { NetSocketClosedException } from '../errors/net-socket-closed.exception';
|
||||
import { TcpEventsMap } from '../events/tcp.events';
|
||||
|
||||
export abstract class TcpSocket {
|
||||
private isClosed = false;
|
||||
@@ -18,10 +12,10 @@ export abstract class TcpSocket {
|
||||
}
|
||||
|
||||
constructor(public readonly socket: Socket) {
|
||||
this.socket.on(DATA_EVENT, this.onData.bind(this));
|
||||
this.socket.on(CONNECT_EVENT, () => (this.isClosed = false));
|
||||
this.socket.on(CLOSE_EVENT, () => (this.isClosed = true));
|
||||
this.socket.on(ERROR_EVENT, () => (this.isClosed = true));
|
||||
this.socket.on('data', this.onData.bind(this));
|
||||
this.socket.on(TcpEventsMap.CONNECT, () => (this.isClosed = false));
|
||||
this.socket.on(TcpEventsMap.CLOSE, () => (this.isClosed = true));
|
||||
this.socket.on(TcpEventsMap.ERROR, () => (this.isClosed = true));
|
||||
}
|
||||
|
||||
public connect(port: number, host: string) {
|
||||
@@ -52,18 +46,21 @@ export abstract class TcpSocket {
|
||||
this.handleSend(message, callback);
|
||||
}
|
||||
|
||||
protected abstract handleSend(message: any, callback?: (err?: any) => void);
|
||||
protected abstract handleSend(
|
||||
message: any,
|
||||
callback?: (err?: any) => void,
|
||||
): any;
|
||||
|
||||
private onData(data: Buffer) {
|
||||
try {
|
||||
this.handleData(data);
|
||||
} catch (e) {
|
||||
this.socket.emit(ERROR_EVENT, e.message);
|
||||
this.socket.emit(TcpEventsMap.ERROR, e.message);
|
||||
this.socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract handleData(data: Buffer | string);
|
||||
protected abstract handleData(data: Buffer | string): any;
|
||||
|
||||
protected emitMessage(data: string) {
|
||||
let message: Record<string, unknown>;
|
||||
@@ -73,6 +70,6 @@ export abstract class TcpSocket {
|
||||
throw new InvalidJSONFormatException(e, data);
|
||||
}
|
||||
message = message || {};
|
||||
this.socket.emit(MESSAGE_EVENT, message);
|
||||
this.socket.emit('message', message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './client';
|
||||
export * from './ctx-host';
|
||||
export * from './decorators';
|
||||
export * from './enums';
|
||||
export * from './events';
|
||||
export * from './exceptions';
|
||||
export * from './helpers';
|
||||
export * from './interfaces';
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ClientGrpc {
|
||||
/**
|
||||
* Returns an instance of the given gRPC service.
|
||||
* @param name Service name
|
||||
* @returns gRPC service
|
||||
*/
|
||||
getService<T extends {}>(name: string): T;
|
||||
/**
|
||||
* Returns an instance of the given gRPC client.
|
||||
* @param name Service name
|
||||
* @returns gRPC client
|
||||
*/
|
||||
getClientByServiceName<T = any>(name: string): T;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ClientProxy } from '../client';
|
||||
import { KafkaStatus } from '../events';
|
||||
import {
|
||||
Consumer,
|
||||
Producer,
|
||||
TopicPartitionOffsetAndMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
|
||||
export interface ClientKafkaProxy
|
||||
extends Omit<ClientProxy<never, KafkaStatus>, 'on'> {
|
||||
/**
|
||||
* Reference to the Kafka consumer instance.
|
||||
*/
|
||||
consumer: Consumer | null;
|
||||
/**
|
||||
* Reference to the Kafka producer instance.
|
||||
*/
|
||||
producer: Producer | null;
|
||||
/**
|
||||
* Subscribes to messages that match the pattern.
|
||||
* Required for message-driven communication style between microservices.
|
||||
* You can't use `send` without subscribing to the message pattern first.
|
||||
* @param pattern Pattern to subscribe to
|
||||
*/
|
||||
subscribeToResponseOf(pattern: unknown): void;
|
||||
/**
|
||||
* Commits the given offsets.
|
||||
* @param topicPartitions Array of topic partitions with their offsets and metadata
|
||||
*/
|
||||
commitOffsets(
|
||||
topicPartitions: TopicPartitionOffsetAndMetadata[],
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface Closeable {
|
||||
close(): void;
|
||||
}
|
||||
@@ -4,7 +4,17 @@ import { Transport } from '../enums';
|
||||
* @publicApi
|
||||
*/
|
||||
export interface CustomTransportStrategy {
|
||||
/**
|
||||
* Unique transport identifier.
|
||||
*/
|
||||
readonly transportId?: Transport | symbol;
|
||||
/**
|
||||
* Method called when the transport is being initialized.
|
||||
* @param callback Function to be called upon initialization
|
||||
*/
|
||||
listen(callback: (...optionalParams: unknown[]) => any): any;
|
||||
/**
|
||||
* Method called when the transport is being terminated.
|
||||
*/
|
||||
close(): any;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './client-grpc.interface';
|
||||
export * from './client-kafka-proxy.interface';
|
||||
export * from './client-metadata.interface';
|
||||
export * from './closeable.interface';
|
||||
export * from './custom-transport-strategy.interface';
|
||||
export * from './deserializer.interface';
|
||||
export * from './message-handler.interface';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Type } from '@nestjs/common';
|
||||
import { ConnectionOptions } from 'tls';
|
||||
import { TlsOptions } from 'tls';
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import { ChannelOptions } from '../external/grpc-options.interface';
|
||||
import {
|
||||
@@ -94,7 +94,7 @@ export interface TcpOptions {
|
||||
retryAttempts?: number;
|
||||
retryDelay?: number;
|
||||
serializer?: Serializer;
|
||||
tlsOptions?: ConnectionOptions;
|
||||
tlsOptions?: TlsOptions;
|
||||
deserializer?: Deserializer;
|
||||
socketClass?: Type<TcpSocket>;
|
||||
};
|
||||
|
||||
@@ -34,12 +34,7 @@ import {
|
||||
} from './context/rpc-metadata-constants';
|
||||
import { BaseRpcContext } from './ctx-host/base-rpc.context';
|
||||
import { Transport } from './enums';
|
||||
import {
|
||||
CustomTransportStrategy,
|
||||
MessageHandler,
|
||||
PatternMetadata,
|
||||
RequestContext,
|
||||
} from './interfaces';
|
||||
import { MessageHandler, PatternMetadata, RequestContext } from './interfaces';
|
||||
import { MicroserviceEntrypointMetadata } from './interfaces/microservice-entrypoint-metadata.interface';
|
||||
import {
|
||||
EventOrMessageListenerDefinition,
|
||||
@@ -66,7 +61,7 @@ export class ListenersController {
|
||||
|
||||
public registerPatternHandlers(
|
||||
instanceWrapper: InstanceWrapper<Controller | Injectable>,
|
||||
server: Server & CustomTransportStrategy,
|
||||
serverInstance: Server,
|
||||
moduleKey: string,
|
||||
) {
|
||||
const { instance } = instanceWrapper;
|
||||
@@ -75,7 +70,7 @@ export class ListenersController {
|
||||
const patternHandlers = this.metadataExplorer.explore(instance as object);
|
||||
const moduleRef = this.container.getModuleByKey(moduleKey);
|
||||
const defaultCallMetadata =
|
||||
server instanceof ServerGrpc
|
||||
serverInstance instanceof ServerGrpc
|
||||
? DEFAULT_GRPC_CALLBACK_METADATA
|
||||
: DEFAULT_CALLBACK_METADATA;
|
||||
|
||||
@@ -83,8 +78,8 @@ export class ListenersController {
|
||||
.filter(
|
||||
({ transport }) =>
|
||||
isUndefined(transport) ||
|
||||
isUndefined(server.transportId) ||
|
||||
transport === server.transportId,
|
||||
isUndefined(serverInstance.transportId) ||
|
||||
transport === serverInstance.transportId,
|
||||
)
|
||||
.reduce((acc, handler) => {
|
||||
handler.patterns.forEach(pattern =>
|
||||
@@ -104,7 +99,7 @@ export class ListenersController {
|
||||
this.insertEntrypointDefinition(
|
||||
instanceWrapper,
|
||||
definition,
|
||||
server.transportId,
|
||||
serverInstance.transportId,
|
||||
);
|
||||
|
||||
if (isStatic) {
|
||||
@@ -131,14 +126,19 @@ export class ListenersController {
|
||||
eventHandler,
|
||||
);
|
||||
};
|
||||
return server.addHandler(
|
||||
return serverInstance.addHandler(
|
||||
pattern,
|
||||
eventHandler,
|
||||
isEventHandler,
|
||||
extras,
|
||||
);
|
||||
} else {
|
||||
return server.addHandler(pattern, proxy, isEventHandler, extras);
|
||||
return serverInstance.addHandler(
|
||||
pattern,
|
||||
proxy,
|
||||
isEventHandler,
|
||||
extras,
|
||||
);
|
||||
}
|
||||
}
|
||||
const asyncHandler = this.createRequestScopedHandler(
|
||||
@@ -150,7 +150,12 @@ export class ListenersController {
|
||||
defaultCallMetadata,
|
||||
isEventHandler,
|
||||
);
|
||||
server.addHandler(pattern, asyncHandler, isEventHandler, extras);
|
||||
serverInstance.addHandler(
|
||||
pattern,
|
||||
asyncHandler,
|
||||
isEventHandler,
|
||||
extras,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ClientsContainer } from './container';
|
||||
import { ExceptionFiltersContext } from './context/exception-filters-context';
|
||||
import { RpcContextCreator } from './context/rpc-context-creator';
|
||||
import { RpcProxy } from './context/rpc-proxy';
|
||||
import { CustomTransportStrategy } from './interfaces';
|
||||
import { ListenersController } from './listeners-controller';
|
||||
import { Server } from './server/server';
|
||||
|
||||
@@ -63,16 +62,13 @@ export class MicroservicesModule<
|
||||
);
|
||||
}
|
||||
|
||||
public setupListeners(
|
||||
container: NestContainer,
|
||||
server: Server & CustomTransportStrategy,
|
||||
) {
|
||||
public setupListeners(container: NestContainer, serverInstance: Server) {
|
||||
if (!this.listenersController) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
const modules = container.getModules();
|
||||
modules.forEach(({ controllers }, moduleRef) =>
|
||||
this.bindListeners(controllers, server, moduleRef),
|
||||
this.bindListeners(controllers, serverInstance, moduleRef),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,13 +88,13 @@ export class MicroservicesModule<
|
||||
|
||||
public bindListeners(
|
||||
controllers: Map<string | symbol | Function, InstanceWrapper<Controller>>,
|
||||
server: Server & CustomTransportStrategy,
|
||||
serverInstance: Server,
|
||||
moduleName: string,
|
||||
) {
|
||||
controllers.forEach(wrapper =>
|
||||
this.listenersController.registerPatternHandlers(
|
||||
wrapper,
|
||||
server,
|
||||
serverInstance,
|
||||
moduleName,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Provider,
|
||||
} from '@nestjs/common';
|
||||
import { ClientProxy, ClientProxyFactory } from '../client';
|
||||
import { Closeable } from '../interfaces';
|
||||
import {
|
||||
ClientsModuleAsyncOptions,
|
||||
ClientsModuleOptions,
|
||||
@@ -101,7 +100,7 @@ export class ClientsModule {
|
||||
};
|
||||
}
|
||||
|
||||
private static assignOnAppShutdownHook(client: ClientProxy & Closeable) {
|
||||
private static assignOnAppShutdownHook(client: ClientProxy) {
|
||||
(client as unknown as OnApplicationShutdown).onApplicationShutdown =
|
||||
client.close;
|
||||
return client;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Injector } from '@nestjs/core/injector/injector';
|
||||
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
|
||||
import { NestApplicationContext } from '@nestjs/core/nest-application-context';
|
||||
import { Transport } from './enums/transport.enum';
|
||||
import { CustomTransportStrategy } from './interfaces/custom-transport-strategy.interface';
|
||||
import { MicroserviceOptions } from './interfaces/microservice-configuration.interface';
|
||||
import { MicroservicesModule } from './microservices-module';
|
||||
import { Server } from './server/server';
|
||||
@@ -27,6 +26,9 @@ const { SocketModule } = optionalRequire(
|
||||
() => require('@nestjs/websockets/socket-module'),
|
||||
);
|
||||
|
||||
type CompleteMicroserviceOptions = NestMicroserviceOptions &
|
||||
MicroserviceOptions;
|
||||
|
||||
export class NestMicroservice
|
||||
extends NestApplicationContext<NestMicroserviceOptions>
|
||||
implements INestMicroservice
|
||||
@@ -36,14 +38,21 @@ export class NestMicroservice
|
||||
});
|
||||
private readonly microservicesModule = new MicroservicesModule();
|
||||
private readonly socketModule = SocketModule ? new SocketModule() : null;
|
||||
private microserviceConfig: NestMicroserviceOptions & MicroserviceOptions;
|
||||
private server: Server & CustomTransportStrategy;
|
||||
private microserviceConfig: CompleteMicroserviceOptions;
|
||||
private serverInstance: Server;
|
||||
private isTerminated = false;
|
||||
private isInitHookCalled = false;
|
||||
private wasInitHookCalled = false;
|
||||
|
||||
/**
|
||||
* Returns an observable that emits status changes.
|
||||
*/
|
||||
get status() {
|
||||
return this.serverInstance.status;
|
||||
}
|
||||
|
||||
constructor(
|
||||
container: NestContainer,
|
||||
config: NestMicroserviceOptions & MicroserviceOptions = {},
|
||||
config: CompleteMicroserviceOptions = {},
|
||||
private readonly graphInspector: GraphInspector,
|
||||
private readonly applicationConfig: ApplicationConfig,
|
||||
) {
|
||||
@@ -60,16 +69,21 @@ export class NestMicroservice
|
||||
this.selectContextModule();
|
||||
}
|
||||
|
||||
public createServer(config: NestMicroserviceOptions & MicroserviceOptions) {
|
||||
public createServer(config: CompleteMicroserviceOptions) {
|
||||
try {
|
||||
this.microserviceConfig = {
|
||||
transport: Transport.TCP,
|
||||
...config,
|
||||
} as any;
|
||||
const { strategy } = config as any;
|
||||
this.server = strategy
|
||||
? strategy
|
||||
: ServerFactory.create(this.microserviceConfig);
|
||||
} as CompleteMicroserviceOptions;
|
||||
|
||||
if ('strategy' in config) {
|
||||
this.serverInstance = config.strategy as Server;
|
||||
return;
|
||||
} else {
|
||||
this.serverInstance = ServerFactory.create(
|
||||
this.microserviceConfig,
|
||||
) as Server;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
throw e;
|
||||
@@ -92,21 +106,36 @@ export class NestMicroservice
|
||||
|
||||
this.setIsInitialized(true);
|
||||
|
||||
if (!this.isInitHookCalled) {
|
||||
if (!this.wasInitHookCalled) {
|
||||
await this.callInitHook();
|
||||
await this.callBootstrapHook();
|
||||
}
|
||||
}
|
||||
|
||||
public registerListeners() {
|
||||
this.microservicesModule.setupListeners(this.container, this.server);
|
||||
this.microservicesModule.setupListeners(
|
||||
this.container,
|
||||
this.serverInstance,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a web socket adapter that will be used for Gateways.
|
||||
* Use to override the default `socket.io` library.
|
||||
*
|
||||
* @param {WebSocketAdapter} adapter
|
||||
* @returns {this}
|
||||
*/
|
||||
public useWebSocketAdapter(adapter: WebSocketAdapter): this {
|
||||
this.applicationConfig.setIoAdapter(adapter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers global exception filters (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...ExceptionFilter} filters
|
||||
*/
|
||||
public useGlobalFilters(...filters: ExceptionFilter[]): this {
|
||||
this.applicationConfig.useGlobalFilters(...filters);
|
||||
filters.forEach(item =>
|
||||
@@ -118,6 +147,11 @@ export class NestMicroservice
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers global pipes (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...PipeTransform} pipes
|
||||
*/
|
||||
public useGlobalPipes(...pipes: PipeTransform<any>[]): this {
|
||||
this.applicationConfig.useGlobalPipes(...pipes);
|
||||
pipes.forEach(item =>
|
||||
@@ -129,6 +163,11 @@ export class NestMicroservice
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers global interceptors (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...NestInterceptor} interceptors
|
||||
*/
|
||||
public useGlobalInterceptors(...interceptors: NestInterceptor[]): this {
|
||||
this.applicationConfig.useGlobalInterceptors(...interceptors);
|
||||
interceptors.forEach(item =>
|
||||
@@ -160,12 +199,17 @@ export class NestMicroservice
|
||||
return this;
|
||||
}
|
||||
|
||||
public async listen() {
|
||||
/**
|
||||
* Starts the microservice.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
public async listen(): Promise<any> {
|
||||
this.assertNotInPreviewMode('listen');
|
||||
!this.isInitialized && (await this.registerModules());
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
this.server.listen((err, info) => {
|
||||
this.serverInstance.listen((err, info) => {
|
||||
if (this.microserviceConfig?.autoFlushLogs ?? true) {
|
||||
this.flushLogs();
|
||||
}
|
||||
@@ -178,8 +222,13 @@ export class NestMicroservice
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the application.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async close(): Promise<any> {
|
||||
await this.server.close();
|
||||
await this.serverInstance.close();
|
||||
if (this.isTerminated) {
|
||||
return;
|
||||
}
|
||||
@@ -187,16 +236,51 @@ export class NestMicroservice
|
||||
await this.closeApplication();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag indicating that the application is initialized.
|
||||
* @param isInitialized Value to set
|
||||
*/
|
||||
public setIsInitialized(isInitialized: boolean) {
|
||||
this.isInitialized = isInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag indicating that the application is terminated.
|
||||
* @param isTerminated Value to set
|
||||
*/
|
||||
public setIsTerminated(isTerminated: boolean) {
|
||||
this.isTerminated = isTerminated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag indicating that the init hook was called.
|
||||
* @param isInitHookCalled Value to set
|
||||
*/
|
||||
public setIsInitHookCalled(isInitHookCalled: boolean) {
|
||||
this.isInitHookCalled = isInitHookCalled;
|
||||
this.wasInitHookCalled = isInitHookCalled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event listener for the given event.
|
||||
* @param event Event name
|
||||
* @param callback Callback to be executed when the event is emitted
|
||||
*/
|
||||
public on(event: string | number | symbol, callback: Function) {
|
||||
if ('on' in this.serverInstance) {
|
||||
return this.serverInstance.on(event as string, callback);
|
||||
}
|
||||
throw new Error('"on" method not supported by the underlying server');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of the underlying server/broker instance,
|
||||
* or a group of servers if there are more than one.
|
||||
*/
|
||||
public unwrap<T>(): T {
|
||||
if ('unwrap' in this.serverInstance) {
|
||||
return this.serverInstance.unwrap();
|
||||
}
|
||||
throw new Error('"unwrap" method not supported by the underlying server');
|
||||
}
|
||||
|
||||
protected async closeApplication(): Promise<any> {
|
||||
@@ -211,7 +295,7 @@ export class NestMicroservice
|
||||
if (this.isTerminated) {
|
||||
return;
|
||||
}
|
||||
await this.server.close();
|
||||
await this.serverInstance.close();
|
||||
this.socketModule && (await this.socketModule.close());
|
||||
this.microservicesModule && (await this.microservicesModule.close());
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ export interface MqttRecordOptions {
|
||||
* MQTT 5.0 properties object
|
||||
*/
|
||||
properties?: {
|
||||
payloadFormatIndicator?: number;
|
||||
payloadFormatIndicator?: boolean;
|
||||
messageExpiryInterval?: number;
|
||||
topicAlias?: string;
|
||||
topicAlias?: number;
|
||||
responseTopic?: string;
|
||||
correlationData?: Buffer;
|
||||
userProperties?: Record<string, string | string[]>;
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import { CustomTransportStrategy, MicroserviceOptions } from '../interfaces';
|
||||
import { Server } from './server';
|
||||
import {
|
||||
CustomStrategy,
|
||||
MicroserviceOptions,
|
||||
MqttOptions,
|
||||
} from '../interfaces';
|
||||
import { ServerGrpc } from './server-grpc';
|
||||
import { ServerKafka } from './server-kafka';
|
||||
import { ServerMqtt } from './server-mqtt';
|
||||
import { ServerNats } from './server-nats';
|
||||
import { ServerRedis } from './server-redis';
|
||||
import { ServerTCP } from './server-tcp';
|
||||
import { ServerRMQ } from './server-rmq';
|
||||
import { ServerTCP } from './server-tcp';
|
||||
|
||||
export class ServerFactory {
|
||||
public static create(
|
||||
microserviceOptions: MicroserviceOptions,
|
||||
): Server & CustomTransportStrategy {
|
||||
const { transport, options } = microserviceOptions as any;
|
||||
public static create(microserviceOptions: MicroserviceOptions) {
|
||||
const { transport, options } = microserviceOptions as Exclude<
|
||||
MicroserviceOptions,
|
||||
CustomStrategy
|
||||
>;
|
||||
switch (transport) {
|
||||
case Transport.REDIS:
|
||||
return new ServerRedis(options);
|
||||
return new ServerRedis(options as ServerRedis['options']);
|
||||
case Transport.NATS:
|
||||
return new ServerNats(options);
|
||||
return new ServerNats(options as ServerNats['options']);
|
||||
case Transport.MQTT:
|
||||
return new ServerMqtt(options);
|
||||
return new ServerMqtt(options as MqttOptions['options']);
|
||||
case Transport.GRPC:
|
||||
return new ServerGrpc(options);
|
||||
return new ServerGrpc(options as ServerGrpc['options']);
|
||||
case Transport.KAFKA:
|
||||
return new ServerKafka(options);
|
||||
return new ServerKafka(options as ServerKafka['options']);
|
||||
case Transport.RMQ:
|
||||
return new ServerRMQ(options);
|
||||
return new ServerRMQ(options as ServerRMQ['options']);
|
||||
default:
|
||||
return new ServerTCP(options);
|
||||
return new ServerTCP(options as ServerTCP['options']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,23 +13,30 @@ import {
|
||||
lastValueFrom,
|
||||
} from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
import {
|
||||
CANCEL_EVENT,
|
||||
GRPC_DEFAULT_PROTO_LOADER,
|
||||
GRPC_DEFAULT_URL,
|
||||
} from '../constants';
|
||||
import { GRPC_DEFAULT_PROTO_LOADER, GRPC_DEFAULT_URL } from '../constants';
|
||||
import { GrpcMethodStreamingType } from '../decorators';
|
||||
import { Transport } from '../enums';
|
||||
import { InvalidGrpcPackageException } from '../errors/invalid-grpc-package.exception';
|
||||
import { InvalidProtoDefinitionException } from '../errors/invalid-proto-definition.exception';
|
||||
import { ChannelOptions } from '../external/grpc-options.interface';
|
||||
import { getGrpcPackageDefinition } from '../helpers';
|
||||
import { CustomTransportStrategy, MessageHandler } from '../interfaces';
|
||||
import { MessageHandler } from '../interfaces';
|
||||
import { GrpcOptions } from '../interfaces/microservice-configuration.interface';
|
||||
import { Server } from './server';
|
||||
|
||||
let grpcPackage: any = {};
|
||||
let grpcProtoLoaderPackage: any = {};
|
||||
const CANCELLED_EVENT = 'cancelled';
|
||||
|
||||
// To enable type safety for gRPC. This cant be uncommented by default
|
||||
// because it would require the user to install the @grpc/grpc-js package even if they dont use gRPC
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type GrpcServer = import('@grpc/grpc-js').Server;
|
||||
// let grpcPackage = {} as typeof import('@grpc/grpc-js');
|
||||
// let grpcProtoLoaderPackage = {} as typeof import('@grpc/proto-loader');
|
||||
|
||||
type GrpcServer = any;
|
||||
let grpcPackage = {} as any;
|
||||
let grpcProtoLoaderPackage = {} as any;
|
||||
|
||||
interface GrpcCall<TRequest = any, TMetadata = any> {
|
||||
request: TRequest;
|
||||
@@ -45,11 +52,16 @@ interface GrpcCall<TRequest = any, TMetadata = any> {
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ServerGrpc extends Server implements CustomTransportStrategy {
|
||||
export class ServerGrpc extends Server<never, never> {
|
||||
public readonly transportId = Transport.GRPC;
|
||||
protected readonly url: string;
|
||||
protected grpcClient: GrpcServer;
|
||||
|
||||
private readonly url: string;
|
||||
private grpcClient: any;
|
||||
get status(): never {
|
||||
throw new Error(
|
||||
'The "status" attribute is not supported by the gRPC transport',
|
||||
);
|
||||
}
|
||||
|
||||
constructor(private readonly options: GrpcOptions['options']) {
|
||||
super();
|
||||
@@ -174,7 +186,7 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
|
||||
return service;
|
||||
}
|
||||
|
||||
getMessageHandler(
|
||||
public getMessageHandler(
|
||||
serviceName: string,
|
||||
methodName: string,
|
||||
streaming: GrpcMethodStreamingType,
|
||||
@@ -264,6 +276,17 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
|
||||
};
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
throw new Error('Method is not supported for gRPC transport');
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends string | number | symbol = string | number | symbol,
|
||||
EventCallback = any,
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
throw new Error('Method is not supported in gRPC mode.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an observable to a GRPC call.
|
||||
*
|
||||
@@ -296,8 +319,8 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
|
||||
// Calls that are cancelled by the client should be successfully resolved here
|
||||
resolve();
|
||||
};
|
||||
call.on(CANCEL_EVENT, cancelHandler);
|
||||
subscription.add(() => call.off(CANCEL_EVENT, cancelHandler));
|
||||
call.on(CANCELLED_EVENT, cancelHandler);
|
||||
subscription.add(() => call.off(CANCELLED_EVENT, cancelHandler));
|
||||
|
||||
// In all cases, when we finalize, end the writable stream
|
||||
// being careful that errors and writes must be emitted _before_ this call is ended
|
||||
@@ -402,7 +425,7 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
|
||||
} else {
|
||||
const response = await lastValueFrom(
|
||||
res.pipe(
|
||||
takeUntil(fromEvent(call as any, CANCEL_EVENT)),
|
||||
takeUntil(fromEvent(call as any, CANCELLED_EVENT)),
|
||||
catchError(err => {
|
||||
callback(err, null);
|
||||
return EMPTY;
|
||||
@@ -469,7 +492,7 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
|
||||
this.messageHandlers.set(route, callback);
|
||||
}
|
||||
|
||||
public async createClient(): Promise<any> {
|
||||
public async createClient() {
|
||||
const channelOptions: ChannelOptions =
|
||||
this.options && this.options.channelOptions
|
||||
? this.options.channelOptions
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { KafkaContext } from '../ctx-host';
|
||||
import { KafkaRequestDeserializer } from '../deserializers/kafka-request.deserializer';
|
||||
import { KafkaHeaders, Transport } from '../enums';
|
||||
import { KafkaStatus } from '../events';
|
||||
import { KafkaRetriableException } from '../exceptions';
|
||||
import {
|
||||
BrokersFunction,
|
||||
@@ -25,12 +26,7 @@ import {
|
||||
RecordMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
import { KafkaLogger, KafkaParser } from '../helpers';
|
||||
import {
|
||||
CustomTransportStrategy,
|
||||
KafkaOptions,
|
||||
OutgoingResponse,
|
||||
ReadPacket,
|
||||
} from '../interfaces';
|
||||
import { KafkaOptions, OutgoingResponse, ReadPacket } from '../interfaces';
|
||||
import { KafkaRequestSerializer } from '../serializers/kafka-request.serializer';
|
||||
import { Server } from './server';
|
||||
|
||||
@@ -39,7 +35,7 @@ let kafkaPackage: any = {};
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
export class ServerKafka extends Server<never, KafkaStatus> {
|
||||
public readonly transportId = Transport.KAFKA;
|
||||
|
||||
protected logger = new Logger(ServerKafka.name);
|
||||
@@ -47,7 +43,6 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
protected consumer: Consumer = null;
|
||||
protected producer: Producer = null;
|
||||
protected parser: KafkaParser = null;
|
||||
|
||||
protected brokers: string[] | BrokersFunction;
|
||||
protected clientId: string;
|
||||
protected groupId: string;
|
||||
@@ -64,7 +59,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
|
||||
this.brokers = clientOptions.brokers || [KAFKA_DEFAULT_BROKER];
|
||||
|
||||
// append a unique id to the clientId and groupId
|
||||
// Append a unique id to the clientId and groupId
|
||||
// so they don't collide with a microservices client
|
||||
this.clientId =
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) + postfixId;
|
||||
@@ -105,6 +100,8 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
});
|
||||
this.consumer = this.client.consumer(consumerOptions);
|
||||
this.producer = this.client.producer(this.options.producer);
|
||||
this.registerConsumerEventListeners();
|
||||
this.registerProducerEventListeners();
|
||||
|
||||
await this.consumer.connect();
|
||||
await this.producer.connect();
|
||||
@@ -112,6 +109,33 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
callback();
|
||||
}
|
||||
|
||||
protected registerConsumerEventListeners() {
|
||||
this.consumer.on(this.consumer.events.CONNECT, () =>
|
||||
this._status$.next(KafkaStatus.CONNECTED),
|
||||
);
|
||||
this.consumer.on(this.consumer.events.DISCONNECT, () =>
|
||||
this._status$.next(KafkaStatus.DISCONNECTED),
|
||||
);
|
||||
this.consumer.on(this.consumer.events.REBALANCING, () =>
|
||||
this._status$.next(KafkaStatus.REBALANCING),
|
||||
);
|
||||
this.consumer.on(this.consumer.events.STOP, () =>
|
||||
this._status$.next(KafkaStatus.STOPPED),
|
||||
);
|
||||
this.consumer.on(this.consumer.events.CRASH, () =>
|
||||
this._status$.next(KafkaStatus.CRASHED),
|
||||
);
|
||||
}
|
||||
|
||||
protected registerProducerEventListeners() {
|
||||
this.producer.on(this.producer.events.CONNECT, () =>
|
||||
this._status$.next(KafkaStatus.CONNECTED),
|
||||
);
|
||||
this.producer.on(this.producer.events.DISCONNECT, () =>
|
||||
this._status$.next(KafkaStatus.DISCONNECTED),
|
||||
);
|
||||
}
|
||||
|
||||
public createClient<T = any>(): T {
|
||||
return new kafkaPackage.Kafka(
|
||||
Object.assign(
|
||||
@@ -204,6 +228,22 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
this.send(replayStream$, publish);
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.client) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.',
|
||||
);
|
||||
}
|
||||
return this.client as T;
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends string | number | symbol = string | number | symbol,
|
||||
EventCallback = any,
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
throw new Error('Method is not supported for Kafka server');
|
||||
}
|
||||
|
||||
private combineStreamsAndThrowIfRetriable(
|
||||
response$: Observable<any>,
|
||||
replayStream$: ReplaySubject<unknown>,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
CONNECT_EVENT,
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
MQTT_DEFAULT_URL,
|
||||
MQTT_SEPARATOR,
|
||||
MQTT_WILDCARD_ALL,
|
||||
@@ -11,9 +8,8 @@ import {
|
||||
} from '../constants';
|
||||
import { MqttContext } from '../ctx-host/mqtt.context';
|
||||
import { Transport } from '../enums';
|
||||
import { MqttClient } from '../external/mqtt-client.interface';
|
||||
import { MqttEvents, MqttEventsMap, MqttStatus } from '../events/mqtt.events';
|
||||
import {
|
||||
CustomTransportStrategy,
|
||||
IncomingRequest,
|
||||
MessageHandler,
|
||||
PacketId,
|
||||
@@ -26,15 +22,24 @@ import { Server } from './server';
|
||||
|
||||
let mqttPackage: any = {};
|
||||
|
||||
// To enable type safety for MQTT. This cant be uncommented by default
|
||||
// because it would require the user to install the mqtt package even if they dont use MQTT
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type MqttClient = import('mqtt').MqttClient;
|
||||
type MqttClient = any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ServerMqtt extends Server implements CustomTransportStrategy {
|
||||
export class ServerMqtt extends Server<MqttEvents, MqttStatus> {
|
||||
public readonly transportId = Transport.MQTT;
|
||||
|
||||
protected readonly url: string;
|
||||
protected mqttClient: MqttClient;
|
||||
|
||||
private readonly url: string;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof MqttEvents;
|
||||
callback: MqttEvents[keyof MqttEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(private readonly options: MqttOptions['options']) {
|
||||
super();
|
||||
@@ -62,14 +67,23 @@ export class ServerMqtt extends Server implements CustomTransportStrategy {
|
||||
public start(
|
||||
callback: (err?: unknown, ...optionalParams: unknown[]) => void,
|
||||
) {
|
||||
this.handleError(this.mqttClient);
|
||||
this.registerErrorListener(this.mqttClient);
|
||||
this.registerReconnectListener(this.mqttClient);
|
||||
this.registerDisconnectListener(this.mqttClient);
|
||||
this.registerCloseListener(this.mqttClient);
|
||||
this.registerConnectListener(this.mqttClient);
|
||||
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.mqttClient.on(event, callback),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
this.bindEvents(this.mqttClient);
|
||||
|
||||
this.mqttClient.on(CONNECT_EVENT, () => callback());
|
||||
this.mqttClient.on(MqttEventsMap.CONNECT, () => callback());
|
||||
}
|
||||
|
||||
public bindEvents(mqttClient: MqttClient) {
|
||||
mqttClient.on(MESSAGE_EVENT, this.getMessageHandler(mqttClient).bind(this));
|
||||
mqttClient.on('message', this.getMessageHandler(mqttClient).bind(this));
|
||||
|
||||
const registeredPatterns = [...this.messageHandlers.keys()];
|
||||
registeredPatterns.forEach(pattern => {
|
||||
@@ -83,6 +97,7 @@ export class ServerMqtt extends Server implements CustomTransportStrategy {
|
||||
|
||||
public close() {
|
||||
this.mqttClient && this.mqttClient.end();
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public createMqttClient(): MqttClient {
|
||||
@@ -216,8 +231,54 @@ export class ServerMqtt extends Server implements CustomTransportStrategy {
|
||||
return `${pattern}/reply`;
|
||||
}
|
||||
|
||||
public handleError(stream: any) {
|
||||
stream.on(ERROR_EVENT, (err: any) => this.logger.error(err));
|
||||
public registerErrorListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.ERROR, (err: unknown) => this.logger.error(err));
|
||||
}
|
||||
|
||||
public registerReconnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.RECONNECT, () => {
|
||||
this._status$.next(MqttStatus.RECONNECTING);
|
||||
|
||||
this.logger.log('MQTT connection lost. Trying to reconnect...');
|
||||
});
|
||||
}
|
||||
|
||||
public registerDisconnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.DISCONNECT, () => {
|
||||
this._status$.next(MqttStatus.DISCONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerCloseListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.CLOSE, () => {
|
||||
this._status$.next(MqttStatus.CLOSED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerConnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.CONNECT, () => {
|
||||
this._status$.next(MqttStatus.CONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.mqttClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.',
|
||||
);
|
||||
}
|
||||
return this.mqttClient as T;
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof MqttEvents = keyof MqttEvents,
|
||||
EventCallback extends MqttEvents[EventKey] = MqttEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.mqttClient) {
|
||||
this.mqttClient.on(event, callback as any);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
protected initializeSerializer(options: MqttOptions['options']) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isUndefined, isObject } from '@nestjs/common/utils/shared.utils';
|
||||
import { isObject, isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import { EventEmitter } from 'events';
|
||||
import { NATS_DEFAULT_URL, NO_MESSAGE_HANDLER } from '../constants';
|
||||
import { NatsContext } from '../ctx-host/nats.context';
|
||||
import { NatsRequestJSONDeserializer } from '../deserializers/nats-request-json.deserializer';
|
||||
import { Transport } from '../enums';
|
||||
import { Client, NatsMsg } from '../external/nats-client.interface';
|
||||
import { CustomTransportStrategy } from '../interfaces';
|
||||
import { NatsEvents, NatsEventsMap, NatsStatus } from '../events/nats.events';
|
||||
import { NatsOptions } from '../interfaces/microservice-configuration.interface';
|
||||
import { IncomingRequest } from '../interfaces/packet.interface';
|
||||
import { NatsRecord } from '../record-builders';
|
||||
@@ -13,13 +13,26 @@ import { Server } from './server';
|
||||
|
||||
let natsPackage = {} as any;
|
||||
|
||||
// To enable type safety for Nats. This cant be uncommented by default
|
||||
// because it would require the user to install the nats package even if they dont use Nats
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type Client = import('nats').NatsConnection;
|
||||
// type NatsMsg = import('nats').Msg;
|
||||
|
||||
type Client = any;
|
||||
type NatsMsg = any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ServerNats extends Server implements CustomTransportStrategy {
|
||||
export class ServerNats extends Server<NatsEvents, NatsStatus> {
|
||||
public readonly transportId = Transport.NATS;
|
||||
|
||||
private natsClient: Client;
|
||||
protected statusEventEmitter = new EventEmitter<{
|
||||
[key in keyof NatsEvents]: Parameters<NatsEvents[key]>;
|
||||
}>();
|
||||
|
||||
constructor(private readonly options: NatsOptions['options']) {
|
||||
super();
|
||||
@@ -37,6 +50,8 @@ export class ServerNats extends Server implements CustomTransportStrategy {
|
||||
) {
|
||||
try {
|
||||
this.natsClient = await this.createNatsClient();
|
||||
|
||||
this._status$.next(NatsStatus.CONNECTED);
|
||||
this.handleStatusUpdates(this.natsClient);
|
||||
this.start(callback);
|
||||
} catch (err) {
|
||||
@@ -65,6 +80,8 @@ export class ServerNats extends Server implements CustomTransportStrategy {
|
||||
|
||||
public async close() {
|
||||
await this.natsClient?.close();
|
||||
this.statusEventEmitter.removeAllListeners();
|
||||
|
||||
this.natsClient = null;
|
||||
}
|
||||
|
||||
@@ -142,10 +159,21 @@ export class ServerNats extends Server implements CustomTransportStrategy {
|
||||
|
||||
switch (status.type) {
|
||||
case 'error':
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
|
||||
this._status$.next(NatsStatus.DISCONNECTED);
|
||||
this.statusEventEmitter.emit(
|
||||
NatsEventsMap.DISCONNECT,
|
||||
status.data as string,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'pingTimer':
|
||||
@@ -156,6 +184,29 @@ export class ServerNats extends Server implements CustomTransportStrategy {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'reconnecting':
|
||||
this._status$.next(NatsStatus.RECONNECTING);
|
||||
break;
|
||||
|
||||
case 'reconnect':
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
|
||||
this._status$.next(NatsStatus.CONNECTED);
|
||||
this.statusEventEmitter.emit(
|
||||
NatsEventsMap.RECONNECT,
|
||||
status.data as string,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
this.statusEventEmitter.emit(NatsEventsMap.UPDATE, status.data);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
@@ -165,6 +216,22 @@ export class ServerNats extends Server implements CustomTransportStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.natsClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.',
|
||||
);
|
||||
}
|
||||
return this.natsClient as T;
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof NatsEvents = keyof NatsEvents,
|
||||
EventCallback extends NatsEvents[EventKey] = NatsEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
this.statusEventEmitter.on(event, callback as any);
|
||||
}
|
||||
|
||||
protected initializeSerializer(options: NatsOptions['options']) {
|
||||
this.serializer = options?.serializer ?? new NatsRecordSerializer();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
NO_MESSAGE_HANDLER,
|
||||
REDIS_DEFAULT_HOST,
|
||||
REDIS_DEFAULT_PORT,
|
||||
@@ -9,12 +7,18 @@ import {
|
||||
import { RedisContext } from '../ctx-host';
|
||||
import { Transport } from '../enums';
|
||||
import {
|
||||
CustomTransportStrategy,
|
||||
IncomingRequest,
|
||||
RedisOptions,
|
||||
} from '../interfaces';
|
||||
RedisEvents,
|
||||
RedisEventsMap,
|
||||
RedisStatus,
|
||||
} from '../events/redis.events';
|
||||
import { IncomingRequest, RedisOptions } from '../interfaces';
|
||||
import { Server } from './server';
|
||||
|
||||
// To enable type safety for Redis. This cant be uncommented by default
|
||||
// because it would require the user to install the ioredis package even if they dont use Redis
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type Redis = import('ioredis').Redis;
|
||||
type Redis = any;
|
||||
|
||||
let redisPackage = {} as any;
|
||||
@@ -22,14 +26,19 @@ let redisPackage = {} as any;
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ServerRedis extends Server implements CustomTransportStrategy {
|
||||
export class ServerRedis extends Server<RedisEvents, RedisStatus> {
|
||||
public readonly transportId = Transport.REDIS;
|
||||
|
||||
private subClient: Redis;
|
||||
private pubClient: Redis;
|
||||
private isExplicitlyTerminated = false;
|
||||
protected subClient: Redis;
|
||||
protected pubClient: Redis;
|
||||
protected isManuallyClosed = false;
|
||||
protected wasInitialConnectionSuccessful = false;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof RedisEvents;
|
||||
callback: RedisEvents[keyof RedisEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(private readonly options: RedisOptions['options']) {
|
||||
constructor(protected readonly options: RedisOptions['options']) {
|
||||
super();
|
||||
|
||||
redisPackage = this.loadPackage('ioredis', ServerRedis.name, () =>
|
||||
@@ -47,8 +56,17 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
|
||||
this.subClient = this.createRedisClient();
|
||||
this.pubClient = this.createRedisClient();
|
||||
|
||||
this.handleError(this.pubClient);
|
||||
this.handleError(this.subClient);
|
||||
[this.subClient, this.pubClient].forEach((client, index) => {
|
||||
const type = index === 0 ? 'pub' : 'sub';
|
||||
this.registerErrorListener(client);
|
||||
this.registerReconnectListener(client);
|
||||
this.registerReadyListener(client);
|
||||
this.registerEndListener(client);
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
client.on(event, (...args: [any]) => callback(type, ...args)),
|
||||
);
|
||||
});
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
this.start(callback);
|
||||
} catch (err) {
|
||||
@@ -67,7 +85,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
|
||||
|
||||
public bindEvents(subClient: Redis, pubClient: Redis) {
|
||||
subClient.on(
|
||||
this.options?.wildcards ? 'pmessage' : MESSAGE_EVENT,
|
||||
this.options?.wildcards ? 'pmessage' : 'message',
|
||||
this.getMessageHandler(pubClient).bind(this),
|
||||
);
|
||||
const subscribePatterns = [...this.messageHandlers.keys()];
|
||||
@@ -87,9 +105,10 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.isExplicitlyTerminated = true;
|
||||
this.isManuallyClosed = true;
|
||||
this.pubClient && this.pubClient.quit();
|
||||
this.subClient && this.subClient.quit();
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public createRedisClient(): Redis {
|
||||
@@ -172,8 +191,52 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
|
||||
return `${pattern}.reply`;
|
||||
}
|
||||
|
||||
public handleError(stream: any) {
|
||||
stream.on(ERROR_EVENT, (err: any) => this.logger.error(err));
|
||||
public registerErrorListener(client: any) {
|
||||
client.on(RedisEventsMap.ERROR, (err: any) => this.logger.error(err));
|
||||
}
|
||||
|
||||
public registerReconnectListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on(RedisEventsMap.RECONNECTING, () => {
|
||||
if (this.isManuallyClosed) {
|
||||
return;
|
||||
}
|
||||
this._status$.next(RedisStatus.RECONNECTING);
|
||||
|
||||
if (this.wasInitialConnectionSuccessful) {
|
||||
this.logger.log('Reconnecting to Redis...');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerReadyListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on(RedisEventsMap.READY, () => {
|
||||
this._status$.next(RedisStatus.CONNECTED);
|
||||
|
||||
this.logger.log('Connected to Redis. Subscribing to channels...');
|
||||
|
||||
if (!this.wasInitialConnectionSuccessful) {
|
||||
this.wasInitialConnectionSuccessful = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerEndListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on('end', () => {
|
||||
if (this.isManuallyClosed) {
|
||||
return;
|
||||
}
|
||||
this._status$.next(RedisStatus.DISCONNECTED);
|
||||
|
||||
this.logger.error(
|
||||
'Disconnected from Redis. No further reconnection attempts will be made.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getClientOptions(): Partial<RedisOptions['options']> {
|
||||
@@ -186,16 +249,40 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
|
||||
}
|
||||
|
||||
public createRetryStrategy(times: number): undefined | number | void {
|
||||
if (this.isExplicitlyTerminated) {
|
||||
if (this.isManuallyClosed) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!this.getOptionsProp(this.options, 'retryAttempts') ||
|
||||
times > this.getOptionsProp(this.options, 'retryAttempts')
|
||||
) {
|
||||
if (!this.getOptionsProp(this.options, 'retryAttempts')) {
|
||||
this.logger.error(
|
||||
'Redis connection closed and retry attempts not specified',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (times > this.getOptionsProp(this.options, 'retryAttempts')) {
|
||||
this.logger.error(`Retry time exhausted`);
|
||||
return;
|
||||
}
|
||||
return this.getOptionsProp(this.options, 'retryDelay') || 0;
|
||||
return this.getOptionsProp(this.options, 'retryDelay') ?? 5000;
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.pubClient || !this.subClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.',
|
||||
);
|
||||
}
|
||||
return [this.pubClient, this.subClient] as T;
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof RedisEvents = keyof RedisEvents,
|
||||
EventCallback extends RedisEvents[EventKey] = RedisEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.subClient && this.pubClient) {
|
||||
this.subClient.on(event, (...args: [any]) => callback('sub', ...args));
|
||||
this.pubClient.on(event, (...args: [any]) => callback('pub', ...args));
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
CONNECTION_FAILED_MESSAGE,
|
||||
CONNECT_EVENT,
|
||||
CONNECT_FAILED_EVENT,
|
||||
DISCONNECTED_RMQ_MESSAGE,
|
||||
DISCONNECT_EVENT,
|
||||
NO_MESSAGE_HANDLER,
|
||||
RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT,
|
||||
RQM_DEFAULT_NOACK,
|
||||
@@ -22,8 +19,9 @@ import {
|
||||
} from '../constants';
|
||||
import { RmqContext } from '../ctx-host';
|
||||
import { Transport } from '../enums';
|
||||
import { RmqEvents, RmqEventsMap, RmqStatus } from '../events/rmq.events';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { CustomTransportStrategy, RmqOptions } from '../interfaces';
|
||||
import { RmqOptions } from '../interfaces';
|
||||
import {
|
||||
IncomingRequest,
|
||||
OutgoingResponse,
|
||||
@@ -32,45 +30,50 @@ import {
|
||||
import { RmqRecordSerializer } from '../serializers/rmq-record.serializer';
|
||||
import { Server } from './server';
|
||||
|
||||
let rmqPackage: any = {};
|
||||
// To enable type safety for RMQ. This cant be uncommented by default
|
||||
// because it would require the user to install the amqplib package even if they dont use RabbitMQ
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
type AmqpConnectionManager =
|
||||
import('amqp-connection-manager').AmqpConnectionManager;
|
||||
type ChannelWrapper = import('amqp-connection-manager').ChannelWrapper;
|
||||
type Message = import('amqplib').Message;
|
||||
|
||||
// type AmqpConnectionManager = any;
|
||||
// type ChannelWrapper = any;
|
||||
// type Message = any;
|
||||
|
||||
let rmqPackage = {} as any; // as typeof import('amqp-connection-manager');
|
||||
|
||||
const INFINITE_CONNECTION_ATTEMPTS = -1;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
export class ServerRMQ extends Server<RmqEvents, RmqStatus> {
|
||||
public readonly transportId = Transport.RMQ;
|
||||
|
||||
protected server: any = null;
|
||||
protected channel: any = null;
|
||||
protected server: AmqpConnectionManager = null;
|
||||
protected channel: ChannelWrapper = null;
|
||||
protected connectionAttempts = 0;
|
||||
protected readonly urls: string[] | RmqUrl[];
|
||||
protected readonly queue: string;
|
||||
protected readonly prefetchCount: number;
|
||||
protected readonly noAck: boolean;
|
||||
protected readonly queueOptions: any;
|
||||
protected readonly isGlobalPrefetchCount: boolean;
|
||||
protected readonly noAssert: boolean;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof RmqEvents;
|
||||
callback: RmqEvents[keyof RmqEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(protected readonly options: RmqOptions['options']) {
|
||||
super();
|
||||
this.urls = this.getOptionsProp(this.options, 'urls') || [RQM_DEFAULT_URL];
|
||||
this.queue =
|
||||
this.getOptionsProp(this.options, 'queue') || RQM_DEFAULT_QUEUE;
|
||||
this.prefetchCount =
|
||||
this.getOptionsProp(this.options, 'prefetchCount') ||
|
||||
RQM_DEFAULT_PREFETCH_COUNT;
|
||||
this.noAck = this.getOptionsProp(this.options, 'noAck', RQM_DEFAULT_NOACK);
|
||||
this.isGlobalPrefetchCount =
|
||||
this.getOptionsProp(this.options, 'isGlobalPrefetchCount') ||
|
||||
RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT;
|
||||
this.queueOptions =
|
||||
this.getOptionsProp(this.options, 'queueOptions') ||
|
||||
RQM_DEFAULT_QUEUE_OPTIONS;
|
||||
this.noAssert =
|
||||
this.getOptionsProp(this.options, 'noAssert') ??
|
||||
this.queueOptions.noAssert ??
|
||||
RQM_DEFAULT_NO_ASSERT;
|
||||
|
||||
this.loadPackage('amqplib', ServerRMQ.name, () => require('amqplib'));
|
||||
rmqPackage = this.loadPackage(
|
||||
@@ -96,16 +99,18 @@ export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
public close(): void {
|
||||
this.channel && this.channel.close();
|
||||
this.server && this.server.close();
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public async start(
|
||||
callback?: (err?: unknown, ...optionalParams: unknown[]) => void,
|
||||
) {
|
||||
this.server = this.createClient();
|
||||
this.server.on(CONNECT_EVENT, () => {
|
||||
this.server.once(RmqEventsMap.CONNECT, () => {
|
||||
if (this.channel) {
|
||||
return;
|
||||
}
|
||||
this._status$.next(RmqStatus.CONNECTED);
|
||||
this.channel = this.server.createChannel({
|
||||
json: false,
|
||||
setup: (channel: any) => this.setupChannel(channel, callback),
|
||||
@@ -117,11 +122,18 @@ export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
'maxConnectionAttempts',
|
||||
INFINITE_CONNECTION_ATTEMPTS,
|
||||
);
|
||||
this.server.on(DISCONNECT_EVENT, (err: any) => {
|
||||
this.logger.error(DISCONNECTED_RMQ_MESSAGE);
|
||||
this.logger.error(err);
|
||||
});
|
||||
this.server.on(CONNECT_FAILED_EVENT, (error: Record<string, unknown>) => {
|
||||
|
||||
this.registerConnectListener();
|
||||
this.registerDisconnectListener();
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.server.on(event, callback),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
const connectFailedEvent = 'connectFailed';
|
||||
this.server.once(connectFailedEvent, (error: Record<string, unknown>) => {
|
||||
this._status$.next(RmqStatus.DISCONNECTED);
|
||||
|
||||
this.logger.error(CONNECTION_FAILED_MESSAGE);
|
||||
if (error?.err) {
|
||||
this.logger.error(error.err);
|
||||
@@ -149,11 +161,41 @@ export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
});
|
||||
}
|
||||
|
||||
private registerConnectListener() {
|
||||
this.server.on(RmqEventsMap.CONNECT, (err: any) => {
|
||||
this._status$.next(RmqStatus.CONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
private registerDisconnectListener() {
|
||||
this.server.on(RmqEventsMap.DISCONNECT, (err: any) => {
|
||||
this._status$.next(RmqStatus.DISCONNECTED);
|
||||
this.logger.error(DISCONNECTED_RMQ_MESSAGE);
|
||||
this.logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
public async setupChannel(channel: any, callback: Function) {
|
||||
if (!this.noAssert) {
|
||||
const noAssert =
|
||||
this.getOptionsProp(this.options, 'noAssert') ??
|
||||
this.queueOptions.noAssert ??
|
||||
RQM_DEFAULT_NO_ASSERT;
|
||||
|
||||
if (!noAssert) {
|
||||
await channel.assertQueue(this.queue, this.queueOptions);
|
||||
}
|
||||
await channel.prefetch(this.prefetchCount, this.isGlobalPrefetchCount);
|
||||
|
||||
const isGlobalPrefetchCount = this.getOptionsProp(
|
||||
this.options,
|
||||
'isGlobalPrefetchCount',
|
||||
RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT,
|
||||
);
|
||||
const prefetchCount = this.getOptionsProp(
|
||||
this.options,
|
||||
'prefetchCount',
|
||||
RQM_DEFAULT_PREFETCH_COUNT,
|
||||
);
|
||||
await channel.prefetch(prefetchCount, isGlobalPrefetchCount);
|
||||
channel.consume(
|
||||
this.queue,
|
||||
(msg: Record<string, any>) => this.handleMessage(msg, channel),
|
||||
@@ -192,7 +234,7 @@ export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
if (!handler) {
|
||||
if (!this.noAck) {
|
||||
this.logger.warn(RQM_NO_MESSAGE_HANDLER`${pattern}`);
|
||||
this.channel.nack(rmqContext.getMessage(), false, false);
|
||||
this.channel.nack(rmqContext.getMessage() as Message, false, false);
|
||||
}
|
||||
const status = 'error';
|
||||
const noHandlerPacket = {
|
||||
@@ -223,7 +265,7 @@ export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
): Promise<any> {
|
||||
const handler = this.getHandlerByPattern(pattern);
|
||||
if (!handler && !this.noAck) {
|
||||
this.channel.nack(context.getMessage(), false, false);
|
||||
this.channel.nack(context.getMessage() as Message, false, false);
|
||||
return this.logger.warn(RQM_NO_EVENT_HANDLER`${pattern}`);
|
||||
}
|
||||
return super.handleEvent(pattern, packet, context);
|
||||
@@ -244,6 +286,26 @@ export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
this.channel.sendToQueue(replyTo, buffer, { correlationId, ...options });
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.server) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.',
|
||||
);
|
||||
}
|
||||
return this.server as T;
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof RmqEvents = keyof RmqEvents,
|
||||
EventCallback extends RmqEvents[EventKey] = RmqEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.server) {
|
||||
this.server.addListener(event, callback);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
protected initializeSerializer(options: RmqOptions['options']) {
|
||||
this.serializer = options?.serializer ?? new RmqRecordSerializer();
|
||||
}
|
||||
|
||||
@@ -2,22 +2,19 @@ import { Type } from '@nestjs/common';
|
||||
import { isString, isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import * as net from 'net';
|
||||
import { Server as NetSocket, Socket } from 'net';
|
||||
import { createServer as tlsCreateServer, TlsOptions } from 'tls';
|
||||
import {
|
||||
CLOSE_EVENT,
|
||||
EADDRINUSE,
|
||||
ECONNREFUSED,
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
NO_MESSAGE_HANDLER,
|
||||
TCP_DEFAULT_HOST,
|
||||
TCP_DEFAULT_PORT,
|
||||
} from '../constants';
|
||||
import { TcpContext } from '../ctx-host/tcp.context';
|
||||
import { Transport } from '../enums';
|
||||
import { TcpEvents, TcpEventsMap, TcpStatus } from '../events/tcp.events';
|
||||
import { JsonSocket, TcpSocket } from '../helpers';
|
||||
import { createServer as tlsCreateServer } from 'tls';
|
||||
import {
|
||||
CustomTransportStrategy,
|
||||
IncomingRequest,
|
||||
PacketId,
|
||||
ReadPacket,
|
||||
@@ -29,17 +26,20 @@ import { Server } from './server';
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ServerTCP extends Server implements CustomTransportStrategy {
|
||||
export class ServerTCP extends Server<TcpEvents, TcpStatus> {
|
||||
public readonly transportId = Transport.TCP;
|
||||
|
||||
protected server: NetSocket;
|
||||
|
||||
private readonly port: number;
|
||||
private readonly host: string;
|
||||
private readonly socketClass: Type<TcpSocket>;
|
||||
private isExplicitlyTerminated = false;
|
||||
private retryAttemptsCount = 0;
|
||||
private tlsOptions?;
|
||||
protected readonly port: number;
|
||||
protected readonly host: string;
|
||||
protected readonly socketClass: Type<TcpSocket>;
|
||||
protected isManuallyTerminated = false;
|
||||
protected retryAttemptsCount = 0;
|
||||
protected tlsOptions?: TlsOptions;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof TcpEvents;
|
||||
callback: TcpEvents[keyof TcpEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(private readonly options: TcpOptions['options']) {
|
||||
super();
|
||||
@@ -57,8 +57,10 @@ export class ServerTCP extends Server implements CustomTransportStrategy {
|
||||
public listen(
|
||||
callback: (err?: unknown, ...optionalParams: unknown[]) => void,
|
||||
) {
|
||||
this.server.once(ERROR_EVENT, (err: Record<string, unknown>) => {
|
||||
this.server.once(TcpEventsMap.ERROR, (err: Record<string, unknown>) => {
|
||||
if (err?.code === EADDRINUSE || err?.code === ECONNREFUSED) {
|
||||
this._status$.next(TcpStatus.DISCONNECTED);
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
});
|
||||
@@ -66,17 +68,18 @@ export class ServerTCP extends Server implements CustomTransportStrategy {
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.isExplicitlyTerminated = true;
|
||||
this.isManuallyTerminated = true;
|
||||
|
||||
this.server.close();
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public bindHandler(socket: Socket) {
|
||||
const readSocket = this.getSocketInstance(socket);
|
||||
readSocket.on(MESSAGE_EVENT, async (msg: ReadPacket & PacketId) =>
|
||||
readSocket.on('message', async (msg: ReadPacket & PacketId) =>
|
||||
this.handleMessage(readSocket, msg),
|
||||
);
|
||||
readSocket.on(ERROR_EVENT, this.handleError.bind(this));
|
||||
readSocket.on(TcpEventsMap.ERROR, this.handleError.bind(this));
|
||||
}
|
||||
|
||||
public async handleMessage(socket: TcpSocket, rawMessage: unknown) {
|
||||
@@ -89,6 +92,7 @@ export class ServerTCP extends Server implements CustomTransportStrategy {
|
||||
if (isUndefined((packet as IncomingRequest).id)) {
|
||||
return this.handleEvent(pattern, packet, tcpContext);
|
||||
}
|
||||
|
||||
const handler = this.getHandlerByPattern(pattern);
|
||||
if (!handler) {
|
||||
const status = 'error';
|
||||
@@ -115,7 +119,7 @@ export class ServerTCP extends Server implements CustomTransportStrategy {
|
||||
|
||||
public handleClose(): undefined | number | NodeJS.Timer {
|
||||
if (
|
||||
this.isExplicitlyTerminated ||
|
||||
this.isManuallyTerminated ||
|
||||
!this.getOptionsProp(this.options, 'retryAttempts') ||
|
||||
this.retryAttemptsCount >=
|
||||
this.getOptionsProp(this.options, 'retryAttempts')
|
||||
@@ -129,7 +133,27 @@ export class ServerTCP extends Server implements CustomTransportStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
private init() {
|
||||
public unwrap<T>(): T {
|
||||
if (!this.server) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.',
|
||||
);
|
||||
}
|
||||
return this.server as T;
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof TcpEvents = keyof TcpEvents,
|
||||
EventCallback extends TcpEvents[EventKey] = TcpEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.server) {
|
||||
this.server.on(event, callback as any);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
protected init() {
|
||||
if (this.tlsOptions) {
|
||||
// TLS enabled, use tls server
|
||||
this.server = tlsCreateServer(
|
||||
@@ -140,11 +164,39 @@ export class ServerTCP extends Server implements CustomTransportStrategy {
|
||||
// TLS disabled, use net server
|
||||
this.server = net.createServer(this.bindHandler.bind(this));
|
||||
}
|
||||
this.server.on(ERROR_EVENT, this.handleError.bind(this));
|
||||
this.server.on(CLOSE_EVENT, this.handleClose.bind(this));
|
||||
this.registerListeningListener(this.server);
|
||||
this.registerErrorListener(this.server);
|
||||
this.registerCloseListener(this.server);
|
||||
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.server.on(event, callback),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
private getSocketInstance(socket: Socket): TcpSocket {
|
||||
protected registerListeningListener(socket: net.Server) {
|
||||
socket.on(TcpEventsMap.LISTENING, () => {
|
||||
this._status$.next(TcpStatus.CONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
protected registerErrorListener(socket: net.Server) {
|
||||
socket.on(TcpEventsMap.ERROR, err => {
|
||||
if ('code' in err && err.code === ECONNREFUSED) {
|
||||
this._status$.next(TcpStatus.DISCONNECTED);
|
||||
}
|
||||
this.handleError(err as any);
|
||||
});
|
||||
}
|
||||
|
||||
protected registerCloseListener(socket: net.Server) {
|
||||
socket.on(TcpEventsMap.CLOSE, () => {
|
||||
this._status$.next(TcpStatus.DISCONNECTED);
|
||||
this.handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
protected getSocketInstance(socket: Socket): TcpSocket {
|
||||
return new this.socketClass(socket);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,20 @@ import {
|
||||
Observable,
|
||||
ObservedValueOf,
|
||||
of,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import { catchError, finalize, mergeMap } from 'rxjs/operators';
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
mergeMap,
|
||||
} from 'rxjs/operators';
|
||||
import { NO_EVENT_HANDLER } from '../constants';
|
||||
import { BaseRpcContext } from '../ctx-host/base-rpc.context';
|
||||
import { IncomingRequestDeserializer } from '../deserializers/incoming-request.deserializer';
|
||||
import { Transport } from '../enums';
|
||||
import {
|
||||
ClientOptions,
|
||||
KafkaOptions,
|
||||
@@ -37,11 +44,52 @@ import { transformPatternToRoute } from '../utils';
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export abstract class Server {
|
||||
export abstract class Server<
|
||||
EventsMap extends Record<string, Function> = Record<string, Function>,
|
||||
Status extends string = string,
|
||||
> {
|
||||
/**
|
||||
* Unique transport identifier.
|
||||
*/
|
||||
readonly transportId?: Transport | symbol;
|
||||
|
||||
protected readonly messageHandlers = new Map<string, MessageHandler>();
|
||||
protected readonly logger: LoggerService = new Logger(Server.name);
|
||||
protected serializer: ConsumerSerializer;
|
||||
protected deserializer: ConsumerDeserializer;
|
||||
protected _status$ = new ReplaySubject<Status>(1);
|
||||
|
||||
/**
|
||||
* Returns an observable that emits status changes.
|
||||
*/
|
||||
public get status(): Observable<Status> {
|
||||
return this._status$.asObservable().pipe(distinctUntilChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event listener for the given event.
|
||||
* @param event Event name
|
||||
* @param callback Callback to be executed when the event is emitted
|
||||
*/
|
||||
public abstract on<
|
||||
EventKey extends keyof EventsMap = keyof EventsMap,
|
||||
EventCallback extends EventsMap[EventKey] = EventsMap[EventKey],
|
||||
>(event: EventKey, callback: EventCallback): any;
|
||||
/**
|
||||
* Returns an instance of the underlying server/broker instance,
|
||||
* or a group of servers if there are more than one.
|
||||
*/
|
||||
public abstract unwrap<T>(): T;
|
||||
|
||||
/**
|
||||
* Method called when server is being initialized.
|
||||
* @param callback Function to be called upon initialization
|
||||
*/
|
||||
public abstract listen(callback: (...optionalParams: unknown[]) => any): any;
|
||||
/**
|
||||
* Method called when server is being terminated.
|
||||
*/
|
||||
public abstract close(): any;
|
||||
|
||||
public addHandler(
|
||||
pattern: any,
|
||||
|
||||
@@ -176,6 +176,21 @@ describe('ClientKafka', () => {
|
||||
run,
|
||||
events: {
|
||||
GROUP_JOIN: 'consumer.group_join',
|
||||
HEARTBEAT: 'consumer.heartbeat',
|
||||
COMMIT_OFFSETS: 'consumer.commit_offsets',
|
||||
FETCH_START: 'consumer.fetch_start',
|
||||
FETCH: 'consumer.fetch',
|
||||
START_BATCH_PROCESS: 'consumer.start_batch_process',
|
||||
END_BATCH_PROCESS: 'consumer.end_batch_process',
|
||||
CONNECT: 'consumer.connect',
|
||||
DISCONNECT: 'consumer.disconnect',
|
||||
STOP: 'consumer.stop',
|
||||
CRASH: 'consumer.crash',
|
||||
REBALANCING: 'consumer.rebalancing',
|
||||
RECEIVED_UNSUBSCRIBED_TOPICS: 'consumer.received_unsubscribed_topics',
|
||||
REQUEST: 'consumer.network.request',
|
||||
REQUEST_TIMEOUT: 'consumer.network.request_timeout',
|
||||
REQUEST_QUEUE_SIZE: 'consumer.network.request_queue_size',
|
||||
},
|
||||
on,
|
||||
};
|
||||
@@ -184,6 +199,14 @@ describe('ClientKafka', () => {
|
||||
return {
|
||||
connect,
|
||||
send,
|
||||
events: {
|
||||
CONNECT: 'producer.connect',
|
||||
DISCONNECT: 'producer.disconnect',
|
||||
REQUEST: 'producer.network.request',
|
||||
REQUEST_TIMEOUT: 'producer.network.request_timeout',
|
||||
REQUEST_QUEUE_SIZE: 'producer.network.request_queue_size',
|
||||
},
|
||||
on,
|
||||
};
|
||||
});
|
||||
kafkaClient = {
|
||||
@@ -250,16 +273,16 @@ describe('ClientKafka', () => {
|
||||
const consumer = { disconnect: sinon.stub().resolves() };
|
||||
const producer = { disconnect: sinon.stub().resolves() };
|
||||
beforeEach(() => {
|
||||
(client as any).consumer = consumer;
|
||||
(client as any).producer = producer;
|
||||
(client as any)._consumer = consumer;
|
||||
(client as any)._producer = producer;
|
||||
});
|
||||
it('should close server', async () => {
|
||||
await client.close();
|
||||
|
||||
expect(consumer.disconnect.calledOnce).to.be.true;
|
||||
expect(producer.disconnect.calledOnce).to.be.true;
|
||||
expect((client as any).consumer).to.be.null;
|
||||
expect((client as any).producer).to.be.null;
|
||||
expect((client as any)._consumer).to.be.null;
|
||||
expect((client as any)._producer).to.be.null;
|
||||
expect((client as any).client).to.be.null;
|
||||
});
|
||||
});
|
||||
@@ -267,7 +290,6 @@ describe('ClientKafka', () => {
|
||||
describe('connect', () => {
|
||||
let consumerAssignmentsStub: sinon.SinonStub;
|
||||
let bindTopicsStub: sinon.SinonStub;
|
||||
// let handleErrorsSpy: sinon.SinonSpy;
|
||||
|
||||
describe('consumer and producer', () => {
|
||||
beforeEach(() => {
|
||||
@@ -285,14 +307,10 @@ describe('ClientKafka', () => {
|
||||
|
||||
expect(createClientStub.calledOnce).to.be.true;
|
||||
expect(producerStub.calledOnce).to.be.true;
|
||||
|
||||
expect(consumerStub.calledOnce).to.be.true;
|
||||
|
||||
expect(on.calledOnce).to.be.true;
|
||||
expect(on.called).to.be.true;
|
||||
expect(client['consumerAssignments']).to.be.empty;
|
||||
|
||||
expect(connect.calledTwice).to.be.true;
|
||||
|
||||
expect(bindTopicsStub.calledOnce).to.be.true;
|
||||
expect(connection).to.deep.equal(producerStub());
|
||||
});
|
||||
@@ -428,7 +446,7 @@ describe('ClientKafka', () => {
|
||||
describe('bindTopics', () => {
|
||||
it('should bind topics from response patterns', async () => {
|
||||
(client as any).responsePatterns = [replyTopic];
|
||||
(client as any).consumer = kafkaClient.consumer();
|
||||
(client as any)._consumer = kafkaClient.consumer();
|
||||
|
||||
await client.bindTopics();
|
||||
|
||||
@@ -443,7 +461,7 @@ describe('ClientKafka', () => {
|
||||
|
||||
it('should bind topics from response patterns with options', async () => {
|
||||
(client as any).responsePatterns = [replyTopic];
|
||||
(client as any).consumer = kafkaClient.consumer();
|
||||
(client as any)._consumer = kafkaClient.consumer();
|
||||
(client as any).options.subscribe = {};
|
||||
(client as any).options.subscribe.fromBeginning = true;
|
||||
|
||||
@@ -567,7 +585,7 @@ describe('ClientKafka', () => {
|
||||
});
|
||||
|
||||
it('should publish packet', async () => {
|
||||
sinon.stub(client as any, 'producer').value({
|
||||
sinon.stub(client as any, '_producer').value({
|
||||
send: sendSpy,
|
||||
});
|
||||
|
||||
@@ -636,13 +654,14 @@ describe('ClientKafka', () => {
|
||||
});
|
||||
|
||||
describe('publish', () => {
|
||||
const waitForNextTick = async () =>
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
const readPacket = {
|
||||
pattern: topic,
|
||||
data: messageValue,
|
||||
};
|
||||
|
||||
let assignPacketIdStub: sinon.SinonStub;
|
||||
|
||||
let normalizePatternSpy: sinon.SinonSpy;
|
||||
let getResponsePatternNameSpy: sinon.SinonSpy;
|
||||
let getReplyTopicPartitionSpy: sinon.SinonSpy;
|
||||
@@ -650,7 +669,6 @@ describe('ClientKafka', () => {
|
||||
let sendSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// spy
|
||||
normalizePatternSpy = sinon.spy(client as any, 'normalizePattern');
|
||||
getResponsePatternNameSpy = sinon.spy(
|
||||
client as any,
|
||||
@@ -663,7 +681,6 @@ describe('ClientKafka', () => {
|
||||
routingMapSetSpy = sinon.spy((client as any).routingMap, 'set');
|
||||
sendSpy = sinon.spy(() => Promise.resolve());
|
||||
|
||||
// stub
|
||||
assignPacketIdStub = sinon
|
||||
.stub(client as any, 'assignPacketId')
|
||||
.callsFake(packet =>
|
||||
@@ -672,45 +689,61 @@ describe('ClientKafka', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
sinon.stub(client as any, 'producer').value({
|
||||
sinon.stub(client as any, '_producer').value({
|
||||
send: sendSpy,
|
||||
});
|
||||
|
||||
// set
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: parseFloat(replyPartition),
|
||||
};
|
||||
});
|
||||
|
||||
it('should assign a packet id', async () => {
|
||||
await client['publish'](readPacket, callback);
|
||||
client['publish'](readPacket, callback);
|
||||
|
||||
await waitForNextTick();
|
||||
|
||||
expect(assignPacketIdStub.calledWith(readPacket)).to.be.true;
|
||||
});
|
||||
|
||||
it('should normalize the pattern', async () => {
|
||||
await client['publish'](readPacket, callback);
|
||||
client['publish'](readPacket, callback);
|
||||
|
||||
await waitForNextTick();
|
||||
|
||||
expect(normalizePatternSpy.calledWith(topic)).to.be.true;
|
||||
});
|
||||
|
||||
it('should get the reply pattern', async () => {
|
||||
await client['publish'](readPacket, callback);
|
||||
client['publish'](readPacket, callback);
|
||||
|
||||
await waitForNextTick();
|
||||
|
||||
expect(getResponsePatternNameSpy.calledWith(topic)).to.be.true;
|
||||
});
|
||||
|
||||
it('should get the reply partition', async () => {
|
||||
await client['publish'](readPacket, callback);
|
||||
client['publish'](readPacket, callback);
|
||||
|
||||
await waitForNextTick();
|
||||
|
||||
expect(getReplyTopicPartitionSpy.calledWith(replyTopic)).to.be.true;
|
||||
});
|
||||
|
||||
it('should add the callback to the routing map', async () => {
|
||||
await client['publish'](readPacket, callback);
|
||||
client['publish'](readPacket, callback);
|
||||
|
||||
await waitForNextTick();
|
||||
|
||||
expect(routingMapSetSpy.calledOnce).to.be.true;
|
||||
expect(routingMapSetSpy.args[0][0]).to.eq(correlationId);
|
||||
expect(routingMapSetSpy.args[0][1]).to.eq(callback);
|
||||
});
|
||||
|
||||
it('should send the message with headers', async () => {
|
||||
await client['publish'](readPacket, callback);
|
||||
client['publish'](readPacket, callback);
|
||||
|
||||
await waitForNextTick();
|
||||
|
||||
expect(sendSpy.calledOnce).to.be.true;
|
||||
expect(sendSpy.args[0][0].topic).to.eq(topic);
|
||||
@@ -731,6 +764,9 @@ describe('ClientKafka', () => {
|
||||
|
||||
it('should remove callback from routing map when unsubscribe', async () => {
|
||||
client['publish'](readPacket, callback)();
|
||||
|
||||
await waitForNextTick();
|
||||
|
||||
expect(client['routingMap'].has(correlationId)).to.be.false;
|
||||
expect(client['routingMap'].size).to.eq(0);
|
||||
});
|
||||
@@ -744,7 +780,7 @@ describe('ClientKafka', () => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
clientProducerStub = sinon.stub(client as any, 'producer').value({
|
||||
clientProducerStub = sinon.stub(client as any, '_producer').value({
|
||||
send: sendStub,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from 'chai';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import * as sinon from 'sinon';
|
||||
import { ClientMqtt } from '../../client/client-mqtt';
|
||||
import { ERROR_EVENT } from '../../constants';
|
||||
import { MqttEventsMap } from '../../events/mqtt.events';
|
||||
import { ReadPacket } from '../../interfaces';
|
||||
import { MqttRecord } from '../../record-builders';
|
||||
|
||||
@@ -245,7 +245,7 @@ describe('ClientMqtt', () => {
|
||||
});
|
||||
describe('connect', () => {
|
||||
let createClientStub: sinon.SinonStub;
|
||||
let handleErrorsSpy: sinon.SinonSpy;
|
||||
let registerErrorListenerSpy: sinon.SinonSpy;
|
||||
let connect$Stub: sinon.SinonStub;
|
||||
let mergeCloseEvent: sinon.SinonStub;
|
||||
|
||||
@@ -255,9 +255,10 @@ describe('ClientMqtt', () => {
|
||||
({
|
||||
addListener: () => ({}),
|
||||
removeListener: () => ({}),
|
||||
on: () => ({}),
|
||||
}) as any,
|
||||
);
|
||||
handleErrorsSpy = sinon.spy(client, 'handleError');
|
||||
registerErrorListenerSpy = sinon.spy(client, 'registerErrorListener');
|
||||
connect$Stub = sinon.stub(client, 'connect$' as any).callsFake(() => ({
|
||||
subscribe: ({ complete }) => complete(),
|
||||
pipe() {
|
||||
@@ -270,7 +271,7 @@ describe('ClientMqtt', () => {
|
||||
});
|
||||
afterEach(() => {
|
||||
createClientStub.restore();
|
||||
handleErrorsSpy.restore();
|
||||
registerErrorListenerSpy.restore();
|
||||
connect$Stub.restore();
|
||||
mergeCloseEvent.restore();
|
||||
});
|
||||
@@ -279,8 +280,8 @@ describe('ClientMqtt', () => {
|
||||
client['mqttClient'] = null;
|
||||
await client.connect();
|
||||
});
|
||||
it('should call "handleError" once', async () => {
|
||||
expect(handleErrorsSpy.called).to.be.true;
|
||||
it('should call "registerErrorListener" once', async () => {
|
||||
expect(registerErrorListenerSpy.called).to.be.true;
|
||||
});
|
||||
it('should call "createClient" once', async () => {
|
||||
expect(createClientStub.called).to.be.true;
|
||||
@@ -296,8 +297,8 @@ describe('ClientMqtt', () => {
|
||||
it('should not call "createClient"', () => {
|
||||
expect(createClientStub.called).to.be.false;
|
||||
});
|
||||
it('should not call "handleError"', () => {
|
||||
expect(handleErrorsSpy.called).to.be.false;
|
||||
it('should not call "registerErrorListener"', () => {
|
||||
expect(registerErrorListenerSpy.called).to.be.false;
|
||||
});
|
||||
it('should not call "connect$"', () => {
|
||||
expect(connect$Stub.called).to.be.false;
|
||||
@@ -316,14 +317,54 @@ describe('ClientMqtt', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('handleError', () => {
|
||||
describe('registerErrorListener', () => {
|
||||
it('should bind error event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
addListener: callback,
|
||||
on: callback,
|
||||
};
|
||||
client.handleError(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(ERROR_EVENT);
|
||||
client.registerErrorListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.ERROR);
|
||||
});
|
||||
});
|
||||
describe('registerConnectListener', () => {
|
||||
it('should bind connect event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerConnectListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.CONNECT);
|
||||
});
|
||||
});
|
||||
describe('registerDisconnectListener', () => {
|
||||
it('should bind disconnect event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerDisconnectListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.DISCONNECT);
|
||||
});
|
||||
});
|
||||
describe('registerOfflineListener', () => {
|
||||
it('should bind offline event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerOfflineListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.OFFLINE);
|
||||
});
|
||||
});
|
||||
describe('registerCloseListener', () => {
|
||||
it('should bind close event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerCloseListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.CLOSE);
|
||||
});
|
||||
});
|
||||
describe('dispatchEvent', () => {
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('ClientNats', () => {
|
||||
beforeEach(async () => {
|
||||
createClientSpy = sinon
|
||||
.stub(client, 'createClient')
|
||||
.callsFake(() => ({}) as any);
|
||||
.callsFake(() => Promise.resolve({}));
|
||||
handleStatusUpdatesSpy = sinon.spy(client, 'handleStatusUpdates');
|
||||
|
||||
await client.connect();
|
||||
@@ -253,7 +253,7 @@ describe('ClientNats', () => {
|
||||
describe('when is not connected', () => {
|
||||
beforeEach(async () => {
|
||||
client['natsClient'] = null;
|
||||
client['clientConnectionPromise'] = null;
|
||||
client['connectionPromise'] = null;
|
||||
await client.connect();
|
||||
});
|
||||
it('should call "handleStatusUpdatesSpy" once', async () => {
|
||||
|
||||
@@ -8,9 +8,15 @@ class TestClientProxy extends ClientProxy {
|
||||
protected async dispatchEvent<T = any>(
|
||||
packet: ReadPacket<any>,
|
||||
): Promise<any> {}
|
||||
|
||||
public async connect() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public publish(pattern, callback): any {}
|
||||
public async close() {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { ClientRedis } from '../../client/client-redis';
|
||||
import { ERROR_EVENT } from '../../constants';
|
||||
import { RedisEventsMap } from '../../events/redis.events';
|
||||
|
||||
describe('ClientRedis', () => {
|
||||
const test = 'test';
|
||||
@@ -28,8 +28,8 @@ describe('ClientRedis', () => {
|
||||
removeListenerSpy: sinon.SinonSpy,
|
||||
unsubscribeSpy: sinon.SinonSpy,
|
||||
connectSpy: sinon.SinonSpy,
|
||||
sub,
|
||||
pub;
|
||||
sub: any,
|
||||
pub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
subscribeSpy = sinon.spy((name, fn) => fn());
|
||||
@@ -43,7 +43,6 @@ describe('ClientRedis', () => {
|
||||
on: (type, handler) => (type === 'subscribe' ? handler() : onSpy()),
|
||||
removeListener: removeListenerSpy,
|
||||
unsubscribe: unsubscribeSpy,
|
||||
addListener: () => ({}),
|
||||
};
|
||||
pub = { publish: publishSpy };
|
||||
(client as any).subClient = sub;
|
||||
@@ -98,7 +97,7 @@ describe('ClientRedis', () => {
|
||||
getReplyPatternStub = sinon
|
||||
.stub(client, 'getReplyPattern')
|
||||
.callsFake(() => channel);
|
||||
subscription = await client['publish'](msg, callback);
|
||||
subscription = client['publish'](msg, callback);
|
||||
subscription(channel, JSON.stringify({ isDisposed: true, id }));
|
||||
});
|
||||
afterEach(() => {
|
||||
@@ -181,23 +180,26 @@ describe('ClientRedis', () => {
|
||||
});
|
||||
});
|
||||
describe('close', () => {
|
||||
const untypedClient = client as any;
|
||||
|
||||
let pubClose: sinon.SinonSpy;
|
||||
let subClose: sinon.SinonSpy;
|
||||
let pub, sub;
|
||||
let pub: any, sub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
pubClose = sinon.spy();
|
||||
subClose = sinon.spy();
|
||||
pub = { quit: pubClose };
|
||||
sub = { quit: subClose };
|
||||
(client as any).pubClient = pub;
|
||||
(client as any).subClient = sub;
|
||||
untypedClient.pubClient = pub;
|
||||
untypedClient.subClient = sub;
|
||||
});
|
||||
it('should close "pub" when it is not null', () => {
|
||||
client.close();
|
||||
expect(pubClose.called).to.be.true;
|
||||
});
|
||||
it('should not close "pub" when it is null', () => {
|
||||
(client as any).pubClient = null;
|
||||
untypedClient.pubClient = null;
|
||||
client.close();
|
||||
expect(pubClose.called).to.be.false;
|
||||
});
|
||||
@@ -206,47 +208,80 @@ describe('ClientRedis', () => {
|
||||
expect(subClose.called).to.be.true;
|
||||
});
|
||||
it('should not close "sub" when it is null', () => {
|
||||
(client as any).subClient = null;
|
||||
untypedClient.subClient = null;
|
||||
client.close();
|
||||
expect(subClose.called).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('connect', () => {
|
||||
let createClientSpy: sinon.SinonSpy;
|
||||
let handleErrorsSpy: sinon.SinonSpy;
|
||||
let registerErrorListenerSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
createClientSpy = sinon.stub(client, 'createClient').callsFake(
|
||||
() =>
|
||||
({
|
||||
on: () => null,
|
||||
addListener: () => null,
|
||||
removeListener: () => null,
|
||||
}) as any,
|
||||
);
|
||||
handleErrorsSpy = sinon.spy(client, 'handleError');
|
||||
registerErrorListenerSpy = sinon.spy(client, 'registerErrorListener');
|
||||
|
||||
client.connect();
|
||||
client['pubClient'] = null;
|
||||
});
|
||||
afterEach(() => {
|
||||
createClientSpy.restore();
|
||||
handleErrorsSpy.restore();
|
||||
registerErrorListenerSpy.restore();
|
||||
});
|
||||
it('should call "createClient" twice', () => {
|
||||
expect(createClientSpy.calledTwice).to.be.true;
|
||||
});
|
||||
it('should call "handleError" twice', () => {
|
||||
expect(handleErrorsSpy.calledTwice).to.be.true;
|
||||
it('should call "registerErrorListener" twice', () => {
|
||||
expect(registerErrorListenerSpy.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('handleError', () => {
|
||||
describe('registerErrorListener', () => {
|
||||
it('should bind error event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
addListener: callback,
|
||||
};
|
||||
client.handleError(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(ERROR_EVENT);
|
||||
client.registerErrorListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(RedisEventsMap.ERROR);
|
||||
});
|
||||
});
|
||||
describe('registerEndListener', () => {
|
||||
it('should bind end event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerEndListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(RedisEventsMap.END);
|
||||
});
|
||||
});
|
||||
describe('registerReadyListener', () => {
|
||||
it('should bind ready event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerReadyListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(RedisEventsMap.READY);
|
||||
});
|
||||
});
|
||||
describe('registerReconnectListener', () => {
|
||||
it('should bind reconnect event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerReconnectListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(
|
||||
RedisEventsMap.RECONNECTING,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('getClientOptions', () => {
|
||||
@@ -262,14 +297,14 @@ describe('ClientRedis', () => {
|
||||
describe('createRetryStrategy', () => {
|
||||
describe('when is terminated', () => {
|
||||
it('should return undefined', () => {
|
||||
(client as any).isExplicitlyTerminated = true;
|
||||
(client as any).isManuallyClosed = true;
|
||||
const result = client.createRetryStrategy(0);
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
});
|
||||
describe('when "retryAttempts" does not exist', () => {
|
||||
it('should return undefined', () => {
|
||||
(client as any).isExplicitlyTerminated = false;
|
||||
(client as any).isManuallyClosed = false;
|
||||
(client as any).options.options = {};
|
||||
(client as any).options.options.retryAttempts = undefined;
|
||||
const result = client.createRetryStrategy(1);
|
||||
@@ -278,7 +313,7 @@ describe('ClientRedis', () => {
|
||||
});
|
||||
describe('when "attempts" count is max', () => {
|
||||
it('should return undefined', () => {
|
||||
(client as any).isExplicitlyTerminated = false;
|
||||
(client as any).isManuallyClosed = false;
|
||||
(client as any).options.options = {};
|
||||
(client as any).options.options.retryAttempts = 3;
|
||||
const result = client.createRetryStrategy(4);
|
||||
@@ -288,7 +323,7 @@ describe('ClientRedis', () => {
|
||||
describe('otherwise', () => {
|
||||
it('should return delay (ms)', () => {
|
||||
(client as any).options = {};
|
||||
(client as any).isExplicitlyTerminated = false;
|
||||
(client as any).isManuallyClosed = false;
|
||||
(client as any).options.retryAttempts = 3;
|
||||
(client as any).options.retryDelay = 3;
|
||||
const result = client.createRetryStrategy(2);
|
||||
|
||||
@@ -11,22 +11,9 @@ describe('ClientRMQ', function () {
|
||||
|
||||
let client: ClientRMQ;
|
||||
|
||||
describe('constructor', () => {
|
||||
it(`should fallback to queueOptions.noAssert when 'noAssert' is undefined`, () => {
|
||||
const queueOptions = {
|
||||
noAssert: true,
|
||||
};
|
||||
const instance = new ClientRMQ({
|
||||
queueOptions,
|
||||
});
|
||||
|
||||
expect(instance).property('noAssert').to.eq(queueOptions.noAssert);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connect', () => {
|
||||
let createClientStub: sinon.SinonStub;
|
||||
let handleErrorsSpy: sinon.SinonSpy;
|
||||
let registerErrorListenerSpy: sinon.SinonSpy;
|
||||
let connect$Stub: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -35,7 +22,7 @@ describe('ClientRMQ', function () {
|
||||
addListener: () => ({}),
|
||||
removeListener: () => ({}),
|
||||
}));
|
||||
handleErrorsSpy = sinon.spy(client, 'handleError');
|
||||
registerErrorListenerSpy = sinon.spy(client, 'registerErrorListener');
|
||||
connect$Stub = sinon.stub(client, 'connect$' as any).callsFake(() => ({
|
||||
subscribe: resolve => resolve(),
|
||||
toPromise() {
|
||||
@@ -56,8 +43,8 @@ describe('ClientRMQ', function () {
|
||||
await client.connect();
|
||||
} catch {}
|
||||
});
|
||||
it('should call "handleError" once', async () => {
|
||||
expect(handleErrorsSpy.called).to.be.true;
|
||||
it('should call "registerErrorListener" once', async () => {
|
||||
expect(registerErrorListenerSpy.called).to.be.true;
|
||||
});
|
||||
it('should call "createClient" once', async () => {
|
||||
expect(createClientStub.called).to.be.true;
|
||||
@@ -74,8 +61,8 @@ describe('ClientRMQ', function () {
|
||||
it('should not call "createClient"', () => {
|
||||
expect(createClientStub.called).to.be.false;
|
||||
});
|
||||
it('should not call "handleError"', () => {
|
||||
expect(handleErrorsSpy.called).to.be.false;
|
||||
it('should not call "registerErrorListener"', () => {
|
||||
expect(registerErrorListenerSpy.called).to.be.false;
|
||||
});
|
||||
it('should not call "connect$"', () => {
|
||||
expect(connect$Stub.called).to.be.false;
|
||||
|
||||
@@ -3,15 +3,18 @@ import { Socket as NetSocket } from 'net';
|
||||
import * as sinon from 'sinon';
|
||||
import { TLSSocket } from 'tls';
|
||||
import { ClientTCP } from '../../client/client-tcp';
|
||||
import { ERROR_EVENT } from '../../constants';
|
||||
import { TcpEventsMap } from '../../events/tcp.events';
|
||||
|
||||
describe('ClientTCP', () => {
|
||||
let client: ClientTCP;
|
||||
let socket;
|
||||
let untypedClient: any;
|
||||
let socket: any;
|
||||
let createSocketStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new ClientTCP({});
|
||||
untypedClient = client as any;
|
||||
|
||||
const onFakeCallback = (event, callback) =>
|
||||
event !== 'error' && event !== 'close' && callback({});
|
||||
|
||||
@@ -63,7 +66,7 @@ describe('ClientTCP', () => {
|
||||
});
|
||||
});
|
||||
describe('handleResponse', () => {
|
||||
let callback;
|
||||
let callback: sinon.SinonSpy;
|
||||
const id = '1';
|
||||
|
||||
describe('when disposed', () => {
|
||||
@@ -106,14 +109,20 @@ describe('ClientTCP', () => {
|
||||
});
|
||||
});
|
||||
describe('connect', () => {
|
||||
let bindEventsSpy: sinon.SinonSpy;
|
||||
let registerConnectListenerSpy: sinon.SinonSpy;
|
||||
let registerErrorListenerSpy: sinon.SinonSpy;
|
||||
let registerCloseListenerSpy: sinon.SinonSpy;
|
||||
let connect$Stub: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
bindEventsSpy = sinon.spy(client, 'bindEvents');
|
||||
registerConnectListenerSpy = sinon.spy(client, 'registerConnectListener');
|
||||
registerErrorListenerSpy = sinon.spy(client, 'registerErrorListener');
|
||||
registerCloseListenerSpy = sinon.spy(client, 'registerCloseListener');
|
||||
});
|
||||
afterEach(() => {
|
||||
bindEventsSpy.restore();
|
||||
registerConnectListenerSpy.restore();
|
||||
registerErrorListenerSpy.restore();
|
||||
registerCloseListenerSpy.restore;
|
||||
});
|
||||
describe('when is not connected', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -130,8 +139,14 @@ describe('ClientTCP', () => {
|
||||
afterEach(() => {
|
||||
connect$Stub.restore();
|
||||
});
|
||||
it('should call "bindEvents" once', async () => {
|
||||
expect(bindEventsSpy.called).to.be.true;
|
||||
it('should call "registerConnectListener" once', async () => {
|
||||
expect(registerConnectListenerSpy.called).to.be.true;
|
||||
});
|
||||
it('should call "registerErrorListener" once', async () => {
|
||||
expect(registerErrorListenerSpy.called).to.be.true;
|
||||
});
|
||||
it('should call "registerCloseListener" once', async () => {
|
||||
expect(registerCloseListenerSpy.called).to.be.true;
|
||||
});
|
||||
it('should call "createSocket" once', async () => {
|
||||
expect(createSocketStub.called).to.be.true;
|
||||
@@ -151,34 +166,31 @@ describe('ClientTCP', () => {
|
||||
expect(createSocketStub.called).to.be.false;
|
||||
});
|
||||
it('should not call "bindEvents"', () => {
|
||||
expect(bindEventsSpy.called).to.be.false;
|
||||
expect(registerConnectListenerSpy.called).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('close', () => {
|
||||
let routingMap;
|
||||
let callback;
|
||||
let routingMap: Map<string, Function>;
|
||||
let callback: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
routingMap = new Map<string, Function>();
|
||||
callback = sinon.spy();
|
||||
routingMap.set('some id', callback);
|
||||
(client as any).socket = socket;
|
||||
(client as any).isConnected = true;
|
||||
(client as any).routingMap = routingMap;
|
||||
|
||||
untypedClient.socket = socket;
|
||||
untypedClient.routingMap = routingMap;
|
||||
client.close();
|
||||
});
|
||||
it('should end() socket', () => {
|
||||
expect(socket.end.called).to.be.true;
|
||||
});
|
||||
it('should set "isConnected" to false', () => {
|
||||
expect((client as any).isConnected).to.be.false;
|
||||
});
|
||||
it('should set "socket" to null', () => {
|
||||
expect((client as any).socket).to.be.null;
|
||||
expect(untypedClient.socket).to.be.null;
|
||||
});
|
||||
it('should clear out the routing map', () => {
|
||||
expect((client as any).routingMap.size).to.be.eq(0);
|
||||
expect(untypedClient.routingMap.size).to.be.eq(0);
|
||||
});
|
||||
it('should call callbacks', () => {
|
||||
expect(
|
||||
@@ -188,14 +200,34 @@ describe('ClientTCP', () => {
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('bindEvents', () => {
|
||||
describe('registerErrorListener', () => {
|
||||
it('should bind error event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.bindEvents(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(ERROR_EVENT);
|
||||
client.registerErrorListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(TcpEventsMap.ERROR);
|
||||
});
|
||||
});
|
||||
describe('registerCloseListener', () => {
|
||||
it('should bind close event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerCloseListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(TcpEventsMap.CLOSE);
|
||||
});
|
||||
});
|
||||
describe('registerConnectListener', () => {
|
||||
it('should bind connect event handler', () => {
|
||||
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
|
||||
const emitter = {
|
||||
on: callback,
|
||||
};
|
||||
client.registerConnectListener(emitter as any);
|
||||
expect(callback.getCall(0).args[0]).to.be.eql(TcpEventsMap.CONNECT);
|
||||
});
|
||||
});
|
||||
describe('dispatchEvent', () => {
|
||||
@@ -207,7 +239,7 @@ describe('ClientTCP', () => {
|
||||
internalSocket = {
|
||||
sendMessage: sendMessageStub,
|
||||
};
|
||||
(client as any).socket = internalSocket;
|
||||
untypedClient.socket = internalSocket;
|
||||
});
|
||||
|
||||
it('should publish packet', async () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { expect } from 'chai';
|
||||
import { AddressInfo, createServer, Socket } from 'net';
|
||||
import { CONNECT_EVENT, MESSAGE_EVENT } from '../../constants';
|
||||
import { TcpEventsMap } from '../../events/tcp.events';
|
||||
import { JsonSocket } from '../../helpers/json-socket';
|
||||
import { longPayload } from './data/long-payload-with-special-chars';
|
||||
import * as helpers from './helpers';
|
||||
import { ip } from './helpers';
|
||||
|
||||
const MESSAGE_EVENT = 'message';
|
||||
|
||||
describe('JsonSocket connection', () => {
|
||||
it('should connect, send and receive message', done => {
|
||||
helpers.createServerAndClient(
|
||||
@@ -179,7 +181,7 @@ describe('JsonSocket connection', () => {
|
||||
expect(serverSocket['isClosed']).to.equal(true);
|
||||
expect(clientSocket['isClosed']).to.equal(true);
|
||||
|
||||
clientSocket.on(CONNECT_EVENT, () => {
|
||||
clientSocket.on(TcpEventsMap.CONNECT, () => {
|
||||
setTimeout(() => {
|
||||
expect(clientSocket['isClosed']).to.equal(false);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Server,
|
||||
Socket,
|
||||
} from 'net';
|
||||
import { ERROR_EVENT } from '../../constants';
|
||||
import { TcpEventsMap } from '../../events/tcp.events';
|
||||
import { JsonSocket } from '../../helpers/json-socket';
|
||||
|
||||
export const ip = '127.0.0.1';
|
||||
@@ -17,7 +17,7 @@ export function createServer(callback: (err?: any, server?: Server) => void) {
|
||||
callback(null, server);
|
||||
});
|
||||
|
||||
server.on(ERROR_EVENT, (err: any) => {
|
||||
server.on(TcpEventsMap.ERROR, (err: any) => {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export function createClient(
|
||||
|
||||
clientSocket.connect(port, ip);
|
||||
|
||||
clientSocket.on(ERROR_EVENT, (err: any) => {
|
||||
clientSocket.on(TcpEventsMap.ERROR, (err: any) => {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CONNECT_EVENT, MESSAGE_EVENT } from '../../constants';
|
||||
import { expect } from 'chai';
|
||||
import { TcpEventsMap } from '../../events/tcp.events';
|
||||
import { JsonSocket } from '../../helpers/json-socket';
|
||||
import * as helpers from './helpers';
|
||||
import { expect } from 'chai';
|
||||
|
||||
const MESSAGE_EVENT = 'message';
|
||||
|
||||
describe('JsonSocket chaining', () => {
|
||||
it('should return the instance when subscribing to event', done => {
|
||||
@@ -13,7 +15,7 @@ describe('JsonSocket chaining', () => {
|
||||
expect(clientSocket.on(MESSAGE_EVENT, () => {})).to.be.instanceof(
|
||||
JsonSocket,
|
||||
);
|
||||
expect(clientSocket.on(CONNECT_EVENT, () => {})).to.deep.equal(
|
||||
expect(clientSocket.on(TcpEventsMap.CONNECT, () => {})).to.deep.equal(
|
||||
clientSocket,
|
||||
);
|
||||
expect(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { expect } from 'chai';
|
||||
import { Socket } from 'net';
|
||||
import * as sinon from 'sinon';
|
||||
import { ERROR_EVENT, MESSAGE_EVENT } from '../../constants';
|
||||
import { TcpEventsMap } from '../../events/tcp.events';
|
||||
import { JsonSocket } from '../../helpers/json-socket';
|
||||
|
||||
const MESSAGE_EVENT = 'message';
|
||||
|
||||
describe('JsonSocket message parsing', () => {
|
||||
const socket = new JsonSocket(new Socket());
|
||||
let messages: string[] = [];
|
||||
@@ -122,7 +124,7 @@ describe('JsonSocket message parsing', () => {
|
||||
expect(socket['buffer']).to.deep.equal('');
|
||||
});
|
||||
|
||||
it(`should emit ${ERROR_EVENT} event on socket`, () => {
|
||||
it(`should emit ${TcpEventsMap.ERROR} event on socket`, () => {
|
||||
const socketEmitSpy: sinon.SinonSpy<any, any> = sinon.spy(
|
||||
socket['socket'],
|
||||
'emit',
|
||||
@@ -131,12 +133,13 @@ describe('JsonSocket message parsing', () => {
|
||||
socket['onData'](packet);
|
||||
|
||||
try {
|
||||
expect(socketEmitSpy.calledOnceWithExactly(ERROR_EVENT, errorMsg)).to
|
||||
.be.true;
|
||||
expect(
|
||||
socketEmitSpy.calledOnceWithExactly(TcpEventsMap.ERROR, errorMsg),
|
||||
).to.be.true;
|
||||
} catch (err) {
|
||||
expect(
|
||||
socketEmitSpy.calledOnceWithExactly(
|
||||
ERROR_EVENT,
|
||||
TcpEventsMap.ERROR,
|
||||
errorMsgNodeBelowV20,
|
||||
),
|
||||
).to.be.true;
|
||||
@@ -169,7 +172,7 @@ describe('JsonSocket message parsing', () => {
|
||||
expect(socket['buffer']).to.deep.equal('');
|
||||
});
|
||||
|
||||
it(`should emit ${ERROR_EVENT} event on socket`, () => {
|
||||
it(`should emit ${TcpEventsMap.ERROR} event on socket`, () => {
|
||||
const socketEmitSpy: sinon.SinonSpy<any, any> = sinon.spy(
|
||||
socket['socket'],
|
||||
'emit',
|
||||
@@ -177,8 +180,9 @@ describe('JsonSocket message parsing', () => {
|
||||
|
||||
socket['onData'](packet);
|
||||
|
||||
expect(socketEmitSpy.calledOnceWithExactly(ERROR_EVENT, errorMsg)).to.be
|
||||
.true;
|
||||
expect(
|
||||
socketEmitSpy.calledOnceWithExactly(TcpEventsMap.ERROR, errorMsg),
|
||||
).to.be.true;
|
||||
socketEmitSpy.restore();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import { expect } from 'chai';
|
||||
import { join } from 'path';
|
||||
import { ReplaySubject, Subject, throwError } from 'rxjs';
|
||||
import * as sinon from 'sinon';
|
||||
import { CANCEL_EVENT } from '../../constants';
|
||||
import { InvalidGrpcPackageException } from '../../errors/invalid-grpc-package.exception';
|
||||
import { InvalidProtoDefinitionException } from '../../errors/invalid-proto-definition.exception';
|
||||
import * as grpcHelpers from '../../helpers/grpc-helpers';
|
||||
import { GrpcMethodStreamingType } from '../../index';
|
||||
import { ServerGrpc } from '../../server';
|
||||
|
||||
const CANCELLED_EVENT = 'cancelled';
|
||||
|
||||
class NoopLogger extends Logger {
|
||||
log(message: any, context?: string): void {}
|
||||
error(message: any, trace?: string, context?: string): void {}
|
||||
@@ -620,7 +621,7 @@ describe('ServerGrpc', () => {
|
||||
const fn = server.createRequestStreamMethod(handler, false);
|
||||
const call = {
|
||||
on: (event, callback) => {
|
||||
if (event !== CANCEL_EVENT) {
|
||||
if (event !== CANCELLED_EVENT) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
@@ -641,7 +642,7 @@ describe('ServerGrpc', () => {
|
||||
const fn = server.createRequestStreamMethod(handler, false);
|
||||
const call = {
|
||||
on: (event, callback) => {
|
||||
if (event !== CANCEL_EVENT) {
|
||||
if (event !== CANCELLED_EVENT) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
@@ -677,7 +678,7 @@ describe('ServerGrpc', () => {
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emitter.dispatchEvent(new Event(CANCEL_EVENT));
|
||||
emitter.dispatchEvent(new Event(CANCELLED_EVENT));
|
||||
};
|
||||
|
||||
const call = {
|
||||
@@ -732,7 +733,7 @@ describe('ServerGrpc', () => {
|
||||
const fn = server.createRequestStreamMethod(handler, true);
|
||||
const call = {
|
||||
on: (event, callback) => {
|
||||
if (event !== CANCEL_EVENT) {
|
||||
if (event !== CANCELLED_EVENT) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,9 +90,11 @@ describe('ServerKafka', () => {
|
||||
let subscribe: sinon.SinonSpy;
|
||||
let run: sinon.SinonSpy;
|
||||
let send: sinon.SinonSpy;
|
||||
let on: sinon.SinonSpy;
|
||||
let consumerStub: sinon.SinonStub;
|
||||
let producerStub: sinon.SinonStub;
|
||||
let client;
|
||||
let client: any;
|
||||
let untypedServer: any;
|
||||
|
||||
beforeEach(() => {
|
||||
server = new ServerKafka({});
|
||||
@@ -101,18 +103,46 @@ describe('ServerKafka', () => {
|
||||
subscribe = sinon.spy();
|
||||
run = sinon.spy();
|
||||
send = sinon.spy();
|
||||
on = sinon.spy();
|
||||
|
||||
consumerStub = sinon.stub(server as any, 'consumer').callsFake(() => {
|
||||
return {
|
||||
connect,
|
||||
subscribe,
|
||||
run,
|
||||
on,
|
||||
events: {
|
||||
GROUP_JOIN: 'consumer.group_join',
|
||||
HEARTBEAT: 'consumer.heartbeat',
|
||||
COMMIT_OFFSETS: 'consumer.commit_offsets',
|
||||
FETCH_START: 'consumer.fetch_start',
|
||||
FETCH: 'consumer.fetch',
|
||||
START_BATCH_PROCESS: 'consumer.start_batch_process',
|
||||
END_BATCH_PROCESS: 'consumer.end_batch_process',
|
||||
CONNECT: 'consumer.connect',
|
||||
DISCONNECT: 'consumer.disconnect',
|
||||
STOP: 'consumer.stop',
|
||||
CRASH: 'consumer.crash',
|
||||
REBALANCING: 'consumer.rebalancing',
|
||||
RECEIVED_UNSUBSCRIBED_TOPICS: 'consumer.received_unsubscribed_topics',
|
||||
REQUEST: 'consumer.network.request',
|
||||
REQUEST_TIMEOUT: 'consumer.network.request_timeout',
|
||||
REQUEST_QUEUE_SIZE: 'consumer.network.request_queue_size',
|
||||
},
|
||||
};
|
||||
});
|
||||
producerStub = sinon.stub(server as any, 'producer').callsFake(() => {
|
||||
return {
|
||||
connect,
|
||||
send,
|
||||
on,
|
||||
events: {
|
||||
CONNECT: 'producer.connect',
|
||||
DISCONNECT: 'producer.disconnect',
|
||||
REQUEST: 'producer.network.request',
|
||||
REQUEST_TIMEOUT: 'producer.network.request_timeout',
|
||||
REQUEST_QUEUE_SIZE: 'producer.network.request_queue_size',
|
||||
},
|
||||
};
|
||||
});
|
||||
client = {
|
||||
@@ -120,6 +150,8 @@ describe('ServerKafka', () => {
|
||||
producer: producerStub,
|
||||
};
|
||||
sinon.stub(server, 'createClient').callsFake(() => client);
|
||||
|
||||
untypedServer = server as any;
|
||||
});
|
||||
|
||||
describe('listen', () => {
|
||||
@@ -127,7 +159,8 @@ describe('ServerKafka', () => {
|
||||
bindEventsStub = sinon
|
||||
.stub(server, 'bindEvents')
|
||||
.callsFake(() => ({}) as any);
|
||||
await server.listen(callback);
|
||||
|
||||
await server.listen(err => console.log(err));
|
||||
expect(bindEventsStub.called).to.be.true;
|
||||
});
|
||||
it('should call callback', async () => {
|
||||
@@ -152,40 +185,40 @@ describe('ServerKafka', () => {
|
||||
const consumer = { disconnect: sinon.spy() };
|
||||
const producer = { disconnect: sinon.spy() };
|
||||
beforeEach(() => {
|
||||
(server as any).consumer = consumer;
|
||||
(server as any).producer = producer;
|
||||
untypedServer.consumer = consumer;
|
||||
untypedServer.producer = producer;
|
||||
});
|
||||
it('should close server', async () => {
|
||||
await server.close();
|
||||
|
||||
expect(consumer.disconnect.calledOnce).to.be.true;
|
||||
expect(producer.disconnect.calledOnce).to.be.true;
|
||||
expect((server as any).consumer).to.be.null;
|
||||
expect((server as any).producer).to.be.null;
|
||||
expect((server as any).client).to.be.null;
|
||||
expect(untypedServer.consumer).to.be.null;
|
||||
expect(untypedServer.producer).to.be.null;
|
||||
expect(untypedServer.client).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('bindEvents', () => {
|
||||
it('should not call subscribe nor run on consumer when there are no messageHandlers', async () => {
|
||||
(server as any).logger = new NoopLogger();
|
||||
untypedServer.logger = new NoopLogger();
|
||||
await server.listen(callback);
|
||||
await server.bindEvents((server as any).consumer);
|
||||
await server.bindEvents(untypedServer.consumer);
|
||||
expect(subscribe.called).to.be.false;
|
||||
expect(run.called).to.be.true;
|
||||
expect(connect.called).to.be.true;
|
||||
});
|
||||
it('should call subscribe and run on consumer when there are messageHandlers', async () => {
|
||||
(server as any).logger = new NoopLogger();
|
||||
untypedServer.logger = new NoopLogger();
|
||||
await server.listen(callback);
|
||||
|
||||
const pattern = 'test';
|
||||
const handler = sinon.spy();
|
||||
(server as any).messageHandlers = objectToMap({
|
||||
untypedServer.messageHandlers = objectToMap({
|
||||
[pattern]: handler,
|
||||
});
|
||||
|
||||
await server.bindEvents((server as any).consumer);
|
||||
await server.bindEvents(untypedServer.consumer);
|
||||
|
||||
expect(subscribe.called).to.be.true;
|
||||
expect(
|
||||
@@ -198,18 +231,18 @@ describe('ServerKafka', () => {
|
||||
expect(connect.called).to.be.true;
|
||||
});
|
||||
it('should call subscribe with options and run on consumer when there are messageHandlers', async () => {
|
||||
(server as any).logger = new NoopLogger();
|
||||
(server as any).options.subscribe = {};
|
||||
(server as any).options.subscribe.fromBeginning = true;
|
||||
untypedServer.logger = new NoopLogger();
|
||||
untypedServer.options.subscribe = {};
|
||||
untypedServer.options.subscribe.fromBeginning = true;
|
||||
await server.listen(callback);
|
||||
|
||||
const pattern = 'test';
|
||||
const handler = sinon.spy();
|
||||
(server as any).messageHandlers = objectToMap({
|
||||
untypedServer.messageHandlers = objectToMap({
|
||||
[pattern]: handler,
|
||||
});
|
||||
|
||||
await server.bindEvents((server as any).consumer);
|
||||
await server.bindEvents(untypedServer.consumer);
|
||||
|
||||
expect(subscribe.called).to.be.true;
|
||||
expect(
|
||||
@@ -337,7 +370,7 @@ describe('ServerKafka', () => {
|
||||
it('should call "handleEvent" if correlation identifier and reply topic are present but the handler is of type eventHandler', async () => {
|
||||
const handler = sinon.spy();
|
||||
(handler as any).isEventHandler = true;
|
||||
(server as any).messageHandlers = objectToMap({
|
||||
untypedServer.messageHandlers = objectToMap({
|
||||
[topic]: handler,
|
||||
});
|
||||
const handleEventSpy = sinon.spy(server, 'handleEvent');
|
||||
@@ -348,7 +381,7 @@ describe('ServerKafka', () => {
|
||||
it('should NOT call "handleEvent" if correlation identifier and reply topic are present but the handler is not of type eventHandler', async () => {
|
||||
const handler = sinon.spy();
|
||||
(handler as any).isEventHandler = false;
|
||||
(server as any).messageHandlers = objectToMap({
|
||||
untypedServer.messageHandlers = objectToMap({
|
||||
[topic]: handler,
|
||||
});
|
||||
const handleEventSpy = sinon.spy(server, 'handleEvent');
|
||||
@@ -368,7 +401,7 @@ describe('ServerKafka', () => {
|
||||
|
||||
it(`should call handler with expected arguments`, async () => {
|
||||
const handler = sinon.spy();
|
||||
(server as any).messageHandlers = objectToMap({
|
||||
untypedServer.messageHandlers = objectToMap({
|
||||
[topic]: handler,
|
||||
});
|
||||
|
||||
|
||||
@@ -30,13 +30,25 @@ describe('ServerMqtt', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onSpy.getCall(0).args[0]).to.be.equal('error');
|
||||
});
|
||||
it('should bind "message" event to handler', () => {
|
||||
it('should bind "reconnect" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onSpy.getCall(1).args[0]).to.be.equal('message');
|
||||
expect(onSpy.getCall(1).args[0]).to.be.equal('reconnect');
|
||||
});
|
||||
it('should bind "disconnect" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onSpy.getCall(2).args[0]).to.be.equal('disconnect');
|
||||
});
|
||||
it('should bind "close" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onSpy.getCall(3).args[0]).to.be.equal('close');
|
||||
});
|
||||
it('should bind "connect" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onSpy.getCall(2).args[0]).to.be.equal('connect');
|
||||
expect(onSpy.getCall(4).args[0]).to.be.equal('connect');
|
||||
});
|
||||
it('should bind "message" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onSpy.getCall(5).args[0]).to.be.equal('message');
|
||||
});
|
||||
describe('when "start" throws an exception', () => {
|
||||
it('should call callback with a thrown error as an argument', () => {
|
||||
|
||||
@@ -233,7 +233,7 @@ describe('ServerRedis', () => {
|
||||
describe('createRetryStrategy', () => {
|
||||
describe('when is terminated', () => {
|
||||
it('should return undefined', () => {
|
||||
(server as any).isExplicitlyTerminated = true;
|
||||
(server as any).isManuallyClosed = true;
|
||||
const result = server.createRetryStrategy(0);
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
@@ -257,7 +257,7 @@ describe('ServerRedis', () => {
|
||||
describe('otherwise', () => {
|
||||
it('should return delay (ms)', () => {
|
||||
(server as any).options = {};
|
||||
(server as any).isExplicitlyTerminated = false;
|
||||
(server as any).isManuallyClosed = false;
|
||||
(server as any).options.retryAttempts = 3;
|
||||
(server as any).options.retryDelay = 3;
|
||||
const result = server.createRetryStrategy(2);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { assert, expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { NO_MESSAGE_HANDLER } from '../../constants';
|
||||
import { BaseRpcContext } from '../../ctx-host/base-rpc.context';
|
||||
import { ServerRMQ } from '../../server/server-rmq';
|
||||
import { RmqContext } from '../../ctx-host';
|
||||
import { ServerRMQ } from '../../server/server-rmq';
|
||||
|
||||
describe('ServerRMQ', () => {
|
||||
let server: ServerRMQ;
|
||||
@@ -15,19 +14,6 @@ describe('ServerRMQ', () => {
|
||||
server = new ServerRMQ({});
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it(`should fallback to queueOptions.noAssert when 'noAssert' is undefined`, () => {
|
||||
const queueOptions = {
|
||||
noAssert: true,
|
||||
};
|
||||
const instance = new ServerRMQ({
|
||||
queueOptions,
|
||||
});
|
||||
|
||||
expect(instance).property('noAssert').to.eq(queueOptions.noAssert);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listen', () => {
|
||||
let createClient: sinon.SinonStub;
|
||||
let onStub: sinon.SinonStub;
|
||||
@@ -47,6 +33,7 @@ describe('ServerRMQ', () => {
|
||||
|
||||
client = {
|
||||
on: onStub,
|
||||
once: onStub,
|
||||
createChannel: createChannelStub,
|
||||
};
|
||||
createClient = sinon.stub(server, 'createClient').callsFake(() => client);
|
||||
@@ -59,17 +46,17 @@ describe('ServerRMQ', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(createClient.called).to.be.true;
|
||||
});
|
||||
it('should bind "connect" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
it('should bind "connect" event to handler', async () => {
|
||||
await server.listen(callbackSpy);
|
||||
expect(onStub.getCall(0).args[0]).to.be.equal('connect');
|
||||
});
|
||||
it('should bind "disconnect" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onStub.getCall(1).args[0]).to.be.equal('disconnect');
|
||||
it('should bind "disconnected" event to handler', async () => {
|
||||
await server.listen(callbackSpy);
|
||||
expect(onStub.getCall(2).args[0]).to.be.equal('disconnect');
|
||||
});
|
||||
it('should bind "connectFailed" event to handler', () => {
|
||||
server.listen(callbackSpy);
|
||||
expect(onStub.getCall(2).args[0]).to.be.equal('connectFailed');
|
||||
it('should bind "connectFailed" event to handler', async () => {
|
||||
await server.listen(callbackSpy);
|
||||
expect(onStub.getCall(3).args[0]).to.be.equal('connectFailed');
|
||||
});
|
||||
describe('when "start" throws an exception', () => {
|
||||
it('should call callback with a thrown error as an argument', () => {
|
||||
@@ -201,8 +188,10 @@ describe('ServerRMQ', () => {
|
||||
beforeEach(() => {
|
||||
(server as any)['queue'] = queue;
|
||||
(server as any)['queueOptions'] = queueOptions;
|
||||
(server as any)['isGlobalPrefetchCount'] = isGlobalPrefetchCount;
|
||||
(server as any)['prefetchCount'] = prefetchCount;
|
||||
(server as any)['options'] = {
|
||||
isGlobalPrefetchCount,
|
||||
prefetchCount,
|
||||
};
|
||||
|
||||
channel = {
|
||||
assertQueue: sinon.spy(() => ({})),
|
||||
@@ -217,7 +206,10 @@ describe('ServerRMQ', () => {
|
||||
expect(channel.assertQueue.calledWith(queue, queueOptions)).to.be.true;
|
||||
});
|
||||
it('should not call "assertQueue" when noAssert is true', async () => {
|
||||
server['noAssert' as any] = true;
|
||||
server['options' as any] = {
|
||||
...(server as any)['options'],
|
||||
noAssert: true,
|
||||
};
|
||||
|
||||
await server.setupChannel(channel, () => null);
|
||||
expect(channel.assertQueue.called).not.to.be.true;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { expect } from 'chai';
|
||||
import { lastValueFrom, Observable, of, throwError as _throw } from 'rxjs';
|
||||
import { throwError as _throw, lastValueFrom, Observable, of } from 'rxjs';
|
||||
import * as sinon from 'sinon';
|
||||
import { Server } from '../../server/server';
|
||||
|
||||
class TestServer extends Server {
|
||||
public on<
|
||||
EventKey extends string = string,
|
||||
EventCallback extends Function = Function,
|
||||
>(event: EventKey, callback: EventCallback) {}
|
||||
public unwrap<T>(): T {
|
||||
return null;
|
||||
}
|
||||
public listen(callback: () => void) {}
|
||||
public close() {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user