@mangar2/mqttservice
Version:
communicates with a MQTT-Style HTTP broker
210 lines (191 loc) • 7.21 kB
JavaScript
/**
* @license
* This software is licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3. It is furnished
* "as is", without any support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*
* @author Volker Böhm
* @copyright Copyright (c) 2020 Volker Böhm
*/
'use strict'
const HttpClient = require('@mangar2/httpservice').HttpClient
const mqttVersion = require('@mangar2/mqttversion')
/**
* @private
* @description
* Converts a QoS to a valid QoS. (0,1,2) by converting it to Number and setting it to "0" on any invalid entry
* @param {number|string} qos QoS to transform
* @return {number} valid QoS
*/
function convertQoSToValidQoS (qos) {
var numberQoS = Number(qos)
if (numberQoS !== 1 && numberQoS !== 2) {
numberQoS = 0
}
return numberQoS
}
/**
* @private
* @description
* Waits for a period of milliseconds
* @param {number} timeoutInMilliseconds timeout of the delay in milliseconds
* @returns {Promise}
*/
function delay (timeoutInMilliseconds) {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timeoutInMilliseconds)
})
}
/**
* Creates a client for publishing messages
* @param {string} host host name (or ip)
* @param {number} port port number
* @param {Object} configuration configuration options
* @param {number} [configuration.retry=60] amount of retries to send a message
* @example
* //Publish the message with topic /a/a, value 1, reason "test", QoS 0, retain 0:
* const publish = new PublishMessage('myhost', 10000, { retry: 60 })
* result = await publish.publish(publishtoken, new Message("/a/a", 1, "test"), 0, 0);
*/
class PublishMessage {
constructor (host, port, configuration) {
if (typeof (configuration) !== 'object') {
configuration = {}
}
this.topicQueues = {}
this.client = new HttpClient(host, port)
this.nextPacketId = 1
this.configuration = {}
this.configuration.retry = !isNaN(configuration.retry) ? configuration.retry : 60
this.terminate = false
}
/**
* @private
* @description
* provides a new packet id
* @returns {number} "nearly unique" packet id (between 0 .. 65536)
*/
providePacketId () {
const packetid = this.nextPacketId
this.nextPacketId++
this.nextPacketId %= 0x10000
if (this.nextPacketId === 0) {
this.nextPacketId = 1
}
return packetid
}
/**
* @private
* @description
* sends a pubrel packet to support QoS 2
* @param {number} packetid packet id of the publish packet that is acknowledged with pubrel
* @param {string} token connection token
* @param {string} topic topic name
*/
async pubrel (packetid, token, topic) {
let success = false
let retryCount = 0
const sendData = mqttVersion.pubrel('1.0', token, packetid)
while (!success && retryCount < this.configuration.retry) {
this.topicQueues[topic].state = 'pubrel'
try {
const result = await this.client.send('/pubrel', 'PUT', sendData.payload, sendData.headers)
success = sendData.resultCheck(result)
} catch (err) {
// success === false; No need to change anything, retry
}
await delay(1000 * Math.min(retryCount * retryCount, 60))
retryCount++
}
return success
}
/**
* @description
* Publishes a message with a defined quality of service automatically generating an id
* @param {string} token connection token
* @param {Object} message message to publish.
* @param {number} qos 0,1,2 quality of service
* @param {boolean} retain True, if message shall be retained for future subscriptions
* @param {string|undefined} version interface version, supports 0.0 and 1.0 (default)
* @throws {string} on any connection error
*/
async publish (token, message, qos, retain = false, version = '1.0') {
if (message.qos !== undefined) {
qos = message.qos
}
if (message.retain !== undefined) {
retain = message.retain
}
qos = convertQoSToValidQoS(qos)
retain = retain === 1 || retain === '1' || retain === true ? 1 : 0
let result
if (qos === 0) {
const sendData = mqttVersion.publish(version, token, message, qos, 0, retain)
await this.client.send('/publish', 'PUT', sendData.payload, sendData.headers).catch(() => {
// Do not care, if send is successful for qos === 0. Still await is needed to safely catch rejects.
})
} else {
if (this.topicQueues[message.topic] === undefined) {
this.topicQueues[message.topic] = { state: 'ready', queue: [] }
}
const topicQueue = this.topicQueues[message.topic]
const sendData = mqttVersion.publish(version, token, message, qos, 0, retain, this.providePacketId())
topicQueue.queue.push(sendData)
if (topicQueue.state === 'ready') {
while (topicQueue.queue.length > 0) {
topicQueue.state = 'publish'
const firstElement = topicQueue.queue.shift()
result = await this.sendMessage(firstElement)
topicQueue.state = 'ready'
}
}
}
return result
}
/**
* @private
* @description
* Sends a message. Please call "publish"
* @param {Object} sendData {payload, headers}
* @throws {string} on any connection error
*/
async sendMessage (sendData) {
let success = false
const headers = sendData.headers
const payload = sendData.payload
const qos = headers.qos
let retryCount = 0
let result
let sendError
while (!success && retryCount < this.configuration.retry) {
try {
result = await this.client.send('/publish', 'PUT', payload, headers)
success = sendData.resultCheck(result)
} catch (err) {
sendError = err
// success === false; No need to change anything, we retry
}
headers.dup = 1
await delay(1000 * Math.min(retryCount * retryCount, 60))
retryCount++
}
if (!success) {
throw Error('Error sending Message: ' + sendError.message)
}
if (qos === 2 && success) {
success = await this.pubrel(headers.packetid, payload.token, payload.message.topic)
}
return success
}
/**
* @description
* Closes the connection to the broker
*/
async close () {
this.terminate = true
await this.client.close()
}
}
module.exports = PublishMessage