@nestjs/microservices
Version:
Nest - modern, fast, powerful node.js web framework (@microservices)
258 lines (257 loc) • 11 kB
JavaScript
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);
}
}