@nestjs/microservices
Version:
Nest - modern, fast, powerful node.js web framework (@microservices)
228 lines (227 loc) • 9.46 kB
JavaScript
import { CONNECTION_FAILED_MESSAGE, MQTT_DEFAULT_URL, MQTT_SEPARATOR, MQTT_WILDCARD_ALL, MQTT_WILDCARD_SINGLE, NO_MESSAGE_HANDLER, } from '../constants.js';
import { MqttContext } from '../ctx-host/mqtt.context.js';
import { Transport } from '../enums/index.js';
import { MqttRecord } from '../record-builders/mqtt.record-builder.js';
import { MqttRecordSerializer } from '../serializers/mqtt-record.serializer.js';
import { Server } from './server.js';
import { isUndefined } from '@nestjs/common/internal';
const INFINITE_CONNECTION_ATTEMPTS = -1;
/**
* @publicApi
*/
export class ServerMqtt extends Server {
options;
transportId = Transport.MQTT;
url;
mqttClient;
connectionAttempts = 0;
hasConnected = false;
pendingEventListeners = [];
constructor(options) {
super();
this.options = options;
this.url = this.getOptionsProp(options, 'url', MQTT_DEFAULT_URL);
this.initializeSerializer(options);
this.initializeDeserializer(options);
}
async listen(callback) {
try {
// Re-arm the attempts limit so that a restarted server gets a fresh
// startup window.
this.connectionAttempts = 0;
this.hasConnected = false;
this.mqttClient = await this.createMqttClient();
this.start(callback);
}
catch (err) {
callback(err);
}
}
start(callback) {
// The bootstrap callback must settle exactly once, no matter whether the
// connection succeeds or the attempts limit is exhausted first.
let listenCallback = callback;
const settleListenCallback = (err) => {
const cb = listenCallback;
listenCallback = undefined;
isUndefined(err) ? cb?.() : cb?.(err);
};
this.registerErrorListener(this.mqttClient, settleListenCallback);
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.once("connect" /* MqttEventsMap.CONNECT */, () => settleListenCallback());
}
bindEvents(mqttClient) {
mqttClient.on('message', this.getMessageHandler(mqttClient).bind(this));
const registeredPatterns = [...this.messageHandlers.keys()];
registeredPatterns.forEach(pattern => {
const handler = this.messageHandlers.get(pattern);
const { isEventHandler, extras } = handler;
const globalSubscribeOptions = this.getOptionsProp(this.options, 'subscribeOptions');
const subscribeOptions = extras?.qos !== undefined
? { ...globalSubscribeOptions, qos: extras.qos }
: globalSubscribeOptions;
mqttClient.subscribe(isEventHandler ? pattern : this.getRequestPattern(pattern), subscribeOptions);
});
}
close() {
this.mqttClient && this.mqttClient.end();
this.pendingEventListeners = [];
}
async createMqttClient() {
const mqttPackage = await this.loadPackage('mqtt', ServerMqtt.name, () => import('mqtt'));
return mqttPackage.connect(this.url, this.options);
}
getMessageHandler(pub) {
return async (channel, buffer, originalPacket) => this.handleMessage(channel, buffer, pub, originalPacket).catch(err => this.handleError(err));
}
async handleMessage(channel, buffer, pub, originalPacket) {
const rawPacket = this.parseMessage(buffer.toString());
const packet = await this.deserializer.deserialize(rawPacket, { channel });
const mqttContext = new MqttContext([channel, originalPacket]);
if (isUndefined(packet.id)) {
return this.handleEvent(channel, packet, mqttContext);
}
const publish = this.getPublisher(pub, mqttContext, packet.id);
const handler = this.getHandlerByPattern(channel);
if (!handler) {
const status = 'error';
const noHandlerPacket = {
id: packet.id,
status,
err: NO_MESSAGE_HANDLER,
};
return publish(noHandlerPacket);
}
return this.handleRequest(mqttContext, async () => this.transformToObservable(await handler(packet.data, mqttContext)), publish);
}
getPublisher(client, context, id) {
return (response) => {
Object.assign(response, { id });
const options = response.response instanceof MqttRecord
? response.response.options
: {};
const outgoingResponse = this.serializer.serialize(response);
return client.publish(this.getReplyPattern(context.getTopic()), outgoingResponse, options);
};
}
parseMessage(content) {
try {
return JSON.parse(content);
}
catch (e) {
return content;
}
}
matchMqttPattern(pattern, topic) {
const patternSegments = pattern.split(MQTT_SEPARATOR);
const topicSegments = topic.split(MQTT_SEPARATOR);
const patternSegmentsLength = patternSegments.length;
const topicSegmentsLength = topicSegments.length;
const lastIndex = patternSegmentsLength - 1;
for (let i = 0; i < patternSegmentsLength; i++) {
const currentPattern = patternSegments[i];
const patternChar = currentPattern[0];
const currentTopic = topicSegments[i];
if (!currentTopic && !currentPattern) {
continue;
}
if (!currentTopic && currentPattern !== MQTT_WILDCARD_ALL) {
return false;
}
if (patternChar === MQTT_WILDCARD_ALL) {
return i === lastIndex;
}
if (patternChar !== MQTT_WILDCARD_SINGLE &&
currentPattern !== currentTopic) {
return false;
}
}
return patternSegmentsLength === topicSegmentsLength;
}
getHandlerByPattern(pattern) {
const route = this.getRouteFromPattern(pattern);
if (this.messageHandlers.has(route)) {
return this.messageHandlers.get(route) || null;
}
for (const [key, value] of this.messageHandlers) {
const keyWithoutSharedPrefix = this.removeHandlerKeySharedPrefix(key);
if (this.matchMqttPattern(keyWithoutSharedPrefix, route)) {
return value;
}
}
return null;
}
removeHandlerKeySharedPrefix(handlerKey) {
return handlerKey && handlerKey.startsWith('$share')
? handlerKey.split('/').slice(2).join('/')
: handlerKey;
}
getRequestPattern(pattern) {
return pattern;
}
getReplyPattern(pattern) {
return `${pattern}/reply`;
}
registerErrorListener(client, callback) {
client.on("error" /* MqttEventsMap.ERROR */, (err) => {
this.logger.error(err);
const maxConnectionAttempts = this.getOptionsProp(this.options, 'maxConnectionAttempts', INFINITE_CONNECTION_ATTEMPTS);
// The limit bounds the initial connection only. Once the server has
// connected, mqtt.js reconnects indefinitely and errors are just logged,
// so a running microservice is never torn down by this option.
if (maxConnectionAttempts === INFINITE_CONNECTION_ATTEMPTS ||
this.hasConnected) {
return;
}
if (++this.connectionAttempts === maxConnectionAttempts) {
this.close();
callback?.(err ?? new Error(CONNECTION_FAILED_MESSAGE));
}
});
}
registerReconnectListener(client) {
client.on("reconnect" /* MqttEventsMap.RECONNECT */, () => {
this._status$.next("reconnecting" /* MqttStatus.RECONNECTING */);
this.logger.log('MQTT connection lost. Trying to reconnect...');
});
}
registerDisconnectListener(client) {
client.on("disconnect" /* MqttEventsMap.DISCONNECT */, () => {
this._status$.next("disconnected" /* MqttStatus.DISCONNECTED */);
});
}
registerCloseListener(client) {
client.on("close" /* MqttEventsMap.CLOSE */, () => {
this._status$.next("closed" /* MqttStatus.CLOSED */);
});
}
registerConnectListener(client) {
client.on("connect" /* MqttEventsMap.CONNECT */, () => {
this.hasConnected = true;
this._status$.next("connected" /* MqttStatus.CONNECTED */);
});
}
unwrap() {
if (!this.mqttClient) {
throw new Error('Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.');
}
return this.mqttClient;
}
on(event, callback) {
if (this.mqttClient) {
this.mqttClient.on(event, callback);
}
else {
this.pendingEventListeners.push({ event, callback });
}
}
initializeSerializer(options) {
this.serializer = options?.serializer ?? new MqttRecordSerializer();
}
}