UNPKG

@nestjs/microservices

Version:

Nest - modern, fast, powerful node.js web framework (@microservices)

239 lines (238 loc) • 9.15 kB
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); } }