UNPKG

@mangar2/mqttservice

Version:

communicates with a MQTT-Style HTTP broker

210 lines (191 loc) 7.21 kB
/** * @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