@nestjs/microservices
Version:
Nest - modern, fast, powerful node.js web framework (@microservices)
239 lines (238 loc) • 9.15 kB
JavaScript
import { Logger } from '@nestjs/common';
import * as net from 'net';
import { EmptyError, fromEvent, lastValueFrom, merge } from 'rxjs';
import { map, share, take, tap } from 'rxjs/operators';
import { connect as tlsConnect } from 'tls';
import { ECONNREFUSED, TCP_DEFAULT_HOST, TCP_DEFAULT_PORT, } from '../constants.js';
import { JsonSocket } from '../helpers/index.js';
import { ClientProxy } from './client-proxy.js';
/**
* @publicApi
*/
export class ClientTCP extends ClientProxy {
logger = new Logger(ClientTCP.name);
port;
host;
socketClass;
tlsOptions;
maxBufferSize;
incompleteMessageTimeout;
maxSendBufferSize;
socket = null;
connectedSockets = new WeakSet();
connectionPromise = null;
pendingEventListeners = [];
constructor(options) {
super();
this.port = this.getOptionsProp(options, 'port', TCP_DEFAULT_PORT);
this.host = this.getOptionsProp(options, 'host', TCP_DEFAULT_HOST);
this.socketClass = this.getOptionsProp(options, 'socketClass', JsonSocket);
this.tlsOptions = this.getOptionsProp(options, 'tlsOptions');
this.maxBufferSize = this.getOptionsProp(options, 'maxBufferSize');
this.incompleteMessageTimeout = this.getOptionsProp(options, 'incompleteMessageTimeout');
this.maxSendBufferSize = this.getOptionsProp(options, 'maxSendBufferSize');
this.initializeSerializer(options);
this.initializeDeserializer(options);
}
connect() {
if (this.connectionPromise) {
return this.connectionPromise;
}
const socket = this.createSocket();
this.socket = socket;
this.registerConnectListener(socket);
this.registerCloseListener(socket);
this.registerErrorListener(socket);
this.pendingEventListeners.forEach(({ event, callback }) => socket.on(event, callback));
const connected$ = this.connect$(socket.netSocket, "error" /* TcpEventsMap.ERROR */, this.tlsOptions ? 'secureConnect' : "connect" /* TcpEventsMap.CONNECT */);
// A TLS socket destroyed during the handshake (see `close()`) closes
// without an "error", which would otherwise leave this pending.
const source$ = (this.tlsOptions
? merge(connected$, fromEvent(socket.netSocket, "close" /* TcpEventsMap.CLOSE */).pipe(map(() => {
throw new Error('Connection closed');
}))).pipe(take(1))
: connected$).pipe(tap(() => {
this.connectedSockets.add(socket);
// A socket replaced by a newer `connect()` call (`close()` followed by
// `connect()` while it was still connecting) still finishes connecting,
// so its listener must stay on it instead of `this.socket`.
socket.on('message', (buffer) => this.handleResponse(buffer));
}), share());
// For TLS connections, the connection is initiated when the socket is created
if (!this.tlsOptions) {
socket.connect(this.port, this.host);
}
this.connectionPromise = lastValueFrom(source$).catch(err => {
if (err instanceof EmptyError) {
return;
}
throw err;
});
return this.connectionPromise;
}
async handleResponse(buffer) {
let incomingResponse;
try {
incomingResponse = await this.deserializer.deserialize(buffer);
}
catch (error) {
// Only the deserializer knows the id, so no request can be failed here.
// Nobody awaits this listener, so a rejection would crash the process.
this.logger.error(`Dropped a response that the deserializer failed on: ${error}`, error?.stack);
return;
}
const { err, response, isDisposed, id } = incomingResponse;
const callback = this.routingMap.get(id);
if (!callback) {
return undefined;
}
if (isDisposed || err) {
return callback({
err,
response,
isDisposed: true,
});
}
callback({
err,
response,
});
}
createSocket() {
let socket;
/**
* TLS enabled, "upgrade" the TCP Socket to TLS
*/
if (this.tlsOptions) {
socket = tlsConnect({
...this.tlsOptions,
port: this.port,
host: this.host,
});
}
else {
socket = new net.Socket();
}
// Pass the framing options only if socketClass is JsonSocket
// For custom socket classes, users should handle them in their own implementation
const hasJsonSocketOptions = this.maxBufferSize !== undefined ||
this.incompleteMessageTimeout !== undefined ||
this.maxSendBufferSize !== undefined;
if (hasJsonSocketOptions && this.socketClass === JsonSocket) {
return new this.socketClass(socket, {
maxBufferSize: this.maxBufferSize,
incompleteMessageTimeout: this.incompleteMessageTimeout,
maxSendBufferSize: this.maxSendBufferSize,
});
}
return new this.socketClass(socket);
}
close() {
if (this.socket) {
// `end()` waits for a pending TLS handshake, which may never finish
if (this.tlsOptions && !this.connectedSockets.has(this.socket)) {
this.socket.netSocket.destroy();
}
else {
this.socket.end();
}
}
this.handleClose();
this.pendingEventListeners = [];
}
registerConnectListener(socket) {
socket.on(this.tlsOptions ? 'secureConnect' : "connect" /* TcpEventsMap.CONNECT */, () => {
if (this.isReplacedSocket(socket)) {
return;
}
this._status$.next("connected" /* TcpStatus.CONNECTED */);
});
}
registerErrorListener(socket) {
socket.on("error" /* TcpEventsMap.ERROR */, err => {
if (err.code !== ECONNREFUSED) {
this.handleError(err);
}
else if (!this.isReplacedSocket(socket)) {
this._status$.next("disconnected" /* TcpStatus.DISCONNECTED */);
}
});
}
registerCloseListener(socket) {
socket.on("close" /* TcpEventsMap.CLOSE */, () => {
if (this.isReplacedSocket(socket)) {
return;
}
this._status$.next("disconnected" /* TcpStatus.DISCONNECTED */);
this.handleClose();
});
}
/**
* Whether a newer `connect()` call has replaced `socket` (e.g., `close()`
* followed by `connect()` before it finished closing), so its late events
* must not tear down or report on the newer connection.
*/
isReplacedSocket(socket) {
return this.socket !== null && this.socket !== socket;
}
handleError(err) {
this.logger.error(err);
}
handleClose() {
this.socket = null;
this.connectionPromise = null;
if (this.routingMap.size > 0) {
const err = new Error('Connection closed');
const callbacks = [...this.routingMap.values()];
this.routingMap.clear();
for (const callback of callbacks) {
try {
callback({ err });
}
catch (callbackErr) {
// A failing callback must not keep the remaining requests pending
// nor prevent the connection from being closed.
this.logger.error(callbackErr);
}
}
}
}
on(event, callback) {
// Kept until `close()`, so the sockets created later (e.g., after the
// server dropped the connection) get it as well
this.pendingEventListeners.push({ event, callback });
if (this.socket) {
this.socket.on(event, callback);
}
}
unwrap() {
if (!this.socket) {
throw new Error('Not initialized. Please call the "connect" method first.');
}
return this.socket.netSocket;
}
publish(partialPacket, callback) {
let cleanup = () => { };
try {
const packet = this.assignPacketId(partialPacket);
const serializedPacket = this.serializer.serialize(packet);
this.routingMap.set(packet.id, callback);
cleanup = () => this.routingMap.delete(packet.id);
this.socket.sendMessage(serializedPacket);
return cleanup;
}
catch (err) {
cleanup();
callback({ err });
return () => { };
}
}
async dispatchEvent(packet) {
const pattern = this.normalizePattern(packet.pattern);
const serializedPacket = this.serializer.serialize({
...packet,
pattern,
});
return this.socket.sendMessage(serializedPacket);
}
}