diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..4e53d5816 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +/types/ +/examples/ +/doc/ +/dist/ +/test/typescript/ \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..51e23bd0c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + env: { + browser: true, + commonjs: true, + es2021: true, + node: true, + mocha: true, + }, + extends: ['airbnb-base', 'plugin:prettier/recommended'], + parserOptions: { + ecmaVersion: 'latest', + }, + rules: { + 'global-require': 'off', + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-unused-vars': 'off', + 'no-underscore-dangle': 'off', + 'no-param-reassign': 'off', + 'no-restricted-syntax': 'off', + camelcase: 'off', + 'default-case': 'off', + 'consistent-return': 'off', + 'import/order': 'off', + 'max-classes-per-file': 'off', + 'no-plusplus': 'off', + 'guard-for-in': 'off', + 'no-bitwise': 'off', + 'class-methods-use-this': 'off', + 'no-continue': 'off', + 'prefer-destructuring': 'off', + 'no-use-before-define': 'off', + }, +} diff --git a/.github/workflows/mqttjs-test.yml b/.github/workflows/mqttjs-test.yml index eb1c47d6c..05d67813d 100644 --- a/.github/workflows/mqttjs-test.yml +++ b/.github/workflows/mqttjs-test.yml @@ -21,27 +21,33 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install Dependencies run: npm ci + + - name: Lint + if: matrix.node-version == '20.x' + # only run on latest node version, no reason to run on all + run: | + npm run lint - name: Test NodeJS - run: npm run test:node + run: npm run test:node # npx mocha test/unique_message_id_provider_client.js --exit env: CI: true - DEBUG: "mqttjs" + DEBUG: "${{ runner.debug == '1' && 'mqttjs:*' || '' }}" - name: Test Typescript run: npm run test:typescript env: CI: true - DEBUG: "mqttjs" + DEBUG: "${{ runner.debug == '1' && 'mqttjs:*' || '' }}" - name: Test Browser if: matrix.node-version == '20.x' diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..a19041c4d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +*.md +README.md +/types/ +/examples/ +/doc/ +/dist/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..3d8d73d66 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + semi: false, + singleQuote: true, + useTabs: true, + tabWidth: 4, + endOfLine: "lf", +}; diff --git a/.vscode/settings.json b/.vscode/settings.json index b788d76e6..820ddab8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ { - "standard.enable": true, - "standard.autoFixOnSave": true, - "editor.defaultFormatter": "standard.vscode-standard" + "eslint.format.enable": true, + "eslint.lintTask.enable": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.fixAll.markdownlint": true + }, } \ No newline at end of file diff --git a/README.md b/README.md index 69b9609f8..5c0aac788 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written in JavaScript for node.js and the browser. -> MQTT [5.0.0-beta.0](https://github.com/mqttjs/MQTT.js/releases/tag/v5.0.0-beta.0) is now available! Try it out and give us feedback! `npm i mqtt@5.0.0-beta.0` +> MQTT [5.0.0 BETA](https://www.npmjs.com/package/mqtt/v/beta) is now available! Try it out and give us [feedback](https://github.com/mqttjs/MQTT.js/issues/1639): `npm i mqtt@beta` ## Table of Contents @@ -101,15 +101,15 @@ For the sake of simplicity, let's put the subscriber and the publisher in the sa const mqtt = require("mqtt"); const client = mqtt.connect("mqtt://test.mosquitto.org"); -client.on("connect", function () { - client.subscribe("presence", function (err) { +client.on("connect", () => { + client.subscribe("presence", (err) => { if (!err) { client.publish("presence", "Hello mqtt"); } }); }); -client.on("message", function (topic, message) { +client.on("message", (topic, message) => { // message is Buffer console.log(message.toString()); client.end(); @@ -305,23 +305,24 @@ Also user can manually register topic-alias pair using PUBLISH topic:'some', ta: ## API -- mqtt.connect() -- mqtt.Client() -- mqtt.Client#publish() -- mqtt.Client#subscribe() -- mqtt.Client#unsubscribe() -- mqtt.Client#end() -- mqtt.Client#removeOutgoingMessage() -- mqtt.Client#reconnect() -- mqtt.Client#handleMessage() -- mqtt.Client#connected -- mqtt.Client#reconnecting -- mqtt.Client#getLastMessageId() -- mqtt.Store() -- mqtt.Store#put() -- mqtt.Store#del() -- mqtt.Store#createStream() -- mqtt.Store#close() +- [`mqtt.connect()`](#connect) +- [`mqtt.Client()`](#client) +- [`mqtt.Client#connect()`](#client-connect) +- [`mqtt.Client#publish()`](#publish) +- [`mqtt.Client#subscribe()`](#subscribe) +- [`mqtt.Client#unsubscribe()`](#unsubscribe) +- [`mqtt.Client#end()`](#end) +- [`mqtt.Client#removeOutgoingMessage()`](#removeOutgoingMessage) +- [`mqtt.Client#reconnect()`](#reconnect) +- [`mqtt.Client#handleMessage()`](#handleMessage) +- [`mqtt.Client#connected`](#connected) +- [`mqtt.Client#reconnecting`](#reconnecting) +- [`mqtt.Client#getLastMessageId()`](#getLastMessageId) +- [`mqtt.Store()`](#store) +- [`mqtt.Store#put()`](#put) +- [`mqtt.Store#del()`](#del) +- [`mqtt.Store#createStream()`](#createStream) +- [`mqtt.Store#close()`](#close) --- @@ -368,7 +369,7 @@ The arguments are: the `connect` event. Typically a `net.Socket`. - `options` is the client connection options (see: the [connect packet](https://github.com/mcollina/mqtt-packet#connect)). Defaults: - `wsOptions`: is the WebSocket connection options. Default is `{}`. - It's specific for WebSockets. For possible options have a look at: https://github.com/websockets/ws/blob/master/doc/ws.md. + It's specific for WebSockets. For possible options have a look at: . - `keepalive`: `60` seconds, set to `0` to disable - `reschedulePings`: reschedule ping messages after sending packets (default `true`) - `clientId`: `'mqttjs_' + Math.random().toString(16).substr(2, 8)` @@ -386,9 +387,11 @@ The arguments are: - `outgoingStore`: a [Store](#store) for the outgoing packets - `queueQoSZero`: if connection is broken, queue outgoing QoS zero messages (default `true`) - `customHandleAcks`: MQTT 5 feature of custom handling puback and pubrec packets. Its callback: + ```js customHandleAcks: function(topic, message, packet, done) {/*some logic wit colling done(error, reasonCode)*/} ``` + - `autoUseTopicAlias`: enabling automatic Topic Alias using functionality - `autoAssignTopicAlias`: enabling automatic Topic Alias assign functionality - `properties`: properties MQTT 5.0. @@ -424,6 +427,7 @@ The arguments are: subscribed topics are automatically subscribed again (default `true`) - `messageIdProvider`: custom messageId provider. when `new UniqueMessageIdProvider()` is set, then non conflict messageId is provided. - `log`: custom log function. Default uses [debug](https://www.npmjs.com/package/debug) package. + - `manualConnect`: prevents the constructor to call `connect`. In this case after the `mqtt.connect` is called you should call `client.connect` manually. In case mqtts (mqtt over tls) is required, the `options` object is passed through to @@ -497,7 +501,7 @@ The following TLS errors will be emitted as an `error` event: `function () {}` -Emitted when mqtt.Client#end() is called. +Emitted when [`mqtt.Client#end()`](#end) is called. If a callback was passed to `mqtt.Client#end()`, this event is emitted once the callback returns. @@ -535,6 +539,12 @@ and connections --- + + +### mqtt.Client#connect() + +By default client connects when constructor is called. To prevent this you can set `manualConnect` option to `true` and call `client.connect()` manually. + ### mqtt.Client#publish(topic, message, [options], [callback]) @@ -746,9 +756,9 @@ Closes the Store. ### Via CDN -The MQTT.js bundle is available through http://unpkg.com, specifically -at https://unpkg.com/mqtt/dist/mqtt.min.js. -See http://unpkg.com for the full documentation on version ranges. +The MQTT.js bundle is available through , specifically +at . +See for the full documentation on version ranges. diff --git a/benchmarks/bombing.js b/benchmarks/bombing.js index 4d158c2b4..bdfa36c73 100755 --- a/benchmarks/bombing.js +++ b/benchmarks/bombing.js @@ -1,26 +1,32 @@ #! /usr/bin/env node -const mqtt = require('../') -const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 }) +const mqtt = require('..') + +const client = mqtt.connect({ + port: 1883, + host: 'localhost', + clean: true, + keepalive: 0, +}) let sent = 0 const interval = 5000 -function count () { - console.log('sent/s', sent / interval * 1000) - sent = 0 +function count() { + console.log('sent/s', (sent / interval) * 1000) + sent = 0 } setInterval(count, interval) -function publish () { - sent++ - client.publish('test', 'payload', publish) +function publish() { + sent++ + client.publish('test', 'payload', publish) } client.on('connect', publish) -client.on('error', function () { - console.log('reconnect!') - client.stream.end() +client.on('error', () => { + console.log('reconnect!') + client.stream.end() }) diff --git a/benchmarks/throughputCounter.js b/benchmarks/throughputCounter.js index c2dbabfc5..fab0ed402 100755 --- a/benchmarks/throughputCounter.js +++ b/benchmarks/throughputCounter.js @@ -1,22 +1,28 @@ #! /usr/bin/env node -const mqtt = require('../') +const mqtt = require('..') -const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 }) +const client = mqtt.connect({ + port: 1883, + host: 'localhost', + clean: true, + encoding: 'binary', + keepalive: 0, +}) let counter = 0 const interval = 5000 -function count () { - console.log('received/s', counter / interval * 1000) - counter = 0 +function count() { + console.log('received/s', (counter / interval) * 1000) + counter = 0 } setInterval(count, interval) -client.on('connect', function () { - count() - this.subscribe('test') - this.on('message', function () { - counter++ - }) +client.on('connect', () => { + count() + this.subscribe('test') + this.on('message', () => { + counter++ + }) }) diff --git a/bin/mqtt.js b/bin/mqtt.js index edba1609d..ee9417288 100755 --- a/bin/mqtt.js +++ b/bin/mqtt.js @@ -1,5 +1,4 @@ #!/usr/bin/env node -'use strict' /* * Copyright (c) 2015-2015 MQTT.js contributors. @@ -10,18 +9,19 @@ const path = require('path') const commist = require('commist')() const helpMe = require('help-me')({ - dir: path.join(path.dirname(require.main.filename), '/../doc'), - ext: '.txt' + dir: path.join(path.dirname(require.main.filename), '/../doc'), + ext: '.txt', }) commist.register('publish', require('./pub')) commist.register('subscribe', require('./sub')) -commist.register('version', function () { - console.log('MQTT.js version:', require('./../package.json').version) + +commist.register('version', () => { + console.log('MQTT.js version:', require('../package.json').version) }) commist.register('help', helpMe.toStdout) if (commist.parse(process.argv.slice(2)) !== null) { - console.log('No such command:', process.argv[2], '\n') - helpMe.toStdout() + console.log('No such command:', process.argv[2], '\n') + helpMe.toStdout() } diff --git a/bin/pub.js b/bin/pub.js index 6309bca3e..d9e3ec8bb 100755 --- a/bin/pub.js +++ b/bin/pub.js @@ -1,145 +1,159 @@ #!/usr/bin/env node -'use strict' - -const mqtt = require('../') +const mqtt = require('..') const { pipeline, Writable } = require('readable-stream') const path = require('path') const fs = require('fs') const concat = require('concat-stream') const helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') + dir: path.join(__dirname, '..', 'doc'), }) const minimist = require('minimist') const split2 = require('split2') -function send (args) { - const client = mqtt.connect(args) - client.on('connect', function () { - client.publish(args.topic, args.message, args, function (err) { - if (err) { - console.warn(err) - } - client.end() - }) - }) - client.on('error', function (err) { - console.warn(err) - client.end() - }) +function send(args) { + const client = mqtt.connect(args) + client.on('connect', () => { + client.publish(args.topic, args.message, args, (err) => { + if (err) { + console.warn(err) + } + client.end() + }) + }) + client.on('error', (err) => { + console.warn(err) + client.end() + }) } -function multisend (args) { - const client = mqtt.connect(args) - const sender = new Writable({ - objectMode: true - }) - sender._write = function (line, enc, cb) { - client.publish(args.topic, line.trim(), args, cb) - } - - client.on('connect', function () { - pipeline(process.stdin, split2(), sender, function (err) { - client.end() - if (err) { - throw err - } - }) - }) +function multisend(args) { + const client = mqtt.connect(args) + const sender = new Writable({ + objectMode: true, + }) + sender._write = (line, enc, cb) => { + client.publish(args.topic, line.trim(), args, cb) + } + + client.on('connect', () => { + pipeline(process.stdin, split2(), sender, (err) => { + client.end() + if (err) { + throw err + } + }) + }) } -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'message', 'clientId', 'i', 'id'], - boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - message: 'm', - qos: 'q', - clientId: ['i', 'id'], - retain: 'r', - username: 'u', - password: 'P', - stdin: 's', - multiline: 'M', - protocol: ['C', 'l'], - help: 'H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - topic: '', - message: '' - } - }) - - if (args.help) { - return helpMe.toStdout('publish') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - args.topic = (args.topic || args._.shift()).toString() - args.message = (args.message || args._.shift()).toString() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('publish') - } - - if (args.stdin) { - if (args.multiline) { - multisend(args) - } else { - process.stdin.pipe(concat(function (data) { - args.message = data - send(args) - })) - } - } else { - send(args) - } +function start(args) { + args = minimist(args, { + string: [ + 'hostname', + 'username', + 'password', + 'key', + 'cert', + 'ca', + 'message', + 'clientId', + 'i', + 'id', + ], + boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + message: 'm', + qos: 'q', + clientId: ['i', 'id'], + retain: 'r', + username: 'u', + password: 'P', + stdin: 's', + multiline: 'M', + protocol: ['C', 'l'], + help: 'H', + ca: 'cafile', + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + topic: '', + message: '', + }, + }) + + if (args.help) { + return helpMe.toStdout('publish') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn( + "# Port: number expected, '%s' was given.", + typeof args.port, + ) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + args.topic = (args.topic || args._.shift()).toString() + args.message = (args.message || args._.shift()).toString() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('publish') + } + + if (args.stdin) { + if (args.multiline) { + multisend(args) + } else { + process.stdin.pipe( + concat((data) => { + args.message = data + send(args) + }), + ) + } + } else { + send(args) + } } module.exports = start if (require.main === module) { - start(process.argv.slice(2)) + start(process.argv.slice(2)) } diff --git a/bin/sub.js b/bin/sub.js index cb113a779..d2e52d5f2 100755 --- a/bin/sub.js +++ b/bin/sub.js @@ -1,123 +1,141 @@ #!/usr/bin/env node -const mqtt = require('../') +const mqtt = require('..') const path = require('path') const fs = require('fs') const helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') + dir: path.join(__dirname, '..', 'doc'), }) const minimist = require('minimist') -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'clientId', 'i', 'id'], - boolean: ['stdin', 'help', 'clean', 'insecure'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - qos: 'q', - clean: 'c', - keepalive: 'k', - clientId: ['i', 'id'], - username: 'u', - password: 'P', - protocol: ['C', 'l'], - verbose: 'v', - help: '-H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - clean: true, - keepAlive: 30 // 30 sec - } - }) - - if (args.help) { - return helpMe.toStdout('subscribe') - } - - args.topic = args.topic || args._.shift() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('subscribe') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - args.keepAlive = args['keep-alive'] - - const client = mqtt.connect(args) - - client.on('connect', function () { - client.subscribe(args.topic, { qos: args.qos }, function (err, result) { - if (err) { - console.error(err) - process.exit(1) - } - - result.forEach(function (sub) { - if (sub.qos > 2) { - console.error('subscription negated to', sub.topic, 'with code', sub.qos) - process.exit(1) - } - }) - }) - }) - - client.on('message', function (topic, payload) { - if (args.verbose) { - console.log(topic, payload.toString()) - } else { - console.log(payload.toString()) - } - }) - - client.on('error', function (err) { - console.warn(err) - client.end() - }) +function start(args) { + args = minimist(args, { + string: [ + 'hostname', + 'username', + 'password', + 'key', + 'cert', + 'ca', + 'clientId', + 'i', + 'id', + ], + boolean: ['stdin', 'help', 'clean', 'insecure'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + qos: 'q', + clean: 'c', + keepalive: 'k', + clientId: ['i', 'id'], + username: 'u', + password: 'P', + protocol: ['C', 'l'], + verbose: 'v', + help: '-H', + ca: 'cafile', + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + clean: true, + keepAlive: 30, // 30 sec + }, + }) + + if (args.help) { + return helpMe.toStdout('subscribe') + } + + args.topic = args.topic || args._.shift() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('subscribe') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn( + "# Port: number expected, '%s' was given.", + typeof args.port, + ) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + args.keepAlive = args['keep-alive'] + + const client = mqtt.connect(args) + + client.on('connect', () => { + client.subscribe(args.topic, { qos: args.qos }, (err, result) => { + if (err) { + console.error(err) + process.exit(1) + } + + result.forEach((sub) => { + if (sub.qos > 2) { + console.error( + 'subscription negated to', + sub.topic, + 'with code', + sub.qos, + ) + process.exit(1) + } + }) + }) + }) + + client.on('message', (topic, payload) => { + if (args.verbose) { + console.log(topic, payload.toString()) + } else { + console.log(payload.toString()) + } + }) + + client.on('error', (err) => { + console.warn(err) + client.end() + }) } module.exports = start if (require.main === module) { - start(process.argv.slice(2)) + start(process.argv.slice(2)) } diff --git a/example.js b/example.js index c5c8568a2..7aa831230 100644 --- a/example.js +++ b/example.js @@ -1,11 +1,12 @@ -const mqtt = require('./') +const mqtt = require('.') + const client = mqtt.connect('mqtt://test.mosquitto.org') client.subscribe('presence') client.publish('presence', 'Hello mqtt') -client.on('message', function (topic, message) { - console.log(message.toString()) +client.on('message', (topic, message) => { + console.log(message.toString()) }) client.end() diff --git a/examples/tls client/mqttclient.js b/examples/tls client/mqttclient.js index dff75d725..9581eccbe 100644 --- a/examples/tls client/mqttclient.js +++ b/examples/tls client/mqttclient.js @@ -39,10 +39,10 @@ const client = mqtt.connect(options) client.subscribe('messages') client.publish('messages', 'Current time is: ' + new Date()) -client.on('message', function (topic, message) { +client.on('message', (topic, message) => { console.log(message) }) -client.on('connect', function () { +client.on('connect', () => { console.log('Connected') }) diff --git a/examples/ws/aedes_server.js b/examples/ws/aedes_server.js index e29032ff4..7ee48a7c0 100644 --- a/examples/ws/aedes_server.js +++ b/examples/ws/aedes_server.js @@ -10,19 +10,19 @@ wss.on('connection', function connection (ws) { aedes.handle(duplex) }) -httpServer.listen(wsPort, function () { +httpServer.listen(wsPort, () => { console.log('websocket server listening on port', wsPort) }) -aedes.on('clientError', function (client, err) { +aedes.on('clientError', (client, err) => { console.log('client error', client.id, err.message, err.stack) }) -aedes.on('connectionError', function (client, err) { +aedes.on('connectionError', (client, err) => { console.log('client error', client, err.message, err.stack) }) -aedes.on('publish', function (packet, client) { +aedes.on('publish', (packet, client) => { if (packet && packet.payload) { console.log('publish packet:', packet.payload.toString()) } @@ -31,12 +31,12 @@ aedes.on('publish', function (packet, client) { } }) -aedes.on('subscribe', function (subscriptions, client) { +aedes.on('subscribe', (subscriptions, client) => { if (client) { console.log('subscribe from client', subscriptions, client.id) } }) -aedes.on('client', function (client) { +aedes.on('client', (client) => { console.log('new client', client.id) }) diff --git a/examples/ws/client.js b/examples/ws/client.js index cc258677d..d2c4bf53d 100644 --- a/examples/ws/client.js +++ b/examples/ws/client.js @@ -33,21 +33,21 @@ const options = { console.log('connecting mqtt client') const client = mqtt.connect(host, options) -client.on('error', function (err) { +client.on('error', (err) => { console.log(err) client.end() }) -client.on('connect', function () { +client.on('connect', () => { console.log('client connected:' + clientId) client.subscribe('topic', { qos: 0 }) client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) }) -client.on('message', function (topic, message, packet) { +client.on('message', (topic, message, packet) => { console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) }) -client.on('close', function () { +client.on('close', () => { console.log(clientId + ' disconnected') }) diff --git a/examples/wss/client_with_proxy.js b/examples/wss/client_with_proxy.js index 2337f660b..021964bfc 100644 --- a/examples/wss/client_with_proxy.js +++ b/examples/wss/client_with_proxy.js @@ -39,22 +39,22 @@ const mqttOptions = { const client = mqtt.connect(parsed, mqttOptions) -client.on('connect', function () { +client.on('connect', () => { console.log('connected') }) -client.on('error', function (a) { +client.on('error', (a) => { console.log('error!' + a) }) -client.on('offline', function (a) { +client.on('offline', (a) => { console.log('lost connection!' + a) }) -client.on('close', function (a) { +client.on('close', (a) => { console.log('connection closed!' + a) }) -client.on('message', function (topic, message) { +client.on('message', (topic, message) => { console.log(message.toString()) }) diff --git a/lib/client.js b/lib/client.js index e938a06bc..361b4b9cf 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,47 +1,53 @@ -'use strict' - /** * Module dependencies */ -const EventEmitter = require('events').EventEmitter -const Store = require('./store') +const { EventEmitter } = require('events') const TopicAliasRecv = require('./topic-alias-recv') const mqttPacket = require('mqtt-packet') const DefaultMessageIdProvider = require('./default-message-id-provider') -const Writable = require('readable-stream').Writable +const { Writable } = require('readable-stream') const reInterval = require('reinterval') const clone = require('rfdc/default') const validations = require('./validations') const debug = require('debug')('mqttjs:client') +const Store = require('./store') const handlePacket = require('./handlers') -const nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } -const setImmediate = global.setImmediate || function (...args) { - const callback = args.shift() - nextTick(callback.bind(null, ...args)) -} +const nextTick = process + ? process.nextTick + : (callback) => { + setTimeout(callback, 0) + } + +const setImmediate = + global.setImmediate || + ((...args) => { + const callback = args.shift() + nextTick(() => { + callback(...args) + }) + }) const defaultConnectOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - resubscribe: true, - writeCache: true + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true, + writeCache: true, } const socketErrors = [ - 'ECONNREFUSED', - 'EADDRINUSE', - 'ECONNRESET', - 'ENOTFOUND', - 'ETIMEDOUT' + 'ECONNREFUSED', + 'EADDRINUSE', + 'ECONNRESET', + 'ENOTFOUND', + 'ETIMEDOUT', ] - /** * MqttClient constructor * @@ -50,1444 +56,1613 @@ const socketErrors = [ * (see Connection#connect) */ class MqttClient extends EventEmitter { - - static defaultId() { - return 'mqttjs_' + Math.random().toString(16).substr(2, 8) - } - - constructor(streamBuilder, options) { - super() - - let k - const that = this - - this.options = options || {} - - // Defaults - for (k in defaultConnectOptions) { - if (typeof this.options[k] === 'undefined') { - this.options[k] = defaultConnectOptions[k] - } else { - this.options[k] = options[k] - } - } - - this.log = this.options.log || debug - this.nop = this._nop.bind(this) - - this.log('MqttClient :: options.protocol', options.protocol) - this.log('MqttClient :: options.protocolVersion', options.protocolVersion) - this.log('MqttClient :: options.username', options.username) - this.log('MqttClient :: options.keepalive', options.keepalive) - this.log('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) - this.log('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) - this.log('MqttClient :: options.properties.topicAliasMaximum', options.properties ? options.properties.topicAliasMaximum : undefined) - - this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : MqttClient.defaultId() - - this.log('MqttClient :: clientId', this.options.clientId) - - this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } - - // Disable pre-generated write cache if requested. Will allocate buffers on-the-fly instead. WARNING: This can affect write performance - if (!this.options.writeCache) { - mqttPacket.writeToStream.cacheNumbers = false - } - - this.streamBuilder = streamBuilder - - this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider - - // Inflight message storages - this.outgoingStore = options.outgoingStore || new Store() - this.incomingStore = options.incomingStore || new Store() - - // Should QoS zero messages be queued when the connection is broken? - this.queueQoSZero = options.queueQoSZero === undefined ? true : options.queueQoSZero - - // map of subscribed topics to support reconnection - this._resubscribeTopics = {} - - // map of a subscribe messageId and a topic - this.messageIdToTopic = {} - - // Ping timer, setup in _setupPingTimer - this.pingTimer = null - // Is the client connected? - this.connected = false - // Are we disconnecting? - this.disconnecting = false - // Packet queue - this.queue = [] - // connack timer - this.connackTimer = null - // Reconnect timer - this.reconnectTimer = null - // Is processing store? - this._storeProcessing = false - // Packet Ids are put into the store during store processing - this._packetIdsDuringStoreProcessing = {} - // Store processing queue - this._storeProcessingQueue = [] - - // Inflight callbacks - this.outgoing = {} - - // True if connection is first time. - this._firstConnection = true - - if (options.properties && options.properties.topicAliasMaximum > 0) { - if (options.properties.topicAliasMaximum > 0xffff) { - this.log('MqttClient :: options.properties.topicAliasMaximum is out of range') - } else { - this.topicAliasRecv = new TopicAliasRecv(options.properties.topicAliasMaximum) - } - } - - // Send queued packets - this.on('connect', function () { - const queue = that.queue - - function deliver() { - const entry = queue.shift() - that.log('deliver :: entry %o', entry) - let packet = null - - if (!entry) { - that._resubscribe() - return - } - - packet = entry.packet - that.log('deliver :: call _sendPacket for %o', packet) - let send = true - if (packet.messageId && packet.messageId !== 0) { - if (!that.messageIdProvider.register(packet.messageId)) { - send = false - } - } - if (send) { - that._sendPacket( - packet, - function (err) { - if (entry.cb) { - entry.cb(err) - } - deliver() - } - ) - } else { - that.log('messageId: %d has already used. The message is skipped and removed.', packet.messageId) - deliver() - } - } - - that.log('connect :: sending queued packets') - deliver() - }) - - this.on('close', function () { - that.log('close :: connected set to `false`') - that.connected = false - - that.log('close :: clearing connackTimer') - clearTimeout(that.connackTimer) - - that.log('close :: clearing ping timer') - if (that.pingTimer !== null) { - that.pingTimer.clear() - that.pingTimer = null - } - - if (that.topicAliasRecv) { - that.topicAliasRecv.clear() - } - - that.log('close :: calling _setupReconnect') - that._setupReconnect() - }) - - this.log('MqttClient :: setting up stream') - this._setupStream() - } - - /** - * setup the event handlers in the inner stream. - * - * @api private - */ - _setupStream() { - const that = this - const writable = new Writable() - const parser = mqttPacket.parser(this.options) - let completeParse = null - const packets = [] - - this.log('_setupStream :: calling method to clear reconnect') - this._clearReconnect() - - this.log('_setupStream :: using streamBuilder provided to client to create stream') - this.stream = this.streamBuilder(this) - - parser.on('packet', function (packet) { - that.log('parser :: on packet push to packets array.') - packets.push(packet) - }) - - function nextTickWork() { - if (packets.length) { - nextTick(work) - } else { - const done = completeParse - completeParse = null - done() - } - } - - function work() { - that.log('work :: getting next packet in queue') - const packet = packets.shift() - - if (packet) { - that.log('work :: packet pulled from queue') - handlePacket(that, packet, nextTickWork) - } else { - that.log('work :: no packets in queue') - const done = completeParse - completeParse = null - that.log('work :: done flag is %s', !!(done)) - if (done) done() - } - } - - writable._write = function (buf, enc, done) { - completeParse = done - that.log('writable stream :: parsing buffer') - parser.parse(buf) - work() - } - - function streamErrorHandler(error) { - that.log('streamErrorHandler :: error', error.message) - if (socketErrors.includes(error.code)) { - // handle error - that.log('streamErrorHandler :: emitting error') - that.emit('error', error) - } else { - that.nop(error) - } - } - - this.log('_setupStream :: pipe stream to writable stream') - this.stream.pipe(writable) - - // Suppress connection errors - this.stream.on('error', streamErrorHandler) - - // Echo stream close - this.stream.on('close', function () { - that.log('(%s)stream :: on close', that.options.clientId) - that._flushVolatile(that.outgoing) - that.log('stream: emit close to MqttClient') - that.emit('close') - }) - - // Send a connect packet - this.log('_setupStream: sending packet `connect`') - const connectPacket = Object.create(this.options) - connectPacket.cmd = 'connect' - if (this.topicAliasRecv) { - if (!connectPacket.properties) { - connectPacket.properties = {} - } - if (this.topicAliasRecv) { - connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max - } - } - // avoid message queue - this._writePacket(connectPacket) - - // Echo connection errors - parser.on('error', this.emit.bind(this, 'error')) - - // auth - if (this.options.properties) { - if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { - that.end(() => this.emit('error', new Error('Packet has no Authentication Method') - )) - return this - } - if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { - const authPacket = { cmd: 'auth', reasonCode: 0, ...this.options.authPacket } - this._writePacket(authPacket) - } - } - - // many drain listeners are needed for qos 1 callbacks if the connection is intermittent - this.stream.setMaxListeners(1000) - - clearTimeout(this.connackTimer) - this.connackTimer = setTimeout(function () { - that.log('!!connectTimeout hit!! Calling _cleanUp with force `true`') - that._cleanUp(true) - }, this.options.connectTimeout) - } - - _flushVolatile(queue) { - if (queue) { - this.log('_flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') - Object.keys(queue).forEach(function (messageId) { - if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - delete queue[messageId] - } - }) - } - } - - _flush(queue) { - if (queue) { - this.log('_flush: queue exists? %b', !!(queue)) - Object.keys(queue).forEach(function (messageId) { - if (typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - // This is suspicious. Why do we only delete this if we have a callback? - // If this is by-design, then adding no as callback would cause this to get deleted unintentionally. - delete queue[messageId] - } - }) - } - } - - _removeTopicAliasAndRecoverTopicName(packet) { - let alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - - let topic = packet.topic.toString() - - this.log('_removeTopicAliasAndRecoverTopicName :: alias %d, topic %o', alias, topic) - - if (topic.length === 0) { - // restore topic from alias - if (typeof alias === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - topic = this.topicAliasSend.getTopicByAlias(alias) - if (typeof topic === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - packet.topic = topic - } - } - } - if (alias) { - delete packet.properties.topicAlias - } - } - - _checkDisconnecting(callback) { - if (this.disconnecting) { - if (callback && callback !== this.nop) { - callback(new Error('client disconnecting')) - } else { - this.emit('error', new Error('client disconnecting')) - } - } - return this.disconnecting - } - - /** - * publish - publish to - * - * @param {String} topic - topic to publish to - * @param {String, Buffer} message - message to publish - * @param {Object} [opts] - publish options, includes: - * {Number} qos - qos level to publish on - * {Boolean} retain - whether or not to retain the message - * {Boolean} dup - whether or not mark a message as duplicate - * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` - * @param {Function} [callback] - function(err){} - * called when publish succeeds or fails - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.publish('topic', 'message'); - * @example - * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); - * @example client.publish('topic', 'message', console.log); - */ - publish(topic, message, opts, callback) { - this.log('publish :: message `%s` to topic `%s`', message, topic) - const options = this.options - - // .publish(topic, payload, cb); - if (typeof opts === 'function') { - callback = opts - opts = null - } - - // default opts - const defaultOpts = { qos: 0, retain: false, dup: false } - opts = { ...defaultOpts, ...opts } - - if (this._checkDisconnecting(callback)) { - return this - } - - const that = this - const publishProc = function () { - let messageId = 0 - if (opts.qos === 1 || opts.qos === 2) { - messageId = that._nextId() - if (messageId === null) { - that.log('No messageId left') - return false - } - } - const packet = { - cmd: 'publish', - topic, - payload: message, - qos: opts.qos, - retain: opts.retain, - messageId, - dup: opts.dup - } - - if (options.protocolVersion === 5) { - packet.properties = opts.properties - } - - that.log('publish :: qos', opts.qos) - switch (opts.qos) { - case 1: - case 2: - // Add to callbacks - that.outgoing[packet.messageId] = { - volatile: false, - cb: callback || that.nop - } - that.log('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, undefined, opts.cbStorePut) - break - default: - that.log('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, callback, opts.cbStorePut) - break - } - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0 || !publishProc()) { - this._storeProcessingQueue.push( - { - invoke: publishProc, - cbStorePut: opts.cbStorePut, - callback - } - ) - } - return this - } - - /** - * subscribe - subscribe to - * - * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} - * @param {Object} [opts] - optional subscription options, includes: - * {Number} qos - subscribe qos level - * @param {Function} [callback] - function(err, granted){} where: - * {Error} err - subscription error (none at the moment!) - * {Array} granted - array of {topic: 't', qos: 0} - * @returns {MqttClient} this - for chaining - * @api public - * @example client.subscribe('topic'); - * @example client.subscribe('topic', {qos: 1}); - * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); - * @example client.subscribe('topic', console.log); - */ - subscribe() { - const that = this - const args = new Array(arguments.length) - for (let i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - const subs = [] - let obj = args.shift() - const resubscribe = obj.resubscribe - let callback = args.pop() || this.nop - let opts = args.pop() - const version = this.options.protocolVersion - - delete obj.resubscribe - - if (typeof obj === 'string') { - obj = [obj] - } - - if (typeof callback !== 'function') { - opts = callback - callback = this.nop - } - - const invalidTopic = validations.validateTopics(obj) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (this._checkDisconnecting(callback)) { - this.log('subscribe: discconecting true') - return this - } - - const defaultOpts = { - qos: 0 - } - if (version === 5) { - defaultOpts.nl = false - defaultOpts.rap = false - defaultOpts.rh = 0 - } - opts = { ...defaultOpts, ...opts } - - function parseSub(topic, subOptions) { - // subOptions is defined only when providing a subs map, use opts otherwise - subOptions = subOptions || opts - if (!Object.prototype.hasOwnProperty.call(that._resubscribeTopics, topic) || - that._resubscribeTopics[topic].qos < subOptions.qos || - resubscribe) { - const currentOpts = { - topic, - qos: subOptions.qos - } - if (version === 5) { - currentOpts.nl = subOptions.nl - currentOpts.rap = subOptions.rap - currentOpts.rh = subOptions.rh - // use opts.properties - currentOpts.properties = opts.properties - } - that.log('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) - subs.push(currentOpts) - } - } - - if (Array.isArray(obj)) { - // array of topics - obj.forEach(function (topic) { - that.log('subscribe: array topic %s', topic) - parseSub(topic) - }) - } else { - // object topic --> subOptions (no properties) - Object - .keys(obj) - .forEach(function (topic) { - that.log('subscribe: object topic %s, %o', topic, obj[topic]) - parseSub(topic, obj[topic]) - }) - } - - if (!subs.length) { - callback(null, []) - return this - } - - const subscribeProc = function () { - const messageId = that._nextId() - if (messageId === null) { - that.log('No messageId left') - return false - } - - const packet = { - cmd: 'subscribe', - subscriptions: subs, - qos: 1, - retain: false, - dup: false, - messageId - } - - if (opts.properties) { - packet.properties = opts.properties - } - - // subscriptions to resubscribe to in case of disconnect - if (that.options.resubscribe) { - that.log('subscribe :: resubscribe true') - const topics = [] - subs.forEach(function (sub) { - if (that.options.reconnectPeriod > 0) { - const topic = { qos: sub.qos } - if (version === 5) { - topic.nl = sub.nl || false - topic.rap = sub.rap || false - topic.rh = sub.rh || 0 - topic.properties = sub.properties - } - that._resubscribeTopics[sub.topic] = topic - topics.push(sub.topic) - } - }) - that.messageIdToTopic[packet.messageId] = topics - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: function (err, packet) { - if (!err) { - const granted = packet.granted - for (let i = 0; i < granted.length; i += 1) { - subs[i].qos = granted[i] - } - } - - callback(err, subs) - } - } - that.log('subscribe :: call _sendPacket') - that._sendPacket(packet) - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0 || !subscribeProc()) { - this._storeProcessingQueue.push( - { - invoke: subscribeProc, - callback - } - ) - } - - return this - } - - /** - * unsubscribe - unsubscribe from topic(s) - * - * @param {String, Array} topic - topics to unsubscribe from - * @param {Object} [opts] - optional subscription options, includes: - * {Object} properties - properties of unsubscribe packet - * @param {Function} [callback] - callback fired on unsuback - * @returns {MqttClient} this - for chaining - * @api public - * @example client.unsubscribe('topic'); - * @example client.unsubscribe('topic', console.log); - */ - unsubscribe() { - const that = this - const args = new Array(arguments.length) - for (let i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - let topic = args.shift() - let callback = args.pop() || this.nop - let opts = args.pop() - if (typeof topic === 'string') { - topic = [topic] - } - - if (typeof callback !== 'function') { - opts = callback - callback = this.nop - } - - const invalidTopic = validations.validateTopics(topic) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (that._checkDisconnecting(callback)) { - return this - } - - const unsubscribeProc = function () { - const messageId = that._nextId() - if (messageId === null) { - that.log('No messageId left') - return false - } - const packet = { - cmd: 'unsubscribe', - qos: 1, - messageId - } - - if (typeof topic === 'string') { - packet.unsubscriptions = [topic] - } else if (Array.isArray(topic)) { - packet.unsubscriptions = topic - } - - if (that.options.resubscribe) { - packet.unsubscriptions.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } - - if (typeof opts === 'object' && opts.properties) { - packet.properties = opts.properties - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: callback - } - - that.log('unsubscribe: call _sendPacket') - that._sendPacket(packet) - - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0 || !unsubscribeProc()) { - this._storeProcessingQueue.push( - { - invoke: unsubscribeProc, - callback - } - ) - } - - return this - } - - /** - * end - close connection - * - * @returns {MqttClient} this - for chaining - * @param {Boolean} force - do not wait for all in-flight messages to be acked - * @param {Object} opts - added to the disconnect packet - * @param {Function} cb - called when the client has been closed - * - * @api public - */ - end(force, opts, cb) { - const that = this - - this.log('end :: (%s)', this.options.clientId) - - if (force == null || typeof force !== 'boolean') { - cb = opts || this.nop - opts = force - force = false - if (typeof opts !== 'object') { - cb = opts - opts = null - if (typeof cb !== 'function') { - cb = this.nop - } - } - } - - if (typeof opts !== 'object') { - cb = opts - opts = null - } - - this.log('end :: cb? %s', !!cb) - cb = cb || this.nop - - function closeStores() { - that.log('end :: closeStores: closing incoming and outgoing stores') - that.disconnected = true - that.incomingStore.close(function (e1) { - that.outgoingStore.close(function (e2) { - that.log('end :: closeStores: emitting end') - that.emit('end') - if (cb) { - const err = e1 || e2 - that.log('end :: closeStores: invoking callback with args') - cb(err) - } - }) - }) - if (that._deferredReconnect) { - that._deferredReconnect() - } - } - - function finish() { - // defer closesStores of an I/O cycle, - // just to make sure things are - // ok for websockets - that.log('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) - that._cleanUp(force, () => { - that.log('end :: finish :: calling process.nextTick on closeStores') - // const boundProcess = nextTick.bind(null, closeStores) - nextTick(closeStores.bind(that)) - }, opts) - } - - if (this.disconnecting) { - cb() - return this - } - - this._clearReconnect() - - this.disconnecting = true - - if (!force && Object.keys(this.outgoing).length > 0) { - // wait 10ms, just to be sure we received all of it - this.log('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) - this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) - } else { - this.log('end :: (%s) :: immediately calling finish', that.options.clientId) - finish() - } - - return this - } - - /** - * removeOutgoingMessage - remove a message in outgoing store - * the outgoing callback will be called withe Error('Message removed') if the message is removed - * - * @param {Number} messageId - messageId to remove message - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.removeOutgoingMessage(client.getLastAllocated()); - */ - removeOutgoingMessage(messageId) { - if (this.outgoing[messageId]) { - const cb = this.outgoing[messageId].cb - this._removeOutgoingAndStoreMessage(messageId, function () { - cb(new Error('Message removed')) - }) - } - return this - } - - /** - * reconnect - connect again using the same options as connect() - * - * @param {Object} [opts] - optional reconnect options, includes: - * {Store} incomingStore - a store for the incoming packets - * {Store} outgoingStore - a store for the outgoing packets - * if opts is not given, current stores are used - * @returns {MqttClient} this - for chaining - * - * @api public - */ - reconnect(opts) { - this.log('client reconnect') - const that = this - const f = function () { - if (opts) { - that.options.incomingStore = opts.incomingStore - that.options.outgoingStore = opts.outgoingStore - } else { - that.options.incomingStore = null - that.options.outgoingStore = null - } - that.incomingStore = that.options.incomingStore || new Store() - that.outgoingStore = that.options.outgoingStore || new Store() - that.disconnecting = false - that.disconnected = false - that._deferredReconnect = null - that._reconnect() - } - - if (this.disconnecting && !this.disconnected) { - this._deferredReconnect = f - } else { - f() - } - return this - } - - /** - * _reconnect - implement reconnection - * @api privateish - */ - _reconnect() { - this.log('_reconnect: emitting reconnect to client') - this.emit('reconnect') - if (this.connected) { - this.end(() => { this._setupStream() }) - this.log('client already connected. disconnecting first.') - } else { - this.log('_reconnect: calling _setupStream') - this._setupStream() - } - } - - /** - * _setupReconnect - setup reconnect timer - */ - _setupReconnect() { - const that = this - - if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { - if (!this.reconnecting) { - this.log('_setupReconnect :: emit `offline` state') - this.emit('offline') - this.log('_setupReconnect :: set `reconnecting` to `true`') - this.reconnecting = true - } - this.log('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) - that.reconnectTimer = setInterval(function () { - that.log('reconnectTimer :: reconnect triggered!') - that._reconnect() - }, that.options.reconnectPeriod) - } else { - this.log('_setupReconnect :: doing nothing...') - } - } - - /** - * _clearReconnect - clear the reconnect timer - */ - _clearReconnect() { - this.log('_clearReconnect : clearing reconnect timer') - if (this.reconnectTimer) { - clearInterval(this.reconnectTimer) - this.reconnectTimer = null - } - } - - /** - * _cleanUp - clean up on connection end - * @api private - */ - _cleanUp(forced, done) { - const opts = arguments[2] || {} - if (done) { - this.log('_cleanUp :: done callback provided for on stream close') - this.stream.on('close', done) - } - - this.log('_cleanUp :: forced? %s', forced) - if (forced) { - if ((this.options.reconnectPeriod === 0) && this.options.clean) { - this._flush(this.outgoing) - } - this.log('_cleanUp :: (%s) :: destroying stream', this.options.clientId) - this.stream.destroy() - } else { - const packet = { cmd: 'disconnect', ...opts } - this.log('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) - this._sendPacket( - packet, - () => { - this.log('_cleanUp :: (%s) :: destroying stream', this.options.clientId) - setImmediate( - () => { - this.stream.end(() => { - this.log('_cleanUp :: (%s) :: stream destroyed', this.options.clientId) - // once stream is closed the 'close' event will fire and that will - // emit client `close` event and call `done` callback if done is provided - }) - } - ) - } - ) - } - - if (!this.disconnecting) { - this.log('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') - this._clearReconnect() - this._setupReconnect() - } - - if (this.pingTimer !== null) { - this.log('_cleanUp :: clearing pingTimer') - this.pingTimer.clear() - this.pingTimer = null - } - - if (done && !this.connected) { - this.log('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) - this.stream.removeListener('close', done) - done() - } - } - - _storeAndSend(packet, cb, cbStorePut) { - this.log('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) - let storePacket = packet - let err - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - err = this._removeTopicAliasAndRecoverTopicName(storePacket) - if (err) { - return cb && cb(err) - } - } - const that = this - this.outgoingStore.put(storePacket, function storedPacket(err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - that._writePacket(packet, cb) - }) - } - - _applyTopicAlias(packet) { - if (this.options.protocolVersion === 5) { - if (packet.cmd === 'publish') { - let alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - const topic = packet.topic.toString() - if (this.topicAliasSend) { - if (alias) { - if (topic.length !== 0) { - // register topic alias - this.log('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) - if (!this.topicAliasSend.put(topic, alias)) { - this.log('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } else { - if (topic.length !== 0) { - if (this.options.autoAssignTopicAlias) { - alias = this.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = { ...(packet.properties), topicAlias: alias } - this.log('applyTopicAlias :: auto assign(use) topic: %s - alias: %d', topic, alias) - } else { - alias = this.topicAliasSend.getLruAlias() - this.topicAliasSend.put(topic, alias) - packet.properties = { ...(packet.properties), topicAlias: alias } - this.log('applyTopicAlias :: auto assign topic: %s - alias: %d', topic, alias) - } - } else if (this.options.autoUseTopicAlias) { - alias = this.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = { ...(packet.properties), topicAlias: alias } - this.log('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) - } - } - } - } - } else if (alias) { - this.log('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } - } - - _nop(err) { - this.log('nop ::', err) - } - - /** Writes the packet to stream and emits events */ - _writePacket(packet, cb) { - this.log('_writePacket :: packet: %O', packet) - this.log('_writePacket :: emitting `packetsend`') - - this.emit('packetsend', packet) - - // When writing a packet, reschedule the ping timer - this._shiftPingInterval() - - this.log('_writePacket :: writing to stream') - const result = mqttPacket.writeToStream(packet, this.stream, this.options) - this.log('_writePacket :: writeToStream result %s', result) - if (!result && cb && cb !== this.nop) { - this.log('_writePacket :: handle events on `drain` once through callback.') - this.stream.once('drain', cb) - } else if (cb) { - this.log('_writePacket :: invoking cb') - cb() - } - } - - /** - * _sendPacket - send or queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @param {Boolean} noStore - send without put to the store - * @api private - */ - _sendPacket(packet, cb, cbStorePut, noStore) { - this.log('_sendPacket :: (%s) :: start', this.options.clientId) - cbStorePut = cbStorePut || this.nop - cb = cb || this.nop - - const err = this._applyTopicAlias(packet) - if (err) { - cb(err) - return - } - - if (!this.connected) { - // allow auth packets to be sent while authenticating with the broker (mqtt5 enhanced auth) - if (packet.cmd === 'auth') { - this._writePacket(this, packet, cb) - return - } - - this.log('_sendPacket :: client not connected. Storing packet offline.') - this._storePacket(packet, cb, cbStorePut) - return - } - - // If "noStore" is true, the message is sent without being recorded in the store. - // Messages that have not received puback or pubcomp remain in the store after disconnection - // and are resent from the store upon reconnection. - // For resend upon reconnection, "noStore" is set to true. This is because the message is already stored in the store. - // This is to avoid interrupting other processes while recording to the store. - if (noStore) { - this._writePacket(packet, cb) - return - } - - switch (packet.cmd) { - case 'publish': - break - case 'pubrel': - this._storeAndSend(packet, cb, cbStorePut) - return - default: - this._writePacket(packet, cb) - return - } - - switch (packet.qos) { - case 2: - case 1: - this._storeAndSend(packet, cb, cbStorePut) - break - /** - * no need of case here since it will be caught by default - * and jshint comply that before default it must be a break - * anyway it will result in -1 evaluation - */ - case 0: - /* falls through */ - default: - this._writePacket(packet, cb) - break - } - this.log('_sendPacket :: (%s) :: end', this.options.clientId) - } - - /** - * _storePacket - queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @api private - */ - _storePacket(packet, cb, cbStorePut) { - this.log('_storePacket :: packet: %o', packet) - this.log('_storePacket :: cb? %s', !!cb) - cbStorePut = cbStorePut || this.nop - - let storePacket = packet - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - const err = this._removeTopicAliasAndRecoverTopicName(storePacket) - if (err) { - return cb && cb(err) - } - } - // check that the packet is not a qos of 0, or that the command is not a publish - if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { - this.queue.push({ packet: storePacket, cb }) - } else if (storePacket.qos > 0) { - cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null - this.outgoingStore.put(storePacket, function (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - }) - } else if (cb) { - cb(new Error('No connection to broker')) - } - } - - /** - * _setupPingTimer - setup the ping timer - * - * @api private - */ - _setupPingTimer() { - this.log('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) - const that = this - - if (!this.pingTimer && this.options.keepalive) { - this.pingResp = true - this.pingTimer = reInterval(function () { - that._checkPing() - }, this.options.keepalive * 1000) - } - } - - /** - * _shiftPingInterval - reschedule the ping interval - * - * @api private - */ - _shiftPingInterval() { - if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { - this.pingTimer.reschedule(this.options.keepalive * 1000) - } - } - - /** - * _checkPing - check if a pingresp has come back, and ping the server again - * - * @api private - */ - _checkPing() { - this.log('_checkPing :: checking ping...') - if (this.pingResp) { - this.log('_checkPing :: ping response received. Clearing flag and sending `pingreq`') - this.pingResp = false - this._sendPacket({ cmd: 'pingreq' }) - } else { - // do a forced cleanup since socket will be in bad shape - this.log('_checkPing :: calling _cleanUp with force true') - this._cleanUp(true) - } - } - - /** - * @param packet the packet received by the broker - * @return the auth packet to be returned to the broker - * @api public - */ - handleAuth(packet, callback) { - callback() - } - - /** - * Handle messages with backpressure support, one at a time. - * Override at will. - * - * @param Packet packet the packet - * @param Function callback call when finished - * @api public - */ - handleMessage(packet, callback) { - callback() - } - - /** - * _nextId - * @return unsigned int - */ - _nextId() { - return this.messageIdProvider.allocate() - } - - /** - * getLastMessageId - * @return unsigned int - */ - getLastMessageId() { - return this.messageIdProvider.getLastAllocated() - } - - /** - * _resubscribe - * @api private - */ - _resubscribe() { - this.log('_resubscribe') - const _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) - if (!this._firstConnection && - (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && - _resubscribeTopicsKeys.length > 0) { - if (this.options.resubscribe) { - if (this.options.protocolVersion === 5) { - this.log('_resubscribe: protocolVersion 5') - for (let topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { - const resubscribeTopic = {} - resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] - resubscribeTopic.resubscribe = true - this.subscribe(resubscribeTopic, { properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties }) - } - } else { - this._resubscribeTopics.resubscribe = true - this.subscribe(this._resubscribeTopics) - } - } else { - this._resubscribeTopics = {} - } - } - - this._firstConnection = false - } - - /** - * _onConnect - * - * @api private - */ - _onConnect(packet) { - if (this.disconnected) { - this.emit('connect', packet) - return - } - - const that = this - - this.connackPacket = packet - this.messageIdProvider.clear() - this._setupPingTimer() - - this.connected = true - - function startStreamProcess() { - let outStore = that.outgoingStore.createStream() - - function clearStoreProcessing() { - that._storeProcessing = false - that._packetIdsDuringStoreProcessing = {} - } - - that.once('close', remove) - outStore.on('error', function (err) { - clearStoreProcessing() - that._flushStoreProcessingQueue() - that.removeListener('close', remove) - that.emit('error', err) - }) - - function remove() { - outStore.destroy() - outStore = null - that._flushStoreProcessingQueue() - clearStoreProcessing() - } - - function storeDeliver() { - // edge case, we wrapped this twice - if (!outStore) { - return - } - - const packet = outStore.read(1) - - let cb - - if (!packet) { - // read when data is available in the future - outStore.once('readable', storeDeliver) - return - } - - that._storeProcessing = true - - // Skip already processed store packets - if (that._packetIdsDuringStoreProcessing[packet.messageId]) { - storeDeliver() - return - } - - // Avoid unnecessary stream read operations when disconnected - if (!that.disconnecting && !that.reconnectTimer) { - cb = that.outgoing[packet.messageId] ? that.outgoing[packet.messageId].cb : null - that.outgoing[packet.messageId] = { - volatile: false, - cb: function (err, status) { - // Ensure that the original callback passed in to publish gets invoked - if (cb) { - cb(err, status) - } - - storeDeliver() - } - } - that._packetIdsDuringStoreProcessing[packet.messageId] = true - if (that.messageIdProvider.register(packet.messageId)) { - that._sendPacket(packet, undefined, undefined, true) - } else { - that.log('messageId: %d has already used.', packet.messageId) - } - } else if (outStore.destroy) { - outStore.destroy() - } - } - - outStore.on('end', function () { - let allProcessed = true - for (const id in that._packetIdsDuringStoreProcessing) { - if (!that._packetIdsDuringStoreProcessing[id]) { - allProcessed = false - break - } - } - if (allProcessed) { - clearStoreProcessing() - that.removeListener('close', remove) - that._invokeAllStoreProcessingQueue() - that.emit('connect', packet) - } else { - startStreamProcess() - } - }) - storeDeliver() - } - // start flowing - startStreamProcess() - } - - _invokeStoreProcessingQueue() { - // If _storeProcessing is true, the message is resending. - // During resend, processing is skipped to prevent new messages from interrupting. #1635 - if (!this._storeProcessing && this._storeProcessingQueue.length > 0) { - const f = this._storeProcessingQueue[0] - if (f && f.invoke()) { - this._storeProcessingQueue.shift() - return true - } - } - return false - } - - _invokeAllStoreProcessingQueue() { - while (this._invokeStoreProcessingQueue()) { /* empty */ } - } - - _flushStoreProcessingQueue() { - for (const f of this._storeProcessingQueue) { - if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) - if (f.callback) f.callback(new Error('Connection closed')) - } - this._storeProcessingQueue.splice(0) - } - - /** - * _removeOutgoingAndStoreMessage - * @param {Number} messageId - messageId to remove message - * @param {Function} cb - called when the message removed - * @api private - */ - _removeOutgoingAndStoreMessage(messageId, cb) { - const self = this - delete this.outgoing[messageId] - self.outgoingStore.del({ messageId }, function (err, packet) { - cb(err, packet) - self.messageIdProvider.deallocate(messageId) - self._invokeStoreProcessingQueue() - }) - } + static defaultId() { + return `mqttjs_${Math.random().toString(16).substr(2, 8)}` + } + + constructor(streamBuilder, options) { + super() + + let k + + this.options = options || {} + + // Defaults + for (k in defaultConnectOptions) { + if (typeof this.options[k] === 'undefined') { + this.options[k] = defaultConnectOptions[k] + } else { + this.options[k] = options[k] + } + } + + this.log = this.options.log || debug + this.noop = this._noop.bind(this) + + this.log('MqttClient :: options.protocol', options.protocol) + this.log( + 'MqttClient :: options.protocolVersion', + options.protocolVersion, + ) + this.log('MqttClient :: options.username', options.username) + this.log('MqttClient :: options.keepalive', options.keepalive) + this.log( + 'MqttClient :: options.reconnectPeriod', + options.reconnectPeriod, + ) + this.log( + 'MqttClient :: options.rejectUnauthorized', + options.rejectUnauthorized, + ) + this.log( + 'MqttClient :: options.properties.topicAliasMaximum', + options.properties + ? options.properties.topicAliasMaximum + : undefined, + ) + + this.options.clientId = + typeof options.clientId === 'string' + ? options.clientId + : MqttClient.defaultId() + + this.log('MqttClient :: clientId', this.options.clientId) + + this.options.customHandleAcks = + options.protocolVersion === 5 && options.customHandleAcks + ? options.customHandleAcks + : (...args) => { + args[3](0) + } + + // Disable pre-generated write cache if requested. Will allocate buffers on-the-fly instead. WARNING: This can affect write performance + if (!this.options.writeCache) { + mqttPacket.writeToStream.cacheNumbers = false + } + + this.streamBuilder = streamBuilder + + this.messageIdProvider = + typeof this.options.messageIdProvider === 'undefined' + ? new DefaultMessageIdProvider() + : this.options.messageIdProvider + + // Inflight message storages + this.outgoingStore = options.outgoingStore || new Store() + this.incomingStore = options.incomingStore || new Store() + + // Should QoS zero messages be queued when the connection is broken? + this.queueQoSZero = + options.queueQoSZero === undefined ? true : options.queueQoSZero + + // map of subscribed topics to support reconnection + this._resubscribeTopics = {} + + // map of a subscribe messageId and a topic + this.messageIdToTopic = {} + + // Ping timer, setup in _setupPingTimer + this.pingTimer = null + // Is the client connected? + this.connected = false + // Are we disconnecting? + this.disconnecting = false + // Packet queue + this.queue = [] + // connack timer + this.connackTimer = null + // Reconnect timer + this.reconnectTimer = null + // Is processing store? + this._storeProcessing = false + // Packet Ids are put into the store during store processing + this._packetIdsDuringStoreProcessing = {} + // Store processing queue + this._storeProcessingQueue = [] + + // Inflight callbacks + this.outgoing = {} + + // True if connection is first time. + this._firstConnection = true + + if (options.properties && options.properties.topicAliasMaximum > 0) { + if (options.properties.topicAliasMaximum > 0xffff) { + this.log( + 'MqttClient :: options.properties.topicAliasMaximum is out of range', + ) + } else { + this.topicAliasRecv = new TopicAliasRecv( + options.properties.topicAliasMaximum, + ) + } + } + + // Send queued packets + this.on('connect', () => { + const { queue } = this + + const deliver = () => { + const entry = queue.shift() + this.log('deliver :: entry %o', entry) + let packet = null + + if (!entry) { + this._resubscribe() + return + } + + packet = entry.packet + this.log('deliver :: call _sendPacket for %o', packet) + let send = true + if (packet.messageId && packet.messageId !== 0) { + if (!this.messageIdProvider.register(packet.messageId)) { + send = false + } + } + if (send) { + this._sendPacket(packet, (err) => { + if (entry.cb) { + entry.cb(err) + } + deliver() + }) + } else { + this.log( + 'messageId: %d has already used. The message is skipped and removed.', + packet.messageId, + ) + deliver() + } + } + + this.log('connect :: sending queued packets') + deliver() + }) + + this.on('close', () => { + this.log('close :: connected set to `false`') + this.connected = false + + this.log('close :: clearing connackTimer') + clearTimeout(this.connackTimer) + + this.log('close :: clearing ping timer') + if (this.pingTimer !== null) { + this.pingTimer.clear() + this.pingTimer = null + } + + if (this.topicAliasRecv) { + this.topicAliasRecv.clear() + } + + this.log('close :: calling _setupReconnect') + this._setupReconnect() + }) + + if (!this.options.manualConnect) { + this.log('MqttClient :: setting up stream') + this.connect() + } + } + + /** + * Setup the event handlers in the inner stream, sends `connect` and `auth` packets + */ + connect() { + const writable = new Writable() + const parser = mqttPacket.parser(this.options) + let completeParse = null + const packets = [] + + this.log('connect :: calling method to clear reconnect') + this._clearReconnect() + + this.log( + 'connect :: using streamBuilder provided to client to create stream', + ) + this.stream = this.streamBuilder(this) + + parser.on('packet', (packet) => { + this.log('parser :: on packet push to packets array.') + packets.push(packet) + }) + + const work = () => { + this.log('work :: getting next packet in queue') + const packet = packets.shift() + + if (packet) { + this.log('work :: packet pulled from queue') + handlePacket(this, packet, nextTickWork) + } else { + this.log('work :: no packets in queue') + const done = completeParse + completeParse = null + this.log('work :: done flag is %s', !!done) + if (done) done() + } + } + + const nextTickWork = () => { + if (packets.length) { + nextTick(work) + } else { + const done = completeParse + completeParse = null + done() + } + } + + writable._write = (buf, enc, done) => { + completeParse = done + this.log('writable stream :: parsing buffer') + parser.parse(buf) + work() + } + + const streamErrorHandler = (error) => { + this.log('streamErrorHandler :: error', error.message) + if (socketErrors.includes(error.code)) { + // handle error + this.log('streamErrorHandler :: emitting error') + this.emit('error', error) + } else { + this.noop(error) + } + } + + this.log('connect :: pipe stream to writable stream') + this.stream.pipe(writable) + + // Suppress connection errors + this.stream.on('error', streamErrorHandler) + + // Echo stream close + this.stream.on('close', () => { + this.log('(%s)stream :: on close', this.options.clientId) + this._flushVolatile(this.outgoing) + this.log('stream: emit close to MqttClient') + this.emit('close') + }) + + // Send a connect packet + this.log('connect: sending packet `connect`') + const connectPacket = Object.create(this.options) + connectPacket.cmd = 'connect' + if (this.topicAliasRecv) { + if (!connectPacket.properties) { + connectPacket.properties = {} + } + if (this.topicAliasRecv) { + connectPacket.properties.topicAliasMaximum = + this.topicAliasRecv.max + } + } + // avoid message queue + this._writePacket(connectPacket) + + // Echo connection errors + parser.on('error', this.emit.bind(this, 'error')) + + // auth + if (this.options.properties) { + if ( + !this.options.properties.authenticationMethod && + this.options.properties.authenticationData + ) { + this.end(() => + this.emit( + 'error', + new Error('Packet has no Authentication Method'), + ), + ) + return this + } + if ( + this.options.properties.authenticationMethod && + this.options.authPacket && + typeof this.options.authPacket === 'object' + ) { + const authPacket = { + cmd: 'auth', + reasonCode: 0, + ...this.options.authPacket, + } + this._writePacket(authPacket) + } + } + + // many drain listeners are needed for qos 1 callbacks if the connection is intermittent + this.stream.setMaxListeners(1000) + + clearTimeout(this.connackTimer) + this.connackTimer = setTimeout(() => { + this.log( + '!!connectTimeout hit!! Calling _cleanUp with force `true`', + ) + this._cleanUp(true) + }, this.options.connectTimeout) + + return this + } + + _flushVolatile(queue) { + if (queue) { + this.log( + '_flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function', + ) + Object.keys(queue).forEach((messageId) => { + if ( + queue[messageId].volatile && + typeof queue[messageId].cb === 'function' + ) { + queue[messageId].cb(new Error('Connection closed')) + delete queue[messageId] + } + }) + } + } + + _flush(queue) { + if (queue) { + this.log('_flush: queue exists? %b', !!queue) + Object.keys(queue).forEach((messageId) => { + if (typeof queue[messageId].cb === 'function') { + queue[messageId].cb(new Error('Connection closed')) + // This is suspicious. Why do we only delete this if we have a callback? + // If this is by-design, then adding no as callback would cause this to get deleted unintentionally. + delete queue[messageId] + } + }) + } + } + + _removeTopicAliasAndRecoverTopicName(packet) { + let alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + + let topic = packet.topic.toString() + + this.log( + '_removeTopicAliasAndRecoverTopicName :: alias %d, topic %o', + alias, + topic, + ) + + if (topic.length === 0) { + // restore topic from alias + if (typeof alias === 'undefined') { + return new Error('Unregistered Topic Alias') + } + topic = this.topicAliasSend.getTopicByAlias(alias) + if (typeof topic === 'undefined') { + return new Error('Unregistered Topic Alias') + } + packet.topic = topic + } + if (alias) { + delete packet.properties.topicAlias + } + } + + _checkDisconnecting(callback) { + if (this.disconnecting) { + if (callback && callback !== this.noop) { + callback(new Error('client disconnecting')) + } else { + this.emit('error', new Error('client disconnecting')) + } + } + return this.disconnecting + } + + /** + * publish - publish to + * + * @param {String} topic - topic to publish to + * @param {String, Buffer} message - message to publish + * @param {Object} [opts] - publish options, includes: + * {Number} qos - qos level to publish on + * {Boolean} retain - whether or not to retain the message + * {Boolean} dup - whether or not mark a message as duplicate + * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` + * @param {Function} [callback] - function(err){} + * called when publish succeeds or fails + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.publish('topic', 'message'); + * @example + * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); + * @example client.publish('topic', 'message', console.log); + */ + publish(topic, message, opts, callback) { + this.log('publish :: message `%s` to topic `%s`', message, topic) + const { options } = this + + // .publish(topic, payload, cb); + if (typeof opts === 'function') { + callback = opts + opts = null + } + + // default opts + const defaultOpts = { qos: 0, retain: false, dup: false } + opts = { ...defaultOpts, ...opts } + + if (this._checkDisconnecting(callback)) { + return this + } + + const publishProc = () => { + let messageId = 0 + if (opts.qos === 1 || opts.qos === 2) { + messageId = this._nextId() + if (messageId === null) { + this.log('No messageId left') + return false + } + } + const packet = { + cmd: 'publish', + topic, + payload: message, + qos: opts.qos, + retain: opts.retain, + messageId, + dup: opts.dup, + } + + if (options.protocolVersion === 5) { + packet.properties = opts.properties + } + + this.log('publish :: qos', opts.qos) + switch (opts.qos) { + case 1: + case 2: + // Add to callbacks + this.outgoing[packet.messageId] = { + volatile: false, + cb: callback || this.noop, + } + this.log('MqttClient:publish: packet cmd: %s', packet.cmd) + this._sendPacket(packet, undefined, opts.cbStorePut) + break + default: + this.log('MqttClient:publish: packet cmd: %s', packet.cmd) + this._sendPacket(packet, callback, opts.cbStorePut) + break + } + return true + } + + if ( + this._storeProcessing || + this._storeProcessingQueue.length > 0 || + !publishProc() + ) { + this._storeProcessingQueue.push({ + invoke: publishProc, + cbStorePut: opts.cbStorePut, + callback, + }) + } + return this + } + + /** + * subscribe - subscribe to + * + * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} + * @param {Object} [opts] - optional subscription options, includes: + * {Number} qos - subscribe qos level + * @param {Function} [callback] - function(err, granted){} where: + * {Error} err - subscription error (none at the moment!) + * {Array} granted - array of {topic: 't', qos: 0} + * @returns {MqttClient} this - for chaining + * @api public + * @example client.subscribe('topic'); + * @example client.subscribe('topic', {qos: 1}); + * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); + * @example client.subscribe('topic', console.log); + */ + subscribe(...args) { + const subs = [] + let obj = args.shift() + const { resubscribe } = obj + let callback = args.pop() || this.noop + let opts = args.pop() + const version = this.options.protocolVersion + + delete obj.resubscribe + + if (typeof obj === 'string') { + obj = [obj] + } + + if (typeof callback !== 'function') { + opts = callback + callback = this.noop + } + + const invalidTopic = validations.validateTopics(obj) + if (invalidTopic !== null) { + setImmediate(callback, new Error(`Invalid topic ${invalidTopic}`)) + return this + } + + if (this._checkDisconnecting(callback)) { + this.log('subscribe: discconecting true') + return this + } + + const defaultOpts = { + qos: 0, + } + if (version === 5) { + defaultOpts.nl = false + defaultOpts.rap = false + defaultOpts.rh = 0 + } + opts = { ...defaultOpts, ...opts } + + const parseSub = (topic, subOptions) => { + // subOptions is defined only when providing a subs map, use opts otherwise + subOptions = subOptions || opts + if ( + !Object.prototype.hasOwnProperty.call( + this._resubscribeTopics, + topic, + ) || + this._resubscribeTopics[topic].qos < subOptions.qos || + resubscribe + ) { + const currentOpts = { + topic, + qos: subOptions.qos, + } + if (version === 5) { + currentOpts.nl = subOptions.nl + currentOpts.rap = subOptions.rap + currentOpts.rh = subOptions.rh + // use opts.properties + currentOpts.properties = opts.properties + } + this.log( + 'subscribe: pushing topic `%s` and qos `%s` to subs list', + currentOpts.topic, + currentOpts.qos, + ) + subs.push(currentOpts) + } + } + + if (Array.isArray(obj)) { + // array of topics + obj.forEach((topic) => { + this.log('subscribe: array topic %s', topic) + parseSub(topic) + }) + } else { + // object topic --> subOptions (no properties) + Object.keys(obj).forEach((topic) => { + this.log('subscribe: object topic %s, %o', topic, obj[topic]) + parseSub(topic, obj[topic]) + }) + } + + if (!subs.length) { + callback(null, []) + return this + } + + const subscribeProc = () => { + const messageId = this._nextId() + if (messageId === null) { + this.log('No messageId left') + return false + } + + const packet = { + cmd: 'subscribe', + subscriptions: subs, + qos: 1, + retain: false, + dup: false, + messageId, + } + + if (opts.properties) { + packet.properties = opts.properties + } + + // subscriptions to resubscribe to in case of disconnect + if (this.options.resubscribe) { + this.log('subscribe :: resubscribe true') + const topics = [] + subs.forEach((sub) => { + if (this.options.reconnectPeriod > 0) { + const topic = { qos: sub.qos } + if (version === 5) { + topic.nl = sub.nl || false + topic.rap = sub.rap || false + topic.rh = sub.rh || 0 + topic.properties = sub.properties + } + this._resubscribeTopics[sub.topic] = topic + topics.push(sub.topic) + } + }) + this.messageIdToTopic[packet.messageId] = topics + } + + this.outgoing[packet.messageId] = { + volatile: true, + cb(err, packet2) { + if (!err) { + const { granted } = packet2 + for (let i = 0; i < granted.length; i += 1) { + subs[i].qos = granted[i] + } + } + + callback(err, subs) + }, + } + this.log('subscribe :: call _sendPacket') + this._sendPacket(packet) + return true + } + + if ( + this._storeProcessing || + this._storeProcessingQueue.length > 0 || + !subscribeProc() + ) { + this._storeProcessingQueue.push({ + invoke: subscribeProc, + callback, + }) + } + + return this + } + + /** + * unsubscribe - unsubscribe from topic(s) + * + * @param {String, Array} topic - topics to unsubscribe from + * @param {Object} [opts] - optional subscription options, includes: + * {Object} properties - properties of unsubscribe packet + * @param {Function} [callback] - callback fired on unsuback + * @returns {MqttClient} this - for chaining + * @api public + * @example client.unsubscribe('topic'); + * @example client.unsubscribe('topic', console.log); + */ + unsubscribe(...args) { + let topic = args.shift() + let callback = args.pop() || this.noop + let opts = args.pop() + if (typeof topic === 'string') { + topic = [topic] + } + + if (typeof callback !== 'function') { + opts = callback + callback = this.noop + } + + const invalidTopic = validations.validateTopics(topic) + if (invalidTopic !== null) { + setImmediate(callback, new Error(`Invalid topic ${invalidTopic}`)) + return this + } + + if (this._checkDisconnecting(callback)) { + return this + } + + const unsubscribeProc = () => { + const messageId = this._nextId() + if (messageId === null) { + this.log('No messageId left') + return false + } + const packet = { + cmd: 'unsubscribe', + qos: 1, + messageId, + } + + if (typeof topic === 'string') { + packet.unsubscriptions = [topic] + } else if (Array.isArray(topic)) { + packet.unsubscriptions = topic + } + + if (this.options.resubscribe) { + packet.unsubscriptions.forEach((topic2) => { + delete this._resubscribeTopics[topic2] + }) + } + + if (typeof opts === 'object' && opts.properties) { + packet.properties = opts.properties + } + + this.outgoing[packet.messageId] = { + volatile: true, + cb: callback, + } + + this.log('unsubscribe: call _sendPacket') + this._sendPacket(packet) + + return true + } + + if ( + this._storeProcessing || + this._storeProcessingQueue.length > 0 || + !unsubscribeProc() + ) { + this._storeProcessingQueue.push({ + invoke: unsubscribeProc, + callback, + }) + } + + return this + } + + /** + * end - close connection + * + * @returns {MqttClient} this - for chaining + * @param {Boolean} force - do not wait for all in-flight messages to be acked + * @param {Object} opts - added to the disconnect packet + * @param {Function} cb - called when the client has been closed + * + * @api public + */ + end(force, opts, cb) { + this.log('end :: (%s)', this.options.clientId) + + if (force == null || typeof force !== 'boolean') { + cb = opts || this.noop + opts = force + force = false + if (typeof opts !== 'object') { + cb = opts + opts = null + if (typeof cb !== 'function') { + cb = this.noop + } + } + } + + if (typeof opts !== 'object') { + cb = opts + opts = null + } + + this.log('end :: cb? %s', !!cb) + cb = cb || this.noop + + const closeStores = () => { + this.log('end :: closeStores: closing incoming and outgoing stores') + this.disconnected = true + this.incomingStore.close((e1) => { + this.outgoingStore.close((e2) => { + this.log('end :: closeStores: emitting end') + this.emit('end') + if (cb) { + const err = e1 || e2 + this.log( + 'end :: closeStores: invoking callback with args', + ) + cb(err) + } + }) + }) + if (this._deferredReconnect) { + this._deferredReconnect() + } + } + + const finish = () => { + // defer closesStores of an I/O cycle, + // just to make sure things are + // ok for websockets + this.log( + 'end :: (%s) :: finish :: calling _cleanUp with force %s', + this.options.clientId, + force, + ) + this._cleanUp( + force, + () => { + this.log( + 'end :: finish :: calling process.nextTick on closeStores', + ) + // const boundProcess = nextTick.bind(null, closeStores) + nextTick(closeStores) + }, + opts, + ) + } + + if (this.disconnecting) { + cb() + return this + } + + this._clearReconnect() + + this.disconnecting = true + + if (!force && Object.keys(this.outgoing).length > 0) { + // wait 10ms, just to be sure we received all of it + this.log( + 'end :: (%s) :: calling finish in 10ms once outgoing is empty', + this.options.clientId, + ) + this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) + } else { + this.log( + 'end :: (%s) :: immediately calling finish', + this.options.clientId, + ) + finish() + } + + return this + } + + /** + * removeOutgoingMessage - remove a message in outgoing store + * the outgoing callback will be called withe Error('Message removed') if the message is removed + * + * @param {Number} messageId - messageId to remove message + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.removeOutgoingMessage(client.getLastAllocated()); + */ + removeOutgoingMessage(messageId) { + if (this.outgoing[messageId]) { + const { cb } = this.outgoing[messageId] + this._removeOutgoingAndStoreMessage(messageId, () => { + cb(new Error('Message removed')) + }) + } + return this + } + + /** + * reconnect - connect again using the same options as connect() + * + * @param {Object} [opts] - optional reconnect options, includes: + * {Store} incomingStore - a store for the incoming packets + * {Store} outgoingStore - a store for the outgoing packets + * if opts is not given, current stores are used + * @returns {MqttClient} this - for chaining + * + * @api public + */ + reconnect(opts) { + this.log('client reconnect') + const f = () => { + if (opts) { + this.options.incomingStore = opts.incomingStore + this.options.outgoingStore = opts.outgoingStore + } else { + this.options.incomingStore = null + this.options.outgoingStore = null + } + this.incomingStore = this.options.incomingStore || new Store() + this.outgoingStore = this.options.outgoingStore || new Store() + this.disconnecting = false + this.disconnected = false + this._deferredReconnect = null + this._reconnect() + } + + if (this.disconnecting && !this.disconnected) { + this._deferredReconnect = f + } else { + f() + } + return this + } + + /** + * _reconnect - implement reconnection + * @api privateish + */ + _reconnect() { + this.log('_reconnect: emitting reconnect to client') + this.emit('reconnect') + if (this.connected) { + this.end(() => { + this.connect() + }) + this.log('client already connected. disconnecting first.') + } else { + this.log('_reconnect: calling connect') + this.connect() + } + } + + /** + * _setupReconnect - setup reconnect timer + */ + _setupReconnect() { + if ( + !this.disconnecting && + !this.reconnectTimer && + this.options.reconnectPeriod > 0 + ) { + if (!this.reconnecting) { + this.log('_setupReconnect :: emit `offline` state') + this.emit('offline') + this.log('_setupReconnect :: set `reconnecting` to `true`') + this.reconnecting = true + } + this.log( + '_setupReconnect :: setting reconnectTimer for %d ms', + this.options.reconnectPeriod, + ) + this.reconnectTimer = setInterval(() => { + this.log('reconnectTimer :: reconnect triggered!') + this._reconnect() + }, this.options.reconnectPeriod) + } else { + this.log('_setupReconnect :: doing nothing...') + } + } + + /** + * _clearReconnect - clear the reconnect timer + */ + _clearReconnect() { + this.log('_clearReconnect : clearing reconnect timer') + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer) + this.reconnectTimer = null + } + } + + /** + * _cleanUp - clean up on connection end + * @api private + */ + _cleanUp(forced, done, opts = {}) { + if (done) { + this.log('_cleanUp :: done callback provided for on stream close') + this.stream.on('close', done) + } + + this.log('_cleanUp :: forced? %s', forced) + if (forced) { + if (this.options.reconnectPeriod === 0 && this.options.clean) { + this._flush(this.outgoing) + } + this.log( + '_cleanUp :: (%s) :: destroying stream', + this.options.clientId, + ) + this.stream.destroy() + } else { + const packet = { cmd: 'disconnect', ...opts } + this.log( + '_cleanUp :: (%s) :: call _sendPacket with disconnect packet', + this.options.clientId, + ) + this._sendPacket(packet, () => { + this.log( + '_cleanUp :: (%s) :: destroying stream', + this.options.clientId, + ) + setImmediate(() => { + this.stream.end(() => { + this.log( + '_cleanUp :: (%s) :: stream destroyed', + this.options.clientId, + ) + // once stream is closed the 'close' event will fire and that will + // emit client `close` event and call `done` callback if done is provided + }) + }) + }) + } + + if (!this.disconnecting) { + this.log( + '_cleanUp :: client not disconnecting. Clearing and resetting reconnect.', + ) + this._clearReconnect() + this._setupReconnect() + } + + if (this.pingTimer !== null) { + this.log('_cleanUp :: clearing pingTimer') + this.pingTimer.clear() + this.pingTimer = null + } + + if (done && !this.connected) { + this.log( + '_cleanUp :: (%s) :: removing stream `done` callback `close` listener', + this.options.clientId, + ) + this.stream.removeListener('close', done) + done() + } + } + + _storeAndSend(packet, cb, cbStorePut) { + this.log( + 'storeAndSend :: store packet with cmd %s to outgoingStore', + packet.cmd, + ) + let storePacket = packet + let err + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + err = this._removeTopicAliasAndRecoverTopicName(storePacket) + if (err) { + return cb && cb(err) + } + } + this.outgoingStore.put(storePacket, (err2) => { + if (err2) { + return cb && cb(err2) + } + cbStorePut() + this._writePacket(packet, cb) + }) + } + + _applyTopicAlias(packet) { + if (this.options.protocolVersion === 5) { + if (packet.cmd === 'publish') { + let alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + const topic = packet.topic.toString() + if (this.topicAliasSend) { + if (alias) { + if (topic.length !== 0) { + // register topic alias + this.log( + 'applyTopicAlias :: register topic: %s - alias: %d', + topic, + alias, + ) + if (!this.topicAliasSend.put(topic, alias)) { + this.log( + 'applyTopicAlias :: error out of range. topic: %s - alias: %d', + topic, + alias, + ) + return new Error( + 'Sending Topic Alias out of range', + ) + } + } + } else if (topic.length !== 0) { + if (this.options.autoAssignTopicAlias) { + alias = this.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = { + ...packet.properties, + topicAlias: alias, + } + this.log( + 'applyTopicAlias :: auto assign(use) topic: %s - alias: %d', + topic, + alias, + ) + } else { + alias = this.topicAliasSend.getLruAlias() + this.topicAliasSend.put(topic, alias) + packet.properties = { + ...packet.properties, + topicAlias: alias, + } + this.log( + 'applyTopicAlias :: auto assign topic: %s - alias: %d', + topic, + alias, + ) + } + } else if (this.options.autoUseTopicAlias) { + alias = this.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = { + ...packet.properties, + topicAlias: alias, + } + this.log( + 'applyTopicAlias :: auto use topic: %s - alias: %d', + topic, + alias, + ) + } + } + } + } else if (alias) { + this.log( + 'applyTopicAlias :: error out of range. topic: %s - alias: %d', + topic, + alias, + ) + return new Error('Sending Topic Alias out of range') + } + } + } + } + + _noop(err) { + this.log('noop ::', err) + } + + /** Writes the packet to stream and emits events */ + _writePacket(packet, cb) { + this.log('_writePacket :: packet: %O', packet) + this.log('_writePacket :: emitting `packetsend`') + + this.emit('packetsend', packet) + + // When writing a packet, reschedule the ping timer + this._shiftPingInterval() + + this.log('_writePacket :: writing to stream') + const result = mqttPacket.writeToStream( + packet, + this.stream, + this.options, + ) + this.log('_writePacket :: writeToStream result %s', result) + if (!result && cb && cb !== this.noop) { + this.log( + '_writePacket :: handle events on `drain` once through callback.', + ) + this.stream.once('drain', cb) + } else if (cb) { + this.log('_writePacket :: invoking cb') + cb() + } + } + + /** + * _sendPacket - send or queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @param {Boolean} noStore - send without put to the store + * @api private + */ + _sendPacket(packet, cb, cbStorePut, noStore) { + this.log('_sendPacket :: (%s) :: start', this.options.clientId) + cbStorePut = cbStorePut || this.noop + cb = cb || this.noop + + const err = this._applyTopicAlias(packet) + if (err) { + cb(err) + return + } + + if (!this.connected) { + // allow auth packets to be sent while authenticating with the broker (mqtt5 enhanced auth) + if (packet.cmd === 'auth') { + this._writePacket(this, packet, cb) + return + } + + this.log( + '_sendPacket :: client not connected. Storing packet offline.', + ) + this._storePacket(packet, cb, cbStorePut) + return + } + + // If "noStore" is true, the message is sent without being recorded in the store. + // Messages that have not received puback or pubcomp remain in the store after disconnection + // and are resent from the store upon reconnection. + // For resend upon reconnection, "noStore" is set to true. This is because the message is already stored in the store. + // This is to avoid interrupting other processes while recording to the store. + if (noStore) { + this._writePacket(packet, cb) + return + } + + switch (packet.cmd) { + case 'publish': + break + case 'pubrel': + this._storeAndSend(packet, cb, cbStorePut) + return + default: + this._writePacket(packet, cb) + return + } + + switch (packet.qos) { + case 2: + case 1: + this._storeAndSend(packet, cb, cbStorePut) + break + /** + * no need of case here since it will be caught by default + * and jshint comply that before default it must be a break + * anyway it will result in -1 evaluation + */ + case 0: + /* falls through */ + default: + this._writePacket(packet, cb) + break + } + this.log('_sendPacket :: (%s) :: end', this.options.clientId) + } + + /** + * _storePacket - queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @api private + */ + _storePacket(packet, cb, cbStorePut) { + this.log('_storePacket :: packet: %o', packet) + this.log('_storePacket :: cb? %s', !!cb) + cbStorePut = cbStorePut || this.noop + + let storePacket = packet + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + const err = this._removeTopicAliasAndRecoverTopicName(storePacket) + if (err) { + return cb && cb(err) + } + } + // check that the packet is not a qos of 0, or that the command is not a publish + if ( + ((storePacket.qos || 0) === 0 && this.queueQoSZero) || + storePacket.cmd !== 'publish' + ) { + this.queue.push({ packet: storePacket, cb }) + } else if (storePacket.qos > 0) { + cb = this.outgoing[storePacket.messageId] + ? this.outgoing[storePacket.messageId].cb + : null + this.outgoingStore.put(storePacket, (err) => { + if (err) { + return cb && cb(err) + } + cbStorePut() + }) + } else if (cb) { + cb(new Error('No connection to broker')) + } + } + + /** + * _setupPingTimer - setup the ping timer + * + * @api private + */ + _setupPingTimer() { + this.log( + '_setupPingTimer :: keepalive %d (seconds)', + this.options.keepalive, + ) + + if (!this.pingTimer && this.options.keepalive) { + this.pingResp = true + this.pingTimer = reInterval(() => { + this._checkPing() + }, this.options.keepalive * 1000) + } + } + + /** + * _shiftPingInterval - reschedule the ping interval + * + * @api private + */ + _shiftPingInterval() { + if ( + this.pingTimer && + this.options.keepalive && + this.options.reschedulePings + ) { + this.pingTimer.reschedule(this.options.keepalive * 1000) + } + } + + /** + * _checkPing - check if a pingresp has come back, and ping the server again + * + * @api private + */ + _checkPing() { + this.log('_checkPing :: checking ping...') + if (this.pingResp) { + this.log( + '_checkPing :: ping response received. Clearing flag and sending `pingreq`', + ) + this.pingResp = false + this._sendPacket({ cmd: 'pingreq' }) + } else { + // do a forced cleanup since socket will be in bad shape + this.log('_checkPing :: calling _cleanUp with force true') + this._cleanUp(true) + } + } + + /** + * @param packet the packet received by the broker + * @return the auth packet to be returned to the broker + * @api public + */ + handleAuth(packet, callback) { + callback() + } + + /** + * Handle messages with backpressure support, one at a time. + * Override at will. + * + * @param Packet packet the packet + * @param Function callback call when finished + * @api public + */ + handleMessage(packet, callback) { + callback() + } + + /** + * _nextId + * @return unsigned int + */ + _nextId() { + return this.messageIdProvider.allocate() + } + + /** + * getLastMessageId + * @return unsigned int + */ + getLastMessageId() { + return this.messageIdProvider.getLastAllocated() + } + + /** + * _resubscribe + * @api private + */ + _resubscribe() { + this.log('_resubscribe') + const _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) + if ( + !this._firstConnection && + (this.options.clean || + (this.options.protocolVersion === 5 && + !this.connackPacket.sessionPresent)) && + _resubscribeTopicsKeys.length > 0 + ) { + if (this.options.resubscribe) { + if (this.options.protocolVersion === 5) { + this.log('_resubscribe: protocolVersion 5') + for ( + let topicI = 0; + topicI < _resubscribeTopicsKeys.length; + topicI++ + ) { + const resubscribeTopic = {} + resubscribeTopic[_resubscribeTopicsKeys[topicI]] = + this._resubscribeTopics[ + _resubscribeTopicsKeys[topicI] + ] + resubscribeTopic.resubscribe = true + this.subscribe(resubscribeTopic, { + properties: + resubscribeTopic[_resubscribeTopicsKeys[topicI]] + .properties, + }) + } + } else { + this._resubscribeTopics.resubscribe = true + this.subscribe(this._resubscribeTopics) + } + } else { + this._resubscribeTopics = {} + } + } + + this._firstConnection = false + } + + /** + * _onConnect + * + * @api private + */ + _onConnect(packet) { + if (this.disconnected) { + this.emit('connect', packet) + return + } + + this.connackPacket = packet + this.messageIdProvider.clear() + this._setupPingTimer() + + this.connected = true + + const startStreamProcess = () => { + let outStore = this.outgoingStore.createStream() + + const remove = () => { + outStore.destroy() + outStore = null + this._flushStoreProcessingQueue() + clearStoreProcessing() + } + + const clearStoreProcessing = () => { + this._storeProcessing = false + this._packetIdsDuringStoreProcessing = {} + } + + this.once('close', remove) + outStore.on('error', (err) => { + clearStoreProcessing() + this._flushStoreProcessingQueue() + this.removeListener('close', remove) + this.emit('error', err) + }) + + const storeDeliver = () => { + // edge case, we wrapped this twice + if (!outStore) { + return + } + + const packet2 = outStore.read(1) + + let cb + + if (!packet2) { + // read when data is available in the future + outStore.once('readable', storeDeliver) + return + } + + this._storeProcessing = true + + // Skip already processed store packets + if (this._packetIdsDuringStoreProcessing[packet2.messageId]) { + storeDeliver() + return + } + + // Avoid unnecessary stream read operations when disconnected + if (!this.disconnecting && !this.reconnectTimer) { + cb = this.outgoing[packet2.messageId] + ? this.outgoing[packet2.messageId].cb + : null + this.outgoing[packet2.messageId] = { + volatile: false, + cb(err, status) { + // Ensure that the original callback passed in to publish gets invoked + if (cb) { + cb(err, status) + } + + storeDeliver() + }, + } + this._packetIdsDuringStoreProcessing[ + packet2.messageId + ] = true + if (this.messageIdProvider.register(packet2.messageId)) { + this._sendPacket(packet2, undefined, undefined, true) + } else { + this.log( + 'messageId: %d has already used.', + packet2.messageId, + ) + } + } else if (outStore.destroy) { + outStore.destroy() + } + } + + outStore.on('end', () => { + let allProcessed = true + for (const id in this._packetIdsDuringStoreProcessing) { + if (!this._packetIdsDuringStoreProcessing[id]) { + allProcessed = false + break + } + } + if (allProcessed) { + clearStoreProcessing() + this.removeListener('close', remove) + this._invokeAllStoreProcessingQueue() + this.emit('connect', packet) + } else { + startStreamProcess() + } + }) + storeDeliver() + } + // start flowing + startStreamProcess() + } + + _invokeStoreProcessingQueue() { + // If _storeProcessing is true, the message is resending. + // During resend, processing is skipped to prevent new messages from interrupting. #1635 + if (!this._storeProcessing && this._storeProcessingQueue.length > 0) { + const f = this._storeProcessingQueue[0] + if (f && f.invoke()) { + this._storeProcessingQueue.shift() + return true + } + } + return false + } + + _invokeAllStoreProcessingQueue() { + while (this._invokeStoreProcessingQueue()) { + /* empty */ + } + } + + _flushStoreProcessingQueue() { + for (const f of this._storeProcessingQueue) { + if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) + if (f.callback) f.callback(new Error('Connection closed')) + } + this._storeProcessingQueue.splice(0) + } + + /** + * _removeOutgoingAndStoreMessage + * @param {Number} messageId - messageId to remove message + * @param {Function} cb - called when the message removed + * @api private + */ + _removeOutgoingAndStoreMessage(messageId, cb) { + const self = this + delete this.outgoing[messageId] + self.outgoingStore.del({ messageId }, (err, packet) => { + cb(err, packet) + self.messageIdProvider.deallocate(messageId) + self._invokeStoreProcessingQueue() + }) + } } module.exports = MqttClient diff --git a/lib/connect/ali.js b/lib/connect/ali.js index 7001e7712..fb6dc3483 100644 --- a/lib/connect/ali.js +++ b/lib/connect/ali.js @@ -1,129 +1,126 @@ -'use strict' - const { Buffer } = require('buffer') -const Transform = require('readable-stream').Transform +const { Transform } = require('readable-stream') const duplexify = require('duplexify') -/* global FileReader */ let my let proxy let stream let isInitialized = false -function buildProxy () { - const proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - my.sendSocketMessage({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function () { - next(new Error()) - } - }) - } - proxy._flush = function socketEnd (done) { - my.closeSocket({ - success: function () { - done() - } - }) - } - - return proxy +function buildProxy() { + const _proxy = new Transform() + _proxy._write = (chunk, encoding, next) => { + my.sendSocketMessage({ + data: chunk.buffer, + success() { + next() + }, + fail() { + next(new Error()) + }, + }) + } + _proxy._flush = (done) => { + my.closeSocket({ + success() { + done() + }, + }) + } + + return _proxy } -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } +function setDefaultOpts(opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } } -function buildUrl (opts, client) { - const protocol = opts.protocol === 'alis' ? 'wss' : 'ws' - let url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url +function buildUrl(opts, client) { + const protocol = opts.protocol === 'alis' ? 'wss' : 'ws' + let url = `${protocol}://${opts.hostname}${opts.path}` + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = `${protocol}://${opts.hostname}:${opts.port}${opts.path}` + } + if (typeof opts.transformWsUrl === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url } -function bindEventHandler () { - if (isInitialized) return - - isInitialized = true - - my.onSocketOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - my.onSocketMessage(function (res) { - if (typeof res.data === 'string') { - const buffer = Buffer.from(res.data, 'base64') - proxy.push(buffer) - } else { - const reader = new FileReader() - reader.addEventListener('load', function () { - let data = reader.result - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - reader.readAsArrayBuffer(res.data) - } - }) - - my.onSocketClose(function () { - stream.end() - stream.destroy() - }) - - my.onSocketError(function (res) { - stream.destroy(res) - }) +function bindEventHandler() { + if (isInitialized) return + + isInitialized = true + + my.onSocketOpen(() => { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + my.onSocketMessage((res) => { + if (typeof res.data === 'string') { + const buffer = Buffer.from(res.data, 'base64') + proxy.push(buffer) + } else { + const reader = new FileReader() + reader.addEventListener('load', () => { + let data = reader.result + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + reader.readAsArrayBuffer(res.data) + } + }) + + my.onSocketClose(() => { + stream.end() + stream.destroy() + }) + + my.onSocketError((res) => { + stream.destroy(res) + }) } -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host +function buildStream(client, opts) { + opts.hostname = opts.hostname || opts.host - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' + const websocketSubProtocol = + opts.protocolId === 'MQIsdp' && opts.protocolVersion === 3 + ? 'mqttv3.1' + : 'mqtt' - setDefaultOpts(opts) + setDefaultOpts(opts) - const url = buildUrl(opts, client) - my = opts.my - my.connectSocket({ - url, - protocols: websocketSubProtocol - }) + const url = buildUrl(opts, client) + my = opts.my + my.connectSocket({ + url, + protocols: websocketSubProtocol, + }) - proxy = buildProxy() - stream = duplexify.obj() + proxy = buildProxy() + stream = duplexify.obj() - bindEventHandler() + bindEventHandler() - return stream + return stream } module.exports = buildStream diff --git a/lib/connect/index.js b/lib/connect/index.js index cff123633..e50b31122 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -1,27 +1,25 @@ -'use strict' - +const url = require('url') const MqttClient = require('../client') const Store = require('../store') const DefaultMessageIdProvider = require('../default-message-id-provider') const UniqueMessageIdProvider = require('../unique-message-id-provider') const { IS_BROWSER } = require('../is-browser') -const url = require('url') const debug = require('debug')('mqttjs') const protocols = {} if (!IS_BROWSER) { - protocols.mqtt = require('./tcp') - protocols.tcp = require('./tcp') - protocols.ssl = require('./tls') - protocols.tls = require('./tls') - protocols.mqtts = require('./tls') + protocols.mqtt = require('./tcp') + protocols.tcp = require('./tcp') + protocols.ssl = require('./tls') + protocols.tls = require('./tls') + protocols.mqtts = require('./tls') } else { - protocols.wx = require('./wx') - protocols.wxs = require('./wx') + protocols.wx = require('./wx') + protocols.wxs = require('./wx') - protocols.ali = require('./ali') - protocols.alis = require('./ali') + protocols.ali = require('./ali') + protocols.alis = require('./ali') } protocols.ws = require('./ws') @@ -32,17 +30,17 @@ protocols.wss = require('./ws') * * @param {Object} [opts] option object */ -function parseAuthOptions (opts) { - let matches - if (opts.auth) { - matches = opts.auth.match(/^(.+):(.+)$/) - if (matches) { - opts.username = matches[1] - opts.password = matches[2] - } else { - opts.username = opts.auth - } - } +function parseAuthOptions(opts) { + let matches + if (opts.auth) { + matches = opts.auth.match(/^(.+):(.+)$/) + if (matches) { + opts.username = matches[1] + opts.password = matches[2] + } else { + opts.username = opts.auth + } + } } /** @@ -51,113 +49,122 @@ function parseAuthOptions (opts) { * @param {String} [brokerUrl] - url of the broker, optional * @param {Object} opts - see MqttClient#constructor */ -function connect (brokerUrl, opts) { - debug('connecting to an MQTT broker...') - if ((typeof brokerUrl === 'object') && !opts) { - opts = brokerUrl - brokerUrl = null - } +function connect(brokerUrl, opts) { + debug('connecting to an MQTT broker...') + if (typeof brokerUrl === 'object' && !opts) { + opts = brokerUrl + brokerUrl = null + } - opts = opts || {} + opts = opts || {} - if (brokerUrl) { - // eslint-disable-next-line + if (brokerUrl) { + // eslint-disable-next-line const parsed = url.parse(brokerUrl, true) - if (parsed.port != null) { - parsed.port = Number(parsed.port) - } - - opts = { ...parsed, ...opts } - - if (opts.protocol === null) { - throw new Error('Missing protocol') - } - - opts.protocol = opts.protocol.replace(/:$/, '') - } - - // merge in the auth options if supplied - parseAuthOptions(opts) - - // support clientId passed in the query string of the url - if (opts.query && typeof opts.query.clientId === 'string') { - opts.clientId = opts.query.clientId - } - - if (opts.cert && opts.key) { - if (opts.protocol) { - if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { - switch (opts.protocol) { - case 'mqtt': - opts.protocol = 'mqtts' - break - case 'ws': - opts.protocol = 'wss' - break - case 'wx': - opts.protocol = 'wxs' - break - case 'ali': - opts.protocol = 'alis' - break - default: - throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!') - } - } - } else { - // A cert and key was provided, however no protocol was specified, so we will throw an error. - throw new Error('Missing secure protocol key') - } - } - - if (!protocols[opts.protocol]) { - const isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 - opts.protocol = [ - 'mqtt', - 'mqtts', - 'ws', - 'wss', - 'wx', - 'wxs', - 'ali', - 'alis' - ].filter(function (key, index) { - if (isSecure && index % 2 === 0) { - // Skip insecure protocols when requesting a secure one. - return false - } - return (typeof protocols[key] === 'function') - })[0] - } - - if (opts.clean === false && !opts.clientId) { - throw new Error('Missing clientId for unclean clients') - } - - if (opts.protocol) { - opts.defaultProtocol = opts.protocol - } - - function wrapper (client) { - if (opts.servers) { - if (!client._reconnectCount || client._reconnectCount === opts.servers.length) { - client._reconnectCount = 0 - } - - opts.host = opts.servers[client._reconnectCount].host - opts.port = opts.servers[client._reconnectCount].port - opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol) - opts.hostname = opts.host - - client._reconnectCount++ - } - - debug('calling streambuilder for', opts.protocol) - return protocols[opts.protocol](client, opts) - } - const client = new MqttClient(wrapper, opts) - client.on('error', function () { /* Automatically set up client error handling */ }) - return client + if (parsed.port != null) { + parsed.port = Number(parsed.port) + } + + opts = { ...parsed, ...opts } + + if (opts.protocol === null) { + throw new Error('Missing protocol') + } + + opts.protocol = opts.protocol.replace(/:$/, '') + } + + // merge in the auth options if supplied + parseAuthOptions(opts) + + // support clientId passed in the query string of the url + if (opts.query && typeof opts.query.clientId === 'string') { + opts.clientId = opts.query.clientId + } + + if (opts.cert && opts.key) { + if (opts.protocol) { + if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { + switch (opts.protocol) { + case 'mqtt': + opts.protocol = 'mqtts' + break + case 'ws': + opts.protocol = 'wss' + break + case 'wx': + opts.protocol = 'wxs' + break + case 'ali': + opts.protocol = 'alis' + break + default: + throw new Error( + `Unknown protocol for secure connection: "${opts.protocol}"!`, + ) + } + } + } else { + // A cert and key was provided, however no protocol was specified, so we will throw an error. + throw new Error('Missing secure protocol key') + } + } + + if (!protocols[opts.protocol]) { + const isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 + opts.protocol = [ + 'mqtt', + 'mqtts', + 'ws', + 'wss', + 'wx', + 'wxs', + 'ali', + 'alis', + ].filter((key, index) => { + if (isSecure && index % 2 === 0) { + // Skip insecure protocols when requesting a secure one. + return false + } + return typeof protocols[key] === 'function' + })[0] + } + + if (opts.clean === false && !opts.clientId) { + throw new Error('Missing clientId for unclean clients') + } + + if (opts.protocol) { + opts.defaultProtocol = opts.protocol + } + + function wrapper(client) { + if (opts.servers) { + if ( + !client._reconnectCount || + client._reconnectCount === opts.servers.length + ) { + client._reconnectCount = 0 + } + + opts.host = opts.servers[client._reconnectCount].host + opts.port = opts.servers[client._reconnectCount].port + opts.protocol = !opts.servers[client._reconnectCount].protocol + ? opts.defaultProtocol + : opts.servers[client._reconnectCount].protocol + opts.hostname = opts.host + + client._reconnectCount++ + } + + debug('calling streambuilder for', opts.protocol) + return protocols[opts.protocol](client, opts) + } + const client = new MqttClient(wrapper, opts) + client.on('error', () => { + /* Automatically set up client error handling */ + }) + return client } module.exports = connect diff --git a/lib/connect/tcp.js b/lib/connect/tcp.js index 607036537..650ce844e 100644 --- a/lib/connect/tcp.js +++ b/lib/connect/tcp.js @@ -1,4 +1,3 @@ -'use strict' const net = require('net') const debug = require('debug')('mqttjs:tcp') @@ -6,15 +5,15 @@ const debug = require('debug')('mqttjs:tcp') variables port and host can be removed since you have all required information in opts object */ -function streamBuilder (client, opts) { - opts.port = opts.port || 1883 - opts.hostname = opts.hostname || opts.host || 'localhost' +function streamBuilder(client, opts) { + opts.port = opts.port || 1883 + opts.hostname = opts.hostname || opts.host || 'localhost' - const port = opts.port - const host = opts.hostname + const { port } = opts + const host = opts.hostname - debug('port %d and host %s', port, host) - return net.createConnection(port, host) + debug('port %d and host %s', port, host) + return net.createConnection(port, host) } module.exports = streamBuilder diff --git a/lib/connect/tls.js b/lib/connect/tls.js index bb01aeb8f..076c64faa 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -1,48 +1,52 @@ -'use strict' const tls = require('tls') const net = require('net') const debug = require('debug')('mqttjs:tls') -function buildBuilder (mqttClient, opts) { - opts.port = opts.port || 8883 - opts.host = opts.hostname || opts.host || 'localhost' - - if (net.isIP(opts.host) === 0) { - opts.servername = opts.host - } - - opts.rejectUnauthorized = opts.rejectUnauthorized !== false - - delete opts.path - - debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) - - const connection = tls.connect(opts) - /* eslint no-use-before-define: [2, "nofunc"] */ - connection.on('secureConnect', function () { - if (opts.rejectUnauthorized && !connection.authorized) { - connection.emit('error', new Error('TLS not authorized')) - } else { - connection.removeListener('error', handleTLSerrors) - } - }) - - function handleTLSerrors (err) { - // How can I get verify this error is a tls error? - if (opts.rejectUnauthorized) { - mqttClient.emit('error', err) - } - - // close this connection to match the behaviour of net - // otherwise all we get is an error from the connection - // and close event doesn't fire. This is a work around - // to enable the reconnect code to work the same as with - // net.createConnection - connection.end() - } - - connection.on('error', handleTLSerrors) - return connection +function buildBuilder(mqttClient, opts) { + opts.port = opts.port || 8883 + opts.host = opts.hostname || opts.host || 'localhost' + + if (net.isIP(opts.host) === 0) { + opts.servername = opts.host + } + + opts.rejectUnauthorized = opts.rejectUnauthorized !== false + + delete opts.path + + debug( + 'port %d host %s rejectUnauthorized %b', + opts.port, + opts.host, + opts.rejectUnauthorized, + ) + + const connection = tls.connect(opts) + /* eslint no-use-before-define: [2, "nofunc"] */ + connection.on('secureConnect', () => { + if (opts.rejectUnauthorized && !connection.authorized) { + connection.emit('error', new Error('TLS not authorized')) + } else { + connection.removeListener('error', handleTLSerrors) + } + }) + + function handleTLSerrors(err) { + // How can I get verify this error is a tls error? + if (opts.rejectUnauthorized) { + mqttClient.emit('error', err) + } + + // close this connection to match the behaviour of net + // otherwise all we get is an error from the connection + // and close event doesn't fire. This is a work around + // to enable the reconnect code to work the same as with + // net.createConnection + connection.end() + } + + connection.on('error', handleTLSerrors) + return connection } module.exports = buildBuilder diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 2cd5cce86..de80c0aad 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,257 +1,265 @@ -'use strict' - const { Buffer } = require('buffer') const WS = require('ws') const debug = require('debug')('mqttjs:ws') const duplexify = require('duplexify') -const Transform = require('readable-stream').Transform +const { Transform } = require('readable-stream') const { IS_BROWSER } = require('../is-browser') const WSS_OPTIONS = [ - 'rejectUnauthorized', - 'ca', - 'cert', - 'key', - 'pfx', - 'passphrase' + 'rejectUnauthorized', + 'ca', + 'cert', + 'key', + 'pfx', + 'passphrase', ] -function buildUrl (opts, client) { - let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url +function buildUrl(opts, client) { + let url = `${opts.protocol}://${opts.hostname}:${opts.port}${opts.path}` + if (typeof opts.transformWsUrl === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url } -function setDefaultOpts (opts) { - const options = opts - if (!opts.hostname) { - options.hostname = 'localhost' - } - if (!opts.port) { - if (opts.protocol === 'wss') { - options.port = 443 - } else { - options.port = 80 - } - } - if (!opts.path) { - options.path = '/' - } - - if (!opts.wsOptions) { - options.wsOptions = {} - } - if (!IS_BROWSER && opts.protocol === 'wss') { - // Add cert/key/ca etc options - WSS_OPTIONS.forEach(function (prop) { - if (Object.prototype.hasOwnProperty.call(opts, prop) && !Object.prototype.hasOwnProperty.call(opts.wsOptions, prop)) { - options.wsOptions[prop] = opts[prop] - } - }) - } - - return options +function setDefaultOpts(opts) { + const options = opts + if (!opts.hostname) { + options.hostname = 'localhost' + } + if (!opts.port) { + if (opts.protocol === 'wss') { + options.port = 443 + } else { + options.port = 80 + } + } + if (!opts.path) { + options.path = '/' + } + + if (!opts.wsOptions) { + options.wsOptions = {} + } + if (!IS_BROWSER && opts.protocol === 'wss') { + // Add cert/key/ca etc options + WSS_OPTIONS.forEach((prop) => { + if ( + Object.prototype.hasOwnProperty.call(opts, prop) && + !Object.prototype.hasOwnProperty.call(opts.wsOptions, prop) + ) { + options.wsOptions[prop] = opts[prop] + } + }) + } + + return options } -function setDefaultBrowserOpts (opts) { - const options = setDefaultOpts(opts) - - if (!options.hostname) { - options.hostname = options.host - } - - if (!options.hostname) { - // Throwing an error in a Web Worker if no `hostname` is given, because we - // can not determine the `hostname` automatically. If connecting to - // localhost, please supply the `hostname` as an argument. - if (typeof (document) === 'undefined') { - throw new Error('Could not determine host. Specify host manually.') - } - const parsed = new URL(document.URL) - options.hostname = parsed.hostname - - if (!options.port) { - options.port = parsed.port - } - } - - // objectMode should be defined for logic - if (options.objectMode === undefined) { - options.objectMode = !(options.binary === true || options.binary === undefined) - } - - return options +function setDefaultBrowserOpts(opts) { + const options = setDefaultOpts(opts) + + if (!options.hostname) { + options.hostname = options.host + } + + if (!options.hostname) { + // Throwing an error in a Web Worker if no `hostname` is given, because we + // can not determine the `hostname` automatically. If connecting to + // localhost, please supply the `hostname` as an argument. + if (typeof document === 'undefined') { + throw new Error('Could not determine host. Specify host manually.') + } + const parsed = new URL(document.URL) + options.hostname = parsed.hostname + + if (!options.port) { + options.port = parsed.port + } + } + + // objectMode should be defined for logic + if (options.objectMode === undefined) { + options.objectMode = !( + options.binary === true || options.binary === undefined + ) + } + + return options } -function createWebSocket (client, url, opts) { - debug('createWebSocket') - debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) - const socket = new WS(url, [websocketSubProtocol], opts.wsOptions) - return socket +function createWebSocket(client, url, opts) { + debug('createWebSocket') + debug(`protocol: ${opts.protocolId} ${opts.protocolVersion}`) + const websocketSubProtocol = + opts.protocolId === 'MQIsdp' && opts.protocolVersion === 3 + ? 'mqttv3.1' + : 'mqtt' + + debug( + `creating new Websocket for url: ${url} and protocol: ${websocketSubProtocol}`, + ) + const socket = new WS(url, [websocketSubProtocol], opts.wsOptions) + return socket } -function createBrowserWebSocket (client, opts) { - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - const url = buildUrl(opts, client) - /* global WebSocket */ - const socket = new WebSocket(url, [websocketSubProtocol]) - socket.binaryType = 'arraybuffer' - return socket +function createBrowserWebSocket(client, opts) { + const websocketSubProtocol = + opts.protocolId === 'MQIsdp' && opts.protocolVersion === 3 + ? 'mqttv3.1' + : 'mqtt' + + const url = buildUrl(opts, client) + const socket = new WebSocket(url, [websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + return socket } -function streamBuilder (client, opts) { - debug('streamBuilder') - const options = setDefaultOpts(opts) - const url = buildUrl(options, client) - const socket = createWebSocket(client, url, options) - const webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) - webSocketStream.url = url - socket.on('close', () => { webSocketStream.destroy() }) - return webSocketStream +function streamBuilder(client, opts) { + debug('streamBuilder') + const options = setDefaultOpts(opts) + const url = buildUrl(options, client) + const socket = createWebSocket(client, url, options) + const webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) + webSocketStream.url = url + socket.on('close', () => { + webSocketStream.destroy() + }) + return webSocketStream } -function browserStreamBuilder (client, opts) { - debug('browserStreamBuilder') - let stream - const options = setDefaultBrowserOpts(opts) - // sets the maximum socket buffer size before throttling - const bufferSize = options.browserBufferSize || 1024 * 512 - - const bufferTimeout = opts.browserBufferTimeout || 1000 - - const coerceToBuffer = !opts.objectMode - - const socket = createBrowserWebSocket(client, opts) - - const proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) - - if (!opts.objectMode) { - proxy._writev = writev - } - proxy.on('close', () => { socket.close() }) - - const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') - - // was already open when passed in - if (socket.readyState === socket.OPEN) { - stream = proxy - } else { - stream = stream = duplexify(undefined, undefined, opts) - if (!opts.objectMode) { - stream._writev = writev - } - - if (eventListenerSupport) { - socket.addEventListener('open', onopen) - } else { - socket.onopen = onopen - } - } - - stream.socket = socket - - if (eventListenerSupport) { - socket.addEventListener('close', onclose) - socket.addEventListener('error', onerror) - socket.addEventListener('message', onmessage) - } else { - socket.onclose = onclose - socket.onerror = onerror - socket.onmessage = onmessage - } - - // methods for browserStreamBuilder - - function buildProxy (options, socketWrite, socketEnd) { - const proxy = new Transform({ - objectModeMode: options.objectMode - }) - - proxy._write = socketWrite - proxy._flush = socketEnd - - return proxy - } - - function onopen () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - } - - function onclose () { - stream.end() - stream.destroy() - } - - function onerror (err) { - stream.destroy(err) - } - - function onmessage (event) { - let data = event.data - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - } - - // this is to be enabled only if objectMode is false - function writev (chunks, cb) { - const buffers = new Array(chunks.length) - for (let i = 0; i < chunks.length; i++) { - if (typeof chunks[i].chunk === 'string') { - buffers[i] = Buffer.from(chunks[i], 'utf8') - } else { - buffers[i] = chunks[i].chunk - } - } - - this._write(Buffer.concat(buffers), 'binary', cb) - } - - function socketWriteBrowser (chunk, enc, next) { - if (socket.bufferedAmount > bufferSize) { - // throttle data until buffered amount is reduced. - setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) - } - - if (coerceToBuffer && typeof chunk === 'string') { - chunk = Buffer.from(chunk, 'utf8') - } - - try { - socket.send(chunk) - } catch (err) { - return next(err) - } - - next() - } - - function socketEndBrowser (done) { - socket.close() - done() - } - - // end methods for browserStreamBuilder - - return stream +function browserStreamBuilder(client, opts) { + debug('browserStreamBuilder') + let stream + const options = setDefaultBrowserOpts(opts) + // sets the maximum socket buffer size before throttling + const bufferSize = options.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + const socket = createBrowserWebSocket(client, opts) + + const proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) + + if (!opts.objectMode) { + proxy._writev = writev + } + proxy.on('close', () => { + socket.close() + }) + + const eventListenerSupport = typeof socket.addEventListener !== 'undefined' + + // was already open when passed in + if (socket.readyState === socket.OPEN) { + stream = proxy + } else { + stream = duplexify(undefined, undefined, opts) + if (!opts.objectMode) { + stream._writev = writev + } + + if (eventListenerSupport) { + socket.addEventListener('open', onOpen) + } else { + socket.onopen = onOpen + } + } + + stream.socket = socket + + if (eventListenerSupport) { + socket.addEventListener('close', onclose) + socket.addEventListener('error', onerror) + socket.addEventListener('message', onmessage) + } else { + socket.onclose = onclose + socket.onerror = onerror + socket.onmessage = onmessage + } + + // methods for browserStreamBuilder + + function buildProxy(pOptions, socketWrite, socketEnd) { + const _proxy = new Transform({ + objectModeMode: pOptions.objectMode, + }) + + _proxy._write = socketWrite + _proxy._flush = socketEnd + + return _proxy + } + + function onOpen() { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + } + + function onclose() { + stream.end() + stream.destroy() + } + + function onerror(err) { + stream.destroy(err) + } + + function onmessage(event) { + let { data } = event + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + } + + // this is to be enabled only if objectMode is false + function writev(chunks, cb) { + const buffers = new Array(chunks.length) + for (let i = 0; i < chunks.length; i++) { + if (typeof chunks[i].chunk === 'string') { + buffers[i] = Buffer.from(chunks[i], 'utf8') + } else { + buffers[i] = chunks[i].chunk + } + } + + this._write(Buffer.concat(buffers), 'binary', cb) + } + + function socketWriteBrowser(chunk, enc, next) { + if (socket.bufferedAmount > bufferSize) { + // throttle data until buffered amount is reduced. + setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) + } + + if (coerceToBuffer && typeof chunk === 'string') { + chunk = Buffer.from(chunk, 'utf8') + } + + try { + socket.send(chunk) + } catch (err) { + return next(err) + } + + next() + } + + function socketEndBrowser(done) { + socket.close() + done() + } + + // end methods for browserStreamBuilder + + return stream } if (IS_BROWSER) { - module.exports = browserStreamBuilder + module.exports = browserStreamBuilder } else { - module.exports = streamBuilder + module.exports = streamBuilder } diff --git a/lib/connect/wx.js b/lib/connect/wx.js index 003099e3e..b6e1139be 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -1,133 +1,132 @@ -'use strict' - const { Buffer } = require('buffer') -const Transform = require('readable-stream').Transform +const { Transform } = require('readable-stream') const duplexify = require('duplexify') /* global wx */ -let socketTask, proxy, stream - -function buildProxy () { - const proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - socketTask.send({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function (errMsg) { - next(new Error(errMsg)) - } - }) - } - proxy._flush = function socketEnd (done) { - socketTask.close({ - success: function () { - done() - } - }) - } - - return proxy +let socketTask +let proxy +let stream + +function buildProxy() { + const _proxy = new Transform() + _proxy._write = (chunk, encoding, next) => { + socketTask.send({ + data: chunk.buffer, + success() { + next() + }, + fail(errMsg) { + next(new Error(errMsg)) + }, + }) + } + _proxy._flush = (done) => { + socketTask.close({ + success() { + done() + }, + }) + } + + return _proxy } -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } +function setDefaultOpts(opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } } -function buildUrl (opts, client) { - const protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' - let url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url +function buildUrl(opts, client) { + const protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' + let url = `${protocol}://${opts.hostname}${opts.path}` + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = `${protocol}://${opts.hostname}:${opts.port}${opts.path}` + } + if (typeof opts.transformWsUrl === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url } -function bindEventHandler () { - socketTask.onOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - socketTask.onMessage(function (res) { - let data = res.data - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - - socketTask.onClose(function () { - stream.end() - stream.destroy() - }) - - socketTask.onError(function (res) { - stream.destroy(new Error(res.errMsg)) - }) +function bindEventHandler() { + socketTask.onOpen(() => { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + socketTask.onMessage((res) => { + let { data } = res + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + + socketTask.onClose(() => { + stream.end() + stream.destroy() + }) + + socketTask.onError((res) => { + stream.destroy(new Error(res.errMsg)) + }) } -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host - - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } - - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - setDefaultOpts(opts) - - const url = buildUrl(opts, client) - socketTask = wx.connectSocket({ - url, - protocols: [websocketSubProtocol] - }) - - proxy = buildProxy() - stream = duplexify.obj() - stream._destroy = function (err, cb) { - socketTask.close({ - success: function () { - cb && cb(err) - } - }) - } - - const destroyRef = stream.destroy - stream.destroy = function () { - stream.destroy = destroyRef - - const self = this - setTimeout(function () { - socketTask.close({ - fail: function () { - self._destroy(new Error()) - } - }) - }, 0) - }.bind(stream) - - bindEventHandler() - - return stream +function buildStream(client, opts) { + opts.hostname = opts.hostname || opts.host + + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } + + const websocketSubProtocol = + opts.protocolId === 'MQIsdp' && opts.protocolVersion === 3 + ? 'mqttv3.1' + : 'mqtt' + + setDefaultOpts(opts) + + const url = buildUrl(opts, client) + socketTask = wx.connectSocket({ + url, + protocols: [websocketSubProtocol], + }) + + proxy = buildProxy() + stream = duplexify.obj() + stream._destroy = (err, cb) => { + socketTask.close({ + success() { + if (cb) cb(err) + }, + }) + } + + const destroyRef = stream.destroy + stream.destroy = () => { + stream.destroy = destroyRef + + setTimeout(() => { + socketTask.close({ + fail() { + stream._destroy(new Error()) + }, + }) + }, 0) + } + + bindEventHandler() + + return stream } module.exports = buildStream diff --git a/lib/default-message-id-provider.js b/lib/default-message-id-provider.js index d7a1da87f..6f544aacf 100644 --- a/lib/default-message-id-provider.js +++ b/lib/default-message-id-provider.js @@ -1,67 +1,63 @@ -'use strict' - /** * DefaultMessageAllocator constructor * @constructor */ class DefaultMessageIdProvider { - constructor () { - /** - * MessageIDs starting with 1 - * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 - */ - this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) - } + constructor() { + /** + * MessageIDs starting with 1 + * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 + */ + this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) + } - /** - * allocate - * - * Get the next messageId. - * @return unsigned int - */ - allocate () { - // id becomes current state of this.nextId and increments afterwards - const id = this.nextId++ - // Ensure 16 bit unsigned int (max 65535, nextId got one higher) - if (this.nextId === 65536) { - this.nextId = 1 - } - return id - } + /** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ + allocate() { + // id becomes current state of this.nextId and increments afterwards + const id = this.nextId++ + // Ensure 16 bit unsigned int (max 65535, nextId got one higher) + if (this.nextId === 65536) { + this.nextId = 1 + } + return id + } - /** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ - getLastAllocated () { - return (this.nextId === 1) ? 65535 : (this.nextId - 1) - } + /** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ + getLastAllocated() { + return this.nextId === 1 ? 65535 : this.nextId - 1 + } - /** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ - register (messageId) { - return true - } + /** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ + register(messageId) { + return true + } - /** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ - deallocate (messageId) { - } + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate(messageId) {} - /** - * clear - * Deallocate all messageIds. - */ - clear () { - } + /** + * clear + * Deallocate all messageIds. + */ + clear() {} } module.exports = DefaultMessageIdProvider diff --git a/lib/handlers/ack.js b/lib/handlers/ack.js index 1109ef11c..de92af60b 100644 --- a/lib/handlers/ack.js +++ b/lib/handlers/ack.js @@ -1,151 +1,153 @@ - // Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND, ETIMEDOUT. const errors = { - 0: '', - 1: 'Unacceptable protocol version', - 2: 'Identifier rejected', - 3: 'Server unavailable', - 4: 'Bad username or password', - 5: 'Not authorized', - 16: 'No matching subscribers', - 17: 'No subscription existed', - 128: 'Unspecified error', - 129: 'Malformed Packet', - 130: 'Protocol Error', - 131: 'Implementation specific error', - 132: 'Unsupported Protocol Version', - 133: 'Client Identifier not valid', - 134: 'Bad User Name or Password', - 135: 'Not authorized', - 136: 'Server unavailable', - 137: 'Server busy', - 138: 'Banned', - 139: 'Server shutting down', - 140: 'Bad authentication method', - 141: 'Keep Alive timeout', - 142: 'Session taken over', - 143: 'Topic Filter invalid', - 144: 'Topic Name invalid', - 145: 'Packet identifier in use', - 146: 'Packet Identifier not found', - 147: 'Receive Maximum exceeded', - 148: 'Topic Alias invalid', - 149: 'Packet too large', - 150: 'Message rate too high', - 151: 'Quota exceeded', - 152: 'Administrative action', - 153: 'Payload format invalid', - 154: 'Retain not supported', - 155: 'QoS not supported', - 156: 'Use another server', - 157: 'Server moved', - 158: 'Shared Subscriptions not supported', - 159: 'Connection rate exceeded', - 160: 'Maximum connect time', - 161: 'Subscription Identifiers not supported', - 162: 'Wildcard Subscriptions not supported' - } + 0: '', + 1: 'Unacceptable protocol version', + 2: 'Identifier rejected', + 3: 'Server unavailable', + 4: 'Bad username or password', + 5: 'Not authorized', + 16: 'No matching subscribers', + 17: 'No subscription existed', + 128: 'Unspecified error', + 129: 'Malformed Packet', + 130: 'Protocol Error', + 131: 'Implementation specific error', + 132: 'Unsupported Protocol Version', + 133: 'Client Identifier not valid', + 134: 'Bad User Name or Password', + 135: 'Not authorized', + 136: 'Server unavailable', + 137: 'Server busy', + 138: 'Banned', + 139: 'Server shutting down', + 140: 'Bad authentication method', + 141: 'Keep Alive timeout', + 142: 'Session taken over', + 143: 'Topic Filter invalid', + 144: 'Topic Name invalid', + 145: 'Packet identifier in use', + 146: 'Packet Identifier not found', + 147: 'Receive Maximum exceeded', + 148: 'Topic Alias invalid', + 149: 'Packet too large', + 150: 'Message rate too high', + 151: 'Quota exceeded', + 152: 'Administrative action', + 153: 'Payload format invalid', + 154: 'Retain not supported', + 155: 'QoS not supported', + 156: 'Use another server', + 157: 'Server moved', + 158: 'Shared Subscriptions not supported', + 159: 'Connection rate exceeded', + 160: 'Maximum connect time', + 161: 'Subscription Identifiers not supported', + 162: 'Wildcard Subscriptions not supported', +} function handleAck(client, packet) { - /* eslint no-fallthrough: "off" */ - const messageId = packet.messageId - const type = packet.cmd - let response = null - const cb = client.outgoing[messageId] ? client.outgoing[messageId].cb : null - let err + /* eslint no-fallthrough: "off" */ + const { messageId } = packet + const type = packet.cmd + let response = null + const cb = client.outgoing[messageId] ? client.outgoing[messageId].cb : null + let err - // Checking `!cb` happens to work, but it's not technically "correct". - // - // Why? client code assumes client "no callback" is the same as client "we're not - // waiting for responses" (puback, pubrec, pubcomp, suback, or unsuback). - // - // It would be better to check `if (!client.outgoing[messageId])` here, but - // there's no reason to change it and risk (another) regression. - // - // The only reason client code works is becaues code in MqttClient.publish, - // MqttClinet.subscribe, and MqttClient.unsubscribe ensures client we will - // have a callback even if the user doesn't pass one in.) - if (!cb) { - client.log('_handleAck :: Server sent an ack in error. Ignoring.') - // Server sent an ack in error, ignore it. - return - } + // Checking `!cb` happens to work, but it's not technically "correct". + // + // Why? client code assumes client "no callback" is the same as client "we're not + // waiting for responses" (puback, pubrec, pubcomp, suback, or unsuback). + // + // It would be better to check `if (!client.outgoing[messageId])` here, but + // there's no reason to change it and risk (another) regression. + // + // The only reason client code works is becaues code in MqttClient.publish, + // MqttClinet.subscribe, and MqttClient.unsubscribe ensures client we will + // have a callback even if the user doesn't pass one in.) + if (!cb) { + client.log('_handleAck :: Server sent an ack in error. Ignoring.') + // Server sent an ack in error, ignore it. + return + } - // Process - client.log('_handleAck :: packet type', type) - switch (type) { - case 'pubcomp': - // same thing as puback for QoS 2 - case 'puback': { - const pubackRC = packet.reasonCode - // Callback - we're done - if (pubackRC && pubackRC > 0 && pubackRC !== 16) { - err = new Error('Publish error: ' + errors[pubackRC]) - err.code = pubackRC - client._removeOutgoingAndStoreMessage(messageId, function () { - cb(err, packet) - }) - } else { - client._removeOutgoingAndStoreMessage(messageId, cb) - } + // Process + client.log('_handleAck :: packet type', type) + switch (type) { + case 'pubcomp': + // same thing as puback for QoS 2 + case 'puback': { + const pubackRC = packet.reasonCode + // Callback - we're done + if (pubackRC && pubackRC > 0 && pubackRC !== 16) { + err = new Error(`Publish error: ${errors[pubackRC]}`) + err.code = pubackRC + client._removeOutgoingAndStoreMessage(messageId, () => { + cb(err, packet) + }) + } else { + client._removeOutgoingAndStoreMessage(messageId, cb) + } - break - } - case 'pubrec': { - response = { - cmd: 'pubrel', - qos: 2, - messageId - } - const pubrecRC = packet.reasonCode + break + } + case 'pubrec': { + response = { + cmd: 'pubrel', + qos: 2, + messageId, + } + const pubrecRC = packet.reasonCode - if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { - err = new Error('Publish error: ' + errors[pubrecRC]) - err.code = pubrecRC - client._removeOutgoingAndStoreMessage(messageId, function () { - cb(err, packet) - }) - } else { - client._sendPacket(response) - } - break - } - case 'suback': { - delete client.outgoing[messageId] - client.messageIdProvider.deallocate(messageId) - for (let grantedI = 0; grantedI < packet.granted.length; grantedI++) { - if ((packet.granted[grantedI] & 0x80) !== 0) { - // suback with Failure status - const topics = client.messageIdToTopic[messageId] - if (topics) { - topics.forEach(function (topic) { - delete client._resubscribeTopics[topic] - }) - } - } - } - delete client.messageIdToTopic[messageId] - client._invokeStoreProcessingQueue() - cb(null, packet) - break - } - case 'unsuback': { - delete client.outgoing[messageId] - client.messageIdProvider.deallocate(messageId) - client._invokeStoreProcessingQueue() - cb(null) - break - } - default: - client.emit('error', new Error('unrecognized packet type')) - } + if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { + err = new Error(`Publish error: ${errors[pubrecRC]}`) + err.code = pubrecRC + client._removeOutgoingAndStoreMessage(messageId, () => { + cb(err, packet) + }) + } else { + client._sendPacket(response) + } + break + } + case 'suback': { + delete client.outgoing[messageId] + client.messageIdProvider.deallocate(messageId) + for ( + let grantedI = 0; + grantedI < packet.granted.length; + grantedI++ + ) { + if ((packet.granted[grantedI] & 0x80) !== 0) { + // suback with Failure status + const topics = client.messageIdToTopic[messageId] + if (topics) { + topics.forEach((topic) => { + delete client._resubscribeTopics[topic] + }) + } + } + } + delete client.messageIdToTopic[messageId] + client._invokeStoreProcessingQueue() + cb(null, packet) + break + } + case 'unsuback': { + delete client.outgoing[messageId] + client.messageIdProvider.deallocate(messageId) + client._invokeStoreProcessingQueue() + cb(null) + break + } + default: + client.emit('error', new Error('unrecognized packet type')) + } - if (client.disconnecting && - Object.keys(client.outgoing).length === 0) { - client.emit('outgoingEmpty') - } + if (client.disconnecting && Object.keys(client.outgoing).length === 0) { + client.emit('outgoingEmpty') + } } -module.exports = handleAck; -module.exports.errors = errors; \ No newline at end of file +module.exports = handleAck +module.exports.errors = errors diff --git a/lib/handlers/auth.js b/lib/handlers/auth.js index 9c2f23d83..6d2d3e9be 100644 --- a/lib/handlers/auth.js +++ b/lib/handlers/auth.js @@ -1,33 +1,34 @@ - +const { errors } = require('./ack') function handleAuth(client, packet) { - const options = client.options - const version = options.protocolVersion - const rc = version === 5 ? packet.reasonCode : packet.returnCode + const { options } = client + const version = options.protocolVersion + const rc = version === 5 ? packet.reasonCode : packet.returnCode - if (version !== 5) { - const err = new Error('Protocol error: Auth packets are only supported in MQTT 5. Your version:' + version) - err.code = rc - client.emit('error', err) - return - } + if (version !== 5) { + const err = new Error( + `Protocol error: Auth packets are only supported in MQTT 5. Your version:${version}`, + ) + err.code = rc + client.emit('error', err) + return + } - client.handleAuth(packet, function (err, packet) { - if (err) { - client.emit('error', err) - return - } + client.handleAuth(packet, (err, packet2) => { + if (err) { + client.emit('error', err) + return + } - if (rc === 24) { - client.reconnecting = false - client._sendPacket(packet) - } else { - const error = new Error('Connection refused: ' + errors[rc]) - err.code = rc - client.emit('error', error) - } - }) + if (rc === 24) { + client.reconnecting = false + client._sendPacket(packet2) + } else { + const error = new Error(`Connection refused: ${errors[rc]}`) + err.code = rc + client.emit('error', error) + } + }) } - -module.exports = handleAuth \ No newline at end of file +module.exports = handleAuth diff --git a/lib/handlers/connack.js b/lib/handlers/connack.js index 55c3d97af..4c52ae80f 100644 --- a/lib/handlers/connack.js +++ b/lib/handlers/connack.js @@ -2,42 +2,50 @@ const { errors } = require('./ack') const TopicAliasSend = require('../topic-alias-send') function handleConnack(client, packet) { - client.log('_handleConnack') - const options = client.options - const version = options.protocolVersion - const rc = version === 5 ? packet.reasonCode : packet.returnCode + client.log('_handleConnack') + const { options } = client + const version = options.protocolVersion + const rc = version === 5 ? packet.reasonCode : packet.returnCode - clearTimeout(client.connackTimer) - delete client.topicAliasSend + clearTimeout(client.connackTimer) + delete client.topicAliasSend - if (packet.properties) { - if (packet.properties.topicAliasMaximum) { - if (packet.properties.topicAliasMaximum > 0xffff) { - client.emit('error', new Error('topicAliasMaximum from broker is out of range')) - return - } - if (packet.properties.topicAliasMaximum > 0) { - client.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) - } - } - if (packet.properties.serverKeepAlive && options.keepalive) { - options.keepalive = packet.properties.serverKeepAlive - client._shiftPingInterval() - } - if (packet.properties.maximumPacketSize) { - if (!options.properties) { options.properties = {} } - options.properties.maximumPacketSize = packet.properties.maximumPacketSize - } - } + if (packet.properties) { + if (packet.properties.topicAliasMaximum) { + if (packet.properties.topicAliasMaximum > 0xffff) { + client.emit( + 'error', + new Error('topicAliasMaximum from broker is out of range'), + ) + return + } + if (packet.properties.topicAliasMaximum > 0) { + client.topicAliasSend = new TopicAliasSend( + packet.properties.topicAliasMaximum, + ) + } + } + if (packet.properties.serverKeepAlive && options.keepalive) { + options.keepalive = packet.properties.serverKeepAlive + client._shiftPingInterval() + } + if (packet.properties.maximumPacketSize) { + if (!options.properties) { + options.properties = {} + } + options.properties.maximumPacketSize = + packet.properties.maximumPacketSize + } + } - if (rc === 0) { - client.reconnecting = false - client._onConnect(packet) - } else if (rc > 0) { - const err = new Error('Connection refused: ' + errors[rc]) - err.code = rc - client.emit('error', err) - } + if (rc === 0) { + client.reconnecting = false + client._onConnect(packet) + } else if (rc > 0) { + const err = new Error(`Connection refused: ${errors[rc]}`) + err.code = rc + client.emit('error', err) + } } -module.exports = handleConnack \ No newline at end of file +module.exports = handleConnack diff --git a/lib/handlers/index.js b/lib/handlers/index.js index 318b5d5ef..51054dbbe 100644 --- a/lib/handlers/index.js +++ b/lib/handlers/index.js @@ -5,54 +5,62 @@ const handleAck = require('./ack') const handlePubrel = require('./pubrel') function handle(client, packet, done) { - const options = client.options + const { options } = client - if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { - client.emit('error', new Error('exceeding packets size ' + packet.cmd)) - client.end({ reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' } }) - return client - } - client.log('_handlePacket :: emitting packetreceive') - client.emit('packetreceive', packet) + if ( + options.protocolVersion === 5 && + options.properties && + options.properties.maximumPacketSize && + options.properties.maximumPacketSize < packet.length + ) { + client.emit('error', new Error(`exceeding packets size ${packet.cmd}`)) + client.end({ + reasonCode: 149, + properties: { reasonString: 'Maximum packet size was exceeded' }, + }) + return client + } + client.log('_handlePacket :: emitting packetreceive') + client.emit('packetreceive', packet) - switch (packet.cmd) { - case 'publish': - handlePublish(client, packet, done) - break - case 'puback': - case 'pubrec': - case 'pubcomp': - case 'suback': - case 'unsuback': - handleAck(client, packet) - done() - break - case 'pubrel': - handlePubrel(client, packet, done) - break - case 'connack': - handleConnack(client, packet) - done() - break - case 'auth': - handleAuth(client, packet) - done() - break - case 'pingresp': - // this will be checked in _checkPing client method every keepalive interval - client.pingResp = true - done() - break - case 'disconnect': - client.emit('disconnect', packet) - done() - break - default: - // TODO: unknown packet received. Should we emit an error? - client.log('_handlePacket :: unknown command') - done() - break - } + switch (packet.cmd) { + case 'publish': + handlePublish(client, packet, done) + break + case 'puback': + case 'pubrec': + case 'pubcomp': + case 'suback': + case 'unsuback': + handleAck(client, packet) + done() + break + case 'pubrel': + handlePubrel(client, packet, done) + break + case 'connack': + handleConnack(client, packet) + done() + break + case 'auth': + handleAuth(client, packet) + done() + break + case 'pingresp': + // this will be checked in _checkPing client method every keepalive interval + client.pingResp = true + done() + break + case 'disconnect': + client.emit('disconnect', packet) + done() + break + default: + // TODO: unknown packet received. Should we emit an error? + client.log('_handlePacket :: unknown command') + done() + break + } } -module.exports = handle \ No newline at end of file +module.exports = handle diff --git a/lib/handlers/publish.js b/lib/handlers/publish.js index d2dbf3221..509fabcf7 100644 --- a/lib/handlers/publish.js +++ b/lib/handlers/publish.js @@ -25,96 +25,143 @@ const validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] for now i just suppressed the warnings */ function handlePublish(client, packet, done) { - client.log('handlePublish: packet %o', packet) - done = typeof done !== 'undefined' ? done : client.nop - let topic = packet.topic.toString() - const message = packet.payload - const qos = packet.qos - const messageId = packet.messageId - const options = client.options - if (client.options.protocolVersion === 5) { - let alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - if (typeof alias !== 'undefined') { - if (topic.length === 0) { - if (alias > 0 && alias <= 0xffff) { - const gotTopic = client.topicAliasRecv.getTopicByAlias(alias) - if (gotTopic) { - topic = gotTopic - client.log('handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) - } else { - client.log('handlePublish :: unregistered topic alias. alias: %d', alias) - client.emit('error', new Error('Received unregistered Topic Alias')) - return - } - } else { - client.log('handlePublish :: topic alias out of range. alias: %d', alias) - client.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } else { - if (client.topicAliasRecv.put(topic, alias)) { - client.log('handlePublish :: registered topic: %s - alias: %d', topic, alias) - } else { - client.log('handlePublish :: topic alias out of range. alias: %d', alias) - client.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } - } - } - client.log('handlePublish: qos %d', qos) - switch (qos) { - case 2: { - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return client.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return client.emit('error', new Error('Wrong reason code for pubrec')) } - if (code) { - client._sendPacket({ cmd: 'pubrec', messageId, reasonCode: code }, done) - } else { - client.incomingStore.put(packet, function () { - client._sendPacket({ cmd: 'pubrec', messageId }, done) - }) - } - }) - break - } - case 1: { - // emit the message event - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return client.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return client.emit('error', new Error('Wrong reason code for puback')) } - if (!code) { client.emit('message', topic, message, packet) } - client.handleMessage(packet, function (err) { - if (err) { - return done && done(err) - } - client._sendPacket({ cmd: 'puback', messageId, reasonCode: code }, done) - }) - }) - break - } - case 0: - // emit the message event - client.emit('message', topic, message, packet) - client.handleMessage(packet, done) - break - default: - // do nothing - client.log('handlePublish: unknown QoS. Doing nothing.') - // log or throw an error about unknown qos - break - } + client.log('handlePublish: packet %o', packet) + done = typeof done !== 'undefined' ? done : client.noop + let topic = packet.topic.toString() + const message = packet.payload + const { qos } = packet + const { messageId } = packet + const { options } = client + if (client.options.protocolVersion === 5) { + let alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + if (typeof alias !== 'undefined') { + if (topic.length === 0) { + if (alias > 0 && alias <= 0xffff) { + const gotTopic = + client.topicAliasRecv.getTopicByAlias(alias) + if (gotTopic) { + topic = gotTopic + client.log( + 'handlePublish :: topic complemented by alias. topic: %s - alias: %d', + topic, + alias, + ) + } else { + client.log( + 'handlePublish :: unregistered topic alias. alias: %d', + alias, + ) + client.emit( + 'error', + new Error('Received unregistered Topic Alias'), + ) + return + } + } else { + client.log( + 'handlePublish :: topic alias out of range. alias: %d', + alias, + ) + client.emit( + 'error', + new Error('Received Topic Alias is out of range'), + ) + return + } + } else if (client.topicAliasRecv.put(topic, alias)) { + client.log( + 'handlePublish :: registered topic: %s - alias: %d', + topic, + alias, + ) + } else { + client.log( + 'handlePublish :: topic alias out of range. alias: %d', + alias, + ) + client.emit( + 'error', + new Error('Received Topic Alias is out of range'), + ) + return + } + } + } + client.log('handlePublish: qos %d', qos) + switch (qos) { + case 2: { + options.customHandleAcks(topic, message, packet, (error, code) => { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { + return client.emit('error', error) + } + if (validReasonCodes.indexOf(code) === -1) { + return client.emit( + 'error', + new Error('Wrong reason code for pubrec'), + ) + } + if (code) { + client._sendPacket( + { cmd: 'pubrec', messageId, reasonCode: code }, + done, + ) + } else { + client.incomingStore.put(packet, () => { + client._sendPacket({ cmd: 'pubrec', messageId }, done) + }) + } + }) + break + } + case 1: { + // emit the message event + options.customHandleAcks(topic, message, packet, (error, code) => { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { + return client.emit('error', error) + } + if (validReasonCodes.indexOf(code) === -1) { + return client.emit( + 'error', + new Error('Wrong reason code for puback'), + ) + } + if (!code) { + client.emit('message', topic, message, packet) + } + client.handleMessage(packet, (err) => { + if (err) { + return done && done(err) + } + client._sendPacket( + { cmd: 'puback', messageId, reasonCode: code }, + done, + ) + }) + }) + break + } + case 0: + // emit the message event + client.emit('message', topic, message, packet) + client.handleMessage(packet, done) + break + default: + // do nothing + client.log('handlePublish: unknown QoS. Doing nothing.') + // log or throw an error about unknown qos + break + } } -module.exports = handlePublish \ No newline at end of file +module.exports = handlePublish diff --git a/lib/handlers/pubrel.js b/lib/handlers/pubrel.js index fb2bc10ef..571ccbc79 100644 --- a/lib/handlers/pubrel.js +++ b/lib/handlers/pubrel.js @@ -1,26 +1,24 @@ - - function handlePubrel(client, packet, done) { - client.log('handling pubrel packet') - callback = typeof done !== 'undefined' ? done : client.nop - const messageId = packet.messageId + client.log('handling pubrel packet') + const callback = typeof done !== 'undefined' ? done : client.noop + const { messageId } = packet - const comp = { cmd: 'pubcomp', messageId } + const comp = { cmd: 'pubcomp', messageId } - client.incomingStore.get(packet, function (err, pub) { - if (!err) { - client.emit('message', pub.topic, pub.payload, pub) - client.handleMessage(pub, function (err) { - if (err) { - return callback(err) - } - client.incomingStore.del(pub, client.nop) - client._sendPacket(comp, callback) - }) - } else { - client._sendPacket(comp, callback) - } - }) + client.incomingStore.get(packet, (err, pub) => { + if (!err) { + client.emit('message', pub.topic, pub.payload, pub) + client.handleMessage(pub, (err2) => { + if (err2) { + return callback(err2) + } + client.incomingStore.del(pub, client.noop) + client._sendPacket(comp, callback) + }) + } else { + client._sendPacket(comp, callback) + } + }) } -module.exports = handlePubrel \ No newline at end of file +module.exports = handlePubrel diff --git a/lib/is-browser.js b/lib/is-browser.js index ac143732f..92c21a3c2 100644 --- a/lib/is-browser.js +++ b/lib/is-browser.js @@ -1,11 +1,11 @@ const legacyIsBrowser = -(typeof process !== 'undefined' && process.title === 'browser') || -// eslint-disable-next-line camelcase - typeof __webpack_require__ === 'function' + (typeof process !== 'undefined' && process.title === 'browser') || + // eslint-disable-next-line camelcase + typeof __webpack_require__ === 'function' const isBrowser = - typeof window !== 'undefined' && typeof document !== 'undefined' + typeof window !== 'undefined' && typeof document !== 'undefined' module.exports = { - IS_BROWSER: isBrowser || legacyIsBrowser + IS_BROWSER: isBrowser || legacyIsBrowser, } diff --git a/lib/store.js b/lib/store.js index 1412967eb..ce7608562 100644 --- a/lib/store.js +++ b/lib/store.js @@ -1,12 +1,11 @@ -'use strict' - /** * Module dependencies */ -const Readable = require('readable-stream').Readable +const { Readable } = require('readable-stream') + const streamsOpts = { objectMode: true } const defaultStoreOptions = { - clean: true + clean: true, } /** @@ -16,109 +15,107 @@ const defaultStoreOptions = { * @param {Object} [options] - store options */ class Store { - constructor (options) { - this.options = options || {} - - // Defaults - this.options = { ...defaultStoreOptions, ...options } - - this._inflights = new Map() - } - - /** - * Adds a packet to the store, a packet is - * anything that has a messageId property. - * - */ - put (packet, cb) { - this._inflights.set(packet.messageId, packet) - - if (cb) { - cb() - } - - return this - } - - /** - * Creates a stream with all the packets in the store - * - */ - createStream () { - const stream = new Readable(streamsOpts) - const values = [] - let destroyed = false - let i = 0 - - this._inflights.forEach(function (value, key) { - values.push(value) - }) - - stream._read = function () { - if (!destroyed && i < values.length) { - this.push(values[i++]) - } else { - this.push(null) - } - } - - stream.destroy = function () { - if (destroyed) { - return - } - - const self = this - - destroyed = true - - setTimeout(function () { - self.emit('close') - }, 0) - } - - return stream - } - - /** - * deletes a packet from the store. - */ - del (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - this._inflights.delete(packet.messageId) - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this - } - - /** - * get a packet from the store. - */ - get (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this - } - - /** - * Close the store - */ - close (cb) { - if (this.options.clean) { - this._inflights = null - } - if (cb) { - cb() - } - } + constructor(options) { + this.options = options || {} + + // Defaults + this.options = { ...defaultStoreOptions, ...options } + + this._inflights = new Map() + } + + /** + * Adds a packet to the store, a packet is + * anything that has a messageId property. + * + */ + put(packet, cb) { + this._inflights.set(packet.messageId, packet) + + if (cb) { + cb() + } + + return this + } + + /** + * Creates a stream with all the packets in the store + * + */ + createStream() { + const stream = new Readable(streamsOpts) + const values = [] + let destroyed = false + let i = 0 + + this._inflights.forEach((value, key) => { + values.push(value) + }) + + stream._read = () => { + if (!destroyed && i < values.length) { + stream.push(values[i++]) + } else { + stream.push(null) + } + } + + stream.destroy = () => { + if (destroyed) { + return + } + + destroyed = true + + setTimeout(() => { + stream.emit('close') + }, 0) + } + + return stream + } + + /** + * deletes a packet from the store. + */ + del(packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + this._inflights.delete(packet.messageId) + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this + } + + /** + * get a packet from the store. + */ + get(packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this + } + + /** + * Close the store + */ + close(cb) { + if (this.options.clean) { + this._inflights = null + } + if (cb) { + cb() + } + } } module.exports = Store diff --git a/lib/topic-alias-recv.js b/lib/topic-alias-recv.js index e6412d68d..949cae680 100644 --- a/lib/topic-alias-recv.js +++ b/lib/topic-alias-recv.js @@ -1,46 +1,44 @@ -'use strict' - /** * Topic Alias receiving manager * This holds alias to topic map * @param {Number} [max] - topic alias maximum entries */ class TopicAliasRecv { - constructor (max) { - this.aliasToTopic = {} - this.max = max - } + constructor(max) { + this.aliasToTopic = {} + this.max = max + } - /** - * Insert or update topic - alias entry. - * @param {String} [topic] - topic - * @param {Number} [alias] - topic alias - * @returns {Boolean} - if success return true otherwise false - */ - put (topic, alias) { - if (alias === 0 || alias > this.max) { - return false - } - this.aliasToTopic[alias] = topic - this.length = Object.keys(this.aliasToTopic).length - return true - } + /** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ + put(topic, alias) { + if (alias === 0 || alias > this.max) { + return false + } + this.aliasToTopic[alias] = topic + this.length = Object.keys(this.aliasToTopic).length + return true + } - /** - * Get topic by alias - * @param {String} [topic] - topic - * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined - */ - getTopicByAlias (alias) { - return this.aliasToTopic[alias] - } + /** + * Get topic by alias + * @param {String} [topic] - topic + * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined + */ + getTopicByAlias(alias) { + return this.aliasToTopic[alias] + } - /** - * Clear all entries - */ - clear () { - this.aliasToTopic = {} - } + /** + * Clear all entries + */ + clear() { + this.aliasToTopic = {} + } } module.exports = TopicAliasRecv diff --git a/lib/topic-alias-send.js b/lib/topic-alias-send.js index f6efcc822..1904a2b35 100644 --- a/lib/topic-alias-send.js +++ b/lib/topic-alias-send.js @@ -1,10 +1,8 @@ -'use strict' - /** * Module dependencies */ const LRUCache = require('lru-cache') -const NumberAllocator = require('number-allocator').NumberAllocator +const { NumberAllocator } = require('number-allocator') /** * Topic Alias sending manager @@ -12,79 +10,79 @@ const NumberAllocator = require('number-allocator').NumberAllocator * @param {Number} [max] - topic alias maximum entries */ class TopicAliasSend { - constructor (max) { - if (max > 0) { - this.aliasToTopic = new LRUCache({ max }) - this.topicToAlias = {} - this.numberAllocator = new NumberAllocator(1, max) - this.max = max - this.length = 0 - } - } + constructor(max) { + if (max > 0) { + this.aliasToTopic = new LRUCache({ max }) + this.topicToAlias = {} + this.numberAllocator = new NumberAllocator(1, max) + this.max = max + this.length = 0 + } + } - /** - * Insert or update topic - alias entry. - * @param {String} [topic] - topic - * @param {Number} [alias] - topic alias - * @returns {Boolean} - if success return true otherwise false - */ - put (topic, alias) { - if (alias === 0 || alias > this.max) { - return false - } - const entry = this.aliasToTopic.get(alias) - if (entry) { - delete this.topicToAlias[entry] - } - this.aliasToTopic.set(alias, topic) - this.topicToAlias[topic] = alias - this.numberAllocator.use(alias) - this.length = this.aliasToTopic.size - return true - } + /** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ + put(topic, alias) { + if (alias === 0 || alias > this.max) { + return false + } + const entry = this.aliasToTopic.get(alias) + if (entry) { + delete this.topicToAlias[entry] + } + this.aliasToTopic.set(alias, topic) + this.topicToAlias[topic] = alias + this.numberAllocator.use(alias) + this.length = this.aliasToTopic.size + return true + } - /** - * Get topic by alias - * @param {Number} [alias] - topic alias - * @returns {String} - if mapped topic exists return topic, otherwise return undefined - */ - getTopicByAlias (alias) { - return this.aliasToTopic.get(alias) - } + /** + * Get topic by alias + * @param {Number} [alias] - topic alias + * @returns {String} - if mapped topic exists return topic, otherwise return undefined + */ + getTopicByAlias(alias) { + return this.aliasToTopic.get(alias) + } - /** - * Get topic by alias - * @param {String} [topic] - topic - * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined - */ - getAliasByTopic (topic) { - const alias = this.topicToAlias[topic] - if (typeof alias !== 'undefined') { - this.aliasToTopic.get(alias) // LRU update - } - return alias - } + /** + * Get topic by alias + * @param {String} [topic] - topic + * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined + */ + getAliasByTopic(topic) { + const alias = this.topicToAlias[topic] + if (typeof alias !== 'undefined') { + this.aliasToTopic.get(alias) // LRU update + } + return alias + } - /** - * Clear all entries - */ - clear () { - this.aliasToTopic.clear() - this.topicToAlias = {} - this.numberAllocator.clear() - this.length = 0 - } + /** + * Clear all entries + */ + clear() { + this.aliasToTopic.clear() + this.topicToAlias = {} + this.numberAllocator.clear() + this.length = 0 + } - /** - * Get Least Recently Used (LRU) topic alias - * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias - */ - getLruAlias () { - const alias = this.numberAllocator.firstVacant() - if (alias) return alias - // get last alias (key) from LRU cache - return [...this.aliasToTopic.keys()][this.aliasToTopic.size - 1] - } + /** + * Get Least Recently Used (LRU) topic alias + * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias + */ + getLruAlias() { + const alias = this.numberAllocator.firstVacant() + if (alias) return alias + // get last alias (key) from LRU cache + return [...this.aliasToTopic.keys()][this.aliasToTopic.size - 1] + } } module.exports = TopicAliasSend diff --git a/lib/unique-message-id-provider.js b/lib/unique-message-id-provider.js index 882bdf7d7..c88be4bb9 100644 --- a/lib/unique-message-id-provider.js +++ b/lib/unique-message-id-provider.js @@ -1,67 +1,61 @@ -'use strict' - -const NumberAllocator = require('number-allocator').NumberAllocator +const { NumberAllocator } = require('number-allocator') /** * UniqueMessageAllocator constructor * @constructor */ class UniqueMessageIdProvider { - constructor () { - if (!(this instanceof UniqueMessageIdProvider)) { - return new UniqueMessageIdProvider() - } - - this.numberAllocator = new NumberAllocator(1, 65535) - } - - /** - * allocate - * - * Get the next messageId. - * @return if messageId is fully allocated then return null, - * otherwise return the smallest usable unsigned int messageId. - */ - allocate () { - this.lastId = this.numberAllocator.alloc() - return this.lastId - } - - /** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ - getLastAllocated () { - return this.lastId - } - - /** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ - register (messageId) { - return this.numberAllocator.use(messageId) - } - - /** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ - deallocate (messageId) { - this.numberAllocator.free(messageId) - } - - /** - * clear - * Deallocate all messageIds. - */ - clear () { - this.numberAllocator.clear() - } + constructor() { + this.numberAllocator = new NumberAllocator(1, 65535) + } + + /** + * allocate + * + * Get the next messageId. + * @return if messageId is fully allocated then return null, + * otherwise return the smallest usable unsigned int messageId. + */ + allocate() { + this.lastId = this.numberAllocator.alloc() + return this.lastId + } + + /** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ + getLastAllocated() { + return this.lastId + } + + /** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ + register(messageId) { + return this.numberAllocator.use(messageId) + } + + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate(messageId) { + this.numberAllocator.free(messageId) + } + + /** + * clear + * Deallocate all messageIds. + */ + clear() { + this.numberAllocator.clear() + } } module.exports = UniqueMessageIdProvider diff --git a/lib/validations.js b/lib/validations.js index 452da7e99..e71dd52a2 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -1,5 +1,3 @@ -'use strict' - /** * Validate a topic to see if it's valid or not. * A topic is valid if it follow below rules: @@ -9,44 +7,44 @@ * @param {String} topic - A topic * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. */ -function validateTopic (topic) { - const parts = topic.split('/') +function validateTopic(topic) { + const parts = topic.split('/') - for (let i = 0; i < parts.length; i++) { - if (parts[i] === '+') { - continue - } + for (let i = 0; i < parts.length; i++) { + if (parts[i] === '+') { + continue + } - if (parts[i] === '#') { - // for Rule #2 - return i === parts.length - 1 - } + if (parts[i] === '#') { + // for Rule #2 + return i === parts.length - 1 + } - if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { - return false - } - } + if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { + return false + } + } - return true + return true } /** * Validate an array of topics to see if any of them is valid or not - * @param {Array} topics - Array of topics + * @param {Array} topics - Array of topics * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one */ -function validateTopics (topics) { - if (topics.length === 0) { - return 'empty_topic_list' - } - for (let i = 0; i < topics.length; i++) { - if (!validateTopic(topics[i])) { - return topics[i] - } - } - return null +function validateTopics(topics) { + if (topics.length === 0) { + return 'empty_topic_list' + } + for (let i = 0; i < topics.length; i++) { + if (!validateTopic(topics[i])) { + return topics[i] + } + } + return null } module.exports = { - validateTopics + validateTopics, } diff --git a/package-lock.json b/package-lock.json index 4d37e7172..bd4207460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,11 @@ "codecov": "^3.8.2", "conventional-changelog-cli": "^3.0.0", "end-of-stream": "^1.4.4", + "eslint": "^8.45.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^5.0.0", "global": "^4.4.0", "mkdirp": "^3.0.1", "mocha": "^10.2.0", @@ -49,12 +54,12 @@ "mqtt-level-store": "^3.1.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", + "prettier": "^3.0.0", "release-it": "^15.11.0", "rimraf": "^5.0.1", "should": "^13.2.3", "sinon": "^15.2.0", "snazzy": "^9.0.0", - "standard": "^17.1.0", "tape": "^5.6.4", "terser": "^5.18.2", "typescript": "^5.1.6" @@ -459,18 +464,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -1024,6 +1017,44 @@ "node": ">=14" } }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pkgr/utils/node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -1980,19 +2011,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" - } - }, "node_modules/arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -2798,42 +2816,6 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, - "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/builtins/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/builtins/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -3397,6 +3379,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -4208,14 +4196,14 @@ } }, "node_modules/degenerator": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-4.0.3.tgz", - "integrity": "sha512-2wY8vmCfxrQpe2PKGYdiWRre5HQRwsAXbAAWRbC+z2b80MEpnWc8A3a9k4TwqwN3Z/Fm3uhNm5vYUZIbMhyRxQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-4.0.4.tgz", + "integrity": "sha512-MTZdZsuNxSBL92rsjx3VFWe57OpRlikyLbcx2B5Dmdv6oScqpMrvpY7zHLMymrUxo3U5+suPUMsNgW/+SZB1lg==", "dev": true, "dependencies": { - "ast-types": "^0.13.2", - "escodegen": "^1.8.1", - "esprima": "^4.0.0", + "ast-types": "^0.13.4", + "escodegen": "^1.14.3", + "esprima": "^4.0.1", "vm2": "^3.9.19" }, "engines": { @@ -4928,17 +4916,17 @@ } }, "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha512-oCOQ8AIC2ciLy/sE2ehafRBleBgDLvzGhBRRev87sP7ovnbvQfqpc3XFI0DhHey2OfVoNV91W+GPC6B3540/5Q==", "dev": true, "dependencies": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", + "fast-levenshtein": "~2.0.4", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "wordwrap": "~1.0.0" }, "engines": { "node": ">= 0.8.0" @@ -4976,9 +4964,9 @@ } }, "node_modules/eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", - "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -5006,7 +4994,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -5018,7 +5005,6 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -5031,57 +5017,35 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" }, "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" } }, - "node_modules/eslint-config-standard-jsx": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz", - "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==", + "node_modules/eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, "peerDependencies": { - "eslint": "^8.8.0", - "eslint-plugin-react": "^7.28.0" + "eslint": ">=7.0.0" } }, "node_modules/eslint-import-resolver-node": { @@ -5104,23 +5068,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-import-resolver-node/node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -5147,49 +5094,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/eslint-plugin-import": { "version": "2.27.5", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", @@ -5236,243 +5140,53 @@ "dependencies": { "esutils": "^2.0.2" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-n": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", - "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", - "dev": true, - "dependencies": { - "builtins": "^5.0.1", - "eslint-plugin-es": "^4.1.0", - "eslint-utils": "^3.0.0", - "ignore": "^5.1.1", - "is-core-module": "^2.11.0", - "minimatch": "^3.1.2", - "resolve": "^1.22.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-n/node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", - "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "node_modules/eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^2.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "url": "https://opencollective.com/prettier" }, "peerDependencies": { - "eslint": ">=5" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } } }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "node_modules/eslint-scope": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", + "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { @@ -5584,18 +5298,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5621,9 +5323,9 @@ } }, "node_modules/espree": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", - "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { "acorn": "^8.9.0", @@ -5872,10 +5574,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6390,18 +6098,6 @@ "xtend": "~4.0.1" } }, - "node_modules/get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -8424,21 +8120,6 @@ "node": "*" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz", - "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -9059,18 +8740,6 @@ "integrity": "sha512-6DzMHJcjbQX/UPHc1rRCBfKlLwDkvuGZ715cIR36wSdYqWXFT35uLXq5P/2orl3tz+t+VOVPxw4yPinQlUDGDQ==", "dev": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/loupe": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", @@ -9525,9 +9194,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.1.tgz", - "integrity": "sha512-reLxBcKUPNBnc/sVtAbxgRVFSegoGeLaSjmphNhcwcolhYLRgtJscn5mRl6YRZNQv40Y7P6JM2YhSIsbL9OB5A==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -10630,36 +10299,6 @@ "node": ">= 0.4" } }, - "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -11021,9 +10660,9 @@ } }, "node_modules/pac-proxy-agent": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-6.0.3.tgz", - "integrity": "sha512-5Hr1KgPDoc21Vn3rsXBirwwDnF/iac1jN/zkpsOYruyT+ZgsUhUOgVwq3v9+ukjZd/yGm/0nzO1fDfl7rkGoHQ==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-6.0.4.tgz", + "integrity": "sha512-FbJYeusBOZNe6bmrC2/+r/HljwExryon16lNKEU82gWiwIPMCEktUPSEAcTkO9K3jd/YPGuX/azZel1ltmo6nQ==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -11077,13 +10716,13 @@ } }, "node_modules/pac-resolver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-6.0.1.tgz", - "integrity": "sha512-dg497MhVT7jZegPRuOScQ/z0aV/5WR0gTdRu1md+Irs9J9o+ls5jIuxjo1WfaTG+eQQkxyn5HMGvWK+w7EIBkQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-6.0.2.tgz", + "integrity": "sha512-EQpuJ2ifOjpZY5sg1Q1ZeAxvtLwR7Mj3RgY8cysPGbsRu3RBXyJFWxnMus9PScjxya/0LzvVDxNh/gl0eXBU4w==", "dev": true, "dependencies": { - "degenerator": "^4.0.1", - "ip": "^1.1.5", + "degenerator": "^4.0.4", + "ip": "^1.1.8", "netmask": "^2.0.2" }, "engines": { @@ -11400,123 +11039,6 @@ "node": ">=0.10.0" } }, - "node_modules/pkg-conf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", - "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0", - "load-json-file": "^5.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-conf/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-conf/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -11688,6 +11210,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -11733,17 +11282,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -12081,12 +11619,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "node_modules/read-only-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", @@ -12345,18 +11877,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/registry-auth-token": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", @@ -12557,17 +12077,20 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-alpn": { @@ -13661,95 +13184,29 @@ "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.0.tgz", - "integrity": "sha512-jaDqlNSzLtWYW4lvQmU0EnxWMUGQiwHasZl5ZEIwx3S/ijZDjZOzs1y1QqKwKs5vqnFpGtizo4NOYX2s0Voq/g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, "dependencies": { - "eslint": "^8.41.0", - "eslint-config-standard": "17.1.0", - "eslint-config-standard-jsx": "^11.0.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-n": "^15.7.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.32.2", - "standard-engine": "^15.0.0", - "version-guard": "^1.1.1" - }, - "bin": { - "standard": "bin/cmd.cjs" + "through": "2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "*" } }, - "node_modules/standard-engine": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.1.0.tgz", - "integrity": "sha512-VHysfoyxFu/ukT+9v49d4BRXIokFRZuH3z1VRxzFArZdjSCFpro6rEIU3ji7e4AoAtuSfKBkiOmsrDqKW5ZSRw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "get-stdin": "^8.0.0", - "minimist": "^1.2.6", - "pkg-conf": "^3.1.0", - "xdg-basedir": "^4.0.0" - }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "node_modules/standard-json": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/standard-json/-/standard-json-1.1.0.tgz", @@ -14056,25 +13513,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", @@ -14223,6 +13661,22 @@ "node": ">= 0.4" } }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", @@ -15031,15 +14485,6 @@ "node": ">= 0.8" } }, - "node_modules/version-guard": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.1.tgz", - "integrity": "sha512-MGQLX89UxmYHgDvcXyjBI0cbmoW+t/dANDppNPrno64rYr8nH4SHSuElQuSYdXGEs0mUzdQe1BY+FhVPNsAmJQ==", - "dev": true, - "engines": { - "node": ">=0.10.48" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -15050,6 +14495,7 @@ "version": "3.9.19", "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz", "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==", + "deprecated": "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.", "dev": true, "dependencies": { "acorn": "^8.7.0", @@ -15063,9 +14509,9 @@ } }, "node_modules/vm2/node_modules/acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -15390,15 +14836,6 @@ "node": ">=6" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -15543,15 +14980,6 @@ } } }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/xmlhttprequest-ssl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", @@ -15976,15 +15404,6 @@ "type-fest": "^0.20.2" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -16412,6 +15831,34 @@ "dev": true, "optional": true }, + "@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "dependencies": { + "open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "requires": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + } + } + } + }, "@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -17208,19 +16655,6 @@ "is-string": "^1.0.7" } }, - "array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" - } - }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -17912,35 +17346,6 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, - "builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "requires": { - "semver": "^7.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, "bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -18390,6 +17795,12 @@ } } }, + "confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -18998,14 +18409,14 @@ "dev": true }, "degenerator": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-4.0.3.tgz", - "integrity": "sha512-2wY8vmCfxrQpe2PKGYdiWRre5HQRwsAXbAAWRbC+z2b80MEpnWc8A3a9k4TwqwN3Z/Fm3uhNm5vYUZIbMhyRxQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-4.0.4.tgz", + "integrity": "sha512-MTZdZsuNxSBL92rsjx3VFWe57OpRlikyLbcx2B5Dmdv6oScqpMrvpY7zHLMymrUxo3U5+suPUMsNgW/+SZB1lg==", "dev": true, "requires": { - "ast-types": "^0.13.2", - "escodegen": "^1.8.1", - "esprima": "^4.0.0", + "ast-types": "^0.13.4", + "escodegen": "^1.14.3", + "esprima": "^4.0.1", "vm2": "^3.9.19" } }, @@ -19598,17 +19009,17 @@ } }, "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha512-oCOQ8AIC2ciLy/sE2ehafRBleBgDLvzGhBRRev87sP7ovnbvQfqpc3XFI0DhHey2OfVoNV91W+GPC6B3540/5Q==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", + "fast-levenshtein": "~2.0.4", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "wordwrap": "~1.0.0" } }, "prelude-ls": { @@ -19636,9 +19047,9 @@ } }, "eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", - "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", @@ -19666,7 +19077,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -19678,7 +19088,6 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -19746,15 +19155,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -19772,17 +19172,22 @@ } } }, - "eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, - "requires": {} + "requires": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + } }, - "eslint-config-standard-jsx": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz", - "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==", + "eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "dev": true, "requires": {} }, @@ -19805,17 +19210,6 @@ "requires": { "ms": "^2.1.1" } - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } } } }, @@ -19825,196 +19219,52 @@ "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "requires": { - "debug": "^3.2.7" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "requires": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "dependencies": { - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", - "dev": true, - "requires": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - } - } - }, - "eslint-plugin-n": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", - "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", - "dev": true, - "requires": { - "builtins": "^5.0.1", - "eslint-plugin-es": "^4.1.0", - "eslint-utils": "^3.0.0", - "ignore": "^5.1.1", - "is-core-module": "^2.11.0", - "minimatch": "^3.1.2", - "resolve": "^1.22.1", - "semver": "^7.3.8" + "debug": "^3.2.7" }, "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "ms": "^2.1.1" } } } }, - "eslint-plugin-promise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", - "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "requires": {} - }, - "eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", "dev": true, "requires": { "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^1.22.1", "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.8" + "tsconfig-paths": "^3.14.1" }, "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -20023,54 +19273,27 @@ "requires": { "esutils": "^2.0.2" } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } } } }, - "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", "dev": true, "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "eslint-scope": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", + "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", "dev": true, "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" } }, "eslint-visitor-keys": { @@ -20080,9 +19303,9 @@ "dev": true }, "espree": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", - "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { "acorn": "^8.9.0", @@ -20276,10 +19499,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -20684,12 +19913,6 @@ } } }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true - }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -22250,18 +21473,6 @@ "through": ">=2.2.7 <3" } }, - "jsx-ast-utils": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz", - "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==", - "dev": true, - "requires": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - } - }, "just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", @@ -22781,15 +21992,6 @@ "integrity": "sha512-6DzMHJcjbQX/UPHc1rRCBfKlLwDkvuGZ715cIR36wSdYqWXFT35uLXq5P/2orl3tz+t+VOVPxw4yPinQlUDGDQ==", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "loupe": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", @@ -23129,9 +22331,9 @@ "dev": true }, "minimatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.1.tgz", - "integrity": "sha512-reLxBcKUPNBnc/sVtAbxgRVFSegoGeLaSjmphNhcwcolhYLRgtJscn5mRl6YRZNQv40Y7P6JM2YhSIsbL9OB5A==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -24026,27 +23228,6 @@ "es-abstract": "^1.20.4" } }, - "object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", - "dev": true, - "requires": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -24296,9 +23477,9 @@ "dev": true }, "pac-proxy-agent": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-6.0.3.tgz", - "integrity": "sha512-5Hr1KgPDoc21Vn3rsXBirwwDnF/iac1jN/zkpsOYruyT+ZgsUhUOgVwq3v9+ukjZd/yGm/0nzO1fDfl7rkGoHQ==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-6.0.4.tgz", + "integrity": "sha512-FbJYeusBOZNe6bmrC2/+r/HljwExryon16lNKEU82gWiwIPMCEktUPSEAcTkO9K3jd/YPGuX/azZel1ltmo6nQ==", "dev": true, "requires": { "agent-base": "^7.0.2", @@ -24342,13 +23523,13 @@ } }, "pac-resolver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-6.0.1.tgz", - "integrity": "sha512-dg497MhVT7jZegPRuOScQ/z0aV/5WR0gTdRu1md+Irs9J9o+ls5jIuxjo1WfaTG+eQQkxyn5HMGvWK+w7EIBkQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-6.0.2.tgz", + "integrity": "sha512-EQpuJ2ifOjpZY5sg1Q1ZeAxvtLwR7Mj3RgY8cysPGbsRu3RBXyJFWxnMus9PScjxya/0LzvVDxNh/gl0eXBU4w==", "dev": true, "requires": { - "degenerator": "^4.0.1", - "ip": "^1.1.5", + "degenerator": "^4.0.4", + "ip": "^1.1.8", "netmask": "^2.0.2" } }, @@ -24599,92 +23780,6 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, - "pkg-conf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", - "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "load-json-file": "^5.2.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - }, - "type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "dev": true - } - } - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -24820,6 +23915,21 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -24853,17 +23963,6 @@ "iterate-value": "^1.0.2" } }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -25144,12 +24243,6 @@ } } }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "read-only-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", @@ -25357,12 +24450,6 @@ "functions-have-names": "^1.2.3" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, "registry-auth-token": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", @@ -25510,12 +24597,12 @@ "dev": true }, "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "requires": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -26413,35 +25500,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.0.tgz", - "integrity": "sha512-jaDqlNSzLtWYW4lvQmU0EnxWMUGQiwHasZl5ZEIwx3S/ijZDjZOzs1y1QqKwKs5vqnFpGtizo4NOYX2s0Voq/g==", - "dev": true, - "requires": { - "eslint": "^8.41.0", - "eslint-config-standard": "17.1.0", - "eslint-config-standard-jsx": "^11.0.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-n": "^15.7.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.32.2", - "standard-engine": "^15.0.0", - "version-guard": "^1.1.1" - } - }, - "standard-engine": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.1.0.tgz", - "integrity": "sha512-VHysfoyxFu/ukT+9v49d4BRXIokFRZuH3z1VRxzFArZdjSCFpro6rEIU3ji7e4AoAtuSfKBkiOmsrDqKW5ZSRw==", - "dev": true, - "requires": { - "get-stdin": "^8.0.0", - "minimist": "^1.2.6", - "pkg-conf": "^3.1.0", - "xdg-basedir": "^4.0.0" - } - }, "standard-json": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/standard-json/-/standard-json-1.1.0.tgz", @@ -26715,22 +25773,6 @@ "strip-ansi": "^6.0.1" } }, - "string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" - } - }, "string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", @@ -26839,6 +25881,16 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + } + }, "syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", @@ -27472,12 +26524,6 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "dev": true }, - "version-guard": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.1.tgz", - "integrity": "sha512-MGQLX89UxmYHgDvcXyjBI0cbmoW+t/dANDppNPrno64rYr8nH4SHSuElQuSYdXGEs0mUzdQe1BY+FhVPNsAmJQ==", - "dev": true - }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -27495,9 +26541,9 @@ }, "dependencies": { "acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-walk": { @@ -27743,12 +26789,6 @@ } } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -27858,12 +26898,6 @@ "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "requires": {} }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, "xmlhttprequest-ssl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", diff --git a/package.json b/package.json index 8947b686e..092d9f1c3 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "main": "mqtt.js", "types": "types/index.d.ts", "scripts": { - "pretest": "standard | snazzy", + "lint": "eslint --ext .js .", + "lint-fix": "eslint --fix --ext .js .", "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", "typescript-compile-execute": "node test/typescript/broker-connect-subscribe-and-publish.js", "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && terser dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", @@ -62,7 +63,7 @@ } }, "pre-commit": [ - "pretest" + "lint" ], "bin": { "mqtt_pub": "./bin/pub.js", @@ -116,6 +117,11 @@ "codecov": "^3.8.2", "conventional-changelog-cli": "^3.0.0", "end-of-stream": "^1.4.4", + "eslint": "^8.45.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^5.0.0", "global": "^4.4.0", "mkdirp": "^3.0.1", "mocha": "^10.2.0", @@ -123,19 +129,14 @@ "mqtt-level-store": "^3.1.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", + "prettier": "^3.0.0", "release-it": "^15.11.0", "rimraf": "^5.0.1", "should": "^13.2.3", "sinon": "^15.2.0", "snazzy": "^9.0.0", - "standard": "^17.1.0", "tape": "^5.6.4", "terser": "^5.18.2", "typescript": "^5.1.6" - }, - "standard": { - "env": [ - "mocha" - ] } } diff --git a/test/abstract_client.js b/test/abstract_client.js index f4ac2c72e..cba436cb4 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -1,3469 +1,3718 @@ -'use strict' - /** * Testing dependencies */ -const should = require('chai').should +const { should } = require('chai') const sinon = require('sinon') -const mqtt = require('../') -const Store = require('./../lib/store') -const assert = require('chai').assert -const ports = require('./helpers/port_list') -const serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +const { assert } = require('chai') const fs = require('fs') const levelStore = require('mqtt-level-store') +const mqtt = require('..') +const Store = require('../lib/store') +const ports = require('./helpers/port_list') +const { serverBuilder } = require('./server_helpers_for_client_tests') const handlePubrel = require('../lib/handlers/pubrel') const handle = require('../lib/handlers/index') const handlePublish = require('../lib/handlers/publish') /** - * These tests try to be consistent with names for servers (brokers) and clients, - * but it can be confusing. To make it easier, here is a handy translation - * chart: - * - * name | meaning - * ---------------|-------- - * client | The MQTT.js client object being tested. A new instance is created for each test (by calling the `connect` function.) - * server | A mock broker that you can control. The same server instance is used for all tests, so only use this if you plan to clean up when you're done. - * serverBuilder | A factory that can make mock test servers (MQTT brokers). Useful if you need to do things that you can't (or don't want to) clean up after your test is done. - * server2 | The name used for mock brokers that are created for an individual test and then destroyed. - * serverClient | An socket on the mock broker. This gets created when your client connects and gets collected when you're done with it. - * - * Also worth noting: - * - * `serverClient.disconnect()` does not disconnect that socket. Instead, it sends an MQTT disconnect packet. - * If you want to disconnect the socket from the broker side, you probably want to use `serverClient.destroy()` - * or `serverClient.stream.destroy()`. - * - */ - -module.exports = function (server, config) { - const version = config.protocolVersion || 4 - - function connect(opts) { - opts = { ...config, ...opts } - return mqtt.connect(opts) - } - - describe('closing', function () { - it('should emit close if stream closes', function (done) { - const client = connect() - - client.once('connect', function () { - client.stream.end() - }) - client.once('close', function () { - client.end((err) => done(err)) - }) - }) - - it('should mark the client as disconnected', function (done) { - const client = connect() - - client.once('close', function () { - client.end((err) => { - if (!client.connected) { - done(err) - } else { - done(new Error('Not marked as disconnected')) - } - }) - assert.isFalse(client.connected) - }) - client.once('connect', function () { - client.stream.end() - }) - }) - - it('should stop ping timer if stream closes', function (done) { - const client = connect() - - client.once('close', function () { - assert.notExists(client.pingTimer) - client.end(true, (err) => done(err)) - }) - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.stream.end() - }) - }) - - it('should emit close after end called', function (done) { - const client = connect() - - client.once('close', function () { - done() - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should emit end after end called and client must be disconnected', function (done) { - const client = connect() - - client.once('end', function () { - if (client.disconnected) { - return done() - } - done(new Error('client must be disconnected')) - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { - const store = new Store() - const client = connect({ incomingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { - const store = new Store() - const client = connect({ outgoingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should return `this` if end called twice', function (done) { - const client = connect() - - client.once('connect', function () { - client.end() - const value = client.end() - if (value === client) { - done() - } else { - done(new Error('Not returning client.')) - } - }) - }) - - it('should emit end only on first client end', function (done) { - const client = connect() - - client.once('end', function () { - const timeout = setTimeout(done.bind(null), 200) - client.once('end', function () { - clearTimeout(timeout) - done(new Error('end was emitted twice')) - }) - client.end() - }) - - client.once('connect', client.end.bind(client)) - }) - - it('should stop ping timer after end called', function (done) { - const client = connect() - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end((err) => { - assert.notExists(client.pingTimer) - done(err) - }) - }) - }) - - it('should be able to end even on a failed connection', function (done) { - const client = connect({ host: 'this_hostname_should_not_exist' }) - - const timeout = setTimeout(function () { - done(new Error('Failed to end a disconnected client')) - }, 500) - - setTimeout(function () { - client.end(function (err) { - clearTimeout(timeout) - done(err) - }) - }, 200) - }) - - it('should emit end even on a failed connection', function (done) { - const client = connect({ host: 'this_hostname_should_not_exist' }) - - const timeout = setTimeout(function () { - done(new Error('Disconnected client has failed to emit end')) - }, 500) - - client.once('end', function () { - clearTimeout(timeout) - done() - }) - - // after 200ms manually invoke client.end - setTimeout(() => { - const boundEnd = client.end.bind(client) - boundEnd() - }, 200) - }) - - it.skip('should emit end only once for a reconnecting client', function (done) { - // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. - // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code - // there will be gists showing the difference between a successful test here and a failed test. For now we - // will add the retries syntax because of the flakiness. - const client = connect({ host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20 }) - setTimeout(done.bind(null), 1000) - const endCallback = function () { - assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') - } - - const spy = sinon.spy(endCallback) - client.on('end', spy) - setTimeout(() => { - client.end.bind(client) - client.end() - }, 300) - }) - }) - - describe('connecting', function () { - it('should connect to the broker', function (done) { - const client = connect() - client.on('error', done) - - server.once('client', function () { - client.end((err) => done(err)) - }) - }) - - it('should send a default client id', function (done) { - const client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'mqttjs') - client.end((err) => done(err)) - }) - }) - }) - - it('should send be clean by default', function (done) { - const client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.strictEqual(packet.clean, true) - done() - }) - }) - }) - - it('should connect with the given client id', function (done) { - const client = connect({ clientId: 'testclient' }) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - client.end((err) => done(err)) - }) - }) - }) - - it('should connect with the client id and unclean state', function (done) { - const client = connect({ clientId: 'testclient', clean: false }) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - assert.isFalse(packet.clean) - client.end(false, (err) => done(err)) - }) - }) - }) - - it('should require a clientId with clean=false', function (done) { - try { - const client = connect({ clean: false }) - client.on('error', function (err) { - done(err) - }) - } catch (err) { - assert.strictEqual(err.message, 'Missing clientId for unclean clients') - done() - } - }) - - it('should default to localhost', function (done) { - const client = connect({ clientId: 'testclient' }) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - done() - }) - }) - }) - - it('should emit connect', function (done) { - const client = connect() - client.once('connect', function () { - client.end(true, (err) => done(err)) - }) - client.once('error', done) - }) - - it('should provide connack packet with connect event', function (done) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - server.once('client', function (serverClient) { - connack.sessionPresent = true - serverClient.connack(connack) - server.once('client', function (serverClient) { - connack.sessionPresent = false - serverClient.connack(connack) - }) - }) - - const client = connect() - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, true) - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, false) - client.end((err) => done(err)) - }) - }) - }) - - it('should mark the client as connected', function (done) { - const client = connect() - client.once('connect', function () { - assert.isTrue(client.connected) - client.end((err) => done(err)) - }) - }) - - it('should emit error on invalid clientId', function (done) { - const client = connect({ clientId: 'invalid' }) - client.once('connect', function () { - done(new Error('Should not emit connect')) - }) - client.once('error', function (error) { - const value = version === 5 ? 128 : 2 - assert.strictEqual(error.code, value) // code for clientID identifer rejected - client.end((err) => done(err)) - }) - }) - - it('should emit error event if the socket refuses the connection', function (done) { - // fake a port - const client = connect({ port: 4557 }) - - client.on('error', function (e) { - assert.equal(e.code, 'ECONNREFUSED') - client.end((err) => done(err)) - }) - }) - - it('should have different client ids', function (done) { - // bug identified in this test: the client.end callback is invoked twice, once when the `end` - // method completes closing the stores and invokes the callback, and another time when the - // stream is closed. When the stream is closed, for some reason the closeStores method is called - // a second time. - const client1 = connect() - const client2 = connect() - - assert.notStrictEqual(client1.options.clientId, client2.options.clientId) - client1.end(true, () => { - client2.end(true, () => { - done() - }) - }) - }) - }) - - describe('handling offline states', function () { - it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { - const client = connect({ reconnectPeriod: 20 }) - - client.on('connect', function () { - this.stream.end() - }) - - client.on('offline', function () { - client.end(true, done) - }) - }) - - it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { - // fake a port - const client = connect({ reconnectPeriod: 20, port: 4557 }) - - client.on('error', function () { }) - - client.on('offline', function () { - client.end(true, done) - }) - }) - }) - - describe('topic validations when subscribing', function () { - it('should be ok for well-formated topics', function (done) { - const client = connect() - client.subscribe( - [ - '+', '+/event', 'event/+', '#', 'event/#', 'system/event/+', - 'system/+/event', 'system/registry/event/#', 'system/+/event/#', - 'system/registry/event/new_device', 'system/+/+/new_device' - ], - function (err) { - client.end(function () { - if (err) { - return done(new Error(err)) - } - done() - }) - } - ) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - const client = connect() - client.subscribe(['#/event', 'event#', 'event+'], function (err) { - client.end(false, function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an empty array for duplicate subs', function (done) { - const client = connect() - client.subscribe('event', function (err, granted1) { - if (err) { - return done(err) - } - client.subscribe('event', function (err, granted2) { - if (err) { - return done(err) - } - assert.isArray(granted2) - assert.isEmpty(granted2) - done() - }) - }) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - const client = connect() - client.subscribe('#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic event#', function (done) { - const client = connect() - client.subscribe('event#', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic system/#/event', function (done) { - const client = connect() - client.subscribe('system/#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for empty topic list', function (done) { - const client = connect() - client.subscribe([], (subErr) => { - client.end((endErr) => { - if (subErr) { - return done(endErr) - } else { - done(new Error('Validations do NOT work')) - } - }) - }) - }) - - it('should return an error (via callbacks) for topic system/+/#/event', function (done) { - const client = connect() - client.subscribe('system/+/#/event', function (subErr) { - client.end(true, (endErr) => { - if (subErr) { - return done(endErr) - } else { - done(new Error('Validations do NOT work')) - } - }) - }) - }) - }) - - describe('offline messages', function () { - it('should queue message until connected', function (done) { - const client = connect() - - client.publish('test', 'test') - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 3) - - client.once('connect', function () { - assert.strictEqual(client.queue.length, 0) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not queue qos 0 messages if queueQoSZero is false', function (done) { - const client = connect({ queueQoSZero: false }) - - client.publish('test', 'test', { qos: 0 }) - assert.strictEqual(client.queue.length, 0) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should queue qos != 0 messages', function (done) { - const client = connect({ queueQoSZero: false }) - - client.publish('test', 'test', { qos: 1 }) - client.publish('test', 'test', { qos: 2 }) - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 2) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not interrupt messages', function (done) { - let client = null - let publishCount = 0 - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function () { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (packet.qos !== 0) { - serverClient.puback({ messageId: packet.messageId }) - } - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - break - case 3: - assert.strictEqual(packet.payload.toString(), 'payload4') - client.end((err1) => { - server2.close((err2) => done(err1 || err2)) - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore, - queueQoSZero: true - }) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'connack') { - setImmediate( - function () { - client.publish('test', 'payload3', { qos: 1 }) - client.publish('test', 'payload4', { qos: 0 }) - } - ) - } - }) - client.publish('test', 'payload1', { qos: 2 }) - client.publish('test', 'payload2', { qos: 2 }) - }) - }) - - it('should not overtake the messages stored in the level-db-store', function (done) { - const storePath = fs.mkdtempSync('test-store_') - const store = levelStore(storePath) - let client = null - const incomingStore = store.incoming - const outgoingStore = store.outgoing - let publishCount = 0 - - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function () { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (packet.qos !== 0) { - serverClient.puback({ messageId: packet.messageId }) - } - - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - - server2.close((err) => { - fs.rmSync(storePath, { recursive: true }) - done(err) - }) - break - } - }) - }) - - const clientOptions = { - port: ports.PORTAND72, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore, - queueQoSZero: true - } - - server2.listen(ports.PORTAND72, function () { - client = connect(clientOptions) - - client.once('close', function () { - client.once('connect', function () { - client.publish('test', 'payload2', { qos: 1 }) - client.publish('test', 'payload3', { qos: 1 }, function () { - client.end(false) - }) - }) - // reconecting - client.reconnect(clientOptions) - }) - - // publish and close - client.once('connect', function () { - client.publish('test', 'payload1', { - qos: 1, - cbStorePut: function () { - client.end(true) - } - }) - }) - }) - }) - - it('should call cb if an outgoing QoS 0 message is not sent', function (done) { - const client = connect({ queueQoSZero: false }) - let called = false - - client.publish('test', 'test', { qos: 0 }, function () { - called = true - }) - - client.on('connect', function () { - assert.isTrue(called) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should delay ending up until all inflight messages are delivered', function (done) { - const client = connect() - let subscribeCalled = false - - client.on('connect', function () { - client.subscribe('test', function () { - subscribeCalled = true - }) - client.publish('test', 'test', function () { - client.end(false, function () { - assert.strictEqual(subscribeCalled, true) - done() - }) - }) - }) - }) - - it('wait QoS 1 publish messages', function (done) { - const client = connect() - let messageReceived = false - - client.on('connect', function () { - client.subscribe('test') - client.publish('test', 'test', { qos: 1 }, function () { - client.end(false, function () { - assert.strictEqual(messageReceived, true) - done() - }) - }) - client.on('message', function () { - messageReceived = true - }) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - }) - }) - - it('does not wait acks when force-closing', function (done) { - // non-running broker - const client = connect('mqtt://localhost:8993') - client.publish('test', 'test', { qos: 1 }) - client.end(true, done) - }) - - it('should call cb if store.put fails', function (done) { - const store = new Store() - store.put = function (packet, cb) { - process.nextTick(cb, new Error('oops there is an error')) - } - const client = connect({ incomingStore: store, outgoingStore: store }) - client.publish('test', 'test', { qos: 2 }, function (err) { - if (err) { - client.end(true, done) - } - }) - }) - }) - - describe('publishing', function () { - it('should publish a message (offline)', function (done) { - const client = connect() - const payload = 'test' - const topic = 'test' - // don't wait on connect to send publish - client.publish(topic, payload) - - server.on('client', onClient) - - function onClient(serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (online)', function (done) { - const client = connect() - const payload = 'test' - const topic = 'test' - // block on connect before sending publish - client.on('connect', function () { - client.publish(topic, payload) - }) - - server.on('client', onClient) - - function onClient(serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (retain, offline)', function (done) { - const client = connect({ queueQoSZero: true }) - const payload = 'test' - const topic = 'test' - let called = false - - client.publish(topic, payload, { retain: true }, function () { - called = true - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, true) - assert.strictEqual(called, true) - client.end(true, done) - }) - }) - }) - - it('should emit a packetsend event', function (done) { - const client = connect() - const payload = 'test_payload' - const topic = 'testTopic' - - client.on('packetsend', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - } else { - done(new Error('packet.cmd was not publish!')) - } - }) - - client.publish(topic, payload) - }) - - it('should accept options', function (done) { - const client = connect() - const payload = 'test' - const topic = 'test' - const opts = { - retain: true, - qos: 1 - } - let received = false - - client.once('connect', function () { - client.publish(topic, payload, opts, function (err) { - assert(received) - client.end(function () { - done(err) - }) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, false, 'incorrect dup') - received = true - }) - }) - }) - - it('should publish with the default options for an empty parameter', function (done) { - const client = connect() - const payload = 'test' - const topic = 'test' - const defaultOpts = { qos: 0, retain: false, dup: false } - - client.once('connect', function () { - client.publish(topic, payload, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') - client.end(true, done) - }) - }) - }) - - it('should mark a message as duplicate when "dup" option is set', function (done) { - const client = connect() - const payload = 'duplicated-test' - const topic = 'test' - const opts = { - retain: true, - qos: 1, - dup: true - } - let received = false - - client.once('connect', function () { - client.publish(topic, payload, opts, function (err) { - assert(received) - client.end(function () { - done(err) - }) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') - received = true - }) - }) - }) - - it('should fire a callback (qos 0)', function (done) { - const client = connect() - - client.once('connect', function () { - client.publish('a', 'b', function () { - client.end((err) => done(err)) - }) - }) - }) - - it('should fire a callback (qos 1)', function (done) { - const client = connect() - const opts = { qos: 1 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end((err) => done(err)) - }) - }) - }) - - it('should fire a callback (qos 1) on error', function (done) { - // 145 = Packet Identifier in use - const pubackReasonCode = 145 - const pubOpts = { qos: 1 } - let client = null - - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function () { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (packet.qos === 1) { - if (version === 5) { - serverClient.puback({ - messageId: packet.messageId, - reasonCode: pubackReasonCode - }) - } else { - serverClient.puback({ messageId: packet.messageId }) - } - } - }) - }) - - server2.listen(ports.PORTAND72, function () { - client = connect({ - port: ports.PORTAND72, - host: 'localhost', - clean: true, - clientId: 'cid1', - reconnectPeriod: 0 - }) - - client.once('connect', function () { - client.publish('a', 'b', pubOpts, function (err) { - if (version === 5) { - assert.strictEqual(err.code, pubackReasonCode) - } else { - assert.ifError(err) - } - setImmediate(function () { - client.end(() => { - server2.close(done()) - }) - }) - }) - }) - }) - }) - - it('should fire a callback (qos 2)', function (done) { - const client = connect() - const opts = { qos: 2 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end((err) => done(err)) - }) - }) - }) - - it('should fire a callback (qos 2) on error', function (done) { - // 145 = Packet Identifier in use - const pubrecReasonCode = 145 - const pubOpts = { qos: 2 } - let client = null - - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function () { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (packet.qos === 2) { - if (version === 5) { - serverClient.pubrec({ - messageId: packet.messageId, - reasonCode: pubrecReasonCode - }) - } else { - serverClient.pubrec({ messageId: packet.messageId }) - } - } - }) - serverClient.on('pubrel', function (packet) { - if (!serverClient.writable) return false - serverClient.pubcomp(packet) - }) - }) - - server2.listen(ports.PORTAND103, function () { - client = connect({ - port: ports.PORTAND103, - host: 'localhost', - clean: true, - clientId: 'cid1', - reconnectPeriod: 0 - }) - - client.once('connect', function () { - client.publish('a', 'b', pubOpts, function (err) { - if (version === 5) { - assert.strictEqual(err.code, pubrecReasonCode) - } else { - assert.ifError(err) - } - setImmediate(function () { - client.end(true, () => { - server2.close(done()) - }) - }) - }) - }) - }) - }) - - it('should support UTF-8 characters in topic', function (done) { - const client = connect() - - client.once('connect', function () { - client.publish('中国', 'hello', function () { - client.end((err) => done(err)) - }) - }) - }) - - it('should support UTF-8 characters in payload', function (done) { - const client = connect() - - client.once('connect', function () { - client.publish('hello', '中国', function () { - client.end((err) => done(err)) - }) - }) - }) - - it('should publish 10 QoS 2 and receive them', function (done) { - const client = connect() - let countSent = 0 - let countReceived = 0 - - function publishNext() { - client.publish('test', 'test', { qos: 2 }, function (err) { - assert.ifError(err) - countSent++ - }) - } - - client.on('connect', function () { - client.subscribe('test', function (err) { - assert.ifError(err) - publishNext() - }) - }) - - client.on('message', function () { - countReceived++ - if (countSent >= 10 && countReceived >= 10) { - client.end(done) - } else { - publishNext() - } - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end() - done('error went offline... didnt see this happen') - }) - - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - }) - }) - - function testQosHandleMessage(qos, done) { - const client = connect() - - let messageEventCount = 0 - let handleMessageCount = 0 - - client.handleMessage = function (packet, callback) { - setTimeout(function () { - handleMessageCount++ - // next message event should not emit until handleMessage completes - assert.strictEqual(handleMessageCount, messageEventCount) - if (handleMessageCount === 10) { - setTimeout(function () { - client.end(true, done) - }) - } - callback() - }, 100) - } - - client.on('message', function (topic, message, packet) { - messageEventCount++ - }) - - client.on('connect', function () { - client.subscribe('test') - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end(true, function () { - done('error went offline... didnt see this happen') - }) - }) - - serverClient.on('subscribe', function () { - for (let i = 0; i < 10; i++) { - serverClient.publish({ - messageId: i, - topic: 'test', - payload: 'test' + i, - qos - }) - } - }) - }) - } - - const qosTests = [0, 1, 2] - qosTests.forEach(function (QoS) { - it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { - testQosHandleMessage(QoS, done) - }) - }) - - it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { - const client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client._sendPacket = sinon.spy() - - handlePublish(client, { - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }, function (err) { - assert.exists(err) - }) - - assert.strictEqual(client._sendPacket.callCount, 0) - client.end() - client.on('connect', function () { done() }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePublish` method', function (done) { - const client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - try { - handlePublish(client, { - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - - it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { - class AsyncStore { - put(packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - close(cb) { - cb() - } - } - - const store = new AsyncStore() - const client = connect({ incomingStore: store }) - - handlePublish(client, { - messageId: 1, - topic: 'test', - payload: 'test', - qos: 1 - }, function () { - client.end((err) => done(err)) - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { - class AsyncStore { - put(packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del(packet, cb) { - process.nextTick(function () { - cb(new Error('Error')) - }) - } - - get(packet, cb) { - process.nextTick(function () { - cb(null, { cmd: 'publish' }) - }) - } - - close(cb) { - cb() - } - } - - const store = new AsyncStore() - const client = connect({ incomingStore: store }) - - handlePublish(client, { - messageId: 1, - topic: 'test', - payload: 'test', - qos: 2 - }, function () { - client.end((err) => done(err)) - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { - class AsyncStore { - put(packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del(packet, cb) { - process.nextTick(function () { - cb(new Error('Error')) - }) - } - - get(packet, cb) { - process.nextTick(function () { - cb(null, { cmd: 'publish' }) - }) - } - - close(cb) { - cb() - } - } - - const store = new AsyncStore() - const client = connect({ incomingStore: store }) - - handlePubrel(client, { - messageId: 1, - qos: 2 - }, function () { - client.end(true, (err) => done(err)) - }) - }) - - it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { - let delComplete = false - class AsyncStore { - put(packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del(packet, cb) { - process.nextTick(function () { - delComplete = true - cb(null) - }) - } - - get(packet, cb) { - process.nextTick(function () { - cb(null, { cmd: 'publish' }) - }) - } - - close(cb) { - cb() - } - } - - const store = new AsyncStore() - const client = connect({ incomingStore: store }) - - handlePubrel(client, { - messageId: 1, - qos: 2 - }, function () { - assert.isTrue(delComplete) - client.end(true, done) - }) - }) - - it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function (done) { - const store = new Store() - const client = connect({ incomingStore: store }) - - const messageId = Math.floor(65535 * Math.random()) - const topic = 'testTopic' - const payload = 'testPayload' - const qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, { qos: 2 }) - - store.put({ - messageId, - topic, - payload, - qos, - cmd: 'publish' - }, function () { - // cleans up the client - client._sendPacket = sinon.spy() - handlePubrel(client, { cmd: 'pubrel', messageId }, function (err) { - assert.exists(err) - assert.strictEqual(client._sendPacket.callCount, 0) - client.end(true, done) - }) - }) - }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePubrel` method', function (done) { - const store = new Store() - const client = connect({ incomingStore: store }) - - const messageId = Math.floor(65535 * Math.random()) - const topic = 'test' - const payload = 'test' - const qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, { qos: 2 }) - - store.put({ - messageId, - topic, - payload, - qos, - cmd: 'publish' - }, function () { - try { - handlePubrel(client, { cmd: 'pubrel', messageId }) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - }) - }) - - it('should keep message order', function (done) { - let publishCount = 0 - let reconnect = false - let client = {} - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () { }) - - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({ messageId: packet.messageId }) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - client.end((err1) => { - server2.close((err2) => done(err1 || err2)) - }) - break - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', { qos: 1 }) - client.publish('topic', 'payload2', { qos: 1 }) - client.end(true) - } else { - client.publish('topic', 'payload3', { qos: 1 }) - } - }) - client.on('close', function () { - if (!reconnect) { - client.reconnect({ - clean: false, - incomingStore, - outgoingStore - }) - reconnect = true - } - }) - }) - }) - - function testCallbackStorePutByQoS(qos, clean, expected, done) { - const client = connect({ - clean, - clientId: 'testId' - }) - - const callbacks = [] - - function cbStorePut() { - callbacks.push('storeput') - } - - client.on('connect', function () { - client.publish('test', 'test', { qos, cbStorePut }, function (err) { - if (err) done(err) - callbacks.push('publish') - assert.deepEqual(callbacks, expected) - client.end(true, done) - }) - }) - } - - const callbackStorePutByQoSParameters = [ - { args: [0, true], expected: ['publish'] }, - { args: [0, false], expected: ['publish'] }, - { args: [1, true], expected: ['storeput', 'publish'] }, - { args: [1, false], expected: ['storeput', 'publish'] }, - { args: [2, true], expected: ['storeput', 'publish'] }, - { args: [2, false], expected: ['storeput', 'publish'] } - ] - - callbackStorePutByQoSParameters.forEach(function (test) { - if (test.args[0] === 0) { // QoS 0 - it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } else { // QoS 1 and 2 - it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } - }) - }) - - describe('unsubscribing', function () { - it('should send an unsubscribe packet (offline)', function (done) { - const client = connect() - let received = false - - client.unsubscribe('test', function (err) { - assert.ifError(err) - assert(received) - client.end(done) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, 'test') - received = true - }) - }) - }) - - it('should send an unsubscribe packet', function (done) { - const client = connect() - const topic = 'topic' - let received = false - - client.once('connect', function () { - client.unsubscribe(topic, function (err) { - assert.ifError(err) - assert(received) - client.end(done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - received = true - }) - }) - }) - - it('should emit a packetsend event', function (done) { - const client = connect() - const testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - client.end(true, done) - } - }) - }) - - it('should emit a packetreceive event', function (done) { - const client = connect() - const testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - client.end(true, done) - } - }) - }) - - it('should accept an array of unsubs', function (done) { - const client = connect() - const topics = ['topic1', 'topic2'] - let received = false - - client.once('connect', function () { - client.unsubscribe(topics, function (err) { - assert.ifError(err) - assert(received) - client.end(done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.deepStrictEqual(packet.unsubscriptions, topics) - received = true - }) - }) - }) - - it('should fire a callback on unsuback', function (done) { - const client = connect() - const topic = 'topic' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - serverClient.unsuback(packet) - }) - }) - }) - - it('should unsubscribe from a chinese topic', function (done) { - const client = connect() - const topic = '中国' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(err => { - done(err) - }) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - }) - }) - }) - }) - - describe('keepalive', function () { - let clock - - // eslint-disable-next-line - beforeEach(function () { - clock = sinon.useFakeTimers() - }) - - afterEach(function () { - clock.restore() - }) - - it('should checkPing at keepalive interval', function (done) { - const interval = 3 - const client = connect({ keepalive: interval }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 1) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 2) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 3) - - client.end(true, done) - }) - }) - - it('should not checkPing if publishing at a higher rate than keepalive', function (done) { - const intervalMs = 3000 - const client = connect({ keepalive: intervalMs / 1000 }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 0) - client.end(true, done) - }) - }) - - it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function (done) { - const intervalMs = 3000 - const client = connect({ - keepalive: intervalMs / 1000, - reschedulePings: false - }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 1) - client.end(true, done) - }) - }) - }) - - describe('pinging', function () { - it('should set a ping timer', function (done) { - const client = connect({ keepalive: 3 }) - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should not set a ping timer keepalive=0', function (done) { - const client = connect({ keepalive: 0 }) - client.on('connect', function () { - assert.notExists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should reconnect if pingresp is not sent', function (done) { - this.timeout(4000) - const client = connect({ keepalive: 1, reconnectPeriod: 100 }) - - // Fake no pingresp being send by stubbing the _handlePingresp function - client.on('packetreceive', function (packet) { - if (packet.cmd === 'pingresp') { - setImmediate(() => { - client.pingResp = false - }) - } - }) - - client.once('connect', function () { - client.once('connect', function () { - client.end(true, done) - }) - }) - }) - - it('should not reconnect if pingresp is successful', function (done) { - const client = connect({ keepalive: 100 }) - client.once('close', function () { - done(new Error('Client closed connection')) - }) - setTimeout(done, 1000) - }) - - it('should defer the next ping when sending a control packet', function (done) { - const client = connect({ keepalive: 1 }) - - client.once('connect', function () { - client._checkPing = sinon.spy() - - client.publish('foo', 'bar') - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - done() - }, 75) - }, 75) - }, 75) - }) - }) - }) - - describe('subscribing', function () { - it('should send a subscribe message (offline)', function (done) { - const client = connect() - - client.subscribe('test') - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - done() - }) - }) - }) - - it('should send a subscribe message', function (done) { - const client = connect() - const topic = 'test' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - const result = { - topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - done() - }) - }) - }) - - it('should emit a packetsend event', function (done) { - const client = connect() - const testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - done() - } - }) - }) - - it('should emit a packetreceive event', function (done) { - const client = connect() - const testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - done() - } - }) - }) - - it('should accept an array of subscriptions', function (done) { - const client = connect() - const subs = ['test1', 'test2'] - - client.once('connect', function () { - client.subscribe(subs) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] - const expected = subs.map(function (i) { - const result = { topic: i, qos: 0 } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - return result - }) - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept a hash of subscriptions', function (done) { - const client = connect() - const topics = { - test1: { qos: 0 }, - test2: { qos: 1 } - } - - client.once('connect', function () { - client.subscribe(topics) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - const expected = [] - - for (const k in topics) { - if (Object.prototype.hasOwnProperty.call(topics, k)) { - const result = { - topic: k, - qos: topics[k].qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - expected.push(result) - } - } - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept an options parameter', function (done) { - const client = connect() - const topic = 'test' - const opts = { qos: 1 } - - client.once('connect', function () { - client.subscribe(topic, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - const expected = [{ - topic, - qos: 1 - }] - - if (version === 5) { - expected[0].nl = false - expected[0].rap = false - expected[0].rh = 0 - } - - assert.deepStrictEqual(packet.subscriptions, expected) - done() - }) - }) - }) - - it('should subscribe with the default options for an empty options parameter', function (done) { - const client = connect() - const topic = 'test' - const defaultOpts = { qos: 0 } - - client.once('connect', function () { - client.subscribe(topic, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - const result = { - topic, - qos: defaultOpts.qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - - assert.include(packet.subscriptions[0], result) - client.end(err => done(err)) - }) - }) - }) - - it('should fire a callback on suback', function (done) { - const client = connect() - const topic = 'test' - - client.once('connect', function () { - client.subscribe(topic, { qos: 2 }, function (err, granted) { - if (err) { - done(err) - } else { - assert.exists(granted, 'granted not given') - const expectedResult = { topic: 'test', qos: 2 } - if (version === 5) { - expectedResult.nl = false - expectedResult.rap = false - expectedResult.rh = 0 - expectedResult.properties = undefined - } - assert.include(granted[0], expectedResult) - client.end(err => done(err)) - } - }) - }) - }) - - it('should fire a callback with error if disconnected (options provided)', function (done) { - const client = connect() - const topic = 'test' - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, { qos: 2 }, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should fire a callback with error if disconnected (options not provided)', function (done) { - const client = connect() - const topic = 'test' - - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should subscribe with a chinese topic', function (done) { - const client = connect() - const topic = '中国' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - const result = { - topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - client.end(done) - }) - }) - }) - }) - - describe('receiving messages', function () { - it('should fire the message event', function (done) { - const client = connect() - const testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - // - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a packetreceive event', function (done) { - const client = connect() - const testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.qos, 1) - assert.strictEqual(packet.topic, testPacket.topic) - assert.strictEqual(packet.payload.toString(), testPacket.payload) - assert.strictEqual(packet.retain, true) - client.end(true, done) - } - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should support binary data', function (done) { - const client = connect({ encoding: 'binary' }) - const testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2)', function (done) { - const client = connect() - const testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2) - repeated publish', function (done) { - const client = connect() - const testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - const messageHandler = function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - - assert.strictEqual(spiedMessageHandler.callCount, 1) - client.end(true, done) - } - - const spiedMessageHandler = sinon.spy(messageHandler) - - client.subscribe(testPacket.topic) - client.on('message', spiedMessageHandler) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - // twice, should be ignored - serverClient.publish(testPacket) - }) - }) - }) - - it('should support a chinese topic', function (done) { - const client = connect({ encoding: 'binary' }) - const testPacket = { - topic: '国', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - }) - - describe('qos handling', function () { - it('should follow qos 0 semantics (trivial)', function (done) { - const client = connect() - const testTopic = 'test' - const testMessage = 'message' - - client.once('connect', function () { - client.subscribe(testTopic, { qos: 0 }, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 0, - retain: false - }) - }) - }) - }) - - it('should follow qos 1 semantics', function (done) { - const client = connect() - const testTopic = 'test' - const testMessage = 'message' - const mid = 50 - - client.once('connect', function () { - client.subscribe(testTopic, { qos: 1 }) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - messageId: mid, - qos: 1 - }) - }) - - serverClient.once('puback', function (packet) { - assert.strictEqual(packet.messageId, mid) - client.end(done) - }) - }) - }) - - it('should follow qos 2 semantics', function (done) { - const client = connect() - const testTopic = 'test' - const testMessage = 'message' - const mid = 253 - let publishReceived = 0 - let pubrecReceived = 0 - let pubrelReceived = 0 - - client.once('connect', function () { - client.subscribe(testTopic, { qos: 2 }) - }) - - client.on('packetreceive', (packet) => { - switch (packet.cmd) { - case 'connack': - case 'suback': - // expected, but not specifically part of QOS 2 semantics - break - case 'publish': - assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') - assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') - publishReceived += 1 - break - case 'pubrel': - assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') - pubrelReceived += 1 - break - default: - should.fail() - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.on('pubrec', function () { - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') - assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') - pubrecReceived += 1 - }) - - serverClient.once('pubcomp', function () { - client.removeAllListeners() - serverClient.removeAllListeners() - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') - assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') - client.end(true, done) - }) - }) - }) - - it('should should empty the incoming store after a qos 2 handshake is completed', function (done) { - const client = connect() - const testTopic = 'test' - const testMessage = 'message' - const mid = 253 - - client.once('connect', function () { - client.subscribe(testTopic, { qos: 2 }) - }) - - client.on('packetreceive', (packet) => { - if (packet.cmd === 'pubrel') { - assert.strictEqual(client.incomingStore._inflights.size, 1) - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.once('pubcomp', function () { - assert.strictEqual(client.incomingStore._inflights.size, 0) - client.removeAllListeners() - client.end(true, done) - }) - }) - }) - - function testMultiplePubrel(shouldSendPubcompFail, done) { - const client = connect() - const testTopic = 'test' - const testMessage = 'message' - const mid = 253 - let pubcompCount = 0 - let pubrelCount = 0 - let handleMessageCount = 0 - let emitMessageCount = 0 - const origSendPacket = client._sendPacket - let shouldSendFail - - client.handleMessage = function (packet, callback) { - handleMessageCount++ - callback() - } - - client.on('message', function () { - emitMessageCount++ - }) - - client._sendPacket = function (packet, sendDone) { - shouldSendFail = packet.cmd === 'pubcomp' && shouldSendPubcompFail - if (sendDone) { - sendDone(shouldSendFail ? new Error('testing pubcomp failure') : undefined) - } - - // send the mocked response - switch (packet.cmd) { - case 'subscribe': { - const suback = { cmd: 'suback', messageId: packet.messageId, granted: [2] } - handle(client, suback, function (err) { - assert.isNotOk(err) - }) - break - } - case 'pubrec': - case 'pubcomp': - { - // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp - if (packet.cmd === 'pubcomp') { - pubcompCount++ - if (pubcompCount === 2) { - // end the test once the client has gone through two rounds of replying to pubrel messages - assert.strictEqual(pubrelCount, 2) - assert.strictEqual(handleMessageCount, 1) - assert.strictEqual(emitMessageCount, 1) - client._sendPacket = origSendPacket - client.end(true, done) - break - } - } - - // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received - const pubrel = { cmd: 'pubrel', messageId: mid } - pubrelCount++ - handle(client, pubrel, function (err) { - if (shouldSendFail) { - assert.exists(err) - assert.instanceOf(err, Error) - } else { - assert.notExists(err) - } - }) - break - } - } - } - - client.once('connect', function () { - client.subscribe(testTopic, { qos: 2 }) - const publish = { cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid } - handle(client, publish, function (err) { - assert.notExists(err) - }) - }) - } - - it('handle qos 2 messages exactly once when multiple pubrel received', function (done) { - testMultiplePubrel(false, done) - }) - - it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function (done) { - testMultiplePubrel(true, done) - }) - }) - - describe('auto reconnect', function () { - it('should mark the client disconnecting if #end called', function (done) { - const client = connect() - - client.end(true, err => { - assert.isTrue(client.disconnecting) - done(err) - }) - }) - - it('should reconnect after stream disconnect', function (done) { - const client = connect() - - let tryReconnect = true - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - client.end(true, done) - } - }) - }) - - it('should emit \'reconnect\' when reconnecting', function (done) { - const client = connect() - let tryReconnect = true - let reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - client.end(true, done) - } - }) - }) - - it('should emit \'offline\' after going offline', function (done) { - const client = connect() - - let tryReconnect = true - let offlineEvent = false - - client.on('offline', function () { - offlineEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(offlineEvent) - client.end(true, done) - } - }) - }) - - it('should not reconnect if it was ended by the user', function (done) { - const client = connect() - - client.on('connect', function () { - client.end() - done() // it will raise an exception if called two times - }) - }) - - it('should setup a reconnect timer on disconnect', function (done) { - const client = connect() - - client.once('connect', function () { - assert.notExists(client.reconnectTimer) - client.stream.end() - }) - - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, done) - }) - }) - - const reconnectPeriodTests = [{ period: 200 }, { period: 2000 }, { period: 4000 }] - reconnectPeriodTests.forEach((test) => { - it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { - this.timeout(10000) - let end - const reconnectSlushTime = 200 - const client = connect({ reconnectPeriod: test.period }) - let reconnect = false - const start = Date.now() - - client.on('connect', function () { - if (!reconnect) { - client.stream.end() - reconnect = true - } else { - end = Date.now() - client.end(() => { - const reconnectPeriodDuringTest = end - start - if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { - // give the connection a 200 ms slush window - done() - } else { - done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) - } - }) - } - }) - }) - }) - - it('should always cleanup successfully on reconnection', function (done) { - const client = connect({ host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1 }) - // bind client.end so that when it is called it is automatically passed in the done callback - setTimeout(() => { - const boundEnd = client.end.bind(client, done) - boundEnd() - }, 50) - }) - - it('should resend in-flight QoS 1 publish messages from the client', function (done) { - this.timeout(4000) - const client = connect({ reconnectPeriod: 200 }) - let serverPublished = false - let clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - check() - }) - - function check() { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight publish messages if disconnecting', function (done) { - const client = connect({ reconnectPeriod: 200 }) - let serverPublished = false - let clientCalledBack = false - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - client.end(true, err => { - assert.isFalse(serverPublished) - assert.isFalse(clientCalledBack) - done(err) - }) - }) - }) - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - }) - }) - }) - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client', function (done) { - const client = connect({ reconnectPeriod: 200 }) - let serverPublished = false - let clientCalledBack = false - - server.once('client', function (serverClient) { - // ignore errors - serverClient.on('error', function () { }) - serverClient.on('publish', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('pubrel', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function () { - clientCalledBack = true - check() - }) - - function check() { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight QoS 1 removed publish messages from the client', function (done) { - const client = connect({ reconnectPeriod: 200 }) - let clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function (err) { - clientCalledBack = true - assert.exists(err, 'error should exist') - assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, (err) => { - done(err) - }) - }) - - it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { - const client = connect({ reconnectPeriod: 200 }) - let clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function (err) { - clientCalledBack = true - assert.strictEqual(err.message, 'Message removed') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, done) - }) - - it('should resubscribe when reconnecting', function (done) { - const client = connect({ reconnectPeriod: 100 }) - let tryReconnect = true - let reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - client.end(done) - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should not resubscribe when reconnecting if resubscribe is disabled', function (done) { - const client = connect({ reconnectPeriod: 100, resubscribe: false }) - let tryReconnect = true - let reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - client.end(true, done) - } - }) - }) - - it('should not resubscribe when reconnecting if suback is error', function (done) { - let tryReconnect = true - let reconnectEvent = false - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos | 0x80 - }) - }) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - const client = connect({ - port: ports.PORTAND49, - host: 'localhost', - reconnectPeriod: 100 - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - client.end(true, (err1) => { - server2.close((err2) => done(err1 || err2)) - }) - } - }) - }) - }) - - it('should preserved incomingStore after disconnecting if clean is false', function (done) { - let reconnect = false - let client = {} - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - if (reconnect) { - serverClient.pubrel({ messageId: 1 }) - } - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) - }) - serverClient.on('pubrec', function (packet) { - client.end(false, function () { - client.reconnect({ - incomingStore, - outgoingStore - }) - }) - }) - serverClient.on('pubcomp', function (packet) { - client.end(true, (err1) => { - server2.close((err2) => done(err1 || err2)) - }) - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.subscribe('test', { qos: 2 }, function () { - }) - reconnect = true - } - }) - client.on('message', function (topic, message) { - assert.strictEqual(topic, 'topic') - assert.strictEqual(message.toString(), 'payload') - }) - }) - }) - - it('should clear outgoing if close from server', function (done) { - let reconnect = false - let client = {} - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - if (reconnect) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - serverClient.destroy() - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: true, - clientId: 'cid1', - keepalive: 1, - reconnectPeriod: 0 - }) - - client.on('connect', function () { - client.subscribe('test', { qos: 2 }, function (e) { - if (!e) { - client.end() - } - }) - }) - - client.on('close', function () { - if (reconnect) { - server2.close((err) => done(err)) - } else { - assert.strictEqual(Object.keys(client.outgoing).length, 0) - reconnect = true - client.reconnect() - } - }) - }) - }) - - it('should resend in-flight QoS 1 publish messages from the client if clean is false', function (done) { - let reconnect = false - let client = {} - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - client.end(true, (err1) => { - server2.close((err2) => done(err1 || err2)) - }) - } else { - client.end(true, () => { - client.reconnect({ - incomingStore, - outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', { qos: 1 }) - } - }) - client.on('error', function () { }) - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client if clean is false', function (done) { - let reconnect = false - let client = {} - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - client.end(true, (err1) => { - server2.close((err2) => done(err1 || err2)) - }) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore, - outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', { qos: 2 }) - } - }) - client.on('error', function () { }) - }) - }) - - it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function (done) { - let reconnect = false - let client = {} - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (!reconnect) { - serverClient.pubrec({ messageId: packet.messageId }) - } - }) - serverClient.on('pubrel', function (packet) { - if (reconnect) { - serverClient.pubcomp({ messageId: packet.messageId }) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore, - outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', { qos: 2 }, function (err) { - assert(reconnect) - assert.ifError(err) - client.end(true, (err1) => { - server2.close((err2) => done(err1 || err2)) - }) - }) - } - }) - client.on('error', function () { }) - }) - }) - - it('should resend in-flight publish messages by published order', function (done) { - let publishCount = 0 - let reconnect = false - let disconnectOnce = true - let client = {} - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () { }) - - serverClient.on('connect', function (packet) { - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({ messageId: packet.messageId }) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - client.end(true, (err1) => { - server2.close((err2) => done(err1 || err2)) - }) - break - } - } else { - if (disconnectOnce) { - client.end(true, function () { - reconnect = true - client.reconnect({ - incomingStore, - outgoingStore - }) - }) - disconnectOnce = false - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore, - outgoingStore - }) - - client.nextId = 65535 - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', { qos: 1 }) - client.publish('topic', 'payload2', { qos: 1 }) - client.publish('topic', 'payload3', { qos: 1 }) - } - }) - client.on('error', function () { }) - }) - }) - - it('should be able to pub/sub if reconnect() is called at close handler', function (done) { - const client = connect({ reconnectPeriod: 0 }) - let tryReconnect = true - let reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - client.reconnect() - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - it('should be able to pub/sub if reconnect() is called at out of close handler', function (done) { - const client = connect({ reconnectPeriod: 0 }) - let tryReconnect = true - let reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - setTimeout(function () { - client.reconnect() - }, 100) - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - context('with alternate server client', function () { - let cachedClientListeners - const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - - beforeEach(function () { - cachedClientListeners = server.listeners('client') - server.removeAllListeners('client') - }) - - afterEach(function () { - server.removeAllListeners('client') - cachedClientListeners.forEach(function (listener) { - server.on('client', listener) - }) - }) - - it('should resubscribe even if disconnect is before suback', function (done) { - const client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - let subscribeCount = 0 - let connectCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - connectCount++ - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, confirm that the only two - // subscribes have taken place, then cleanup and exit - if (connectCount >= 2) { - assert.strictEqual(subscribeCount, 2) - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - - it('should resubscribe exactly once', function (done) { - const client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - let subscribeCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, only two subs - // subscribes have taken place, then cleanup and exit - if (subscribeCount === 2) { - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - }) - }) - - describe('message id to subscription topic mapping', () => { - it('should not create a mapping if resubscribe is disabled', function (done) { - const client = connect({ resubscribe: false }) - client.subscribe('test1') - client.subscribe('test2') - assert.strictEqual(Object.keys(client.messageIdToTopic).length, 0) - client.end(true, done) - }) - - it('should create a mapping for each subscribe call', function (done) { - const client = connect() - client.subscribe('test1') - assert.strictEqual(Object.keys(client.messageIdToTopic).length, 1) - client.subscribe('test2') - assert.strictEqual(Object.keys(client.messageIdToTopic).length, 2) - - client.subscribe(['test3', 'test4']) - assert.strictEqual(Object.keys(client.messageIdToTopic).length, 3) - client.subscribe(['test5', 'test6']) - assert.strictEqual(Object.keys(client.messageIdToTopic).length, 4) - - client.end(true, done) - }) - - it('should remove the mapping after suback', function (done) { - const client = connect() - client.once('connect', function () { - client.subscribe('test1', { qos: 2 }, function () { - assert.strictEqual(Object.keys(client.messageIdToTopic).length, 0) - - client.subscribe(['test2', 'test3'], { qos: 2 }, function () { - assert.strictEqual(Object.keys(client.messageIdToTopic).length, 0) - client.end(done) - }) - }) - }) - }) - }) + * These tests try to be consistent with names for servers (brokers) and clients, + * but it can be confusing. To make it easier, here is a handy translation + * chart: + * + * name | meaning + * ---------------|-------- + * client | The MQTT.js client object being tested. A new instance is created for each test (by calling the `connect` function.) + * server | A mock broker that you can control. The same server instance is used for all tests, so only use this if you plan to clean up when you're done. + * serverBuilder | A factory that can make mock test servers (MQTT brokers). Useful if you need to do things that you can't (or don't want to) clean up after your test is done. + * server2 | The name used for mock brokers that are created for an individual test and then destroyed. + * serverClient | An socket on the mock broker. This gets created when your client connects and gets collected when you're done with it. + * + * Also worth noting: + * + * `serverClient.disconnect()` does not disconnect that socket. Instead, it sends an MQTT disconnect packet. + * If you want to disconnect the socket from the broker side, you probably want to use `serverClient.destroy()` + * or `serverClient.stream.destroy()`. + * + */ + +module.exports = (server, config) => { + const version = config.protocolVersion || 4 + + function connect(opts) { + opts = { ...config, ...opts } + return mqtt.connect(opts) + } + + describe('closing', () => { + it('should emit close if stream closes', function _test(done) { + const client = connect() + + client.once('connect', () => { + client.stream.end() + }) + client.once('close', () => { + client.end((err) => done(err)) + }) + }) + + it('should mark the client as disconnected', function _test(done) { + const client = connect() + + client.once('close', () => { + client.end((err) => { + if (!client.connected) { + done(err) + } else { + done(new Error('Not marked as disconnected')) + } + }) + assert.isFalse(client.connected) + }) + client.once('connect', () => { + client.stream.end() + }) + }) + + it('should stop ping timer if stream closes', function _test(done) { + const client = connect() + + client.once('close', () => { + assert.notExists(client.pingTimer) + client.end(true, (err) => done(err)) + }) + + client.once('connect', () => { + assert.exists(client.pingTimer) + client.stream.end() + }) + }) + + it('should emit close after end called', function _test(done) { + const client = connect() + + client.once('close', () => { + done() + }) + + client.once('connect', () => { + client.end() + }) + }) + + it('should emit end after end called and client must be disconnected', function _test(done) { + const client = connect() + + client.once('end', () => { + if (client.disconnected) { + return done() + } + done(new Error('client must be disconnected')) + }) + + client.once('connect', () => { + client.end() + }) + }) + + it('should pass store close error to end callback but not to end listeners (incomingStore)', function _test(done) { + const store = new Store() + const client = connect({ incomingStore: store }) + + store.close = (cb) => { + cb(new Error('test')) + } + client.once('end', (...args) => { + if (args.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', () => { + client.end((testError) => { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should pass store close error to end callback but not to end listeners (outgoingStore)', function _test(done) { + const store = new Store() + const client = connect({ outgoingStore: store }) + + store.close = (cb) => { + cb(new Error('test')) + } + client.once('end', (...args) => { + if (args.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', () => { + client.end((testError) => { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should return `this` if end called twice', function _test(done) { + const client = connect() + + client.once('connect', () => { + client.end() + const value = client.end() + if (value === client) { + done() + } else { + done(new Error('Not returning client.')) + } + }) + }) + + it('should emit end only on first client end', function _test(done) { + const client = connect() + + client.once('end', () => { + const timeout = setTimeout(() => done(), 200) + client.once('end', () => { + clearTimeout(timeout) + done(new Error('end was emitted twice')) + }) + client.end() + }) + + client.once('connect', () => { + client.end() + }) + }) + + it('should stop ping timer after end called', function _test(done) { + const client = connect() + + client.once('connect', () => { + assert.exists(client.pingTimer) + client.end((err) => { + assert.notExists(client.pingTimer) + done(err) + }) + }) + }) + + it('should be able to end even on a failed connection', function _test(done) { + const client = connect({ host: 'this_hostname_should_not_exist' }) + + const timeout = setTimeout(() => { + done(new Error('Failed to end a disconnected client')) + }, 500) + + setTimeout(() => { + client.end((err) => { + clearTimeout(timeout) + done(err) + }) + }, 200) + }) + + it('should emit end even on a failed connection', function _test(done) { + const client = connect({ host: 'this_hostname_should_not_exist' }) + + const timeout = setTimeout(() => { + done(new Error('Disconnected client has failed to emit end')) + }, 500) + + client.once('end', () => { + clearTimeout(timeout) + done() + }) + + // after 200ms manually invoke client.end + setTimeout(() => { + client.end.call(client) + }, 200) + }) + + it.skip('should emit end only once for a reconnecting client', function _test(done) { + // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. + // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code + // there will be gists showing the difference between a successful test here and a failed test. For now we + // will add the retries syntax because of the flakiness. + const client = connect({ + host: 'this_hostname_should_not_exist', + connectTimeout: 10, + reconnectPeriod: 20, + }) + setTimeout(() => done(), 1000) + const endCallback = () => { + assert.strictEqual( + spy.callCount, + 1, + 'end was emitted more than once for reconnecting client', + ) + } + + const spy = sinon.spy(endCallback) + client.on('end', spy) + setTimeout(() => { + client.end.call(client) + }, 300) + }) + }) + + describe('connecting', () => { + it('should connect to the broker', function _test(done) { + const client = connect() + client.on('error', done) + + server.once('client', () => { + client.end((err) => done(err)) + }) + }) + + it('should send a default client id', function _test(done) { + const client = connect() + client.on('error', done) + + server.once('client', (serverClient) => { + serverClient.once('connect', (packet) => { + assert.include(packet.clientId, 'mqttjs') + client.end((err) => done(err)) + }) + }) + }) + + it('should send be clean by default', function _test(done) { + const client = connect() + client.on('error', done) + + server.once('client', (serverClient) => { + serverClient.once('connect', (packet) => { + assert.strictEqual(packet.clean, true) + done() + }) + }) + }) + + it('should connect with the given client id', function _test(done) { + const client = connect({ clientId: 'testclient' }) + client.on('error', (err) => { + throw err + }) + + server.once('client', (serverClient) => { + serverClient.once('connect', (packet) => { + assert.include(packet.clientId, 'testclient') + client.end((err) => done(err)) + }) + }) + }) + + it('should connect with the client id and unclean state', function _test(done) { + const client = connect({ clientId: 'testclient', clean: false }) + client.on('error', (err) => { + throw err + }) + + server.once('client', (serverClient) => { + serverClient.once('connect', (packet) => { + assert.include(packet.clientId, 'testclient') + assert.isFalse(packet.clean) + client.end(false, (err) => done(err)) + }) + }) + }) + + it('should require a clientId with clean=false', function _test(done) { + try { + const client = connect({ clean: false }) + client.on('error', (err) => { + done(err) + }) + } catch (err) { + assert.strictEqual( + err.message, + 'Missing clientId for unclean clients', + ) + done() + } + }) + + it('should default to localhost', function _test(done) { + const client = connect({ clientId: 'testclient' }) + client.on('error', (err) => { + throw err + }) + + server.once('client', (serverClient) => { + serverClient.once('connect', (packet) => { + assert.include(packet.clientId, 'testclient') + done() + }) + }) + }) + + it('should emit connect', function _test(done) { + const client = connect() + client.once('connect', () => { + client.end(true, (err) => done(err)) + }) + client.once('error', done) + }) + + it('should provide connack packet with connect event', function _test(done) { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + server.once('client', (serverClient) => { + connack.sessionPresent = true + serverClient.connack(connack) + server.once('client', (serverClient2) => { + connack.sessionPresent = false + serverClient2.connack(connack) + }) + }) + + const client = connect() + client.once('connect', (packet) => { + assert.strictEqual(packet.sessionPresent, true) + client.once('connect', (packet2) => { + assert.strictEqual(packet2.sessionPresent, false) + client.end((err) => done(err)) + }) + }) + }) + + it('should mark the client as connected', function _test(done) { + const client = connect() + client.once('connect', () => { + assert.isTrue(client.connected) + client.end((err) => done(err)) + }) + }) + + it('should emit error on invalid clientId', function _test(done) { + const client = connect({ clientId: 'invalid' }) + client.once('connect', () => { + done(new Error('Should not emit connect')) + }) + client.once('error', (error) => { + const value = version === 5 ? 128 : 2 + assert.strictEqual(error.code, value) // code for clientID identifer rejected + client.end((err) => done(err)) + }) + }) + + it('should emit error event if the socket refuses the connection', function _test(done) { + // fake a port + const client = connect({ port: 4557 }) + + client.on('error', (e) => { + assert.equal(e.code, 'ECONNREFUSED') + client.end((err) => done(err)) + }) + }) + + it('should have different client ids', function _test(done) { + // bug identified in this test: the client.end callback is invoked twice, once when the `end` + // method completes closing the stores and invokes the callback, and another time when the + // stream is closed. When the stream is closed, for some reason the closeStores method is called + // a second time. + const client1 = connect() + const client2 = connect() + + assert.notStrictEqual( + client1.options.clientId, + client2.options.clientId, + ) + client1.end(true, () => { + client2.end(true, () => { + done() + }) + }) + }) + }) + + describe('handling offline states', () => { + it('should emit offline event once when the client transitions from connected states to disconnected ones', function _test(done) { + const client = connect({ reconnectPeriod: 20 }) + + client.on('connect', () => { + client.stream.end() + }) + + client.on('offline', () => { + client.end(true, done) + }) + }) + + it('should emit offline event once when the client (at first) can NOT connect to servers', function _test(done) { + // fake a port + const client = connect({ reconnectPeriod: 20, port: 4557 }) + + client.on('error', () => {}) + + client.on('offline', () => { + client.end(true, done) + }) + }) + }) + + describe('topic validations when subscribing', () => { + it('should be ok for well-formated topics', function _test(done) { + const client = connect() + client.subscribe( + [ + '+', + '+/event', + 'event/+', + '#', + 'event/#', + 'system/event/+', + 'system/+/event', + 'system/registry/event/#', + 'system/+/event/#', + 'system/registry/event/new_device', + 'system/+/+/new_device', + ], + (err) => { + client.end(() => { + if (err) { + return done(new Error(err)) + } + done() + }) + }, + ) + }) + + it('should return an error (via callbacks) for topic #/event', function _test(done) { + const client = connect() + client.subscribe(['#/event', 'event#', 'event+'], (err) => { + client.end(false, () => { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an empty array for duplicate subs', function _test(done) { + const client = connect() + client.subscribe('event', (err, granted1) => { + if (err) { + return done(err) + } + client.subscribe('event', (err2, granted2) => { + if (err2) { + return done(err2) + } + assert.isArray(granted2) + assert.isEmpty(granted2) + done() + }) + }) + }) + + it('should return an error (via callbacks) for topic #/event', function _test(done) { + const client = connect() + client.subscribe('#/event', (err) => { + client.end(() => { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic event#', function _test(done) { + const client = connect() + client.subscribe('event#', (err) => { + client.end(() => { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic system/#/event', function _test(done) { + const client = connect() + client.subscribe('system/#/event', (err) => { + client.end(() => { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for empty topic list', function _test(done) { + const client = connect() + client.subscribe([], (subErr) => { + client.end((endErr) => { + if (subErr) { + return done(endErr) + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic system/+/#/event', function _test(done) { + const client = connect() + client.subscribe('system/+/#/event', (subErr) => { + client.end(true, (endErr) => { + if (subErr) { + return done(endErr) + } + done(new Error('Validations do NOT work')) + }) + }) + }) + }) + + describe('offline messages', () => { + it('should queue message until connected', function _test(done) { + const client = connect() + + client.publish('test', 'test') + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 3) + + client.once('connect', () => { + assert.strictEqual(client.queue.length, 0) + setTimeout(() => { + client.end(true, done) + }, 10) + }) + }) + + it('should not queue qos 0 messages if queueQoSZero is false', function _test(done) { + const client = connect({ queueQoSZero: false }) + + client.publish('test', 'test', { qos: 0 }) + assert.strictEqual(client.queue.length, 0) + client.on('connect', () => { + setTimeout(() => { + client.end(true, done) + }, 10) + }) + }) + + it('should queue qos != 0 messages', function _test(done) { + const client = connect({ queueQoSZero: false }) + + client.publish('test', 'test', { qos: 1 }) + client.publish('test', 'test', { qos: 2 }) + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 2) + client.on('connect', () => { + setTimeout(() => { + client.end(true, done) + }, 10) + }) + }) + + it('should not interrupt messages', function _test(done) { + let client = null + let publishCount = 0 + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', () => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + if (packet.qos !== 0) { + serverClient.puback({ messageId: packet.messageId }) + } + switch (publishCount++) { + case 0: + assert.strictEqual( + packet.payload.toString(), + 'payload1', + ) + break + case 1: + assert.strictEqual( + packet.payload.toString(), + 'payload2', + ) + break + case 2: + assert.strictEqual( + packet.payload.toString(), + 'payload3', + ) + break + case 3: + assert.strictEqual( + packet.payload.toString(), + 'payload4', + ) + client.end((err1) => { + server2.close((err2) => done(err1 || err2)) + }) + } + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + queueQoSZero: true, + }) + client.on('packetreceive', (packet) => { + if (packet.cmd === 'connack') { + setImmediate(() => { + client.publish('test', 'payload3', { qos: 1 }) + client.publish('test', 'payload4', { qos: 0 }) + }) + } + }) + client.publish('test', 'payload1', { qos: 2 }) + client.publish('test', 'payload2', { qos: 2 }) + }) + }) + + it('should not overtake the messages stored in the level-db-store', function _test(done) { + const storePath = fs.mkdtempSync('test-store_') + const store = levelStore(storePath) + let client = null + const incomingStore = store.incoming + const outgoingStore = store.outgoing + let publishCount = 0 + + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', () => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + if (packet.qos !== 0) { + serverClient.puback({ messageId: packet.messageId }) + } + + switch (publishCount++) { + case 0: + assert.strictEqual( + packet.payload.toString(), + 'payload1', + ) + break + case 1: + assert.strictEqual( + packet.payload.toString(), + 'payload2', + ) + break + case 2: + assert.strictEqual( + packet.payload.toString(), + 'payload3', + ) + + server2.close((err) => { + fs.rmSync(storePath, { recursive: true }) + done(err) + }) + break + } + }) + }) + + const clientOptions = { + port: ports.PORTAND72, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + queueQoSZero: true, + } + + server2.listen(ports.PORTAND72, () => { + client = connect(clientOptions) + + client.once('close', () => { + client.once('connect', () => { + client.publish('test', 'payload2', { qos: 1 }) + client.publish('test', 'payload3', { qos: 1 }, () => { + client.end(false) + }) + }) + // reconecting + client.reconnect(clientOptions) + }) + + // publish and close + client.once('connect', () => { + client.publish('test', 'payload1', { + qos: 1, + cbStorePut() { + client.end(true) + }, + }) + }) + }) + }) + + it('should call cb if an outgoing QoS 0 message is not sent', function _test(done) { + const client = connect({ queueQoSZero: false }) + let called = false + + client.publish('test', 'test', { qos: 0 }, () => { + called = true + }) + + client.on('connect', () => { + assert.isTrue(called) + setTimeout(() => { + client.end(true, done) + }, 10) + }) + }) + + it('should delay ending up until all inflight messages are delivered', function _test(done) { + const client = connect() + let subscribeCalled = false + + client.on('connect', () => { + client.subscribe('test', () => { + subscribeCalled = true + }) + client.publish('test', 'test', () => { + client.end(false, () => { + assert.strictEqual(subscribeCalled, true) + done() + }) + }) + }) + }) + + it('wait QoS 1 publish messages', function _test(done) { + const client = connect() + let messageReceived = false + + client.on('connect', () => { + client.subscribe('test') + client.publish('test', 'test', { qos: 1 }, () => { + client.end(false, () => { + assert.strictEqual(messageReceived, true) + done() + }) + }) + client.on('message', () => { + messageReceived = true + }) + }) + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + serverClient.on('publish', (packet) => { + serverClient.publish(packet) + }) + }) + }) + }) + + it('does not wait acks when force-closing', function _test(done) { + // non-running broker + const client = connect('mqtt://localhost:8993') + client.publish('test', 'test', { qos: 1 }) + client.end(true, done) + }) + + it('should call cb if store.put fails', function _test(done) { + const store = new Store() + store.put = (packet, cb) => { + process.nextTick(cb, new Error('oops there is an error')) + } + const client = connect({ + incomingStore: store, + outgoingStore: store, + }) + client.publish('test', 'test', { qos: 2 }, (err) => { + if (err) { + client.end(true, done) + } + }) + }) + }) + + describe('publishing', () => { + it('should publish a message (offline)', function _test(done) { + const client = connect() + const payload = 'test' + const topic = 'test' + // don't wait on connect to send publish + client.publish(topic, payload) + + server.on('client', onClient) + + function onClient(serverClient) { + serverClient.once('connect', () => { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', (packet) => { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (online)', function _test(done) { + const client = connect() + const payload = 'test' + const topic = 'test' + // block on connect before sending publish + client.on('connect', () => { + client.publish(topic, payload) + }) + + server.on('client', onClient) + + function onClient(serverClient) { + serverClient.once('connect', () => { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', (packet) => { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (retain, offline)', function _test(done) { + const client = connect({ queueQoSZero: true }) + const payload = 'test' + const topic = 'test' + let called = false + + client.publish(topic, payload, { retain: true }, () => { + called = true + }) + + server.once('client', (serverClient) => { + serverClient.once('publish', (packet) => { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, true) + assert.strictEqual(called, true) + client.end(true, done) + }) + }) + }) + + it('should emit a packetsend event', function _test(done) { + const client = connect() + const payload = 'test_payload' + const topic = 'testTopic' + + client.on('packetsend', (packet) => { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + } else { + done(new Error('packet.cmd was not publish!')) + } + }) + + client.publish(topic, payload) + }) + + it('should accept options', function _test(done) { + const client = connect() + const payload = 'test' + const topic = 'test' + const opts = { + retain: true, + qos: 1, + } + let received = false + + client.once('connect', () => { + client.publish(topic, payload, opts, (err) => { + assert(received) + client.end(() => { + done(err) + }) + }) + }) + + server.once('client', (serverClient) => { + serverClient.once('publish', (packet) => { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual( + packet.retain, + opts.retain, + 'incorrect ret', + ) + assert.strictEqual(packet.dup, false, 'incorrect dup') + received = true + }) + }) + }) + + it('should publish with the default options for an empty parameter', function _test(done) { + const client = connect() + const payload = 'test' + const topic = 'test' + const defaultOpts = { qos: 0, retain: false, dup: false } + + client.once('connect', () => { + client.publish(topic, payload, {}) + }) + + server.once('client', (serverClient) => { + serverClient.once('publish', (packet) => { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual( + packet.qos, + defaultOpts.qos, + 'incorrect qos', + ) + assert.strictEqual( + packet.retain, + defaultOpts.retain, + 'incorrect ret', + ) + assert.strictEqual( + packet.dup, + defaultOpts.dup, + 'incorrect dup', + ) + client.end(true, done) + }) + }) + }) + + it('should mark a message as duplicate when "dup" option is set', function _test(done) { + const client = connect() + const payload = 'duplicated-test' + const topic = 'test' + const opts = { + retain: true, + qos: 1, + dup: true, + } + let received = false + + client.once('connect', () => { + client.publish(topic, payload, opts, (err) => { + assert(received) + client.end(() => { + done(err) + }) + }) + }) + + server.once('client', (serverClient) => { + serverClient.once('publish', (packet) => { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual( + packet.retain, + opts.retain, + 'incorrect ret', + ) + assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') + received = true + }) + }) + }) + + it('should fire a callback (qos 0)', function _test(done) { + const client = connect() + + client.once('connect', () => { + client.publish('a', 'b', () => { + client.end((err) => done(err)) + }) + }) + }) + + it('should fire a callback (qos 1)', function _test(done) { + const client = connect() + const opts = { qos: 1 } + + client.once('connect', () => { + client.publish('a', 'b', opts, () => { + client.end((err) => done(err)) + }) + }) + }) + + it('should fire a callback (qos 1) on error', function _test(done) { + // 145 = Packet Identifier in use + const pubackReasonCode = 145 + const pubOpts = { qos: 1 } + let client = null + + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', () => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + if (packet.qos === 1) { + if (version === 5) { + serverClient.puback({ + messageId: packet.messageId, + reasonCode: pubackReasonCode, + }) + } else { + serverClient.puback({ messageId: packet.messageId }) + } + } + }) + }) + + server2.listen(ports.PORTAND72, () => { + client = connect({ + port: ports.PORTAND72, + host: 'localhost', + clean: true, + clientId: 'cid1', + reconnectPeriod: 0, + }) + + client.once('connect', () => { + client.publish('a', 'b', pubOpts, (err) => { + if (version === 5) { + assert.strictEqual(err.code, pubackReasonCode) + } else { + assert.ifError(err) + } + setImmediate(() => { + client.end(() => { + server2.close(done()) + }) + }) + }) + }) + }) + }) + + it('should fire a callback (qos 2)', function _test(done) { + const client = connect() + const opts = { qos: 2 } + + client.once('connect', () => { + client.publish('a', 'b', opts, () => { + client.end((err) => done(err)) + }) + }) + }) + + it('should fire a callback (qos 2) on error', function _test(done) { + // 145 = Packet Identifier in use + const pubrecReasonCode = 145 + const pubOpts = { qos: 2 } + let client = null + + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', () => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + if (packet.qos === 2) { + if (version === 5) { + serverClient.pubrec({ + messageId: packet.messageId, + reasonCode: pubrecReasonCode, + }) + } else { + serverClient.pubrec({ messageId: packet.messageId }) + } + } + }) + serverClient.on('pubrel', (packet) => { + if (!serverClient.writable) return false + serverClient.pubcomp(packet) + }) + }) + + server2.listen(ports.PORTAND103, () => { + client = connect({ + port: ports.PORTAND103, + host: 'localhost', + clean: true, + clientId: 'cid1', + reconnectPeriod: 0, + }) + + client.once('connect', () => { + client.publish('a', 'b', pubOpts, (err) => { + if (version === 5) { + assert.strictEqual(err.code, pubrecReasonCode) + } else { + assert.ifError(err) + } + setImmediate(() => { + client.end(true, () => { + server2.close(done()) + }) + }) + }) + }) + }) + }) + + it('should support UTF-8 characters in topic', function _test(done) { + const client = connect() + + client.once('connect', () => { + client.publish('中国', 'hello', () => { + client.end((err) => done(err)) + }) + }) + }) + + it('should support UTF-8 characters in payload', function _test(done) { + const client = connect() + + client.once('connect', () => { + client.publish('hello', '中国', () => { + client.end((err) => done(err)) + }) + }) + }) + + it('should publish 10 QoS 2 and receive them', function _test(done) { + const client = connect() + let countSent = 0 + let countReceived = 0 + + function publishNext() { + client.publish('test', 'test', { qos: 2 }, (err) => { + assert.ifError(err) + countSent++ + }) + } + + client.on('connect', () => { + client.subscribe('test', (err) => { + assert.ifError(err) + publishNext() + }) + }) + + client.on('message', () => { + countReceived++ + if (countSent >= 10 && countReceived >= 10) { + client.end(done) + } else { + publishNext() + } + }) + + server.once('client', (serverClient) => { + serverClient.on('offline', () => { + client.end() + done('error went offline... didnt see this happen') + }) + + serverClient.on('subscribe', () => { + serverClient.on('publish', (packet) => { + serverClient.publish(packet) + }) + }) + }) + }) + + function testQosHandleMessage(qos, done) { + const client = connect() + + let messageEventCount = 0 + let handleMessageCount = 0 + + client.handleMessage = (packet, callback) => { + setTimeout(() => { + handleMessageCount++ + // next message event should not emit until handleMessage completes + assert.strictEqual(handleMessageCount, messageEventCount) + if (handleMessageCount === 10) { + setTimeout(() => { + client.end(true, done) + }) + } + callback() + }, 100) + } + + client.on('message', (topic, message, packet) => { + messageEventCount++ + }) + + client.on('connect', () => { + client.subscribe('test') + }) + + server.once('client', (serverClient) => { + serverClient.on('offline', () => { + client.end(true, () => { + done('error went offline... didnt see this happen') + }) + }) + + serverClient.on('subscribe', () => { + for (let i = 0; i < 10; i++) { + serverClient.publish({ + messageId: i, + topic: 'test', + payload: `test${i}`, + qos, + }) + } + }) + }) + } + + const qosTests = [0, 1, 2] + qosTests.forEach((QoS) => { + it(`should publish 10 QoS ${QoS}and receive them only when \`handleMessage\` finishes`, function _test(done) { + testQosHandleMessage(QoS, done) + }) + }) + + it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function _test(done) { + const client = connect() + + client.handleMessage = (packet, callback) => { + callback(new Error('Error thrown by the application')) + } + + client._sendPacket = sinon.spy() + + handlePublish( + client, + { + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1, + }, + (err) => { + assert.exists(err) + }, + ) + + assert.strictEqual(client._sendPacket.callCount, 0) + client.end() + client.on('connect', () => { + done() + }) + }) + + it( + 'should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePublish` method', + function _test(done) { + const client = connect() + + client.handleMessage = (packet, callback) => { + callback(new Error('Error thrown by the application')) + } + + try { + handlePublish(client, { + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1, + }) + client.end(true, done) + } catch (err) { + client.end(true, () => { + done(err) + }) + } + }, + ) + + it('should handle error with async incoming store in QoS 1 `handlePublish` method', function _test(done) { + class AsyncStore { + put(packet, cb) { + process.nextTick(() => { + cb(null, 'Error') + }) + } + + close(cb) { + cb() + } + } + + const store = new AsyncStore() + const client = connect({ incomingStore: store }) + + handlePublish( + client, + { + messageId: 1, + topic: 'test', + payload: 'test', + qos: 1, + }, + () => { + client.end((err) => done(err)) + }, + ) + }) + + it('should handle error with async incoming store in QoS 2 `handlePublish` method', function _test(done) { + class AsyncStore { + put(packet, cb) { + process.nextTick(() => { + cb(null, 'Error') + }) + } + + del(packet, cb) { + process.nextTick(() => { + cb(new Error('Error')) + }) + } + + get(packet, cb) { + process.nextTick(() => { + cb(null, { cmd: 'publish' }) + }) + } + + close(cb) { + cb() + } + } + + const store = new AsyncStore() + const client = connect({ incomingStore: store }) + + handlePublish( + client, + { + messageId: 1, + topic: 'test', + payload: 'test', + qos: 2, + }, + () => { + client.end((err) => done(err)) + }, + ) + }) + + it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function _test(done) { + class AsyncStore { + put(packet, cb) { + process.nextTick(() => { + cb(null, 'Error') + }) + } + + del(packet, cb) { + process.nextTick(() => { + cb(new Error('Error')) + }) + } + + get(packet, cb) { + process.nextTick(() => { + cb(null, { cmd: 'publish' }) + }) + } + + close(cb) { + cb() + } + } + + const store = new AsyncStore() + const client = connect({ incomingStore: store }) + + handlePubrel( + client, + { + messageId: 1, + qos: 2, + }, + () => { + client.end(true, (err) => done(err)) + }, + ) + }) + + it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function _test(done) { + let delComplete = false + class AsyncStore { + put(packet, cb) { + process.nextTick(() => { + cb(null, 'Error') + }) + } + + del(packet, cb) { + process.nextTick(() => { + delComplete = true + cb(null) + }) + } + + get(packet, cb) { + process.nextTick(() => { + cb(null, { cmd: 'publish' }) + }) + } + + close(cb) { + cb() + } + } + + const store = new AsyncStore() + const client = connect({ incomingStore: store }) + + handlePubrel( + client, + { + messageId: 1, + qos: 2, + }, + () => { + assert.isTrue(delComplete) + client.end(true, done) + }, + ) + }) + + it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function _test(done) { + const store = new Store() + const client = connect({ incomingStore: store }) + + const messageId = Math.floor(65535 * Math.random()) + const topic = 'testTopic' + const payload = 'testPayload' + const qos = 2 + + client.handleMessage = (packet, callback) => { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', () => { + client.subscribe(topic, { qos: 2 }) + + store.put( + { + messageId, + topic, + payload, + qos, + cmd: 'publish', + }, + () => { + // cleans up the client + client._sendPacket = sinon.spy() + handlePubrel( + client, + { cmd: 'pubrel', messageId }, + (err) => { + assert.exists(err) + assert.strictEqual( + client._sendPacket.callCount, + 0, + ) + client.end(true, done) + }, + ) + }, + ) + }) + }) + + it( + 'should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePubrel` method', + function _test(done) { + const store = new Store() + const client = connect({ incomingStore: store }) + + const messageId = Math.floor(65535 * Math.random()) + const topic = 'test' + const payload = 'test' + const qos = 2 + + client.handleMessage = (packet, callback) => { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', () => { + client.subscribe(topic, { qos: 2 }) + + store.put( + { + messageId, + topic, + payload, + qos, + cmd: 'publish', + }, + () => { + try { + handlePubrel(client, { + cmd: 'pubrel', + messageId, + }) + client.end(true, done) + } catch (err) { + client.end(true, () => { + done(err) + }) + } + }, + ) + }) + }, + ) + + it('should keep message order', function _test(done) { + let publishCount = 0 + let reconnect = false + let client = {} + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const server2 = serverBuilder(config.protocol, (serverClient) => { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', () => {}) + + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + serverClient.puback({ messageId: packet.messageId }) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual( + packet.payload.toString(), + 'payload1', + ) + break + case 1: + assert.strictEqual( + packet.payload.toString(), + 'payload2', + ) + break + case 2: + assert.strictEqual( + packet.payload.toString(), + 'payload3', + ) + client.end((err1) => { + server2.close((err2) => done(err1 || err2)) + }) + break + } + } + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + }) + + client.on('connect', () => { + if (!reconnect) { + client.publish('topic', 'payload1', { qos: 1 }) + client.publish('topic', 'payload2', { qos: 1 }) + client.end(true) + } else { + client.publish('topic', 'payload3', { qos: 1 }) + } + }) + client.on('close', () => { + if (!reconnect) { + client.reconnect({ + clean: false, + incomingStore, + outgoingStore, + }) + reconnect = true + } + }) + }) + }) + + function testCallbackStorePutByQoS(qos, clean, expected, done) { + const client = connect({ + clean, + clientId: 'testId', + }) + + const callbacks = [] + + function cbStorePut() { + callbacks.push('storeput') + } + + client.on('connect', () => { + client.publish('test', 'test', { qos, cbStorePut }, (err) => { + if (err) done(err) + callbacks.push('publish') + assert.deepEqual(callbacks, expected) + client.end(true, done) + }) + }) + } + + const callbackStorePutByQoSParameters = [ + { args: [0, true], expected: ['publish'] }, + { args: [0, false], expected: ['publish'] }, + { args: [1, true], expected: ['storeput', 'publish'] }, + { args: [1, false], expected: ['storeput', 'publish'] }, + { args: [2, true], expected: ['storeput', 'publish'] }, + { args: [2, false], expected: ['storeput', 'publish'] }, + ] + + callbackStorePutByQoSParameters.forEach((test) => { + if (test.args[0] === 0) { + // QoS 0 + it(`should not call cbStorePut when publishing message with QoS \`${test.args[0]}\` and clean \`${test.args[1]}\``, function _test(done) { + testCallbackStorePutByQoS( + test.args[0], + test.args[1], + test.expected, + done, + ) + }) + } else { + // QoS 1 and 2 + it(`should call cbStorePut before publish completes when publishing message with QoS \`${test.args[0]}\` and clean \`${test.args[1]}\``, function _test(done) { + testCallbackStorePutByQoS( + test.args[0], + test.args[1], + test.expected, + done, + ) + }) + } + }) + }) + + describe('unsubscribing', () => { + it('should send an unsubscribe packet (offline)', function _test(done) { + const client = connect() + let received = false + + client.unsubscribe('test', (err) => { + assert.ifError(err) + assert(received) + client.end(done) + }) + + server.once('client', (serverClient) => { + serverClient.once('unsubscribe', (packet) => { + assert.include(packet.unsubscriptions, 'test') + received = true + }) + }) + }) + + it('should send an unsubscribe packet', function _test(done) { + const client = connect() + const topic = 'topic' + let received = false + + client.once('connect', () => { + client.unsubscribe(topic, (err) => { + assert.ifError(err) + assert(received) + client.end(done) + }) + }) + + server.once('client', (serverClient) => { + serverClient.once('unsubscribe', (packet) => { + assert.include(packet.unsubscriptions, topic) + received = true + }) + }) + }) + + it('should emit a packetsend event', function _test(done) { + const client = connect() + const testTopic = 'testTopic' + + client.once('connect', () => { + client.subscribe(testTopic) + }) + + client.on('packetsend', (packet) => { + if (packet.cmd === 'subscribe') { + client.end(true, done) + } + }) + }) + + it('should emit a packetreceive event', function _test(done) { + const client = connect() + const testTopic = 'testTopic' + + client.once('connect', () => { + client.subscribe(testTopic) + }) + + client.on('packetreceive', (packet) => { + if (packet.cmd === 'suback') { + client.end(true, done) + } + }) + }) + + it('should accept an array of unsubs', function _test(done) { + const client = connect() + const topics = ['topic1', 'topic2'] + let received = false + + client.once('connect', () => { + client.unsubscribe(topics, (err) => { + assert.ifError(err) + assert(received) + client.end(done) + }) + }) + + server.once('client', (serverClient) => { + serverClient.once('unsubscribe', (packet) => { + assert.deepStrictEqual(packet.unsubscriptions, topics) + received = true + }) + }) + }) + + it('should fire a callback on unsuback', function _test(done) { + const client = connect() + const topic = 'topic' + + client.once('connect', () => { + client.unsubscribe(topic, () => { + client.end(true, done) + }) + }) + + server.once('client', (serverClient) => { + serverClient.once('unsubscribe', (packet) => { + serverClient.unsuback(packet) + }) + }) + }) + + it('should unsubscribe from a chinese topic', function _test(done) { + const client = connect() + const topic = '中国' + + client.once('connect', () => { + client.unsubscribe(topic, () => { + client.end((err) => { + done(err) + }) + }) + }) + + server.once('client', (serverClient) => { + serverClient.once('unsubscribe', (packet) => { + assert.include(packet.unsubscriptions, topic) + }) + }) + }) + }) + + describe('keepalive', () => { + let clock + + // eslint-disable-next-line + beforeEach(() => { + clock = sinon.useFakeTimers() + }) + + afterEach(() => { + clock.restore() + }) + + it('should checkPing at keepalive interval', function _test(done) { + const interval = 3 + const client = connect({ keepalive: interval }) + + client._checkPing = sinon.spy() + + client.once('connect', () => { + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 1) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 2) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 3) + + client.end(true, done) + }) + }) + + it('should not checkPing if publishing at a higher rate than keepalive', function _test(done) { + const intervalMs = 3000 + const client = connect({ keepalive: intervalMs / 1000 }) + + client._checkPing = sinon.spy() + + client.once('connect', () => { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 0) + client.end(true, done) + }) + }) + + it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function _test(done) { + const intervalMs = 3000 + const client = connect({ + keepalive: intervalMs / 1000, + reschedulePings: false, + }) + + client._checkPing = sinon.spy() + + client.once('connect', () => { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 1) + client.end(true, done) + }) + }) + }) + + describe('pinging', () => { + it('should set a ping timer', function _test(done) { + const client = connect({ keepalive: 3 }) + client.once('connect', () => { + assert.exists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should not set a ping timer keepalive=0', function _test(done) { + const client = connect({ keepalive: 0 }) + client.on('connect', () => { + assert.notExists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should reconnect if pingresp is not sent', function _test(done) { + this.timeout(4000) + const client = connect({ keepalive: 1, reconnectPeriod: 100 }) + + // Fake no pingresp being send by stubbing the _handlePingresp function + client.on('packetreceive', (packet) => { + if (packet.cmd === 'pingresp') { + setImmediate(() => { + client.pingResp = false + }) + } + }) + + client.once('connect', () => { + client.once('connect', () => { + client.end(true, done) + }) + }) + }) + + it('should not reconnect if pingresp is successful', function _test(done) { + const client = connect({ keepalive: 100 }) + client.once('close', () => { + done(new Error('Client closed connection')) + }) + setTimeout(() => { + client.removeAllListeners('close') + client.end(true, done) + }, 1000) + }) + + it('should defer the next ping when sending a control packet', function _test(done) { + const client = connect({ keepalive: 1 }) + + client.once('connect', () => { + client._checkPing = sinon.spy() + + client.publish('foo', 'bar') + setTimeout(() => { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(() => { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(() => { + assert.strictEqual(client._checkPing.callCount, 0) + done() + }, 75) + }, 75) + }, 75) + }) + }) + }) + + describe('subscribing', () => { + it('should send a subscribe message (offline)', function _test(done) { + const client = connect() + + client.subscribe('test') + + server.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + done() + }) + }) + }) + + it('should send a subscribe message', function _test(done) { + const client = connect() + const topic = 'test' + + client.once('connect', () => { + client.subscribe(topic) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', (packet) => { + const result = { + topic, + qos: 0, + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + done() + }) + }) + }) + + it('should emit a packetsend event', function _test(done) { + const client = connect() + const testTopic = 'testTopic' + + client.once('connect', () => { + client.subscribe(testTopic) + }) + + client.on('packetsend', (packet) => { + if (packet.cmd === 'subscribe') { + done() + } + }) + }) + + it('should emit a packetreceive event', function _test(done) { + const client = connect() + const testTopic = 'testTopic' + + client.once('connect', () => { + client.subscribe(testTopic) + }) + + client.on('packetreceive', (packet) => { + if (packet.cmd === 'suback') { + done() + } + }) + }) + + it('should accept an array of subscriptions', function _test(done) { + const client = connect() + const subs = ['test1', 'test2'] + + client.once('connect', () => { + client.subscribe(subs) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', (packet) => { + // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] + const expected = subs.map((i) => { + const result = { topic: i, qos: 0 } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + return result + }) + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept a hash of subscriptions', function _test(done) { + const client = connect() + const topics = { + test1: { qos: 0 }, + test2: { qos: 1 }, + } + + client.once('connect', () => { + client.subscribe(topics) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', (packet) => { + const expected = [] + + for (const k in topics) { + if (Object.prototype.hasOwnProperty.call(topics, k)) { + const result = { + topic: k, + qos: topics[k].qos, + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + expected.push(result) + } + } + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept an options parameter', function _test(done) { + const client = connect() + const topic = 'test' + const opts = { qos: 1 } + + client.once('connect', () => { + client.subscribe(topic, opts) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', (packet) => { + const expected = [ + { + topic, + qos: 1, + }, + ] + + if (version === 5) { + expected[0].nl = false + expected[0].rap = false + expected[0].rh = 0 + } + + assert.deepStrictEqual(packet.subscriptions, expected) + done() + }) + }) + }) + + it('should subscribe with the default options for an empty options parameter', function _test(done) { + const client = connect() + const topic = 'test' + const defaultOpts = { qos: 0 } + + client.once('connect', () => { + client.subscribe(topic, {}) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', (packet) => { + const result = { + topic, + qos: defaultOpts.qos, + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + + assert.include(packet.subscriptions[0], result) + client.end((err) => done(err)) + }) + }) + }) + + it('should fire a callback on suback', function _test(done) { + const client = connect() + const topic = 'test' + + client.once('connect', () => { + client.subscribe(topic, { qos: 2 }, (err, granted) => { + if (err) { + done(err) + } else { + assert.exists(granted, 'granted not given') + const expectedResult = { topic: 'test', qos: 2 } + if (version === 5) { + expectedResult.nl = false + expectedResult.rap = false + expectedResult.rh = 0 + expectedResult.properties = undefined + } + assert.include(granted[0], expectedResult) + client.end((err2) => done(err2)) + } + }) + }) + }) + + it('should fire a callback with error if disconnected (options provided)', function _test(done) { + const client = connect() + const topic = 'test' + client.once('connect', () => { + client.end(true, () => { + client.subscribe(topic, { qos: 2 }, (err, granted) => { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should fire a callback with error if disconnected (options not provided)', function _test(done) { + const client = connect() + const topic = 'test' + + client.once('connect', () => { + client.end(true, () => { + client.subscribe(topic, (err, granted) => { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should subscribe with a chinese topic', function _test(done) { + const client = connect() + const topic = '中国' + + client.once('connect', () => { + client.subscribe(topic) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', (packet) => { + const result = { + topic, + qos: 0, + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + client.end(done) + }) + }) + }) + }) + + describe('receiving messages', () => { + it('should fire the message event', function _test(done) { + const client = connect() + const testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5, + } + + // + client.subscribe(testPacket.topic) + client.once('message', (topic, message, packet) => { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a packetreceive event', function _test(done) { + const client = connect() + const testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5, + } + + client.subscribe(testPacket.topic) + client.on('packetreceive', (packet) => { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.qos, 1) + assert.strictEqual(packet.topic, testPacket.topic) + assert.strictEqual( + packet.payload.toString(), + testPacket.payload, + ) + assert.strictEqual(packet.retain, true) + client.end(true, done) + } + }) + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + serverClient.publish(testPacket) + }) + }) + }) + + it('should support binary data', function _test(done) { + const client = connect({ encoding: 'binary' }) + const testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5, + } + + client.subscribe(testPacket.topic) + client.once('message', (topic, message, packet) => { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2)', function _test(done) { + const client = connect() + const testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5, + } + + server.testPublish = testPacket + + client.subscribe(testPacket.topic) + client.once('message', (topic, message, packet) => { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2) - repeated publish', function _test(done) { + const client = connect() + const testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5, + } + + server.testPublish = testPacket + + const messageHandler = (topic, message, packet) => { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + + assert.strictEqual(spiedMessageHandler.callCount, 1) + client.end(true, done) + } + + const spiedMessageHandler = sinon.spy(messageHandler) + + client.subscribe(testPacket.topic) + client.on('message', spiedMessageHandler) + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + serverClient.publish(testPacket) + // twice, should be ignored + serverClient.publish(testPacket) + }) + }) + }) + + it('should support a chinese topic', function _test(done) { + const client = connect({ encoding: 'binary' }) + const testPacket = { + topic: '国', + payload: 'message', + retain: true, + qos: 1, + messageId: 5, + } + + client.subscribe(testPacket.topic) + client.once('message', (topic, message, packet) => { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + serverClient.publish(testPacket) + }) + }) + }) + }) + + describe('qos handling', () => { + it('should follow qos 0 semantics (trivial)', function _test(done) { + const client = connect() + const testTopic = 'test' + const testMessage = 'message' + + client.once('connect', () => { + client.subscribe(testTopic, { qos: 0 }, () => { + client.end(true, done) + }) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 0, + retain: false, + }) + }) + }) + }) + + it('should follow qos 1 semantics', function _test(done) { + const client = connect() + const testTopic = 'test' + const testMessage = 'message' + const mid = 50 + + client.once('connect', () => { + client.subscribe(testTopic, { qos: 1 }) + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + messageId: mid, + qos: 1, + }) + }) + + serverClient.once('puback', (packet) => { + assert.strictEqual(packet.messageId, mid) + client.end(done) + }) + }) + }) + + it('should follow qos 2 semantics', function _test(done) { + const client = connect() + const testTopic = 'test' + const testMessage = 'message' + const mid = 253 + let publishReceived = 0 + let pubrecReceived = 0 + let pubrelReceived = 0 + + client.once('connect', () => { + client.subscribe(testTopic, { qos: 2 }) + }) + + client.on('packetreceive', (packet) => { + switch (packet.cmd) { + case 'connack': + case 'suback': + // expected, but not specifically part of QOS 2 semantics + break + case 'publish': + assert.strictEqual( + pubrecReceived, + 0, + 'server received pubrec before client sent', + ) + assert.strictEqual( + pubrelReceived, + 0, + 'server received pubrec before client sent', + ) + publishReceived += 1 + break + case 'pubrel': + assert.strictEqual( + publishReceived, + 1, + 'only 1 publish must be received before a pubrel', + ) + assert.strictEqual( + pubrecReceived, + 1, + 'invalid number of PUBREC messages (not only 1)', + ) + pubrelReceived += 1 + break + default: + should.fail() + } + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid, + }) + }) + + serverClient.on('pubrec', () => { + assert.strictEqual( + publishReceived, + 1, + 'invalid number of PUBLISH messages received', + ) + assert.strictEqual( + pubrecReceived, + 0, + 'invalid number of PUBREC messages recevied', + ) + pubrecReceived += 1 + }) + + serverClient.once('pubcomp', () => { + client.removeAllListeners() + serverClient.removeAllListeners() + assert.strictEqual( + publishReceived, + 1, + 'invalid number of PUBLISH messages', + ) + assert.strictEqual( + pubrecReceived, + 1, + 'invalid number of PUBREC messages', + ) + assert.strictEqual( + pubrelReceived, + 1, + 'invalid nubmer of PUBREL messages', + ) + client.end(true, done) + }) + }) + }) + + it('should should empty the incoming store after a qos 2 handshake is completed', function _test(done) { + const client = connect() + const testTopic = 'test' + const testMessage = 'message' + const mid = 253 + + client.once('connect', () => { + client.subscribe(testTopic, { qos: 2 }) + }) + + client.on('packetreceive', (packet) => { + if (packet.cmd === 'pubrel') { + assert.strictEqual(client.incomingStore._inflights.size, 1) + } + }) + + server.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid, + }) + }) + + serverClient.once('pubcomp', () => { + assert.strictEqual(client.incomingStore._inflights.size, 0) + client.removeAllListeners() + client.end(true, done) + }) + }) + }) + + function testMultiplePubrel(shouldSendPubcompFail, done) { + const client = connect() + const testTopic = 'test' + const testMessage = 'message' + const mid = 253 + let pubcompCount = 0 + let pubrelCount = 0 + let handleMessageCount = 0 + let emitMessageCount = 0 + const origSendPacket = client._sendPacket + let shouldSendFail + + client.handleMessage = (packet, callback) => { + handleMessageCount++ + callback() + } + + client.on('message', () => { + emitMessageCount++ + }) + + client._sendPacket = (packet, sendDone) => { + shouldSendFail = + packet.cmd === 'pubcomp' && shouldSendPubcompFail + if (sendDone) { + sendDone( + shouldSendFail + ? new Error('testing pubcomp failure') + : undefined, + ) + } + + // send the mocked response + switch (packet.cmd) { + case 'subscribe': { + const suback = { + cmd: 'suback', + messageId: packet.messageId, + granted: [2], + } + handle(client, suback, (err) => { + assert.isNotOk(err) + }) + break + } + case 'pubrec': + case 'pubcomp': { + // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp + if (packet.cmd === 'pubcomp') { + pubcompCount++ + if (pubcompCount === 2) { + // end the test once the client has gone through two rounds of replying to pubrel messages + assert.strictEqual(pubrelCount, 2) + assert.strictEqual(handleMessageCount, 1) + assert.strictEqual(emitMessageCount, 1) + client._sendPacket = origSendPacket + client.end(true, done) + break + } + } + + // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received + const pubrel = { cmd: 'pubrel', messageId: mid } + pubrelCount++ + handle(client, pubrel, (err) => { + if (shouldSendFail) { + assert.exists(err) + assert.instanceOf(err, Error) + } else { + assert.notExists(err) + } + }) + break + } + } + } + + client.once('connect', () => { + client.subscribe(testTopic, { qos: 2 }) + const publish = { + cmd: 'publish', + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid, + } + handle(client, publish, (err) => { + assert.notExists(err) + }) + }) + } + + it('handle qos 2 messages exactly once when multiple pubrel received', function _test(done) { + testMultiplePubrel(false, done) + }) + + it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function _test(done) { + testMultiplePubrel(true, done) + }) + }) + + describe('auto reconnect', () => { + it('should mark the client disconnecting if #end called', function _test(done) { + const client = connect() + + client.end(true, (err) => { + assert.isTrue(client.disconnecting) + done(err) + }) + }) + + it('should reconnect after stream disconnect', function _test(done) { + const client = connect() + + let tryReconnect = true + + client.on('connect', () => { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + client.end(true, done) + } + }) + }) + + it("should emit 'reconnect' when reconnecting", function _test(done) { + const client = connect() + let tryReconnect = true + let reconnectEvent = false + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', () => { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + client.end(true, done) + } + }) + }) + + it("should emit 'offline' after going offline", function _test(done) { + const client = connect() + + let tryReconnect = true + let offlineEvent = false + + client.on('offline', () => { + offlineEvent = true + }) + + client.on('connect', () => { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(offlineEvent) + client.end(true, done) + } + }) + }) + + it('should not reconnect if it was ended by the user', function _test(done) { + const client = connect() + + client.on('connect', () => { + client.end() + done() // it will raise an exception if called two times + }) + }) + + it('should setup a reconnect timer on disconnect', function _test(done) { + const client = connect() + + client.once('connect', () => { + assert.notExists(client.reconnectTimer) + client.stream.end() + }) + + client.once('close', () => { + assert.exists(client.reconnectTimer) + client.end(true, done) + }) + }) + + const reconnectPeriodTests = [ + { period: 200 }, + { period: 2000 }, + { period: 4000 }, + ] + reconnectPeriodTests.forEach((test) => { + it(`should allow specification of a reconnect period (${test.period}ms)`, function _test(done) { + this.timeout(10000) + let end + const reconnectSlushTime = 200 + const client = connect({ reconnectPeriod: test.period }) + let reconnect = false + const start = Date.now() + + client.on('connect', () => { + if (!reconnect) { + client.stream.end() + reconnect = true + } else { + end = Date.now() + client.end(() => { + const reconnectPeriodDuringTest = end - start + if ( + reconnectPeriodDuringTest >= + test.period - reconnectSlushTime && + reconnectPeriodDuringTest <= + test.period + reconnectSlushTime + ) { + // give the connection a 200 ms slush window + done() + } else { + done( + new Error( + `Strange reconnect period: ${reconnectPeriodDuringTest}`, + ), + ) + } + }) + } + }) + }) + }) + + it('should always cleanup successfully on reconnection', function _test(done) { + const client = connect({ + host: 'this_hostname_should_not_exist', + connectTimeout: 0, + reconnectPeriod: 1, + }) + // bind client.end so that when it is called it is automatically passed in the done callback + setTimeout(() => { + client.end.call(client, done) + }, 50) + }) + + it('should resend in-flight QoS 1 publish messages from the client', function _test(done) { + this.timeout(4000) + const client = connect({ reconnectPeriod: 200 }) + let serverPublished = false + let clientCalledBack = false + + server.once('client', (serverClient) => { + serverClient.on('connect', () => { + setImmediate(() => { + serverClient.stream.destroy() + }) + }) + + server.once('client', (serverClientNew) => { + serverClientNew.on('publish', () => { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, () => { + clientCalledBack = true + check() + }) + + function check() { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight publish messages if disconnecting', function _test(done) { + const client = connect({ reconnectPeriod: 200 }) + let serverPublished = false + let clientCalledBack = false + server.once('client', (serverClient) => { + serverClient.on('connect', () => { + setImmediate(() => { + serverClient.stream.destroy() + client.end(true, (err) => { + assert.isFalse(serverPublished) + assert.isFalse(clientCalledBack) + done(err) + }) + }) + }) + server.once('client', (serverClientNew) => { + serverClientNew.on('publish', () => { + serverPublished = true + }) + }) + }) + client.publish('hello', 'world', { qos: 1 }, () => { + clientCalledBack = true + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client', function _test(done) { + const client = connect({ reconnectPeriod: 200 }) + let serverPublished = false + let clientCalledBack = false + + server.once('client', (serverClient) => { + // ignore errors + serverClient.on('error', () => {}) + serverClient.on('publish', () => { + setImmediate(() => { + serverClient.stream.destroy() + }) + }) + + server.once('client', (serverClientNew) => { + serverClientNew.on('pubrel', () => { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, () => { + clientCalledBack = true + check() + }) + + function check() { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight QoS 1 removed publish messages from the client', function _test(done) { + const client = connect({ reconnectPeriod: 200 }) + let clientCalledBack = false + + server.once('client', (serverClient) => { + serverClient.on('connect', () => { + setImmediate(() => { + serverClient.stream.destroy() + }) + }) + + server.once('client', (serverClientNew) => { + serverClientNew.on('publish', () => { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, (err) => { + clientCalledBack = true + assert.exists(err, 'error should exist') + assert.strictEqual( + err.message, + 'Message removed', + 'error message is incorrect', + ) + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, (err) => { + done(err) + }) + }) + + it('should not resend in-flight QoS 2 removed publish messages from the client', function _test(done) { + const client = connect({ reconnectPeriod: 200 }) + let clientCalledBack = false + + server.once('client', (serverClient) => { + serverClient.on('connect', () => { + setImmediate(() => { + serverClient.stream.destroy() + }) + }) + + server.once('client', (serverClientNew) => { + serverClientNew.on('publish', () => { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, (err) => { + clientCalledBack = true + assert.strictEqual(err.message, 'Message removed') + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, done) + }) + + it('should resubscribe when reconnecting', function _test(done) { + const client = connect({ reconnectPeriod: 100 }) + let tryReconnect = true + let reconnectEvent = false + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', () => { + if (tryReconnect) { + client.subscribe('hello', () => { + client.stream.end() + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + client.end(done) + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should not resubscribe when reconnecting if resubscribe is disabled', function _test(done) { + const client = connect({ reconnectPeriod: 100, resubscribe: false }) + let tryReconnect = true + let reconnectEvent = false + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', () => { + if (tryReconnect) { + client.subscribe('hello', () => { + client.stream.end() + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + should.fail() + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual( + Object.keys(client._resubscribeTopics).length, + 0, + ) + client.end(true, done) + } + }) + }) + + it('should not resubscribe when reconnecting if suback is error', function _test(done) { + let tryReconnect = true + let reconnectEvent = false + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', (packet) => { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos | 0x80), + }) + serverClient.pubrel({ + messageId: Math.floor(Math.random() * 9000) + 1000, + }) + }) + }) + + server2.listen(ports.PORTAND49, () => { + const client = connect({ + port: ports.PORTAND49, + host: 'localhost', + reconnectPeriod: 100, + }) + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', () => { + if (tryReconnect) { + client.subscribe('hello', () => { + client.stream.end() + + server.once('client', (serverClient) => { + serverClient.on('subscribe', () => { + should.fail() + }) + }) + }) + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual( + Object.keys(client._resubscribeTopics).length, + 0, + ) + client.end(true, (err1) => { + server2.close((err2) => done(err1 || err2)) + }) + } + }) + }) + }) + + it('should preserved incomingStore after disconnecting if clean is false', function _test(done) { + let reconnect = false + let client = {} + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + if (reconnect) { + serverClient.pubrel({ messageId: 1 }) + } + }) + serverClient.on('subscribe', (packet) => { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos), + }) + serverClient.publish({ + topic: 'topic', + payload: 'payload', + qos: 2, + messageId: 1, + retain: false, + }) + }) + serverClient.on('pubrec', (packet) => { + client.end(false, () => { + client.reconnect({ + incomingStore, + outgoingStore, + }) + }) + }) + serverClient.on('pubcomp', (packet) => { + client.end(true, (err1) => { + server2.close((err2) => done(err1 || err2)) + }) + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + }) + + client.on('connect', () => { + if (!reconnect) { + client.subscribe('test', { qos: 2 }, () => {}) + reconnect = true + } + }) + client.on('message', (topic, message) => { + assert.strictEqual(topic, 'topic') + assert.strictEqual(message.toString(), 'payload') + }) + }) + }) + + it('should clear outgoing if close from server', function _test(done) { + let reconnect = false + let client = {} + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', (packet) => { + if (reconnect) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos), + }) + } else { + serverClient.destroy() + } + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: true, + clientId: 'cid1', + keepalive: 1, + reconnectPeriod: 0, + }) + + client.on('connect', () => { + client.subscribe('test', { qos: 2 }, (e) => { + if (!e) { + client.end() + } + }) + }) + + client.on('close', () => { + if (reconnect) { + server2.close((err) => done(err)) + } else { + assert.strictEqual( + Object.keys(client.outgoing).length, + 0, + ) + reconnect = true + client.reconnect() + } + }) + }) + }) + + it('should resend in-flight QoS 1 publish messages from the client if clean is false', function _test(done) { + let reconnect = false + let client = {} + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + if (reconnect) { + client.end(true, (err1) => { + server2.close((err2) => done(err1 || err2)) + }) + } else { + client.end(true, () => { + client.reconnect({ + incomingStore, + outgoingStore, + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + }) + + client.on('connect', () => { + if (!reconnect) { + client.publish('topic', 'payload', { qos: 1 }) + } + }) + client.on('error', () => {}) + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client if clean is false', function _test(done) { + let reconnect = false + let client = {} + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + if (reconnect) { + client.end(true, (err1) => { + server2.close((err2) => done(err1 || err2)) + }) + } else { + client.end(true, () => { + client.reconnect({ + incomingStore, + outgoingStore, + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + }) + + client.on('connect', () => { + if (!reconnect) { + client.publish('topic', 'payload', { qos: 2 }) + } + }) + client.on('error', () => {}) + }) + }) + + it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function _test(done) { + let reconnect = false + let client = {} + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const server2 = serverBuilder(config.protocol, (serverClient) => { + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + if (!reconnect) { + serverClient.pubrec({ messageId: packet.messageId }) + } + }) + serverClient.on('pubrel', (packet) => { + if (reconnect) { + serverClient.pubcomp({ messageId: packet.messageId }) + } else { + client.end(true, () => { + client.reconnect({ + incomingStore, + outgoingStore, + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + }) + + client.on('connect', () => { + if (!reconnect) { + client.publish( + 'topic', + 'payload', + { qos: 2 }, + (err) => { + assert(reconnect) + assert.ifError(err) + client.end(true, (err1) => { + server2.close((err2) => done(err1 || err2)) + }) + }, + ) + } + }) + client.on('error', () => {}) + }) + }) + + it('should resend in-flight publish messages by published order', function _test(done) { + let publishCount = 0 + let reconnect = false + let disconnectOnce = true + let client = {} + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const server2 = serverBuilder(config.protocol, (serverClient) => { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', () => {}) + + serverClient.on('connect', (packet) => { + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', (packet) => { + serverClient.puback({ messageId: packet.messageId }) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual( + packet.payload.toString(), + 'payload1', + ) + break + case 1: + assert.strictEqual( + packet.payload.toString(), + 'payload2', + ) + break + case 2: + assert.strictEqual( + packet.payload.toString(), + 'payload3', + ) + client.end(true, (err1) => { + server2.close((err2) => done(err1 || err2)) + }) + break + } + } else if (disconnectOnce) { + client.end(true, () => { + reconnect = true + client.reconnect({ + incomingStore, + outgoingStore, + }) + }) + disconnectOnce = false + } + }) + }) + + server2.listen(ports.PORTAND50, () => { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore, + outgoingStore, + }) + + client.nextId = 65535 + + client.on('connect', () => { + if (!reconnect) { + client.publish('topic', 'payload1', { qos: 1 }) + client.publish('topic', 'payload2', { qos: 1 }) + client.publish('topic', 'payload3', { qos: 1 }) + } + }) + client.on('error', () => {}) + }) + }) + + it('should be able to pub/sub if reconnect() is called at close handler', function _test(done) { + const client = connect({ reconnectPeriod: 0 }) + let tryReconnect = true + let reconnectEvent = false + + client.on('close', () => { + if (tryReconnect) { + tryReconnect = false + client.reconnect() + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', () => { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', () => { + client.end() + }) + } + }) + }) + + it('should be able to pub/sub if reconnect() is called at out of close handler', function _test(done) { + const client = connect({ reconnectPeriod: 0 }) + let tryReconnect = true + let reconnectEvent = false + + client.on('close', () => { + if (tryReconnect) { + tryReconnect = false + setTimeout(() => { + client.reconnect() + }, 100) + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', () => { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', () => { + client.end() + }) + } + }) + }) + + context('with alternate server client', () => { + let cachedClientListeners + const connack = + version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + + beforeEach(() => { + cachedClientListeners = server.listeners('client') + server.removeAllListeners('client') + }) + + afterEach(() => { + server.removeAllListeners('client') + cachedClientListeners.forEach((listener) => { + server.on('client', listener) + }) + }) + + it('should resubscribe even if disconnect is before suback', function _test(done) { + const client = connect({ reconnectPeriod: 100, ...config }) + let subscribeCount = 0 + let connectCount = 0 + + server.on('client', (serverClient) => { + serverClient.on('connect', () => { + connectCount++ + serverClient.connack(connack) + }) + + serverClient.on('subscribe', () => { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, confirm that the only two + // subscribes have taken place, then cleanup and exit + if (connectCount >= 2) { + assert.strictEqual(subscribeCount, 2) + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + + it('should resubscribe exactly once', function _test(done) { + const client = connect({ reconnectPeriod: 100, ...config }) + let subscribeCount = 0 + + server.on('client', (serverClient) => { + serverClient.on('connect', () => { + serverClient.connack(connack) + }) + + serverClient.on('subscribe', () => { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, only two subs + // subscribes have taken place, then cleanup and exit + if (subscribeCount === 2) { + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + }) + }) + + describe('message id to subscription topic mapping', () => { + it('should not create a mapping if resubscribe is disabled', function _test(done) { + const client = connect({ resubscribe: false }) + client.subscribe('test1') + client.subscribe('test2') + assert.strictEqual(Object.keys(client.messageIdToTopic).length, 0) + client.end(true, done) + }) + + it('should create a mapping for each subscribe call', function _test(done) { + const client = connect() + client.subscribe('test1') + assert.strictEqual(Object.keys(client.messageIdToTopic).length, 1) + client.subscribe('test2') + assert.strictEqual(Object.keys(client.messageIdToTopic).length, 2) + + client.subscribe(['test3', 'test4']) + assert.strictEqual(Object.keys(client.messageIdToTopic).length, 3) + client.subscribe(['test5', 'test6']) + assert.strictEqual(Object.keys(client.messageIdToTopic).length, 4) + + client.end(true, done) + }) + + it('should remove the mapping after suback', function _test(done) { + const client = connect() + client.once('connect', () => { + client.subscribe('test1', { qos: 2 }, () => { + assert.strictEqual( + Object.keys(client.messageIdToTopic).length, + 0, + ) + + client.subscribe(['test2', 'test3'], { qos: 2 }, () => { + assert.strictEqual( + Object.keys(client.messageIdToTopic).length, + 0, + ) + client.end(done) + }) + }) + }) + }) + }) } diff --git a/test/abstract_store.js b/test/abstract_store.js index bae4bb033..56a060f61 100644 --- a/test/abstract_store.js +++ b/test/abstract_store.js @@ -1,136 +1,130 @@ -'use strict' - require('should') -module.exports = function abstractStoreTest (build) { - let store +module.exports = function abstractStoreTest(build) { + let store - // eslint-disable-next-line + // eslint-disable-next-line beforeEach(function (done) { - build(function (err, _store) { - store = _store - done(err) - }) - }) - - afterEach(function (done) { - store.close(done) - }) - - it('should put and stream in-flight packets', function (done) { - const packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet) - done() - }) - }) - }) - - it('should support destroying the stream', function (done) { - const packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - const stream = store.createStream() - stream.on('close', done) - stream.destroy() - }) - }) - - it('should add and del in-flight packets', function (done) { - const packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del(packet, function () { - store - .createStream() - .on('data', function () { - done(new Error('this should never happen')) - }) - .on('end', done) - }) - }) - }) - - it('should replace a packet when doing put with the same messageId', function (done) { - const packet1 = { - cmd: 'publish', // added - topic: 'hello', - payload: 'world', - qos: 2, - messageId: 42 - } - const packet2 = { - cmd: 'pubrel', // added - qos: 2, - messageId: 42 - } - - store.put(packet1, function () { - store.put(packet2, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet2) - done() - }) - }) - }) - }) - - it('should return the original packet on del', function (done) { - const packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del({ messageId: 42 }, function (err, deleted) { - if (err) { - throw err - } - deleted.should.eql(packet) - done() - }) - }) - }) - - it('should get a packet with the same messageId', function (done) { - const packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.get({ messageId: 42 }, function (err, fromDb) { - if (err) { - throw err - } - fromDb.should.eql(packet) - done() - }) - }) - }) + build((err, _store) => { + store = _store + done(err) + }) + }) + + afterEach(function test(done) { + store.close(done) + }) + + it('should put and stream in-flight packets', function test(done) { + const packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42, + } + + store.put(packet, () => { + store.createStream().on('data', (data) => { + data.should.eql(packet) + done() + }) + }) + }) + + it('should support destroying the stream', function test(done) { + const packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42, + } + + store.put(packet, () => { + const stream = store.createStream() + stream.on('close', done) + stream.destroy() + }) + }) + + it('should add and del in-flight packets', function test(done) { + const packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42, + } + + store.put(packet, () => { + store.del(packet, () => { + store + .createStream() + .on('data', () => { + done(new Error('this should never happen')) + }) + .on('end', done) + }) + }) + }) + + it('should replace a packet when doing put with the same messageId', function test(done) { + const packet1 = { + cmd: 'publish', // added + topic: 'hello', + payload: 'world', + qos: 2, + messageId: 42, + } + const packet2 = { + cmd: 'pubrel', // added + qos: 2, + messageId: 42, + } + + store.put(packet1, () => { + store.put(packet2, () => { + store.createStream().on('data', (data) => { + data.should.eql(packet2) + done() + }) + }) + }) + }) + + it('should return the original packet on del', function test(done) { + const packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42, + } + + store.put(packet, () => { + store.del({ messageId: 42 }, (err, deleted) => { + if (err) { + throw err + } + deleted.should.eql(packet) + done() + }) + }) + }) + + it('should get a packet with the same messageId', function test(done) { + const packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42, + } + + store.put(packet, () => { + store.get({ messageId: 42 }, (err, fromDb) => { + if (err) { + throw err + } + fromDb.should.eql(packet) + done() + }) + }) + }) } diff --git a/test/browser/server.js b/test/browser/server.js index 73066eda3..fe1349967 100644 --- a/test/browser/server.js +++ b/test/browser/server.js @@ -1,129 +1,134 @@ -'use strict' - const WS = require('ws') + const WebSocketServer = WS.Server const Connection = require('mqtt-connection') const http = require('http') -const handleClient = function (client) { - const self = this - - if (!self.clients) { - self.clients = {} - } - - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({ returnCode: 2 }) - } else { - client.connack({ returnCode: 0 }) - } - self.clients[packet.clientId] = client - client.subscriptions = [] - }) - - client.on('publish', function (packet) { - let k, c, s, publish - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - - for (k in self.clients) { - c = self.clients[k] - publish = false - - for (let i = 0; i < c.subscriptions.length; i++) { - s = c.subscriptions[i] - - if (s.test(packet.topic)) { - publish = true - } - } - - if (publish) { - try { - c.publish({ topic: packet.topic, payload: packet.payload }) - } catch (error) { - delete self.clients[k] - } - } - } - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - let qos, topic, reg - const granted = [] - - for (let i = 0; i < packet.subscriptions.length; i++) { - qos = packet.subscriptions[i].qos - topic = packet.subscriptions[i].topic - reg = new RegExp(topic.replace('+', '[^/]+').replace('#', '.+') + '$') - - granted.push(qos) - client.subscriptions.push(reg) - } - - client.suback({ messageId: packet.messageId, granted }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) +const handleClient = (client) => { + const self = this + + if (!self.clients) { + self.clients = {} + } + + client.on('connect', (packet) => { + if (packet.clientId === 'invalid') { + client.connack({ returnCode: 2 }) + } else { + client.connack({ returnCode: 0 }) + } + self.clients[packet.clientId] = client + client.subscriptions = [] + }) + + client.on('publish', (packet) => { + let k + let c + let s + let publish + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + + for (k in self.clients) { + c = self.clients[k] + publish = false + + for (let i = 0; i < c.subscriptions.length; i++) { + s = c.subscriptions[i] + + if (s.test(packet.topic)) { + publish = true + } + } + + if (publish) { + try { + c.publish({ topic: packet.topic, payload: packet.payload }) + } catch (error) { + delete self.clients[k] + } + } + } + }) + + client.on('pubrel', (packet) => { + client.pubcomp(packet) + }) + + client.on('pubrec', (packet) => { + client.pubrel(packet) + }) + + client.on('pubcomp', () => { + // Nothing to be done + }) + + client.on('subscribe', (packet) => { + let qos + let topic + let reg + const granted = [] + + for (let i = 0; i < packet.subscriptions.length; i++) { + qos = packet.subscriptions[i].qos + topic = packet.subscriptions[i].topic + reg = new RegExp( + `${topic.replace('+', '[^/]+').replace('#', '.+')}$`, + ) + + granted.push(qos) + client.subscriptions.push(reg) + } + + client.suback({ messageId: packet.messageId, granted }) + }) + + client.on('unsubscribe', (packet) => { + client.unsuback(packet) + }) + + client.on('pingreq', () => { + client.pingresp() + }) } -function start (startPort, done) { - const server = http.createServer() - const wss = new WebSocketServer({ server }) - - wss.on('connection', function (ws) { - if (!(ws.protocol === 'mqtt' || - ws.protocol === 'mqttv3.1')) { - return ws.close() - } - - const stream = WS.createWebSocketStream(ws) - const connection = new Connection(stream) - handleClient.call(server, connection) - }) - server.listen(startPort, done) - server.on('request', function (req, res) { - res.statusCode = 404 - res.end('Not Found') - }) - return server +function start(startPort, done) { + const server = http.createServer() + const wss = new WebSocketServer({ server }) + + wss.on('connection', (ws) => { + if (!(ws.protocol === 'mqtt' || ws.protocol === 'mqttv3.1')) { + return ws.close() + } + + const stream = WS.createWebSocketStream(ws) + const connection = new Connection(stream) + handleClient.call(server, connection) + }) + server.listen(startPort, done) + server.on('request', (req, res) => { + res.statusCode = 404 + res.end('Not Found') + }) + return server } const port = process.env.PORT || process.env.AIRTAP_SUPPORT_PORT if (require.main === module) { - start(port, function (err) { - if (err) { - console.error(err) - return - } - console.log('tunnelled server started on port', port) - }) + start(port, (err) => { + if (err) { + console.error(err) + return + } + console.log('tunnelled server started on port', port) + }) } diff --git a/test/browser/test.js b/test/browser/test.js index 37bc95cfa..8825a2e4c 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -1,8 +1,6 @@ -'use strict' - const test = require('tape') -const mqtt = require('../../dist/mqtt.min') const _URL = require('url') +const mqtt = require('../../dist/mqtt.min') // eslint-disable-next-line const parsed = _URL.parse(document.URL) const isHttps = parsed.protocol === 'https:' @@ -10,43 +8,50 @@ const port = parsed.port || (isHttps ? 443 : 80) const host = parsed.hostname const protocol = isHttps ? 'wss' : 'ws' -const client = mqtt.connect({ protocolId: 'MQIsdp', protocolVersion: 3, protocol, port, host, log: console.log.bind(console) }) -client.on('offline', function () { - console.log('client offline') +const client = mqtt.connect({ + protocolId: 'MQIsdp', + protocolVersion: 3, + protocol, + port, + host, + log: console.log.bind(console), +}) +client.on('offline', () => { + console.log('client offline') }) -client.on('connect', function () { - console.log('client connect') +client.on('connect', () => { + console.log('client connect') }) -client.on('reconnect', function () { - console.log('client reconnect') +client.on('reconnect', () => { + console.log('client reconnect') }) -test('MQTT.js browser test', function (t) { - t.plan(6) - client.on('connect', function () { - client.on('message', function (topic, msg) { - t.equal(topic, 'hello', 'should match topic') - t.equal(msg.toString(), 'Hello World!', 'should match payload') - client.end(() => { - t.pass('client should close') - }) - }) +test('MQTT.js browser test', (t) => { + t.plan(6) + client.on('connect', () => { + client.on('message', (topic, msg) => { + t.equal(topic, 'hello', 'should match topic') + t.equal(msg.toString(), 'Hello World!', 'should match payload') + client.end(() => { + t.pass('client should close') + }) + }) - client.subscribe('hello', function (err) { - t.error(err, 'no error on subscribe') - if (!err) { - client.publish('hello', 'Hello World!', function (err) { - t.error(err, 'no error on publish') - }) - } - }) - }) + client.subscribe('hello', (err) => { + t.error(err, 'no error on subscribe') + if (!err) { + client.publish('hello', 'Hello World!', (err2) => { + t.error(err2, 'no error on publish') + }) + } + }) + }) - client.on('error', function (err) { - t.fail(err, 'no error') - }) + client.on('error', (err) => { + t.fail(err, 'no error') + }) - client.once('close', function () { - t.pass('should emit close') - }) + client.once('close', () => { + t.pass('should emit close') + }) }) diff --git a/test/client.js b/test/client.js index 2d78a81d4..e9127174d 100644 --- a/test/client.js +++ b/test/client.js @@ -1,492 +1,529 @@ -'use strict' - const mqtt = require('..') -const assert = require('chai').assert +const { assert } = require('chai') const { fork } = require('child_process') const path = require('path') -const abstractClientTests = require('./abstract_client') const net = require('net') const eos = require('end-of-stream') const mqttPacket = require('mqtt-packet') -const Duplex = require('readable-stream').Duplex +const { Duplex } = require('readable-stream') const Connection = require('mqtt-connection') -const MqttServer = require('./server').MqttServer const util = require('util') const ports = require('./helpers/port_list') -const serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +const { serverBuilder } = require('./server_helpers_for_client_tests') const debug = require('debug')('TEST:client') +const { MqttServer } = require('./server') +const abstractClientTests = require('./abstract_client') -describe('MqttClient', function () { - let client - const server = serverBuilder('mqtt') - const config = { protocol: 'mqtt', port: ports.PORT } - server.listen(ports.PORT) - - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) - - abstractClientTests(server, config) - - describe('creating', function () { - it('should allow instantiation of MqttClient', function (done) { - try { - client = new mqtt.MqttClient(function () { - throw Error('break') - }, {}) - client.end() - } catch (err) { - assert.strictEqual(err.message, 'break') - done() - } - }) - - it('should disable number cache if specified in options', function (done) { - try { - assert.isTrue(mqttPacket.writeToStream.cacheNumbers) - client = new mqtt.MqttClient(function () { - throw Error('break') - }, { writeCache: false }) - client.end() - } catch (err) { - assert.isFalse(mqttPacket.writeToStream.cacheNumbers) - done() - } - }) - }) - - describe('message ids', function () { - it('should increment the message id', function (done) { - client = mqtt.connect(config) - const currentId = client._nextId() - - assert.equal(client._nextId(), currentId + 1) - client.end((err) => done(err)) - }) - - it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ returnCode: 0 }) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - client = mqtt.connect({ - port: ports.PORTAND49, - host: 'localhost' - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'pubcomp') { - client.end((err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - } - }) - }) - }) - - it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function (done) { - const parser = mqttPacket.parser() - const max = 1000 - let count = 0 - const duplex = new Duplex({ - read: function (n) { }, - write: function (chunk, enc, cb) { - parser.parse(chunk) - cb() // nothing to do - } - }) - client = new mqtt.MqttClient(function () { - return duplex - }, {}) - - client.on('message', function (t, p, packet) { - if (++count === max) { - // BUGBUG: the client.end callback never gets called here - // client.end((err) => done(err)) - client.end() - done() - } - }) - - parser.on('packet', function (packet) { - const packets = [] - - if (packet.cmd === 'connect') { - duplex.push(mqttPacket.generate({ - cmd: 'connack', - sessionPresent: false, - returnCode: 0 - })) - - for (let i = 0; i < max; i++) { - packets.push(mqttPacket.generate({ - cmd: 'publish', - topic: Buffer.from('hello'), - payload: Buffer.from('world'), - retain: false, - dup: false, - messageId: i + 1, - qos: 1 - })) - } - - duplex.push(Buffer.concat(packets)) - } - }) - }) - }) - - describe('flushing', function () { - it('should attempt to complete pending unsub and send on ping timeout', function (done) { - this.timeout(10000) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ returnCode: 0 }) - }) - }).listen(ports.PORTAND72) - - let pubCallbackCalled = false - let unsubscribeCallbackCalled = false - client = mqtt.connect({ - port: ports.PORTAND72, - host: 'localhost', - keepalive: 1, - connectTimeout: 350, - reconnectPeriod: 0 - }) - client.once('connect', () => { - client.publish('fakeTopic', 'fakeMessage', { qos: 1 }, (err, result) => { - assert.exists(err) - pubCallbackCalled = true - }) - client.unsubscribe('fakeTopic', (err, result) => { - assert.exists(err) - unsubscribeCallbackCalled = true - }) - setTimeout(() => { - client.end((err1) => { - assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') - server2.close((err2) => { - done(err1 || err2) - }) - }) - }, 5000) - }) - }) - }) - - describe('reconnecting', function () { - it('should attempt to reconnect once server is down', function (done) { - this.timeout(30000) - - const innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) - innerServer.on('close', (code) => { - if (code) { - done(util.format('child process closed with code %d', code)) - } - }) - - innerServer.on('exit', (code) => { - if (code) { - done(util.format('child process exited with code %d', code)) - } - }) - - client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) - client.once('connect', function () { - innerServer.kill('SIGINT') // mocks server shutdown - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, (err) => done(err)) - }) - }) - }) - - it('should reconnect if a connack is not received in an interval', function (done) { - this.timeout(2000) - - const server2 = net.createServer().listen(ports.PORTAND43) - - server2.on('connection', function (c) { - eos(c, function () { - server2.close() - }) - }) - - server2.on('listening', function () { - client = mqtt.connect({ - servers: [ - { port: ports.PORTAND43, host: 'localhost_fake' }, - { port: ports.PORT, host: 'localhost' } - ], - connectTimeout: 500 - }) - - server.once('client', function () { - client.end(false, (err) => { - done(err) - }) - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - - it('should not be cleared by the connack timer', function (done) { - this.timeout(4000) - - const server2 = net.createServer().listen(ports.PORTAND44) - - server2.on('connection', function (c) { - c.destroy() - }) - - server2.once('listening', function () { - const connectTimeout = 1000 - const reconnectPeriod = 100 - const expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) - let reconnects = 0 - client = mqtt.connect({ - port: ports.PORTAND44, - host: 'localhost', - connectTimeout, - reconnectPeriod - }) - - client.on('reconnect', function () { - reconnects++ - if (reconnects >= expectedReconnects) { - client.end(true, (err) => done(err)) - } - }) - }) - }) - - it('should not keep requeueing the first message when offline', function (done) { - this.timeout(2500) - - const server2 = serverBuilder('mqtt').listen(ports.PORTAND45) - client = mqtt.connect({ - port: ports.PORTAND45, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - server2.on('client', function (serverClient) { - client.publish('hello', 'world', { qos: 1 }, function () { - serverClient.destroy() - server2.close(() => { - debug('now publishing message in an offline state') - client.publish('hello', 'world', { qos: 1 }) - }) - }) - }) - - setTimeout(function () { - if (client.queue.length === 0) { - debug('calling final client.end()') - client.end(true, (err) => done(err)) - } else { - debug('calling client.end()') - // Do not call done. We want to trigger a reconnect here. - client.end(true) - } - }, 2000) - }) - - it('should not send the same subscribe multiple times on a flaky connection', function (done) { - this.timeout(3500) - - const KILL_COUNT = 4 - const subIds = {} - let killedConnections = 0 - client = mqtt.connect({ - port: ports.PORTAND46, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - const server2 = new MqttServer(function (serverClient) { - debug('client received on server2.') - debug('subscribing to topic `topic`') - client.subscribe('topic', function () { - debug('once subscribed to topic, end client, destroy serverClient, and close server.') - serverClient.destroy() - server2.close(() => { - client.end(true, (err) => done(err)) - }) - }) - - serverClient.on('subscribe', function (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few sub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - } else { - // Keep track of acks - if (!subIds[packet.messageId]) { - subIds[packet.messageId] = 0 - } - subIds[packet.messageId]++ - if (subIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) - client.end(true) - serverClient.end() - server2.destroy() - } - - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } - }) - }).listen(ports.PORTAND46) - }) - - it('should not fill the queue of subscribes if it cannot connect', function (done) { - this.timeout(2500) - const server2 = net.createServer(function (stream) { - const serverClient = new Connection(stream) - - serverClient.on('error', function (e) { /* do nothing */ }) - serverClient.on('connect', function (packet) { - serverClient.connack({ returnCode: 0 }) - serverClient.destroy() - }) - }) - - server2.listen(ports.PORTAND48, function () { - client = mqtt.connect({ - port: ports.PORTAND48, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - client.subscribe('hello') - - setTimeout(function () { - assert.equal(client.queue.length, 1) - client.end(true, (err) => done(err)) - }, 1000) - }) - }) - - it('should not send the same publish multiple times on a flaky connection', function (done) { - this.timeout(3500) - - const KILL_COUNT = 4 - let killedConnections = 0 - const pubIds = {} - client = mqtt.connect({ - port: ports.PORTAND47, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - const server2 = net.createServer(function (stream) { - const serverClient = new Connection(stream) - serverClient.on('error', function () { }) - serverClient.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - serverClient.connack({ returnCode: 2 }) - } else { - serverClient.connack({ returnCode: 0 }) - } - }) - - this.emit('client', serverClient) - }).listen(ports.PORTAND47) - - server2.on('client', function (serverClient) { - client.publish('topic', 'data', { qos: 1 }, function () { - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - - serverClient.on('publish', function onPublish (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few pub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - - // to avoid receiving inflight messages - serverClient.removeListener('publish', onPublish) - } else { - // Keep track of acks - if (!pubIds[packet.messageId]) { - pubIds[packet.messageId] = 0 - } - - pubIds[packet.messageId]++ - - if (pubIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) - client.end(true) - serverClient.destroy() - server2.destroy() - } - - serverClient.puback(packet) - } - }) - }) - }) - }) - - it('check emit error on checkDisconnection w/o callback', function (done) { - this.timeout(15000) - - const server2 = new MqttServer(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0 - }) - }) - client.on('publish', function (packet) { - setImmediate(function () { - packet.reasonCode = 0 - client.puback(packet) - }) - }) - }).listen(ports.PORTAND118) - - const opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - client = mqtt.connect(opts) - - // wait for the client to receive an error... - client.on('error', function (error) { - assert.equal(error.message, 'client disconnecting') - server2.close((err) => done(err)) - }) - client.on('connect', function () { - client.end(function () { - client._checkDisconnecting() - }) - }) - }) +describe('MqttClient', () => { + let client + const server = serverBuilder('mqtt') + const config = { protocol: 'mqtt', port: ports.PORT } + server.listen(ports.PORT) + + after(() => { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) + + describe('creating', () => { + it('should allow instantiation of MqttClient', function test(done) { + try { + client = new mqtt.MqttClient(() => { + throw Error('break') + }, {}) + client.end() + } catch (err) { + assert.strictEqual(err.message, 'break') + done() + } + }) + + it('should disable number cache if specified in options', function test(done) { + try { + assert.isTrue(mqttPacket.writeToStream.cacheNumbers) + client = new mqtt.MqttClient( + () => { + throw Error('break') + }, + { writeCache: false }, + ) + client.end() + } catch (err) { + assert.isFalse(mqttPacket.writeToStream.cacheNumbers) + done() + } + }) + }) + + describe('message ids', () => { + it('should increment the message id', function test(done) { + client = mqtt.connect(config) + const currentId = client._nextId() + + assert.equal(client._nextId(), currentId + 1) + client.end((err) => done(err)) + }) + + it("should not throw an error if packet's messageId is not found when receiving a pubrel packet", function test(done) { + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ returnCode: 0 }) + serverClient.pubrel({ + messageId: Math.floor(Math.random() * 9000) + 1000, + }) + }) + }) + + server2.listen(ports.PORTAND49, () => { + client = mqtt.connect({ + port: ports.PORTAND49, + host: 'localhost', + }) + + client.on('packetsend', (packet) => { + if (packet.cmd === 'pubcomp') { + client.end((err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + } + }) + }) + }) + + it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function test(done) { + const parser = mqttPacket.parser() + const max = 1000 + let count = 0 + const duplex = new Duplex({ + read(n) {}, + write(chunk, enc, cb) { + parser.parse(chunk) + cb() // nothing to do + }, + }) + client = new mqtt.MqttClient(() => duplex, {}) + + client.on('message', (t, p, packet) => { + if (++count === max) { + // BUGBUG: the client.end callback never gets called here + // client.end((err) => done(err)) + client.end() + done() + } + }) + + parser.on('packet', (packet) => { + const packets = [] + + if (packet.cmd === 'connect') { + duplex.push( + mqttPacket.generate({ + cmd: 'connack', + sessionPresent: false, + returnCode: 0, + }), + ) + + for (let i = 0; i < max; i++) { + packets.push( + mqttPacket.generate({ + cmd: 'publish', + topic: Buffer.from('hello'), + payload: Buffer.from('world'), + retain: false, + dup: false, + messageId: i + 1, + qos: 1, + }), + ) + } + + duplex.push(Buffer.concat(packets)) + } + }) + }) + }) + + describe('flushing', () => { + it('should attempt to complete pending unsub and send on ping timeout', function test(done) { + this.timeout(10000) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ returnCode: 0 }) + }) + }).listen(ports.PORTAND72) + + let pubCallbackCalled = false + let unsubscribeCallbackCalled = false + client = mqtt.connect({ + port: ports.PORTAND72, + host: 'localhost', + keepalive: 1, + connectTimeout: 350, + reconnectPeriod: 0, + }) + client.once('connect', () => { + client.publish( + 'fakeTopic', + 'fakeMessage', + { qos: 1 }, + (err, result) => { + assert.exists(err) + pubCallbackCalled = true + }, + ) + client.unsubscribe('fakeTopic', (err, result) => { + assert.exists(err) + unsubscribeCallbackCalled = true + }) + setTimeout(() => { + client.end((err1) => { + assert.strictEqual( + pubCallbackCalled && unsubscribeCallbackCalled, + true, + 'callbacks not invoked', + ) + server2.close((err2) => { + done(err1 || err2) + }) + }) + }, 5000) + }) + }) + }) + + describe('reconnecting', () => { + it('should attempt to reconnect once server is down', function test(done) { + this.timeout(30000) + + const innerServer = fork( + path.join(__dirname, 'helpers', 'server_process.js'), + { + execArgv: ['--inspect'], + }, + ) + innerServer.on('close', (code) => { + if (code) { + done(util.format('child process closed with code %d', code)) + } + }) + + innerServer.on('exit', (code) => { + if (code) { + done(util.format('child process exited with code %d', code)) + } + }) + + client = mqtt.connect({ + port: 3481, + host: 'localhost', + keepalive: 1, + }) + client.once('connect', () => { + innerServer.kill('SIGINT') // mocks server shutdown + client.once('close', () => { + assert.exists(client.reconnectTimer) + client.end(true, (err) => done(err)) + }) + }) + }) + + it('should reconnect if a connack is not received in an interval', function test(done) { + this.timeout(2000) + + const server2 = net.createServer().listen(ports.PORTAND43) + + server2.on('connection', (c) => { + eos(c, () => { + server2.close() + }) + }) + + server2.on('listening', () => { + client = mqtt.connect({ + servers: [ + { port: ports.PORTAND43, host: 'localhost_fake' }, + { port: ports.PORT, host: 'localhost' }, + ], + connectTimeout: 500, + }) + + server.once('client', () => { + client.end(false, (err) => { + done(err) + }) + }) + + client.once('connect', () => { + client.stream.destroy() + }) + }) + }) + + it('should not be cleared by the connack timer', function test(done) { + this.timeout(4000) + + const server2 = net.createServer().listen(ports.PORTAND44) + + server2.on('connection', (c) => { + c.destroy() + }) + + server2.once('listening', () => { + const connectTimeout = 1000 + const reconnectPeriod = 100 + const expectedReconnects = Math.floor( + connectTimeout / reconnectPeriod, + ) + let reconnects = 0 + client = mqtt.connect({ + port: ports.PORTAND44, + host: 'localhost', + connectTimeout, + reconnectPeriod, + }) + + client.on('reconnect', () => { + reconnects++ + if (reconnects >= expectedReconnects) { + client.end(true, (err) => done(err)) + } + }) + }) + }) + + it('should not keep requeueing the first message when offline', function test(done) { + this.timeout(2500) + + const server2 = serverBuilder('mqtt').listen(ports.PORTAND45) + client = mqtt.connect({ + port: ports.PORTAND45, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300, + }) + + server2.on('client', (serverClient) => { + client.publish('hello', 'world', { qos: 1 }, () => { + serverClient.destroy() + server2.close(() => { + debug('now publishing message in an offline state') + client.publish('hello', 'world', { qos: 1 }) + }) + }) + }) + + setTimeout(() => { + if (client.queue.length === 0) { + debug('calling final client.end()') + client.end(true, (err) => done(err)) + } else { + debug('calling client.end()') + // Do not call done. We want to trigger a reconnect here. + client.end(true) + } + }, 2000) + }) + + it('should not send the same subscribe multiple times on a flaky connection', function test(done) { + this.timeout(3500) + + const KILL_COUNT = 4 + const subIds = {} + let killedConnections = 0 + client = mqtt.connect({ + port: ports.PORTAND46, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300, + }) + + const server2 = new MqttServer((serverClient) => { + debug('client received on server2.') + debug('subscribing to topic `topic`') + client.subscribe('topic', () => { + debug( + 'once subscribed to topic, end client, destroy serverClient, and close server.', + ) + serverClient.destroy() + server2.close(() => { + client.end(true, (err) => done(err)) + }) + }) + + serverClient.on('subscribe', (packet) => { + if (killedConnections < KILL_COUNT) { + // Kill the first few sub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + } else { + // Keep track of acks + if (!subIds[packet.messageId]) { + subIds[packet.messageId] = 0 + } + subIds[packet.messageId]++ + if (subIds[packet.messageId] > 1) { + done( + new Error( + `Multiple duplicate acked subscriptions received for messageId ${packet.messageId}`, + ), + ) + client.end(true) + serverClient.end() + server2.destroy() + } + + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos), + }) + } + }) + }).listen(ports.PORTAND46) + }) + + it('should not fill the queue of subscribes if it cannot connect', function test(done) { + this.timeout(2500) + const server2 = net.createServer((stream) => { + const serverClient = new Connection(stream) + + serverClient.on('error', (e) => { + /* do nothing */ + }) + serverClient.on('connect', (packet) => { + serverClient.connack({ returnCode: 0 }) + serverClient.destroy() + }) + }) + + server2.listen(ports.PORTAND48, () => { + client = mqtt.connect({ + port: ports.PORTAND48, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300, + }) + + client.subscribe('hello') + + setTimeout(() => { + assert.equal(client.queue.length, 1) + client.end(true, (err) => done(err)) + }, 1000) + }) + }) + + it('should not send the same publish multiple times on a flaky connection', function test(done) { + this.timeout(3500) + + const KILL_COUNT = 4 + let killedConnections = 0 + const pubIds = {} + client = mqtt.connect({ + port: ports.PORTAND47, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300, + }) + + const server2 = net + .createServer((stream) => { + const serverClient = new Connection(stream) + serverClient.on('error', () => {}) + serverClient.on('connect', (packet) => { + if (packet.clientId === 'invalid') { + serverClient.connack({ returnCode: 2 }) + } else { + serverClient.connack({ returnCode: 0 }) + } + }) + + server2.emit('client', serverClient) + }) + .listen(ports.PORTAND47) + + server2.on('client', (serverClient) => { + client.publish('topic', 'data', { qos: 1 }, () => { + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }) + + serverClient.on('publish', function onPublish(packet) { + if (killedConnections < KILL_COUNT) { + // Kill the first few pub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + + // to avoid receiving inflight messages + serverClient.removeListener('publish', onPublish) + } else { + // Keep track of acks + if (!pubIds[packet.messageId]) { + pubIds[packet.messageId] = 0 + } + + pubIds[packet.messageId]++ + + if (pubIds[packet.messageId] > 1) { + done( + new Error( + `Multiple duplicate acked publishes received for messageId ${packet.messageId}`, + ), + ) + client.end(true) + serverClient.destroy() + server2.destroy() + } + + serverClient.puback(packet) + } + }) + }) + }) + }) + + it('check emit error on checkDisconnection w/o callback', function test(done) { + this.timeout(15000) + + const server2 = new MqttServer((c) => { + c.on('connect', (packet) => { + c.connack({ + reasonCode: 0, + }) + }) + c.on('publish', (packet) => { + setImmediate(() => { + packet.reasonCode = 0 + c.puback(packet) + }) + }) + }).listen(ports.PORTAND118) + + const opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5, + } + client = mqtt.connect(opts) + + // wait for the client to receive an error... + client.on('error', (error) => { + assert.equal(error.message, 'client disconnecting') + server2.close((err) => done(err)) + }) + client.on('connect', () => { + client.end(() => { + client._checkDisconnecting() + }) + }) + }) }) diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index 8a516cf21..fa1a61078 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -1,1128 +1,1251 @@ -'use strict' - +const { assert } = require('chai') const mqtt = require('..') const abstractClientTests = require('./abstract_client') -const MqttServer = require('./server').MqttServer -const assert = require('chai').assert -const serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +const { MqttServer } = require('./server') +const { serverBuilder } = require('./server_helpers_for_client_tests') const ports = require('./helpers/port_list') - -describe('MQTT 5.0', function () { - const server = serverBuilder('mqtt').listen(ports.PORTAND115) - const config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } - - abstractClientTests(server, config) - - it('topic should be complemented on receive', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - properties: { - topicAliasMaximum: 3 - } - } - const client = mqtt.connect(opts) - let publishCount = 0 - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - assert.strictEqual(packet.properties.topicAliasMaximum, 3) - serverClient.connack({ - reasonCode: 0 - }) - // register topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // overwrite registered topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test2', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('message', function (topic, messagee, packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 3: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - break - } - }) - }) - - it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoUseTopicAlias: true - } - const client = mqtt.connect(opts) - - let publishCount = 0 - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias by autoApplyTopicAlias - client.publish('test1', 'Message') - }) - }) - - it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoAssignTopicAlias: true - } - const client = mqtt.connect(opts) - - let publishCount = 0 - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 2) - break - case 2: - assert.strictEqual(packet.topic, 'test3') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 3: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 4: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 5: - assert.strictEqual(packet.topic, 'test4') - assert.strictEqual(packet.properties.topicAlias, 2) - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message') - client.publish('test2', 'Message') - client.publish('test3', 'Message') - - // use topicAlias - client.publish('test1', 'Message') - client.publish('test3', 'Message') - - // renew LRU topicAlias - client.publish('test4', 'Message') - }) - }) - - it('topicAlias should be removed and topic restored on resend', function (done) { - this.timeout(15000) - - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore, - outgoingStore, - clean: false, - reconnectPeriod: 100 - } - const client = mqtt.connect(opts) - - let connectCount = 0 - let publishCount = 0 - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 2: { - assert.strictEqual(packet.topic, 'test1') - let alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - serverClient.puback({ messageId: packet.messageId }) - break - } - case 3: { - assert.strictEqual(packet.topic, 'test1') - let alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - serverClient.puback({ messageId: packet.messageId }) - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - break - } - } - }) - }).listen(ports.PORTAND103) - - client.once('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('topicAlias should be removed and topic restored on offline publish', function (done) { - this.timeout(15000) - - const incomingStore = new mqtt.Store({ clean: false }) - const outgoingStore = new mqtt.Store({ clean: false }) - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore, - outgoingStore, - clean: false, - reconnectPeriod: 100 - } - const client = mqtt.connect(opts) - - let connectCount = 0 - let publishCount = 0 - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: { - assert.strictEqual(packet.topic, 'test1') - let alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - assert.strictEqual(packet.qos, 1) - serverClient.puback({ messageId: packet.messageId }) - break - } - case 1: { - assert.strictEqual(packet.topic, 'test1') - let alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - assert.strictEqual(packet.qos, 0) - break - } - case 2: { - assert.strictEqual(packet.topic, 'test1') - let alias3 - if (packet.properties) { - alias3 = packet.properties.topicAlias - } - assert.strictEqual(alias3, undefined) - assert.strictEqual(packet.qos, 0) - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - break - } - } - }) - }).listen(ports.PORTAND103) - - client.once('close', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - const client = mqtt.connect(opts) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 4 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - const client = mqtt.connect(opts) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 1 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - }) - - it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - properties: { - topicAliasMaximum: 3 - } - } - const client = mqtt.connect(opts) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 4 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - it('should throw an error if broker PUBLISH topicAlias:0', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - properties: { - topicAliasMaximum: 3 - } - } - const client = mqtt.connect(opts) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 0 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { - this.timeout(15000) - - const opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - properties: { - topicAliasMaximum: 3 - } - } - const client = mqtt.connect(opts) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: '', // use topic alias - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } // in range topic alias - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received unregistered Topic Alias') - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - it('should throw an error if there is Auth Data with no Auth Method', function (done) { - this.timeout(5000) - const opts = { host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) } } - console.log('client connecting') - const client = mqtt.connect(opts) - client.on('error', function (error) { - console.log('error hit') - assert.strictEqual(error.message, 'Packet has no Authentication Method') - // client will not be connected, so we will call done. - assert.isTrue(client.disconnected, 'validate client is disconnected') - client.end(true, done) - }) - }) - - it('auth packet', function (done) { - this.timeout(15000) - const opts = { host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {} } - const client = mqtt.connect(opts) - server.once('client', function (c) { - console.log('server received client') - c.on('auth', function (packet) { - console.log('serverClient received auth: packet %o', packet) - client.end(done) - }) - }) - console.log('calling mqtt connect') - }) - - it('Maximum Packet Size', function (done) { - this.timeout(15000) - const opts = { host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 } } - const client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'exceeding packets size connack') - client.end(true, done) - }) - }) - - it('Change values of some properties by server response', function (done) { - this.timeout(15000) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - serverKeepAlive: 16, - maximumPacketSize: 95 - } - }) - }) - }).listen(ports.PORTAND116) - const opts = { - host: 'localhost', - port: ports.PORTAND116, - protocolVersion: 5, - properties: { - topicAliasMaximum: 10, - serverKeepAlive: 11, - maximumPacketSize: 100 - } - } - const client = mqtt.connect(opts) - client.on('connect', function () { - assert.strictEqual(client.options.keepalive, 16) - assert.strictEqual(client.options.properties.maximumPacketSize, 95) - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { - this.timeout(15000) - let tryReconnect = true - let reconnectEvent = false - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - serverClient.on('subscribe', function () { - if (!tryReconnect) { - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - } - }) - }) - }).listen(ports.PORTAND316) - const opts = { - host: 'localhost', - port: ports.PORTAND316, - protocolVersion: 5 - } - const client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { - // this.timeout(15000) - let tryReconnect = true - let reconnectEvent = false - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - serverClient.on('subscribe', function (packet) { - if (!reconnectEvent) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - if (!tryReconnect) { - assert.strictEqual(packet.properties.userProperties.test, 'test') - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - } - } - }) - }).listen(ports.PORTAND326) - - const opts = { - host: 'localhost', - port: ports.PORTAND326, - protocolVersion: 5 - } - const client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - const serverThatSendsErrors = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - packet.reasonCode = 142 - delete packet.cmd - serverClient.puback(packet) - break - case 2: - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubcomp(packet) - }) - }) - - it('Subscribe properties', function (done) { - this.timeout(15000) - const opts = { - host: 'localhost', - port: ports.PORTAND119, - protocolVersion: 5 - } - const subOptions = { properties: { subscriptionIdentifier: 1234 } } - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('subscribe', function (packet) { - assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) - client.end(true, (err1) => { - server2.close((err2) => { - done(err1 || err2) - }) - }) - }) - }).listen(ports.PORTAND119) - - const client = mqtt.connect(opts) - client.on('connect', function () { - client.subscribe('a/b', subOptions) - }) - }) - - it('puback handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - const opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5 - } - const client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', { qos: 1 }, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - it('pubrec handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND118) - const opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - const client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', { qos: 2 }, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - it('puback handling custom reason code', function (done) { - // this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - const opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - let code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - - serverClient.on('puback', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - const client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', { qos: 1 }) - }) - }) - - it('server side disconnect', function (done) { - this.timeout(15000) - const server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - serverClient.disconnect({ reasonCode: 128 }) - server2.close() - }) - }) - server2.listen(ports.PORTAND327) - const opts = { - host: 'localhost', - port: ports.PORTAND327, - protocolVersion: 5 - } - - const client = mqtt.connect(opts) - client.once('disconnect', function (disconnectPacket) { - assert.strictEqual(disconnectPacket.reasonCode, 128) - client.end(true, (err) => done(err)) - }) - }) - - it('pubrec handling custom reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - const opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - let code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - - serverClient.on('pubrec', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - }) - - const client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', { qos: 1 }) - }) - }) - - it('puback handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - const opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - const code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - const client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - client.once('connect', function () { - client.subscribe('a/b', { qos: 1 }) - }) - }) - - it('pubrec handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - const opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - const code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - const client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - client.once('connect', function () { - client.subscribe('a/b', { qos: 1 }) - }) - }) - - it('puback handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - const opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - let code = 0 - if (topic === 'a/b') { - code = 124124 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - const client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for puback') - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - client.once('connect', function () { - client.subscribe('a/b', { qos: 1 }) - }) - }) - - it('pubrec handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - const opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - let code = 0 - if (topic === 'a/b') { - code = 34535 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - const client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for pubrec') - client.end(true, (err1) => { - serverThatSendsErrors.close((err2) => { - done(err1 || err2) - }) - }) - }) - client.once('connect', function () { - client.subscribe('a/b', { qos: 1 }) - }) - }) +const { close } = require('inspector') + +describe('MQTT 5.0', () => { + const server = serverBuilder('mqtt').listen(ports.PORTAND115) + const config = { + protocol: 'mqtt', + port: ports.PORTAND115, + protocolVersion: 5, + properties: { maximumPacketSize: 200 }, + } + + abstractClientTests(server, config) + + it('topic should be complemented on receive', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + properties: { + topicAliasMaximum: 3, + }, + } + const client = mqtt.connect(opts) + let publishCount = 0 + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + assert.strictEqual(packet.properties.topicAliasMaximum, 3) + serverClient.connack({ + reasonCode: 0, + }) + // register topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 }, + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 }, + }) + // overwrite registered topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test2', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 }, + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 }, + }) + }) + }).listen(ports.PORTAND103) + + client.on('message', (topic, messagee, packet) => { + switch (publishCount++) { + case 0: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 3: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + break + } + }) + }) + + it('registered topic alias should automatically used if autoUseTopicAlias is true', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoUseTopicAlias: true, + } + const client = mqtt.connect(opts) + + let publishCount = 0 + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3, + }, + }) + }) + serverClient.on('publish', (packet) => { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', () => { + // register topicAlias + client.publish('test1', 'Message', { + properties: { topicAlias: 1 }, + }) + // use topicAlias + client.publish('', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias by autoApplyTopicAlias + client.publish('test1', 'Message') + }) + }) + + it('topicAlias is automatically used if autoAssignTopicAlias is true', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoAssignTopicAlias: true, + } + const client = mqtt.connect(opts) + + let publishCount = 0 + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3, + }, + }) + }) + serverClient.on('publish', (packet) => { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 2) + break + case 2: + assert.strictEqual(packet.topic, 'test3') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 3: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 4: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 5: + assert.strictEqual(packet.topic, 'test4') + assert.strictEqual(packet.properties.topicAlias, 2) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', () => { + // register topicAlias + client.publish('test1', 'Message') + client.publish('test2', 'Message') + client.publish('test3', 'Message') + + // use topicAlias + client.publish('test1', 'Message') + client.publish('test3', 'Message') + + // renew LRU topicAlias + client.publish('test4', 'Message') + }) + }) + + it('topicAlias should be removed and topic restored on resend', function test(done) { + this.timeout(15000) + + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore, + outgoingStore, + clean: false, + reconnectPeriod: 100, + } + const client = mqtt.connect(opts) + + let connectCount = 0 + let publishCount = 0 + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3, + }, + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3, + }, + }) + break + } + }) + serverClient.on('publish', (packet) => { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + setImmediate(() => { + serverClient.stream.destroy() + }) + break + case 2: { + assert.strictEqual(packet.topic, 'test1') + let alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + serverClient.puback({ messageId: packet.messageId }) + break + } + case 3: { + assert.strictEqual(packet.topic, 'test1') + let alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + serverClient.puback({ messageId: packet.messageId }) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + break + } + } + }) + }).listen(ports.PORTAND103) + + client.once('connect', () => { + // register topicAlias + client.publish('test1', 'Message', { + qos: 1, + properties: { topicAlias: 1 }, + }) + // use topicAlias + client.publish('', 'Message', { + qos: 1, + properties: { topicAlias: 1 }, + }) + }) + }) + + it('topicAlias should be removed and topic restored on offline publish', function test(done) { + this.timeout(15000) + + const incomingStore = new mqtt.Store({ clean: false }) + const outgoingStore = new mqtt.Store({ clean: false }) + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore, + outgoingStore, + clean: false, + reconnectPeriod: 100, + } + const client = mqtt.connect(opts) + + let connectCount = 0 + let publishCount = 0 + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3, + }, + }) + setImmediate(() => { + serverClient.stream.destroy() + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3, + }, + }) + break + } + }) + serverClient.on('publish', (packet) => { + switch (publishCount++) { + case 0: { + assert.strictEqual(packet.topic, 'test1') + let alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + assert.strictEqual(packet.qos, 1) + serverClient.puback({ messageId: packet.messageId }) + break + } + case 1: { + assert.strictEqual(packet.topic, 'test1') + let alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + assert.strictEqual(packet.qos, 0) + break + } + case 2: { + assert.strictEqual(packet.topic, 'test1') + let alias3 + if (packet.properties) { + alias3 = packet.properties.topicAlias + } + assert.strictEqual(alias3, undefined) + assert.strictEqual(packet.qos, 0) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + break + } + } + }) + }).listen(ports.PORTAND103) + + client.once('close', () => { + // register topicAlias + client.publish('test1', 'Message', { + qos: 0, + properties: { topicAlias: 1 }, + }) + // use topicAlias + client.publish('', 'Message', { + qos: 0, + properties: { topicAlias: 1 }, + }) + client.publish('', 'Message', { + qos: 1, + properties: { topicAlias: 1 }, + }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + } + const client = mqtt.connect(opts) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3, + }, + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', () => { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 4 } }, + (error) => { + assert.strictEqual( + error.message, + 'Sending Topic Alias out of range', + ) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }, + ) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + } + const client = mqtt.connect(opts) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', () => { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 1 } }, + (error) => { + assert.strictEqual( + error.message, + 'Sending Topic Alias out of range', + ) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }, + ) + }) + }) + + it('should throw an error if broker PUBLISH out of range topicAlias', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + properties: { + topicAliasMaximum: 3, + }, + } + const client = mqtt.connect(opts) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 4 }, + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', (error) => { + assert.strictEqual( + error.message, + 'Received Topic Alias is out of range', + ) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + + it('should throw an error if broker PUBLISH topicAlias:0', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + properties: { + topicAliasMaximum: 3, + }, + } + const client = mqtt.connect(opts) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 0 }, + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', (error) => { + assert.strictEqual( + error.message, + 'Received Topic Alias is out of range', + ) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + + it('should throw an error if broker PUBLISH unregistered topicAlias', function test(done) { + this.timeout(15000) + + const opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + properties: { + topicAliasMaximum: 3, + }, + } + const client = mqtt.connect(opts) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: '', // use topic alias + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 }, // in range topic alias + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', (error) => { + assert.strictEqual( + error.message, + 'Received unregistered Topic Alias', + ) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + + it('should throw an error if there is Auth Data with no Auth Method', function test(done) { + this.timeout(5000) + const opts = { + host: 'localhost', + port: ports.PORTAND115, + protocolVersion: 5, + properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }, + } + console.log('client connecting') + const client = mqtt.connect(opts) + client.on('error', (error) => { + console.log('error hit') + assert.strictEqual( + error.message, + 'Packet has no Authentication Method', + ) + // client will not be connected, so we will call done. + assert.isTrue( + client.disconnected, + 'validate client is disconnected', + ) + client.end(true, done) + }) + }) + + it('auth packet', function test(done) { + this.timeout(2500) + const opts = { + host: 'localhost', + port: ports.PORTAND115, + protocolVersion: 5, + properties: { authenticationMethod: 'json' }, + authPacket: {}, + manualConnect: true, + } + let authSent = false + + const client = mqtt.connect(opts) + server.once('client', (c) => { + // this test is flaky, there is a race condition + // that could make the test fail as the auth packet + // is sent by the client even before connack so it could arrive before + // the clientServer is listening for the auth packet. To avoid this + // if the event is not emitted we simply check if + // the auth packet is sent after 1 second. + let closeTimeout = setTimeout(() => { + assert.isTrue(authSent) + closeTimeout = null + client.end(true, done) + }, 1000) + + c.on('auth', (packet) => { + if (closeTimeout) { + clearTimeout(closeTimeout) + client.end(done) + } + }) + }) + client.on('packetsend', (packet) => { + if (packet.cmd === 'auth') { + authSent = true + } + }) + + client.connect() + }) + + it('Maximum Packet Size', function test(done) { + this.timeout(15000) + const opts = { + host: 'localhost', + port: ports.PORTAND115, + protocolVersion: 5, + properties: { maximumPacketSize: 1 }, + } + const client = mqtt.connect(opts) + client.on('error', (error) => { + assert.strictEqual(error.message, 'exceeding packets size connack') + client.end(true, done) + }) + }) + + it('Change values of some properties by server response', function test(done) { + this.timeout(15000) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + properties: { + serverKeepAlive: 16, + maximumPacketSize: 95, + }, + }) + }) + }).listen(ports.PORTAND116) + const opts = { + host: 'localhost', + port: ports.PORTAND116, + protocolVersion: 5, + properties: { + topicAliasMaximum: 10, + serverKeepAlive: 11, + maximumPacketSize: 100, + }, + } + const client = mqtt.connect(opts) + client.on('connect', () => { + assert.strictEqual(client.options.keepalive, 16) + assert.strictEqual(client.options.properties.maximumPacketSize, 95) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function test(done) { + this.timeout(15000) + let tryReconnect = true + let reconnectEvent = false + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + }) + serverClient.on('subscribe', () => { + if (!tryReconnect) { + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + } + }) + }) + }).listen(ports.PORTAND316) + const opts = { + host: 'localhost', + port: ports.PORTAND316, + protocolVersion: 5, + } + const client = mqtt.connect(opts) + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', (connack) => { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', () => { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and properties', function test(done) { + // this.timeout(15000) + let tryReconnect = true + let reconnectEvent = false + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + }) + }) + serverClient.on('subscribe', (packet) => { + if (!reconnectEvent) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos), + }) + } else if (!tryReconnect) { + assert.strictEqual( + packet.properties.userProperties.test, + 'test', + ) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + } + }) + }).listen(ports.PORTAND326) + + const opts = { + host: 'localhost', + port: ports.PORTAND326, + protocolVersion: 5, + } + const client = mqtt.connect(opts) + + client.on('reconnect', () => { + reconnectEvent = true + }) + + client.on('connect', (connack) => { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe( + 'hello', + { properties: { userProperties: { test: 'test' } } }, + () => { + client.stream.end() + }, + ) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + const serverThatSendsErrors = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + }) + }) + serverClient.on('publish', (packet) => { + setImmediate(() => { + switch (packet.qos) { + case 0: + break + case 1: + packet.reasonCode = 142 + delete packet.cmd + serverClient.puback(packet) + break + case 2: + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', (packet) => { + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubcomp(packet) + }) + }) + + it('Subscribe properties', function test(done) { + this.timeout(15000) + const opts = { + host: 'localhost', + port: ports.PORTAND119, + protocolVersion: 5, + } + const subOptions = { properties: { subscriptionIdentifier: 1234 } } + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + }) + }) + serverClient.on('subscribe', (packet) => { + assert.strictEqual( + packet.properties.subscriptionIdentifier, + subOptions.properties.subscriptionIdentifier, + ) + client.end(true, (err1) => { + server2.close((err2) => { + done(err1 || err2) + }) + }) + }) + }).listen(ports.PORTAND119) + + const client = mqtt.connect(opts) + client.on('connect', () => { + client.subscribe('a/b', subOptions) + }) + }) + + it('puback handling errors check', function test(done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + const opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + } + const client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', { qos: 1 }, (err, packet) => { + assert.strictEqual( + err.message, + 'Publish error: Session taken over', + ) + assert.strictEqual(err.code, 142) + }) + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + + it('pubrec handling errors check', function test(done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND118) + const opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5, + } + const client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', { qos: 2 }, (err, packet) => { + assert.strictEqual( + err.message, + 'Publish error: Session taken over', + ) + assert.strictEqual(err.code, 142) + }) + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + + it('puback handling custom reason code', function test(done) { + // this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + const opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks(topic, message, packet, cb) { + let code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + }, + } + + serverThatSendsErrors.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: 'a/b', + payload: 'payload', + qos: 1, + messageId: 1, + }) + }) + + serverClient.on('puback', (packet) => { + assert.strictEqual(packet.reasonCode, 128) + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + + const client = mqtt.connect(opts) + client.once('connect', () => { + client.subscribe('a/b', { qos: 1 }) + }) + }) + + it('server side disconnect', function test(done) { + this.timeout(15000) + const server2 = new MqttServer((serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ + reasonCode: 0, + }) + serverClient.disconnect({ reasonCode: 128 }) + server2.close() + }) + }) + server2.listen(ports.PORTAND327) + const opts = { + host: 'localhost', + port: ports.PORTAND327, + protocolVersion: 5, + } + + const client = mqtt.connect(opts) + client.once('disconnect', (disconnectPacket) => { + assert.strictEqual(disconnectPacket.reasonCode, 128) + client.end(true, (err) => done(err)) + }) + }) + + it('pubrec handling custom reason code', function test(done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + const opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks(topic, message, packet, cb) { + let code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + }, + } + const client = mqtt.connect(opts) + client.once('connect', () => { + client.subscribe('a/b', { qos: 1 }) + }) + + serverThatSendsErrors.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: 'a/b', + payload: 'payload', + qos: 2, + messageId: 1, + }) + }) + + serverClient.on('pubrec', (packet) => { + assert.strictEqual(packet.reasonCode, 128) + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + }) + }) + + it('puback handling custom reason code with error', function test(done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + const opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks(topic, message, packet, cb) { + const code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + }, + } + + serverThatSendsErrors.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: 'a/b', + payload: 'payload', + qos: 1, + messageId: 1, + }) + }) + }) + + const client = mqtt.connect(opts) + client.on('error', (error) => { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + client.once('connect', () => { + client.subscribe('a/b', { qos: 1 }) + }) + }) + + it('pubrec handling custom reason code with error', function test(done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + const opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks(topic, message, packet, cb) { + const code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + }, + } + + serverThatSendsErrors.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: 'a/b', + payload: 'payload', + qos: 2, + messageId: 1, + }) + }) + }) + + const client = mqtt.connect(opts) + client.on('error', (error) => { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + client.once('connect', () => { + client.subscribe('a/b', { qos: 1 }) + }) + }) + + it('puback handling custom invalid reason code', function test(done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + const opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks(topic, message, packet, cb) { + let code = 0 + if (topic === 'a/b') { + code = 124124 + } + cb(code) + }, + } + + serverThatSendsErrors.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: 'a/b', + payload: 'payload', + qos: 1, + messageId: 1, + }) + }) + }) + + const client = mqtt.connect(opts) + client.on('error', (error) => { + assert.strictEqual(error.message, 'Wrong reason code for puback') + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + client.once('connect', () => { + client.subscribe('a/b', { qos: 1 }) + }) + }) + + it('pubrec handling custom invalid reason code', function test(done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + const opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks(topic, message, packet, cb) { + let code = 0 + if (topic === 'a/b') { + code = 34535 + } + cb(code) + }, + } + + serverThatSendsErrors.once('client', (serverClient) => { + serverClient.once('subscribe', () => { + serverClient.publish({ + topic: 'a/b', + payload: 'payload', + qos: 2, + messageId: 1, + }) + }) + }) + + const client = mqtt.connect(opts) + client.on('error', (error) => { + assert.strictEqual(error.message, 'Wrong reason code for pubrec') + client.end(true, (err1) => { + serverThatSendsErrors.close((err2) => { + done(err1 || err2) + }) + }) + }) + client.once('connect', () => { + client.subscribe('a/b', { qos: 1 }) + }) + }) }) diff --git a/test/helpers/port_list.js b/test/helpers/port_list.js index 27138954b..a8a0616dd 100644 --- a/test/helpers/port_list.js +++ b/test/helpers/port_list.js @@ -24,28 +24,28 @@ const PORTAND327 = PORT + 327 const PORTAND400 = PORT + 400 module.exports = { - PORT, - PORTAND40, - PORTAND41, - PORTAND42, - PORTAND43, - PORTAND44, - PORTAND45, - PORTAND46, - PORTAND47, - PORTAND48, - PORTAND49, - PORTAND50, - PORTAND72, - PORTAND103, - PORTAND114, - PORTAND115, - PORTAND116, - PORTAND117, - PORTAND118, - PORTAND119, - PORTAND316, - PORTAND326, - PORTAND327, - PORTAND400 + PORT, + PORTAND40, + PORTAND41, + PORTAND42, + PORTAND43, + PORTAND44, + PORTAND45, + PORTAND46, + PORTAND47, + PORTAND48, + PORTAND49, + PORTAND50, + PORTAND72, + PORTAND103, + PORTAND114, + PORTAND115, + PORTAND116, + PORTAND117, + PORTAND118, + PORTAND119, + PORTAND316, + PORTAND326, + PORTAND327, + PORTAND400, } diff --git a/test/helpers/server.js b/test/helpers/server.js index aae053517..112c9a8a4 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -1,53 +1,54 @@ -'use strict' - -const MqttServer = require('../server').MqttServer -const MqttSecureServer = require('../server').MqttSecureServer const fs = require('fs') +const { MqttServer } = require('../server') +const { MqttSecureServer } = require('../server') -module.exports.init_server = function (PORT) { - const server = new MqttServer(function (client) { - client.on('connect', function () { - client.connack(0) - }) +module.exports.init_server = (PORT) => { + const server = new MqttServer((client) => { + client.on('connect', () => { + client.connack(0) + }) - client.on('publish', function (packet) { - switch (packet.qos) { - case 1: - client.puback({ messageId: packet.messageId }) - break - case 2: - client.pubrec({ messageId: packet.messageId }) - break - default: - break - } - }) + client.on('publish', (packet) => { + switch (packet.qos) { + case 1: + client.puback({ messageId: packet.messageId }) + break + case 2: + client.pubrec({ messageId: packet.messageId }) + break + default: + break + } + }) - client.on('pubrel', function (packet) { - client.pubcomp({ messageId: packet.messageId }) - }) + client.on('pubrel', (packet) => { + client.pubcomp({ messageId: packet.messageId }) + }) - client.on('pingreq', function () { - client.pingresp() - }) + client.on('pingreq', () => { + client.pingresp() + }) - client.on('disconnect', function () { - client.stream.end() - }) - }) - server.listen(PORT) - return server + client.on('disconnect', () => { + client.stream.end() + }) + }) + server.listen(PORT) + return server } -module.exports.init_secure_server = function (port, key, cert) { - const server = new MqttSecureServer({ - key: fs.readFileSync(key), - cert: fs.readFileSync(cert) - }, function (client) { - client.on('connect', function () { - client.connack({ returnCode: 0 }) - }) - }) - server.listen(port) - return server +module.exports.init_secure_server = (port, key, cert) => { + const server = new MqttSecureServer( + { + key: fs.readFileSync(key), + cert: fs.readFileSync(cert), + }, + (client) => { + client.on('connect', () => { + client.connack({ returnCode: 0 }) + }) + }, + ) + server.listen(port) + return server } diff --git a/test/helpers/server_process.js b/test/helpers/server_process.js index 0875cd9da..60df9a987 100644 --- a/test/helpers/server_process.js +++ b/test/helpers/server_process.js @@ -1,9 +1,7 @@ -'use strict' +const { MqttServer } = require('../server') -const MqttServer = require('../server').MqttServer - -new MqttServer(function (client) { - client.on('connect', function () { - client.connack({ returnCode: 0 }) - }) +new MqttServer((client) => { + client.on('connect', () => { + client.connack({ returnCode: 0 }) + }) }).listen(3481, 'localhost') diff --git a/test/message-id-provider.js b/test/message-id-provider.js index 3d359def3..8ac0163ca 100644 --- a/test/message-id-provider.js +++ b/test/message-id-provider.js @@ -1,90 +1,89 @@ -'use strict' -const assert = require('chai').assert +const { assert } = require('chai') const DefaultMessageIdProvider = require('../lib/default-message-id-provider') const UniqueMessageIdProvider = require('../lib/unique-message-id-provider') -describe('message id provider', function () { - describe('default', function () { - it('should return 1 once the internal counter reached limit', function () { - const provider = new DefaultMessageIdProvider() - provider.nextId = 65535 +describe('message id provider', () => { + describe('default', () => { + it('should return 1 once the internal counter reached limit', () => { + const provider = new DefaultMessageIdProvider() + provider.nextId = 65535 - assert.equal(provider.allocate(), 65535) - assert.equal(provider.allocate(), 1) - }) + assert.equal(provider.allocate(), 65535) + assert.equal(provider.allocate(), 1) + }) - it('should return 65535 for last message id once the internal counter reached limit', function () { - const provider = new DefaultMessageIdProvider() - provider.nextId = 65535 + it('should return 65535 for last message id once the internal counter reached limit', () => { + const provider = new DefaultMessageIdProvider() + provider.nextId = 65535 - assert.equal(provider.allocate(), 65535) - assert.equal(provider.getLastAllocated(), 65535) - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - }) - it('should return true when register with non allocated messageId', function () { - const provider = new DefaultMessageIdProvider() - assert.equal(provider.register(10), true) - }) - }) - describe('unique', function () { - it('should return 1, 2, 3.., when allocate', function () { - const provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - }) - it('should skip registerd messageId', function () { - const provider = new UniqueMessageIdProvider() - assert.equal(provider.register(2), true) - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 3) - }) - it('should return false register allocated messageId', function () { - const provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.register(1), false) - assert.equal(provider.register(5), true) - assert.equal(provider.register(5), false) - }) - it('should retrun correct last messageId', function () { - const provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.register(2), true) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.allocate(), 3) - assert.equal(provider.getLastAllocated(), 3) - }) - it('should be reusable deallocated messageId', function () { - const provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - provider.deallocate(2) - assert.equal(provider.allocate(), 2) - }) - it('should allocate all messageId and then return null', function () { - const provider = new UniqueMessageIdProvider() - for (let i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.deallocate(10000) - assert.equal(provider.allocate(), 10000) - assert.equal(provider.allocate(), null) - }) - it('should all messageId reallocatable after clear', function () { - const provider = new UniqueMessageIdProvider() - for (let i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.clear() - for (let i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - }) - }) + assert.equal(provider.allocate(), 65535) + assert.equal(provider.getLastAllocated(), 65535) + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + }) + it('should return true when register with non allocated messageId', () => { + const provider = new DefaultMessageIdProvider() + assert.equal(provider.register(10), true) + }) + }) + describe('unique', () => { + it('should return 1, 2, 3.., when allocate', () => { + const provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + }) + it('should skip registerd messageId', () => { + const provider = new UniqueMessageIdProvider() + assert.equal(provider.register(2), true) + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 3) + }) + it('should return false register allocated messageId', () => { + const provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.register(1), false) + assert.equal(provider.register(5), true) + assert.equal(provider.register(5), false) + }) + it('should retrun correct last messageId', () => { + const provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.register(2), true) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.allocate(), 3) + assert.equal(provider.getLastAllocated(), 3) + }) + it('should be reusable deallocated messageId', () => { + const provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + provider.deallocate(2) + assert.equal(provider.allocate(), 2) + }) + it('should allocate all messageId and then return null', () => { + const provider = new UniqueMessageIdProvider() + for (let i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.deallocate(10000) + assert.equal(provider.allocate(), 10000) + assert.equal(provider.allocate(), null) + }) + it('should all messageId reallocatable after clear', () => { + const provider = new UniqueMessageIdProvider() + for (let i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.clear() + for (let i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + }) + }) }) diff --git a/test/mqtt.js b/test/mqtt.js index 24e84c859..aa3bc4465 100644 --- a/test/mqtt.js +++ b/test/mqtt.js @@ -1,230 +1,236 @@ -'use strict' - const fs = require('fs') const path = require('path') -const mqtt = require('../') - -describe('mqtt', function () { - describe('#connect', function () { - it('should return an MqttClient when connect is called with mqtt:/ url', function (done) { - const c = mqtt.connect('mqtt://localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) +const mqtt = require('..') - it('should throw an error when called with no protocol specified', function () { - (function () { - mqtt.connect('foo.bar.com') - }).should.throw('Missing protocol') - }) +describe('mqtt', () => { + describe('#connect', () => { + it('should return an MqttClient when connect is called with mqtt:/ url', function test(done) { + const c = mqtt.connect('mqtt://localhost:1883') - it('should throw an error when called with no protocol specified - with options', function () { - (function () { - mqtt.connect('tcp://foo.bar.com', { protocol: null }) - }).should.throw('Missing protocol') - }) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - it('should return an MqttClient with username option set', function (done) { - const c = mqtt.connect('mqtt://user:pass@localhost:1883') + it('should throw an error when called with no protocol specified', () => { + ;(() => { + mqtt.connect('foo.bar.com') + }).should.throw('Missing protocol') + }) - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.options.should.have.property('password', 'pass') - c.end((err) => done(err)) - }) + it('should throw an error when called with no protocol specified - with options', () => { + ;(() => { + mqtt.connect('tcp://foo.bar.com', { protocol: null }) + }).should.throw('Missing protocol') + }) - it('should return an MqttClient with username and password options set', function (done) { - const c = mqtt.connect('mqtt://user@localhost:1883') + it('should return an MqttClient with username option set', function test(done) { + const c = mqtt.connect('mqtt://user:pass@localhost:1883') - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.end((err) => done(err)) - }) + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.options.should.have.property('password', 'pass') + c.end((err) => done(err)) + }) - it('should return an MqttClient with the clientid with random value', function (done) { - const c = mqtt.connect('mqtt://user@localhost:1883') + it('should return an MqttClient with username and password options set', function test(done) { + const c = mqtt.connect('mqtt://user@localhost:1883') - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end((err) => done(err)) - }) + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.end((err) => done(err)) + }) - it('should return an MqttClient with the clientid with empty string', function (done) { - const c = mqtt.connect('mqtt://user@localhost:1883?clientId=') + it('should return an MqttClient with the clientid with random value', function test(done) { + const c = mqtt.connect('mqtt://user@localhost:1883') - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - c.end((err) => done(err)) - }) + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end((err) => done(err)) + }) - it('should return an MqttClient with the clientid option set', function (done) { - const c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') + it('should return an MqttClient with the clientid with empty string', function test(done) { + const c = mqtt.connect('mqtt://user@localhost:1883?clientId=') - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end((err) => done(err)) - }) + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + c.end((err) => done(err)) + }) + + it('should return an MqttClient with the clientid option set', function test(done) { + const c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') - it('should return an MqttClient when connect is called with tcp:/ url', function (done) { - const c = mqtt.connect('tcp://localhost') + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end((err) => done(err)) + }) - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + it('should return an MqttClient when connect is called with tcp:/ url', function test(done) { + const c = mqtt.connect('tcp://localhost') - it('should return an MqttClient with correct host when called with a host and port', function (done) { - const c = mqtt.connect('tcp://user:pass@localhost:1883') + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.options.should.have.property('hostname', 'localhost') - c.options.should.have.property('port', 1883) - c.end((err) => done(err)) - }) + it('should return an MqttClient with correct host when called with a host and port', function test(done) { + const c = mqtt.connect('tcp://user:pass@localhost:1883') - const sslOpts = { - keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), - certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), - caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')] - } + c.options.should.have.property('hostname', 'localhost') + c.options.should.have.property('port', 1883) + c.end((err) => done(err)) + }) - it('should return an MqttClient when connect is called with mqtts:/ url', function (done) { - const c = mqtt.connect('mqtts://localhost', sslOpts) + const sslOpts = { + keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), + certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), + caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')], + } - c.options.should.have.property('protocol', 'mqtts') + it('should return an MqttClient when connect is called with mqtts:/ url', function test(done) { + const c = mqtt.connect('mqtts://localhost', sslOpts) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'mqtts') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - it('should return an MqttClient when connect is called with ssl:/ url', function (done) { - const c = mqtt.connect('ssl://localhost', sslOpts) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.options.should.have.property('protocol', 'ssl') + it('should return an MqttClient when connect is called with ssl:/ url', function test(done) { + const c = mqtt.connect('ssl://localhost', sslOpts) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'ssl') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - it('should return an MqttClient when connect is called with ws:/ url', function (done) { - const c = mqtt.connect('ws://localhost', sslOpts) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.options.should.have.property('protocol', 'ws') + it('should return an MqttClient when connect is called with ws:/ url', function test(done) { + const c = mqtt.connect('ws://localhost', sslOpts) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'ws') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - it('should return an MqttClient when connect is called with wss:/ url', function (done) { - const c = mqtt.connect('wss://localhost', sslOpts) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.options.should.have.property('protocol', 'wss') + it('should return an MqttClient when connect is called with wss:/ url', function test(done) { + const c = mqtt.connect('wss://localhost', sslOpts) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'wss') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - const sslOpts2 = { - key: fs.readFileSync(path.join(__dirname, 'helpers', 'private-key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem')), - ca: [fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem'))] - } + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - it('should throw an error when it is called with cert and key set but no protocol specified', function () { - // to do rewrite wrap function - (function () { - mqtt.connect(sslOpts2) - }).should.throw('Missing secure protocol key') - }) + const sslOpts2 = { + key: fs.readFileSync( + path.join(__dirname, 'helpers', 'private-key.pem'), + ), + cert: fs.readFileSync( + path.join(__dirname, 'helpers', 'public-cert.pem'), + ), + ca: [ + fs.readFileSync( + path.join(__dirname, 'helpers', 'public-cert.pem'), + ), + ], + } - it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', function () { - (function () { - sslOpts2.protocol = 'UNKNOWNPROTOCOL' - mqtt.connect(sslOpts2) - }).should.throw() - }) + it('should throw an error when it is called with cert and key set but no protocol specified', () => { + // to do rewrite wrap function + ;(() => { + mqtt.connect(sslOpts2) + }).should.throw('Missing secure protocol key') + }) - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function (done) { - sslOpts2.protocol = 'mqtt' - const c = mqtt.connect(sslOpts2) + it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', () => { + ;(() => { + sslOpts2.protocol = 'UNKNOWNPROTOCOL' + mqtt.connect(sslOpts2) + }).should.throw() + }) - c.options.should.have.property('protocol', 'mqtts') + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function test(done) { + sslOpts2.protocol = 'mqtt' + const c = mqtt.connect(sslOpts2) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'mqtts') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function (done) { - sslOpts2.protocol = 'mqtts' - const c = mqtt.connect(sslOpts2) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.options.should.have.property('protocol', 'mqtts') + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function test(done) { + sslOpts2.protocol = 'mqtts' + const c = mqtt.connect(sslOpts2) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'mqtts') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function (done) { - sslOpts2.protocol = 'ws' - const c = mqtt.connect(sslOpts2) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.options.should.have.property('protocol', 'wss') + it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function test(done) { + sslOpts2.protocol = 'ws' + const c = mqtt.connect(sslOpts2) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'wss') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function (done) { - sslOpts2.protocol = 'wss' - const c = mqtt.connect(sslOpts2) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.options.should.have.property('protocol', 'wss') + it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function test(done) { + sslOpts2.protocol = 'wss' + const c = mqtt.connect(sslOpts2) - c.on('error', function () {}) + c.options.should.have.property('protocol', 'wss') - c.should.be.instanceOf(mqtt.MqttClient) - c.end((err) => done(err)) - }) + c.on('error', () => {}) - it('should return an MqttClient with the clientid with option of clientId as empty string', function (done) { - const c = mqtt.connect('mqtt://localhost:1883', { - clientId: '' - }) + c.should.be.instanceOf(mqtt.MqttClient) + c.end((err) => done(err)) + }) - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - c.end((err) => done(err)) - }) + it('should return an MqttClient with the clientid with option of clientId as empty string', function test(done) { + const c = mqtt.connect('mqtt://localhost:1883', { + clientId: '', + }) - it('should return an MqttClient with the clientid with option of clientId empty', function (done) { - const c = mqtt.connect('mqtt://localhost:1883') + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + c.end((err) => done(err)) + }) - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end((err) => done(err)) - }) + it('should return an MqttClient with the clientid with option of clientId empty', function test(done) { + const c = mqtt.connect('mqtt://localhost:1883') - it('should return an MqttClient with the clientid with option of with specific clientId', function (done) { - const c = mqtt.connect('mqtt://localhost:1883', { - clientId: '123' - }) + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end((err) => done(err)) + }) - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end((err) => done(err)) - }) - }) + it('should return an MqttClient with the clientid with option of with specific clientId', function test(done) { + const c = mqtt.connect('mqtt://localhost:1883', { + clientId: '123', + }) + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end((err) => done(err)) + }) + }) }) diff --git a/test/mqtt_store.js b/test/mqtt_store.js index 9fa7e915d..d5c6dff40 100644 --- a/test/mqtt_store.js +++ b/test/mqtt_store.js @@ -1,11 +1,9 @@ -'use strict' - const mqtt = require('../lib/connect') -describe('store in lib/connect/index.js (webpack entry point)', function () { - it('should create store', function (done) { - const store = new mqtt.Store() - store.should.be.instanceOf(mqtt.Store) - done() - }) +describe('store in lib/connect/index.js (webpack entry point)', () => { + it('should create store', function test(done) { + const store = new mqtt.Store() + store.should.be.instanceOf(mqtt.Store) + done() + }) }) diff --git a/test/secure_client.js b/test/secure_client.js index d63902258..630408269 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -1,185 +1,187 @@ -'use strict' - -const mqtt = require('..') const path = require('path') -const abstractClientTests = require('./abstract_client') const fs = require('fs') +const mqtt = require('..') +const abstractClientTests = require('./abstract_client') + const port = 9899 const KEY = path.join(__dirname, 'helpers', 'tls-key.pem') const CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') const WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') -const MqttSecureServer = require('./server').MqttSecureServer -const assert = require('chai').assert - -const serverListener = function (client) { - // this is the Server's MQTT Client - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({ returnCode: 2 }) - } else { - server.emit('connect', client) - client.connack({ returnCode: 0 }) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - /* jshint -W027 */ - /* eslint default-case:0 */ - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - /* jshint +W027 */ - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) +const { MqttSecureServer } = require('./server') +const { assert } = require('chai') + +const serverListener = (client) => { + // this is the Server's MQTT Client + client.on('connect', (packet) => { + if (packet.clientId === 'invalid') { + client.connack({ returnCode: 2 }) + } else { + server.emit('connect', client) + client.connack({ returnCode: 0 }) + } + }) + + client.on('publish', (packet) => { + setImmediate(() => { + /* jshint -W027 */ + /* eslint default-case:0 */ + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + /* jshint +W027 */ + }) + }) + + client.on('pubrel', (packet) => { + client.pubcomp(packet) + }) + + client.on('pubrec', (packet) => { + client.pubrel(packet) + }) + + client.on('pubcomp', () => { + // Nothing to be done + }) + + client.on('subscribe', (packet) => { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos), + }) + }) + + client.on('unsubscribe', (packet) => { + client.unsuback(packet) + }) + + client.on('pingreq', () => { + client.pingresp() + }) } -const server = new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) -}, serverListener).listen(port) - -describe('MqttSecureClient', function () { - const config = { protocol: 'mqtts', port, rejectUnauthorized: false } - abstractClientTests(server, config) - - describe('with secure parameters', function () { - it('should validate successfully the CA', function (done) { - const client = mqtt.connect({ - protocol: 'mqtts', - port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI', function (done) { - const client = mqtt.connect('mqtts://localhost:' + port, { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI with path', function (done) { - const client = mqtt.connect('mqtts://localhost:' + port + '/', { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate unsuccessfully the CA', function (done) { - const client = mqtt.connect({ - protocol: 'mqtts', - port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.once('error', function (err) { - err.should.be.instanceOf(Error) - client.end((err) => done(err)) - }) - }) - - it('should emit close on TLS error', function (done) { - const client = mqtt.connect({ - protocol: 'mqtts', - port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.on('error', function () {}) - - client.once('close', function () { - client.end((err) => done(err)) - }) - }) - - it('should support SNI on the TLS connection', function (done) { - server.removeAllListeners('secureConnection') // clear eventHandler - server.once('secureConnection', function (tlsSocket) { // one time eventHandler - assert.equal(tlsSocket.servername, hostname) // validate SNI set - server.setupConnection(tlsSocket) - }) - - const hostname = 'localhost' - const client = mqtt.connect({ - protocol: 'mqtts', - port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true, - host: hostname - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - server.on('secureConnection', server.setupConnection) // reset eventHandler - client.end((err) => done(err)) - }) - }) - }) +const server = new MqttSecureServer( + { + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT), + }, + serverListener, +).listen(port) + +describe('MqttSecureClient', () => { + const config = { protocol: 'mqtts', port, rejectUnauthorized: false } + abstractClientTests(server, config) + + describe('with secure parameters', () => { + it('should validate successfully the CA', function test(done) { + const client = mqtt.connect({ + protocol: 'mqtts', + port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + }) + + client.on('error', (err) => { + done(err) + }) + + server.once('connect', () => { + done() + }) + }) + + it('should validate successfully the CA using URI', function test(done) { + const client = mqtt.connect(`mqtts://localhost:${port}`, { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + }) + + client.on('error', (err) => { + done(err) + }) + + server.once('connect', () => { + done() + }) + }) + + it('should validate successfully the CA using URI with path', function test(done) { + const client = mqtt.connect(`mqtts://localhost:${port}/`, { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + }) + + client.on('error', (err) => { + done(err) + }) + + server.once('connect', () => { + done() + }) + }) + + it('should validate unsuccessfully the CA', function test(done) { + const client = mqtt.connect({ + protocol: 'mqtts', + port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true, + }) + + client.once('error', (err) => { + err.should.be.instanceOf(Error) + client.end((err2) => done(err2)) + }) + }) + + it('should emit close on TLS error', function test(done) { + const client = mqtt.connect({ + protocol: 'mqtts', + port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true, + }) + + client.on('error', () => {}) + + client.once('close', () => { + client.end((err) => done(err)) + }) + }) + + it('should support SNI on the TLS connection', function test(done) { + const hostname = 'localhost' + + server.removeAllListeners('secureConnection') // clear eventHandler + server.once('secureConnection', (tlsSocket) => { + // one time eventHandler + assert.equal(tlsSocket.servername, hostname) // validate SNI set + server.setupConnection(tlsSocket) + }) + + const client = mqtt.connect({ + protocol: 'mqtts', + port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + host: hostname, + }) + + client.on('error', (err) => { + done(err) + }) + + server.once('connect', () => { + server.on('secureConnection', server.setupConnection) // reset eventHandler + client.end((err) => done(err)) + }) + }) + }) }) diff --git a/test/server.js b/test/server.js index a190444e5..8a3732eed 100644 --- a/test/server.js +++ b/test/server.js @@ -1,5 +1,3 @@ -'use strict' - const net = require('net') const tls = require('tls') const Connection = require('mqtt-connection') @@ -10,22 +8,21 @@ const Connection = require('mqtt-connection') * @param {Function} listener - fired on client connection */ class MqttServer extends net.Server { - constructor (listener) { - super() - this.connectionList = [] + constructor(listener) { + super() + this.connectionList = [] - const that = this - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - const connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - }) + this.on('connection', (duplex) => { + this.connectionList.push(duplex) + const connection = new Connection(duplex, () => { + this.emit('client', connection) + }) + }) - if (listener) { - this.on('client', listener) - } - } + if (listener) { + this.on('client', listener) + } + } } /** @@ -34,21 +31,21 @@ class MqttServer extends net.Server { * @param {Function} listener - fired on client connection */ class MqttServerNoWait extends net.Server { - constructor (listener) { - super() - this.connectionList = [] + constructor(listener) { + super() + this.connectionList = [] - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - const connection = new Connection(duplex) - // do not wait for connection to return to send it to the client. - this.emit('client', connection) - }) + this.on('connection', (duplex) => { + this.connectionList.push(duplex) + const connection = new Connection(duplex) + // do not wait for connection to return to send it to the client. + this.emit('client', connection) + }) - if (listener) { - this.on('client', listener) - } - } + if (listener) { + this.on('client', listener) + } + } } /** @@ -58,35 +55,33 @@ class MqttServerNoWait extends net.Server { * @param {Function} listener */ class MqttSecureServer extends tls.Server { - constructor (opts, listener) { - if (typeof opts === 'function') { - listener = opts - opts = {} - } + constructor(opts, listener) { + if (typeof opts === 'function') { + listener = opts + opts = {} + } - // sets a listener for the 'connection' event - super(opts) - this.connectionList = [] + // sets a listener for the 'connection' event + super(opts) + this.connectionList = [] - this.on('secureConnection', function (socket) { - this.connectionList.push(socket) - const that = this - const connection = new Connection(socket, function () { - that.emit('client', connection) - }) - }) + this.on('secureConnection', (socket) => { + this.connectionList.push(socket) + const connection = new Connection(socket, () => { + this.emit('client', connection) + }) + }) - if (listener) { - this.on('client', listener) - } - } + if (listener) { + this.on('client', listener) + } + } - setupConnection (duplex) { - const that = this - const connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - } + setupConnection(duplex) { + const connection = new Connection(duplex, () => { + this.emit('client', connection) + }) + } } exports.MqttServer = MqttServer diff --git a/test/server_helpers_for_client_tests.js b/test/server_helpers_for_client_tests.js index 647aacb0b..c1a9685b3 100644 --- a/test/server_helpers_for_client_tests.js +++ b/test/server_helpers_for_client_tests.js @@ -1,17 +1,16 @@ -'use strict' - -const MqttServer = require('./server').MqttServer -const MqttSecureServer = require('./server').MqttSecureServer +const { MqttServer } = require('./server') const debug = require('debug')('TEST:server_helpers') const path = require('path') const fs = require('fs') + const KEY = path.join(__dirname, 'helpers', 'tls-key.pem') const CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') const http = require('http') const WebSocket = require('ws') const MQTTConnection = require('mqtt-connection') +const { MqttSecureServer } = require('./server') /** * This will build the client for the server to use during testing, and set up the @@ -19,131 +18,135 @@ const MQTTConnection = require('mqtt-connection') * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' * @param {Function} handler - event handler */ -function serverBuilder (protocol, handler) { - const defaultHandler = function (serverClient) { - serverClient.on('auth', function (packet) { - if (serverClient.writable) return false - const rc = 'reasonCode' - const connack = {} - connack[rc] = 0 - serverClient.connack(connack) - }) - serverClient.on('connect', function (packet) { - if (!serverClient.writable) return false - let rc = 'returnCode' - const connack = {} - if (serverClient.options && serverClient.options.protocolVersion === 5) { - rc = 'reasonCode' - if (packet.clientId === 'invalid') { - connack[rc] = 128 - } else { - connack[rc] = 0 - } - } else { - if (packet.clientId === 'invalid') { - connack[rc] = 2 - } else { - connack[rc] = 0 - } - } - if (packet.properties && packet.properties.authenticationMethod) { - return false - } else { - serverClient.connack(connack) - } - }) - - serverClient.on('publish', function (packet) { - if (!serverClient.writable) return false - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - serverClient.puback(packet) - break - case 2: - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - if (!serverClient.writable) return false - serverClient.pubcomp(packet) - }) - - serverClient.on('pubrec', function (packet) { - if (!serverClient.writable) return false - serverClient.pubrel(packet) - }) - - serverClient.on('pubcomp', function () { - // Nothing to be done - }) - - serverClient.on('subscribe', function (packet) { - if (!serverClient.writable) return false - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - serverClient.on('unsubscribe', function (packet) { - if (!serverClient.writable) return false - packet.granted = packet.unsubscriptions.map(function () { return 0 }) - serverClient.unsuback(packet) - }) - - serverClient.on('pingreq', function () { - if (!serverClient.writable) return false - serverClient.pingresp() - }) - - serverClient.on('end', function () { - debug('disconnected from server') - }) - } - - if (!handler) { - handler = defaultHandler - } - - if ( - protocol === 'mqtt') { - return new MqttServer(handler) - } else if ( - protocol === 'mqtts') { - return new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) - }, - handler) - } else if ( - protocol === 'ws') { - const attachWebsocketServer = function (server) { - const webSocketServer = new WebSocket.Server({ server, perMessageDeflate: false }) - - webSocketServer.on('connection', function (ws) { - const stream = WebSocket.createWebSocketStream(ws) - const connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - server.emit('client', connection) - stream.on('error', function () {}) - connection.on('error', function () {}) - connection.on('close', function () {}) - }) - } - - const httpServer = http.createServer() - attachWebsocketServer(httpServer) - httpServer.on('client', handler) - return httpServer - } +function serverBuilder(protocol, handler) { + const defaultHandler = (serverClient) => { + serverClient.on('auth', (packet) => { + if (serverClient.writable) return false + const rc = 'reasonCode' + const connack = {} + connack[rc] = 0 + serverClient.connack(connack) + }) + serverClient.on('connect', (packet) => { + if (!serverClient.writable) return false + let rc = 'returnCode' + const connack = {} + if ( + serverClient.options && + serverClient.options.protocolVersion === 5 + ) { + rc = 'reasonCode' + if (packet.clientId === 'invalid') { + connack[rc] = 128 + } else { + connack[rc] = 0 + } + } else if (packet.clientId === 'invalid') { + connack[rc] = 2 + } else { + connack[rc] = 0 + } + if (packet.properties && packet.properties.authenticationMethod) { + return false + } + serverClient.connack(connack) + }) + + serverClient.on('publish', (packet) => { + if (!serverClient.writable) return false + setImmediate(() => { + switch (packet.qos) { + case 0: + break + case 1: + serverClient.puback(packet) + break + case 2: + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', (packet) => { + if (!serverClient.writable) return false + serverClient.pubcomp(packet) + }) + + serverClient.on('pubrec', (packet) => { + if (!serverClient.writable) return false + serverClient.pubrel(packet) + }) + + serverClient.on('pubcomp', () => { + // Nothing to be done + }) + + serverClient.on('subscribe', (packet) => { + if (!serverClient.writable) return false + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos), + }) + }) + + serverClient.on('unsubscribe', (packet) => { + if (!serverClient.writable) return false + packet.granted = packet.unsubscriptions.map(() => 0) + serverClient.unsuback(packet) + }) + + serverClient.on('pingreq', () => { + if (!serverClient.writable) return false + serverClient.pingresp() + }) + + serverClient.on('end', () => { + debug('disconnected from server') + }) + } + + if (!handler) { + handler = defaultHandler + } + + if (protocol === 'mqtt') { + return new MqttServer(handler) + } + if (protocol === 'mqtts') { + return new MqttSecureServer( + { + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT), + }, + handler, + ) + } + if (protocol === 'ws') { + const attachWebsocketServer = (server) => { + const webSocketServer = new WebSocket.Server({ + server, + perMessageDeflate: false, + }) + + webSocketServer.on('connection', (ws) => { + server.connectionList.push(ws) + const stream = WebSocket.createWebSocketStream(ws) + const connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + server.emit('client', connection) + stream.on('error', () => {}) + connection.on('error', () => {}) + connection.on('close', () => {}) + }) + } + + const httpServer = http.createServer() + httpServer.connectionList = [] + attachWebsocketServer(httpServer) + httpServer.on('client', handler) + return httpServer + } } exports.serverBuilder = serverBuilder diff --git a/test/store.js b/test/store.js index f97fe72cc..6788039fe 100644 --- a/test/store.js +++ b/test/store.js @@ -1,10 +1,8 @@ -'use strict' - const Store = require('../lib/store') -const abstractTest = require('../test/abstract_store') +const abstractTest = require('./abstract_store') -describe('in-memory store', function () { - abstractTest(function (done) { - done(null, new Store()) - }) +describe('in-memory store', () => { + abstractTest(function test(done) { + done(null, new Store()) + }) }) diff --git a/test/unique_message_id_provider_client.js b/test/unique_message_id_provider_client.js index 8853583c3..c67d99b9f 100644 --- a/test/unique_message_id_provider_client.js +++ b/test/unique_message_id_provider_client.js @@ -1,21 +1,23 @@ -'use strict' - const abstractClientTests = require('./abstract_client') -const serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +const { serverBuilder } = require('./server_helpers_for_client_tests') const UniqueMessageIdProvider = require('../lib/unique-message-id-provider') const ports = require('./helpers/port_list') -describe('UniqueMessageIdProviderMqttClient', function () { - const server = serverBuilder('mqtt') - const config = { protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider() } - server.listen(ports.PORTAND400) +describe('UniqueMessageIdProviderMqttClient', () => { + const server = serverBuilder('mqtt') + const config = { + protocol: 'mqtt', + port: ports.PORTAND400, + messageIdProvider: new UniqueMessageIdProvider(), + } + server.listen(ports.PORTAND400) - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) + after(() => { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) - abstractClientTests(server, config) + abstractClientTests(server, config) }) diff --git a/test/util.js b/test/util.js index f0b8d0f9d..922c2b334 100644 --- a/test/util.js +++ b/test/util.js @@ -1,15 +1,12 @@ -'use strict' +const { Transform } = require('readable-stream') -const Transform = require('readable-stream').Transform - -module.exports.testStream = function () { - return new Transform({ - transform (buf, enc, cb) { - const that = this - setImmediate(function () { - that.push(buf) - cb() - }) - } - }) +module.exports.testStream = () => { + return new Transform({ + transform(buf, enc, cb) { + setImmediate(() => { + this.push(buf) + cb() + }) + }, + }) } diff --git a/test/websocket_client.js b/test/websocket_client.js index 0ff253fb4..06516ca4c 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -1,193 +1,214 @@ -'use strict' - const http = require('http') const WebSocket = require('ws') const MQTTConnection = require('mqtt-connection') +const assert = require('assert') const abstractClientTests = require('./abstract_client') const ports = require('./helpers/port_list') -const MqttServerNoWait = require('./server').MqttServerNoWait -const mqtt = require('../') -const assert = require('assert') +const { MqttServerNoWait } = require('./server') +const mqtt = require('..') + const port = 9999 const httpServer = http.createServer() -function attachWebsocketServer (httpServer) { - const webSocketServer = new WebSocket.Server({ server: httpServer, perMessageDeflate: false }) - - webSocketServer.on('connection', function (ws) { - const stream = WebSocket.createWebSocketStream(ws) - const connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - httpServer.emit('client', connection) - stream.on('error', function () { }) - connection.on('error', function () { }) - }) - - return httpServer +function attachWebsocketServer(httpServer2) { + const webSocketServer = new WebSocket.Server({ + server: httpServer2, + perMessageDeflate: false, + }) + + webSocketServer.on('connection', (ws) => { + const stream = WebSocket.createWebSocketStream(ws) + const connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + httpServer2.emit('client', connection) + stream.on('error', () => {}) + connection.on('error', () => {}) + }) + + return httpServer2 } -function attachClientEventHandlers (client) { - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({ returnCode: 2 }) - } else { - httpServer.emit('connect', client) - client.connack({ returnCode: 0 }) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) +function attachClientEventHandlers(client) { + client.on('connect', (packet) => { + if (packet.clientId === 'invalid') { + client.connack({ returnCode: 2 }) + } else { + httpServer.emit('connect', client) + client.connack({ returnCode: 0 }) + } + }) + + client.on('publish', (packet) => { + setImmediate(() => { + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + }) + }) + + client.on('pubrel', (packet) => { + client.pubcomp(packet) + }) + + client.on('pubrec', (packet) => { + client.pubrel(packet) + }) + + client.on('pubcomp', () => { + // Nothing to be done + }) + + client.on('subscribe', (packet) => { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map((e) => e.qos), + }) + }) + + client.on('unsubscribe', (packet) => { + client.unsuback(packet) + }) + + client.on('pingreq', () => { + client.pingresp() + }) } attachWebsocketServer(httpServer) httpServer.on('client', attachClientEventHandlers).listen(port) -describe('Websocket Client', function () { - const baseConfig = { protocol: 'ws', port } - - function makeOptions (custom) { - return { ...baseConfig, ...(custom || {}) } - } - - it('should use mqtt as the protocol by default', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqtt') - }) - mqtt.connect(makeOptions()).on('connect', function () { - this.end(true, (err) => done(err)) - }) - }) - - it('should be able to transform the url (for e.g. to sign it)', function (done) { - const baseUrl = 'ws://localhost:9999/mqtt' - const sig = '?AUTH=token' - const expected = baseUrl + sig - let actual - const opts = makeOptions({ - path: '/mqtt', - transformWsUrl: function (url, opt, client) { - assert.equal(url, baseUrl) - assert.strictEqual(opt, opts) - assert.strictEqual(client.options, opts) - assert.strictEqual(typeof opt.transformWsUrl, 'function') - assert(client instanceof mqtt.MqttClient) - url += sig - actual = url - return url - } - }) - mqtt.connect(opts) - .on('connect', function () { - assert.equal(this.stream.url, expected) - assert.equal(actual, expected) - this.end(true, (err) => done(err)) - }) - }) - - it('should use mqttv3.1 as the protocol if using v3.1', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqttv3.1') - }) - - const opts = makeOptions({ - protocolId: 'MQIsdp', - protocolVersion: 3 - }) - - mqtt.connect(opts).on('connect', function () { - this.end(true, (err) => done(err)) - }) - }) - - describe('reconnecting', () => { - it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { - let serverPort42Connected = false - const handler = function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ returnCode: 0 }) - }) - } - this.timeout(15000) - const actualURL41 = 'wss://localhost:9917/' - const actualURL42 = 'ws://localhost:9918/' - const serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) - const serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) - - serverPort42.on('listening', function () { - const client = mqtt.connect({ - protocol: 'wss', - servers: [ - { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, - { port: ports.PORTAND41, host: 'localhost' } - ], - keepalive: 50 - }) - serverPort41.once('client', function (c) { - assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') - assert(serverPort42Connected) - c.stream.destroy() - client.end(true, (err1) => { - serverPort41.close((err2) => { - done(err1 || err2) - }) - }) - }) - serverPort42.once('client', function (c) { - serverPort42Connected = true - assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') - c.stream.destroy() - serverPort42.close() - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - }) - - abstractClientTests(httpServer, makeOptions()) +describe('Websocket Client', () => { + const baseConfig = { protocol: 'ws', port } + + function makeOptions(custom) { + return { ...baseConfig, ...(custom || {}) } + } + + it('should use mqtt as the protocol by default', function test(done) { + httpServer.once('client', (client) => { + assert.strictEqual(client.protocol, 'mqtt') + }) + const client = mqtt.connect(makeOptions()) + + client.on('connect', () => { + client.end(true, (err) => done(err)) + }) + }) + + it('should be able to transform the url (for e.g. to sign it)', function test(done) { + const baseUrl = 'ws://localhost:9999/mqtt' + const sig = '?AUTH=token' + const expected = baseUrl + sig + let actual + const opts = makeOptions({ + path: '/mqtt', + transformWsUrl(url, opt, client) { + assert.equal(url, baseUrl) + assert.strictEqual(opt, opts) + assert.strictEqual(client.options, opts) + assert.strictEqual(typeof opt.transformWsUrl, 'function') + assert(client instanceof mqtt.MqttClient) + url += sig + actual = url + return url + }, + }) + const client = mqtt.connect(opts) + + client.on('connect', () => { + assert.equal(client.stream.url, expected) + assert.equal(actual, expected) + client.end(true, (err) => done(err)) + }) + }) + + it('should use mqttv3.1 as the protocol if using v3.1', function test(done) { + httpServer.once('client', (client) => { + assert.strictEqual(client.protocol, 'mqttv3.1') + }) + + const opts = makeOptions({ + protocolId: 'MQIsdp', + protocolVersion: 3, + }) + + const client = mqtt.connect(opts) + + client.on('connect', () => { + client.end(true, (err) => done(err)) + }) + }) + + describe('reconnecting', () => { + it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function test(done) { + let serverPort42Connected = false + const handler = (serverClient) => { + serverClient.on('connect', (packet) => { + serverClient.connack({ returnCode: 0 }) + }) + } + this.timeout(15000) + const actualURL41 = 'wss://localhost:9917/' + const actualURL42 = 'ws://localhost:9918/' + const serverPort41 = new MqttServerNoWait(handler).listen( + ports.PORTAND41, + ) + const serverPort42 = new MqttServerNoWait(handler).listen( + ports.PORTAND42, + ) + + serverPort42.on('listening', () => { + const client = mqtt.connect({ + protocol: 'wss', + servers: [ + { + port: ports.PORTAND42, + host: 'localhost', + protocol: 'ws', + }, + { port: ports.PORTAND41, host: 'localhost' }, + ], + keepalive: 50, + }) + serverPort41.once('client', (c) => { + assert.equal( + client.stream.url, + actualURL41, + 'Protocol for second client should use the default protocol: wss, on port: port + 41.', + ) + assert(serverPort42Connected) + c.stream.destroy() + client.end(true, (err1) => { + serverPort41.close((err2) => { + done(err1 || err2) + }) + }) + }) + serverPort42.once('client', (c) => { + serverPort42Connected = true + assert.equal( + client.stream.url, + actualURL42, + 'Protocol for connection should use ws, on port: port + 42.', + ) + c.stream.destroy() + serverPort42.close() + }) + + client.once('connect', () => { + client.stream.destroy() + }) + }) + }) + }) + + abstractClientTests(httpServer, makeOptions()) }) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index f4408fd3d..c71ea344f 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -107,7 +107,12 @@ export interface IClientOptions extends ISecureClientOptions { responseTopic?: string, correlationData?: Buffer, userProperties?: UserProperties - } + }, + + authPacket?: any + + /** Prevent to call `connect` in constructor */ + manualConnect?: boolean } transformWsUrl?: (url: string, options: IClientOptions, client: MqttClient) => string, properties?: { diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts index 2914392ee..d19c5febc 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -121,6 +121,11 @@ export declare class MqttClient extends events.EventEmitter { public once (event: 'end' | 'reconnect' | 'offline' | 'outgoingEmpty', cb: () => void): this public once (event: string, cb: Function): this + /** + * Setup the event handlers in the inner stream, sends `connect` and `auth` packets + */ + public connect(): this + /** * publish - publish to *