@nebulae/backend-node-tools
Version:
Tools collection for NebulaE Microservices Node Backends
202 lines (191 loc) • 5.56 kB
JavaScript
'use strict';
const MQTT = require('async-mqtt');
const Rx = require('rxjs');
const uuidv4 = require('uuid/v4');
const os = require('os');
const { ConsoleLogger } = require('../log');
const {
switchMap,
filter,
map,
timeout,
first,
mapTo,
mergeMap,
reduce
} = require('rxjs/operators');
class MqttBroker {
constructor({ mqttServerUrl, replyTimeout, topicPrefix = '', connOps = {} }) {
this.mqttServerUrl = mqttServerUrl;
this.senderId = os.hostname();
this.replyTimeout = replyTimeout;
this.topicPrefix = topicPrefix;
/**
* Rx Subject for incoming messages
*/
this.incomingMessages$ = new Rx.BehaviorSubject();
/**
* MQTT Client
*/
this.listeningTopics = [];
this.mqttClient = MQTT.connect(this.mqttServerUrl, connOps);
this.mqttClient.on('connect', () => ConsoleLogger.i(`Mqtt client connected`));
this.mqttClient.on('message', (topic, message) => {
const envelope = JSON.parse(message);
// message is Buffer
this.incomingMessages$.next({
topic: topic,
id: envelope.id,
type: envelope.type,
data: envelope.data,
attributes: envelope.attributes,
correlationId: envelope.attributes.correlationId
});
});
}
/**
* Sends a Message to the given topic
* @param {string} topic topic to publish
* @param {string} type message type
* @param {Object} message payload
* @param {Object} ops {correlationId, messageId}
*/
send$(topic, type, payload, ops = {}) {
return this.publish$(topic, type, payload, ops);
}
/**
* Sends a Message to the given topic and wait for a reply
* @param {string} topic send topic
* @param {string} responseTopic response topic
* @param {string} type message(payload) type
* @param {Object} message payload
* @param {number} timeout wait timeout millis
* @param {boolean} ignoreSelfEvents ignore messages comming from this clien
* @param {Object} ops {correlationId, messageId}
*
* Returns an Observable that resolves the message response
*/
sendAndGetReply$(
topic,
responseTopic,
type,
payload,
timeout = this.replyTimeout,
ignoreSelfEvents = true,
ops = {}
) {
return this.send$(topic, type, payload, ops).pipe(
switchMap(messageId =>
this.getMessageReply$(
responseTopic,
messageId,
timeout,
ignoreSelfEvents
)
)
);
}
/**
* Returns an observable that waits for the message response or throws an error if timeout is exceded
* @param {string} topic response topic
* @param {string} correlationId
* @param {number} timeout
*/
getMessageReply$(
topic,
correlationId,
timeout = this.replyTimeout,
ignoreSelfEvents = true
) {
return this.configMessageListener$([topic]).pipe(
switchMap(() =>
this.incomingMessages$.pipe(
filter(msg => msg),
filter(
msg =>
!ignoreSelfEvents || msg.attributes.senderId !== this.senderId
),
filter(msg => msg && msg.correlationId === correlationId),
map(msg => msg.data),
timeout(timeout),
first()
)
)
);
}
/**
* Returns an Observable that will emit any incoming message
* @param {string[] ?} topics topic to listen
* @param {string[] ?} types message types to listen
* @param {boolean ?} ignoreSelfEvents
*/
getMessageListener$(topics = [], types = [], ignoreSelfEvents = true) {
return this.configMessageListener$(topics).pipe(
switchMap(
() => this.incomingMessages$.pipe(
filter(msg => msg),
filter(
msg => !ignoreSelfEvents || msg.attributes.senderId !== this.senderId
),
filter(msg => topics.length === 0 || topics.indexOf(msg.topic) > -1),
filter(msg => types.length === 0 || types.indexOf(msg.type) > -1)
)
)
);
}
/**
* Publish data throught a topic
* Returns an Observable that resolves to the sent message ID
* @param {string} topicName
* @param {string} type message(data) type
* @param {Object} data
* @param {Object} ops {correlationId, messageId}
*/
publish$(topicName, type, data, { correlationId, messageId } = {}) {
const uuid = messageId || uuidv4();
const dataBuffer = JSON.stringify({
id: uuid,
type,
data,
attributes: {
senderId: this.senderId,
correlationId
}
});
return Rx.defer(() =>
this.mqttClient.publish(`${topicName}`, dataBuffer, { qos: 0 })
).pipe(mapTo(uuid));
}
/**
* Config the broker to listen to several topics
* Returns an observable that resolves to a stream of subscribed topics
* @param {Array} topics topics to listen
*/
configMessageListener$(topics) {
return Rx.from(topics).pipe(
filter(topic => this.listeningTopics.indexOf(topic) === -1),
mergeMap(topic =>
Rx.defer(() => this.mqttClient.subscribe(`${this.topicPrefix}${topic}`)).pipe(
map(() => {
this.listeningTopics.push(topic);
return topic;
})
)
),
reduce((acc, topic) => {
acc.push(topic);
return acc;
}, [])
);
}
/**
* Disconnect the broker and return an observable that completes when disconnected
*/
disconnectBroker$() {
return Rx.fromPromise(this.mqttClient.end());
}
}
/**
* @returns {MqttBroker}
*/
module.exports = MqttBroker;