UNPKG

@nestjs/microservices

Version:

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

258 lines (257 loc) • 11 kB
import * as net from 'net'; import { isObservable, of, ReplaySubject } from 'rxjs'; import { switchMap, takeUntil } from 'rxjs/operators'; import { createServer as tlsCreateServer } from 'tls'; import { ECONNREFUSED, NO_MESSAGE_HANDLER, TCP_DEFAULT_HOST, TCP_DEFAULT_PORT, } from '../constants.js'; import { getHandlerObservableNotifier } from '../context/rpc-context-creator.js'; import { TcpContext } from '../ctx-host/tcp.context.js'; import { Transport } from '../enums/index.js'; import { JsonSocket } from '../helpers/index.js'; import { InvalidTcpDataReceptionException } from '../errors/invalid-tcp-data-reception.exception.js'; import { Server } from './server.js'; import { isUndefined } from '@nestjs/common/internal'; /** * @publicApi */ export class ServerTCP extends Server { options; transportId = Transport.TCP; server; port; host; socketClass; maxBufferSize; incompleteMessageTimeout; maxSendBufferSize; drainTimeout; isManuallyTerminated = false; retryAttemptsCount = 0; retryTimer; tlsOptions; pendingEventListeners = []; /** * Sockets accepted by this server that are still open. "net.Server#close" * only stops the server from accepting new connections, so these are tracked * separately and torn down on "close" - otherwise the process outlives the * shutdown and handlers keep running on already established connections. */ openSockets = new Set(); /** * Notifiers that emit once a socket bound by "bindHandler" can no longer be * written to. They replay, so that a response stream that only starts after * the peer has gone ends right away as well. */ disconnectNotifiers = new WeakMap(); constructor(options) { super(); this.options = options; 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.drainTimeout = this.getOptionsProp(options, 'drainTimeout'); this.init(); this.initializeSerializer(options); this.initializeDeserializer(options); } listen(callback) { // The once('error') listener outlives a successful listen, so a later // error must find the callback already settled. let listenCallback = callback; const settleListenCallback = (err) => { const cb = listenCallback; listenCallback = undefined; isUndefined(err) ? cb?.() : cb?.(err); }; this.server.once("error" /* TcpEventsMap.ERROR */, err => { if (!listenCallback) { return; } this._status$.next("disconnected" /* TcpStatus.DISCONNECTED */); settleListenCallback(err); }); this.server.listen(this.port, this.host, () => settleListenCallback()); } close() { this.isManuallyTerminated = true; if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = undefined; } this.server.close(); this.closeOpenSockets(); this.pendingEventListeners = []; } bindHandler(socket) { this.trackOpenSocket(socket); const readSocket = this.getSocketInstance(socket); this.trackDisconnect(readSocket); readSocket.on('message', (msg) => this.handleMessage(readSocket, msg).catch(err => this.handleError(err))); readSocket.on("error" /* TcpEventsMap.ERROR */, err => { const invalidError = new InvalidTcpDataReceptionException(err); this.handleError(invalidError); }); } async handleMessage(socket, rawMessage) { const packet = await this.deserializer.deserialize(rawMessage); const pattern = this.getPatternAsString(packet.pattern); const tcpContext = new TcpContext([socket, pattern, packet.metadata]); if (isUndefined(packet.id)) { return this.handleEvent(pattern, packet, tcpContext); } this.assertValidPacketId(packet.id); const handler = this.getHandlerByPattern(pattern); if (!handler) { const status = 'error'; const noHandlerPacket = this.serializer.serialize({ id: packet.id, status, err: NO_MESSAGE_HANDLER, }); return socket.sendMessage(noHandlerPacket); } this.warnIfEventHandlerReplies(handler, pattern); return this.handleRequest(tcpContext, async () => this.endOnDisconnect(await handler(packet.data, tcpContext), tcpContext, socket), data => { Object.assign(data, { id: packet.id }); const outgoingResponse = this.serializer.serialize(data); socket.sendMessage(outgoingResponse); }); } handleClose() { if (this.retryTimer) { return this.retryTimer; } if (this.isManuallyTerminated || !this.getOptionsProp(this.options, 'retryAttempts') || this.retryAttemptsCount >= this.getOptionsProp(this.options, 'retryAttempts', 0)) { return undefined; } ++this.retryAttemptsCount; this.retryTimer = setTimeout(() => { this.retryTimer = undefined; if (!this.isManuallyTerminated) { this.server.listen(this.port, this.host); } }, this.getOptionsProp(this.options, 'retryDelay', 0)); return this.retryTimer; } unwrap() { if (!this.server) { throw new Error('Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.'); } return this.server; } on(event, callback) { if (this.server) { this.server.on(event, callback); } else { this.pendingEventListeners.push({ event, callback }); } } init() { if (this.tlsOptions) { // TLS enabled, use tls server this.server = tlsCreateServer(this.tlsOptions, this.bindHandler.bind(this)); // "bindHandler" only runs once the TLS handshake is over. The raw socket // is tracked from the moment it is accepted, so that "close" also tears // down connections that are still in the middle of their handshake. this.server.on('connection', socket => this.trackOpenSocket(socket)); } else { // TLS disabled, use net server this.server = net.createServer(this.bindHandler.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 = []; } registerListeningListener(socket) { socket.on("listening" /* TcpEventsMap.LISTENING */, () => { this._status$.next("connected" /* TcpStatus.CONNECTED */); }); } registerErrorListener(socket) { socket.on("error" /* TcpEventsMap.ERROR */, err => { if ('code' in err && err.code === ECONNREFUSED) { this._status$.next("disconnected" /* TcpStatus.DISCONNECTED */); } this.handleError(err); }); } registerCloseListener(socket) { socket.on("close" /* TcpEventsMap.CLOSE */, () => { this._status$.next("disconnected" /* TcpStatus.DISCONNECTED */); this.handleClose(); }); } /** * Keeps a reference to an accepted socket so that it can be destroyed when * the server is closed, and drops it again once it closes on its own. */ trackOpenSocket(socket) { if (!socket) { return; } this.openSockets.add(socket); socket.on("close" /* TcpEventsMap.CLOSE */, () => this.openSockets.delete(socket)); } /** * Destroys every socket still open. Called on shutdown so that "close" does * not leave the process alive, and so that no further messages are dispatched * to handlers over connections established before the shutdown. */ closeOpenSockets() { this.openSockets.forEach(socket => socket.destroy()); this.openSockets.clear(); } /** * Notes when the socket can no longer be written to: "TcpSocket" drops every * message once the socket has closed or errored. */ trackDisconnect(socket) { const disconnect$ = new ReplaySubject(1); socket.on("close" /* TcpEventsMap.CLOSE */, () => disconnect$.next()); socket.on("error" /* TcpEventsMap.ERROR */, () => disconnect$.next()); this.disconnectNotifiers.set(socket, disconnect$); } /** * Turns a handler result into the response stream. As for HTTP, the stream * ends once the peer disconnects only if the handler itself returned (or * resolved to) an Observable: any other handler, and the interceptors * around it, run to completion. */ endOnDisconnect(result, context, socket) { const response$ = this.transformToObservable(result); const disconnect$ = this.disconnectNotifiers.get(socket); if (!isObservable(result) || !disconnect$) { return response$; } // Without interceptors, "result" is what the handler returned const handlerObservable$ = getHandlerObservableNotifier(context) ?? of(null); return response$.pipe(takeUntil(handlerObservable$.pipe(switchMap(() => disconnect$)))); } getSocketInstance(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 || this.drainTimeout !== undefined; if (hasJsonSocketOptions && this.socketClass === JsonSocket) { return new this.socketClass(socket, { maxBufferSize: this.maxBufferSize, incompleteMessageTimeout: this.incompleteMessageTimeout, maxSendBufferSize: this.maxSendBufferSize, drainTimeout: this.drainTimeout, }); } return new this.socketClass(socket); } }