├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json └── test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2017 10 | }, 11 | "rules": { 12 | "quotes": [ 13 | 1, 14 | "single" 15 | ], 16 | "no-mixed-requires": [ 17 | 0, 18 | false 19 | ], 20 | "no-underscore-dangle": 0, 21 | "yoda": [ 22 | 1, 23 | "always" 24 | ], 25 | "indent": [ 26 | 2, 27 | 2, 28 | { 29 | "SwitchCase": 1 30 | } 31 | ], 32 | "brace-style": [ 33 | 2, 34 | "1tbs" 35 | ], 36 | "comma-style": [ 37 | 2, 38 | "last" 39 | ], 40 | "default-case": 2, 41 | "func-style": [ 42 | 2, 43 | "declaration" 44 | ], 45 | "guard-for-in": 2, 46 | "no-floating-decimal": 2, 47 | "no-nested-ternary": 2, 48 | "no-undefined": 2, 49 | "radix": 2, 50 | "space-before-function-paren": [ 51 | 1, 52 | "always" 53 | ], 54 | "keyword-spacing": [ 55 | 2, 56 | { 57 | "after": true 58 | } 59 | ], 60 | "space-before-blocks": 2, 61 | "spaced-comment": [ 62 | 2, 63 | "always", 64 | { 65 | "exceptions": [ 66 | "-" 67 | ], 68 | "markers": [ 69 | "eslint", 70 | "jshint", 71 | "global" 72 | ] 73 | } 74 | ], 75 | "strict": [ 76 | 2, 77 | "global" 78 | ], 79 | "wrap-iife": 2, 80 | "camelcase": 0, 81 | "new-cap": 0 82 | } 83 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | *.lock 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | package-lock.json 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

async-mqtt

2 |

Promise wrapper over MQTT.js

3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 |

18 | 19 | **IMPORTANT: Make sure you handle rejections from returned promises because they won't crash the process** 20 | 21 | ## API 22 | 23 | The API is the same as [MQTT.js](https://github.com/mqttjs/MQTT.js#api), except the following functions now return promises instead of taking callbacks 24 | 25 | - publish 26 | - subscribe 27 | - unsubscribe 28 | - end 29 | 30 | 31 | ## Example 32 | 33 | ```javascript 34 | const MQTT = require("async-mqtt"); 35 | 36 | const client = MQTT.connect("tcp://somehost.com:1883"); 37 | 38 | // When passing async functions as event listeners, make sure to have a try catch block 39 | 40 | const doStuff = async () => { 41 | 42 | console.log("Starting"); 43 | try { 44 | await client.publish("wow/so/cool", "It works!"); 45 | // This line doesn't run until the server responds to the publish 46 | await client.end(); 47 | // This line doesn't run until the client has disconnected without error 48 | console.log("Done"); 49 | } catch (e){ 50 | // Do something about it! 51 | console.log(e.stack); 52 | process.exit(); 53 | } 54 | } 55 | 56 | client.on("connect", doStuff); 57 | ``` 58 | 59 | Alternately you can skip the event listeners and get a promise. 60 | 61 | ```js 62 | const MQTT = require("async-mqtt"); 63 | 64 | run() 65 | 66 | async function run() { 67 | const client = await MQTT.connectAsync("tcp://somehost.com:1883") 68 | 69 | console.log("Starting"); 70 | try { 71 | await client.publish("wow/so/cool", "It works!"); 72 | // This line doesn't run until the server responds to the publish 73 | await client.end(); 74 | // This line doesn't run until the client has disconnected without error 75 | console.log("Done"); 76 | } catch (e){ 77 | // Do something about it! 78 | console.log(e.stack); 79 | process.exit(); 80 | } 81 | } 82 | 83 | ``` 84 | 85 | ## Wrapping existing client 86 | 87 | ```javascript 88 | const { AsyncClient } = require("async-mqtt"); 89 | 90 | const client = getRegularMQTTClientFromSomewhere(); 91 | 92 | const asyncClient = new AsyncClient(client); 93 | 94 | asyncClient.publish("foo/bar", "baz").then(() => { 95 | console.log("We async now"); 96 | return asyncClient.end(); 97 | }); 98 | ``` 99 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MqttClient, 3 | IClientOptions, 4 | IClientPublishOptions, 5 | IClientSubscribeOptions, 6 | ISubscriptionGrant, 7 | ISubscriptionMap, 8 | } from 'mqtt'; 9 | 10 | export * from 'mqtt/types/lib/client-options'; 11 | export * from 'mqtt/types/lib/store'; 12 | 13 | export { 14 | // mqtt/types/lib/client 15 | ISubscriptionGrant, 16 | ISubscriptionRequest, 17 | ISubscriptionMap, 18 | OnMessageCallback, 19 | OnPacketCallback, 20 | OnErrorCallback, 21 | IStream, 22 | 23 | // mqtt-packet 24 | QoS, 25 | PacketCmd, 26 | IPacket, 27 | IConnectPacket, 28 | IPublishPacket, 29 | IConnackPacket, 30 | ISubscription, 31 | ISubscribePacket, 32 | ISubackPacket, 33 | IUnsubscribePacket, 34 | IUnsubackPacket, 35 | IPubackPacket, 36 | IPubcompPacket, 37 | IPubrelPacket, 38 | IPubrecPacket, 39 | IPingreqPacket, 40 | IPingrespPacket, 41 | IDisconnectPacket, 42 | Packet 43 | } from 'mqtt' 44 | 45 | export interface IMqttClient extends MqttClient {} 46 | 47 | export declare class AsyncMqttClient extends MqttClient { 48 | constructor (client: IMqttClient); 49 | 50 | public subscribe (topic: string | string[], opts: IClientSubscribeOptions): Promise 51 | public subscribe (topic: string | string[] | ISubscriptionMap): Promise 52 | /* original */ public subscribe (topic: string | string[], opts: IClientSubscribeOptions, callback: never): this 53 | /* original */ public subscribe (topic: string | string[] | ISubscriptionMap, callback: never): this 54 | 55 | public unsubscribe (topic: string | string[]): Promise 56 | /* original */ public unsubscribe (topic: string | string[], callback: never): this; 57 | 58 | public publish (topic: string, message: string | Buffer, opts: IClientPublishOptions): Promise 59 | public publish (topic: string, message: string | Buffer): Promise 60 | /* original */ public publish (topic: string, message: string | Buffer, opts: IClientPublishOptions, callback: never): this 61 | /* original */ public publish (topic: string, message: string | Buffer, callback: never): this 62 | 63 | public end (force?: boolean): Promise 64 | /* original */ public end (force: boolean, callback: never): this; 65 | } 66 | 67 | export declare function connect (brokerUrl?: string | any, opts?: IClientOptions): AsyncMqttClient 68 | export declare function connectAsync (brokerUrl: string | any, opts?: IClientOptions, allowRetries?: boolean): Promise 69 | 70 | export { AsyncMqttClient as AsyncClient } 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mqtt = require('mqtt'); 4 | 5 | class AsyncClient { 6 | constructor (client) { 7 | this._client = client; 8 | } 9 | 10 | set handleMessage (newHandler) { 11 | this._client.handleMessage = newHandler; 12 | } 13 | 14 | get handleMessage () { 15 | return this._client.handleMessage; 16 | } 17 | 18 | get connected () { 19 | return this._client.connected; 20 | } 21 | 22 | get reconnecting () { 23 | return this._client.reconnecting; 24 | } 25 | 26 | publish (...args) { 27 | return new Promise((resolve, reject) => { 28 | this._client.publish(...args, (err, result) => { 29 | if (err) reject(err) 30 | else resolve(result) 31 | }) 32 | }) 33 | } 34 | 35 | subscribe (...args) { 36 | return new Promise((resolve, reject) => { 37 | this._client.subscribe(...args, (err, result) => { 38 | if (err) reject(err) 39 | else resolve(result) 40 | }) 41 | }) 42 | } 43 | 44 | unsubscribe (...args) { 45 | return new Promise((resolve, reject) => { 46 | this._client.unsubscribe(...args, (err, result) => { 47 | if (err) reject(err) 48 | else resolve(result) 49 | }) 50 | }) 51 | } 52 | 53 | end (...args) { 54 | return new Promise((resolve, reject) => { 55 | this._client.end(...args, (err, result) => { 56 | if (err) reject(err) 57 | else resolve(result) 58 | }) 59 | }) 60 | } 61 | 62 | reconnect (...args) { 63 | return this._client.reconnect(...args); 64 | } 65 | 66 | addListener (...args) { 67 | return this._client.addListener(...args); 68 | } 69 | 70 | emit (...args) { 71 | return this._client.emit(...args); 72 | } 73 | 74 | eventNames (...args) { 75 | return this._client.eventNames(...args); 76 | } 77 | 78 | getLastMessageId (...args) { 79 | return this._client.getLastMessageId(...args); 80 | } 81 | 82 | getMaxListeners (...args) { 83 | return this._client.getMaxListeners(...args); 84 | } 85 | 86 | listenerCount (...args) { 87 | return this._client.listenerCount(...args); 88 | } 89 | 90 | listeners (...args) { 91 | return this._client.listeners(...args); 92 | } 93 | 94 | off (...args) { 95 | return this._client.off(...args); 96 | } 97 | 98 | on (...args) { 99 | return this._client.on(...args); 100 | } 101 | 102 | once (...args) { 103 | return this._client.once(...args); 104 | } 105 | 106 | prependListener (...args) { 107 | return this._client.prependListener(...args); 108 | } 109 | 110 | prependOnceListener (...args) { 111 | return this._client.prependOnceListener(...args); 112 | } 113 | 114 | rawListeners (...args) { 115 | return this._client.rawListeners(...args); 116 | } 117 | 118 | removeAllListeners (...args) { 119 | return this._client.removeAllListeners(...args); 120 | } 121 | 122 | removeListener (...args) { 123 | return this._client.removeListener(...args); 124 | } 125 | 126 | removeOutgoingMessage (...args) { 127 | return this._client.removeOutgoingMessage(...args); 128 | } 129 | 130 | setMaxListeners (...args) { 131 | return this._client.setMaxListeners(...args); 132 | } 133 | 134 | } 135 | 136 | 137 | module.exports = { 138 | connect (brokerURL, opts) { 139 | const client = mqtt.connect(brokerURL, opts); 140 | const asyncClient = new AsyncClient(client); 141 | 142 | return asyncClient; 143 | }, 144 | connectAsync (brokerURL, opts, allowRetries=true) { 145 | const client = mqtt.connect(brokerURL, opts); 146 | const asyncClient = new AsyncClient(client); 147 | 148 | return new Promise((resolve, reject) => { 149 | // Listeners added to client to trigger promise resolution 150 | const promiseResolutionListeners = { 151 | connect: (connack) => { 152 | removePromiseResolutionListeners(); 153 | resolve(asyncClient); // Resolve on connect 154 | }, 155 | end: () => { 156 | removePromiseResolutionListeners(); 157 | resolve(asyncClient); // Resolve on end 158 | }, 159 | error: (err) => { 160 | removePromiseResolutionListeners(); 161 | client.end(); 162 | reject(err); // Reject on error 163 | } 164 | }; 165 | 166 | // If retries are not allowed, reject on close 167 | if (false === allowRetries) { 168 | promiseResolutionListeners.close = () => { 169 | promiseResolutionListeners.error('Couldn\'t connect to server'); 170 | } 171 | } 172 | 173 | // Remove listeners added to client by this promise 174 | function removePromiseResolutionListeners () { 175 | Object.keys(promiseResolutionListeners).forEach((eventName) => { 176 | client.removeListener(eventName, promiseResolutionListeners[eventName]); 177 | }); 178 | }; 179 | 180 | // Add listeners to client 181 | Object.keys(promiseResolutionListeners).forEach((eventName) => { 182 | client.on(eventName, promiseResolutionListeners[eventName]); 183 | }); 184 | }); 185 | }, 186 | AsyncClient 187 | }; 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-mqtt", 3 | "version": "2.6.3", 4 | "description": "Promise wrapper over MQTT.js", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "node test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/mqttjs/async-mqtt.git" 13 | }, 14 | "keywords": [ 15 | "mqtt", 16 | "promise", 17 | "async", 18 | "publish", 19 | "subscribe" 20 | ], 21 | "author": "rangermauve", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/mqttjs/async-mqtt/issues" 25 | }, 26 | "homepage": "https://github.com/mqttjs/async-mqtt#readme", 27 | "dependencies": { 28 | "mqtt": "^4.3.7" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^14.0.5", 32 | "eslint": "^7.32.0", 33 | "mqtt-connection": "^4.0.0", 34 | "tape": "^5.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const net = require('net'); 4 | const Connection = require('mqtt-connection'); 5 | 6 | class Server extends net.Server { 7 | constructor (listener) { 8 | super() 9 | this.connectionList = [] 10 | 11 | this.on('connection', duplex => { 12 | this.connectionList.push(duplex) 13 | const connection = new Connection(duplex, () => { 14 | this.emit('client', connection) 15 | }) 16 | }) 17 | 18 | if (listener) { 19 | this.on('client', listener) 20 | } 21 | } 22 | } 23 | 24 | 25 | const AsyncMQTT = require('./'); 26 | const { AsyncClient } = AsyncMQTT; 27 | 28 | const test = require('tape'); 29 | 30 | const SERVER_PORT = 1883; 31 | const SERVER_URL = `mqtt://localhost:${SERVER_PORT}`; 32 | 33 | const server = buildServer().listen(SERVER_PORT); 34 | server.unref(); 35 | 36 | server.on('listening', runTests); 37 | 38 | function runTests () { 39 | test('Connect should return an instance of AsyncClient', t => { 40 | t.plan(1); 41 | const client = AsyncMQTT.connect(SERVER_URL); 42 | 43 | t.ok(client instanceof AsyncClient, 'Connect returned an AsyncClient'); 44 | 45 | client.on('connect', () => { 46 | client.end(); 47 | }) 48 | }); 49 | 50 | test('ConnectAsync should return AsyncClient after connection', t => { 51 | t.plan(1); 52 | 53 | AsyncMQTT.connectAsync(SERVER_URL, {}, false).then((client) => { 54 | t.ok(client.connected, 'AsyncClient is connected'); 55 | 56 | client.end(); 57 | }, (error) => { 58 | t.fail(error); 59 | }); 60 | 61 | 62 | }); 63 | 64 | test('Should be able to listen on event on client', t => { 65 | t.plan(1); 66 | 67 | const client = AsyncMQTT.connect(SERVER_URL); 68 | 69 | client.once('connect', () => { 70 | t.pass('Connected'); 71 | client.end(); 72 | }); 73 | }); 74 | 75 | test('client.connected should be true', t => { 76 | t.plan(1); 77 | 78 | const client = AsyncMQTT.connect(SERVER_URL); 79 | 80 | client.once('connect', () => { 81 | t.equal(client.connected, true) 82 | client.end(); 83 | }); 84 | }); 85 | 86 | test('Calling end() should resolve once disconnected', t => { 87 | t.plan(2); 88 | 89 | const client = AsyncMQTT.connect(SERVER_URL); 90 | 91 | client.on('close', () => { 92 | t.pass('Close event occured'); 93 | }); 94 | 95 | client.on('connect', () => { 96 | // Wait for connect to emit before ending 97 | client.end().then(() => { 98 | t.pass('End resolved'); 99 | }); 100 | }); 101 | }); 102 | 103 | test('Calling subscribe should resolve once subscribed', t => { 104 | t.plan(1); 105 | 106 | const client = AsyncMQTT.connect(SERVER_URL); 107 | 108 | client.subscribe('example', { 109 | qos: 1 110 | }).then(() => { 111 | t.pass('Subscribed'); 112 | client.end(); 113 | }) 114 | }); 115 | 116 | test('Calling unsubscribe should resolve once completed', t => { 117 | t.plan(1); 118 | 119 | const client = AsyncMQTT.connect(SERVER_URL); 120 | 121 | client.subscribe('example', { 122 | qos: 1 123 | }).then(() => client.unsubscribe('example')).then(() => { 124 | t.pass('Unsunbscribed'); 125 | return client.end(); 126 | }); 127 | }); 128 | 129 | test('Calling publish should resolve once completed', t => { 130 | t.plan(1); 131 | 132 | const client = AsyncMQTT.connect(SERVER_URL); 133 | 134 | client.publish('example', 'test', { 135 | qos: 1 136 | }).then(() => { 137 | t.pass('Published'); 138 | return client.end(); 139 | }); 140 | }); 141 | 142 | test('Calling getLastMessageId should return number after published', t => { 143 | t.plan(1); 144 | 145 | const client = AsyncMQTT.connect(SERVER_URL); 146 | 147 | client.publish('example', 'test', { 148 | qos: 1 149 | }).then(() => { 150 | t.ok('number' === typeof client.getLastMessageId(), 'Message id is number'); 151 | return client.end(); 152 | }); 153 | }); 154 | 155 | test('Calling reconnect after end should resolve once reconnect', t => { 156 | t.plan(2); 157 | 158 | const client = AsyncMQTT.connect(SERVER_URL); 159 | 160 | client.once('reconnect', () => { 161 | t.pass('Reconnect event occured'); 162 | client.once('connect', () => { 163 | client.end(); 164 | }); 165 | }); 166 | 167 | client.once('connect', () => { 168 | client.end().then(() => { 169 | t.pass('End resolved'); 170 | client.reconnect(); 171 | }); 172 | }); 173 | }); 174 | } 175 | 176 | // Taken from MQTT.js tests 177 | function buildServer () { 178 | return new Server(client => { 179 | client.on('connect', ({clientId}) => { 180 | if ('invalid' === clientId) { 181 | client.connack({returnCode: 2}) 182 | } else { 183 | client.connack({returnCode: 0}) 184 | } 185 | }) 186 | 187 | client.on('publish', packet => { 188 | setImmediate(() => { 189 | switch (packet.qos) { 190 | case 0: 191 | break 192 | case 1: 193 | client.puback(packet) 194 | break 195 | case 2: 196 | client.pubrec(packet) 197 | break 198 | default: 199 | break 200 | } 201 | }) 202 | }) 203 | 204 | client.on('pubrel', packet => { 205 | client.pubcomp(packet) 206 | }) 207 | 208 | client.on('pubrec', packet => { 209 | client.pubrel(packet) 210 | }) 211 | 212 | client.on('pubcomp', () => { 213 | // Nothing to be done 214 | }) 215 | 216 | client.on('subscribe', ({messageId, subscriptions}) => { 217 | client.suback({ 218 | messageId, 219 | granted: subscriptions.map(({qos}) => qos) 220 | }) 221 | }) 222 | 223 | client.on('unsubscribe', packet => { 224 | client.unsuback(packet) 225 | }) 226 | 227 | client.on('pingreq', () => { 228 | client.pingresp() 229 | }) 230 | }); 231 | } 232 | --------------------------------------------------------------------------------