Repository: mqttjs/MQTT.js Branch: main Commit: 280a3119d8f1 Files: 133 Total size: 535.2 KB Directory structure: gitextract__ue8z3qn/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.md │ └── workflows/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.md │ ├── browser-tests.yml │ ├── electron-tests.yml │ ├── mqttjs-test.yml │ ├── release-it.yml │ ├── semantic-pr.yml │ └── stale.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .release-it.json ├── CHANGELOG.OLD.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE.md ├── README.md ├── benchmarks/ │ ├── bombing.js │ └── throughputCounter.js ├── electron-test/ │ ├── .gitignore │ ├── README │ ├── forge.config.js │ ├── package.json │ ├── src/ │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── renderer.js │ ├── test/ │ │ ├── service/ │ │ │ └── server_launcher.ts │ │ └── specs/ │ │ └── test.e2e.ts │ ├── tsconfig.json │ └── wdio.conf.ts ├── esbuild.js ├── eslint.config.js ├── example.ts ├── examples/ │ ├── client/ │ │ ├── secure-client.js │ │ ├── simple-both.js │ │ ├── simple-publish.js │ │ └── simple-subscribe.js │ ├── tls client/ │ │ ├── crt.ca.cg.pem │ │ ├── mqttclient.js │ │ ├── tls-cert.pem │ │ └── tls-key.pem │ ├── vite-example/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── assets/ │ │ │ │ ├── base.css │ │ │ │ └── main.css │ │ │ └── main.js │ │ └── vite.config.js │ ├── ws/ │ │ ├── aedes_server.js │ │ └── client.js │ └── wss/ │ └── client_with_proxy.js ├── help/ │ ├── help.txt │ ├── publish.txt │ └── subscribe.txt ├── nyc.config.js ├── package.json ├── src/ │ ├── bin/ │ │ ├── mqtt.ts │ │ ├── pub.ts │ │ └── sub.ts │ ├── index.ts │ ├── lib/ │ │ ├── BufferedDuplex.ts │ │ ├── KeepaliveManager.ts │ │ ├── TypedEmitter.ts │ │ ├── client.ts │ │ ├── connect/ │ │ │ ├── ali.ts │ │ │ ├── index.ts │ │ │ ├── socks.ts │ │ │ ├── tcp.ts │ │ │ ├── tls.ts │ │ │ ├── ws.ts │ │ │ └── wx.ts │ │ ├── default-message-id-provider.ts │ │ ├── get-timer.ts │ │ ├── handlers/ │ │ │ ├── ack.ts │ │ │ ├── auth.ts │ │ │ ├── connack.ts │ │ │ ├── index.ts │ │ │ ├── publish.ts │ │ │ └── pubrel.ts │ │ ├── is-browser.ts │ │ ├── shared.ts │ │ ├── store.ts │ │ ├── topic-alias-recv.ts │ │ ├── topic-alias-send.ts │ │ ├── unique-message-id-provider.ts │ │ └── validations.ts │ └── mqtt.ts ├── test/ │ ├── browser/ │ │ ├── certs/ │ │ │ ├── server-cert.pem │ │ │ └── server-key.pem │ │ ├── test.js │ │ └── worker.js │ └── node/ │ ├── abstract_client.ts │ ├── abstract_store.ts │ ├── client.ts │ ├── client_mqtt5.ts │ ├── helpers/ │ │ ├── TeardownHelper.ts │ │ ├── leaked.ts │ │ ├── port_list.ts │ │ ├── private-csr.pem │ │ ├── private-key.pem │ │ ├── public-cert.pem │ │ ├── public-key.pem │ │ ├── server.ts │ │ ├── server_process.ts │ │ ├── tls-cert.pem │ │ ├── tls-key.pem │ │ ├── wrong-cert.pem │ │ ├── wrong-csr.pem │ │ └── wrong-key.pem │ ├── keepaliveManager.ts │ ├── message-id-provider.ts │ ├── mqtt.ts │ ├── mqtt_store.ts │ ├── secure_client.ts │ ├── server.ts │ ├── server_helpers_for_client_tests.ts │ ├── socks.ts │ ├── store.ts │ ├── unique_message_id_provider_client.ts │ ├── util.ts │ └── websocket_client.ts ├── tsconfig.build.json ├── tsconfig.json └── web-test-runner.config.mjs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 2 trim_trailing_whitespace = true ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Template for bug reports title: '[Bug]: ' labels: - bug body: - type: input attributes: label: MQTTjs Version validations: required: true - type: input attributes: label: Broker description: >- The broker you are using (Aedes, Mosca, mosquitto, RabbitMQ, HiveMQ etc...) validations: required: true - type: dropdown attributes: label: Environment options: - NodeJS - Browser validations: required: true - type: textarea attributes: label: Description description: A clear and concise description of the problem validations: required: true - type: textarea attributes: label: Minimal Reproduction description: provide steps to reproduce the problem validations: required: true - type: textarea attributes: label: Debug logs description: Provide logs to help diagnose your issue placeholder: >- On NodeJS simply run your application using `DEBUG=mqttjs*` env var, on browsers you should enable `verbose` level in console or you can set `log` option to `console.log.bind(console)` validations: required: true - type: markdown attributes: value: >- This template was generated with [Issue Forms Creator](https://issue-forms-creator.netlify.app) ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: ❓ Ask a Question url: https://github.com/mqttjs/MQTT.js/discussions/new about: If you have any other questions, ask them here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Template for bug reports title: '[Bug]: ' labels: - bug body: - type: input attributes: label: MQTTjs Version validations: required: true - type: input attributes: label: Broker description: >- The broker you are using (Aedes, Mosca, mosquitto, RabbitMQ, HiveMQ etc...) validations: required: true - type: dropdown attributes: label: Environment options: - NodeJS - Browser validations: required: true - type: textarea attributes: label: Description description: A clear and concise description of the problem validations: required: true - type: textarea attributes: label: Minimal Reproduction description: provide steps to reproduce the problem validations: required: true - type: textarea attributes: label: Debug logs description: Provide logs to help diagnose your issue placeholder: >- On NodeJS simply run your application using `DEBUG=mqttjs*` env var, on browsers you should enable `verbose` level in console or you can set `log` option to `console.log.bind(console)` validations: required: true - type: markdown attributes: value: >- This template was generated with [Issue Forms Creator](https://issue-forms-creator.netlify.app) ================================================ FILE: .github/workflows/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: ❓ Ask a Question url: https://github.com/mqttjs/MQTT.js/discussions/new about: If you have any other questions, ask them here. ================================================ FILE: .github/workflows/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/browser-tests.yml ================================================ name: Browser Tests on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] jobs: browser: runs-on: ubuntu-latest container: mcr.microsoft.com/playwright:v1.57.0-noble # https://playwright.dev/docs/ci-intro#via-containers strategy: matrix: node-version: [22.x] fail-fast: false steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install Dependencies run: npm ci - name: Test Browser # only run on latest node version, no reason to run on all timeout-minutes: 2 env: HOME: /root run: | npm run unit-test:browser ================================================ FILE: .github/workflows/electron-tests.yml ================================================ name: Electron Tests on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] jobs: browser: runs-on: ubuntu-latest strategy: matrix: node-version: [22.x] fail-fast: false steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install Dependencies run: npm ci - name: Build MQTT.JS run: npm run build - name: Run headless test run: cd electron-test && npm i && xvfb-run npm run wdio ================================================ FILE: .github/workflows/mqttjs-test.yml ================================================ name: MQTT.js Tests on: workflow_dispatch: inputs: logLevel: description: 'Debug Filter' required: true default: 'mqttjs*' push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [22.x, 24.x] fail-fast: false steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install Dependencies run: npm ci - name: Lint if: matrix.node-version == '22.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 timeout-minutes: 5 env: CI: true DEBUG: "${{ runner.debug == '1' && 'mqttjs:*' || '' }}" # upload coverage to Codecov # https://app.codecov.io/gh/mqttjs/MQTT.js - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: directory: ./coverage/ fail_ci_if_error: true flags: unittests name: codecov-mqttjs token: ${{ secrets.CODECOV_TOKEN }} verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: files: ./junit.xml fail_ci_if_error: false flags: unittests-results name: codecov-mqttjs-test-results token: ${{ secrets.CODECOV_TOKEN }} verbose: true ================================================ FILE: .github/workflows/release-it.yml ================================================ # ######################################################################### # Creates a new release using `workflow_dispatch` event trigger with `type` # as input to describe the type of release to create name: 'Release-it: Create a new release on demand' on: workflow_dispatch: inputs: type: description: 'Type/Options. `major --preRelease=beta`, `--preRelease`, `major`, `patch`, `minor` or `major`' required: false default: 'patch' jobs: release: permissions: contents: write id-token: write runs-on: [ubuntu-latest] strategy: matrix: node-version: [22.x] steps: - name: Checkout main uses: actions/checkout@v6 with: ref: 'main' fetch-depth: 0 # fetch all commits history to create the changelog token: ${{ secrets.GH_TOKEN }} - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Upgrade npm for OIDC support (requires npm >= 11.5.1) run: npm install -g npm@latest - name: Install dependencies run: npm ci - name: Initialize Git user run: | git config --global user.email "${{ github.actor }}@users.noreply.github.com }}" git config --global user.name "${{ github.actor }}" - name: Initialize NPM config run: | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Make the release env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: | # When all commits since the latest major tag should be added to the changelog, use --git.tagExclude='*[-]*' npx release-it ${{github.event.inputs.type}} --git.tagExclude='*[-]*' --ci --verbose ================================================ FILE: .github/workflows/semantic-pr.yml ================================================ name: "Semantic PR Title" on: pull_request_target: types: - opened - edited - synchronize permissions: pull-requests: read jobs: main: name: Validate PR title runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/stale.yml ================================================ name: Close inactive issues on: schedule: - cron: "30 1 * * *" jobs: close-stale: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v10 with: days-before-issue-stale: 365 days-before-issue-close: 14 stale-issue-label: "stale" stale-issue-message: | This is an automated message to let you know that this issue has gone 365 days without any activity. In order to ensure that we work on issues that still matter, this issue will be closed in 14 days. If this issue is still important, you can simply comment with a "bump" to keep it open. Thank you for your contribution. close-issue-message: "This issue was automatically closed due to inactivity." days-before-pr-stale: -1 days-before-pr-close: -1 repo-token: ${{ secrets.GITHUB_TOKEN }} exempt-all-assignees: true exempt-all-milestones: true ascending: true operations-per-run: 300 debug-only: false ================================================ FILE: .gitignore ================================================ node_modules certs/* [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ npm-debug.log dist/ yarn.lock coverage .nyc_output .idea/* test/typescript/.idea/* test/typescript/*.js test/typescript/*.map # VS Code stuff **/typings/** .vscode/ .npmrc junit.xml /build/ # macOS stuff .DS_Store .DS_Store? ================================================ FILE: .prettierignore ================================================ *.md README.md /types/ /examples/ /doc/ /dist/ ================================================ FILE: .prettierrc.js ================================================ module.exports = { semi: false, singleQuote: true, useTabs: true, tabWidth: 4, endOfLine: "lf", }; ================================================ FILE: .release-it.json ================================================ { "$schema": "https://unpkg.com/release-it@19/schema/release-it.json", "github": { "release": true }, "git": { "tagName": "v${version}", "commitMessage": "chore(release): ${version}", "requireBranch": "main" }, "hooks": { "before:init": [ "npm run test" ] }, "npm": { "publish": true, "skipChecks": true }, "plugins": { "@release-it/conventional-changelog": { "preset": "angular", "infile": "CHANGELOG.md" } } } ================================================ FILE: CHANGELOG.OLD.md ================================================ # Release History ## 4.3.7 ### PR fix: regression from #1401 and allow CI test failures to break gitthub workflow (#1443) ## 4.3.6 ### PR chore: update CI fix(browser): require buffer (#1420) fix(types): connect function proper overloads (#1416) ## 4.3.5 ### PR fix(drain-leak): fix regression introduced in #1301 (#1401) ## 4.3.4 ### PR fix(dependency): migrate LruMap from collections to lru-cache (#1396) ## 4.3.3 ### PR fix(publish): call callback when messageId available (#1393) fix: remove collections.js depdendency from number-allocator (#1394) ### PR fix(dependencies): update collections (#1386) ## 4.3.2 ### PR fix(dependencies): update collections (#1386) ## 4.3.1 ### PR fix(dependencies): remove babel-eslint and snazzy (#1383) ## 4.3.0 ### PR refined topic alias support (#1301) fix security vulnerability in ws stream (#1307) skip TLS SNI if host is IP address (#1311) update readme about vNext discussions (#1328) update readme sample (#1331) add support for ALPN TLS extension (#1332) align onConnectCallback with specs expecting connack packet (#1333) fix resubscribe messageId allocate twice (#1337) rework examples to be a bit more specific (#1352) readme typo fixed (#1353) fix(typescript): use correct version of @types/ws (#1358) fix(type): fix push properties types (#1359) fix: audit dev dependencies (#1374) fix(type): add properties type for IClientSubscribeOptions (#1378) feat(client): auth handler for enhanced auth (#1380) ## 4.2.8 ### PR Fix ws vulnerability and typescript bug (#1292) ## 4.2.7 ### PR #1287 - Fix production vulnerabilities (#1289) #1215 - Add missing 'duplexify' dependency (#1266) Improve type definition for 'wsOptions' (#1256) Improve Typescript Declaratiosn for userProperties (#1249) #1235 - Call the end on the WebSocket stream when WebSocket close event is emitted. (#1239) #1201 - Uncaught TypeError: net.createConnection is not a function. (#1236) Improve Documentation for Browserify (#1224) ## v4.2.6 and Below The release history has been documented in the GitHub releases and tags historically. ================================================ FILE: CHANGELOG.md ================================================ # Changelog # [5.15.0](/compare/v5.14.1...v5.15.0) (2026-02-02) ### Features * exported topic validation functions for external use (#2036) 396e02a, closes #2036 ## [5.14.1](https://github.com/mqttjs/MQTT.js/compare/v5.14.0...v5.14.1) (2025-09-04) ### Bug Fixes * **connect/ws:** ensure proxy stream is writable ([#2024](https://github.com/mqttjs/MQTT.js/issues/2024)) ([fe45405](https://github.com/mqttjs/MQTT.js/commit/fe454056d5b54718b1b3c90611026da4c908faef)), closes [#1914](https://github.com/mqttjs/MQTT.js/issues/1914) * isWebWorkerEnv check for Deno environment ([#2023](https://github.com/mqttjs/MQTT.js/issues/2023)) ([a6e74ad](https://github.com/mqttjs/MQTT.js/commit/a6e74add5e41cf9d2a3b62357e0c0531a128e7cd)) # [5.14.0](https://github.com/mqttjs/MQTT.js/compare/v5.13.3...v5.14.0) (2025-07-30) ## [5.13.3](https://github.com/mqttjs/MQTT.js/compare/v5.13.2...v5.13.3) (2025-07-18) ## [5.13.2](https://github.com/mqttjs/MQTT.js/compare/v5.13.1...v5.13.2) (2025-07-07) ### Bug Fixes * move back @types/ws from dev to deps ([#2008](https://github.com/mqttjs/MQTT.js/issues/2008)) ([959b132](https://github.com/mqttjs/MQTT.js/commit/959b132a492f43902deb994d119d07f15f955390)), closes [#1991](https://github.com/mqttjs/MQTT.js/issues/1991) ## [5.13.1](https://github.com/mqttjs/MQTT.js/compare/v5.13.0...v5.13.1) (2025-06-03) # [5.13.0](https://github.com/mqttjs/MQTT.js/compare/v5.12.1...v5.13.0) (2025-05-09) ### Features * add `subscribeBatchSize` option to split subscribe packets for AWS IoT Core ([#1995](https://github.com/mqttjs/MQTT.js/issues/1995)) ([6b719c8](https://github.com/mqttjs/MQTT.js/commit/6b719c8cd11397bb13f06c36b3fa5a3840befacb)) ## [5.12.1](https://github.com/mqttjs/MQTT.js/compare/v5.12.0...v5.12.1) (2025-05-06) ### Bug Fixes * add `@typescript-eslint/consistent-type-imports` rule and normalised all imports ([640cd3b](https://github.com/mqttjs/MQTT.js/commit/640cd3b9bd7d9a7d8ef59fa3fdd57a3a1c3ba345)) # [5.12.0](https://github.com/mqttjs/MQTT.js/compare/v5.11.1...v5.12.0) (2025-04-28) ### Features * **exports:** add react-native entry point to package.json ([#1988](https://github.com/mqttjs/MQTT.js/issues/1988)) ([622d3d8](https://github.com/mqttjs/MQTT.js/commit/622d3d8e7d8149bbb2ee03a01254e0ab9bf42e6c)) ## [5.11.1](https://github.com/mqttjs/MQTT.js/compare/v5.11.0...v5.11.1) (2025-04-23) ### Bug Fixes * **deps:** removed unused 'reinterval' dependency and move two @type/ dependencies to devDependencies. ([bb8c694](https://github.com/mqttjs/MQTT.js/commit/bb8c6947edc4d85d77f13f8c06df7a979a270a7f)) # [5.11.0](https://github.com/mqttjs/MQTT.js/compare/v5.10.4...v5.11.0) (2025-04-10) ### Bug Fixes * correct return type of `validateTopis` to include `null` ([#1979](https://github.com/mqttjs/MQTT.js/issues/1979)) ([935784a](https://github.com/mqttjs/MQTT.js/commit/935784aa8235fd9f38ac17820edf357ed7d5ab75)) ### Features * add intrinsic support for SOCKS proxies ([#1966](https://github.com/mqttjs/MQTT.js/issues/1966)) ([ee0fcce](https://github.com/mqttjs/MQTT.js/commit/ee0fcce4bd3e1af318b0144b9e0928c571c02173)) ## [5.10.4](https://github.com/mqttjs/MQTT.js/compare/v5.10.3...v5.10.4) (2025-02-26) ### Bug Fixes * **browser:** bump readable-stream@4.7.0 to fix BigInt issues ([#1963](https://github.com/mqttjs/MQTT.js/issues/1963)) ([9749891](https://github.com/mqttjs/MQTT.js/commit/9749891137c0eb57f20c13e17196bccbdec1029e)) * **browser:** prevent error stream.push() after EOF ([#1932](https://github.com/mqttjs/MQTT.js/issues/1932)) ([df2e8fe](https://github.com/mqttjs/MQTT.js/commit/df2e8febcccd4574a046887e612113a85900b86d)) * **electron-test:** webdriver session not created ([#1972](https://github.com/mqttjs/MQTT.js/issues/1972)) ([57691cf](https://github.com/mqttjs/MQTT.js/commit/57691cf9a088dee69e0dea637ae5742c6dfae833)) * move protocol "clean" after merge of parsedOptions ([#1965](https://github.com/mqttjs/MQTT.js/issues/1965)) ([1c19ca0](https://github.com/mqttjs/MQTT.js/commit/1c19ca0d0a810ee9a925d17552e9d84f70aa718b)) ## [5.10.3](https://github.com/mqttjs/MQTT.js/compare/v5.10.2...v5.10.3) (2024-11-26) ### Bug Fixes * add option to reconnect if connack has an error code ([#1948](https://github.com/mqttjs/MQTT.js/issues/1948)) ([fa19586](https://github.com/mqttjs/MQTT.js/commit/fa19586cf0482b007e136e013cf7c9a423b882eb)) ## [5.10.2](https://github.com/mqttjs/MQTT.js/compare/v5.10.1...v5.10.2) (2024-11-13) ### Features * **electron-test:** move third hosted broker to self hosted broker ([#1926](https://github.com/mqttjs/MQTT.js/issues/1926)) ([1ca3f9e](https://github.com/mqttjs/MQTT.js/commit/1ca3f9e37f6ef7fe86307e292042c6542ca35241)) ## [5.10.1](https://github.com/mqttjs/MQTT.js/compare/v5.10.0...v5.10.1) (2024-08-28) ### Bug Fixes * **browser:** handle `Blob` payloads ([#1930](https://github.com/mqttjs/MQTT.js/issues/1930)) ([86b7959](https://github.com/mqttjs/MQTT.js/commit/86b795983d86847e1da334fd0d30cbd80f92b540)) # [5.10.0](https://github.com/mqttjs/MQTT.js/compare/v5.9.1...v5.10.0) (2024-08-14) ### Bug Fixes * **test:** close all open connections in abstract_client test ([#1917](https://github.com/mqttjs/MQTT.js/issues/1917)) ([661c30a](https://github.com/mqttjs/MQTT.js/commit/661c30aecb8d7531fc052a7770519267067840fb)) * **types:** unsubscribe options type ([#1921](https://github.com/mqttjs/MQTT.js/issues/1921)) ([18a357c](https://github.com/mqttjs/MQTT.js/commit/18a357ce98c2e7ae053afefaf7c56a0d3a8e62b7)) ### Features * add `suback` packet to subscribe callback ([#1923](https://github.com/mqttjs/MQTT.js/issues/1923)) ([93f4482](https://github.com/mqttjs/MQTT.js/commit/93f4482570b6e96d81a5466ea94c3fd7308ff31c)) * add unsubscribe ack packet to the unsubscribe callback ([#1922](https://github.com/mqttjs/MQTT.js/issues/1922)) ([8bcf304](https://github.com/mqttjs/MQTT.js/commit/8bcf3042a9133acf8d266d73bc67153a69660e05)) ## [5.9.1](https://github.com/mqttjs/MQTT.js/compare/v5.9.0...v5.9.1) (2024-08-01) ### Bug Fixes * **browser:** ensure proxy is defined ([ffc9805](https://github.com/mqttjs/MQTT.js/commit/ffc9805a51adf88bded6a1af1c0f66004e9e0f08)) * **browser:** prevent error `stream.push() after EOF` ([#1915](https://github.com/mqttjs/MQTT.js/issues/1915)) ([b5cc835](https://github.com/mqttjs/MQTT.js/commit/b5cc835fed9bd624c20d5f4f42b15c3cfa4b3fbe)), closes [#1914](https://github.com/mqttjs/MQTT.js/issues/1914) * **test:** close open connections ([#1911](https://github.com/mqttjs/MQTT.js/issues/1911)) ([053a7be](https://github.com/mqttjs/MQTT.js/commit/053a7be91f93a0a27c63ca5ed488d9206fdec960)) # [5.9.0](https://github.com/mqttjs/MQTT.js/compare/v5.8.1...v5.9.0) (2024-07-26) ### Bug Fixes * tets hang up ([#1906](https://github.com/mqttjs/MQTT.js/issues/1906)) ([c462530](https://github.com/mqttjs/MQTT.js/commit/c462530d2ec0b61a20cc43f188254bf2b403787a)) * **types:** add connectAsync overload signature with allowRetries ([#1909](https://github.com/mqttjs/MQTT.js/issues/1909)) ([6b278dc](https://github.com/mqttjs/MQTT.js/commit/6b278dca5a5b82b07835344f3c129ddd5b73e6e8)) ### Features * add `forceNativeWebSocket` client option ([#1910](https://github.com/mqttjs/MQTT.js/issues/1910)) ([103d172](https://github.com/mqttjs/MQTT.js/commit/103d1721d68952e536a3704a05a569c95f0a1987)), closes [#1796](https://github.com/mqttjs/MQTT.js/issues/1796) [#1895](https://github.com/mqttjs/MQTT.js/issues/1895) ## [5.8.1](https://github.com/mqttjs/MQTT.js/compare/v5.8.0...v5.8.1) (2024-07-18) ### Bug Fixes * connect after client.end not working ([#1902](https://github.com/mqttjs/MQTT.js/issues/1902)) ([fbe5294](https://github.com/mqttjs/MQTT.js/commit/fbe52949b47378768fd325f01682a766a5965dfe)) * reschedule pings problem ([#1904](https://github.com/mqttjs/MQTT.js/issues/1904)) ([8e14d3e](https://github.com/mqttjs/MQTT.js/commit/8e14d3eac01f4fcccfc1ee657e0158d0644951ce)) # [5.8.0](https://github.com/mqttjs/MQTT.js/compare/v5.7.3...v5.8.0) (2024-07-05) ### Features * add compatibility with txiki.js ([#1895](https://github.com/mqttjs/MQTT.js/issues/1895)) ([37b08c9](https://github.com/mqttjs/MQTT.js/commit/37b08c99fead5282e38b851ce1006f09521b038c)) * allow to pass custom timer for keepalive manager ([#1896](https://github.com/mqttjs/MQTT.js/issues/1896)) ([ee81184](https://github.com/mqttjs/MQTT.js/commit/ee811844d07365ca98721be90c4e1c2c1d8623b9)) ## [5.7.3](https://github.com/mqttjs/MQTT.js/compare/v5.7.2...v5.7.3) (2024-06-26) ### Bug Fixes * **wechat:** do not ignore path with `wx` protocol ([#1894](https://github.com/mqttjs/MQTT.js/issues/1894)) ([300c0b4](https://github.com/mqttjs/MQTT.js/commit/300c0b4dc5a37d8594a4cb1af5836c095b4d823c)), closes [#1892](https://github.com/mqttjs/MQTT.js/issues/1892) ## [5.7.2](https://github.com/mqttjs/MQTT.js/compare/v5.7.1...v5.7.2) (2024-06-19) ### Bug Fixes * **security:** bump ws@8.17.1 and other audit issues ([#1891](https://github.com/mqttjs/MQTT.js/issues/1891)) ([096baaa](https://github.com/mqttjs/MQTT.js/commit/096baaaa882627554efd4bc9985ce8a5f2dfda5e)) ## [5.7.1](https://github.com/mqttjs/MQTT.js/compare/v5.7.0...v5.7.1) (2024-06-18) ### Bug Fixes * suback Error Codes Handling ([#1887](https://github.com/mqttjs/MQTT.js/issues/1887)) ([2a98e5e](https://github.com/mqttjs/MQTT.js/commit/2a98e5e878cad632fe86e738144688ee2b14a7dd)) # [5.7.0](https://github.com/mqttjs/MQTT.js/compare/v5.6.2...v5.7.0) (2024-05-28) ### Features * add `unixSocket` option and `+unix` suffix support to protocol ([#1874](https://github.com/mqttjs/MQTT.js/issues/1874)) ([1004c78](https://github.com/mqttjs/MQTT.js/commit/1004c78db7d6763f21c98fa3db2f12e688ca33ff)) ## [5.6.2](https://github.com/mqttjs/MQTT.js/compare/v5.6.1...v5.6.2) (2024-05-23) ### Bug Fixes * prevent url.parse to set `path` option ([#1871](https://github.com/mqttjs/MQTT.js/issues/1871)) ([de0174f](https://github.com/mqttjs/MQTT.js/commit/de0174f033367dde352d1eff339064e704f610e1)), closes [#1870](https://github.com/mqttjs/MQTT.js/issues/1870) ## [5.6.1](https://github.com/mqttjs/MQTT.js/compare/v5.6.0...v5.6.1) (2024-05-17) ### Bug Fixes * update is-browser.ts to account `undefined` navigator ([#1868](https://github.com/mqttjs/MQTT.js/issues/1868)) ([0111a7a](https://github.com/mqttjs/MQTT.js/commit/0111a7af4b71f2a973a712a1f0df6574660d6ec0)), closes [/github.com/mqttjs/MQTT.js/commit/6a03d29b86dc4fe8eae04eaf0f9fc661f1c3d1ea#commitcomment-142114121](https://github.com//github.com/mqttjs/MQTT.js/commit/6a03d29b86dc4fe8eae04eaf0f9fc661f1c3d1ea/issues/commitcomment-142114121) [/github.com/mqttjs/MQTT.js/pull/1868#pullrequestreview-2062507553](https://github.com//github.com/mqttjs/MQTT.js/pull/1868/issues/pullrequestreview-2062507553) # [5.6.0](https://github.com/mqttjs/MQTT.js/compare/v5.5.6...v5.6.0) (2024-05-13) ### Features * keepalive manager ([#1865](https://github.com/mqttjs/MQTT.js/issues/1865)) ([bad160a](https://github.com/mqttjs/MQTT.js/commit/bad160af2a7b76a5159652e6d3757e7798337261)) ## [5.5.6](https://github.com/mqttjs/MQTT.js/compare/v5.5.5...v5.5.6) (2024-05-13) ### Bug Fixes * do not shift pings on 'publish' packets ([#1866](https://github.com/mqttjs/MQTT.js/issues/1866)) ([e4d4663](https://github.com/mqttjs/MQTT.js/commit/e4d4663bcd5f87399b9d7bf101b364cda1c48d0e)), closes [#1863](https://github.com/mqttjs/MQTT.js/issues/1863) [#1861](https://github.com/mqttjs/MQTT.js/issues/1861) * **electron:** detect electron context ([#1856](https://github.com/mqttjs/MQTT.js/issues/1856)) ([6a03d29](https://github.com/mqttjs/MQTT.js/commit/6a03d29b86dc4fe8eae04eaf0f9fc661f1c3d1ea)) * **ws:** ignored `host` option and default hostname in browser ([c6580a6](https://github.com/mqttjs/MQTT.js/commit/c6580a6685821c60a4595986227dba2a615b9958)), closes [#1730](https://github.com/mqttjs/MQTT.js/issues/1730) ## [5.5.5](https://github.com/mqttjs/MQTT.js/compare/v5.5.4...v5.5.5) (2024-04-30) ### Bug Fixes * keepalive issues ([#1855](https://github.com/mqttjs/MQTT.js/issues/1855)) ([4f242f4](https://github.com/mqttjs/MQTT.js/commit/4f242f47bc8568299f04bade8aa4d1d11b939912)) ## [5.5.4](https://github.com/mqttjs/MQTT.js/compare/v5.5.3...v5.5.4) (2024-04-26) ### Bug Fixes * allow to use unix sockets in connect ([#1852](https://github.com/mqttjs/MQTT.js/issues/1852)) ([22c97b5](https://github.com/mqttjs/MQTT.js/commit/22c97b5f7536e3e36317c3b28dc0d70557b820ac)), closes [#1040](https://github.com/mqttjs/MQTT.js/issues/1040) * **react-native:** process.nextTick is not a function error ([#1849](https://github.com/mqttjs/MQTT.js/issues/1849)) ([f62e207](https://github.com/mqttjs/MQTT.js/commit/f62e207def81b174af83d2e9525cddd1ce960fc3)) ## [5.5.3](https://github.com/mqttjs/MQTT.js/compare/v5.5.2...v5.5.3) (2024-04-19) ### Bug Fixes * possible race condition in ping timer ([#1848](https://github.com/mqttjs/MQTT.js/issues/1848)) ([0b7d687](https://github.com/mqttjs/MQTT.js/commit/0b7d687282e6342d5276946dfd4c4d1e0a66ba47)), closes [#1845](https://github.com/mqttjs/MQTT.js/issues/1845) * wrong mqttjs version printed ([#1847](https://github.com/mqttjs/MQTT.js/issues/1847)) ([a24cf14](https://github.com/mqttjs/MQTT.js/commit/a24cf14654cb0fa74da1be2671dfaf57071fec40)) ## [5.5.2](https://github.com/mqttjs/MQTT.js/compare/v5.5.1...v5.5.2) (2024-04-12) ### Bug Fixes * **react-native:** error Cannot create URL for blob ([#1840](https://github.com/mqttjs/MQTT.js/issues/1840)) ([fc8fafb](https://github.com/mqttjs/MQTT.js/commit/fc8fafbdf5e01edc487192393293b944e77f5920)) ## [5.5.1](https://github.com/mqttjs/MQTT.js/compare/v5.5.0...v5.5.1) (2024-04-10) ### Bug Fixes * **browser:** uncaught error when stream is destroyed with error ([380f286](https://github.com/mqttjs/MQTT.js/commit/380f286d46f1c3d7a64c7bd851bbe8d84b797074)), closes [#1839](https://github.com/mqttjs/MQTT.js/issues/1839) # [5.5.0](https://github.com/mqttjs/MQTT.js/compare/v5.4.0...v5.5.0) (2024-03-18) ### Bug Fixes * **browser:** force closing client doesn't destroy websocket correctly ([#1820](https://github.com/mqttjs/MQTT.js/issues/1820)) ([f9b1204](https://github.com/mqttjs/MQTT.js/commit/f9b1204d7e0a04bb809be6205091fd89281b1e73)), closes [#1817](https://github.com/mqttjs/MQTT.js/issues/1817) * expose mqttjs version on `MqttClient.VERSION` ([#1821](https://github.com/mqttjs/MQTT.js/issues/1821)) ([50776a7](https://github.com/mqttjs/MQTT.js/commit/50776a74c73c188f67faf399af90cfe0957a0e1f)) ### Features * `timerVariant` option to choose between native and worker timers ([#1818](https://github.com/mqttjs/MQTT.js/issues/1818)) ([547519d](https://github.com/mqttjs/MQTT.js/commit/547519daa8353a2d8a7fe9e4ae715601570b085f)) # [5.4.0](https://github.com/mqttjs/MQTT.js/compare/v5.3.6...v5.4.0) (2024-03-13) ### Bug Fixes * add keepalive test in webworker ([#1807](https://github.com/mqttjs/MQTT.js/issues/1807)) ([8697b06](https://github.com/mqttjs/MQTT.js/commit/8697b06cae3265422620c38b76126381502a9c17)) * improve some flaky tests ([#1801](https://github.com/mqttjs/MQTT.js/issues/1801)) ([78e8f13](https://github.com/mqttjs/MQTT.js/commit/78e8f139ee0ad61e752421b9e594bea742af9745)) * print MQTTjs version and environment on constructor ([#1816](https://github.com/mqttjs/MQTT.js/issues/1816)) ([c0a6668](https://github.com/mqttjs/MQTT.js/commit/c0a666887ec313ee82142a825166e5b1d2e668bb)) * some others flaky tests ([#1808](https://github.com/mqttjs/MQTT.js/issues/1808)) ([f988058](https://github.com/mqttjs/MQTT.js/commit/f9880588244ac35c945302fad474f6c47f27acbc)) * update worker-timers from 7.0.78 to 7.1.4 ([#1813](https://github.com/mqttjs/MQTT.js/issues/1813)) ([2b75186](https://github.com/mqttjs/MQTT.js/commit/2b751861f2af7b914c3eb84265fb8474428045ec)), closes [#1802](https://github.com/mqttjs/MQTT.js/issues/1802) * wrong default export for browser ([#1800](https://github.com/mqttjs/MQTT.js/issues/1800)) ([6237f45](https://github.com/mqttjs/MQTT.js/commit/6237f45f3f455b1b6ae7d339fc8a56a5eff91dc2)) ### Features * emit `Keepalive timeout` error and speed up tests using fake timers ([#1798](https://github.com/mqttjs/MQTT.js/issues/1798)) ([5d9bf10](https://github.com/mqttjs/MQTT.js/commit/5d9bf1004ba76098d4ae315fa7a4b44a9d26750b)) ## [5.3.6](https://github.com/mqttjs/MQTT.js/compare/v5.3.5...v5.3.6) (2024-02-26) ### Bug Fixes * **browser:** add `navigator` polifilly for wechat mini ([#1796](https://github.com/mqttjs/MQTT.js/issues/1796)) ([c26908a](https://github.com/mqttjs/MQTT.js/commit/c26908a242fa1f573689b03f554bb95d83e61c84)), closes [#1789](https://github.com/mqttjs/MQTT.js/issues/1789) * emit `error` event on connack timeout ([#1781](https://github.com/mqttjs/MQTT.js/issues/1781)) ([56e6e23](https://github.com/mqttjs/MQTT.js/commit/56e6e23c0fb775bfd16edf04d6b28f6bbcf05023)) ## [5.3.5](https://github.com/mqttjs/MQTT.js/compare/v5.3.4...v5.3.5) (2024-01-23) ### Bug Fixes * bump help-me version to fix vulnerability in glob/inflight ([#1773](https://github.com/mqttjs/MQTT.js/issues/1773)) ([72f99dc](https://github.com/mqttjs/MQTT.js/commit/72f99dcb33b016bced8a2c03ac857c3940ddcda3)) * keepalive causes a reconnect loop when connection is lost ([#1779](https://github.com/mqttjs/MQTT.js/issues/1779)) ([3da5e84](https://github.com/mqttjs/MQTT.js/commit/3da5e84a158985cbe7bdf60d3a9744b71d98bb56)), closes [#1778](https://github.com/mqttjs/MQTT.js/issues/1778) ## [5.3.4](https://github.com/mqttjs/MQTT.js/compare/v5.3.3...v5.3.4) (2023-12-22) ### Bug Fixes * leaked `close` listener in `startStreamProcess` loop ([#1759](https://github.com/mqttjs/MQTT.js/issues/1759)) ([0c10ef6](https://github.com/mqttjs/MQTT.js/commit/0c10ef680ccc34bbe49948d414f36879d816e4e0)) * typo in `client.ts` ([#1763](https://github.com/mqttjs/MQTT.js/issues/1763)) ([e3528ac](https://github.com/mqttjs/MQTT.js/commit/e3528ac32d9dc165f8f1238397bd4d02e1990279)) ## [5.3.3](https://github.com/mqttjs/MQTT.js/compare/v5.3.2...v5.3.3) (2023-12-05) ### Bug Fixes * don't use worker timers in worker and add web worker tests ([#1755](https://github.com/mqttjs/MQTT.js/issues/1755)) ([38fb6ae](https://github.com/mqttjs/MQTT.js/commit/38fb6ae16073ce31e38dbc1e41a155ad98e04dcc)) * improve worker tests ([#1757](https://github.com/mqttjs/MQTT.js/issues/1757)) ([4facb18](https://github.com/mqttjs/MQTT.js/commit/4facb18dd9f81bb6af437a6257960e6e878349ad)) ## [5.3.2](https://github.com/mqttjs/MQTT.js/compare/v5.3.1...v5.3.2) (2023-12-04) ### Bug Fixes * **browser:** use worker timers to prevent unexpected client close ([#1753](https://github.com/mqttjs/MQTT.js/issues/1753)) ([35448f3](https://github.com/mqttjs/MQTT.js/commit/35448f386687030e7b68bd88f5f4852fbb833c9d)) * catch all socket errors ([#1752](https://github.com/mqttjs/MQTT.js/issues/1752)) ([a50e85c](https://github.com/mqttjs/MQTT.js/commit/a50e85ccf780621cdf2fd0a0bfcf5575a590f173)) * prop `window` is not defined in web worker ([#1749](https://github.com/mqttjs/MQTT.js/issues/1749)) ([6591404](https://github.com/mqttjs/MQTT.js/commit/6591404b38c73550157e22f3e57683a634bb919c)) ## [5.3.1](https://github.com/mqttjs/MQTT.js/compare/v5.3.0...v5.3.1) (2023-11-28) ### Bug Fixes * improve environment detection in is-browser utility ([#1744](https://github.com/mqttjs/MQTT.js/issues/1744)) ([b094142](https://github.com/mqttjs/MQTT.js/commit/b09414285d5c27cf76a9ff72cbb5ffe8ecec3981)) * typescript compile error ([2655feb](https://github.com/mqttjs/MQTT.js/commit/2655feb7a182c53bfa5ea7321b4e1a6d5b031311)), closes [#1746](https://github.com/mqttjs/MQTT.js/issues/1746) # [5.3.0](https://github.com/mqttjs/MQTT.js/compare/v5.2.2...v5.3.0) (2023-11-18) ### Features * **browser:** websockets improvements and bundle optimizations ([#1732](https://github.com/mqttjs/MQTT.js/issues/1732)) ([0928f85](https://github.com/mqttjs/MQTT.js/commit/0928f8575a7b4c717fbd960c802e1dc41b436d0e)) ## [5.2.2](https://github.com/mqttjs/MQTT.js/compare/v5.2.1...v5.2.2) (2023-11-14) ### Bug Fixes * add default export ([#1740](https://github.com/mqttjs/MQTT.js/issues/1740)) ([fdb498f](https://github.com/mqttjs/MQTT.js/commit/fdb498fe7ebbdf2be0d1fbcb897f093d4fa40d05)) ## [5.2.1](https://github.com/mqttjs/MQTT.js/compare/v5.2.0...v5.2.1) (2023-11-10) ### Bug Fixes * make `import mqtt from 'mqtt'` work in browsers ([#1734](https://github.com/mqttjs/MQTT.js/issues/1734)) ([80e29a9](https://github.com/mqttjs/MQTT.js/commit/80e29a9dc6bb6ad51a7ac968361a550bc1da68cb)) # [5.2.0](https://github.com/mqttjs/MQTT.js/compare/v5.1.4...v5.2.0) (2023-11-09) ### Features * esm version `dist/mqtt.esm.js` and replace `browserify` with `esbuild` ([#1731](https://github.com/mqttjs/MQTT.js/issues/1731)) ([3d6c3be](https://github.com/mqttjs/MQTT.js/commit/3d6c3be60eae8416dbfea1d15a826c0b5fc52c45)) ## [5.1.4](https://github.com/mqttjs/MQTT.js/compare/v5.1.3...v5.1.4) (2023-10-30) ### Bug Fixes * crash with React Native ([#1724](https://github.com/mqttjs/MQTT.js/issues/1724)) ([f6123f2](https://github.com/mqttjs/MQTT.js/commit/f6123f22a11a4eb4c34c874b47056cea7ef264a6)) * unambiguously detect web workers ([#1728](https://github.com/mqttjs/MQTT.js/issues/1728)) ([e44368c](https://github.com/mqttjs/MQTT.js/commit/e44368c0d7541d005ad668d5d44d080e29ca5778)) ## [5.1.3](https://github.com/mqttjs/MQTT.js/compare/v5.1.2...v5.1.3) (2023-10-20) ### Bug Fixes * add all `EventListener` methods to `TypedEventEmitter` interface ([#1718](https://github.com/mqttjs/MQTT.js/issues/1718)) ([b96882a](https://github.com/mqttjs/MQTT.js/commit/b96882a7e5ff2869badbbd34c9b2e1ac51c25d2a)) ## [5.1.2](https://github.com/mqttjs/MQTT.js/compare/v5.1.1...v5.1.2) (2023-10-10) ### Bug Fixes * detect web worker ([#1711](https://github.com/mqttjs/MQTT.js/issues/1711)) ([a75a467](https://github.com/mqttjs/MQTT.js/commit/a75a467e3524aef1d6038ed4ed14ab0407c146cb)) ## [5.1.1](https://github.com/mqttjs/MQTT.js/compare/v5.1.0...v5.1.1) (2023-10-09) ### Bug Fixes * restore nodejs 16 compatibility ([a347c0d](https://github.com/mqttjs/MQTT.js/commit/a347c0d81ff800c1469d8497542a8c5973b59e33)), closes [#1710](https://github.com/mqttjs/MQTT.js/issues/1710) # [5.1.0](https://github.com/mqttjs/MQTT.js/compare/v5.0.5...v5.1.0) (2023-10-04) ### Bug Fixes * **types:** import type error ([#1705](https://github.com/mqttjs/MQTT.js/issues/1705)) ([0960b68](https://github.com/mqttjs/MQTT.js/commit/0960b68f9b612640318931e971d7a715f0945bdd)) ### Features * custom websocket support ([#1696](https://github.com/mqttjs/MQTT.js/issues/1696)) ([d6fd3a8](https://github.com/mqttjs/MQTT.js/commit/d6fd3a8316642a17ff1e90b4d6c9d4656c3831e5)) ## [5.0.5](https://github.com/mqttjs/MQTT.js/compare/v5.0.4...v5.0.5) (2023-09-08) ### Bug Fixes * publish/subscribe/unsubscribe types and missing types exports ([#1688](https://github.com/mqttjs/MQTT.js/issues/1688)) ([2df6af7](https://github.com/mqttjs/MQTT.js/commit/2df6af717a7458eff1bf69be026734c973ade0a6)) ## [5.0.4](https://github.com/mqttjs/MQTT.js/compare/v5.0.3...v5.0.4) (2023-08-31) ### Bug Fixes * export js file in dist folder ([#1596](https://github.com/mqttjs/MQTT.js/issues/1596)) ([#1677](https://github.com/mqttjs/MQTT.js/issues/1677)) ([cbe0dc6](https://github.com/mqttjs/MQTT.js/commit/cbe0dc6be52bb3a5a9fa1f5b390973bf57f9da47)) * move exported types out of dev dependencies ([#1676](https://github.com/mqttjs/MQTT.js/issues/1676)) ([844e4ff](https://github.com/mqttjs/MQTT.js/commit/844e4ff6a75911e0d5f5fad75341ffc04eed1b15)) * set default value false for reconnecting in constructor ([#1674](https://github.com/mqttjs/MQTT.js/issues/1674)) ([#1678](https://github.com/mqttjs/MQTT.js/issues/1678)) ([312b57b](https://github.com/mqttjs/MQTT.js/commit/312b57ba982209d874d65a0857a019991a2f9b0d)) ## [5.0.3](https://github.com/mqttjs/MQTT.js/compare/v5.0.2...v5.0.3) (2023-08-16) ### Bug Fixes * browser detection ([183b35a](https://github.com/mqttjs/MQTT.js/commit/183b35aa3ed98fbbcbea6805994ef7c3cc8ee616)), closes [#1671](https://github.com/mqttjs/MQTT.js/issues/1671) * close and end callbacks not executed in the WeChat mini program ([#1664](https://github.com/mqttjs/MQTT.js/issues/1664)) ([15ff607](https://github.com/mqttjs/MQTT.js/commit/15ff607f4c938d0e7a23c99413db7496cae12e48)) ## [5.0.2](https://github.com/mqttjs/MQTT.js/compare/v5.0.1...v5.0.2) (2023-08-03) ### Bug Fixes * **cli:** cli commands not working ([#1660](https://github.com/mqttjs/MQTT.js/issues/1660)) ([1bea132](https://github.com/mqttjs/MQTT.js/commit/1bea132e97eeeb7187525dcf7417761388919075)) * import mqtt correctly in test ([8f15557](https://github.com/mqttjs/MQTT.js/commit/8f15557d0c4e455f91c96df5793d32451f1601d3)) * **tests:** abstract store test types ([0ddd097](https://github.com/mqttjs/MQTT.js/commit/0ddd0976bb8dd7dc1d434e8bb954d440ba653fb2)) ## [5.0.1](https://github.com/mqttjs/MQTT.js/compare/v5.0.0...v5.0.1) (2023-07-31) ### Bug Fixes * resubscribe when no session present ([#895](https://github.com/mqttjs/MQTT.js/issues/895)) ([#1650](https://github.com/mqttjs/MQTT.js/issues/1650)) ([37acda6](https://github.com/mqttjs/MQTT.js/commit/37acda655e202025373311624e19589ae7ef5970)) * **types:** wrong `incomingStore` and `outgoingStore` ([8133eba](https://github.com/mqttjs/MQTT.js/commit/8133eba152e81ed77e6aa18eb2cc351c3c901aa8)) # [5.0.0](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0) (2023-07-25) ### Bug Fixes * help message for client ID param for sub command is incorrect ([#1643](https://github.com/mqttjs/MQTT.js/issues/1643)) ([8521888](https://github.com/mqttjs/MQTT.js/commit/85218884728da85b626de6af0ac0bc9c26045f43)) * **types:** better streamBuilder types ([247e187](https://github.com/mqttjs/MQTT.js/commit/247e187b22e4ae916d1d89013e617b19688914dc)) * **types:** better types ([#1645](https://github.com/mqttjs/MQTT.js/issues/1645)) ([0f29bff](https://github.com/mqttjs/MQTT.js/commit/0f29bffb7e9088a1388139dcae04bb8731debc38)) * use explicit `connect` packet and infer types from `mqtt-packet` ([#1646](https://github.com/mqttjs/MQTT.js/issues/1646)) ([2a49ed3](https://github.com/mqttjs/MQTT.js/commit/2a49ed324e330deb5ca2ba8044b9196fc411ab8a)) ### Features * promises support ([#1644](https://github.com/mqttjs/MQTT.js/issues/1644)) ([d02e176](https://github.com/mqttjs/MQTT.js/commit/d02e17697f351b5fc2ed6d2cf689cbe40b829b9d)) # [5.0.0-beta.4](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0) (2023-07-21) # [5.0.0-beta.3](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0) (2023-07-19) ### Bug Fixes * make tests more reliable ([#1534](https://github.com/mqttjs/MQTT.js/issues/1534)) ([1076143](https://github.com/mqttjs/MQTT.js/commit/1076143a7ed6b07b91ded9985cc9a0bbb5a84da4)) * problem with publish callback invoked twice ([#1635](https://github.com/mqttjs/MQTT.js/issues/1635)) ([79b23a8](https://github.com/mqttjs/MQTT.js/commit/79b23a8f76abaceec67f063b6da0ee57a2c60697)) * **types:** subscribe definition ([#1527](https://github.com/mqttjs/MQTT.js/issues/1527)) ([debb7d9](https://github.com/mqttjs/MQTT.js/commit/debb7d93c17f5b68704c160ccd88e7e1db87d92d)) * chore!: remove unused deps, convert to ES2015 class (#1633) ([d71b000](https://github.com/mqttjs/MQTT.js/commit/d71b000773e4954c9a2ecbf4f750dac58017ef1a)), closes [#1633](https://github.com/mqttjs/MQTT.js/issues/1633) ### BREAKING CHANGES * when creating an `MqttClient` instance `new` is now required # [5.0.0-beta.2](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0) (2023-07-03) ### Bug Fixes * browser tests not working ([#1628](https://github.com/mqttjs/MQTT.js/issues/1628)) ([8775fcd](https://github.com/mqttjs/MQTT.js/commit/8775fcdad952b39fa4b79dbe912ca42033be030a)) * setImmediate polyfill ([#1626](https://github.com/mqttjs/MQTT.js/issues/1626)) ([0ed0754](https://github.com/mqttjs/MQTT.js/commit/0ed0754b95b92df51ed49ae63058b31fdba1d415)) ### Features * option to disable `writeCache` and fix leak in subscriptions ([#1622](https://github.com/mqttjs/MQTT.js/issues/1622)) ([c8aa654](https://github.com/mqttjs/MQTT.js/commit/c8aa6540dbf68ffb0d88c287e2c862b28d3fb6e6)), closes [#1535](https://github.com/mqttjs/MQTT.js/issues/1535) [#1151](https://github.com/mqttjs/MQTT.js/issues/1151) # [5.0.0-beta.1](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0) (2023-06-29) ### Bug Fixes * `_storeProcessing` staying true after outStore got emptied ([#1492](https://github.com/mqttjs/MQTT.js/issues/1492)) ([f3f7be7](https://github.com/mqttjs/MQTT.js/commit/f3f7be76199115a622fde2590d44b1bb0cf57d41)) * consistency, used `this` instead of `that` ([#1618](https://github.com/mqttjs/MQTT.js/issues/1618)) ([800825b](https://github.com/mqttjs/MQTT.js/commit/800825bf619d83ef713a5b2fa1533bbf6ccac872)) * prevent store message on store when it's restored ([#1255](https://github.com/mqttjs/MQTT.js/issues/1255)) ([8d68c8c](https://github.com/mqttjs/MQTT.js/commit/8d68c8c3e38aede52741a06838933011a6fccc43)) # [5.0.0-beta.0](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0) (2023-06-27) ### Bug Fixes * add missing export of UniqueMessageIdProvider and DefaultMessageIdProvider ([#1572](https://github.com/mqttjs/MQTT.js/issues/1572)) ([aa2e0ad](https://github.com/mqttjs/MQTT.js/commit/aa2e0ad49aadf333141f18cb85d2582abb8e19fc)) * IS_BROWSER check is now safer and more agnostic about the bundler ([#1571](https://github.com/mqttjs/MQTT.js/issues/1571)) ([b48b4b4](https://github.com/mqttjs/MQTT.js/commit/b48b4b4e79690c96033ea2df387c11f3bc26bf6a)) * **test:** `topicAliasMaximum` tests ([#1612](https://github.com/mqttjs/MQTT.js/issues/1612)) ([f1e5518](https://github.com/mqttjs/MQTT.js/commit/f1e5518150ea45067b87104abd9fed64ec13a48c)) * topicAliasMaximum under must be under Connect properties ([#1519](https://github.com/mqttjs/MQTT.js/issues/1519)) ([3b2e1cb](https://github.com/mqttjs/MQTT.js/commit/3b2e1cb7c4bf33ff66bcd1cc3091790a9635f19a)) * **types:** missing null declaration for error in subscription callback ([#1589](https://github.com/mqttjs/MQTT.js/issues/1589)) ([afc067b](https://github.com/mqttjs/MQTT.js/commit/afc067be2ca83990209b6176adec06f9a4c76a2c)) * **types:** topic alias controls and password ([#1509](https://github.com/mqttjs/MQTT.js/issues/1509)) ([85c9341](https://github.com/mqttjs/MQTT.js/commit/85c9341bba2676cfd069ec38a1a7cfda71647b68)) * chore!: drop support for node 12-14 (#1615) ([a2cbf61](https://github.com/mqttjs/MQTT.js/commit/a2cbf61c2a051a5ee69a50e00688e8ace79e7ef5)), closes [#1615](https://github.com/mqttjs/MQTT.js/issues/1615) ### BREAKING CHANGES * Dropped support for NodeJS 12-14 # [5.0.0-beta.4](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.4) (2023-07-21) # [5.0.0-beta.3](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.4) (2023-07-19) ### Bug Fixes * make tests more reliable ([#1534](https://github.com/mqttjs/MQTT.js/issues/1534)) ([1076143](https://github.com/mqttjs/MQTT.js/commit/1076143a7ed6b07b91ded9985cc9a0bbb5a84da4)) * problem with publish callback invoked twice ([#1635](https://github.com/mqttjs/MQTT.js/issues/1635)) ([79b23a8](https://github.com/mqttjs/MQTT.js/commit/79b23a8f76abaceec67f063b6da0ee57a2c60697)) * **types:** subscribe definition ([#1527](https://github.com/mqttjs/MQTT.js/issues/1527)) ([debb7d9](https://github.com/mqttjs/MQTT.js/commit/debb7d93c17f5b68704c160ccd88e7e1db87d92d)) * chore!: remove unused deps, convert to ES2015 class (#1633) ([d71b000](https://github.com/mqttjs/MQTT.js/commit/d71b000773e4954c9a2ecbf4f750dac58017ef1a)), closes [#1633](https://github.com/mqttjs/MQTT.js/issues/1633) ### BREAKING CHANGES * when creating an `MqttClient` instance `new` is now required # [5.0.0-beta.2](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.4) (2023-07-03) ### Bug Fixes * browser tests not working ([#1628](https://github.com/mqttjs/MQTT.js/issues/1628)) ([8775fcd](https://github.com/mqttjs/MQTT.js/commit/8775fcdad952b39fa4b79dbe912ca42033be030a)) * setImmediate polyfill ([#1626](https://github.com/mqttjs/MQTT.js/issues/1626)) ([0ed0754](https://github.com/mqttjs/MQTT.js/commit/0ed0754b95b92df51ed49ae63058b31fdba1d415)) ### Features * option to disable `writeCache` and fix leak in subscriptions ([#1622](https://github.com/mqttjs/MQTT.js/issues/1622)) ([c8aa654](https://github.com/mqttjs/MQTT.js/commit/c8aa6540dbf68ffb0d88c287e2c862b28d3fb6e6)), closes [#1535](https://github.com/mqttjs/MQTT.js/issues/1535) [#1151](https://github.com/mqttjs/MQTT.js/issues/1151) # [5.0.0-beta.1](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.4) (2023-06-29) ### Bug Fixes * `_storeProcessing` staying true after outStore got emptied ([#1492](https://github.com/mqttjs/MQTT.js/issues/1492)) ([f3f7be7](https://github.com/mqttjs/MQTT.js/commit/f3f7be76199115a622fde2590d44b1bb0cf57d41)) * consistency, used `this` instead of `that` ([#1618](https://github.com/mqttjs/MQTT.js/issues/1618)) ([800825b](https://github.com/mqttjs/MQTT.js/commit/800825bf619d83ef713a5b2fa1533bbf6ccac872)) * prevent store message on store when it's restored ([#1255](https://github.com/mqttjs/MQTT.js/issues/1255)) ([8d68c8c](https://github.com/mqttjs/MQTT.js/commit/8d68c8c3e38aede52741a06838933011a6fccc43)) # [5.0.0-beta.0](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.4) (2023-06-27) ### Bug Fixes * add missing export of UniqueMessageIdProvider and DefaultMessageIdProvider ([#1572](https://github.com/mqttjs/MQTT.js/issues/1572)) ([aa2e0ad](https://github.com/mqttjs/MQTT.js/commit/aa2e0ad49aadf333141f18cb85d2582abb8e19fc)) * IS_BROWSER check is now safer and more agnostic about the bundler ([#1571](https://github.com/mqttjs/MQTT.js/issues/1571)) ([b48b4b4](https://github.com/mqttjs/MQTT.js/commit/b48b4b4e79690c96033ea2df387c11f3bc26bf6a)) * **test:** `topicAliasMaximum` tests ([#1612](https://github.com/mqttjs/MQTT.js/issues/1612)) ([f1e5518](https://github.com/mqttjs/MQTT.js/commit/f1e5518150ea45067b87104abd9fed64ec13a48c)) * topicAliasMaximum under must be under Connect properties ([#1519](https://github.com/mqttjs/MQTT.js/issues/1519)) ([3b2e1cb](https://github.com/mqttjs/MQTT.js/commit/3b2e1cb7c4bf33ff66bcd1cc3091790a9635f19a)) * **types:** missing null declaration for error in subscription callback ([#1589](https://github.com/mqttjs/MQTT.js/issues/1589)) ([afc067b](https://github.com/mqttjs/MQTT.js/commit/afc067be2ca83990209b6176adec06f9a4c76a2c)) * **types:** topic alias controls and password ([#1509](https://github.com/mqttjs/MQTT.js/issues/1509)) ([85c9341](https://github.com/mqttjs/MQTT.js/commit/85c9341bba2676cfd069ec38a1a7cfda71647b68)) * chore!: drop support for node 12-14 (#1615) ([a2cbf61](https://github.com/mqttjs/MQTT.js/commit/a2cbf61c2a051a5ee69a50e00688e8ace79e7ef5)), closes [#1615](https://github.com/mqttjs/MQTT.js/issues/1615) ### BREAKING CHANGES * Dropped support for NodeJS 12-14 # [5.0.0-beta.3](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.3) (2023-07-19) ### Bug Fixes * make tests more reliable ([#1534](https://github.com/mqttjs/MQTT.js/issues/1534)) ([1076143](https://github.com/mqttjs/MQTT.js/commit/1076143a7ed6b07b91ded9985cc9a0bbb5a84da4)) * problem with publish callback invoked twice ([#1635](https://github.com/mqttjs/MQTT.js/issues/1635)) ([79b23a8](https://github.com/mqttjs/MQTT.js/commit/79b23a8f76abaceec67f063b6da0ee57a2c60697)) * **types:** subscribe definition ([#1527](https://github.com/mqttjs/MQTT.js/issues/1527)) ([debb7d9](https://github.com/mqttjs/MQTT.js/commit/debb7d93c17f5b68704c160ccd88e7e1db87d92d)) * chore!: remove unused deps, convert to ES2015 class (#1633) ([d71b000](https://github.com/mqttjs/MQTT.js/commit/d71b000773e4954c9a2ecbf4f750dac58017ef1a)), closes [#1633](https://github.com/mqttjs/MQTT.js/issues/1633) ### BREAKING CHANGES * when creating an `MqttClient` instance `new` is now required # [5.0.0-beta.2](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.3) (2023-07-03) ### Bug Fixes * browser tests not working ([#1628](https://github.com/mqttjs/MQTT.js/issues/1628)) ([8775fcd](https://github.com/mqttjs/MQTT.js/commit/8775fcdad952b39fa4b79dbe912ca42033be030a)) * setImmediate polyfill ([#1626](https://github.com/mqttjs/MQTT.js/issues/1626)) ([0ed0754](https://github.com/mqttjs/MQTT.js/commit/0ed0754b95b92df51ed49ae63058b31fdba1d415)) ### Features * option to disable `writeCache` and fix leak in subscriptions ([#1622](https://github.com/mqttjs/MQTT.js/issues/1622)) ([c8aa654](https://github.com/mqttjs/MQTT.js/commit/c8aa6540dbf68ffb0d88c287e2c862b28d3fb6e6)), closes [#1535](https://github.com/mqttjs/MQTT.js/issues/1535) [#1151](https://github.com/mqttjs/MQTT.js/issues/1151) # [5.0.0-beta.1](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.3) (2023-06-29) ### Bug Fixes * `_storeProcessing` staying true after outStore got emptied ([#1492](https://github.com/mqttjs/MQTT.js/issues/1492)) ([f3f7be7](https://github.com/mqttjs/MQTT.js/commit/f3f7be76199115a622fde2590d44b1bb0cf57d41)) * consistency, used `this` instead of `that` ([#1618](https://github.com/mqttjs/MQTT.js/issues/1618)) ([800825b](https://github.com/mqttjs/MQTT.js/commit/800825bf619d83ef713a5b2fa1533bbf6ccac872)) * prevent store message on store when it's restored ([#1255](https://github.com/mqttjs/MQTT.js/issues/1255)) ([8d68c8c](https://github.com/mqttjs/MQTT.js/commit/8d68c8c3e38aede52741a06838933011a6fccc43)) # [5.0.0-beta.0](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.3) (2023-06-27) ### Bug Fixes * add missing export of UniqueMessageIdProvider and DefaultMessageIdProvider ([#1572](https://github.com/mqttjs/MQTT.js/issues/1572)) ([aa2e0ad](https://github.com/mqttjs/MQTT.js/commit/aa2e0ad49aadf333141f18cb85d2582abb8e19fc)) * IS_BROWSER check is now safer and more agnostic about the bundler ([#1571](https://github.com/mqttjs/MQTT.js/issues/1571)) ([b48b4b4](https://github.com/mqttjs/MQTT.js/commit/b48b4b4e79690c96033ea2df387c11f3bc26bf6a)) * **test:** `topicAliasMaximum` tests ([#1612](https://github.com/mqttjs/MQTT.js/issues/1612)) ([f1e5518](https://github.com/mqttjs/MQTT.js/commit/f1e5518150ea45067b87104abd9fed64ec13a48c)) * topicAliasMaximum under must be under Connect properties ([#1519](https://github.com/mqttjs/MQTT.js/issues/1519)) ([3b2e1cb](https://github.com/mqttjs/MQTT.js/commit/3b2e1cb7c4bf33ff66bcd1cc3091790a9635f19a)) * **types:** missing null declaration for error in subscription callback ([#1589](https://github.com/mqttjs/MQTT.js/issues/1589)) ([afc067b](https://github.com/mqttjs/MQTT.js/commit/afc067be2ca83990209b6176adec06f9a4c76a2c)) * **types:** topic alias controls and password ([#1509](https://github.com/mqttjs/MQTT.js/issues/1509)) ([85c9341](https://github.com/mqttjs/MQTT.js/commit/85c9341bba2676cfd069ec38a1a7cfda71647b68)) * chore!: drop support for node 12-14 (#1615) ([a2cbf61](https://github.com/mqttjs/MQTT.js/commit/a2cbf61c2a051a5ee69a50e00688e8ace79e7ef5)), closes [#1615](https://github.com/mqttjs/MQTT.js/issues/1615) ### BREAKING CHANGES * Dropped support for NodeJS 12-14 # [5.0.0-beta.2](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.2) (2023-07-03) ### Bug Fixes * browser tests not working ([#1628](https://github.com/mqttjs/MQTT.js/issues/1628)) ([8775fcd](https://github.com/mqttjs/MQTT.js/commit/8775fcdad952b39fa4b79dbe912ca42033be030a)) * setImmediate polyfill ([#1626](https://github.com/mqttjs/MQTT.js/issues/1626)) ([0ed0754](https://github.com/mqttjs/MQTT.js/commit/0ed0754b95b92df51ed49ae63058b31fdba1d415)) ### Features * option to disable `writeCache` and fix leak in subscriptions ([#1622](https://github.com/mqttjs/MQTT.js/issues/1622)) ([c8aa654](https://github.com/mqttjs/MQTT.js/commit/c8aa6540dbf68ffb0d88c287e2c862b28d3fb6e6)), closes [#1535](https://github.com/mqttjs/MQTT.js/issues/1535) [#1151](https://github.com/mqttjs/MQTT.js/issues/1151) # [5.0.0-beta.1](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.2) (2023-06-29) ### Bug Fixes * `_storeProcessing` staying true after outStore got emptied ([#1492](https://github.com/mqttjs/MQTT.js/issues/1492)) ([f3f7be7](https://github.com/mqttjs/MQTT.js/commit/f3f7be76199115a622fde2590d44b1bb0cf57d41)) * consistency, used `this` instead of `that` ([#1618](https://github.com/mqttjs/MQTT.js/issues/1618)) ([800825b](https://github.com/mqttjs/MQTT.js/commit/800825bf619d83ef713a5b2fa1533bbf6ccac872)) * prevent store message on store when it's restored ([#1255](https://github.com/mqttjs/MQTT.js/issues/1255)) ([8d68c8c](https://github.com/mqttjs/MQTT.js/commit/8d68c8c3e38aede52741a06838933011a6fccc43)) # [5.0.0-beta.0](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.2) (2023-06-27) ### Bug Fixes * add missing export of UniqueMessageIdProvider and DefaultMessageIdProvider ([#1572](https://github.com/mqttjs/MQTT.js/issues/1572)) ([aa2e0ad](https://github.com/mqttjs/MQTT.js/commit/aa2e0ad49aadf333141f18cb85d2582abb8e19fc)) * IS_BROWSER check is now safer and more agnostic about the bundler ([#1571](https://github.com/mqttjs/MQTT.js/issues/1571)) ([b48b4b4](https://github.com/mqttjs/MQTT.js/commit/b48b4b4e79690c96033ea2df387c11f3bc26bf6a)) * **test:** `topicAliasMaximum` tests ([#1612](https://github.com/mqttjs/MQTT.js/issues/1612)) ([f1e5518](https://github.com/mqttjs/MQTT.js/commit/f1e5518150ea45067b87104abd9fed64ec13a48c)) * topicAliasMaximum under must be under Connect properties ([#1519](https://github.com/mqttjs/MQTT.js/issues/1519)) ([3b2e1cb](https://github.com/mqttjs/MQTT.js/commit/3b2e1cb7c4bf33ff66bcd1cc3091790a9635f19a)) * **types:** missing null declaration for error in subscription callback ([#1589](https://github.com/mqttjs/MQTT.js/issues/1589)) ([afc067b](https://github.com/mqttjs/MQTT.js/commit/afc067be2ca83990209b6176adec06f9a4c76a2c)) * **types:** topic alias controls and password ([#1509](https://github.com/mqttjs/MQTT.js/issues/1509)) ([85c9341](https://github.com/mqttjs/MQTT.js/commit/85c9341bba2676cfd069ec38a1a7cfda71647b68)) * chore!: drop support for node 12-14 (#1615) ([a2cbf61](https://github.com/mqttjs/MQTT.js/commit/a2cbf61c2a051a5ee69a50e00688e8ace79e7ef5)), closes [#1615](https://github.com/mqttjs/MQTT.js/issues/1615) ### BREAKING CHANGES * Dropped support for NodeJS 12-14 # [5.0.0-beta.1](https://github.com/mqttjs/MQTT.js/compare/v5.0.0-beta.0...v5.0.0-beta.1) (2023-06-29) ### Bug Fixes * `_storeProcessing` staying true after outStore got emptied ([#1492](https://github.com/mqttjs/MQTT.js/issues/1492)) ([f3f7be7](https://github.com/mqttjs/MQTT.js/commit/f3f7be76199115a622fde2590d44b1bb0cf57d41)) * consistency, used `this` instead of `that` ([#1618](https://github.com/mqttjs/MQTT.js/issues/1618)) ([800825b](https://github.com/mqttjs/MQTT.js/commit/800825bf619d83ef713a5b2fa1533bbf6ccac872)) * prevent store message on store when it's restored ([#1255](https://github.com/mqttjs/MQTT.js/issues/1255)) ([8d68c8c](https://github.com/mqttjs/MQTT.js/commit/8d68c8c3e38aede52741a06838933011a6fccc43)) # [5.0.0-beta.0](https://github.com/mqttjs/MQTT.js/compare/v4.3.7...v5.0.0-beta.0) (2023-06-27) ### Bug Fixes * add missing export of UniqueMessageIdProvider and DefaultMessageIdProvider ([#1572](https://github.com/mqttjs/MQTT.js/issues/1572)) ([aa2e0ad](https://github.com/mqttjs/MQTT.js/commit/aa2e0ad49aadf333141f18cb85d2582abb8e19fc)) * IS_BROWSER check is now safer and more agnostic about the bundler ([#1571](https://github.com/mqttjs/MQTT.js/issues/1571)) ([b48b4b4](https://github.com/mqttjs/MQTT.js/commit/b48b4b4e79690c96033ea2df387c11f3bc26bf6a)) * **test:** `topicAliasMaximum` tests ([#1612](https://github.com/mqttjs/MQTT.js/issues/1612)) ([f1e5518](https://github.com/mqttjs/MQTT.js/commit/f1e5518150ea45067b87104abd9fed64ec13a48c)) * topicAliasMaximum under must be under Connect properties ([#1519](https://github.com/mqttjs/MQTT.js/issues/1519)) ([3b2e1cb](https://github.com/mqttjs/MQTT.js/commit/3b2e1cb7c4bf33ff66bcd1cc3091790a9635f19a)) * **types:** missing null declaration for error in subscription callback ([#1589](https://github.com/mqttjs/MQTT.js/issues/1589)) ([afc067b](https://github.com/mqttjs/MQTT.js/commit/afc067be2ca83990209b6176adec06f9a4c76a2c)) * **types:** topic alias controls and password ([#1509](https://github.com/mqttjs/MQTT.js/issues/1509)) ([85c9341](https://github.com/mqttjs/MQTT.js/commit/85c9341bba2676cfd069ec38a1a7cfda71647b68)) * chore!: drop support for node 12-14 (#1615) ([a2cbf61](https://github.com/mqttjs/MQTT.js/commit/a2cbf61c2a051a5ee69a50e00688e8ace79e7ef5)), closes [#1615](https://github.com/mqttjs/MQTT.js/issues/1615) ### BREAKING CHANGES * Dropped support for NodeJS 12-14 ## [4.3.7](https://github.com/mqttjs/MQTT.js/compare/v4.3.6...v4.3.7) (2022-03-14) ### Bug Fixes * fix regression from [#1401](https://github.com/mqttjs/MQTT.js/issues/1401) and allow CI test failures to break gitthub workflow ([#1443](https://github.com/mqttjs/MQTT.js/issues/1443)) ([accd78e](https://github.com/mqttjs/MQTT.js/commit/accd78e38aa82c8cc1ea04029e56494276776c87)) ## [4.3.6](https://github.com/mqttjs/MQTT.js/compare/v4.3.5...v4.3.6) (2022-02-17) ### Bug Fixes * buffer is not defined in browser ([#1420](https://github.com/mqttjs/MQTT.js/issues/1420)) ([f5ab1b5](https://github.com/mqttjs/MQTT.js/commit/f5ab1b5d2a04813178fb478a7e345c0acf544258)) * **types:** connect function proper overloads for its parameters ([#1416](https://github.com/mqttjs/MQTT.js/issues/1416)) ([28c4040](https://github.com/mqttjs/MQTT.js/commit/28c4040c21246710f7ea3e161bc4145ba916c0de)) ## [4.3.5](https://github.com/mqttjs/MQTT.js/compare/v4.3.4...v4.3.5) (2022-02-07) ### Bug Fixes * **sendPacket:** drain leak ([#1401](https://github.com/mqttjs/MQTT.js/issues/1401)) ([7ec4b8f](https://github.com/mqttjs/MQTT.js/commit/7ec4b8fd602e220f50693cb83f082dab764ed3f2)) ## [4.3.4](https://github.com/mqttjs/MQTT.js/compare/v4.3.3...v4.3.4) (2022-01-06) ### Bug Fixes * migrate LruMap from collections to lru-cache. ([#1396](https://github.com/mqttjs/MQTT.js/issues/1396)) ([5c67037](https://github.com/mqttjs/MQTT.js/commit/5c670370c603f09ee25fbaba961156f59eaee1a2)) ## [4.3.3](https://github.com/mqttjs/MQTT.js/compare/v4.3.2...v4.3.3) (2022-01-05) ### Bug Fixes * remove collections.js dependency from number-allocator. ([#1394](https://github.com/mqttjs/MQTT.js/issues/1394)) ([ee75c32](https://github.com/mqttjs/MQTT.js/commit/ee75c322d6f31a3279f5a7f15ee4122760b1cc94)), closes [#1392](https://github.com/mqttjs/MQTT.js/issues/1392) ## [4.3.2](https://github.com/mqttjs/MQTT.js/compare/v4.3.1...v4.3.2) (2021-12-29) ### Bug Fixes * **dependency:** Updated collections.js related package version. ([#1386](https://github.com/mqttjs/MQTT.js/issues/1386)) ([df89a2e](https://github.com/mqttjs/MQTT.js/commit/df89a2edf4fa15d3f8d56cd0b8290f9ddde7ceb8)) ## [4.3.1](https://github.com/mqttjs/MQTT.js/compare/v4.3.0...v4.3.1) (2021-12-24) ### Bug Fixes * **dependencies:** remove babel-eslint and update snazzy ([#1383](https://github.com/mqttjs/MQTT.js/issues/1383)) ([66d43d4](https://github.com/mqttjs/MQTT.js/commit/66d43d4f33e6af405468c94112f3d1361af773dc)) # [4.3.0](https://github.com/mqttjs/MQTT.js/compare/v4.2.8...v4.3.0) (2021-12-22) ### Bug Fixes * **client:** Refined Topic Alias support. (Implement [#1300](https://github.com/mqttjs/MQTT.js/issues/1300)) ([#1301](https://github.com/mqttjs/MQTT.js/issues/1301)) ([c92b877](https://github.com/mqttjs/MQTT.js/commit/c92b877292d314e3e0b5d8f84b7f4b68a266aba2)) * **README:** typo Support ([#1353](https://github.com/mqttjs/MQTT.js/issues/1353)) ([c424426](https://github.com/mqttjs/MQTT.js/commit/c424426cd6345eba1f8016335839a667b3928e40)) * **resubscribe:** message id allocate twice ([#1337](https://github.com/mqttjs/MQTT.js/issues/1337)) ([7466819](https://github.com/mqttjs/MQTT.js/commit/7466819d62a5db554e41bf75e939a90f0dc46fe6)) * **tls:** Skip TLS SNI if host is IP address ([#1311](https://github.com/mqttjs/MQTT.js/issues/1311)) ([2679952](https://github.com/mqttjs/MQTT.js/commit/2679952587a0e3e1b5fcbfd6b11fca72c65fba95)) * **type:** add properties type for IClientSubscribeOptions ([#1378](https://github.com/mqttjs/MQTT.js/issues/1378)) ([8de9394](https://github.com/mqttjs/MQTT.js/commit/8de9394fa9afd61a1e0e726b0fe9d3637ed17cc9)) * **type:** fix push properties types ([#1359](https://github.com/mqttjs/MQTT.js/issues/1359)) ([cb6bdcb](https://github.com/mqttjs/MQTT.js/commit/cb6bdcb2c6c9e23f87bb24dbd1458eb0509cb02f)) * types ([#1341](https://github.com/mqttjs/MQTT.js/issues/1341)) ([59fab36](https://github.com/mqttjs/MQTT.js/commit/59fab369d2738edcf62306a67375763d737bc4ad)) * **typescript:** OnConnectCallback with specs expecting Connack packet ([#1333](https://github.com/mqttjs/MQTT.js/issues/1333)) ([e3e15c3](https://github.com/mqttjs/MQTT.js/commit/e3e15c3d791615a8fcab46b331678dd5a5a755a0)) * **typescript:** Use correct version of @types/ws ([#1358](https://github.com/mqttjs/MQTT.js/issues/1358)) ([6581d33](https://github.com/mqttjs/MQTT.js/commit/6581d3340602903d3434a0053eeabe7019595ea2)) * websocket and typescript ([9979443](https://github.com/mqttjs/MQTT.js/commit/997944380702c17d6b144b499685e591b3178c11)) * **websockets:** revert URL WHATWG changes ([a3dd38e](https://github.com/mqttjs/MQTT.js/commit/a3dd38ed4374b0baa359430472f34078369ef02c)) ### Features * add support for ALPN TLS extension ([#1332](https://github.com/mqttjs/MQTT.js/issues/1332)) ([06f2fd2](https://github.com/mqttjs/MQTT.js/commit/06f2fd2d7666ec462f9f21c3bd19c35797de9083)) * **client:** auth handler for enhanced auth ([#1380](https://github.com/mqttjs/MQTT.js/issues/1380)) ([d5850b7](https://github.com/mqttjs/MQTT.js/commit/d5850b7ba2653da84d53fcb57e5767e4b9cbb09d)) ### Reverts * Revert "fix: types (#1341)" (#1344) ([e6672e8](https://github.com/mqttjs/MQTT.js/commit/e6672e80a48db6273af6bde338035d473ee3305a)), closes [#1341](https://github.com/mqttjs/MQTT.js/issues/1341) [#1344](https://github.com/mqttjs/MQTT.js/issues/1344) ## [4.2.5](https://github.com/mqttjs/MQTT.js/compare/v4.2.4...v4.2.5) (2020-11-12) ### Bug Fixes * **auth opts:** Default to null for false-y values ([#1197](https://github.com/mqttjs/MQTT.js/issues/1197)) ([6a0e50a](https://github.com/mqttjs/MQTT.js/commit/6a0e50a52214f5e3b221d9f3d0bb86c5896e84c1)) ## [4.2.4](https://github.com/mqttjs/MQTT.js/compare/v4.2.3...v4.2.4) (2020-10-29) ### Bug Fixes * **ws:** add all parts of object to opts ([#1194](https://github.com/mqttjs/MQTT.js/issues/1194)) ([6240565](https://github.com/mqttjs/MQTT.js/commit/62405653b33ec5e5e0c8077e3bc9e9ee9a335cbe)) ## [4.2.3](https://github.com/mqttjs/MQTT.js/compare/v4.2.2...v4.2.3) (2020-10-27) ### Bug Fixes * **secure:** do not override password and username ([#1190](https://github.com/mqttjs/MQTT.js/issues/1190)) ([298dbb2](https://github.com/mqttjs/MQTT.js/commit/298dbb2e7e11e390794128b694a40986497b374c)) ## [4.2.2](https://github.com/mqttjs/MQTT.js/compare/v4.2.1...v4.2.2) (2020-10-27) ### Bug Fixes * check if client connected when reconnecting ([#1162](https://github.com/mqttjs/MQTT.js/issues/1162)) ([541f201](https://github.com/mqttjs/MQTT.js/commit/541f201834968eeee5b8599e3b29d8daecd4aac4)), closes [#1152](https://github.com/mqttjs/MQTT.js/issues/1152) * replace url.parse by WHATWG URL API ([#1147](https://github.com/mqttjs/MQTT.js/issues/1147)) ([70a247c](https://github.com/mqttjs/MQTT.js/commit/70a247c29e0b05ddd8755e7b9c8c41a4c25b431b)), closes [#1130](https://github.com/mqttjs/MQTT.js/issues/1130) * use 'readable-stream' instead of 'stream' ([#1170](https://github.com/mqttjs/MQTT.js/issues/1170)) ([04184e1](https://github.com/mqttjs/MQTT.js/commit/04184e16d349d020a520c0f77391f421a6755816)) ## [4.2.1](https://github.com/mqttjs/MQTT.js/compare/v4.2.0...v4.2.1) (2020-08-24) ### Bug Fixes * **websocket:** browser in ws ([#1145](https://github.com/mqttjs/MQTT.js/issues/1145)) ([40177ca](https://github.com/mqttjs/MQTT.js/commit/40177cac9a7d7e829b21963e1582c3eb9c13f20a)) # [4.2.0](https://github.com/mqttjs/MQTT.js/compare/v4.1.0...v4.2.0) (2020-08-12) ### Bug Fixes * **browser support:** correct browser detection for webpack ([#1135](https://github.com/mqttjs/MQTT.js/issues/1135)) ([eedc2b2](https://github.com/mqttjs/MQTT.js/commit/eedc2b26cd6063a0b1152432a00f70de5e0b9bae)) * **browser support:** do not use process.nextTick without check that it exists ([#1136](https://github.com/mqttjs/MQTT.js/issues/1136)) ([963e554](https://github.com/mqttjs/MQTT.js/commit/963e554d3da2e4149c6f99b4fbe3aad6e620b955)) * **mqtt stores:** improve error handling and tests ([#1133](https://github.com/mqttjs/MQTT.js/issues/1133)) ([9c61419](https://github.com/mqttjs/MQTT.js/commit/9c614192dc7f7be20f715b7236f13e0b60717dce)) * path for bin files ([#1107](https://github.com/mqttjs/MQTT.js/issues/1107)) ([43cc1d1](https://github.com/mqttjs/MQTT.js/commit/43cc1d1f96e32b022ead3c8ce9c6ff4cbe2c3820)) * **typescript:** fix payloadFormatIndicator to boolean type ([#1115](https://github.com/mqttjs/MQTT.js/issues/1115)) ([5adb12a](https://github.com/mqttjs/MQTT.js/commit/5adb12a6f73c63e47ff9acd54bbcaef4f11c4baa)) ### Features * **mqtt5:** add properties object to publish options ([e8326ce](https://github.com/mqttjs/MQTT.js/commit/e8326ce3baf06a1bcdbd70c33c5178bc06f8959a)) * **websockets:** websocket-streams to ws ([#1108](https://github.com/mqttjs/MQTT.js/issues/1108)) ([b2c1215](https://github.com/mqttjs/MQTT.js/commit/b2c121511c7437b64724e9f1e89ebcd27e3c2cce)) # [4.1.0](https://github.com/mqttjs/MQTT.js/compare/v4.0.1...v4.1.0) (2020-05-19) ## [4.0.1](https://github.com/mqttjs/MQTT.js/compare/v4.0.0...v4.0.1) (2020-05-07) ### Reverts * Revert "docs: adding client flowchart" ([ef2d590](https://github.com/mqttjs/MQTT.js/commit/ef2d5907efd5eed14aa3f46a2bf18b42ee0b3687)) # [4.0.0](https://github.com/mqttjs/MQTT.js/compare/v3.0.0...v4.0.0) (2020-04-27) ### Bug Fixes * remove only ([#1058](https://github.com/mqttjs/MQTT.js/issues/1058)) ([c8ee0e2](https://github.com/mqttjs/MQTT.js/commit/c8ee0e2c2380b87cab4a31a0fcabaab9100d62c7)) ### Features * **client:** error handling and test resilience ([#1076](https://github.com/mqttjs/MQTT.js/issues/1076)) ([2e46e08](https://github.com/mqttjs/MQTT.js/commit/2e46e08396f7a854ff53454bd0fa1f1d96b1dd27)) * connection error handler ([#1053](https://github.com/mqttjs/MQTT.js/issues/1053)) ([3cea393](https://github.com/mqttjs/MQTT.js/commit/3cea393e2608e4c091f6bccdcf2d7bfd703bb98b)) * support SNI on TLS ([#1055](https://github.com/mqttjs/MQTT.js/issues/1055)) ([f6534c2](https://github.com/mqttjs/MQTT.js/commit/f6534c2d8348afadc91c4d6c636447430be4642b)) # [3.0.0](https://github.com/mqttjs/MQTT.js/compare/v2.18.8...v3.0.0) (2019-05-27) ### Bug Fixes * delete completed incoming QOS 2 messages ([#893](https://github.com/mqttjs/MQTT.js/issues/893)) ([9a39faa](https://github.com/mqttjs/MQTT.js/commit/9a39faa37a3f12f10610af2b87b5be86375dc402)) ## [2.18.8](https://github.com/mqttjs/MQTT.js/compare/v2.18.7...v2.18.8) (2018-08-30) ## [2.18.7](https://github.com/mqttjs/MQTT.js/compare/v2.18.6...v2.18.7) (2018-08-26) ## [2.18.6](https://github.com/mqttjs/MQTT.js/compare/v2.18.5...v2.18.6) (2018-08-25) ## [2.18.5](https://github.com/mqttjs/MQTT.js/compare/v2.18.4...v2.18.5) (2018-08-23) ## [2.18.4](https://github.com/mqttjs/MQTT.js/compare/v2.18.3...v2.18.4) (2018-08-22) ## [2.18.3](https://github.com/mqttjs/MQTT.js/compare/v2.18.2...v2.18.3) (2018-07-19) ## [2.18.2](https://github.com/mqttjs/MQTT.js/compare/v2.18.1...v2.18.2) (2018-06-28) ## [2.18.1](https://github.com/mqttjs/MQTT.js/compare/v2.18.0...v2.18.1) (2018-06-12) # [2.18.0](https://github.com/mqttjs/MQTT.js/compare/v2.17.0...v2.18.0) (2018-05-12) # [2.17.0](https://github.com/mqttjs/MQTT.js/compare/v2.16.0...v2.17.0) (2018-03-25) # [2.16.0](https://github.com/mqttjs/MQTT.js/compare/v2.15.3...v2.16.0) (2018-03-01) ## [2.15.3](https://github.com/mqttjs/MQTT.js/compare/v2.15.2...v2.15.3) (2018-02-16) ## [2.15.2](https://github.com/mqttjs/MQTT.js/compare/v2.15.1...v2.15.2) (2018-02-08) ## [2.15.1](https://github.com/mqttjs/MQTT.js/compare/v2.15.0...v2.15.1) (2018-01-09) # [2.15.0](https://github.com/mqttjs/MQTT.js/compare/v2.14.0...v2.15.0) (2017-12-09) # [2.14.0](https://github.com/mqttjs/MQTT.js/compare/v2.13.1...v2.14.0) (2017-11-04) ## [2.13.1](https://github.com/mqttjs/MQTT.js/compare/v2.13.0...v2.13.1) (2017-10-16) # [2.13.0](https://github.com/mqttjs/MQTT.js/compare/v2.12.1...v2.13.0) (2017-09-12) ## [2.12.1](https://github.com/mqttjs/MQTT.js/compare/v2.12.0...v2.12.1) (2017-09-08) # [2.12.0](https://github.com/mqttjs/MQTT.js/compare/v2.11.0...v2.12.0) (2017-08-18) # [2.11.0](https://github.com/mqttjs/MQTT.js/compare/v2.10.0...v2.11.0) (2017-08-03) # [2.10.0](https://github.com/mqttjs/MQTT.js/compare/v2.9.3...v2.10.0) (2017-07-31) ## [2.9.3](https://github.com/mqttjs/MQTT.js/compare/v2.9.2...v2.9.3) (2017-07-25) ## [2.9.2](https://github.com/mqttjs/MQTT.js/compare/v2.9.1...v2.9.2) (2017-07-21) ## [2.9.1](https://github.com/mqttjs/MQTT.js/compare/v2.9.0...v2.9.1) (2017-07-06) # [2.9.0](https://github.com/mqttjs/MQTT.js/compare/v2.8.2...v2.9.0) (2017-06-16) ## [2.8.2](https://github.com/mqttjs/MQTT.js/compare/v2.8.1...v2.8.2) (2017-06-06) ## [2.8.1](https://github.com/mqttjs/MQTT.js/compare/v2.8.0...v2.8.1) (2017-06-03) # [2.8.0](https://github.com/mqttjs/MQTT.js/compare/v2.7.2...v2.8.0) (2017-05-26) ## [2.7.2](https://github.com/mqttjs/MQTT.js/compare/v2.7.0...v2.7.2) (2017-05-15) # [2.7.0](https://github.com/mqttjs/MQTT.js/compare/v2.6.2...v2.7.0) (2017-05-01) ## [2.6.2](https://github.com/mqttjs/MQTT.js/compare/v2.6.1...v2.6.2) (2017-04-10) ## [2.6.1](https://github.com/mqttjs/MQTT.js/compare/v2.6.0...v2.6.1) (2017-04-09) # [2.6.0](https://github.com/mqttjs/MQTT.js/compare/v2.5.2...v2.6.0) (2017-04-07) ## [2.5.2](https://github.com/mqttjs/MQTT.js/compare/v2.5.1...v2.5.2) (2017-04-03) ## [2.5.1](https://github.com/mqttjs/MQTT.js/compare/v2.5.0...v2.5.1) (2017-04-01) # [2.5.0](https://github.com/mqttjs/MQTT.js/compare/v2.4.0...v2.5.0) (2017-03-18) # [2.4.0](https://github.com/mqttjs/MQTT.js/compare/v2.3.1...v2.4.0) (2017-02-14) ## [2.3.1](https://github.com/mqttjs/MQTT.js/compare/v2.3.0...v2.3.1) (2017-01-30) # [2.3.0](https://github.com/mqttjs/MQTT.js/compare/v2.2.1...v2.3.0) (2017-01-23) ## [2.2.1](https://github.com/mqttjs/MQTT.js/compare/v2.2.0...v2.2.1) (2017-01-07) # [2.2.0](https://github.com/mqttjs/MQTT.js/compare/v2.1.3...v2.2.0) (2017-01-04) ## [2.1.3](https://github.com/mqttjs/MQTT.js/compare/v2.1.2...v2.1.3) (2016-11-17) ## [2.1.2](https://github.com/mqttjs/MQTT.js/compare/v2.1.1...v2.1.2) (2016-11-17) ## [2.1.1](https://github.com/mqttjs/MQTT.js/compare/v2.1.0...v2.1.1) (2016-11-13) # [2.1.0](https://github.com/mqttjs/MQTT.js/compare/v2.0.1...v2.1.0) (2016-11-13) ## [2.0.1](https://github.com/mqttjs/MQTT.js/compare/v2.0.0...v2.0.1) (2016-09-26) # [2.0.0](https://github.com/mqttjs/MQTT.js/compare/v1.14.1...v2.0.0) (2016-09-15) ## [1.14.1](https://github.com/mqttjs/MQTT.js/compare/v1.14.0...v1.14.1) (2016-08-25) # [1.14.0](https://github.com/mqttjs/MQTT.js/compare/v1.13.0...v1.14.0) (2016-08-17) # [1.13.0](https://github.com/mqttjs/MQTT.js/compare/v1.12.0...v1.13.0) (2016-07-25) # [1.12.0](https://github.com/mqttjs/MQTT.js/compare/v1.11.2...v1.12.0) (2016-06-25) ## [1.11.2](https://github.com/mqttjs/MQTT.js/compare/v1.11.1...v1.11.2) (2016-06-17) ## [1.11.1](https://github.com/mqttjs/MQTT.js/compare/v1.11.0...v1.11.1) (2016-06-16) # [1.11.0](https://github.com/mqttjs/MQTT.js/compare/v1.10.0...v1.11.0) (2016-06-04) # [1.10.0](https://github.com/mqttjs/MQTT.js/compare/v1.9.0...v1.10.0) (2016-04-27) # [1.9.0](https://github.com/mqttjs/MQTT.js/compare/v1.8.0...v1.9.0) (2016-04-25) # [1.8.0](https://github.com/mqttjs/MQTT.js/compare/v1.7.5...v1.8.0) (2016-04-10) ## [1.7.5](https://github.com/mqttjs/MQTT.js/compare/v1.7.4...v1.7.5) (2016-03-18) ## [0.17.4](https://github.com/mqttjs/MQTT.js/compare/v1.7.3...v0.17.4) (2016-03-18) ## [1.7.3](https://github.com/mqttjs/MQTT.js/compare/v1.7.2...v1.7.3) (2016-02-27) ## [1.7.2](https://github.com/mqttjs/MQTT.js/compare/v1.7.1...v1.7.2) (2016-02-18) ## [1.7.1](https://github.com/mqttjs/MQTT.js/compare/v1.7.0...v1.7.1) (2016-02-09) # [1.7.0](https://github.com/mqttjs/MQTT.js/compare/v1.6.3...v1.7.0) (2016-01-22) ## [1.6.3](https://github.com/mqttjs/MQTT.js/compare/v1.6.2...v1.6.3) (2015-12-23) ## [1.6.2](https://github.com/mqttjs/MQTT.js/compare/v1.6.1...v1.6.2) (2015-12-20) ## [1.6.1](https://github.com/mqttjs/MQTT.js/compare/v1.6.0...v1.6.1) (2015-12-10) # [1.6.0](https://github.com/mqttjs/MQTT.js/compare/v1.5.0...v1.6.0) (2015-11-28) # [1.5.0](https://github.com/mqttjs/MQTT.js/compare/v1.4.3...v1.5.0) (2015-10-26) ## [1.4.3](https://github.com/mqttjs/MQTT.js/compare/v1.4.2...v1.4.3) (2015-10-02) ## [1.4.2](https://github.com/mqttjs/MQTT.js/compare/v1.4.1...v1.4.2) (2015-10-02) ## [1.4.1](https://github.com/mqttjs/MQTT.js/compare/v1.4.0...v1.4.1) (2015-09-15) # [1.4.0](https://github.com/mqttjs/MQTT.js/compare/v1.3.5...v1.4.0) (2015-09-02) ## [1.3.5](https://github.com/mqttjs/MQTT.js/compare/v1.3.4...v1.3.5) (2015-07-12) ## [1.3.4](https://github.com/mqttjs/MQTT.js/compare/v1.3.3...v1.3.4) (2015-07-07) ## [1.3.3](https://github.com/mqttjs/MQTT.js/compare/v1.3.2...v1.3.3) (2015-07-03) ## [1.3.2](https://github.com/mqttjs/MQTT.js/compare/v1.3.1...v1.3.2) (2015-06-26) ## [1.3.1](https://github.com/mqttjs/MQTT.js/compare/v1.3.0...v1.3.1) (2015-06-22) # [1.3.0](https://github.com/mqttjs/MQTT.js/compare/v1.2.1...v1.3.0) (2015-06-11) ## [1.2.1](https://github.com/mqttjs/MQTT.js/compare/v1.2.0...v1.2.1) (2015-06-08) # [1.2.0](https://github.com/mqttjs/MQTT.js/compare/v1.1.5...v1.2.0) (2015-05-21) ## [1.1.5](https://github.com/mqttjs/MQTT.js/compare/v1.1.4...v1.1.5) (2015-05-15) ## [1.1.4](https://github.com/mqttjs/MQTT.js/compare/v1.1.3...v1.1.4) (2015-05-10) ## [1.1.3](https://github.com/mqttjs/MQTT.js/compare/v1.1.2...v1.1.3) (2015-04-06) ## [1.1.2](https://github.com/mqttjs/MQTT.js/compare/v1.1.1...v1.1.2) (2015-03-16) ## [1.1.1](https://github.com/mqttjs/MQTT.js/compare/v1.1.0...v1.1.1) (2015-03-12) # [1.1.0](https://github.com/mqttjs/MQTT.js/compare/v1.0.11...v1.1.0) (2015-02-28) ## [1.0.11](https://github.com/mqttjs/MQTT.js/compare/v1.0.10...v1.0.11) (2015-02-28) ## [1.0.10](https://github.com/mqttjs/MQTT.js/compare/v1.0.9...v1.0.10) (2015-02-15) ### Reverts * Revert "Use port for protocol when none is provided" ([ed01032](https://github.com/mqttjs/MQTT.js/commit/ed010327d4ba8370612418ba780ae7ffef66c66e)) ## [1.0.9](https://github.com/mqttjs/MQTT.js/compare/v1.0.8...v1.0.9) (2015-02-13) ## [1.0.8](https://github.com/mqttjs/MQTT.js/compare/v1.0.7...v1.0.8) (2015-02-06) ## [1.0.7](https://github.com/mqttjs/MQTT.js/compare/v1.0.6...v1.0.7) (2015-02-01) ## [1.0.6](https://github.com/mqttjs/MQTT.js/compare/v1.0.5...v1.0.6) (2015-01-29) ## [1.0.5](https://github.com/mqttjs/MQTT.js/compare/v1.0.4...v1.0.5) (2015-01-22) ## [1.0.4](https://github.com/mqttjs/MQTT.js/compare/v1.0.3...v1.0.4) (2015-01-22) ## [1.0.3](https://github.com/mqttjs/MQTT.js/compare/v1.0.2...v1.0.3) (2015-01-21) ## [1.0.2](https://github.com/mqttjs/MQTT.js/compare/v1.0.1...v1.0.2) (2015-01-19) ## [1.0.1](https://github.com/mqttjs/MQTT.js/compare/v1.0.0...v1.0.1) (2015-01-13) # [1.0.0](https://github.com/mqttjs/MQTT.js/compare/v0.3.13...v1.0.0) (2015-01-13) ## [0.3.13](https://github.com/mqttjs/MQTT.js/compare/v0.3.12...v0.3.13) (2014-11-11) ## [0.3.12](https://github.com/mqttjs/MQTT.js/compare/v0.3.11...v0.3.12) (2014-08-31) ## [0.3.11](https://github.com/mqttjs/MQTT.js/compare/v0.3.10...v0.3.11) (2014-07-11) ## [0.3.10](https://github.com/mqttjs/MQTT.js/compare/v0.3.9...v0.3.10) (2014-06-21) ## [0.3.9](https://github.com/mqttjs/MQTT.js/compare/v0.3.8...v0.3.9) (2014-05-27) ## [0.3.8](https://github.com/mqttjs/MQTT.js/compare/v0.3.7...v0.3.8) (2014-03-19) ## [0.3.7](https://github.com/mqttjs/MQTT.js/compare/v0.3.6...v0.3.7) (2013-11-28) ## [0.3.6](https://github.com/mqttjs/MQTT.js/compare/v0.3.5...v0.3.6) (2013-11-28) ## [0.3.5](https://github.com/mqttjs/MQTT.js/compare/v0.3.4...v0.3.5) (2013-11-27) ## [0.3.3](https://github.com/mqttjs/MQTT.js/compare/v0.3.2...v0.3.3) (2013-09-30) ## [0.3.2](https://github.com/mqttjs/MQTT.js/compare/v0.3.1...v0.3.2) (2013-09-19) ## [0.3.1](https://github.com/mqttjs/MQTT.js/compare/v0.3.0...v0.3.1) (2013-08-22) # [0.3.0](https://github.com/mqttjs/MQTT.js/compare/v0.2.11...v0.3.0) (2013-08-21) ### Reverts * Revert "Adding a little buffer to cope with slow connections." ([ff1e3ed](https://github.com/mqttjs/MQTT.js/commit/ff1e3ed8613d4d57b66318019b64f5cb160b1bb2)) ## [0.2.11](https://github.com/mqttjs/MQTT.js/compare/v0.2.10...v0.2.11) (2013-07-20) ## [0.2.10](https://github.com/mqttjs/MQTT.js/compare/v0.2.9...v0.2.10) (2013-06-12) ## [0.2.9](https://github.com/mqttjs/MQTT.js/compare/v0.2.8...v0.2.9) (2013-05-29) ## [0.2.8](https://github.com/mqttjs/MQTT.js/compare/v0.2.5...v0.2.8) (2013-05-27) ## [0.2.5](https://github.com/mqttjs/MQTT.js/compare/v0.2.4...v0.2.5) (2013-03-23) ## [0.2.4](https://github.com/mqttjs/MQTT.js/compare/v0.2.3...v0.2.4) (2013-03-07) ## [0.2.3](https://github.com/mqttjs/MQTT.js/compare/v0.2.2...v0.2.3) (2013-03-07) ## [0.2.2](https://github.com/mqttjs/MQTT.js/compare/0.2.0...v0.2.2) (2013-03-06) # [0.2.0](https://github.com/mqttjs/MQTT.js/compare/0.1.8...0.2.0) (2013-02-28) ## [0.1.8](https://github.com/mqttjs/MQTT.js/compare/v0.1.3...0.1.8) (2013-02-12) ## [0.1.3](https://github.com/mqttjs/MQTT.js/compare/v0.1.2...v0.1.3) (2012-02-06) ## [0.1.2](https://github.com/mqttjs/MQTT.js/compare/v0.1.1...v0.1.2) (2012-01-23) ## [0.1.1](https://github.com/mqttjs/MQTT.js/compare/v0.1.0...v0.1.1) (2012-01-18) # 0.1.0 (2012-01-17) ================================================ FILE: CONTRIBUTING.md ================================================ # MQTT.js is an OPEN Open Source Project ----------------------------------------- ## What? Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. ## Rules There are a few basic ground-rules for contributors: 1. **No `--force` pushes** or modifying the Git history in any way. 1. **Non-main branches** ought to be used for ongoing work. 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 1. Contributors should attempt to adhere to the prevailing code-style. ## Releases Declaring formal releases remains the prerogative of the project maintainer. ## Changes to this arrangement This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. ----------------------------------------- ================================================ FILE: DEVELOPMENT.md ================================================ # MQTTjs Development This document aims to help you get started with developing MQTT.js. ## Release Process In order to create a new release you have two options: 1. Locally run `npm run release` and follow the interactive CLI 2. Manually trigger the GitHub Action `release` workflow specifyin the type of release you want to create ## Tests To run the tests, you can use the following command: ```sh npm test ``` This will run both `browser` and `node` tests. ### Running specific tests For example, you can run `node -r esbuild-register --test test/keepaliveManager.ts` ### Browser Browser tests use [`wtr`](https://modern-web.dev/docs/test-runner/overview/) as the test runner. To build browser bundle using [esbuild](https://esbuild.github.io/) and run browser tests, you can use the following command: ```sh npm run test:browser ``` The configuration file is [web-test-runner.config.msj](./web-test-runner.config.mjs). It starts a local broker using [aedes-cli](https://github.com/moscajs/aedes-cli) with `ws` and `wss` support and then runs the tests in 3 different browsers: `chrome`, `firefox` and `safari`. The tests are located in the `test/browser` directory and there are also tests for service workers in the `test/browser/worker.js` directory. When developing/debugging tests it's useful to run the tests in a single browser, for example: ```sh npx wtr --manual --open ``` This will open the browser on `localhost:8001` and lets you choose the test to run by clicking on the link with the test name. By opening the DevTools you will be able to see the tests output and put debugger in both worker and main tests files. Be aware that tests will use the bundled version of the library, so you need to run `npm run build` before running the tests. If you need to debug issues in the code it could be useful to enable source maps when building, in order to do this just set `sourcemap: true` in [esbuild.js](./esbuild.js) file and run `npm run build`. ### Node For NodeJS tests we use the NodeJS [Test Runner](https://nodejs.org/api/test.html). To run the tests, you can use the following command: ```sh npm run test:node ``` The tests are located in the `test` directory. The entrypoint of tests is `runTests.ts` file. It is used to filter the tests to run, set concurrency and create a nice looking tests summary (see reason [here](https://github.com/nodejs/help/issues/3902#issuecomment-1726033310)) When developing/debugging tests it's useful to run the tests for a single file/test, for example: ```sh node -r esbuild-register --test --inspect test/client.ts ``` If you want to run tests using a filter, you can use the `--test-name-pattern` flag: ```sh node -r esbuild-register --test --test-name-pattern="should resend in-flight QoS" --inspect test/client.ts ``` You can also run tests in watch mode using the `--watch` flag. ## Lint You can run and automatically fix linting issues with ```sh npm run lint-fix ``` ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) ===================== Copyright (c) 2015-2016 MQTT.js contributors --------------------------------------- *MQTT.js contributors listed at * Copyright 2011-2014 by Adam Rudd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ![mqtt.js](https://raw.githubusercontent.com/mqttjs/MQTT.js/137ee0e3940c1f01049a30248c70f24dc6e6f829/MQTT.js.png) ![Github Test Status](https://github.com/mqttjs/MQTT.js/workflows/MQTT.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/mqttjs/MQTT.js/branch/master/graph/badge.svg)](https://codecov.io/gh/mqttjs/MQTT.js) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/mqttjs/MQTT.js/graphs/commit-activity) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/mqttjs/MQTT.js/pulls) [![node](https://img.shields.io/node/v/mqtt.svg) ![npm](https://img.shields.io/npm/v/mqtt.svg?logo=npm)](https://www.npmjs.com/package/mqtt) [![NPM Downloads](https://img.shields.io/npm/dm/mqtt.svg)](https://npm-compare.com/mqtt/#timeRange=THREE_YEARS) MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written in JavaScript for node.js and the browser. ## Table of Contents - [Upgrade notes](#notes) - [Installation](#install) - [Example](#example) - [React Native](#react-native) - [Import Styles](#example) - [Command Line Tools](#cli) - [API](#api) - [Browser](#browser) - [About QoS](#qos) - [TypeScript](#typescript) - [Weapp and Ali support](#weapp-alipay) - [Contributing](#contributing) - [Sponsor](#sponsor) - [License](#license) MQTT.js is an OPEN Open Source Project, see the [Contributing](#contributing) section to find out what this means. [![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) ## Important notes for existing users **v5.0.0** (07/2023) - Removes support for all end of life node versions (v12 and v14), and now supports node v18 and v20. - Completely rewritten in Typescript 🚀. - When creating `MqttClient` instance `new` is now required. **v4.0.0** (Released 04/2020) removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to debug logging, along with some feature additions. As a **breaking change**, by default a error handler is built into the MQTT.js client, so if any errors are emitted and the user has not created an event handler on the client for errors, the client will not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. **v3.0.0** adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. **Note:** MQTT v5 support is experimental as it has not been implemented by brokers yet. **v2.0.0** removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending packets. It also removes all the deprecated functionality in v1.0.0, mainly `mqtt.createConnection` and `mqtt.Server`. From v2.0.0, subscriptions are restored upon reconnection if `clean: true`. v1.x.x is now in _LTS_, and it will keep being supported as long as there are v0.8, v0.10 and v0.12 users. As a **breaking change**, the `encoding` option in the old client is removed, and now everything is UTF-8 with the exception of the `password` in the CONNECT message and `payload` in the PUBLISH message, which are `Buffer`. Another **breaking change** is that MQTT.js now defaults to MQTT v3.1.1, so to support old brokers, please read the [client options doc](#client). **v1.0.0** improves the overall architecture of the project, which is now split into three components: MQTT.js keeps the Client, [mqtt-connection](http://npm.im/mqtt-connection) includes the barebone Connection code for server-side usage, and [mqtt-packet](http://npm.im/mqtt-packet) includes the protocol parser and generator. The new Client improves performance by a 30% factor, embeds Websocket support ([MOWS](http://npm.im/mows) is now deprecated), and it has a better support for QoS 1 and 2. The previous API is still supported but deprecated, as such, it is not documented in this README. ## Installation ```sh npm install mqtt --save ``` ## Example For the sake of simplicity, let's put the subscriber and the publisher in the same file: ```js const mqtt = require("mqtt"); const client = mqtt.connect("mqtt://test.mosquitto.org"); client.on("connect", () => { client.subscribe("presence", (err) => { if (!err) { client.publish("presence", "Hello mqtt"); } }); }); client.on("message", (topic, message) => { // message is Buffer console.log(message.toString()); client.end(); }); ``` output: ```sh Hello mqtt ``` ### React Native MQTT.js can be used in React Native applications. To use it, see the [React Native example](https://github.com/MaximoLiberata/react-native-mqtt.js-example) If you want to run your own MQTT broker, you can use [Mosquitto](http://mosquitto.org) or [Aedes-cli](https://github.com/moscajs/aedes-cli), and launch it. You can also use a test instance: test.mosquitto.org. If you do not want to install a separate broker, you can try using the [Aedes](https://github.com/moscajs/aedes). ## Import styles ### CommonJS (Require) ```js const mqtt = require("mqtt") // require mqtt const client = mqtt.connect("mqtt://test.mosquitto.org") // create a client ``` ### ES6 Modules (Import) #### Default import ```js import mqtt from "mqtt"; // import namespace "mqtt" let client = mqtt.connect("mqtt://test.mosquitto.org"); // create a client ``` #### Importing individual components ```js import { connect } from "mqtt"; // import connect from mqtt let client = connect("mqtt://test.mosquitto.org"); // create a client ``` ## Command Line Tools MQTT.js bundles a command to interact with a broker. In order to have it available on your path, you should install MQTT.js globally: ```sh npm install mqtt -g ``` Then, on one terminal ```sh mqtt sub -t 'hello' -h 'test.mosquitto.org' -v ``` On another ```sh mqtt pub -t 'hello' -h 'test.mosquitto.org' -m 'from MQTT.js' ``` See `mqtt help ` for the command help. ## Debug Logs MQTT.js uses the [debug](https://www.npmjs.com/package/debug#cmd) package for debugging purposes. To enable debug logs, add the following environment variable on runtime : ```ps # (example using PowerShell, the VS Code default) $env:DEBUG='mqttjs*' ``` ## About Reconnection An important part of any websocket connection is what to do when a connection drops off and the client needs to reconnect. MQTT has built-in reconnection support that can be configured to behave in ways that suit the application. #### Refresh Authentication Options / Signed Urls with `transformWsUrl` (Websocket Only) When an mqtt connection drops and needs to reconnect, it's common to require that any authentication associated with the connection is kept current with the underlying auth mechanism. For instance some applications may pass an auth token with connection options on the initial connection, while other cloud services may require a url be signed with each connection. By the time the reconnect happens in the application lifecycle, the original auth data may have expired. To address this we can use a hook called `transformWsUrl` to manipulate either of the connection url or the client options at the time of a reconnect. Example (update clientId & username on each reconnect): ```js const transformWsUrl = (url, options, client) => { client.options.username = `token=${this.get_current_auth_token()}`; client.options.clientId = `${this.get_updated_clientId()}`; return `${this.get_signed_cloud_url(url)}`; } const connection = await mqtt.connectAsync(, { ..., transformWsUrl: transformUrl, }); ``` Now every time a new WebSocket connection is opened (hopefully not too often), we will get a fresh signed url or fresh auth token data. Note: Currently this hook does _not_ support promises, meaning that in order to use the latest auth token, you must have some outside mechanism running that handles application-level authentication refreshing so that the websocket connection can simply grab the latest valid token or signed url. #### Customize Websockets with `createWebsocket` (Websocket Only) When you need to add a custom websocket subprotocol or header to open a connection through a proxy with custom authentication this callback allows you to create your own instance of a websocket which will be used in the mqtt client. ```js const createWebsocket = (url, websocketSubProtocols, options) => { const subProtocols = [ websocketSubProtocols[0], 'myCustomSubprotocolOrOAuthToken', ] return new WebSocket(url, subProtocols) } const client = await mqtt.connectAsync(, { ..., createWebsocket: createWebsocket, }); ``` #### Enabling Reconnection with `reconnectPeriod` option To ensure that the mqtt client automatically tries to reconnect when the connection is dropped, you must set the client option `reconnectPeriod` to a value greater than 0. A value of 0 will disable reconnection and then terminate the final connection when it drops. The default value is 1000 ms which means it will try to reconnect 1 second after losing the connection. Note that this will only enable reconnects after either a connection timeout, or after a successful connection. It will _not_ (by default) enable retrying connections that are actively denied with a CONNACK error by the server. To also enable automatic reconnects for CONNACK errors, set `reconnectOnConnackError: true`. ## About Topic Alias Management ### Enabling automatic Topic Alias using If the client sets the option `autoUseTopicAlias:true` then MQTT.js uses existing topic alias automatically. example scenario: ```bash 1. PUBLISH topic:'t1', ta:1 (register) 2. PUBLISH topic:'t1' -> topic:'', ta:1 (auto use existing map entry) 3. PUBLISH topic:'t2', ta:1 (register overwrite) 4. PUBLISH topic:'t2' -> topic:'', ta:1 (auto use existing map entry based on the receent map) 5. PUBLISH topic:'t1' (t1 is no longer mapped to ta:1) ``` User doesn't need to manage which topic is mapped to which topic alias. If the user want to register topic alias, then publish topic with topic alias. If the user want to use topic alias, then publish topic without topic alias. If there is a mapped topic alias then added it as a property and update the topic to empty string. ### Enabling automatic Topic Alias assign If the client sets the option `autoAssignTopicAlias:true` then MQTT.js uses existing topic alias automatically. If no topic alias exists, then assign a new vacant topic alias automatically. If topic alias is fully used, then LRU(Least Recently Used) topic-alias entry is overwritten. example scenario: ```bash The broker returns CONNACK (TopicAliasMaximum:3) 1. PUBLISH topic:'t1' -> 't1', ta:1 (auto assign t1:1 and register) 2. PUBLISH topic:'t1' -> '' , ta:1 (auto use existing map entry) 3. PUBLISH topic:'t2' -> 't2', ta:2 (auto assign t1:2 and register. 2 was vacant) 4. PUBLISH topic:'t3' -> 't3', ta:3 (auto assign t1:3 and register. 3 was vacant) 5. PUBLISH topic:'t4' -> 't4', ta:1 (LRU entry is overwritten) ``` Also user can manually register topic-alias pair using PUBLISH topic:'some', ta:X. It works well with automatic topic alias assign. ## API - [`mqtt.connect()`](#connect) - [`mqtt.connectAsync()`](#connect-async) - [`mqtt.Client()`](#client) - [`mqtt.Client#connect()`](#client-connect) - [`mqtt.Client#publish()`](#publish) - [`mqtt.Client#publishAsync()`](#publish-async) - [`mqtt.Client#subscribe()`](#subscribe) - [`mqtt.Client#subscribeAsync()`](#subscribe-async) - [`mqtt.Client#unsubscribe()`](#unsubscribe) - [`mqtt.Client#unsubscribeAsync()`](#unsubscribe-async) - [`mqtt.Client#end()`](#end) - [`mqtt.Client#endAsync()`](#end-async) - [`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) --- ### mqtt.connect([url], options) Connects to the broker specified by the given url and options and returns a [Client](#client). The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', 'tls', 'ws', 'wss', 'wxs', 'alis'. If you are trying to connect to a unix socket just append the `+unix` suffix to the protocol (ex: `mqtt+unix`). This will set the `unixSocket` property automatically. The URL can also be an object as returned by [`URL.parse()`](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost), in that case the two objects are merged, i.e. you can pass a single object with both the URL and the connect options. You can also specify a `servers` options with content: `[{ host: 'localhost', port: 1883 }, ... ]`, in that case that array is iterated at every connect. For all MQTT-related options, see the [Client](#client) constructor. ### connectAsync([url], options) Asynchronous wrapper around the [`connect`](#connect) function. Returns a `Promise` that resolves to a `mqtt.Client` instance when the client fires a `'connect'` or `'end'` event, or rejects with an error if the `'error'` is fired. Note that the `manualConnect` option will cause the promise returned by this function to never resolve or reject as the underlying client never fires any events. --- ### mqtt.Client(streamBuilder, options) The `Client` class wraps a client connection to an MQTT broker over an arbitrary transport method (TCP, TLS, WebSocket, ecc). `Client` is an [EventEmitter](https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter) that has it's own [events](#events) `Client` automatically handles the following: - Regular server pings - QoS flow - Automatic reconnections - Start publishing before being connected The arguments are: - `streamBuilder` is a function that returns a subclass of the `Stream` class that supports 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: . - `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)` - `protocolId`: `'MQTT'` - `protocolVersion`: `4` - `clean`: `true`, set to false to receive QoS 1 and 2 messages while offline - `reconnectPeriod`: `1000` milliseconds, interval between two reconnections. Disable auto reconnect by setting to `0`. - `reconnectOnConnackError`: `false`, whether to also reconnect if a CONNACK is received with an error. - `connectTimeout`: `30 * 1000` milliseconds, time to wait before a CONNACK is received - `username`: the username required by your broker, if any - `password`: the password required by your broker, if any - `socksProxy`: establish TCP and TLS connections via a socks proxy (URL, supported protocols are `socks5://`, `socks5h://`, `socks4://`, `socks4a://`) - `socksTimeout`: timeout for connecting to the socks proxy - `incomingStore`: a [Store](#store) for the incoming packets - `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 with calling done(error, reasonCode)*/} ``` - `autoUseTopicAlias`: enabling automatic Topic Alias using functionality - `autoAssignTopicAlias`: enabling automatic Topic Alias assign functionality - `properties`: properties MQTT 5.0. `object` that supports the following properties: - `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, - `receiveMaximum`: representing the Receive Maximum value `number`, - `maximumPacketSize`: representing the Maximum Packet Size the Client is willing to accept `number`, - `topicAliasMaximum`: representing the Topic Alias Maximum value indicates the highest value that the Client will accept as a Topic Alias sent by the Server `number`, - `requestResponseInformation`: The Client uses this value to request the Server to return Response Information in the CONNACK `boolean`, - `requestProblemInformation`: The Client uses this value to indicate whether the Reason String or User Properties are sent in the case of failures `boolean`, - `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - `authenticationMethod`: the name of the authentication method used for extended authentication `string`, - `authenticationData`: Binary Data containing authentication data `binary` - `authPacket`: settings for auth packet `object` - `will`: a message that will sent by the broker automatically when the client disconnect badly. The format is: - `topic`: the topic to publish - `payload`: the message to publish - `qos`: the QoS - `retain`: the retain flag - `properties`: properties of will by MQTT 5.0: - `willDelayInterval`: representing the Will Delay Interval in seconds `number`, - `payloadFormatIndicator`: Will Message is UTF-8 Encoded Character Data or not `boolean`, - `messageExpiryInterval`: value is the lifetime of the Will Message in seconds and is sent as the Publication Expiry Interval when the Server publishes the Will Message `number`, - `contentType`: describing the content of the Will Message `string`, - `responseTopic`: String which is used as the Topic Name for a response message `string`, - `correlationData`: The Correlation Data is used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, - `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` - `transformWsUrl` : optional `(url, options, client) => url` function For ws/wss protocols only. Can be used to implement signing urls which upon reconnect can have become expired. - `createWebsocket` : optional `url, websocketSubProtocols, options) => Websocket` function For ws/wss protocols only. Can be used to implement a custom websocket subprotocol or implementation. - `resubscribe` : if connection is broken and reconnects, subscribed topics are automatically subscribed again (default `true`) - `subscribeBatchSize` : optional `number` Maximum number of topics per SUBSCRIBE packet. When the number of topics to subscribe exceeds this value, the client will automatically split them into multiple SUBSCRIBE packets of this size. - `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. - `timerVariant`: defaults to `auto`, which tries to determine which timer is most appropriate for you environment, if you're having detection issues, you can set it to `worker` or `native`. If none suits you, you can pass a timer object with set and clear properties: ```js timerVariant: { set: (func, timer) => setInterval(func, timer), clear: (id) => clearInterval(id) } ``` - `forceNativeWebSocket`: set to true if you're having detection issues (i.e. the `ws does not work in the browser` exception) to force the use of native WebSocket. It is important to note that if set to true for the first client created, then all the clients will use native WebSocket. And conversely, if not set or set to false, all will use the detection result. - `unixSocket`: if you want to connect to a unix socket, set this to true Instead of setting `socksProxy` you can also supple the same parameter via the environment variable `MQTTJS_SOCKS_PROXY`. In case mqtts (mqtt over tls) is required, the `options` object is passed through to [`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback). If using a **self-signed certificate**, set `rejectUnauthorized: false`. However, be cautious as this exposes you to potential man in the middle attacks and isn't recommended for production. For those supporting multiple TLS protocols on a single port, like MQTTS and MQTT over WSS, utilize the `ALPNProtocols` option. This lets you define the Application Layer Protocol Negotiation (ALPN) protocol. You can set `ALPNProtocols` as a string array, Buffer, or Uint8Array based on your setup. If you are connecting to a broker that supports only MQTT 3.1 (not 3.1.1 compliant), you should pass these additional options: ```js { protocolId: 'MQIsdp', protocolVersion: 3 } ``` This is confirmed on RabbitMQ 3.2.4, and on Mosquitto < 1.3. Mosquitto version 1.3 and 1.4 works fine without those. #### Event `'connect'` `function (connack) {}` Emitted on successful (re)connection (i.e. connack rc=0). - `connack` received connack packet. When `clean` connection option is `false` and server has a previous session for `clientId` connection option, then `connack.sessionPresent` flag is `true`. When that is the case, you may rely on stored session and prefer not to send subscribe commands for the client. #### Event `'reconnect'` `function () {}` Emitted when a reconnect starts. #### Event `'close'` `function () {}` Emitted after a disconnection. #### Event `'disconnect'` `function (packet) {}` Emitted after receiving disconnect packet from broker. MQTT 5.0 feature. #### Event `'offline'` `function () {}` Emitted when the client goes offline. #### Event `'error'` `function (error) {}` Emitted when the client cannot connect (i.e. connack rc != 0) or when a parsing error occurs. The following TLS errors will be emitted as an `error` event: - `ECONNREFUSED` - `ECONNRESET` - `EADDRINUSE` - `ENOTFOUND` #### Event `'end'` `function () {}` 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. #### Event `'message'` `function (topic, message, packet) {}` Emitted when the client receives a publish packet - `topic` topic of the received packet - `message` payload of the received packet - `packet` received packet, as defined in [mqtt-packet](https://github.com/mcollina/mqtt-packet#publish) #### Event `'packetsend'` `function (packet) {}` Emitted when the client sends any packet. This includes .published() packets as well as packets used by MQTT for managing subscriptions and connections - `packet` received packet, as defined in [mqtt-packet](https://github.com/mcollina/mqtt-packet) #### Event `'packetreceive'` `function (packet) {}` Emitted when the client receives any packet. This includes packets from subscribed topics as well as packets used by MQTT for managing subscriptions and connections - `packet` received packet, as defined in [mqtt-packet](https://github.com/mcollina/mqtt-packet) --- ### 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]) Publish a message to a topic - `topic` is the topic to publish to, `String` - `message` is the message to publish, `Buffer` or `String` - `options` is the options to publish with, including: - `qos` QoS level, `Number`, default `0` - `retain` retain flag, `Boolean`, default `false` - `dup` mark as duplicate flag, `Boolean`, default `false` - `properties`: MQTT 5.0 properties `object` - `payloadFormatIndicator`: Payload is UTF-8 Encoded Character Data or not `boolean`, - `messageExpiryInterval`: the lifetime of the Application Message in seconds `number`, - `topicAlias`: value that is used to identify the Topic instead of using the Topic Name `number`, - `responseTopic`: String which is used as the Topic Name for a response message `string`, - `correlationData`: used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, - `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - `subscriptionIdentifier`: representing the identifier of the subscription `number`, - `contentType`: String describing the content of the Application Message `string` - `cbStorePut` - `function ()`, fired when message is put into `outgoingStore` if QoS is `1` or `2`. - `callback` - `function (err, packet)`, fired when the QoS handling completes, or at the next tick if QoS 0. An error occurs if client is disconnecting. ### mqtt.Client#publishAsync(topic, message, [options]) Async [`publish`](#publish). Returns a `Promise`. A packet is anything that has a `messageId` property. --- ### mqtt.Client#subscribe(topic/topic array/topic object, [options], [callback]) Subscribe to a topic or topics - `topic` is a `String` topic to subscribe to or an `Array` of topics to subscribe to. It can also be an object, it has as object keys the topic name and as value the QoS, like `{'test1': {qos: 0}, 'test2': {qos: 1}}`. MQTT `topic` wildcard characters are supported (`+` - for single level and `#` - for multi level) - `options` is the options to subscribe with, including: - `qos` QoS subscription level, default 0 - `nl` No Local MQTT 5.0 flag (If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the ClientID of the publishing connection) - `rap` Retain as Published MQTT 5.0 flag (If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If false, Application Messages forwarded using this subscription have the RETAIN flag set to 0.) - `rh` Retain Handling MQTT 5.0 (This option specifies whether retained messages are sent when the subscription is established.) - `properties`: `object` - `subscriptionIdentifier`: representing the identifier of the subscription `number`, - `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` - `callback` - `function (err, granted)` callback fired on suback where: - `err` a subscription error or an error that occurs when client is disconnecting - `granted` is an array of `{topic, qos}` where: - `topic` is a subscribed to topic - `qos` is the granted QoS level on it ### mqtt.Client#subscribeAsync(topic/topic array/topic object, [options]) Async [`subscribe`](#subscribe). Returns a `Promise`. --- ### mqtt.Client#unsubscribe(topic/topic array, [options], [callback]) Unsubscribe from a topic or topics - `topic` is a `String` topic or an array of topics to unsubscribe from - `options`: options of unsubscribe. - `properties`: `object` - `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` - `callback` - `function (err)`, fired on unsuback. An error occurs if client is disconnecting. ### mqtt.Client#unsubscribeAsync(topic/topic array, [options]) Async [`unsubscribe`](#unsubscribe). Returns a `Promise`. --- ### mqtt.Client#end([force], [options], [callback]) Close the client, accepts the following options: - `force`: passing it to true will close the client right away, without waiting for the in-flight messages to be acked. This parameter is optional. - `options`: options of disconnect. - `reasonCode`: Disconnect Reason Code `number` - `properties`: `object` - `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, - `reasonString`: representing the reason for the disconnect `string`, - `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - `serverReference`: String which can be used by the Client to identify another Server to use `string` - `callback`: will be called when the client is closed. This parameter is optional. ### mqtt.Client#endAsync([force], [options]) Async [`end`](#end). Returns a `Promise`. --- ### mqtt.Client#removeOutgoingMessage(mId) Remove a message from the outgoingStore. The outgoing callback will be called with Error('Message removed') if the message is removed. After this function is called, the messageId is released and becomes reusable. - `mId`: The messageId of the message in the outgoingStore. --- ### mqtt.Client#reconnect() Connect again using the same options as connect() --- ### mqtt.Client#handleMessage(packet, callback) Handle messages with backpressure support, one at a time. Override at will, but **always call `callback`**, or the client will hang. --- ### mqtt.Client#connected Boolean : set to `true` if the client is connected. `false` otherwise. --- ### mqtt.Client#getLastMessageId() Number : get last message id. This is for sent messages only. --- ### mqtt.Client#reconnecting Boolean : set to `true` if the client is trying to reconnect to the server. `false` otherwise. --- ### mqtt.Store(options) In-memory implementation of the message store. - `options` is the store options: - `clean`: `true`, clean inflight messages when close is called (default `true`) Other implementations of `mqtt.Store`: - [mqtt-jsonl-store](https://github.com/robertsLando/mqtt-jsonl-store) which uses [jsonl-db](https://github.com/AlCalzone/jsonl-db) to store inflight data, it works only on Node. - [mqtt-level-store](http://npm.im/mqtt-level-store) which uses [Level-browserify](http://npm.im/level-browserify) to store the inflight data, making it usable both in Node and the Browser. - [mqtt-nedb-store](https://github.com/behrad/mqtt-nedb-store) which uses [nedb](https://www.npmjs.com/package/nedb) to store the inflight data. - [mqtt-localforage-store](http://npm.im/mqtt-localforage-store) which uses [localForage](http://npm.im/localforage) to store the inflight data, making it usable in the Browser without browserify. --- ### mqtt.Store#put(packet, callback) Adds a packet to the store, a packet is anything that has a `messageId` property. The callback is called when the packet has been stored. --- ### mqtt.Store#createStream() Creates a stream with all the packets in the store. --- ### mqtt.Store#del(packet, cb) Removes a packet from the store, a packet is anything that has a `messageId` property. The callback is called when the packet has been removed. --- ### mqtt.Store#close(cb) Closes the Store. ## Browser > [!IMPORTANT] > The only protocol supported in browsers is MQTT over WebSockets, so you must use `ws://` or `wss://` protocols. While the [ws](https://www.npmjs.com/package/ws) module is used in NodeJS, [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) is used in browsers. This is totally transparent to users except for the following: - The `wsOption` is not supported in browsers. - Browsers doesn't allow to catch many WebSocket errors for [security reasons](https://stackoverflow.com/a/31003057) as: > Access to this information could allow a malicious Web page to gain information about your network, so they require browsers report all connection-time errors in an indistinguishable way. So listening for `client.on('error')` may not catch all the errors you would get in NodeJS env. ### Bundle MQTT.js is bundled using [esbuild](https://esbuild.github.io/). It is tested working with all bundlers like Webpack, Vite and React. You can find all mqtt bundles versions in `dist` folder: - `mqtt.js` - iife format, not minified - `mqtt.min.js` - iife format, minified - `mqtt.esm.js` - esm format minified Starting from MQTT.js > 5.2.0 you can import mqtt in your code like this: ```js import mqtt from 'mqtt' ``` This will be automatically handled by your bundler. Otherwise you can choose to use a specific bundle like: ```js import * as mqtt from 'mqtt/dist/mqtt' import * as mqtt from 'mqtt/dist/mqtt.min' import mqtt from 'mqtt/dist/mqtt.esm' ``` ### Via CDN The MQTT.js bundle is available through , specifically at . See for the full documentation on version ranges. ## About QoS Here is how QoS works: - QoS 0 : received **at most once** : The packet is sent, and that's it. There is no validation about whether it has been received. - QoS 1 : received **at least once** : The packet is sent and stored as long as the client has not received a confirmation from the server. MQTT ensures that it _will_ be received, but there can be duplicates. - QoS 2 : received **exactly once** : Same as QoS 1 but there is no duplicates. About data consumption, obviously, QoS 2 > QoS 1 > QoS 0, if that's a concern to you. ## Usage with TypeScript Starting from v5 this project is written in TypeScript and the type definitions are included in the package. Example: ```ts import { connect } from "mqtt" const client = connect('mqtt://test.mosquitto.org') ``` ## WeChat and Ali Mini Program support ### WeChat Mini Program Supports [WeChat Mini Program](https://mp.weixin.qq.com/). Use the `wxs` protocol. See [the WeChat docs](https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-socket.html). ```js import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only' // import before mqtt. import 'esbuild-plugin-polyfill-node/polyfills/navigator' const mqtt = require("mqtt"); const client = mqtt.connect("wxs://test.mosquitto.org", { timerVariant: 'native' // more info ref issue: #1797 }); ``` ### Ali Mini Program Supports [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). Use the `alis` protocol. See [the Alipay docs](https://docs.alipay.com/mini/developer/getting-started). ```js const mqtt = require("mqtt"); const client = mqtt.connect("alis://test.mosquitto.org"); ``` ## Contributing MQTT.js is an **OPEN Open Source Project**. This means that: > Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. See the [CONTRIBUTING.md](https://github.com/mqttjs/MQTT.js/blob/master/CONTRIBUTING.md) file for more details. ### Contributors MQTT.js is only possible due to the excellent work of the following contributors: | Name | GitHub | Twitter | | ------------------ | -------------------------------------------------- | ---------------------------------------------------------- | | Adam Rudd | [GitHub/adamvr](https://github.com/adamvr) | [Twitter/@adam_vr](http://twitter.com/adam_vr) | | Matteo Collina | [GitHub/mcollina](https://github.com/mcollina) | [Twitter/@matteocollina](http://twitter.com/matteocollina) | | Maxime Agor | [GitHub/4rzael](https://github.com/4rzael) | [Twitter/@4rzael](http://twitter.com/4rzael) | | Siarhei Buntsevich | [GitHub/scarry1992](https://github.com/scarry1992) | | | Daniel Lando | [GitHub/robertsLando](https://github.com/robertsLando) | | ## Sponsor If you would like to support MQTT.js, please consider sponsoring the author and active maintainers: - [Matteo Collina](https://github.com/sponsors/mcollina): author of MQTT.js - [Daniel Lando](https://github.com/sponsors/robertsLando): active maintainer ## License MIT ================================================ FILE: benchmarks/bombing.js ================================================ #! /usr/bin/env node 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 } setInterval(count, interval) function publish() { sent++ client.publish('test', 'payload', publish) } client.on('connect', publish) client.on('error', () => { console.log('reconnect!') client.stream.end() }) ================================================ FILE: benchmarks/throughputCounter.js ================================================ #! /usr/bin/env node const mqtt = require('..') 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 } setInterval(count, interval) client.on('connect', () => { count() this.subscribe('test') this.on('message', () => { counter++ }) }) ================================================ FILE: electron-test/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock .DS_Store # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Webpack .webpack/ # Vite .vite/ # Electron-Forge out/ ================================================ FILE: electron-test/README ================================================ ``` npm i npm run wdio ``` ================================================ FILE: electron-test/forge.config.js ================================================ const { FusesPlugin } = require('@electron-forge/plugin-fuses'); const { FuseV1Options, FuseVersion } = require('@electron/fuses'); module.exports = { packagerConfig: { asar: true, }, rebuildConfig: {}, makers: [ { name: '@electron-forge/maker-zip', platforms: ['linux'], }, ], plugins: [ { name: '@electron-forge/plugin-auto-unpack-natives', config: {}, }, // Fuses are used to enable/disable various Electron functionality // at package time, before code signing the application new FusesPlugin({ version: FuseVersion.V1, [FuseV1Options.RunAsNode]: false, [FuseV1Options.EnableCookieEncryption]: true, [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, [FuseV1Options.EnableNodeCliInspectArguments]: false, [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, [FuseV1Options.OnlyLoadAppFromAsar]: true, }), ], }; ================================================ FILE: electron-test/package.json ================================================ { "name": "electron-test", "productName": "electron-test", "version": "1.0.0", "description": "My Electron application description", "main": "src/index.js", "scripts": { "start": "electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish", "lint": "echo \"No linting configured\"", "wdio": "npm run package && wdio run ./wdio.conf.ts" }, "devDependencies": { "@electron-forge/cli": "7.6.1", "@electron-forge/maker-zip": "7.6.1", "@electron-forge/plugin-auto-unpack-natives": "7.6.1", "@electron-forge/plugin-fuses": "7.6.1", "@electron/fuses": "1.8.0", "@wdio/cli": "9.9.1", "@wdio/local-runner": "9.9.1", "@wdio/mocha-framework": "9.9.0", "@wdio/spec-reporter": "9.9.0", "aedes-cli": "0.8.0", "electron": "34.2.0", "typescript": "^5.4.5", "wdio-electron-service": "8.0.0" }, "keywords": [], "author": { "name": "axi92" }, "license": "MIT", "dependencies": { "jquery": "3.7.1" } } ================================================ FILE: electron-test/src/index.css ================================================ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; margin: auto; max-width: 38rem; padding: 2rem; } ================================================ FILE: electron-test/src/index.html ================================================ Hello World!

💖 Hello World!

Welcome to your Electron application.

Mqtt connection status: offline
Protocol: ================================================ FILE: electron-test/src/index.js ================================================ const { app, BrowserWindow } = require('electron'); const path = require('node:path'); const createWindow = () => { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, show: true, webPreferences: { sandbox: false, nodeIntegration: true, contextIsolation: false, }, }); // and load the index.html of the app. mainWindow.loadFile(path.join(__dirname, 'index.html')); // Open the DevTools. // mainWindow.webContents.openDevTools(); }; // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { createWindow(); // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); ================================================ FILE: electron-test/src/renderer.js ================================================ window.$ = window.jQuery = require('jquery') const path = require('node:path'); const mqtt = require(path.join(process.cwd(), '../build/mqtt')) console.log('start connecting...') const client = mqtt.connect({ protocol: 'mqtt', port: 1883, }) client.on('connect', () => { $('#status').text('online') $('#protocol').text(client.options.protocol) }) ================================================ FILE: electron-test/test/service/server_launcher.ts ================================================ import type { Services } from '@wdio/types' import { resolve as pathResolve } from 'path' const { start } = require('aedes-cli') export default class ServerLauncher implements Services.ServiceInstance { #aedesBroker: any constructor() { this.#aedesBroker = null } async onPrepare(): Promise { const keyPath = pathResolve(__dirname, '../../../test/browser/certs/server-key.pem') const certPath = pathResolve(__dirname, '../../../test/browser/certs/server-cert.pem') this.#aedesBroker = await start({ protos: ['tcp', 'tls'], port: 1883, tlsPort: 8883, key: keyPath, cert: certPath, verbose: true, stats: false, }) } async onComplete(): Promise { if (!this.#aedesBroker?.servers) { return } for (const server of this.#aedesBroker.servers) { if (server.listening) { await new Promise((resolve, reject) => { server.close((err: any) => { if (err) reject(err) else resolve() }) }) } } } } ================================================ FILE: electron-test/test/specs/test.e2e.ts ================================================ import { expect, $ } from '@wdio/globals' // import { join } from 'path' import isBrowser from '../../../build/lib/is-browser' // import isBrowser from join(process.cwd(), '../build/lib/is-browser') describe('Electron Testing', () => { it('should render electron window', async () => { await expect($('h1')).toHaveText('💖 Hello World!') }) it('should connect', async() => { await expect($('#status')).toHaveText('online') }) it('should not be a browser context', async() =>{ expect(isBrowser).toBe(false) }) it('should use protocoll mqtt', async() => { await expect($('#protocol')).toHaveText('mqtt') }) }) ================================================ FILE: electron-test/tsconfig.json ================================================ { "compilerOptions": { "moduleResolution": "node", "module": "commonjs", "target": "es2022", "lib": [ "es2022", "dom" ], "types": [ "node", "@wdio/globals/types", "expect-webdriverio", "@wdio/mocha-framework", "wdio-electron-service" ], "skipLibCheck": true, "noEmit": true, "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": [ "test", "wdio.conf.ts" ] } ================================================ FILE: electron-test/wdio.conf.ts ================================================ import { resolve as pathResolve } from 'node:path'; import ServerLauncher from './test/service/server_launcher' const electronAppBinaryPath = pathResolve('./out/electron-test-linux-x64/electron-test'); export const config: WebdriverIO.Config = { // // ==================== // Runner Configuration // ==================== // WebdriverIO supports running e2e tests as well as unit and component tests. runner: 'local', tsConfigPath: './tsconfig.json', // // ================== // Specify Test Files // ================== // Define which test specs should run. The pattern is relative to the directory // of the configuration file being run. // // The specs are defined as an array of spec files (optionally using wildcards // that will be expanded). The test for each spec file will be run in a separate // worker process. In order to have a group of spec files run in the same worker // process simply enclose them in an array within the specs array. // // The path of the spec files will be resolved relative from the directory of // of the config file unless it's absolute. // specs: [ './test/specs/**/*.ts' ], // Patterns to exclude. exclude: [ // 'path/to/excluded/files' ], // // ============ // Capabilities // ============ // Define your capabilities here. WebdriverIO can run multiple capabilities at the same // time. Depending on the number of capabilities, WebdriverIO launches several test // sessions. Within your capabilities you can overwrite the spec and exclude options in // order to group specific specs to a specific capability. // // First, you can define how many instances should be started at the same time. Let's // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec // files and you set maxInstances to 10, all spec files will get tested at the same time // and 30 processes will get spawned. The property handles how many capabilities // from the same test should run tests. // maxInstances: 10, // // If you have trouble getting all important capabilities together, check out the // Sauce Labs platform configurator - a great tool to configure your capabilities: // https://saucelabs.com/platform/platform-configurator // capabilities: [{ browserName: 'electron', // Electron service options // see https://webdriver.io/docs/desktop-testing/electron/configuration/#service-options 'wdio:electronServiceOptions': { appBinaryPath: electronAppBinaryPath, } }], // // =================== // Test Configurations // =================== // Define all options that are relevant for the WebdriverIO instance here // // Level of logging verbosity: trace | debug | info | warn | error | silent logLevel: 'info', // // Set specific log levels per logger // loggers: // - webdriver, webdriverio // - @wdio/browserstack-service, @wdio/lighthouse-service, @wdio/sauce-service // - @wdio/mocha-framework, @wdio/jasmine-framework // - @wdio/local-runner // - @wdio/sumologic-reporter // - @wdio/cli, @wdio/config, @wdio/utils // Level of logging verbosity: trace | debug | info | warn | error | silent // logLevels: { // webdriver: 'info', // '@wdio/appium-service': 'info' // }, // // If you only want to run your tests until a specific amount of tests have failed use // bail (default is 0 - don't bail, run all tests). bail: 0, // // Set a base URL in order to shorten url command calls. If your `url` parameter starts // with `/`, the base url gets prepended, not including the path portion of your baseUrl. // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url // gets prepended directly. // baseUrl: 'http://localhost:8080', // // Default timeout for all waitFor* commands. waitforTimeout: 10000, // // Default timeout in milliseconds for request // if browser driver or grid doesn't send response connectionRetryTimeout: 120000, // // Default request retries count connectionRetryCount: 3, // // Test runner services // Services take over a specific job you don't want to take care of. They enhance // your test setup with almost no effort. Unlike plugins, they don't add new // commands. Instead, they hook themselves up into the test process. services: [ [ServerLauncher, {}], 'electron', ], // Framework you want to run your specs with. // The following are supported: Mocha, Jasmine, and Cucumber // see also: https://webdriver.io/docs/frameworks // // Make sure you have the wdio adapter package for the specific framework installed // before running any tests. framework: 'mocha', // // The number of times to retry the entire specfile when it fails as a whole // specFileRetries: 1, // // Delay in seconds between the spec file retry attempts // specFileRetriesDelay: 0, // // Whether or not retried spec files should be retried immediately or deferred to the end of the queue // specFileRetriesDeferred: false, // // Test reporter for stdout. // The only one supported by default is 'dot' // see also: https://webdriver.io/docs/dot-reporter reporters: ['spec'], // Options to be passed to Mocha. // See the full list at http://mochajs.org/ mochaOpts: { ui: 'bdd', timeout: 60000 }, // // ===== // Hooks // ===== // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance // it and to build services around it. You can either apply a single function or an array of // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got // resolved to continue. /** * Gets executed once before all workers get launched. * @param {object} config wdio configuration object * @param {Array.} capabilities list of capabilities details */ // onPrepare: function (config, capabilities) { // }, /** * Gets executed before a worker process is spawned and can be used to initialize specific service * for that worker as well as modify runtime environments in an async fashion. * @param {string} cid capability id (e.g 0-0) * @param {object} caps object containing capabilities for session that will be spawn in the worker * @param {object} specs specs to be run in the worker process * @param {object} args object that will be merged with the main configuration once worker is initialized * @param {object} execArgv list of string arguments passed to the worker process */ // onWorkerStart: function (cid, caps, specs, args, execArgv) { // }, /** * Gets executed just after a worker process has exited. * @param {string} cid capability id (e.g 0-0) * @param {number} exitCode 0 - success, 1 - fail * @param {object} specs specs to be run in the worker process * @param {number} retries number of retries used */ // onWorkerEnd: function (cid, exitCode, specs, retries) { // }, /** * Gets executed just before initialising the webdriver session and test framework. It allows you * to manipulate configurations depending on the capability or spec. * @param {object} config wdio configuration object * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that are to be run * @param {string} cid worker id (e.g. 0-0) */ // beforeSession: function (config, capabilities, specs, cid) { // }, /** * Gets executed before test execution begins. At this point you can access to all global * variables like `browser`. It is the perfect place to define custom commands. * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that are to be run * @param {object} browser instance of created browser/device session */ // before: function (capabilities, specs) { // }, /** * Runs before a WebdriverIO command gets executed. * @param {string} commandName hook command name * @param {Array} args arguments that command would receive */ // beforeCommand: function (commandName, args) { // }, /** * Hook that gets executed before the suite starts * @param {object} suite suite details */ // beforeSuite: function (suite) { // }, /** * Function to be executed before a test (in Mocha/Jasmine) starts. */ // beforeTest: function (test, context) { // }, /** * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling * beforeEach in Mocha) */ // beforeHook: function (test, context, hookName) { // }, /** * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling * afterEach in Mocha) */ // afterHook: function (test, context, { error, result, duration, passed, retries }, hookName) { // }, /** * Function to be executed after a test (in Mocha/Jasmine only) * @param {object} test test object * @param {object} context scope object the test was executed with * @param {Error} result.error error object in case the test fails, otherwise `undefined` * @param {*} result.result return object of test function * @param {number} result.duration duration of test * @param {boolean} result.passed true if test has passed, otherwise false * @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }` */ // afterTest: function(test, context, { error, result, duration, passed, retries }) { // }, /** * Hook that gets executed after the suite has ended * @param {object} suite suite details */ // afterSuite: function (suite) { // }, /** * Runs after a WebdriverIO command gets executed * @param {string} commandName hook command name * @param {Array} args arguments that command would receive * @param {number} result 0 - command success, 1 - command error * @param {object} error error object if any */ // afterCommand: function (commandName, args, result, error) { // }, /** * Gets executed after all tests are done. You still have access to all global variables from * the test. * @param {number} result 0 - test pass, 1 - test fail * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that ran */ // after: function (result, capabilities, specs) { // }, /** * Gets executed right after terminating the webdriver session. * @param {object} config wdio configuration object * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that ran */ // afterSession: function (config, capabilities, specs) { // }, /** * Gets executed after all workers got shut down and the process is about to exit. An error * thrown in the onComplete hook will result in the test run failing. * @param {object} exitCode 0 - success, 1 - fail * @param {object} config wdio configuration object * @param {Array.} capabilities list of capabilities details * @param {} results object containing test results */ // onComplete: function(exitCode, config, capabilities, results) { // }, /** * Gets executed when a refresh happens. * @param {string} oldSessionId session ID of the old session * @param {string} newSessionId session ID of the new session */ // onReload: function(oldSessionId, newSessionId) { // } /** * Hook that gets executed before a WebdriverIO assertion happens. * @param {object} params information about the assertion to be executed */ // beforeAssertion: function(params) { // } /** * Hook that gets executed after a WebdriverIO assertion happened. * @param {object} params information about the assertion that was executed, including its results */ // afterAssertion: function(params) { // } } ================================================ FILE: esbuild.js ================================================ const { build } = require('esbuild') const { polyfillNode } = require('esbuild-plugin-polyfill-node'); const { rimraf } = require('rimraf') const fs = require('fs') const { version } = require('./package.json'); const outdir = 'dist' /** * @type {import('esbuild').BuildOptions} */ const options = { entryPoints: ['build/index.js'], bundle: true, outfile: `${outdir}/mqtt.js`, format: 'iife', platform: 'browser', globalName: 'mqtt', sourcemap: false, // this can be enabled while debugging, if we decide to keep this enabled we should also ship the `src` folder to npm plugins: [ polyfillNode({ polyfills: [ 'readable-stream' ] }), { name: 'resolve-package-json', setup(build) { // when importing 'package.json' we want to provide a custom object like { version: '1.2.3' } build.onResolve({ filter: /package\.json$/ }, args => { return { path: args.path, namespace: 'package-json' } }) build.onLoad({ filter: /.*/, namespace: 'package-json' }, args => { return { contents: JSON.stringify({ version }), loader: 'json' } } ) } }, { name: 'resolve-socks', setup(build) { // socks is not supported in the browser and adds several 100kb to the build, so stub it build.onResolve({ filter: /socks$/ }, args => { return { path: args.path, namespace: 'socks-stub' } }) build.onLoad({ filter: /.*/, namespace: 'socks-stub' }, args => { return { contents: 'module.exports = {}', loader: 'js' } } ) } }, ], } async function run() { const start = Date.now() await rimraf(outdir) await build(options) options.minify = true options.outfile = `${outdir}/mqtt.min.js` await build(options) options.outfile = `${outdir}/mqtt.esm.js` options.format = 'esm' await build(options) console.log(`Build time: ${Date.now() - start}ms`) console.log('Build output:') // log generated files with their size in KB const files = fs.readdirSync(outdir) for (const file of files) { const stat = fs.statSync(`${outdir}/${file}`) console.log(`- ${file} ${Math.round(stat.size / 1024 * 100) / 100} KB`) } } run().catch((e) => { console.error(e) process.exit(1) }) ================================================ FILE: eslint.config.js ================================================ const { defineConfig, globalIgnores, } = require("eslint/config"); const tsParser = require("@typescript-eslint/parser"); const typescriptEslintEslintPlugin = require("@typescript-eslint/eslint-plugin"); const globals = require("globals"); const js = require("@eslint/js"); const { FlatCompat, } = require("@eslint/eslintrc"); const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all }); module.exports = defineConfig([{ languageOptions: { parser: tsParser, globals: { ...globals.browser, ...globals.commonjs, ...globals.node, ...globals.worker, }, sourceType: "module", parserOptions: { project: "tsconfig.json", tsconfigRootDir: __dirname, }, }, plugins: { "@typescript-eslint": typescriptEslintEslintPlugin, }, extends: compat.extends("plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"), rules: { "global-require": "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-unused-vars": ["error", { args: "none", }], "no-underscore-dangle": "off", "no-param-reassign": "off", "no-restricted-syntax": "off", "default-case": "off", "consistent-return": "off", "max-classes-per-file": "off", "no-plusplus": "off", "no-bitwise": "off", "class-methods-use-this": "off", "no-continue": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": ["error", { args: "none", }], "@typescript-eslint/naming-convention": "off", "@typescript-eslint/dot-notation": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports", "fixStyle": "inline-type-imports", "disallowTypeAnnotations": true, }], }, }, globalIgnores([ "types/", "examples/", "doc/", "dist/", "build/", "electron-test/", "**/*.js", "**/*.mjs", ])]); ================================================ FILE: example.ts ================================================ import mqtt from './src/index' const client = mqtt.connect('mqtt://broker.hivemq.com', { keepalive: 3, port: 1883, reconnectPeriod: 15000, rejectUnauthorized: false, }) const randomNumber = Math.floor(Math.random() * 1000) const testTopic = `presence_${randomNumber.toString()}` function publish() { const msg = `Hello mqtt ${new Date().toISOString()}` client.publish(testTopic, msg, { qos: 1 }, (err2) => { if (!err2) { console.log('message published') } else { console.error(err2) } }) } client.subscribe(testTopic, (err) => { if (!err) { console.log('subscribed to', testTopic) } else { console.error(err) } }) client.on('message', (topic, message) => { console.log('received message "%s" from topic "%s"', message, topic) }) setInterval(() => { publish() }, 2000) client.on('error', (err) => { console.error(err) }) client.on('connect', () => { console.log('connected') publish() }) client.on('disconnect', () => { console.log('disconnected') }) client.on('offline', () => { console.log('offline') }) client.on('reconnect', () => { console.log('reconnect') }) ================================================ FILE: examples/client/secure-client.js ================================================ 'use strict' const mqtt = require('../..') const path = require('path') const fs = require('fs') const KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-key.pem')) const CERT = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-cert.pem')) const PORT = 8443 const options = { port: PORT, key: KEY, cert: CERT, rejectUnauthorized: false } const client = mqtt.connect(options) client.subscribe('messages') client.publish('messages', 'Current time is: ' + new Date()) client.on('message', function (topic, message) { console.log(message) }) ================================================ FILE: examples/client/simple-both.js ================================================ 'use strict' const mqtt = require('../..') const client = mqtt.connect() // or const client = mqtt.connect({ port: 1883, host: '192.168.1.100', keepalive: 10000}); client.subscribe('presence') client.publish('presence', 'bin hier') client.on('message', function (topic, message) { console.log(message) }) client.end() ================================================ FILE: examples/client/simple-publish.js ================================================ 'use strict' const mqtt = require('../..') const client = mqtt.connect() client.publish('presence', 'hello!') client.end() ================================================ FILE: examples/client/simple-subscribe.js ================================================ 'use strict' const mqtt = require('../..') const client = mqtt.connect() client.subscribe('presence') client.on('message', function (topic, message) { console.log(message) }) ================================================ FILE: examples/tls client/crt.ca.cg.pem ================================================ -----BEGIN CERTIFICATE----- MIIF7zCCA9egAwIBAgIJAOeJR1p1PU3qMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD VQQGEwJFUzERMA8GA1UECAwIWmFyYWdvemExETAPBgNVBAcMCFphcmFnb3phMRkw FwYDVQQKDBBNUVRUIGZvciBub2RlLmpzMRAwDgYDVQQLDAdNUVRULmpzMQ0wCwYD VQQDDARtcXR0MRwwGgYJKoZIhvcNAQkBFg1mYWtlQG1haWwuY29tMB4XDTEzMDgz MDEzMDIwNVoXDTIzMDgyODEzMDIwNVowgY0xCzAJBgNVBAYTAkVTMREwDwYDVQQI DAhaYXJhZ296YTERMA8GA1UEBwwIWmFyYWdvemExGTAXBgNVBAoMEE1RVFQgZm9y IG5vZGUuanMxEDAOBgNVBAsMB01RVFQuanMxDTALBgNVBAMMBG1xdHQxHDAaBgkq hkiG9w0BCQEWDWZha2VAbWFpbC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQC7Of6OppOE+xwPdPcsT0w3keCa5k4ufZCqUAHex7+mLlrpjfCjQ2z6 Rm0XBiCu9vy+xvLtbGDh5e/gocjAkkEywjbtrRMiFq5i41BNT3dzEWb9cCXvMWYa RxQgIqouJUz5r+TbaP1bm4gAWTHmp09ccoIs9Tykxhyc1nZxXVrEsHF4aBmuw5NJ ZwxK1tJTgP4m5H38Ms7ahGpByPsnMg6GBRs/Yen0mGhOsG+MU4TFiQb4bwIxg8Eu ntGP1gARvtmyTkkTDhfksRs+muEV17uPtdhGNS/0CGRWaZ2mjEYyD70Ijl2grLd4 6Vz27uPaqUvbgntPNadKqFN+jEHTtptou3k6V9C8CeLHIq+5N6abfPVHBzaqyNqg QelzpSgQQBJ1H0CYREjzAs9uLfeep5ejW99Ik4YwtL6UrTVUyGzGgAl9mevZN5a4 7mEY7MNUFdwigq0ZpbZmzYiuOURGYnoiy5o64balG5XH6Zh6B1WWhK7CArPVosz8 eoQacj1WEM5d2Ivg1OLlEdD8FZDABv5CMTmRvnoFQuuIDzWVfrhdcZQ2tQuNLWrz YDKheCunPkAIFOlGi70Xv3DVrTCr6kixwL2p9MHTzF4xiWWtiOv41ZXHTMG0t2I3 YmA45FEO5JawebPgUoGhoc2vgIw5Jo9dcGtwLCqBHSnCojPoTipVhQIDAQABo1Aw TjAdBgNVHQ4EFgQU1yVv/ezoDLs+qjbx0O4KiHpC41swHwYDVR0jBBgwFoAU1yVv /ezoDLs+qjbx0O4KiHpC41swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOC AgEAScnfOewEk59DgfICJJ2vhcI33wqqn54zhJ1pi8SX3e7PLv26UEUXZaddIqeZ JzA/IWF+GCBQFAL7Z+sI4djXx/UpZp5ptCQBFc0tinHk1CGlC0E+LI3JS/cnFf+2 L8VKZHbSf4ua2f/VMJo7uoyrw/gQHgUToAlYYWpGcIKKm7d0JYQE60wlHk9TXgCc s9XAwI+bP9VKNQkZCeooODG/5VcxdJafZSU3rW1WniFcD/R+ZNq7FZYbM+2u2mRt Qm7Hh/FjrN4Hnmf3xdNUE0NLHznwk4CD6EeQukN12yP2ccubnG6Z7HFFdV0g9fEP AVMsgY/9E9Te/BBoQKjhIg8c274ozIOsCHODx15Mn52848sq0LIQjyeOH4rtuWLL 1dFE1ysY2gzSMUtrP+on+r6F1GkndFszxfDrBcZMXs85VAy3eKfY/jzUMrdfn0YJ 36Wz7F40vnOUd2ni24kaOfnRodbu3lOEYD6l5fDGP79kfITyy+dtL6ExTLZQmEn+ xKsWM9bBkV4STpFiTF61tJwzlcAL1ZDLqDaSwsM8UDZopnDgvklNoJK9XzdLwD1X PofOtUe08G4tq5cBDVURLKif+7EfCyAqvUptQ3MJarhoXzhDy9CjtN8TmWexKC1q kB5DBML0Y4NnqTEnfYCs/XFPosaS+0GximGySJcg08ay6ZA= -----END CERTIFICATE----- ================================================ FILE: examples/tls client/mqttclient.js ================================================ 'use strict' /** ************************** IMPORTANT NOTE *********************************** The certificate used on this example has been generated for a host named stark. So as host we SHOULD use stark if we want the server to be authorized. For testing this we should add on the computer running this example a line on the hosts file: /etc/hosts [UNIX] OR \System32\drivers\etc\hosts [Windows] The line to add on the file should be as follows: stark *******************************************************************************/ const mqtt = require('mqtt') const fs = require('fs') const path = require('path') const KEY = fs.readFileSync(path.join(__dirname, '/tls-key.pem')) const CERT = fs.readFileSync(path.join(__dirname, '/tls-cert.pem')) const TRUSTED_CA_LIST = fs.readFileSync(path.join(__dirname, '/crt.ca.cg.pem')) const PORT = 1883 const HOST = 'stark' const options = { port: PORT, host: HOST, key: KEY, cert: CERT, rejectUnauthorized: true, // The CA list will be used to determine if server is authorized ca: TRUSTED_CA_LIST, protocol: 'mqtts' } const client = mqtt.connect(options) client.subscribe('messages') client.publish('messages', 'Current time is: ' + new Date()) client.on('message', (topic, message) => { console.log(message) }) client.on('connect', () => { console.log('Connected') }) ================================================ FILE: examples/tls client/tls-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIICATCCAWoCCQC2pNY4sfld/jANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 cyBQdHkgTHRkMB4XDTEzMDgyNzEyNTU0NVoXDTEzMDkyNjEyNTU0NVowRTELMAkG A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzXGU 1mZUBqLwoP1fWkiZeypiKgWICUdNm+d2JHXnpQMEVBxSvsaRGOnzWVvgbMVxmD7n 5/p9qQGTj8FY/+t2NHpbt1I9lGV0+BlZxGJvyvHikEAXPD85EEFhqSbDwgkVuMqa w08njqhJJ37fbd2ux6w4woRrDTN4r9CNMhFb9QECAwEAATANBgkqhkiG9w0BAQUF AAOBgQBIlZYo1rf8GlISuV1haSBm8U/uiyjIX/pTE5Cs7Kb84SPzKB0tHnGGCa2t Lu+TEwetF3NatuI1biqYuevQSfmEM75zsRSwt1P40sJ2y9B1XRTdamHOHCYCJG/b rti7WJYjvO8JsCUeB6M+5jFodbmvjsGgAHLLUINXrxOqYe+PWg== -----END CERTIFICATE----- ================================================ FILE: examples/tls client/tls-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDNcZTWZlQGovCg/V9aSJl7KmIqBYgJR02b53YkdeelAwRUHFK+ xpEY6fNZW+BsxXGYPufn+n2pAZOPwVj/63Y0elu3Uj2UZXT4GVnEYm/K8eKQQBc8 PzkQQWGpJsPCCRW4yprDTyeOqEknft9t3a7HrDjChGsNM3iv0I0yEVv1AQIDAQAB AoGBALv9P+WEE0VTWf7mepdBsXfbi6HKF/Xtkh2kCh5I6WO8Q/y3Qhwh1OnIQg41 nUHK1iwq+8fxFYVN1PoJQWhEzI6JdBCrn88oADo/aVm1mGN5CWr3pwn92SAVMhbw 442AWWG81RStrr2uPhLBNE6U/4P835qM8uG4rCP+5Z5SzX7VAkEA+TptuSc0TEkL 5B/Nml2fYNfbQvRGVzyCbdCXdgkeZt5xuSuwDgC4GvWgjL+SAN1fjTek/Iez5NnL xHa5w93j2wJBANMGmRTaTxvpGdkUi/utTPtCp6GXL7hS9v41LClmQTYBOYscPn2b Dny2fyZPp29sZ7+AvXHWZxw7QtH+jO2Xz1MCQCI7vlqSYgKgffulyq4LchrxS3LU 7tyIuTmwTz2tXvmuUFyo/ZPO0XsShi0PG1T3E2roW8c8NJ+Ysv6XeEjJL8UCQG0Z /S0tzTa15no4SEM/jwxcosRFoRNgOXimTwW8azybl3+Xg6t27h+GTuikyAEwf9cf nVJssfSDowFk5MG1+icCQQCqBOTXEukcJRXZixkpfEuuvS3RNzOYwG4ReKjpvWPy EvsfHoCsO1Sz9qz8DXpwl3GEWUGGTfWwBfereX6HLXj+ -----END RSA PRIVATE KEY----- ================================================ FILE: examples/vite-example/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules .DS_Store dist dist-ssr coverage *.local /cypress/videos/ /cypress/screenshots/ # Editor directories and files .vscode/* !.vscode/extensions.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vite-example/README.md ================================================ # vite-example This template should help get you started developing with Vue 3 in Vite. ## Recommended IDE Setup [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). ## Customize configuration See [Vite Configuration Reference](https://vitejs.dev/config/). ## Project Setup ```sh npm install ``` ### Compile and Hot-Reload for Development ```sh npm run dev ``` ### Compile and Minify for Production ```sh npm run build ``` ================================================ FILE: examples/vite-example/index.html ================================================ Vite App
================================================ FILE: examples/vite-example/package.json ================================================ { "name": "vite-example", "version": "0.0.0", "private": true, "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "mqtt": "file:../../", "process": "^0.11.10", "vue": "^3.3.4" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@vitejs/plugin-vue": "^4.2.3", "rollup-plugin-polyfill-node": "^0.12.0", "vite": "^4.5.3" } } ================================================ FILE: examples/vite-example/src/App.vue ================================================ ================================================ FILE: examples/vite-example/src/assets/base.css ================================================ /* color palette from */ :root { --vt-c-white: #ffffff; --vt-c-white-soft: #f8f8f8; --vt-c-white-mute: #f2f2f2; --vt-c-black: #181818; --vt-c-black-soft: #222222; --vt-c-black-mute: #282828; --vt-c-indigo: #2c3e50; --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); --vt-c-text-light-1: var(--vt-c-indigo); --vt-c-text-light-2: rgba(60, 60, 60, 0.66); --vt-c-text-dark-1: var(--vt-c-white); --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); } /* semantic color variables for this project */ :root { --color-background: var(--vt-c-white); --color-background-soft: var(--vt-c-white-soft); --color-background-mute: var(--vt-c-white-mute); --color-border: var(--vt-c-divider-light-2); --color-border-hover: var(--vt-c-divider-light-1); --color-heading: var(--vt-c-text-light-1); --color-text: var(--vt-c-text-light-1); --section-gap: 160px; } @media (prefers-color-scheme: dark) { :root { --color-background: var(--vt-c-black); --color-background-soft: var(--vt-c-black-soft); --color-background-mute: var(--vt-c-black-mute); --color-border: var(--vt-c-divider-dark-2); --color-border-hover: var(--vt-c-divider-dark-1); --color-heading: var(--vt-c-text-dark-1); --color-text: var(--vt-c-text-dark-2); } } *, *::before, *::after { box-sizing: border-box; margin: 0; font-weight: normal; } body { min-height: 100vh; color: var(--color-text); background: var(--color-background); transition: color 0.5s, background-color 0.5s; line-height: 1.6; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 15px; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ================================================ FILE: examples/vite-example/src/assets/main.css ================================================ @import './base.css'; #app { max-width: 1280px; margin: 0 auto; padding: 2rem; font-weight: normal; } a, .green { text-decoration: none; color: hsla(160, 100%, 37%, 1); transition: 0.4s; } @media (hover: hover) { a:hover { background-color: hsla(160, 100%, 37%, 0.2); } } @media (min-width: 1024px) { body { display: flex; place-items: center; } #app { display: grid; grid-template-columns: 1fr 1fr; padding: 0 2rem; } } ================================================ FILE: examples/vite-example/src/main.js ================================================ import './assets/main.css' import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vite-example/vite.config.js ================================================ import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), } }, }) ================================================ FILE: examples/ws/aedes_server.js ================================================ const aedes = require('aedes')() const httpServer = require('http').createServer() const WebSocket = require('ws') const wsPort = 8080 // Here we are creating the Websocket Server that is using the HTTP Server... const wss = new WebSocket.Server({ server: httpServer }) wss.on('connection', function connection (ws) { const duplex = WebSocket.createWebSocketStream(ws) aedes.handle(duplex) }) httpServer.listen(wsPort, () => { console.log('websocket server listening on port', wsPort) }) aedes.on('clientError', (client, err) => { console.log('client error', client.id, err.message, err.stack) }) aedes.on('connectionError', (client, err) => { console.log('client error', client, err.message, err.stack) }) aedes.on('publish', (packet, client) => { if (packet && packet.payload) { console.log('publish packet:', packet.payload.toString()) } if (client) { console.log('message from client', client.id) } }) aedes.on('subscribe', (subscriptions, client) => { if (client) { console.log('subscribe from client', subscriptions, client.id) } }) aedes.on('client', (client) => { console.log('new client', client.id) }) ================================================ FILE: examples/ws/client.js ================================================ 'use strict' const mqtt = require('../../') const clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) // This sample should be run in tandem with the aedes_server.js file. // Simply run it: // $ node aedes_server.js // // Then run this file in a separate console: // $ node websocket_sample.js // const host = 'ws://localhost:8080' const options = { keepalive: 30, clientId, protocolId: 'MQTT', protocolVersion: 4, clean: true, reconnectPeriod: 1000, connectTimeout: 30 * 1000, will: { topic: 'WillMsg', payload: 'Connection Closed abnormally..!', qos: 0, retain: false }, rejectUnauthorized: false } console.log('connecting mqtt client') const client = mqtt.connect(host, options) client.on('error', (err) => { console.log(err) client.end() }) 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', (topic, message, packet) => { console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) }) client.on('close', () => { console.log(clientId + ' disconnected') }) ================================================ FILE: examples/wss/client_with_proxy.js ================================================ 'use strict' const mqtt = require('mqtt') const url = require('url') const HttpsProxyAgent = require('https-proxy-agent') /* host: host of the endpoint you want to connect e.g. my.mqqt.host.com path: path to you endpoint e.g. '/foo/bar/mqtt' */ const endpoint = 'wss://' /* create proxy agent proxy: your proxy e.g. proxy.foo.bar.com port: http proxy port e.g. 8080 */ const proxy = process.env.http_proxy || 'http://:' // eslint-disable-next-line const parsed = url.parse(endpoint) // eslint-disable-next-line const proxyOpts = url.parse(proxy) // true for wss proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true const agent = new HttpsProxyAgent(proxyOpts) const wsOptions = { agent // other wsOptions // foo:'bar' } const mqttOptions = { keepalive: 60, reschedulePings: true, protocolId: 'MQTT', protocolVersion: 4, reconnectPeriod: 1000, connectTimeout: 30 * 1000, clean: true, clientId: 'testClient', wsOptions } const client = mqtt.connect(parsed, mqttOptions) client.on('connect', () => { console.log('connected') }) client.on('error', (a) => { console.log('error!' + a) }) client.on('offline', (a) => { console.log('lost connection!' + a) }) client.on('close', (a) => { console.log('connection closed!' + a) }) client.on('message', (topic, message) => { console.log(message.toString()) }) ================================================ FILE: help/help.txt ================================================ MQTT.js command line interface, available commands are: * publish publish a message to the broker * subscribe subscribe for updates from the broker * version the current MQTT.js version * help help about commands Launch 'mqtt help [command]' to know more about the commands. ================================================ FILE: help/publish.txt ================================================ Usage: mqtt publish [opts] topic [message] Available options: -h/--hostname HOST the broker host -p/--port PORT the broker port -i/--client-id ID the client id -q/--qos 0/1/2 the QoS of the message -t/--topic TOPIC the message topic -m/--message MSG the message body -r/--retain send a retained message -s/--stdin read the message body from stdin -M/--multiline read lines from stdin as multiple messages -u/--username USER the username -P/--password PASS the password -C/--protocol PROTO the protocol to use, 'mqtt', 'mqtts', 'ws' or 'wss' --key PATH path to the key file --cert PATH path to the cert file --ca PATH path to the ca certificate --insecure do not verify the server certificate --will-topic TOPIC the will topic --will-payload BODY the will message --will-qos 0/1/2 the will qos --will-retain send a will retained message -H/--help show this ================================================ FILE: help/subscribe.txt ================================================ Usage: mqtt subscribe [opts] [topic] Available options: -h/--hostname HOST the broker host -p/--port PORT the broker port -i/--clientId ID the client id -q/--qos 0/1/2 the QoS of the message --no-clean do not discard any pending message for the given id -t/--topic TOPIC the message topic -k/--keepalive SEC send a ping every SEC seconds -u/--username USER the username -P/--password PASS the password -l/--protocol PROTO the protocol to use, 'mqtt', 'mqtts', 'ws' or 'wss' --key PATH path to the key file --cert PATH path to the cert file --ca PATH path to the ca certificate --insecure do not verify the server certificate --will-topic TOPIC the will topic --will-message BODY the will message --will-qos 0/1/2 the will qos --will-retain send a will retained message -v/--verbose print the topic before the message -H/--help show this ================================================ FILE: nyc.config.js ================================================ module.exports = { include: [ 'src/**', ], exclude: [ 'src/bin/*', 'src/lib/BufferedDuplex.ts', 'src/connect/wx.ts', 'src/connect/ali.ts', ], reporter: [ 'text', 'lcov' ], branches: 80, functions: 89, lines: 86, statements: 86, 'check-coverage': true } ================================================ FILE: package.json ================================================ { "name": "mqtt", "description": "A library for the MQTT protocol", "version": "5.15.0", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", "Siarhei Buntsevich (https://github.com/scarry1992)", "Yoseph Maguire (https://github.com/YoDaMa)", "Daniel Lando (https://github.com/robertsLando)" ], "keywords": [ "mqtt", "publish/subscribe", "publish", "subscribe" ], "license": "MIT", "repository": { "type": "git", "url": "git://github.com/mqttjs/MQTT.js.git" }, "main": "./build/index.js", "module": "./dist/mqtt.esm.js", "bin": { "mqtt_pub": "./build/bin/pub.js", "mqtt_sub": "./build/bin/sub.js", "mqtt": "./build/bin/mqtt.js" }, "files": [ "dist/", "CONTRIBUTING.md", "LICENSE.md", "help/", "build/" ], "exports": { ".": { "react-native": "./dist/mqtt.esm.js", "browser": { "import": "./dist/mqtt.esm.js", "default": "./dist/mqtt.min.js" }, "default": "./build/index.js" }, "./package.json": "./package.json", "./*.map": "./build/*.js.map", "./dist/*": "./dist/*.js", "./*": "./build/*.js" }, "types": "build/index.d.ts", "typesVersions": { "*": { "*": [ "./build/index.d.ts" ] } }, "scripts": { "lint": "eslint --ext .ts .", "lint-fix": "eslint --fix --ext .ts .", "build:ts": "rimraf build/ && tsc -p tsconfig.build.json", "build:browser": "node esbuild.js", "build": "npm run build:ts && npm run build:browser", "prepare": "npm run build", "unit-test:node": "node -r esbuild-register --test-concurrency 4 --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=spec --test-reporter-destination=stdout --test test/node/*.ts ", "unit-test:browser": "wtr", "test:node": "node_modules/.bin/nyc npm run unit-test:node", "test:browser": "npm run build && npm run unit-test:browser", "test": "npm run test:node", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", "changelog-init": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", "release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it" }, "publishConfig": { "registry": "https://registry.npmjs.org", "provenance": true }, "pre-commit": [ "lint" ], "engines": { "node": ">=16.0.0" }, "browser": { "./mqtt.js": "./dist/mqtt.js", "fs": false, "tls": false, "net": false }, "dependencies": { "@types/readable-stream": "^4.0.21", "@types/ws": "^8.18.1", "commist": "^3.2.0", "concat-stream": "^2.0.0", "debug": "^4.4.1", "help-me": "^5.0.0", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "mqtt-packet": "^9.0.2", "number-allocator": "^1.0.14", "readable-stream": "^4.7.0", "rfdc": "^1.4.1", "socks": "^2.8.6", "split2": "^4.2.0", "worker-timers": "^8.0.23", "ws": "^8.18.3" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.32.0", "@esm-bundle/chai": "^4.3.4", "@release-it/conventional-changelog": "^10.0.1", "@tsconfig/node20": "^20.1.6", "@types/chai": "^5.2.2", "@types/node": "^20.17.16", "@types/sinon": "^17.0.4", "@types/tape": "^5.8.1", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", "@web/test-runner": "^0.20.2", "@web/test-runner-playwright": "^0.11.1", "aedes-cli": "^0.8.0", "chai": "^5.2.1", "chokidar": "^4.0.3", "conventional-changelog-cli": "^5.0.0", "end-of-stream": "^1.4.5", "esbuild": "^0.25.8", "esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-register": "^3.6.0", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.3", "global": "^4.4.0", "globals": "^16.3.0", "leaked-handles": "^5.2.0", "mkdirp": "^3.0.1", "mqtt-connection": "^4.1.0", "mqtt-level-store": "^3.1.0", "nyc": "^17.1.0", "pre-commit": "^1.2.2", "prettier": "^3.6.2", "release-it": "^19.0.4", "rimraf": "^6.0.1", "should": "^13.2.3", "sinon": "^17.0.2", "snazzy": "^9.0.0", "tape": "^5.9.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" } } ================================================ FILE: src/bin/mqtt.ts ================================================ #!/usr/bin/env node /* * Copyright (c) 2015-2015 MQTT.js contributors. * Copyright (c) 2011-2014 Adam Rudd. * * See LICENSE for more information */ import path from 'path' import Commist from 'commist' import help from 'help-me' import publish from './pub' import subscribe from './sub' import { MQTTJS_VERSION } from '../mqtt' const helpMe = help({ dir: path.join(__dirname, '../../', 'help'), ext: '.txt', }) const commist = Commist() commist.register('publish', publish) commist.register('pub', publish) commist.register('subscribe', subscribe) commist.register('sub', subscribe) commist.register('version', () => { console.log('MQTT.js version:', MQTTJS_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() } ================================================ FILE: src/bin/pub.ts ================================================ #!/usr/bin/env node import { Writable } from 'readable-stream' import path from 'path' import fs from 'fs' import concat from 'concat-stream' import help from 'help-me' import minimist, { type ParsedArgs } from 'minimist' import split2 from 'split2' import { type IClientOptions, type IClientPublishOptions } from 'src/lib/client' import { pipeline } from 'stream' import { connect } from '../mqtt' const helpMe = help({ dir: path.join(__dirname, '../../', 'help'), }) function send(args: ParsedArgs) { const client = connect(args as IClientOptions) client.on('connect', () => { client.publish( args.topic, args.message, args as IClientPublishOptions, (err) => { if (err) { console.warn(err) } client.end() }, ) }) client.on('error', (err) => { console.warn(err) client.end() }) } function multisend(args: ParsedArgs) { const client = connect(args as IClientOptions) const sender = new Writable({ objectMode: true, }) sender._write = (line, enc, cb) => { client.publish( args.topic, line.trim(), args as IClientPublishOptions, cb, ) } client.on('connect', () => { pipeline(process.stdin, split2(), sender, (err) => { client.end() if (err) { throw err } }) }) } export default function start(args: string[]) { const parsedArgs = 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 (parsedArgs.help) { return helpMe.toStdout('publish') } if (parsedArgs.key) { parsedArgs.key = fs.readFileSync(parsedArgs.key) } if (parsedArgs.cert) { parsedArgs.cert = fs.readFileSync(parsedArgs.cert) } if (parsedArgs.ca) { parsedArgs.ca = fs.readFileSync(parsedArgs.ca) } if (parsedArgs.key && parsedArgs.cert && !parsedArgs.protocol) { parsedArgs.protocol = 'mqtts' } if (parsedArgs.port) { if (typeof parsedArgs.port !== 'number') { console.warn( "# Port: number expected, '%s' was given.", typeof parsedArgs.port, ) return } } if (parsedArgs['will-topic']) { parsedArgs.will = {} parsedArgs.will.topic = parsedArgs['will-topic'] parsedArgs.will.payload = parsedArgs['will-message'] parsedArgs.will.qos = parsedArgs['will-qos'] parsedArgs.will.retain = parsedArgs['will-retain'] } if (parsedArgs.insecure) { parsedArgs.rejectUnauthorized = false } parsedArgs.topic = (parsedArgs.topic || parsedArgs._.shift())?.toString() parsedArgs.message = ( parsedArgs.message || parsedArgs._.shift() )?.toString() if (!parsedArgs.topic) { console.error('missing topic\n') return helpMe.toStdout('publish') } if (parsedArgs.stdin) { if (parsedArgs.multiline) { multisend(parsedArgs) } else { process.stdin.pipe( concat((data) => { parsedArgs.message = data send(parsedArgs) }), ) } } else { send(parsedArgs) } } if (require.main === module) { start(process.argv.slice(2)) } ================================================ FILE: src/bin/sub.ts ================================================ #!/usr/bin/env node import path from 'path' import fs from 'fs' import minimist from 'minimist' import help from 'help-me' import { type IClientOptions } from 'src/lib/client' import { connect } from '../mqtt' const helpMe = help({ dir: path.join(__dirname, '../../', 'help'), }) export default function start(args: string[]) { const parsedArgs = 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 (parsedArgs.help) { return helpMe.toStdout('subscribe') } parsedArgs.topic = parsedArgs.topic || parsedArgs._.shift() if (!parsedArgs.topic) { console.error('missing topic\n') return helpMe.toStdout('subscribe') } if (parsedArgs.key) { parsedArgs.key = fs.readFileSync(parsedArgs.key) } if (parsedArgs.cert) { parsedArgs.cert = fs.readFileSync(parsedArgs.cert) } if (parsedArgs.ca) { parsedArgs.ca = fs.readFileSync(parsedArgs.ca) } if (parsedArgs.key && parsedArgs.cert && !parsedArgs.protocol) { parsedArgs.protocol = 'mqtts' } if (parsedArgs.insecure) { parsedArgs.rejectUnauthorized = false } if (parsedArgs.port) { if (typeof parsedArgs.port !== 'number') { console.warn( "# Port: number expected, '%s' was given.", typeof parsedArgs.port, ) return } } if (parsedArgs['will-topic']) { parsedArgs.will = {} parsedArgs.will.topic = parsedArgs['will-topic'] parsedArgs.will.payload = parsedArgs['will-message'] parsedArgs.will.qos = parsedArgs['will-qos'] parsedArgs.will.retain = parsedArgs['will-retain'] } parsedArgs.keepAlive = parsedArgs['keep-alive'] const client = connect(parsedArgs as IClientOptions) client.on('connect', () => { client.subscribe( parsedArgs.topic, { qos: parsedArgs.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 (parsedArgs.verbose) { console.log(topic, payload.toString()) } else { console.log(payload.toString()) } }) client.on('error', (err) => { console.warn(err) client.end() }) } if (require.main === module) { start(process.argv.slice(2)) } ================================================ FILE: src/index.ts ================================================ import * as mqtt from './mqtt' export default mqtt export * from './mqtt' ================================================ FILE: src/lib/BufferedDuplex.ts ================================================ import { Duplex, type Transform } from 'readable-stream' import { Buffer } from 'buffer' import { type IClientOptions } from './client' /** * Utils writev function for browser, ensure to write Buffers to socket (convert strings). */ export function writev( chunks: { chunk: any; encoding: string }[], cb: (err?: Error) => void, ) { 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].chunk, 'utf8') } else { buffers[i] = chunks[i].chunk } } this._write(Buffer.concat(buffers), 'binary', cb) } /** * How this works: * - `socket` is the `WebSocket` instance, the connection to our broker. * - `proxy` is a `Transform`, it ensure data written to the `socket` is a `Buffer`. * This class buffers the data written to the `proxy` (so then to `socket`) until the `socket` is ready. * The stream returned from this class, will be passed to the `MqttClient`. */ export class BufferedDuplex extends Duplex { public socket: WebSocket private proxy: Transform private isSocketOpen: boolean private writeQueue: Array<{ chunk: any encoding: string cb: (err?: Error) => void }> constructor(opts: IClientOptions, proxy: Transform, socket: WebSocket) { super({ objectMode: true, }) this.proxy = proxy this.socket = socket this.writeQueue = [] if (!opts.objectMode) { this._writev = writev.bind(this) } this.isSocketOpen = false this.proxy.on('data', (chunk) => { if (!this.destroyed && this.readable) { this.push(chunk) } }) } _read(size?: number): void { this.proxy.read(size) } _write(chunk: any, encoding: string, cb: (err?: Error) => void) { if (!this.isSocketOpen) { // Buffer the data in a queue this.writeQueue.push({ chunk, encoding, cb }) } else { this.writeToProxy(chunk, encoding, cb) } } _final(callback: (error?: Error) => void): void { this.writeQueue = [] this.proxy.end(callback) } _destroy(err: Error, callback: (error: Error) => void): void { this.writeQueue = [] // do not pass error here otherwise we should listen for `error` event on proxy to prevent uncaught exception this.proxy.destroy() callback(err) } /** Method to call when socket is ready to stop buffering writes */ socketReady() { this.emit('connect') this.isSocketOpen = true this.processWriteQueue() } private writeToProxy( chunk: any, encoding: string, cb: (err?: Error) => void, ) { if (this.proxy.write(chunk, encoding) === false) { this.proxy.once('drain', cb) } else { cb() } } private processWriteQueue() { while (this.writeQueue.length > 0) { const { chunk, encoding, cb } = this.writeQueue.shift()! this.writeToProxy(chunk, encoding, cb) } } } ================================================ FILE: src/lib/KeepaliveManager.ts ================================================ import type MqttClient from './client' import getTimer, { type Timer } from './get-timer' import type { TimerVariant } from './shared' export default class KeepaliveManager { private _keepalive: number private timerId: number private timer: Timer private destroyed = false private counter: number private client: MqttClient private _keepaliveTimeoutTimestamp: number private _intervalEvery: number /** Timestamp of next keepalive timeout */ get keepaliveTimeoutTimestamp() { return this._keepaliveTimeoutTimestamp } /** Milliseconds of the actual interval */ get intervalEvery() { return this._intervalEvery } get keepalive() { return this._keepalive } constructor(client: MqttClient, variant: TimerVariant | Timer) { this.client = client this.timer = typeof variant === 'object' && 'set' in variant && 'clear' in variant ? variant : getTimer(variant) this.setKeepalive(client.options.keepalive) } private clear() { if (this.timerId) { this.timer.clear(this.timerId) this.timerId = null } } /** Change the keepalive */ setKeepalive(value: number) { // keepalive is in seconds value *= 1000 if (isNaN(value) || value <= 0 || value > 2147483647) { throw new Error( `Keepalive value must be an integer between 0 and 2147483647. Provided value is ${value}`, ) } this._keepalive = value this.reschedule() this.client['log'](`KeepaliveManager: set keepalive to ${value}ms`) } destroy() { this.clear() this.destroyed = true } reschedule() { if (this.destroyed) { return } this.clear() this.counter = 0 // https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Figure_3.5_Keep const keepAliveTimeout = Math.ceil(this._keepalive * 1.5) this._keepaliveTimeoutTimestamp = Date.now() + keepAliveTimeout this._intervalEvery = Math.ceil(this._keepalive / 2) this.timerId = this.timer.set(() => { // this should never happen, but just in case if (this.destroyed) { return } this.counter += 1 // after keepalive seconds, send a pingreq if (this.counter === 2) { this.client.sendPing() } else if (this.counter > 2) { this.client.onKeepaliveTimeout() } }, this._intervalEvery) } } ================================================ FILE: src/lib/TypedEmitter.ts ================================================ import EventEmitter from 'events' import { applyMixin } from './shared' export type EventHandler = // Add more overloads as necessary | ((arg1: any, arg2: any, arg3: any, arg4: any) => void) | ((arg1: any, arg2: any, arg3: any) => void) | ((arg1: any, arg2: any) => void) | ((arg1: any) => void) | ((...args: any[]) => void) export interface TypedEventEmitter< TEvents extends Record, > { on( event: TEvent, callback: TEvents[TEvent], ): this once( event: TEvent, callback: TEvents[TEvent], ): this prependListener( event: TEvent, callback: TEvents[TEvent], ): this prependOnceListener( event: TEvent, callback: TEvents[TEvent], ): this removeListener( event: TEvent, callback: TEvents[TEvent], ): this off( event: TEvent, callback: TEvents[TEvent], ): this removeAllListeners(event?: keyof TEvents): this emit( event: TEvent, ...args: Parameters ): boolean setMaxListeners(n: number): this getMaxListeners(): number listeners( eventName: TEvent, ): TEvents[TEvent][] rawListeners( eventName: TEvent, ): TEvents[TEvent][] listenerCount( event: TEvent, listener?: TEvents[TEvent], ): number eventNames(): Array } // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class TypedEventEmitter< TEvents extends Record, > {} // Make TypedEventEmitter inherit from EventEmitter without actually extending applyMixin(TypedEventEmitter, EventEmitter) ================================================ FILE: src/lib/client.ts ================================================ /** * Module dependencies */ import mqttPacket, { type IAuthPacket, IConnackPacket, IDisconnectPacket, IPublishPacket, type ISubscribePacket, type ISubscription, type IUnsubscribePacket, Packet, type QoS, type ISubackPacket, type IConnectPacket, } from 'mqtt-packet' import { type DuplexOptions, Writable } from 'readable-stream' import clone from 'rfdc/default' import _debug from 'debug' import type { ClientOptions } from 'ws' import { type ClientRequestArgs } from 'http' import * as validations from './validations' import Store, { type IStore } from './store' import handlePacket from './handlers' import DefaultMessageIdProvider, { type IMessageIdProvider, } from './default-message-id-provider' import TopicAliasRecv from './topic-alias-recv' import { type DoneCallback, type ErrorWithReasonCode, ErrorWithSubackPacket, type GenericCallback, type IStream, MQTTJS_VERSION, type StreamBuilder, type TimerVariant, type VoidCallback, nextTick, } from './shared' import type TopicAliasSend from './topic-alias-send' import { TypedEventEmitter } from './TypedEmitter' import KeepaliveManager from './KeepaliveManager' import isBrowser, { isWebWorker } from './is-browser' import { type Timer } from './get-timer' const setImmediate = globalThis.setImmediate || (((...args: any[]) => { const callback = args.shift() nextTick(() => { callback(...args) }) }) as typeof globalThis.setImmediate) const defaultConnectOptions: IClientOptions = { keepalive: 60, reschedulePings: true, protocolId: 'MQTT', protocolVersion: 4, reconnectPeriod: 1000, connectTimeout: 30 * 1000, clean: true, resubscribe: true, subscribeBatchSize: null, writeCache: true, timerVariant: 'auto', } export type BaseMqttProtocol = | 'wss' | 'ws' | 'mqtt' | 'mqtts' | 'tcp' | 'ssl' | 'wx' | 'wxs' | 'ali' | 'alis' // create a type that allows all MqttProtocol + `+unix` string export type MqttProtocolWithUnix = `${BaseMqttProtocol}+unix` export type MqttProtocol = BaseMqttProtocol | MqttProtocolWithUnix export type StorePutCallback = () => void export interface ISecureClientOptions { /** * optional private keys in PEM format */ key?: string | string[] | Buffer | Buffer[] | any[] keyPath?: string /** * optional cert chains in PEM format */ cert?: string | string[] | Buffer | Buffer[] certPath?: string /** * Optionally override the trusted CA certificates in PEM format */ ca?: string | string[] | Buffer | Buffer[] caPaths?: string | string[] rejectUnauthorized?: boolean /** * optional alpn's */ ALPNProtocols?: string[] | Buffer[] | Uint8Array[] | Buffer | Uint8Array } export type AckHandler = ( topic: string, message: Buffer, packet: any, cb: (error: Error | number, code?: number) => void, ) => void export interface IClientOptions extends ISecureClientOptions { /** CLIENT PROPERTIES */ /** Encoding to use. Example 'binary' */ encoding?: BufferEncoding /** Set browser buffer size. Default to 512KB */ browserBufferSize?: number /** used in ws protocol to set `objectMode` */ binary?: boolean /** Used on ali protocol */ my?: any /** Manually call `connect` after creating client instance */ manualConnect?: boolean /** Custom auth packet properties */ authPacket?: Partial /** Disable/Enable writeToStream.cacheNumbers */ writeCache?: boolean /** Should be set to `host` */ servername?: string /** The default protocol to use when using `servers` and no protocol is specified */ defaultProtocol?: MqttProtocol /** Support clientId passed in the query string of the url */ query?: Record /** Auth string in the format : */ auth?: string /** Optional SOCKS proxy to use for TCP / TLS connections , i.e. socks5://localhost:1333, socks4://localhost:1333, socks5h://localhost:1333 . Default is socks5h. */ socksProxy?: string /** Timeout for establishing a socks connection */ socksTimeout?: number /** Custom ack handler */ customHandleAcks?: AckHandler /** Broker port */ port?: number /** Broker host. Does NOT include port */ host?: string /** @deprecated use `host instead */ hostname?: string /** Set to true if the connection is to a unix socket */ unixSocket?: boolean /** Websocket `path` added as suffix or Unix socket path when `unixSocket` option is true */ path?: string /** The `MqttProtocol` to use */ protocol?: MqttProtocol /** Websocket options */ wsOptions?: ClientOptions | ClientRequestArgs | DuplexOptions /** * 1000 milliseconds, interval between two reconnections */ reconnectPeriod?: number /** * Set to true to enable the reconnect period to apply if the initial * connection is denied with an error in the CONNACK packet, such as with an * authentication error. */ reconnectOnConnackError?: boolean /** * 30 * 1000 milliseconds, time to wait before a CONNACK is received */ connectTimeout?: number /** * a Store for the incoming packets */ incomingStore?: IStore /** * a Store for the outgoing packets */ outgoingStore?: IStore /** Enable/Disable queue for QoS 0 packets */ queueQoSZero?: boolean /** Custom log function, default uses `debug` */ log?: (...args: any[]) => void /** automatically use topic alias */ autoUseTopicAlias?: boolean /** automatically assign topic alias */ autoAssignTopicAlias?: boolean /** Set to false to disable ping reschedule. When enabled ping messages are rescheduled on each message sent */ reschedulePings?: boolean /** List of broker servers. On each reconnect try the next server will be used */ servers?: Array<{ host: string port: number protocol?: | 'wss' | 'ws' | 'mqtt' | 'mqtts' | 'tcp' | 'ssl' | 'wx' | 'wxs' }> /** * true, set to false to disable re-subscribe functionality */ resubscribe?: boolean /** * Maximum number of topics to include in a single SUBSCRIBE packet. * When subscribing to more topics than this, the client will automatically * split them into batches of this size. * This is useful on AWS IoT Core, which limits each SUBSCRIBE packet to 8 topics. */ subscribeBatchSize?: number /** when defined this function will be called to transform the url string generated by MqttClient from provided options */ transformWsUrl?: ( url: string, options: IClientOptions, client: MqttClient, ) => string /** when defined this function will be called to create the Websocket instance, used to add custom protocols or websocket implementations */ createWebsocket?: ( url: string, websocketSubProtocols: string[], options: IClientOptions, ) => any /** Custom message id provider */ messageIdProvider?: IMessageIdProvider /** When using websockets, this is the timeout used when writing to socket. Default 1000 (1s) */ browserBufferTimeout?: number /** * When using websockets, this sets the `objectMode` option. * When in objectMode, streams can push Strings and Buffers * as well as any other JavaScript object. * Another major difference is that when in objectMode, * the internal buffering algorithm counts objects rather than bytes. * This means if we have a Transform stream with the highWaterMark option set to 5, * the stream will only buffer a maximum of 5 objects internally */ objectMode?: boolean /** CONNECT PACKET PROPERTIES */ /** * 'mqttjs_' + Math.random().toString(16).substr(2, 8) */ clientId?: string /** * 3=MQTT 3.1 4=MQTT 3.1.1 5=MQTT 5.0. Defaults to 4 */ protocolVersion?: IConnectPacket['protocolVersion'] /** * 'MQTT' */ protocolId?: IConnectPacket['protocolId'] /** * true, set to false to receive QoS 1 and 2 messages while offline */ clean?: boolean /** * 60 seconds, set to 0 to disable */ keepalive?: number /** * the username required by your broker, if any */ username?: string /** * the password required by your broker, if any */ password?: Buffer | string /** * a message that will sent by the broker automatically when the client disconnect badly. */ will?: IConnectPacket['will'] /** see `connect` packet: https://github.com/mqttjs/mqtt-packet/blob/master/types/index.d.ts#L65 */ properties?: IConnectPacket['properties'] /** * @description 'auto', set to 'native' or 'worker' if you're having issues with 'auto' detection * or pass a custom timer object */ timerVariant?: TimerVariant | Timer /** * false, set to true to force the use of native WebSocket if you're having issues with the detection */ forceNativeWebSocket?: boolean } export interface IClientPublishOptions { /** * the QoS */ qos?: QoS /** * the retain flag */ retain?: boolean /** * whether or not mark a message as duplicate */ dup?: boolean /* * MQTT 5.0 properties object */ properties?: IPublishPacket['properties'] /** * callback called when message is put into `outgoingStore` */ cbStorePut?: StorePutCallback } export interface IClientReconnectOptions { /** * a Store for the incoming packets */ incomingStore?: Store /** * a Store for the outgoing packets */ outgoingStore?: Store } export interface IClientSubscribeProperties { /* * MQTT 5.0 properties object of subscribe * */ properties?: ISubscribePacket['properties'] } export interface IClientSubscribeOptions extends IClientSubscribeProperties { /** * the QoS */ qos: QoS /* * no local flag * */ nl?: boolean /* * Retain As Published flag * */ rap?: boolean /* * Retain Handling option * */ rh?: number } export interface ISubscriptionRequest extends IClientSubscribeOptions { /** * is a subscribed to topic */ topic: string } export interface ISubscriptionGrant extends Omit { /** * is the granted qos level on it, may return 128 on error */ qos: QoS | 128 } export type ISubscriptionMap = { /** * object which has topic names as object keys and as value the options, like {'test1': {qos: 0}, 'test2': {qos: 2}}. */ [topic: string]: IClientSubscribeOptions } & { resubscribe?: boolean } export interface IClientUnsubscribeProperties { /* * MQTT 5.0 properties object for unsubscribe * */ properties?: IUnsubscribePacket['properties'] } export { IConnackPacket, IDisconnectPacket, IPublishPacket, Packet } export type OnConnectCallback = (packet: IConnackPacket) => void export type OnDisconnectCallback = (packet: IDisconnectPacket) => void export type ClientSubscribeCallback = ( err: Error | null, granted?: ISubscriptionGrant[], packet?: ISubackPacket, ) => void export type OnMessageCallback = ( topic: string, payload: Buffer, packet: IPublishPacket, ) => void export type OnPacketCallback = (packet: Packet) => void export type OnCloseCallback = () => void export type OnErrorCallback = (error: Error | ErrorWithReasonCode) => void export type PacketCallback = ( error?: Error | ErrorWithReasonCode, packet?: Packet, ) => any export type CloseCallback = (error?: Error) => void export interface MqttClientEventCallbacks { connect: OnConnectCallback message: OnMessageCallback packetsend: OnPacketCallback packetreceive: OnPacketCallback disconnect: OnDisconnectCallback error: OnErrorCallback close: OnCloseCallback end: VoidCallback reconnect: VoidCallback offline: VoidCallback outgoingEmpty: VoidCallback } /** * MqttClient constructor * * @param {Stream} stream - stream * @param {Object} [options] - connection options * (see Connection#connect) */ export default class MqttClient extends TypedEventEmitter { public static VERSION = MQTTJS_VERSION /** Public fields */ /** It's true when client is connected to broker */ public connected: boolean public disconnecting: boolean public disconnected: boolean public reconnecting: boolean public incomingStore: IStore public outgoingStore: IStore public options: IClientOptions public queueQoSZero: boolean public _reconnectCount: number public log: (...args: any[]) => void public messageIdProvider: IMessageIdProvider public outgoing: Record< number, { volatile: boolean; cb: (err: Error, packet?: Packet) => void } > public messageIdToTopic: Record public noop: (error?: any) => void public keepaliveManager: KeepaliveManager /** * The connection to the Broker. In browsers env this also have `socket` property * set to the `WebSocket` instance. */ public stream: IStream public queue: { packet: Packet; cb: PacketCallback }[] /* Private fields */ /** Function used to build the stream */ private streamBuilder: StreamBuilder private _resubscribeTopics: ISubscriptionMap private connackTimer: NodeJS.Timeout private reconnectTimer: NodeJS.Timeout private _storeProcessing: boolean /** keep a reference of packets that have been successfully processed from outgoing store */ private _packetIdsDuringStoreProcessing: Record private _storeProcessingQueue: { invoke: () => any cbStorePut?: DoneCallback callback: GenericCallback }[] private _firstConnection: boolean private topicAliasRecv: TopicAliasRecv private topicAliasSend: TopicAliasSend private _deferredReconnect: () => void private connackPacket: IConnackPacket public static defaultId() { return `mqttjs_${Math.random().toString(16).substr(2, 8)}` } constructor(streamBuilder: StreamBuilder, options: IClientOptions) { super() this.options = options || {} // Defaults for (const 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('mqttjs:client') this.noop = this._noop.bind(this) this.log('MqttClient :: version:', MqttClient.VERSION) if (isWebWorker) { this.log('MqttClient :: environment', 'webworker') } else { this.log( 'MqttClient :: environment', isBrowser ? 'browser' : 'node', ) } 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](null, 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 = {} // Keepalive manager, setup in _setupKeepaliveManager this.keepaliveManager = null // Is the client connected? this.connected = false // Are we disconnecting? this.disconnecting = false // Are we reconnecting? this.reconnecting = 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._destroyKeepaliveManager() 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() } } /** * @param packet the packet received by the broker * @return the auth packet to be returned to the broker * @api public */ public handleAuth(packet: IAuthPacket, callback: PacketCallback) { 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 */ public handleMessage(packet: IPublishPacket, callback: DoneCallback) { callback() } /** * _nextId * @return unsigned int */ private _nextId() { return this.messageIdProvider.allocate() } /** * getLastMessageId * @return unsigned int */ public getLastMessageId() { return this.messageIdProvider.getLastAllocated() } /** * Setup the event handlers in the inner stream, sends `connect` and `auth` packets */ public 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() if (this.disconnected && !this.reconnecting) { this.incomingStore = this.options.incomingStore || new Store() this.outgoingStore = this.options.outgoingStore || new Store() this.disconnecting = false this.disconnected = false } 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) // error.code will only be set on NodeJS env, browser don't allow to detect errors on sockets // also emitting errors on browsers seems to create issues if (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.log('stream: emit close to MqttClient') this.emit('close') }) // Send a connect packet this.log('connect: sending packet `connect`') const connectPacket: IConnectPacket = { cmd: 'connect', protocolId: this.options.protocolId, protocolVersion: this.options.protocolVersion, clean: this.options.clean, clientId: this.options.clientId, keepalive: this.options.keepalive, username: this.options.username, password: this.options.password as Buffer, properties: this.options.properties, } if (this.options.will) { connectPacket.will = { ...this.options.will, payload: this.options.will?.payload as Buffer, } } 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: IAuthPacket = { 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.emit('error', new Error('connack timeout')) this._cleanUp(true) }, this.options.connectTimeout) return this } /** * 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); */ public publish(topic: string, message: string | Buffer): MqttClient public publish( topic: string, message: string | Buffer, callback?: PacketCallback, ): MqttClient public publish( topic: string, message: string | Buffer, opts?: IClientPublishOptions, callback?: PacketCallback, ): MqttClient public publish( topic: string, message: string | Buffer, opts?: IClientPublishOptions | DoneCallback, callback?: PacketCallback, ): MqttClient { this.log('publish :: message `%s` to topic `%s`', message, topic) const { options } = this // .publish(topic, payload, cb); if (typeof opts === 'function') { callback = opts as DoneCallback opts = null } opts = opts || {} // default opts const defaultOpts: IClientPublishOptions = { qos: 0, retain: false, dup: false, } opts = { ...defaultOpts, ...opts } const { qos, retain, dup, properties, cbStorePut } = opts if (this._checkDisconnecting(callback)) { return this } const publishProc = () => { let messageId = 0 if (qos === 1 || qos === 2) { messageId = this._nextId() if (messageId === null) { this.log('No messageId left') return false } } const packet: IPublishPacket = { cmd: 'publish', topic, payload: message, qos, retain, messageId, dup, } if (options.protocolVersion === 5) { packet.properties = properties } this.log('publish :: qos', qos) switch (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, cbStorePut) break default: this.log('MqttClient:publish: packet cmd: %s', packet.cmd) this._sendPacket(packet, callback, cbStorePut) break } return true } if ( this._storeProcessing || this._storeProcessingQueue.length > 0 || !publishProc() ) { this._storeProcessingQueue.push({ invoke: publishProc, cbStorePut: opts.cbStorePut, callback, }) } return this } public publishAsync( topic: string, message: string | Buffer, ): Promise public publishAsync( topic: string, message: string | Buffer, opts?: IClientPublishOptions, ): Promise public publishAsync( topic: string, message: string | Buffer, opts?: IClientPublishOptions, ): Promise { return new Promise((resolve, reject) => { this.publish(topic, message, opts, (err, packet) => { if (err) { reject(err) } else { resolve(packet) } }) }) } /** * 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); */ public subscribe( topicObject: string | string[] | ISubscriptionMap, ): MqttClient public subscribe( topicObject: string | string[] | ISubscriptionMap, callback?: ClientSubscribeCallback, ): MqttClient public subscribe( topicObject: string | string[] | ISubscriptionMap, opts?: IClientSubscribeOptions | IClientSubscribeProperties, ): MqttClient public subscribe( topicObject: string | string[] | ISubscriptionMap, opts?: IClientSubscribeOptions | IClientSubscribeProperties, callback?: ClientSubscribeCallback, ): MqttClient public subscribe( topicObject: string | string[] | ISubscriptionMap, opts?: | IClientSubscribeOptions | IClientSubscribeProperties | ClientSubscribeCallback, callback?: ClientSubscribeCallback, ): MqttClient { const version = this.options.protocolVersion if (typeof opts === 'function') { callback = opts } callback = callback || this.noop // force re-subscribe on reconnect. This is only true // when provided `topicObject` is `this._resubscribeTopics` let resubscribe = false let topicsList = [] if (typeof topicObject === 'string') { topicObject = [topicObject] topicsList = topicObject } else if (Array.isArray(topicObject)) { topicsList = topicObject } else if (typeof topicObject === 'object') { resubscribe = topicObject.resubscribe delete topicObject.resubscribe topicsList = Object.keys(topicObject) } // validate topics const invalidTopic = validations.validateTopics(topicsList) 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: Partial = { qos: 0, } if (version === 5) { defaultOpts.nl = false defaultOpts.rap = false defaultOpts.rh = 0 } opts = { ...defaultOpts, ...opts } as IClientSubscribeOptions const { properties } = opts const subs: ISubscriptionRequest[] = [] const parseSub = ( topic: string, subOptions?: IClientSubscribeOptions, ) => { // subOptions is defined only when providing a subs map, use opts otherwise subOptions = (subOptions || opts) as IClientSubscribeOptions if ( !Object.prototype.hasOwnProperty.call( this._resubscribeTopics, topic, ) || this._resubscribeTopics[topic].qos < subOptions.qos || resubscribe ) { const currentOpts: ISubscription & IClientSubscribeProperties = { topic, qos: subOptions.qos, } if (version === 5) { currentOpts.nl = subOptions.nl currentOpts.rap = subOptions.rap currentOpts.rh = subOptions.rh // use opts.properties currentOpts.properties = properties } this.log( 'subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos, ) subs.push(currentOpts) } } if (Array.isArray(topicObject)) { // array of topics topicObject.forEach((topic) => { this.log('subscribe: array topic %s', topic) parseSub(topic) }) } else { // object topic --> subOptions (no properties) Object.keys(topicObject).forEach((topic) => { this.log( 'subscribe: object topic %s, %o', topic, topicObject[topic], ) parseSub(topic, topicObject[topic]) }) } if (!subs.length) { callback(null, []) return this } const subscribeChunkedSubs = ( chunkedSubs: ISubscriptionRequest[], messageId: number, ): Promise => { const packet: ISubscribePacket = { cmd: 'subscribe', subscriptions: chunkedSubs, // qos: 1, // retain: false, // dup: false, messageId, } if (properties) { packet.properties = properties } // subscriptions to resubscribe to in case of disconnect if (this.options.resubscribe) { this.log('subscribe :: resubscribe true') const topics = [] chunkedSubs.forEach((sub) => { if (this.options.reconnectPeriod > 0) { const topic: IClientSubscribeOptions = { 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 } const promise = new Promise((resolve, reject) => { this.outgoing[packet.messageId] = { volatile: true, cb(err, packet2: ISubackPacket) { if (!err) { const { granted } = packet2 for ( let grantedI = 0; grantedI < granted.length; grantedI += 1 ) { chunkedSubs[grantedI].qos = granted[ grantedI ] as QoS } } if (!err) { resolve(packet2) } else { reject( new ErrorWithSubackPacket(err.message, packet2), ) } }, } }) this.log('subscribe :: call _sendPacket') this._sendPacket(packet) return promise } const subscribeProc = () => { const batchSize = this.options.subscribeBatchSize ?? subs.length const subscribePromises: Promise[] = [] for (let i = 0; i < subs.length; i += batchSize) { const chunkedSubs = subs.slice(i, i + batchSize) const messageId = this._nextId() if (messageId === null) { this.log('No messageId left') return false } subscribePromises.push( subscribeChunkedSubs(chunkedSubs, messageId), ) } Promise.all(subscribePromises) .then((packets) => { callback(null, subs, packets.at(-1)) }) .catch((err: ErrorWithSubackPacket) => { callback(err, subs, err.packet) }) return true } if ( this._storeProcessing || this._storeProcessingQueue.length > 0 || !subscribeProc() ) { this._storeProcessingQueue.push({ invoke: subscribeProc, callback, }) } return this } public subscribeAsync( topicObject: string | string[] | ISubscriptionMap, ): Promise public subscribeAsync( topicObject: string | string[] | ISubscriptionMap, opts?: IClientSubscribeOptions | IClientSubscribeProperties, ): Promise public subscribeAsync( topicObject: string | string[] | ISubscriptionMap, opts?: IClientSubscribeOptions | IClientSubscribeProperties, ): Promise { return new Promise((resolve, reject) => { this.subscribe(topicObject, opts, (err, granted) => { if (err) { reject(err) } else { resolve(granted) } }) }) } /** * 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); */ public unsubscribe(topic: string | string[]): MqttClient public unsubscribe( topic: string | string[], opts?: IClientUnsubscribeProperties, ): MqttClient public unsubscribe( topic: string | string[], callback?: PacketCallback, ): MqttClient public unsubscribe( topic: string | string[], opts?: IClientUnsubscribeProperties, callback?: PacketCallback, ): MqttClient public unsubscribe( topic: string | string[], opts?: IClientUnsubscribeProperties | PacketCallback, callback?: PacketCallback, ): MqttClient { if (typeof topic === 'string') { topic = [topic] } if (typeof opts === 'function') { callback = 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: IUnsubscribePacket = { cmd: 'unsubscribe', // qos: 1, messageId, unsubscriptions: [], } 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 } public unsubscribeAsync( topic: string | string[], ): Promise public unsubscribeAsync( topic: string | string[], opts?: IClientUnsubscribeProperties, ): Promise public unsubscribeAsync( topic: string | string[], opts?: IClientUnsubscribeProperties, ): Promise { return new Promise((resolve, reject) => { this.unsubscribe(topic, opts, (err, packet) => { if (err) { reject(err) } else { resolve(packet) } }) }) } /** * 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 */ public end(cb?: DoneCallback): MqttClient public end(force?: boolean): MqttClient public end(opts?: Partial, cb?: DoneCallback): MqttClient public end(force?: boolean, cb?: DoneCallback): MqttClient public end( force?: boolean, opts?: Partial, cb?: DoneCallback, ): MqttClient public end( force?: boolean | Partial | DoneCallback, opts?: Partial | DoneCallback, cb?: DoneCallback, ): MqttClient { this.log('end :: (%s)', this.options.clientId) if (force == null || typeof force !== 'boolean') { cb = cb || (opts as DoneCallback) opts = force as Partial force = false } if (typeof opts !== 'object') { cb = cb || opts opts = null } this.log('end :: cb? %s', !!cb) if (!cb || typeof cb !== 'function') { 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() } else if ( this.options.reconnectPeriod === 0 || this.options.manualConnect ) { this.disconnecting = false } } 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 } public endAsync(): Promise public endAsync(force?: boolean): Promise public endAsync(opts?: Partial): Promise public endAsync( force?: boolean, opts?: Partial, ): Promise public endAsync( force?: boolean | Partial, opts?: Partial, ): Promise { return new Promise((resolve, reject) => { this.end(force as boolean, opts, (err) => { if (err) { reject(err) } else { resolve() } }) }) } /** * 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()); */ public removeOutgoingMessage(messageId: number): MqttClient { 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 */ public reconnect( opts?: Pick, ): MqttClient { 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 } /** * PRIVATE METHODS * ===================== * */ /** * Flush all outgoing messages marked as `volatile` in `outgoing` queue. Volatile messages * typically are subscription and unsubscription requests. */ private _flushVolatile() { if (this.outgoing) { this.log( '_flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function', ) Object.keys(this.outgoing).forEach((messageId) => { if ( this.outgoing[messageId].volatile && typeof this.outgoing[messageId].cb === 'function' ) { this.outgoing[messageId].cb(new Error('Connection closed')) delete this.outgoing[messageId] } }) } } /** * Flush all outgoing messages */ private _flush() { if (this.outgoing) { this.log('_flush: queue exists? %b', !!this.outgoing) Object.keys(this.outgoing).forEach((messageId) => { if (typeof this.outgoing[messageId].cb === 'function') { this.outgoing[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 this.outgoing[messageId] } }) } } private _removeTopicAliasAndRecoverTopicName(packet: IPublishPacket) { let alias: number | undefined 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 } } private _checkDisconnecting(callback?: GenericCallback) { if (this.disconnecting) { if (callback && callback !== this.noop) { callback(new Error('client disconnecting')) } else { this.emit('error', new Error('client disconnecting')) } } return this.disconnecting } /** * _reconnect - implement reconnection * @api private */ private _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 */ private _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 */ private _clearReconnect() { this.log('_clearReconnect : clearing reconnect timer') if (this.reconnectTimer) { clearInterval(this.reconnectTimer) this.reconnectTimer = null } } /** * _cleanUp - clean up on connection end * @api private */ private _cleanUp(forced: boolean, done?: DoneCallback, 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.log( '_cleanUp :: (%s) :: destroying stream', this.options.clientId, ) this.stream.destroy() } else { const packet: IDisconnectPacket = { 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.reconnecting) { this.log( '_cleanUp :: client not disconnecting/reconnecting. Clearing and resetting reconnect.', ) this._clearReconnect() this._setupReconnect() } this._destroyKeepaliveManager() if (done && !this.connected) { this.log( '_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId, ) this.stream.removeListener('close', done) done() } } private _storeAndSend( packet: Packet, cb: DoneCallback, cbStorePut: DoneCallback, ) { this.log( 'storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd, ) let storePacket = packet let err: Error | undefined 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 as IPublishPacket, ) if (err) { return cb && cb(err) } } this.outgoingStore.put(storePacket, (err2) => { if (err2) { return cb && cb(err2) } cbStorePut() this._writePacket(packet, cb) }) } private _applyTopicAlias(packet: Packet) { if (this.options.protocolVersion === 5) { if (packet.cmd === 'publish') { let alias: number 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') } } } } private _noop(err?: Error) { this.log('noop ::', err) } /** Writes the packet to stream and emits events */ private _writePacket(packet: Packet, cb?: DoneCallback) { this.log('_writePacket :: packet: %O', packet) this.log('_writePacket :: emitting `packetsend`') this.emit('packetsend', packet) 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 */ private _sendPacket( packet: Packet, cb?: DoneCallback, cbStorePut?: DoneCallback, noStore?: boolean, ) { 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(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 */ private _storePacket( packet: Packet, cb: DoneCallback, cbStorePut: DoneCallback, ) { 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 as IPublishPacket, ) if (err) { return cb && cb(err) } } const qos = (storePacket as IPublishPacket).qos || 0 // check that the packet is not a qos of 0, or that the command is not a publish if ((qos === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { this.queue.push({ packet: storePacket, cb }) } else if (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')) } } /** * _setupKeepaliveManager - setup the keepalive manager */ private _setupKeepaliveManager() { this.log( '_setupKeepaliveManager :: keepalive %d (seconds)', this.options.keepalive, ) if (!this.keepaliveManager && this.options.keepalive) { this.keepaliveManager = new KeepaliveManager( this, this.options.timerVariant, ) } } private _destroyKeepaliveManager() { if (this.keepaliveManager) { this.log('_destroyKeepaliveManager :: destroying keepalive manager') this.keepaliveManager.destroy() this.keepaliveManager = null } } /** * Reschedule the ping interval */ public reschedulePing(force = false) { if ( this.keepaliveManager && this.options.keepalive && (force || this.options.reschedulePings) ) { this._reschedulePing() } } /** * Mostly needed for test purposes */ private _reschedulePing() { this.log('_reschedulePing :: rescheduling ping') this.keepaliveManager.reschedule() } public sendPing() { this.log('_sendPing :: sending pingreq') this._sendPacket({ cmd: 'pingreq' }) } public onKeepaliveTimeout() { this.emit('error', new Error('Keepalive timeout')) this.log('onKeepaliveTimeout :: calling _cleanUp with force true') this._cleanUp(true) } /** * _resubscribe * @api private */ private _resubscribe() { this.log('_resubscribe') const _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) if ( !this._firstConnection && // Only resubscribe in case of clean connection or if the server does not have a stored session. // The Session Present flag is available since v3.1.1 // https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349254 (this.options.clean || (this.options.protocolVersion >= 4 && !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: ISubscriptionMap = {} 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 */ private _onConnect(packet: IConnackPacket) { if (this.disconnected) { this.emit('connect', packet) return } this.connackPacket = packet this.messageIdProvider.clear() this._setupKeepaliveManager() this.connected = true /** check if there are packets in outgoing store and stream them */ const startStreamProcess = () => { let outStore = this.outgoingStore.createStream() /** destroy the outgoing store stream */ const remove = () => { outStore.destroy() outStore = null this._flushStoreProcessingQueue() clearStoreProcessing() } /** stop store processing and clear packets id processed */ 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) }) /** Read next packet in outgoing store and send it */ const storeDeliver = () => { // edge case, we wrapped this twice if (!outStore) { return } const packet2 = outStore.read(1) let cb: PacketCallback 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 } } this.removeListener('close', remove) if (allProcessed) { clearStoreProcessing() this._invokeAllStoreProcessingQueue() this.emit('connect', packet) } else { startStreamProcess() } }) storeDeliver() } // start flowing startStreamProcess() } private _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 } private _invokeAllStoreProcessingQueue() { while (this._invokeStoreProcessingQueue()) { /* empty */ } } private _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 */ private _removeOutgoingAndStoreMessage( messageId: number, cb: PacketCallback, ) { delete this.outgoing[messageId] this.outgoingStore.del({ messageId }, (err, packet) => { cb(err, packet) this.messageIdProvider.deallocate(messageId) this._invokeStoreProcessingQueue() }) } } ================================================ FILE: src/lib/connect/ali.ts ================================================ import { Buffer } from 'buffer' import { Transform } from 'readable-stream' import { type IStream, type StreamBuilder } from '../shared' import { type IClientOptions } from '../client' import type MqttClient from '../client' import { BufferedDuplex } from '../BufferedDuplex' let my: any let proxy: Transform let stream: BufferedDuplex let isInitialized = false 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: IClientOptions) { if (!opts.hostname) { opts.hostname = 'localhost' } if (!opts.path) { opts.path = '/' } if (!opts.wsOptions) { opts.wsOptions = {} } } function buildUrl(opts: IClientOptions, client: MqttClient) { 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(() => { stream.socketReady() }) 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', () => { if (reader.result instanceof ArrayBuffer) { proxy.push(Buffer.from(reader.result)) return } proxy.push(Buffer.from(reader.result, 'utf-8')) }) reader.readAsArrayBuffer(res.data) } }) my.onSocketClose(() => { stream.end() stream.destroy() }) my.onSocketError((err) => { stream.destroy(err) }) } const buildStream: StreamBuilder = (client, opts): IStream => { 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) my = opts.my // https://miniprogram.alipay.com/docs/miniprogram/mpdev/api_network_connectsocket my.connectSocket({ url, protocols: websocketSubProtocol, }) proxy = buildProxy() stream = new BufferedDuplex(opts, proxy, my) bindEventHandler() return stream } export default buildStream ================================================ FILE: src/lib/connect/index.ts ================================================ /* eslint-disable @typescript-eslint/no-require-imports */ import _debug from 'debug' import url from 'url' import MqttClient, { type IClientOptions, type MqttClientEventCallbacks, type MqttProtocol, } from '../client' import isBrowser from '../is-browser' import { type StreamBuilder } from '../shared' // Handling the process.nextTick is not a function error in react-native applications. if (typeof process?.nextTick !== 'function') { process.nextTick = setImmediate } const debug = _debug('mqttjs') let protocols: Record = null /** * Parse the auth attribute and merge username and password in the options object. * * @param {Object} [opts] option object */ function parseAuthOptions(opts: IClientOptions) { let matches: RegExpMatchArray | null if (opts.auth) { matches = opts.auth.match(/^(.+):(.+)$/) if (matches) { const [, username, password] = matches opts.username = username opts.password = password } else { opts.username = opts.auth } } } /** * connect - connect to an MQTT broker. */ function connect(brokerUrl: string): MqttClient function connect(opts: IClientOptions): MqttClient function connect(brokerUrl: string, opts?: IClientOptions): MqttClient function connect( brokerUrl: string | IClientOptions, opts?: IClientOptions, ): MqttClient { debug('connecting to an MQTT broker...') if (typeof brokerUrl === 'object' && !opts) { opts = brokerUrl brokerUrl = '' } opts = opts || {} // try to parse the broker url if (brokerUrl && typeof brokerUrl === 'string') { const parsedUrl = url.parse(brokerUrl, true) const parsedOptions: Partial = {} if (parsedUrl.port != null) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore parsedOptions.port = Number(parsedUrl.port) } parsedOptions.host = parsedUrl.hostname parsedOptions.query = parsedUrl.query as Record parsedOptions.auth = parsedUrl.auth parsedOptions.protocol = parsedUrl.protocol as MqttProtocol parsedOptions.path = parsedUrl.path opts = { ...parsedOptions, ...opts } // when parsing an url expect the protocol to be set if (!opts.protocol) { throw new Error('Missing protocol') } opts.protocol = opts.protocol.replace(/:$/, '') as MqttProtocol } opts.unixSocket = opts.unixSocket || opts.protocol?.includes('+unix') if (opts.unixSocket) { opts.protocol = opts.protocol.replace('+unix', '') as MqttProtocol } else if ( !opts.protocol?.startsWith('ws') && !opts.protocol?.startsWith('wx') ) { // consider path only with ws protocol or unix socket // url.parse could return path (for example when url ends with a `/`) // that could break the connection. See https://github.com/mqttjs/MQTT.js/pull/1874 delete opts.path } // 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 (isBrowser || opts.unixSocket) { opts.socksProxy = undefined } else if ( opts.socksProxy === undefined && typeof process !== 'undefined' ) { opts.socksProxy = process.env['MQTTJS_SOCKS_PROXY'] } 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') } } // only loads the protocols once if (!protocols) { protocols = {} if (!isBrowser && !opts.forceNativeWebSocket) { protocols.ws = require('./ws').streamBuilder protocols.wss = require('./ws').streamBuilder protocols.mqtt = require('./tcp').default protocols.tcp = require('./tcp').default protocols.ssl = require('./tls').default protocols.tls = protocols.ssl protocols.mqtts = require('./tls').default } else { protocols.ws = require('./ws').browserStreamBuilder protocols.wss = require('./ws').browserStreamBuilder protocols.wx = require('./wx').default protocols.wxs = require('./wx').default protocols.ali = require('./ali').default protocols.alis = require('./ali').default } } if (!protocols[opts.protocol]) { const isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 // returns the first available protocol based on available protocols (that depends on environment) // if no protocol is specified this will return mqtt on node and ws on browser // if secure it will return mqtts on node and wss on browser 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] as MqttProtocol } if (opts.clean === false && !opts.clientId) { throw new Error('Missing clientId for unclean clients') } if (opts.protocol) { opts.defaultProtocol = opts.protocol } function wrapper(client: MqttClient) { 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 } function connectAsync(brokerUrl: string): Promise function connectAsync(opts: IClientOptions): Promise function connectAsync( brokerUrl: string, opts?: IClientOptions, ): Promise function connectAsync( brokerUrl: string, opts: IClientOptions, allowRetries: boolean, ): Promise function connectAsync( brokerUrl: string | IClientOptions, opts?: IClientOptions, allowRetries = true, ): Promise { return new Promise((resolve, reject) => { const client = connect(brokerUrl as string, opts) const promiseResolutionListeners: Partial = { connect: (connack) => { removePromiseResolutionListeners() resolve(client) // Resolve on connect }, end: () => { removePromiseResolutionListeners() resolve(client) // Resolve on end }, error: (err) => { removePromiseResolutionListeners() client.end() reject(err) // Reject on error }, } // If retries are not allowed, reject on close if (allowRetries === false) { promiseResolutionListeners.close = () => { promiseResolutionListeners.error( new Error("Couldn't connect to server"), ) } } // Remove listeners added to client by this promise function removePromiseResolutionListeners() { Object.keys(promiseResolutionListeners).forEach((eventName) => { client.off( eventName as keyof MqttClientEventCallbacks, promiseResolutionListeners[eventName], ) }) } // Add listeners to client Object.keys(promiseResolutionListeners).forEach((eventName) => { client.on( eventName as keyof MqttClientEventCallbacks, promiseResolutionListeners[eventName], ) }) }) } export default connect export { connectAsync } ================================================ FILE: src/lib/connect/socks.ts ================================================ import _debug from 'debug' import { Duplex } from 'stream' import { SocksClient, type SocksProxy } from 'socks' import * as dns from 'dns' import { type SocksProxyType } from 'socks/typings/common/constants' import { promisify } from 'util' import { type Socket } from 'net' import assert from 'assert' import { type IStream } from '../shared' const debug = _debug('mqttjs:socks') export interface SocksConnectionOptions { timeout?: number lookup?: (hostname: string) => Promise<{ address: string }> } class ProxyStream extends Duplex { private _flowing = false private _socket?: Socket constructor() { super({ autoDestroy: false }) this.cork() } _start(socket: Socket): void { debug('proxy stream started') assert(!this._socket) if (this.destroyed) { socket.destroy(this.errored) return } this._socket = socket if (!this._flowing) socket.pause() socket.on('data', this._onData) socket.on('end', this._onEnd) socket.on('error', this._onError) socket.on('close', this._onClose) socket.emit('connect') this.uncork() } _write( chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void, ): void { assert(this._socket) this._socket.write(chunk, callback) } _read(size: number): void { this._flowing = true this._socket?.resume?.() } _destroy( error: Error | null, callback: (error?: Error | null) => void, ): void { this._socket?.destroy?.(error) callback(error) } private _onData = (chunk: any): void => { assert(this._socket) this._flowing = this.push(chunk) if (!this._flowing) this._socket.pause() } private _onEnd = (): void => { debug('proxy stream received EOF') this.push(null) } private _onClose = (): void => { debug('proxy stream closed') this.destroy() } private _onError = (err: any): void => { debug('proxy stream died with error %s', err) this.destroy(err) } } function fatal(e: T): T { try { if ((e as any).code === undefined) (e as any).code = 'SOCKS' return e } catch { return e } } function typeFromProtocol( proto: string, ): [SocksProxyType | undefined, boolean] { switch (proto) { case 'socks5h:': return [5, true] case 'socks4a:': return [4, true] case 'socks5:': return [5, false] case 'socks4:': return [4, false] default: return [undefined, false] } } function parseSocksUrl(url: string): [SocksProxy, boolean] { const parsedUrl = new URL(url) if (parsedUrl.pathname || parsedUrl.hash || parsedUrl.search) { throw fatal(new Error('bad SOCKS URL')) } const [type, resolveThroughProxy] = typeFromProtocol(parsedUrl.protocol) if (!type) { throw fatal(new Error('bad SOCKS URL: invalid protocol')) } const port = parseInt(parsedUrl.port, 10) if (Number.isNaN(port)) { throw fatal(new Error('bad SOCKS URL: invalid port')) } const proxy: SocksProxy = { host: parsedUrl.hostname, port, type, } return [proxy, resolveThroughProxy] } async function connectSocks( destinationHost: string, destinationPort: number, socksUrl: string, stream: ProxyStream, options: SocksConnectionOptions = {}, ): Promise { const lookup = options.lookup ?? promisify(dns.lookup) const [proxy, resolveThroughProxy] = parseSocksUrl(socksUrl) if (!resolveThroughProxy) { debug('resolving %s locally', destinationHost) destinationHost = ( await lookup(destinationHost, { family: proxy.type === 4 ? 4 : 0, }) ).address } debug( 'establishing SOCKS%d connection to %s:%d via %s:%d', proxy.type, destinationHost, destinationPort, proxy.host, proxy.port, ) const socksClient = new SocksClient({ command: 'connect', destination: { host: destinationHost, port: destinationPort, }, proxy: { ...proxy }, timeout: options.timeout, }) socksClient.connect() socksClient.on('established', ({ socket }) => stream._start(socket)) socksClient.on('error', (e) => { debug('SOCKS failed: %s', e) stream.destroy(fatal(e)) }) } export default function openSocks( destinationHost: string, destinationPort: number, socksUrl: string, options?: SocksConnectionOptions, ): IStream { debug( 'SOCKS connection to %s:%d via %s', destinationHost, destinationPort, socksUrl, ) const stream = new ProxyStream() connectSocks( destinationHost, destinationPort, socksUrl, stream, options, ).catch((e) => { debug('SOCKS failed: %s', e) stream.destroy(e) }) return stream } ================================================ FILE: src/lib/connect/tcp.ts ================================================ import net from 'net' import _debug from 'debug' import { type StreamBuilder } from '../shared' import openSocks from './socks' const debug = _debug('mqttjs:tcp') /* variables port and host can be removed since you have all required information in opts object */ const buildStream: StreamBuilder = (client, opts) => { opts.port = opts.port || 1883 opts.hostname = opts.hostname || opts.host || 'localhost' if (opts.socksProxy) { return openSocks(opts.hostname, opts.port, opts.socksProxy, { timeout: opts.socksTimeout, }) } const { port, path } = opts const host = opts.hostname debug('port %d and host %s', port, host) return net.createConnection({ port, host, path }) } export default buildStream ================================================ FILE: src/lib/connect/tls.ts ================================================ import { type TLSSocket, connect as tlsConnect } from 'tls' import net from 'net' import _debug from 'debug' import { type StreamBuilder } from '../shared' import { type IClientOptions } from '../client' import openSocks from './socks' const debug = _debug('mqttjs:tls') function connect(opts: IClientOptions): TLSSocket { const { host, port, socksProxy, ...rest } = opts if (socksProxy !== undefined) { const socket = openSocks(host, port, socksProxy, { timeout: opts.socksTimeout, }) return tlsConnect({ ...rest, socket, }) } return tlsConnect(opts) } const buildStream: StreamBuilder = (client, 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 = connect(opts) connection.on('secureConnect', () => { if (opts.rejectUnauthorized && !connection.authorized) { connection.emit('error', new Error('TLS not authorized')) } else { connection.removeListener('error', handleTLSerrors) } }) function handleTLSerrors(err: Error) { // How can I get verify this error is a tls error? if (opts.rejectUnauthorized) { client.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 } export default buildStream ================================================ FILE: src/lib/connect/ws.ts ================================================ import { Buffer } from 'buffer' import Ws, { type ClientOptions } from 'ws' import _debug from 'debug' import { Transform } from 'readable-stream' import { type IStream, type StreamBuilder } from '../shared' import isBrowser from '../is-browser' import { type IClientOptions } from '../client' import type MqttClient from '../client' import { BufferedDuplex, writev } from '../BufferedDuplex' const debug = _debug('mqttjs:ws') const WSS_OPTIONS = [ 'rejectUnauthorized', 'ca', 'cert', 'key', 'pfx', 'passphrase', ] function buildUrl(opts: IClientOptions, client: MqttClient) { 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: IClientOptions) { const options = opts 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 (!isBrowser && !opts.forceNativeWebSocket && 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: IClientOptions) { 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 = Number(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: MqttClient, url: string, opts: IClientOptions, ) { 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}`, ) let socket: Ws if (opts.createWebsocket) { socket = opts.createWebsocket(url, [websocketSubProtocol], opts) } else { socket = new Ws( url, [websocketSubProtocol], opts.wsOptions as ClientOptions, ) } return socket } /* istanbul ignore next */ function createBrowserWebSocket(client: MqttClient, opts: IClientOptions) { const websocketSubProtocol = opts.protocolId === 'MQIsdp' && opts.protocolVersion === 3 ? 'mqttv3.1' : 'mqtt' const url = buildUrl(opts, client) let socket: WebSocket if (opts.createWebsocket) { socket = opts.createWebsocket(url, [websocketSubProtocol], opts) } else { socket = new WebSocket(url, [websocketSubProtocol]) } socket.binaryType = 'arraybuffer' return socket } const streamBuilder: StreamBuilder = (client, opts): IStream => { debug('streamBuilder') const options = setDefaultOpts(opts) options.hostname = options.hostname || options.host || 'localhost' const url = buildUrl(options, client) const socket = createWebSocket(client, url, options) // @ts-expect-error - This is a type confusion because of the overlap between browser oriented code and Node.js oriented code. const webSocketStream = Ws.createWebSocketStream(socket, options.wsOptions) webSocketStream['url'] = url socket.on('close', () => { webSocketStream.destroy() }) return webSocketStream } /* istanbul ignore next */ const browserStreamBuilder: StreamBuilder = (client, opts) => { debug('browserStreamBuilder') let stream: BufferedDuplex | (Transform & { socket?: WebSocket }) 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 // the websocket connection const socket = createBrowserWebSocket(client, opts) // the proxy is a transform stream that forwards data to the socket // it ensures data written to socket is a Buffer const proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) if (!opts.objectMode) { proxy._writev = writev.bind(proxy) } proxy.on('close', () => { socket.close() }) const eventListenerSupport = typeof socket.addEventListener !== 'undefined' // was already open when passed in if (socket.readyState === socket.OPEN) { stream = proxy stream.socket = socket } else { // socket is not open. Use this to buffer writes until it is opened stream = new BufferedDuplex(opts, proxy, socket) if (eventListenerSupport) { socket.addEventListener('open', onOpen) } else { socket.onopen = onOpen } } 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: IClientOptions, socketWrite: typeof socketWriteBrowser, socketEnd: typeof socketEndBrowser, ) { const _proxy = new Transform({ objectMode: pOptions.objectMode, }) _proxy._write = socketWrite _proxy._flush = socketEnd return _proxy } function onOpen() { debug('WebSocket onOpen') if (stream instanceof BufferedDuplex) { stream.socketReady() } } /** * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event */ function onClose(event: CloseEvent) { debug('WebSocket onClose', event) stream.end() stream.destroy() } /** * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event */ function onError(err: Event) { debug('WebSocket onError', err) const error = new Error('WebSocket error') error['event'] = err stream.destroy(error) } /** * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event */ async function onMessage(event: MessageEvent) { if (!proxy || !proxy.readable || !proxy.writable) { return } let { data } = event if (data instanceof ArrayBuffer) data = Buffer.from(data) else if (data instanceof Blob) data = Buffer.from(await new Response(data).arrayBuffer()) else data = Buffer.from(data as string, 'utf8') proxy.push(data) } function socketWriteBrowser( chunk: any, enc: string, next: (err?: Error) => void, ) { if (socket.bufferedAmount > bufferSize) { // throttle data until buffered amount is reduced. setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) return } if (coerceToBuffer && typeof chunk === 'string') { chunk = Buffer.from(chunk, 'utf8') } try { // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send (note this doesn't have a cb as second arg) socket.send(chunk) } catch (err) { return next(err) } next() } function socketEndBrowser(done: (error?: Error, data?: any) => void) { socket.close() done() } // end methods for browserStreamBuilder return stream } export { browserStreamBuilder, streamBuilder } ================================================ FILE: src/lib/connect/wx.ts ================================================ import { Buffer } from 'buffer' import { Transform } from 'readable-stream' import { type StreamBuilder } from '../shared' import { type IClientOptions } from '../client' import type MqttClient from '../client' import { BufferedDuplex } from '../BufferedDuplex' /* global wx */ let socketTask: any let proxy: Transform let stream: BufferedDuplex declare global { const wx: any } 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 buildUrl(opts: IClientOptions, client: MqttClient) { 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(() => { stream.socketReady() }) 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.emit('close') stream.end() stream.destroy() }) socketTask.onError((error) => { const err = new Error(error.errMsg) stream.destroy(err) }) } const buildStream: StreamBuilder = (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) // https://github.com/wechat-miniprogram/api-typings/blob/master/types/wx/lib.wx.api.d.ts#L20984 socketTask = wx.connectSocket({ url, protocols: [websocketSubProtocol], }) proxy = buildProxy() stream = new BufferedDuplex(opts, proxy, socketTask) stream._destroy = (err, cb) => { socketTask.close({ success() { if (cb) cb(err) }, }) } const destroyRef = stream.destroy stream.destroy = (err, cb) => { stream.destroy = destroyRef setTimeout(() => { socketTask.close({ fail() { stream._destroy(err, cb) }, }) }, 0) return stream } bindEventHandler() return stream } export default buildStream ================================================ FILE: src/lib/default-message-id-provider.ts ================================================ export interface IMessageIdProvider { /** * Allocate the first vacant messageId. The messageId become occupied status. * @return {Number} - The first vacant messageId. If all messageIds are occupied, return null. */ allocate(): number | null /** * Get the last allocated messageId. * @return {Number} - messageId. */ getLastAllocated(): number | null /** * Register the messageId. The messageId become occupied status. * If the messageId has already been occupied, then return false. * @param {number} num - The messageId to request use. * @return {boolean} - If `num` was not occupied, then return true, otherwise return false. */ register(num: number): boolean /** * Deallocate the messageId. The messageId become vacant status. * @param {Number} num - The messageId to deallocate. The messageId must be occupied status. * In other words, the messageId must be allocated by allocate() or * occupied by register(). */ deallocate(num: number): void /** * Clear all occupied messageIds. * The all messageIds are set to vacant status. */ clear(): void } /** * DefaultMessageAllocator constructor * @constructor */ export default class DefaultMessageIdProvider implements IMessageIdProvider { private nextId: number 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 } /** * 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: number) { return true } /** * deallocate * Deallocate messageId. * @param { unsigned int } - messageId to deallocate, */ deallocate(messageId: number) {} /** * clear * Deallocate all messageIds. */ clear() {} } ================================================ FILE: src/lib/get-timer.ts ================================================ import { clearInterval as clearI, setInterval as setI } from 'worker-timers' import isBrowser, { isWebWorker, isReactNativeBrowser } from './is-browser' import type { TimerVariant } from './shared' // dont directly assign globals to class props otherwise this throws in web workers: Uncaught TypeError: Illegal invocation // See: https://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome export interface Timer { set: typeof setI clear: typeof clearI } const workerTimer: Timer = { set: setI, clear: clearI, } const nativeTimer: Timer = { set: (func, time) => setInterval(func, time), clear: (timerId) => clearInterval(timerId), } const getTimer = (variant: TimerVariant): Timer => { switch (variant) { case 'native': { return nativeTimer } case 'worker': { return workerTimer } case 'auto': default: { return isBrowser && !isWebWorker && !isReactNativeBrowser ? workerTimer : nativeTimer } } } export default getTimer ================================================ FILE: src/lib/handlers/ack.ts ================================================ // Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND, ETIMEDOUT. import { type PacketHandler, ErrorWithReasonCode } from '../shared' export const ReasonCodes = { 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', } const handleAck: PacketHandler = (client, packet) => { /* 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 = null // 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 ErrorWithReasonCode( `Publish error: ${ReasonCodes[pubackRC]}`, 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 if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { err = new ErrorWithReasonCode( `Publish error: ${ReasonCodes[pubrecRC]}`, pubrecRC, ) client['_removeOutgoingAndStoreMessage'](messageId, () => { cb(err, packet) }) } else { client['_sendPacket'](response) } break } case 'suback': { delete client.outgoing[messageId] client.messageIdProvider.deallocate(messageId) const granted = packet.granted as number[] for (let grantedI = 0; grantedI < granted.length; grantedI++) { const subackRC = granted[grantedI] if ((subackRC & 0x80) !== 0) { err = new Error(`Subscribe error: ${ReasonCodes[subackRC]}`) err.code = subackRC // 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(err, packet) break } case 'unsuback': { delete client.outgoing[messageId] client.messageIdProvider.deallocate(messageId) client['_invokeStoreProcessingQueue']() cb(null, packet) break } default: client.emit('error', new Error('unrecognized packet type')) } if (client.disconnecting && Object.keys(client.outgoing).length === 0) { client.emit('outgoingEmpty') } } export default handleAck ================================================ FILE: src/lib/handlers/auth.ts ================================================ import { type IAuthPacket } from 'mqtt-packet' import { ErrorWithReasonCode, type PacketHandler } from '../shared' import { ReasonCodes } from './ack' const handleAuth: PacketHandler = ( client, packet: IAuthPacket & { returnCode: number }, ) => { const { options } = client const version = options.protocolVersion const rc = version === 5 ? packet.reasonCode : packet.returnCode if (version !== 5) { const err = new ErrorWithReasonCode( `Protocol error: Auth packets are only supported in MQTT 5. Your version:${version}`, rc, ) client.emit('error', err) return } client.handleAuth( packet, (err: ErrorWithReasonCode, packet2: IAuthPacket) => { if (err) { client.emit('error', err) return } if (rc === 24) { client.reconnecting = false client['_sendPacket'](packet2) } else { const error = new ErrorWithReasonCode( `Connection refused: ${ReasonCodes[rc]}`, rc, ) client.emit('error', error) } }, ) } export default handleAuth ================================================ FILE: src/lib/handlers/connack.ts ================================================ import { type IConnackPacket } from 'mqtt-packet' import { ReasonCodes } from './ack' import TopicAliasSend from '../topic-alias-send' import { ErrorWithReasonCode, type PacketHandler } from '../shared' const handleConnack: PacketHandler = (client, packet: IConnackPacket) => { client.log('_handleConnack') const { options } = client const version = options.protocolVersion const rc = version === 5 ? packet.reasonCode : packet.returnCode 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 } 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 ErrorWithReasonCode( `Connection refused: ${ReasonCodes[rc]}`, rc, ) client.emit('error', err) if (client.options.reconnectOnConnackError) { client['_cleanUp'](true) } } } export default handleConnack ================================================ FILE: src/lib/handlers/index.ts ================================================ import handlePublish from './publish' import handleAuth from './auth' import handleConnack from './connack' import handleAck from './ack' import handlePubrel from './pubrel' import { type PacketHandler } from '../shared' const handle: PacketHandler = (client, packet, done) => { 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) switch (packet.cmd) { case 'publish': // DO NOT SHIFT PING HERE, this would lead to https://github.com/mqttjs/MQTT.js/issues/1861 handlePublish(client, packet, done) break case 'puback': case 'pubrec': case 'pubcomp': case 'suback': case 'unsuback': client.reschedulePing() handleAck(client, packet) done() break case 'pubrel': client.reschedulePing() handlePubrel(client, packet, done) break case 'connack': // no need to reschedule ping here as keepalive manager is created after successll connect // (when onConnect is called at the end of handleConnack) handleConnack(client, packet) done() break case 'auth': client.reschedulePing() handleAuth(client, packet) done() break case 'pingresp': client.log('_handlePacket :: received pingresp') client.reschedulePing(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 } } export default handle ================================================ FILE: src/lib/handlers/publish.ts ================================================ import { type IPublishPacket } from 'mqtt-packet' import { type PacketHandler } from '../shared' const validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] /* those late 2 case should be rewrite to comply with coding style: case 1: case 0: // do not wait sending a puback // no callback passed if (1 === qos) { this._sendPacket({ cmd: 'puback', messageId: messageId }); } // emit the message event for both qos 1 and 0 this.emit('message', topic, message, packet); this.handleMessage(packet, done); break; default: // do nothing but every switch mus have a default // log or throw an error about unknown qos break; for now i just suppressed the warnings */ const handlePublish: PacketHandler = (client, packet: IPublishPacket, done) => { 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: number 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 as Buffer, packet, (error, code) => { if (typeof error === 'number') { code = error error = null } if (error) { return client.emit('error', error as 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 as Buffer, packet, (error, code) => { if (typeof error === 'number') { code = error error = null } if (error) { return client.emit('error', error as Error) } if (validReasonCodes.indexOf(code) === -1) { return client.emit( 'error', new Error('Wrong reason code for puback'), ) } if (!code) { client.emit('message', topic, message as Buffer, 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 as Buffer, 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 } } export default handlePublish ================================================ FILE: src/lib/handlers/pubrel.ts ================================================ import { type IPubcompPacket, type IPublishPacket, type IPubrelPacket, } from 'mqtt-packet' import { type PacketHandler } from '../shared' const handlePubrel: PacketHandler = (client, packet: IPubrelPacket, done) => { client.log('handling pubrel packet') const callback = typeof done !== 'undefined' ? done : client.noop const { messageId } = packet const comp: IPubcompPacket = { cmd: 'pubcomp', messageId } client.incomingStore.get(packet, (err, pub: IPublishPacket) => { if (!err) { client.emit('message', pub.topic, pub.payload as Buffer, 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) } }) } export default handlePubrel ================================================ FILE: src/lib/is-browser.ts ================================================ // Global type declaration for Deno declare global { const Deno: any } const isStandardBrowserEnv = () => { // window is only defined when it is a browser if (typeof window !== 'undefined') { // Is the process an electron application // check if we are in electron `renderer` const electronRenderCheck = typeof navigator !== 'undefined' && navigator.userAgent?.toLowerCase().indexOf(' electron/') > -1 if (electronRenderCheck && process?.versions) { const electronMainCheck = Object.prototype.hasOwnProperty.call( process.versions, 'electron', ) // Both electron checks are only true if the following webPreferences are set in the main electron BrowserWindow() // webPreferences: { // sandbox: false, // nodeIntegration: true // contextIsolation: false // } return !electronMainCheck } return typeof window.document !== 'undefined' } // return false if nothing is detected return false } const isWebWorkerEnv = () => Boolean( typeof self === 'object' && self?.constructor?.name?.includes('WorkerGlobalScope') && typeof Deno === 'undefined', ) const isReactNativeEnv = () => typeof navigator !== 'undefined' && navigator.product === 'ReactNative' const isBrowser = isStandardBrowserEnv() || isWebWorkerEnv() || isReactNativeEnv() export const isWebWorker = isWebWorkerEnv() export const isReactNativeBrowser = isReactNativeEnv() export default isBrowser ================================================ FILE: src/lib/shared.ts ================================================ import type { Packet, ISubackPacket } from 'mqtt-packet' import type { Duplex as NativeDuplex } from 'node:stream' import type { Duplex } from 'readable-stream' import type MqttClient from './client' import type { IClientOptions } from './client' export type DoneCallback = (error?: Error) => void export type GenericCallback = (error?: Error, result?: T) => void export type VoidCallback = () => void export type IStream = (Duplex | NativeDuplex) & { /** only set on browsers, it's a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) */ socket?: any } export type StreamBuilder = ( client: MqttClient, opts?: IClientOptions, ) => IStream export type Callback = () => void export type PacketHandler = ( client: MqttClient, packet: Packet, done?: DoneCallback, ) => void export type TimerVariant = 'auto' | 'worker' | 'native' export class ErrorWithReasonCode extends Error { public code: number public constructor(message: string, code: number) { super(message) this.code = code // We need to set the prototype explicitly Object.setPrototypeOf(this, ErrorWithReasonCode.prototype) Object.getPrototypeOf(this).name = 'ErrorWithReasonCode' } } export class ErrorWithSubackPacket extends Error { public packet: ISubackPacket public constructor(message: string, packet: ISubackPacket) { super(message) this.packet = packet // We need to set the prototype explicitly Object.setPrototypeOf(this, ErrorWithSubackPacket.prototype) Object.getPrototypeOf(this).name = 'ErrorWithSubackPacket' } } // eslint-disable-next-line @typescript-eslint/no-empty-object-type export type Constructor = new (...args: any[]) => T export function applyMixin( target: Constructor, mixin: Constructor, includeConstructor = false, ): void { // Figure out the inheritance chain of the mixin const inheritanceChain: Constructor[] = [mixin] while (true) { const current = inheritanceChain[0] const base = Object.getPrototypeOf(current) if (base?.prototype) { inheritanceChain.unshift(base) } else { break } } for (const ctor of inheritanceChain) { for (const prop of Object.getOwnPropertyNames(ctor.prototype)) { // Do not override the constructor if (includeConstructor || prop !== 'constructor') { Object.defineProperty( target.prototype, prop, Object.getOwnPropertyDescriptor(ctor.prototype, prop) ?? Object.create(null), ) } } } } export const nextTick = typeof process?.nextTick === 'function' ? process.nextTick : (callback: () => void) => { setTimeout(callback, 0) } // eslint-disable-next-line @typescript-eslint/no-require-imports export const MQTTJS_VERSION = require('../../package.json').version ================================================ FILE: src/lib/store.ts ================================================ /** * Module dependencies */ import { Readable } from 'readable-stream' import { type Packet } from 'mqtt-packet' import { type DoneCallback } from './shared' const streamsOpts = { objectMode: true } const defaultStoreOptions = { clean: true, } export interface IStoreOptions { /** * true, clear _inflights at close */ clean?: boolean } export type PacketCallback = (error?: Error, packet?: Packet) => void export interface IStore { /** * Adds a packet to the store, a packet is * anything that has a messageId property. * */ put(packet: Packet, cb: DoneCallback): IStore /** * Creates a stream with all the packets in the store * */ createStream(): Readable /** * deletes a packet from the store. */ del(packet: Pick, cb: PacketCallback): IStore /** * get a packet from the store. */ get(packet: Pick, cb: PacketCallback): IStore /** * Close the store */ close(cb: DoneCallback): void } /** * In-memory implementation of the message store * This can actually be saved into files. * * @param {Object} [options] - store options */ export default class Store implements IStore { private options: IStoreOptions private _inflights: Map constructor(options?: IStoreOptions) { 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: Packet, cb: DoneCallback) { 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 = (err) => { if (destroyed) { return } destroyed = true setTimeout(() => { stream.emit('close') }, 0) return stream } return stream } /** * deletes a packet from the store. */ del(packet: Pick, cb: PacketCallback) { const toDelete = this._inflights.get(packet.messageId) if (toDelete) { this._inflights.delete(packet.messageId) cb(null, toDelete) } else if (cb) { cb(new Error('missing packet')) } return this } /** * get a packet from the store. */ get(packet: Pick, cb: PacketCallback) { const storedPacket = this._inflights.get(packet.messageId) if (storedPacket) { cb(null, storedPacket) } else if (cb) { cb(new Error('missing packet')) } return this } /** * Close the store */ close(cb: DoneCallback) { if (this.options.clean) { this._inflights = null } if (cb) { cb() } } } ================================================ FILE: src/lib/topic-alias-recv.ts ================================================ /** * Topic Alias receiving manager * This holds alias to topic map * @param {Number} [max] - topic alias maximum entries */ export default class TopicAliasRecv { private aliasToTopic: Record public max: number public length: number constructor(max: number) { 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: string, alias: number): boolean { 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: number): string { return this.aliasToTopic[alias] } /** * Clear all entries */ clear() { this.aliasToTopic = {} } } ================================================ FILE: src/lib/topic-alias-send.ts ================================================ /** * Module dependencies */ import { LRUCache } from 'lru-cache' import { NumberAllocator } from 'number-allocator' /** * Topic Alias sending manager * This holds both topic to alias and alias to topic map * @param {Number} [max] - topic alias maximum entries */ export default class TopicAliasSend { private aliasToTopic: LRUCache private topicToAlias: Record private max: number private numberAllocator: NumberAllocator public length: number constructor(max: number) { 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: string, alias: number): boolean { 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: number): string { 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: string): number | undefined { 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 } /** * Get Least Recently Used (LRU) topic alias * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias */ getLruAlias(): number { const alias = this.numberAllocator.firstVacant() if (alias) return alias // get last alias (key) from LRU cache return [...this.aliasToTopic.keys()][this.aliasToTopic.size - 1] } } ================================================ FILE: src/lib/unique-message-id-provider.ts ================================================ import { NumberAllocator } from 'number-allocator' import { type IMessageIdProvider } from './default-message-id-provider' /** * UniqueMessageAllocator constructor * @constructor */ export default class UniqueMessageIdProvider implements IMessageIdProvider { private numberAllocator: NumberAllocator private lastId: number 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: number) { return this.numberAllocator.use(messageId) as boolean } /** * deallocate * Deallocate messageId. * @param { unsigned int } - messageId to deallocate, */ deallocate(messageId: number) { this.numberAllocator.free(messageId) } /** * clear * Deallocate all messageIds. */ clear() { this.numberAllocator.clear() } } ================================================ FILE: src/lib/validations.ts ================================================ /** * Validate a topic to see if it's valid or not. * A topic is valid if it follow below rules: * - Rule #1: If any part of the topic is not `+` or `#`, then it must not contain `+` and '#' * - Rule #2: Part `#` must be located at the end of the mailbox * * @param {String} topic - A topic * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. */ export function validateTopic(topic: string): boolean { const parts = topic.split('/') 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].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { return false } } return true } /** * Validate an array of topics to see if any of them is valid or not * @param {Array} topics - Array of topics * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one */ export function validateTopics(topics: string[]): string | null { 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 } ================================================ FILE: src/mqtt.ts ================================================ /* * Copyright (c) 2015-2015 MQTT.js contributors. * Copyright (c) 2011-2014 Adam Rudd. * * See LICENSE for more information */ import MqttClient from './lib/client' import DefaultMessageIdProvider from './lib/default-message-id-provider' import UniqueMessageIdProvider from './lib/unique-message-id-provider' import Store, { IStore } from './lib/store' import connect, { connectAsync } from './lib/connect' import KeepaliveManager from './lib/KeepaliveManager' export const Client = MqttClient export { connect, connectAsync, MqttClient, Store, DefaultMessageIdProvider, UniqueMessageIdProvider, IStore, KeepaliveManager, } export * from './lib/client' export * from './lib/shared' export * from './lib/validations' export { ReasonCodes } from './lib/handlers/ack' export type { Timer } from './lib/get-timer' ================================================ FILE: test/browser/certs/server-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIFCTCCAvGgAwIBAgIUfsHWL7ricrpv7uEMmHoDNUFt0W0wDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMTAyNDA5NTYwMFoXDTI0MTAy MzA5NTYwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF AAOCAg8AMIICCgKCAgEAsdar/fZYadOItHPwI+7suwVc3p8TN7Gd0opRjAHj03es uVwOQ42DQgOpn1uq8nqHgZ5/F0rEvb39CXDMtgChPZDjZVqUX7ciW235klPYROeJ 0ytFJIZQG1ejgHGmwy7Setc+9QJhCm5H9qdfRYDv6G7S/Q771cXmB0g3vp0VDkk6 +4Av2NbNsCe4P1LTi1rvLh23wfHV/FovtLBl25PgC5RmLbBSIO14bJqreRbfyHSG yc/RvU+yJghoCaJPV5lkuhHstxekAGUCwuI7R73Hia9+VJsCbDDEvoH1Peu4MPzG Bk1nksJFcfYJmSgusSqVq0PIdF8ZZcIStaC45YDmu5D9whhHf0wC2yhNbAPCZvkh GwpaK9v6km6v+0LOzkh0rjk+HRjIpXi/E8WGRXwfEljslx3fb/mKDxNYu0l7vNRm t1ToMJ273ccABhj5qxjmef1JeUK+XWCjBhPiRmzWEQqgjmiEQVONRFcLcY5EurZP nEs36RINmr3ErrpWdjugUiRBixnCFOlKPRS6hg23vwPJb2U1F1LQx2tHJnJ+E4rY qin0tImbQyljXMoW5MEhF3jNpPguGVsfqQd/z/7QHjjP2/ZFXKSfiB2pwfU7pcNM 2Gi5dC9/gTAgLPKiMyikh558c5IYh9foNA5oVPCBFdnQ/DoQYXL7P99OC6yj+VMC AwEAAaNTMFEwHQYDVR0OBBYEFODr4e1O3Ujjn5xKJP/WWCoEN24mMB8GA1UdIwQY MBaAFODr4e1O3Ujjn5xKJP/WWCoEN24mMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI hvcNAQELBQADggIBAI14f2RXKbBZlR1yry9FJCX/IvmwgwYyVYL/rjFFIWNqfs8C VoYEmmF62ujgv6vYTXhQpdn7INRqsCGVI2DTRz4Y63diek7ShamznQt0NiQGHl// P4PO0XB5YpLRJy22X9RuUj0PmZyG4HHqnqBg40YWbYB0ttkbR5D5QmxEJA1iDrX4 Z7qq64izwRF4l0x1ubOpU5YkPWSE5VgOY88sy2dnxKTEcwGF0JcBBAvAiByvIbDX uxn3xXA6AMne+B7UhMHiIowfHCJs8dDK6/SVMSpz+A7roZP5RZK0OCLqYCklnSOo QSflXpd6+FsytwETEPpWouNWDIple9/rUgLLBofFoo4vMNCW4syNVEmdNvAYo5M3 nLSzx32IkSoF4VhKIJnbT3ROTVB37ZvQNbJXzP/jbgzgEJa/EKZwms5ZwE9aX2GM WhJcTBr1Wkde8M+1kw7RxcIBTiFVEhxug6EuzMN33m4CwngJQj+pqnctrCWi5P8D KcGoZoHSZ+6AEBsCRJHWNl5NApcKVfiRtmDH1zNjdj+bDpT4G6c5CKt06nN79heO rjwNoHQ08/DRyvXAarefu4vOHB3VbMNmU34Px36gd47b0PxATGLqwSmlTBhQqp/e xb5l9K4cENFew08PwZGMzjwFQ4MK3AQC0g0HSgKVQATkJWZwUR6H29wfr1I3 -----END CERTIFICATE----- ================================================ FILE: test/browser/certs/server-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCx1qv99lhp04i0 c/Aj7uy7BVzenxM3sZ3SilGMAePTd6y5XA5DjYNCA6mfW6ryeoeBnn8XSsS9vf0J cMy2AKE9kONlWpRftyJbbfmSU9hE54nTK0UkhlAbV6OAcabDLtJ61z71AmEKbkf2 p19FgO/obtL9DvvVxeYHSDe+nRUOSTr7gC/Y1s2wJ7g/UtOLWu8uHbfB8dX8Wi+0 sGXbk+ALlGYtsFIg7Xhsmqt5Ft/IdIbJz9G9T7ImCGgJok9XmWS6Eey3F6QAZQLC 4jtHvceJr35UmwJsMMS+gfU967gw/MYGTWeSwkVx9gmZKC6xKpWrQ8h0XxllwhK1 oLjlgOa7kP3CGEd/TALbKE1sA8Jm+SEbClor2/qSbq/7Qs7OSHSuOT4dGMileL8T xYZFfB8SWOyXHd9v+YoPE1i7SXu81Ga3VOgwnbvdxwAGGPmrGOZ5/Ul5Qr5dYKMG E+JGbNYRCqCOaIRBU41EVwtxjkS6tk+cSzfpEg2avcSuulZ2O6BSJEGLGcIU6Uo9 FLqGDbe/A8lvZTUXUtDHa0cmcn4TitiqKfS0iZtDKWNcyhbkwSEXeM2k+C4ZWx+p B3/P/tAeOM/b9kVcpJ+IHanB9Tulw0zYaLl0L3+BMCAs8qIzKKSHnnxzkhiH1+g0 DmhU8IEV2dD8OhBhcvs/304LrKP5UwIDAQABAoICABQ64+N8Glr7fcHCreQgjJx1 GTdCkvM9yMLHtPa/dkplgugDwU73nPatp6Y2+CTPlx+XHJpVM+PpELektcd/Hdh8 5WlFBbuM/821HnQCBrLxQIadlabN1/SsTQSnqRKV/nU2jB+0pBOwL46EQ/xflQBK BHAAvC7Owtfm5AnSdlqNg7X9bd6gXNNM9BLRWhAwXs9OVnYrtlhtF/Uu2yoV0cxE wYoxVGkQ1SdMQZ+rivcTeQvMZhhqJaKu8nV6GcV7b8USP0J6tXoiriQ7i7EhtROu u35mCGoEnYpAYASpuREViB9Kad2ngjKFtwTNreC92TsbdwT0ln/dX7clRTM9AmzO PIrNk5/rslc/S9wsNQgsNFOuFFzo8WElMZYEdQ5AuqJHGG+tILs1DtHLgwWKXgub gdf7lRpe9SQ5C3oIk0zSDpeMWBesMwAjQYwPKvJzvN0jJw+cFM4n0QcwFU2q6bjm GkvGR6uFD4a0xNOmqZu7A6gBESf1+9daIW3shVMIlVcIzo4/XS8vNwku70PDcfeA PiDIR7ovZR/kipDbDYKiY/4zFLB+EJ6p5lsKAu91zeCkLRnAGyJmZFgL3Xb4espt 5zV31TU2itw349N99SjyKJICbSckgp50grCZp1316g69wXP7XGIoidj8Wi44MYyT TSKgojvRKeYOyd3+7EExAoIBAQDfq66CxWqL3ayLVM7aEweycfpDVnHwgSMCh+tf 2dMugoNF2WWk0mtulW9bJRRbNXUcpq7MrhXP5TQph1Fykfe7jQ0w/A9cHC34G9ud ZbrMRSFCPqhNoE7k6sseB8bIYKZjlPW7hds+WfmXnl/rHZ6COpcxtvVlPKXRLG77 QhVZ/gCQXhbwPD8FjajTw3KVnCYYmupeEmZ1XP6XG/KWjeM+wW5zN/O71Dmzhlfh r+zNwxP6opxoBDJL2qAVluLoym9ZekGFF5WALjNInUmuzQt2bEjiIBP66wkiR6jV bKawIzBTZULflZLdjPZeYcccjiLFuF7DMM1m0FZZRPyYn5OxAoIBAQDLixmhFqQx 3ChBfUsqaRuqrxA2VUZqyr2EdF3bSRYHu7TenfAzdzVKga4WcIQ+ROSODs/Bkwki CCUMulFq0LNZUvBwebgFer/uyozK4H+JVfXi63NLSBcgb4SnJN4OAIvBVhS+hoeS e7EFWBLW67a6ie5HmQig1UiHOSV5LjpSgmcRkI76SWLu4iYnXhRPGX+yDJ7wgwMI erd7apVvFXCK3PI0KykiB3Q8USGrxFA0PoEOSlRzOF7QirXH+HnHZpWDLSDTJYMd Xc6Aoab3Q9B+K3EP0+jIFXl8c35rVKNtiI7mT8K5DsQSLnT1zTknO9pDNoJhDlIQ TzkwPLwFM/JDAoIBAQC9sEADr6E4lPrr/zSeRV1VvMvdp8ZhjWM/lj3LwFQVMaJa 7pABRGLQGfOifMPSxndXoRTFiS3+bgfa4GP2okxSxsDVNi9gkpV5wUZzKf9NlaMr JzBdBQNjqKwx/65Z6X2zHnV4X4EcU/oU17/JCgmPsFjhVKSxAsotp11HwTeSZpPb 26/gdXeNIiJ31V2r/A1i+J6iwUAbcI6JYK5PeG2snycol98hAE8fLWQChsxxhGvB N38lx8lQA02YgapWergMTl6anPGxD312q839bO+3swXsn//R4NqSM+u/dLG41174 mvwn2hX/xrh6Oro+QVzECiRzrUPFWhKhCpyeY1dBAoIBAQCuS9cOkozYBWaTbe9H yenfJzoCzNMX77SKD/uvI86xoJMpbqWZ7KK9wn6IyKD1kZkF7LkLUhWoBsFzPLkr drud6d/q8NAcIRjqKpiVwdh0ih+cyXJZLMphBB3b4z4jT2RdkpEA9cJyNWlI4jp/ wCkhY6ufs/QnhXwuD0l/k45tySzpZZiJyv3lAdMaMv3BHlyy4wvXeNjIEg28qXB9 RyawQrNE6RC4CxoF7JOguuLTWhK5Yf0fDU5j1eQD8UqW7clqIuLQ7HZyVznV/Jc1 deS7pgeTPis/6uO1JlFDJ58q1J1Jq1NblMFhsFaHhc1DQ4WLuMm8wr6TKMQYAmof KnRxAoIBAE6+Aa3YYr5FHYIi11zFFyN15BABWSnXB1vhjD+t7usWwypakmM0DEWQ wrnAnSPosWjz3l+Oq/5j9H5kkkd5VXxVkj7w33NIHTbEjQt42cBOI6qDuRyO0rKF kh8ZgusQ1/qPJVa7r2d7u65F+UnB3LGUiXjWk9Pi62yq9JzvHUUk5jqidrQC30h2 OPOWF4gcaQ8xuv4ENu1rt4WvAnPNadBI1JQah+RYi5k0P1PyUzBwbKsfTLa+ogjM t8lM7ZkMqC5GYD5LkxR9tmTkQzqTDli6g+Vk7Z/g2fpN6u347Qm1sP5Iz3kcadU5 /qk0BiP0ZuvLwsPUJeqhrsC20tQWLMo= -----END PRIVATE KEY----- ================================================ FILE: test/browser/test.js ================================================ import { expect } from '@esm-bundle/chai'; import mqtt from '../../'; // this will resolve to mqtt/dist/mqtt.esm.js // needed to test no-esm version /dist/mqtt.js /** @type { import('../../src') }*/ const mqtt2 = window.mqtt // get browser name const userAgent = navigator.userAgent.toLowerCase().replace(/ /g, '_').replace(/\//g, '_') let browser = 'unknown' if (userAgent.includes('chrome')) { browser = 'chrome' } else if (userAgent.includes('firefox')) { browser = 'firefox' } else if (userAgent.includes('safari')) { browser = 'safari' } const browserTopic = `test/${browser}` console.log('User Agent:', userAgent) console.log('Browser:', browser) function testProto(proto, port, cb = () => { }) { const testTopic = `${browserTopic}/${proto}` describe('MQTT.js browser test with ' + proto.toUpperCase(), () => { after(() => { if (client) { client.end(() => { cb() client = null; }); } else { cb() } }) /** @type { import('../../src').MqttClient }*/ let client = null; it('should connect-publish-subscribe', (done) => { expect(typeof mqtt.Client.VERSION).to.equal('string') client = mqtt.connect(`${proto}://localhost:${port}`, { // log: console.log.bind(console), clientId: `testClient-${browser}-${proto}`, }) client.on('offline', () => { console.log('client offline') done(new Error('client offline')) }) client.on('connect', () => { console.log('client connect') }) client.on('reconnect', () => { console.log('client reconnect') }) const payload = 'Hello World!' client.on('connect', () => { client.on('message', (topic, msg) => { expect(topic).to.equal(testTopic); expect(msg.toString()).to.equal(payload); client.end(() => { client = null; done(); }); }); client.subscribe(testTopic, (err) => { expect(err).to.not.exist; if (!err) { client.publish(testTopic, payload, (err2) => { expect(err2).to.not.exist; }); } }); }); client.on('error', (err) => { done(err); }); }) }) } describe('MQTT.js browser tests', () => { it('should work with ESM version', (done) => { expect(mqtt2).to.exist expect(mqtt2.connect).to.be.a('function') expect(mqtt2.Client).to.be.a('function') done() }) it('should work in a Web Worker', (done) => { const worker = new Worker('test/browser/worker.js') let ready = false worker.onmessage = (e) => { if (e.data === 'worker ready') { ready = true } else if(e.data === 'keepalive'){ worker.onerror = null // worker.terminate() expect(ready).to.be.true done() }else { done(Error(e.data)) } } worker.onerror = (e) => { done(Error(e.message)) } }) testProto('ws', window.wsPort, () => { testProto('wss', window.wssPort) }) }) ================================================ FILE: test/browser/worker.js ================================================ importScripts('/dist/mqtt.js'); /** @type { import('../../src') }*/ const MQTT = mqtt; console.log('worker start'); console.log('worker MQTT', MQTT); const client = MQTT.connect(`ws://localhost:4000`, { clientId: `testClient-worker_` + Math.random().toString(16).substr(2, 8), keepalive: 2, }); client.on('offline', () => { console.log('worker client offline'); }) client.on('reconnect', () => { console.log('worker client reconnect'); }) client.on('error', (err) => { console.log('worker client error', err); }) client.on('packetsend', (packet) => { if (packet.cmd === 'pingreq') { postMessage('keepalive'); client.end(() => { console.log('worker client end'); }); } }) client.on('connect', () => { console.log('worker client connect'); postMessage('worker ready'); }) ================================================ FILE: test/node/abstract_client.ts ================================================ /** * Testing dependencies */ import { assert } from 'chai' import sinon from 'sinon' import fs from 'fs' import levelStore from 'mqtt-level-store' import { type IPublishPacket, type IPubrelPacket, type ISubackPacket, type QoS, } from 'mqtt-packet' import { type DoneCallback, ErrorWithReasonCode } from 'src/lib/shared' import { fail } from 'assert' import { describe, it, beforeEach, afterEach, after } from 'node:test' import Store from '../../src/lib/store' import serverBuilderFn from './server_helpers_for_client_tests' import handlePubrel from '../../src/lib/handlers/pubrel' import TeardownHelper from './helpers/TeardownHelper' import handle from '../../src/lib/handlers/index' import handlePublish from '../../src/lib/handlers/publish' import mqtt, { type IClientOptions, type IClientPublishOptions, type IClientSubscribeOptions, type ISubscriptionMap, type ISubscriptionRequest, } from '../../src' /** * 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()`. * */ const fakeTimersOptions = { shouldClearNativeTimers: true, } export default function abstractTest(server, config, ports) { const version = config.protocolVersion || 4 const teardownHelper = new TeardownHelper() function connect(opts?: IClientOptions | string) { if (typeof opts === 'string') { opts = { host: opts } } opts = { ...config, ...opts } as IClientOptions const instance = mqtt.connect(opts) teardownHelper.addClient(instance) return instance } function serverBuilder(...args: Parameters) { const instance = serverBuilderFn(...args) teardownHelper.addServer(instance) return instance } async function beforeEachExec() { await teardownHelper.runAll() teardownHelper.reset({ removeOnce: true }) } async function afterExec() { await teardownHelper.runAll() teardownHelper.reset() } after(afterExec) describe('closing', () => { beforeEach(beforeEachExec) after(afterExec) it('should emit close if stream closes', function _test(t, 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(t, 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 destroy keepalive manager if stream closes', function _test(t, done) { const client = connect() client.once('close', () => { assert.notExists(client.keepaliveManager) client.end(true, (err) => done(err)) }) client.once('connect', () => { assert.exists(client.keepaliveManager) client.stream.end() }) }) it('should emit close after end called', function _test(t, 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(t, 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(t, 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(t, 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(t, 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(t, 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 destroy keepalive manager after end called', function _test(t, done) { const client = connect() client.once('connect', () => { assert.exists(client.keepaliveManager) client.end((err) => { assert.notExists(client.keepaliveManager) done(err) }) }) }) it('should be able to end even on a failed connection', function _test(t, 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(t, done) { const client = connect({ host: 'this_hostname_should_not_exist' }) let timeoutEmitted = false const timeout = setTimeout(() => { timeoutEmitted = true done(new Error('Disconnected client has failed to emit end')) }, 500) client.once('end', () => { // Prevent hanging test if `end` is not emitted before timeout if (!timeoutEmitted) { 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(t, 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', () => { beforeEach(beforeEachExec) after(afterExec) it('should connect to the broker', function _test(t, 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(t, 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(t, done) { const client = connect() client.on('error', done) server.once('client', (serverClient) => { serverClient.once('connect', (packet) => { assert.strictEqual(packet.clean, true) client.end((err) => done(err)) }) }) }) it('should connect with the given client id', function _test(t, 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(t, 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(t, done) { let errorCaught = false try { const client = connect({ clean: false }) client.on('error', (err) => { done(err) }) } catch (err) { errorCaught = true assert.strictEqual( err.message, 'Missing clientId for unclean clients', ) done() } finally { if (!errorCaught) { done(new Error('Client should have thrown an error')) } } }) it('should default to localhost', function _test(t, 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 emit connect', function _test(t, done) { const client = connect() client.once('connect', (packet: mqtt.IConnackPacket) => { assert.equal(packet.cmd, 'connack') client.end(true, (err) => done(err)) }) client.once('error', done) }) it('should provide connack packet with connect event', function _test(t, done) { const connack = version === 5 ? { reasonCode: 0, sessionPresent: undefined } : { returnCode: 0, sessionPresent: undefined } 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(t, 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(t, done) { const client = connect({ clientId: 'invalid' }) client.once('connect', () => { done(new Error('Should not emit connect')) }) client.once('error', (error: ErrorWithReasonCode) => { 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(t, done) { // fake a port const client = connect({ port: 4557 }) client.on('error', (e: any) => { assert.equal(e.code, 'ECONNREFUSED') client.end((err) => done(err)) }) }) it('should have different client ids', function _test(t, 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', () => { beforeEach(beforeEachExec) after(afterExec) it('should emit offline event once when the client transitions from connected states to disconnected ones', function _test(t, 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(t, 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', () => { beforeEach(beforeEachExec) after(afterExec) it('should be ok for well-formated topics', function _test(t, 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(err) } done() }) }, ) }) it('should return an error (via callbacks) for topic #/event', function _test(t, 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(t, 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) client.end((err3) => done(err3)) }) }) }) it('should return an error (via callbacks) for topic #/event', function _test(t, 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(t, 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(t, 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(t, 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(t, 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', () => { beforeEach(beforeEachExec) after(afterExec) it('should queue message until connected', function _test(t, 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) client.end((err) => done(err)) }) }) it('should not queue qos 0 messages if queueQoSZero is false', function _test(t, done) { const client = connect({ queueQoSZero: false }) client.publish('test', 'test', { qos: 0 }) assert.strictEqual(client.queue.length, 0) client.on('connect', () => { client.end((err) => done(err)) }) }) it('should queue qos != 0 messages', function _test(t, 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', () => { client.end((err) => done(err)) }) }) it('should not interrupt messages', function _test(t, done) { let client: mqtt.MqttClient | null = 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(false, done) } }) }) 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(t, done) { teardownHelper.add({ executeOnce: true }, async () => { await new Promise((resolve) => { fs.rm(storePath, { recursive: true }, () => { resolve() }) }) }) const storePath = fs.mkdtempSync('test-store_') const store = levelStore(storePath) let client: mqtt.MqttClient | null = 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', ) client.end(false, done) 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 }) }) }) // 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(t, done) { const client = connect({ queueQoSZero: false }) let called = false client.publish('test', 'test', { qos: 0 }, () => { called = true }) client.on('connect', () => { assert.isTrue(called) client.end((err) => done(err)) }) }) it('should delay ending up until all inflight messages are delivered', function _test(t, 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(t, 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(t, 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(t, done) { const store = new Store() store.put = (packet, cb) => { process.nextTick(cb, new Error('oops there is an error')) return store } const client = connect({ incomingStore: store, outgoingStore: store, }) client.publish('test', 'test', { qos: 2 }, (err) => { if (err) { client.end(true, done) } }) }) }) describe('publishing', () => { beforeEach(beforeEachExec) after(afterExec) it('should publish a message (offline)', function _test(t, 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(t, 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(t, 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(t, 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(t, done) { const client = connect() const payload = 'test' const topic = 'test' const opts: IClientPublishOptions = { 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(t, 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(t, done) { const client = connect() const payload = 'duplicated-test' const topic = 'test' const opts: IClientPublishOptions = { 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(t, done) { const client = connect() client.once('connect', () => { // callback args can be typed client.publish('a', 'b', (_, packet?: mqtt.Packet) => { assert.isUndefined(packet) client.end((err) => done(err)) }) }) }) it('should fire a callback (qos 1)', function _test(t, done) { const client = connect() const opts: IClientPublishOptions = { qos: 1 } client.once('connect', () => { client.publish('a', 'b', opts, (_, packet?: mqtt.Packet) => { assert.exists(packet) client.end((err) => done(err)) }) }) }) it('should fire a callback (qos 1) on error', function _test(t, done) { // 145 = Packet Identifier in use const pubackReasonCode = 145 const pubOpts: IClientPublishOptions = { qos: 1 } let client: mqtt.MqttClient | null = 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, packet?: mqtt.Packet) => { assert.exists(packet) if (version === 5) { if (err instanceof ErrorWithReasonCode) { assert.strictEqual( err.code, pubackReasonCode, ) } else { assert.instanceOf(err, ErrorWithReasonCode) } } else { assert.ifError(err) } done() }, ) }) }) }) it('should fire a callback (qos 2)', function _test(t, done) { const client = connect() const opts: IClientPublishOptions = { qos: 2 } client.once('connect', () => { client.publish('a', 'b', opts, (_, packet?: mqtt.Packet) => { assert.exists(packet) client.end((err) => done(err)) }) }) }) it('should fire a callback (qos 2) on error', function _test(t, done) { // 145 = Packet Identifier in use const pubrecReasonCode = 145 const pubOpts: IClientPublishOptions = { qos: 2 } let client: mqtt.MqttClient | null = 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, packet?: mqtt.Packet) => { assert.exists(packet) if (version === 5) { if (err instanceof ErrorWithReasonCode) { assert.strictEqual( err.code, pubrecReasonCode, ) } else { assert.instanceOf(err, ErrorWithReasonCode) } } else { assert.ifError(err) } done() }, ) }) }) }) it('should support UTF-8 characters in topic', function _test(t, 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(t, 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(t, 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) { teardownHelper.add({ executeOnce: true, order: 1 }, () => { if (clock) { clock.restore() } }) const clock = sinon.useFakeTimers({ ...fakeTimersOptions, toFake: ['setTimeout'], }) 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) }, 10) clock.tick(10) } callback() }, 10) clock.tick(10) } 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(t, done) { testQosHandleMessage(qos, done) }) }) it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function _test(t, done) { const client = connect() client.handleMessage = (packet, callback) => { callback(new Error('Error thrown by the application')) } const sendSpy = sinon.spy() client['_sendPacket'] = sendSpy handlePublish( client, { cmd: 'publish', messageId: Math.floor(65535 * Math.random()), topic: 'test', payload: 'test', qos: 1, dup: false, retain: false, }, (err) => { assert.exists(err) }, ) assert.strictEqual(sendSpy.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(t, done) { const client = connect() client.handleMessage = (packet, callback) => { callback(new Error('Error thrown by the application')) } try { handlePublish(client, { cmd: 'publish', messageId: Math.floor(65535 * Math.random()), topic: 'test', payload: 'test', qos: 1, dup: false, retain: false, }) 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(t, done) { class AsyncStore extends Store { put(packet, cb) { process.nextTick(() => { cb(null, 'Error') }) return this } close(cb) { cb() } } const store = new AsyncStore() const client = connect({ incomingStore: store }) handlePublish( client, { cmd: 'publish', messageId: 1, topic: 'test', payload: 'test', qos: 1, dup: false, retain: false, }, () => { client.end((err) => done(err)) }, ) }) it('should handle error with async incoming store in QoS 2 `handlePublish` method', function _test(t, done) { class AsyncStore extends Store { put(packet, cb) { process.nextTick(() => { cb(null, 'Error') }) return this } del(packet, cb) { process.nextTick(() => { cb(new Error('Error')) }) return this } get(packet, cb) { process.nextTick(() => { cb(null, { cmd: 'publish' }) }) return this } close(cb) { cb() } } const store = new AsyncStore() const client = connect({ incomingStore: store }) handlePublish( client, { cmd: 'publish', dup: false, retain: false, 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(t, done) { class AsyncStore extends Store { put(packet, cb) { process.nextTick(() => { cb(null, 'Error') }) return this } del(packet, cb) { process.nextTick(() => { cb(new Error('Error')) }) return this } get(packet, cb) { process.nextTick(() => { cb(null, { cmd: 'publish' }) }) return this } close(cb) { cb() } } const store = new AsyncStore() const client = connect({ incomingStore: store }) handlePubrel( client, { cmd: 'pubrel', 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(t, done) { let delComplete = false class AsyncStore extends Store { put(packet, cb) { process.nextTick(() => { cb(null, 'Error') }) return this } del(packet, cb) { process.nextTick(() => { delComplete = true cb(null) }) return this } get(packet, cb) { process.nextTick(() => { cb(null, { cmd: 'publish' }) }) return this } close(cb) { cb() } } const store = new AsyncStore() const client = connect({ incomingStore: store }) handlePubrel( client, { cmd: 'pubrel', 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(t, 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', dup: false, retain: false, }, () => { const spy = sinon.spy() // cleans up the client client['_sendPacket'] = spy handlePubrel( client, { cmd: 'pubrel', messageId }, (err) => { assert.exists(err) assert.strictEqual(spy.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(t, 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', dup: false, retain: false, }, () => { try { handlePubrel(client, { cmd: 'pubrel', messageId, }) client.end(true, done) } catch (err) { client.end(true, () => { done(err) }) } }, ) }) }, ) it('should keep message order', function _test(t, done) { let publishCount = 0 let reconnect = false let client: mqtt.MqttClient const incomingStore = new Store({ clean: false }) const outgoingStore = new 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', ) done() 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, TODO: should we handle this? incomingStore, outgoingStore, }) reconnect = true } }) }) }) function testCallbackStorePutByQoS( qos: number, clean: boolean, expected: string[], done: DoneCallback, ) { const client = connect({ clean, clientId: 'testId', }) const callbacks = [] function cbStorePut() { callbacks.push('storeput') } client.on('connect', () => { client.publish( 'test', 'test', { qos: qos as 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(t, done) { testCallbackStorePutByQoS( test.args[0] as number, test.args[1] as boolean, 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(t, done) { testCallbackStorePutByQoS( test.args[0] as number, test.args[1] as boolean, test.expected, done, ) }) } }) }) describe('unsubscribing', () => { beforeEach(beforeEachExec) after(afterExec) it('should send an unsubscribe packet (offline)', function _test(t, 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(t, 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(t, 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(t, 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(t, 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(t, done) { const client = connect() const topic = 'topic' client.once('connect', () => { // callback args can be typed client.unsubscribe(topic, (_, packet?: mqtt.Packet) => { assert.isDefined(packet) client.end(true, done) }) }) server.once('client', (serverClient) => { serverClient.once('unsubscribe', (packet) => { serverClient.unsuback(packet) }) }) }) it('should unsubscribe from a chinese topic', function _test(t, 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: sinon.SinonFakeTimers beforeEach(async () => { await beforeEachExec() clock = sinon.useFakeTimers(fakeTimersOptions) }) afterEach(() => { clock.restore() }) after(afterExec) it('should send ping at keepalive interval', function _test(t, done) { const interval = 3000 const client = connect({ keepalive: interval / 1000 }) const spy = sinon.spy(client, 'sendPing') client.on('error', (err) => { client.end(true, () => { done(err) }) }) let pingReceived = 0 client.on('packetreceive', (packet) => { if (packet.cmd === 'pingresp') { process.nextTick(() => { pingReceived++ assert.strictEqual(spy.callCount, pingReceived) if (pingReceived === 3) { client.end(true, done) } else { clock.tick(interval) } }) clock.tick(1) } }) client.once('connect', () => { clock.tick(interval) }) }) it('should not shift ping on publish', function _test(t, done) { const intervalMs = 3000 const client = connect({ keepalive: intervalMs / 1000 }) const spy = sinon.spy(client, '_reschedulePing' as any) let serverClient function fakePub() { client.publish('foo', 'bar') serverClient.publish({ topic: 'foo', payload: 'bar', }) clock.tick(1) } server.once('client', (_serverClient) => { // send fake packet to client serverClient = _serverClient serverClient.on('publish', () => { // needed to trigger the setImmediate inside server publish listener and send suback clock.tick(1) }) }) let received = 0 client.on('packetreceive', (packet) => { if (packet.cmd === 'publish') { clock.tick(intervalMs) received++ assert.strictEqual(spy.callCount, 0) if (received === 2) { client.end(true, done) } } }) client.once('connect', () => { fakePub() fakePub() }) }) const reschedulePing = (reschedulePings: boolean) => { it( `should ${ !reschedulePings ? 'not ' : '' }reschedule pings if publishing at a higher rate than keepalive and reschedulePings===${reschedulePings}`, { timeout: 4000, }, function _test(t, done) { clock.restore() teardownHelper.add( { executeOnce: true, order: 1, }, () => { if (localClock) { localClock.restore() } }, ) const localClock = sinon.useFakeTimers({ ...fakeTimersOptions, toFake: ['setTimeout'], }) const intervalMs = 3000 const client = connect({ keepalive: intervalMs / 1000, reschedulePings, }) const spyReschedule = sinon.spy( client, '_reschedulePing' as any, ) let received = 0 client.on('packetreceive', (packet) => { if (packet.cmd === 'puback') { process.nextTick(() => { localClock.tick(intervalMs) ++received if (received === 2) { if (reschedulePings) { assert.strictEqual( spyReschedule.callCount, received, ) } else { assert.strictEqual( spyReschedule.callCount, 0, ) } client.end((err) => done(err)) } }) } }) client.once('connect', () => { // reset call count (it's called also on connack) spyReschedule.resetHistory() // use qos1 so the puback is received (to reschedule ping) client.publish('foo', 'bar', { qos: 1 }) client.publish('foo', 'bar', { qos: 1 }) }) }, ) } reschedulePing(true) reschedulePing(false) const pingresp = (reschedulePings: boolean) => { it(`should shift ping on pingresp when reschedulePings===${reschedulePings}`, function _test(t, done) { const intervalMs = 3000 const client = connect({ keepalive: intervalMs / 1000, reschedulePings, }) const spy = sinon.spy(client, '_reschedulePing' as any) client.on('packetreceive', (packet) => { if (packet.cmd === 'pingresp') { process.nextTick(() => { assert.strictEqual(spy.callCount, 1) client.end(true, done) }) } }) client.on('error', (err) => { client.end(true, () => { done(err) }) }) client.once('connect', () => { clock.tick(intervalMs) }) }) } pingresp(true) pingresp(false) }) describe('pinging', () => { beforeEach(beforeEachExec) after(afterExec) it('should setup keepalive manager', function _test(t, done) { const client = connect({ keepalive: 3 }) client.once('connect', () => { assert.exists(client.keepaliveManager) client.end(true, done) }) }) it('should not setup keepalive manager if keepalive=0', function _test(t, done) { const client = connect({ keepalive: 0 }) client.on('connect', () => { assert.notExists(client.keepaliveManager) client.end(true, done) }) }) it( 'should reconnect on keepalive timeout', { timeout: 10000, }, function _test(t, done) { const clock = sinon.useFakeTimers(fakeTimersOptions) t.after(() => { clock.restore() }) const options: IClientOptions = { keepalive: 60, reconnectPeriod: 5000, } const client = connect(options) client.once('connect', () => { client.once('error', (err) => { assert.equal(err.message, 'Keepalive timeout') client.once('connect', () => { client.end(true, done) clock.tick(100) }) }) client.once('close', () => { // Wait for the reconnect to happen clock.tick(client.options.reconnectPeriod) }) const timeoutTimestamp = client.keepaliveManager.keepaliveTimeoutTimestamp clock.tick(timeoutTimestamp - Date.now()) }) }, ) it( 'should not reconnect if pingresp is successful', { timeout: 1000 }, function _test(t, done) { const clock = sinon.useFakeTimers(fakeTimersOptions) t.after(() => { clock.restore() }) const client = connect({ keepalive: 10 }) client.once('close', () => { done(new Error('Client closed connection')) }) client.once('connect', () => { setImmediate(() => { // make keepalive check trigger const timeoutTimestamp = client.keepaliveManager.keepaliveTimeoutTimestamp clock.tick(timeoutTimestamp - Date.now()) }) client.on('packetsend', (packet) => { if (packet.cmd === 'pingreq') { client.removeAllListeners('close') client.end(true, done) clock.tick(100) } }) clock.tick(1) }) }, ) }) describe('subscribing', () => { beforeEach(beforeEachExec) after(afterExec) it('should send a subscribe message (offline)', function _test(t, done) { const client = connect() client.subscribe('test') server.once('client', (serverClient) => { serverClient.once('subscribe', () => { client.end((err) => done(err)) }) }) }) it('should send a subscribe message', function _test(t, done) { const client = connect() const topic = 'test' client.once('connect', () => { client.subscribe(topic) }) server.once('client', (serverClient) => { serverClient.once('subscribe', (packet) => { const result: ISubscriptionRequest = { topic, qos: 0, } 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 emit a packetsend event', function _test(t, done) { const client = connect() const testTopic = 'testTopic' client.once('connect', () => { client.subscribe(testTopic) }) client.on('packetsend', (packet) => { if (packet.cmd === 'subscribe') { client.end((err) => done(err)) } }) }) it('should emit a packetreceive event', function _test(t, done) { const client = connect() const testTopic = 'testTopic' client.once('connect', () => { client.subscribe(testTopic) }) client.on('packetreceive', (packet) => { if (packet.cmd === 'suback') { client.end((err) => done(err)) } }) }) it('should accept an array of subscriptions', function _test(t, 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: ISubscriptionRequest = { 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(t, done) { const client = connect() const topics: ISubscriptionMap = { 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: ISubscriptionRequest = { 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(t, done) { const client = connect() const topic = 'test' const opts: IClientSubscribeOptions = { qos: 1 } client.once('connect', () => { client.subscribe(topic, opts) }) server.once('client', (serverClient) => { serverClient.once('subscribe', (packet) => { const expected: ISubscriptionRequest[] = [ { topic, qos: 1, }, ] if (version === 5) { expected[0].nl = false expected[0].rap = false expected[0].rh = 0 } assert.deepStrictEqual(packet.subscriptions, expected) client.end((err) => done(err)) }) }) }) it('should subscribe with the default options for an empty options parameter', function _test(t, 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: ISubscriptionRequest = { topic, qos: defaultOpts.qos as 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(t, done) { const client = connect() const topic = 'test' client.once('connect', () => { client.subscribe(topic, { qos: 2 }, (err, granted, suback) => { if (err) { done(err) } else { assert.exists(granted, 'granted not given') const expectedResult: ISubscriptionRequest = { topic: 'test', qos: 2, } if (version === 5) { expectedResult.nl = false expectedResult.rap = false expectedResult.rh = 0 expectedResult.properties = undefined } assert.include(granted[0], expectedResult) assert.exists(suback, 'suback not given') assert.deepStrictEqual(suback.granted, [2]) client.end((err2) => done(err2)) } }) }) }) it('should fire a callback with error if disconnected (options provided)', function _test(t, 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(t, 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(t, done) { const client = connect() const topic = '中国' client.once('connect', () => { client.subscribe(topic) }) server.once('client', (serverClient) => { serverClient.once('subscribe', (packet) => { const result: ISubscriptionRequest = { topic, qos: 0, } if (version === 5) { result.nl = false result.rap = false result.rh = 0 } assert.include(packet.subscriptions[0], result) client.end(done) }) }) }) it('should send multiple subscribe packets when topic count exceeds batchSize', function _test(t, done) { const client = connect({ subscribeBatchSize: 2 }) const subs = ['test1', 'test2', 'test3'] client.once('connect', () => { client.subscribe(subs) }) const spy = sinon.spy() server.once('client', (serverClient) => { serverClient.on('subscribe', spy) }) client.on('end', () => { assert.strictEqual(spy.callCount, 2) for (let i = 0; i < 2; i++) { // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] const expected = subs .slice(i * 2, i * 2 + 2) .map((topic) => { const result: ISubscriptionRequest = { topic, qos: 0, } if (version === 5) { result.nl = false result.rap = false result.rh = 0 } return result }) assert.deepStrictEqual( spy.getCall(i).args[0].subscriptions, expected, ) } done() }) setTimeout(() => { client.end() }, 300) }) }) describe('receiving messages', () => { beforeEach(beforeEachExec) after(afterExec) it('should fire the message event', function _test(t, 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: mqtt.IPublishPacket) => { 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(t, done) { const client = connect() const testPacket = { topic: 'test', payload: 'message', retain: true, qos: 1, messageId: 5, } client.subscribe(testPacket.topic) client.on('packetreceive', (packet: mqtt.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(t, 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(t, 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(t, 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(t, 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', () => { beforeEach(beforeEachExec) after(afterExec) it('should follow qos 0 semantics (trivial)', function _test(t, 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(t, 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(t, 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: 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(t, 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: ISubackPacket = { 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: IPubrelPacket = { 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: IPublishPacket = { cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid, dup: false, retain: false, } handle(client, publish, (err) => { assert.notExists(err) }) }) } it('handle qos 2 messages exactly once when multiple pubrel received', function _test(t, done) { testMultiplePubrel(false, done) }) it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function _test(t, done) { testMultiplePubrel(true, done) }) }) describe('auto reconnect', () => { beforeEach(beforeEachExec) after(afterExec) it('should mark the client disconnecting if #end called', function _test(t, done) { const client = connect() client.end(true, (err) => { assert.isTrue(client.disconnecting) done(err) }) }) it('should reconnect after stream disconnect', function _test(t, done) { const clock = sinon.useFakeTimers(fakeTimersOptions) t.after(() => { clock.restore() }) const client = connect({ reconnectPeriod: 1000 }) let tryReconnect = true client.on('connect', () => { if (tryReconnect) { client.stream.end() client.once('close', () => { clock.tick(client.options.reconnectPeriod) }) tryReconnect = false } else { client.end(true, done) clock.tick(100) } }) }) it("should emit 'reconnect' when reconnecting", function _test(t, done) { const clock = sinon.useFakeTimers(fakeTimersOptions) t.after(() => { clock.restore() }) const client = connect({ reconnectPeriod: 1000, }) let tryReconnect = true let reconnectEvent = false client.on('reconnect', () => { reconnectEvent = true }) client.on('connect', () => { if (tryReconnect) { client.stream.end() client.once('close', () => { clock.tick(client.options.reconnectPeriod) }) tryReconnect = false } else { assert.isTrue(reconnectEvent) client.end(true, done) clock.tick(100) } }) }) it("should emit 'offline' after going offline", function _test(t, done) { const clock = sinon.useFakeTimers(fakeTimersOptions) t.after(() => { clock.restore() }) const client = connect({ reconnectPeriod: 1000, }) let tryReconnect = true let offlineEvent = false client.on('offline', () => { offlineEvent = true }) client.on('connect', () => { if (tryReconnect) { client.stream.end() tryReconnect = false client.once('close', () => { clock.tick(client.options.reconnectPeriod) }) } else { assert.isTrue(offlineEvent) client.end(true, done) clock.tick(100) } }) }) it('should not reconnect if it was ended by the user', function _test(t, done) { const client = connect() client.on('connect', () => { client.end((err) => done(err)) }) }) it('should setup a reconnect timer on disconnect', function _test(t, 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)`, { timeout: 10000, }, function _test(t, done) { const clock = sinon.useFakeTimers(fakeTimersOptions) t.after(() => { clock.restore() }) let end const reconnectSlushTime = 200 const client = connect({ reconnectPeriod: test.period }) let reconnect = false const start = clock.now client.on('connect', () => { if (!reconnect) { client.stream.end() client.once('close', () => { // ensure the tick is done after the reconnect timer is setup (on close) clock.tick(test.period) }) reconnect = true } else { end = clock.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}`, ), ) } }) clock.tick(100) } }) }, ) }) it('should always cleanup successfully on reconnection', function _test(t, done) { teardownHelper.add({ executeOnce: true, order: 1 }, () => { if (clock) { clock.restore() } }) const clock = sinon.useFakeTimers({ ...fakeTimersOptions, toFake: ['setTimeout'], }) 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(() => { setTimeout(() => { client.end(done) }, 10) clock.tick(10) }, 10) clock.tick(10) }) it('should emit connack timeout error', function _test(t, done) { // Use fake timers to simulate the timeout. The setTimeout inside the client connection // will inactive by other tests (maybe) causing this test never ends. const clock = sinon.useFakeTimers({ ...fakeTimersOptions, toFake: ['setTimeout'], }) const connectTimeout = 10 t.after(() => { clock.restore() }) const client = connect({ connectTimeout, reconnectPeriod: 5000, }) .on('connect', () => { clock.tick(connectTimeout) }) .on('error', (err) => { assert.equal(err.message, 'connack timeout') client.end(true, done) }) }) it('should reconnect on connack error if requested', function _test(t, done) { let connackErrors = 0 const rcNotAuthorized = 135 const server2 = serverBuilder(config.protocol, (serverClient) => { serverClient.on('connect', () => { const rc = connackErrors === 0 ? rcNotAuthorized : 0 const connack = version === 5 ? { reasonCode: rc } : { returnCode: rc } serverClient.connack(connack) }) }) teardownHelper.addServer(server2) server2.listen(ports.PORTAND50, () => { const client = connect({ host: 'localhost', port: ports.PORTAND50, reconnectPeriod: 10, reconnectOnConnackError: true, }) teardownHelper.addClient(client) client.on('error', (err) => { assert.instanceOf(err, ErrorWithReasonCode) assert.equal( (err as ErrorWithReasonCode).code, rcNotAuthorized, ) assert.equal(connackErrors, 0) connackErrors++ }) client.on('connect', () => { assert.equal(connackErrors, 1) done() }) }) }) it( 'should resend in-flight QoS 1 publish messages from the client', { timeout: 4000, }, function _test(t, done) { const client = connect({ reconnectPeriod: 200 }) let serverPublished = false let clientCalledBack = false // client is connected the first time server.once('client', (serverClient) => { // destroy the stream before the publish is acknowledged serverClient.once('connect', () => { setImmediate(() => { serverClient.stream.destroy() }) }) // after 200ms the client should reconnect server.once('client', (serverClientNew) => { serverClientNew.on('publish', () => { serverPublished = true }) }) }) // ensure that on first reconnect the publish is still not acknowledged client.once('reconnect', () => { // client callback should not be triggered on first connection assert.isFalse(clientCalledBack) }) client.publish('hello', 'world', { qos: 1 }, () => { clientCalledBack = true }) client.on('packetreceive', (packet) => { if (packet.cmd === 'puback') { assert.isTrue(serverPublished) setImmediate(() => { assert.isTrue(clientCalledBack) client.end(true, done) }) } }) }, ) it('should not resend in-flight publish messages if disconnecting', function _test(t, done) { const client = connect({ reconnectPeriod: 200 }) let serverPublished = false let clientCalledBack = false server.once('client', (serverClient) => { serverClient.once('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', { timeout: 4000, }, function _test(t, 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 }) }) }) client.publish('hello', 'world', { qos: 2 }, () => { clientCalledBack = true }) client.on('packetreceive', (packet) => { if (packet.cmd === 'pubcomp') { assert.isTrue(serverPublished) setImmediate(() => { assert.isTrue(clientCalledBack) client.end(true, done) }) } }) }, ) it('should not resend in-flight QoS 1 removed publish messages from the client', function _test(t, done) { const client = connect({ reconnectPeriod: 100 }) let clientCalledBack = false server.once('client', (serverClient) => { serverClient.on('connect', () => { setImmediate(() => { serverClient.stream.destroy() }) }) server.once('client', (serverClientNew) => { serverClientNew.on('publish', () => { done(Error('should not have received publish')) }) }) }) 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(t, 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', () => { done(Error('should not have received publish')) }) }) }) 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(t, 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 resubscribe when clean=false and sessionPresent=false', function _test(t, done) { const client = connect({ clientId: 'test', reconnectPeriod: 100, clean: false, protocolVersion: 4, }) 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(t, 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', () => { 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(t, done) { let tryReconnect = true let reconnectEvent = false let client: mqtt.MqttClient | null = null 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, () => { 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', () => { fail() }) }) }) tryReconnect = false } else { assert.isTrue(reconnectEvent) assert.strictEqual( Object.keys(client['_resubscribeTopics']).length, 0, ) done() } }) }) }) it('should preserved incomingStore after disconnecting if clean is false', function _test(t, done) { let reconnect = false let client: mqtt.MqttClient | null = null 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) => { done() }) }) 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(t, done) { let reconnect = false let client: mqtt.MqttClient | null = null 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) { done() } 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(t, done) { let reconnect = false let client: mqtt.MqttClient | null = null 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) { done() } 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(t, done) { let reconnect = false let client: mqtt.MqttClient | null = null 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) { done() } 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(t, done) { let reconnect = false let client: mqtt.MqttClient | null = null 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) done() }, ) } }) client.on('error', () => {}) }) }) it('should resend in-flight publish messages by published order', function _test(t, done) { let publishCount = 0 let reconnect = false let disconnectOnce = true let client: mqtt.MqttClient | null = null 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', ) done() 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(t, 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(t, done) { teardownHelper.add({ executeOnce: true, order: 1 }, () => { if (clock) { clock.restore() } }) const clock = sinon.useFakeTimers({ ...fakeTimersOptions, toFake: ['setTimeout'], }) const client = connect({ reconnectPeriod: 0 }) let tryReconnect = true let reconnectEvent = false client.on('close', () => { if (tryReconnect) { tryReconnect = false setTimeout(() => { client.reconnect() }, 100) clock.tick(100) } else { assert.isTrue(reconnectEvent) done() } }) client.on('reconnect', () => { reconnectEvent = true }) client.on('connect', () => { if (tryReconnect) { client.end() } else { client.subscribe('hello', () => { client.end() }) } }) }) describe('with alternate server client', () => { let cachedClientListeners const connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } beforeEach(async () => { await beforeEachExec() cachedClientListeners = server.listeners('client') server.removeAllListeners('client') }) afterEach(() => { server.removeAllListeners('client') cachedClientListeners.forEach((listener) => { server.on('client', listener) }) }) after(afterExec) it('should resubscribe even if disconnect is before suback', function _test(t, 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(t, 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', () => { beforeEach(beforeEachExec) after(afterExec) it('should not create a mapping if resubscribe is disabled', function _test(t, 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(t, 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(t, 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) }) }) }) }) }) } ================================================ FILE: test/node/abstract_store.ts ================================================ import { type IPublishPacket, type IPubrelPacket } from 'mqtt-packet' import 'should' import { it, beforeEach, afterEach } from 'node:test' import { type IStore } from '../../src' export default function abstractStoreTest( build: (cb: (err?: Error, store?: IStore) => void) => void, ) { let store: IStore beforeEach((_ctx, done) => { build((err, _store) => { store = _store done(err) }) }) afterEach((_ctx, done) => { store.close(done) }) it('should put and stream in-flight packets', function _test(t, done) { const packet: IPublishPacket = { topic: 'hello', payload: 'world', qos: 1, messageId: 42, cmd: 'publish', dup: false, retain: false, } store.put(packet, () => { store.createStream().on('data', (data) => { data.should.eql(packet) done() }) }) }) it('should support destroying the stream', function _test(t, done) { const packet: IPublishPacket = { topic: 'hello', payload: 'world', qos: 1, messageId: 42, cmd: 'publish', dup: false, retain: false, } store.put(packet, () => { const stream = store.createStream() stream.on('close', done) stream.destroy() }) }) it('should add and del in-flight packets', function _test(t, done) { const packet: IPublishPacket = { topic: 'hello', payload: 'world', qos: 1, messageId: 42, cmd: 'publish', dup: false, retain: false, } 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(t, done) { const packet1: IPublishPacket = { cmd: 'publish', // added topic: 'hello', payload: 'world', qos: 2, messageId: 42, dup: false, retain: false, } const packet2: IPubrelPacket = { 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(t, done) { const packet: IPublishPacket = { topic: 'hello', payload: 'world', qos: 1, messageId: 42, cmd: 'publish', dup: false, retain: false, } 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(t, done) { const packet: IPublishPacket = { topic: 'hello', payload: 'world', qos: 1, messageId: 42, cmd: 'publish', dup: false, retain: false, } store.put(packet, () => { store.get({ messageId: 42 }, (err, fromDb) => { if (err) { throw err } fromDb.should.eql(packet) done() }) }) }) } ================================================ FILE: test/node/client.ts ================================================ import { useFakeTimers } from 'sinon' import { assert } from 'chai' import { fork } from 'child_process' import path from 'path' import net from 'net' import eos from 'end-of-stream' import mqttPacket from 'mqtt-packet' import { Duplex } from 'readable-stream' import Connection from 'mqtt-connection' import util from 'util' import _debug from 'debug' import { type IClientOptions } from 'src/lib/client' import { describe, it, after } from 'node:test' import getPorts from './helpers/port_list' import serverBuilder from './server_helpers_for_client_tests' import { MqttServer } from './server' import abstractClientTests from './abstract_client' import mqtt, { MQTTJS_VERSION } from '../../src' const debug = _debug('mqttjs:client-test') const ports = getPorts(2) describe('MqttClient', () => { let client: mqtt.MqttClient const server = serverBuilder('mqtt') const config: IClientOptions = { 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() } process.exit(0) }) it('should have static VERSION set', function _test(t) { assert.equal(mqtt.MqttClient.VERSION, MQTTJS_VERSION) }) abstractClientTests(server, config, ports) describe('creating', () => { it('should allow instantiation of MqttClient', function _test(t, 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(t, done) { try { assert.isTrue(mqttPacket.writeToStream.cacheNumbers) client = new mqtt.MqttClient( () => { throw Error('break') }, { writeCache: false }, ) client.end() } catch { assert.isFalse(mqttPacket.writeToStream.cacheNumbers) done() } }) }) describe('message ids', () => { it('should increment the message id', function _test(t, 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(t, 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(t, 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', (topic, 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: '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', { timeout: 10000, }, function _test(t, done) { 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, // disable reconnect }) client.once('connect', () => { client.publish( 'fakeTopic', 'fakeMessage', { qos: 1 }, (err) => { // connection closed assert.exists(err) pubCallbackCalled = true }, ) client.unsubscribe('fakeTopic', (err, result) => { // connection closed assert.exists(err) unsubscribeCallbackCalled = true }) client.once('error', (err) => { assert.equal(err.message, 'Keepalive timeout') const originalFLush = client['_flush'] // flush will be called on _cleanUp because of keepalive timeout client['_flush'] = function _flush() { originalFLush.call(client) client.end((err1) => { assert.strictEqual( pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks should be invoked with error', ) server2.close((err2) => { done(err1 || err2) }) }) } }) }) }, ) }) describe('reconnecting', () => { it( 'should attempt to reconnect once server is down', { timeout: 5000, }, function _test(t, done) { const args = ['-r', 'ts-node/register'] if (process.env.DEBUG_SERVER) { args.unshift('--inspect') } const innerServer = fork( path.join(__dirname, 'helpers', 'server_process.ts'), { execArgv: args, }, ) 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', { timeout: 2000, }, function _test(t, done) { 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', { timeout: 4000, }, function _test(t, done) { 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', { timeout: 1000, }, function _test(t, done) { 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 }) }) }) }) let reconnections = 0 client.on('reconnect', () => { reconnections++ if (reconnections === 2) { 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) done(Error('client queue not empty')) } } }) }, ) it( 'should not send the same subscribe multiple times on a flaky connection', { timeout: 3500, }, function _test(t, done) { 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.close() } 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', { timeout: 2500, }, function _test(t, done) { 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', { timeout: 3500, }, function _test(t, done) { 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.close() } serverClient.puback(packet) } }) }) }, ) }) it( 'check emit error on checkDisconnection w/o callback', { timeout: 15000, }, function _test(t, done) { 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: IClientOptions = { 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']() }) }) }, ) describe('connect manually', () => { it( 'should not throw an error when publish after second connect', { timeout: 10000, }, async function _test(t) { const clock = useFakeTimers({ shouldClearNativeTimers: true, toFake: ['setTimeout'], }) t.after(async () => { clock.restore() if (client) { await client.endAsync(true) } }) const fail = await new Promise((resolveParent) => { let countConnects = 0 const publishInterval = ( repetible: number, timeout: number, callback: (threwError: boolean) => void, ): void => { const method = () => new Promise((resolve) => { client.publish('test', 'test', (err) => { if ( err?.message.toLocaleLowerCase() === 'client disconnecting' ) { resolve(true) } else { resolve(false) } }) }) if (repetible <= 0) { callback(false) return } method().then((threwError) => { clock.tick(timeout) if (threwError) { callback(true) return } publishInterval(repetible - 1, timeout, callback) }) } client = mqtt.connect(config) client.on('connect', () => { ++countConnects const intervalRepetible = 4 const intervalTimeout = 250 const connectTimeout = intervalRepetible * intervalTimeout publishInterval( intervalRepetible, intervalTimeout, (threwError) => { if (countConnects === 2) { resolveParent(threwError) } }, ) if (countConnects === 1) { clock.setTimeout(() => { client.end(() => client.connect()) }, connectTimeout) } }) }) assert.isFalse(fail, 'disconnecting variable was not reset') }, ) it( 'reset disconnecting variable to false after disconnect when option reconnectPeriod=0', { timeout: 10000, }, async function _test(t) { client = await mqtt.connectAsync({ ...config, reconnectPeriod: 0, }) assert.isFalse( client.disconnecting, 'disconnecting should be false after connect', ) const endPromise = client.endAsync() assert.isTrue( client.disconnecting, 'disconnecting should be true processing end', ) await endPromise assert.isFalse( client.disconnecting, 'disconnecting should be false after end', ) }, ) it( 'reset disconnecting variable to false after disconnect when option manualConnect=true', { timeout: 10000, }, async function _test(t) { client = mqtt.connect({ ...config, manualConnect: true, }) await new Promise((resolve, reject) => { client .connect() .on('error', (err) => { reject(err) }) .once('connect', () => { resolve(undefined) }) }) assert.isFalse( client.disconnecting, 'disconnecting should be false after connect', ) const endPromise = client.endAsync() assert.isTrue( client.disconnecting, 'disconnecting should be true processing end', ) await endPromise assert.isFalse( client.disconnecting, 'disconnecting should be false after end', ) }, ) }) describe('async methods', () => { it( 'connect-subscribe-unsubscribe-end', { timeout: 15000, }, function _test(t) { return new Promise(async (resolve, reject) => { server.once('client', (serverClient) => { serverClient.on('publish', async (packet) => { assert.equal(packet.topic, 'hello') assert.equal(packet.payload.toString(), 'world') await client.unsubscribeAsync('hello') await client.endAsync() resolve() }) }) client = await mqtt.connectAsync(config) const sub = await client.subscribeAsync('hello') assert.equal(sub[0].topic, 'hello') assert.equal(sub[0].qos, 0) await client.publishAsync('hello', 'world') }) }, ) it( 'connect should throw error', { timeout: 5000, }, async function _test(t) { let error = false try { await mqtt.connectAsync({ port: 1000, host: '127.0.0.1', reconnectPeriod: 0, }) } catch (err) { error = true assert.isTrue(err.message.includes('ECONNREFUSED')) } assert.isTrue(error) }, ) it( 'publish should throw error', { timeout: 5000, }, async function _test(t) { let error = false try { client = await mqtt.connectAsync(config) client.disconnecting = true await client.publishAsync('#/#', 'world') } catch (err) { error = true client.disconnecting = false assert.equal(err.message, 'client disconnecting') } await client.endAsync() assert.isTrue(error) }, ) }) }) ================================================ FILE: test/node/client_mqtt5.ts ================================================ import { assert } from 'chai' import { after, describe, it } from 'node:test' import abstractClientTests from './abstract_client' import { MqttServer } from './server' import serverBuilder from './server_helpers_for_client_tests' import getPorts from './helpers/port_list' import mqtt, { type ErrorWithReasonCode } from '../../src' const ports = getPorts(1) describe('MQTT 5.0', () => { const server = serverBuilder('mqtt').listen(ports.PORTAND115) const config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 }, } after(() => { // clean up and make sure the server is no longer listening... if (server.listening) { server.close() } process.exit(0) }) abstractClientTests(server, config, ports) it( 'topic should be complemented on receive', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const incomingStore = new mqtt.Store({ clean: false }) const outgoingStore = new mqtt.Store({ clean: false }) const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const incomingStore = new mqtt.Store({ clean: false }) const outgoingStore = new mqtt.Store({ clean: false }) const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 5000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }, } const client = mqtt.connect(opts) client.on('error', (error) => { 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', { timeout: 2500, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const server2 = new MqttServer((serverClient) => { serverClient.on('connect', (packet) => { serverClient.connack({ reasonCode: 0, properties: { serverKeepAlive: 16, maximumPacketSize: 95, }, }) }) }).listen(ports.PORTAND116) const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { 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: mqtt.IClientOptions = { 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', { // timeout: 15000, }, function _test(t, 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: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND117) const opts: mqtt.IClientOptions = { host: 'localhost', port: ports.PORTAND117, protocolVersion: 5, } const client = mqtt.connect(opts) client.once('connect', () => { client.publish( 'a/b', 'message', { qos: 1 }, (err: ErrorWithReasonCode) => { 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', { timeout: 15000, }, function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND118) const opts: mqtt.IClientOptions = { host: 'localhost', port: ports.PORTAND118, protocolVersion: 5, } const client = mqtt.connect(opts) client.once('connect', () => { client.publish( 'a/b', 'message', { qos: 2 }, (err: ErrorWithReasonCode) => { 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', { // timeout: 15000, }, function _test(t, done) { // this.timeout(15000) serverThatSendsErrors.listen(ports.PORTAND117) const opts: mqtt.IClientOptions = { 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('suback handling error codes', function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND117) serverThatSendsErrors.once('client', (serverClient) => { serverClient.on('subscribe', (packet) => { serverClient.suback({ messageId: packet.messageId, granted: packet.subscriptions.map((e) => 135), }) }) }) const client = mqtt.connect({ protocolVersion: 5, port: ports.PORTAND117, host: 'localhost', }) client.subscribe('$SYS/#', (subErr) => { client.end(true, (endErr) => { serverThatSendsErrors.close((err2) => { if (subErr) { assert.strictEqual( subErr.message, 'Subscribe error: Not authorized', ) return done(err2 || endErr) } done(new Error('Suback errors do NOT work')) }) }) }) }) it( 'server side disconnect', { timeout: 15000, }, function _test(t, done) { 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: mqtt.IClientOptions = { host: 'localhost', port: ports.PORTAND327, protocolVersion: 5, } const client = mqtt.connect(opts) client.once( 'disconnect', (disconnectPacket: mqtt.IDisconnectPacket) => { assert.strictEqual(disconnectPacket.reasonCode, 128) client.end(true, (err) => done(err)) }, ) }, ) it( 'pubrec handling custom reason code', { timeout: 15000, }, function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND117) const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND117) const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND117) const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND117) const opts: mqtt.IClientOptions = { 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', { timeout: 15000, }, function _test(t, done) { serverThatSendsErrors.listen(ports.PORTAND117) const opts: mqtt.IClientOptions = { 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 }) }) }, ) }) ================================================ FILE: test/node/helpers/TeardownHelper.ts ================================================ import type { MqttClient } from 'src' import { randomUUID } from 'node:crypto' import { isAsyncFunction } from 'node:util/types' import type serverBuilder from '../server_helpers_for_client_tests' type ServerBuilderInstance = ReturnType type AddOptions = { /** * @description * If `true`, the method will be executed only one time and then removed from the store. * * @default true */ executeOnce?: boolean /** * @description * The order in which the method will be executed. * If `order===0` the method will be executed after all methods before it that were added. * * @default 0 */ order?: number } type ResetOptions = { /** * @description * If `true`, only the methods that have the option `executeOnce` set to `true` will be removed. * * @default false */ removeOnce?: boolean } type Method = | Promise | ((...args: any[]) => Promise) | ((...args: any[]) => any) /** * @description * Class to help clean the environment or close opened connections after tests finish. * Also, you can add custom methods to be executed after the tests finish, like * deleting temporary files or closing connections. * * @example * ``` * import { describe, it } from 'node:test' * import mqtt from './src' * import serverBuilder from './test/server_helpers_for_client_tests' * import TeardownHelper from './test/helpers/TeardownHelper' * * * describe('Test', () => { * const teardownHelper = new TeardownHelper() * * it('should clean the client and server', (t, done) => { * t.after(async () => { * await teardownHelper.runAll() * }) * * const server = serverBuilder('8883') * const client = mqtt.connect('mqtt://localhost') * * teardownHelper.addServer(server) * teardownHelper.addClient(client) * }) * }) * ``` * * @example * ``` * import { describe, it, after } from 'node:test' * import mqtt from './src' * import serverBuilder from './test/server_helpers_for_client_tests' * import TeardownHelper from './test/helpers/TeardownHelper' * * * describe('Test', () => { * * const teardownHelper = new TeardownHelper() * let server = serverBuilder('8883') * * teardownHelper.add({}, async () => { * if (server?.listening) { * await new Promise((resolve, reject) => { * server.close((err) => { * if (err) reject(err) * else resolve() * }) * }) * } * }) * * after(async () => { * await teardownHelper.runAll() * }) * * it('should clean the client and server', (t, done) => { * server = serverBuilder('8883') * const client = mqtt.connect('mqtt://localhost') * * teardownHelper.addClient(client) * done() * }) * * }) * ``` */ class TeardownHelper { #methods: Map< string, { options: AddOptions method: Method args: any[] } > constructor() { this.#methods = new Map() } /** * @description * Add a client to close. */ addClient(client: MqttClient) { this.add({}, this.closeClient, client) } /** * @description * Add a server to close. */ addServer(server: ServerBuilderInstance) { this.add({}, this.closeServer, server) } /** * @param options Options to be passed to the method. * @param method It can be a promise or a function that returns a promise. * @param args Arguments to be passed to the method. * * @description * Add a method to be executed */ add( options: AddOptions | undefined, method: Method, ...args: T ): string { const id = randomUUID() this.#methods.set(id, { method, args, options: { executeOnce: true, order: 0, ...options }, }) return id } /** * @description * Remove all methods stored. */ reset(options?: ResetOptions) { if (options?.removeOnce) { for (const [id, { options: methodOptions }] of this.#methods) { if (methodOptions.executeOnce) { this.#methods.delete(id) } } } else { this.#methods.clear() } } /** * @description * Close the `client` connection. * * @default * Use the `client` set in the class. */ async closeClient(client: MqttClient) { if (client) { await new Promise((resolve, reject) => { client.end(true, (err) => { if (err) reject(err) else resolve() }) }) } } /** * @description * Close the `server` connection. * * @default * Use the `server` set in the class. */ async closeServer(server: ServerBuilderInstance) { if (server?.listening) { await new Promise((resolve, reject) => { server.close((err) => { if (err) reject(err) else resolve() }) }) } } /** * @param id Method id to be executed. * * @description * Execute a method stored by its id. * If the method has the option `executeOnce` set to `true`, it will be removed after execution. */ async run(id: string) { const method = this.#methods.get(id) if (!method) { return } if (method.options.executeOnce) { this.#methods.delete(id) } if (method.method instanceof Promise) { await method.method } else { await method.method(...method.args) } } /** * @description * Execute all methods stored. * If a method has the option `executeOnce` set to `true`, it will be removed after execution. */ async runAll() { if (this.#methods.size === 0) { return } const methodStored: (AddOptions & { key: string })[] = [] for (const [key, { options }] of this.#methods) { methodStored.push({ ...options, key }) } methodStored.sort((a, b) => b.order - a.order) const methods: Array> = [] for (const { key, ...options } of methodStored) { const { method, args } = this.#methods.get(key) if (method instanceof Promise) { methods.push(method) } else if (isAsyncFunction(method)) { const promise = new Promise((resolve, reject) => { method(...args) .then(resolve) .catch(reject) }) methods.push(promise) } else { const promise = new Promise((resolve, reject) => { try { const result = method(...args) resolve(result) } catch (error) { reject(error) } }) methods.push(promise) } if (options.executeOnce) { this.#methods.delete(key) } } const results = await Promise.allSettled(methods) for (const result of results) { if (result.status === 'rejected') { if (result.reason instanceof Error) throw result.reason else throw new Error(result.reason) } } } } export default TeardownHelper ================================================ FILE: test/node/helpers/leaked.ts ================================================ // include this as first module when looking for leaked handles import leaked from 'leaked-handles' leaked.set({ fullStack: true, timeout: 15000, debugSockets: true, }) ================================================ FILE: test/node/helpers/port_list.ts ================================================ /** * Method used to get ports for testing * @param i Index to shift the ports by * @returns */ export default function getPorts(i = 0) { const PORT = 10000 + i * 400 const ports = { PORT, PORTAND40: PORT + 40, PORTAND41: PORT + 41, PORTAND42: PORT + 42, PORTAND43: PORT + 43, PORTAND44: PORT + 44, PORTAND45: PORT + 45, PORTAND46: PORT + 46, PORTAND47: PORT + 47, PORTAND48: PORT + 48, PORTAND49: PORT + 49, PORTAND50: PORT + 50, PORTAND72: PORT + 72, PORTAND103: PORT + 103, PORTAND114: PORT + 114, PORTAND115: PORT + 115, PORTAND116: PORT + 116, PORTAND117: PORT + 117, PORTAND118: PORT + 118, PORTAND119: PORT + 119, PORTAND316: PORT + 316, PORTAND326: PORT + 326, PORTAND327: PORT + 327, PORTAND400: PORT + 400, } return ports } ================================================ FILE: test/node/helpers/private-csr.pem ================================================ -----BEGIN CERTIFICATE REQUEST----- MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBANtzIZmqf7h3axO9mzo2VhiF/BF3Y4E/fDTkFy27 PgssS/ipFOMe/IxyM/hA/o/bQb0BY+sH5s1020kNH79umtabWMaDmOi8bvmHWtVC cYhn3mhbRFWcORdTnfQ8uRYXZGeoupjlhfrKkQCoSAFKh1OzU7aNx4CjMAjSa4py trMAVNJ37RryhsfMuHAeG8+0Eo3qmYyaplpurtr8A3HWV65R2VFCwZ5hKG8I9X2F 3UrYKHr4xlxOgjD8j2OfYZxpGHI6YexJ28aR0xlsWfzS+TKKFVxy8ntgPGL0ZXL3 vss80mAcBl9FfsJzufn4IHOYspX1OEM0M7plMmQw/yNT9B8CAwEAAaAAMA0GCSqG SIb3DQEBBQUAA4IBAQBsONiE5HTjfR1pDrWPIhbLqMO3AqmuB5AwpQm8kAaM2Oz1 DI/a8bHYyODMiyWUPTtwLMQWcJpAG2ZhE18gLqFwXZR1XSOxY1yF+uZ7Ls3hwzbq 9A6O254B5wXBnXkVbzZwFshV5HWiZwVivF5GDyLRsMoS2EtUHoDEP4YIRK0kPL9H m3BB334KlWTc8NNXFFG62OL7q2fa8xRHlN8SYfeUjy79eEoBdHv5wL/ZN/YBCDNJ 2zrYUvbOmfoq1e+6AczZ6xAHHeneUQuaOF225aMwHHZTiP2TlIeFXwBvzV1BWIJv dOaHX/f3NamKoGvwYyIR1FrI2FpXTJLRE/eu7TFD -----END CERTIFICATE REQUEST----- ================================================ FILE: test/node/helpers/private-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA23Mhmap/uHdrE72bOjZWGIX8EXdjgT98NOQXLbs+CyxL+KkU 4x78jHIz+ED+j9tBvQFj6wfmzXTbSQ0fv26a1ptYxoOY6Lxu+Yda1UJxiGfeaFtE VZw5F1Od9Dy5FhdkZ6i6mOWF+sqRAKhIAUqHU7NTto3HgKMwCNJrinK2swBU0nft GvKGx8y4cB4bz7QSjeqZjJqmWm6u2vwDcdZXrlHZUULBnmEobwj1fYXdStgoevjG XE6CMPyPY59hnGkYcjph7EnbxpHTGWxZ/NL5MooVXHLye2A8YvRlcve+yzzSYBwG X0V+wnO5+fggc5iylfU4QzQzumUyZDD/I1P0HwIDAQABAoIBAQDNgNdqS5wnZs1D Qz/mF5QwiugugxsPoh/yd9as4LeNRwIt7ki9F/twmlHInTTGCpFZKcAkDNY6eMAR fNTKNA2UAw3zeLDs4ekai4KoSvx+vKYuG6m2cgGUsp0sZuD8qxM/b2auX+JDpQZ9 Exm6+8wWucwfHE5DTI5i9In4sMweeuiEUYnndTzElkvnP/44h1fGSU1iGUKn/ftc P4X+3SU68KMT3kUsEBavtmSdyeG/lSFEjm73FwVIRZ+PfbQX2hDD+mmseAXGFKi1 HudtQkEzTvYR+QAgvtjNgt/0qxFtPdj7Y+iRkCZQSJToAw8z6vwUn1qNCADauGMI X6KIm8XBAoGBAPiwMLYpIqp1rksINbqpbVqjtqsoejQuPYeEF7OXHbH9il7pWrQF wLbogo3YXX+a66RreVMhsUeq7+pIf/sK2lT73gDpFfvZnJG1ww94QkHBEPso0bN9 pcGgceIK7KRRAiAl5Mjw6pZZNnIBxlIFaSbBqQau74NfdaalMBF2wi+3AoGBAOHm 3ttFtVjVlb2fHoiGNZCZDv3gnsQXZlCxS+rQ4XEmEWKHAH4T3+Kzmo8jWoX+DGGD 6UkxWHv7e+KrYIZDi7Dd2HFV0gHN6d1SNdPix3vN114bNOrbfqxuEVT5PdFHSuel 5d3ix+3U+tpHamwb88eyeq6Q3t5Lcl3gIRGLzo7ZAoGBAKVuLzk+K/1Qw1zOXU+K nWAKP92j04caq3uWd13UTMC2dHGmsdvHZ+dEzHQnVisol1CM3exbIV8XavliuR/6 nDqkQY5Bf4pFvE2Bp/yGdyzejblF8hmAn98qKBfCRKEZ8lwIWSUCfkr9laZJX+/4 AXbypMn5XQL7YXw1rsAvTAYJAoGAV4ZL8kkf6jtWuRFdkyfsuQmUdWkCGpe2XK1U 7LXhoyVMtw/3cOHibMOJrsvT1vaHdYDWcjVcQy084qXj0CF7jhtmMQM/StOtOMMR d/b1s1Idj6ia6CQDAGvk6zdmbB9jNj1gwoeLTuqmBsyEvz5VRZoxTlFzCE3TEew0 48d3UIECgYBMxnLByVQA3pQWWIZZyqt+HgJAphYPdpnPalblQAbuCksKTZ/QKDkW dzih1PQROVrYrX7VwJ3/I8gXIuvKVtN1NKOS3a0JtbJQhpH4YbRwyQskXWYP8oYa MjBGPymNDhZh0zoGWzst5uR3NpdNV+7yNYPvyxzVNjlPjtAUqIxjBg== -----END RSA PRIVATE KEY----- ================================================ FILE: test/node/helpers/public-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDBjCCAe4CCQDkrq1PMPtmfzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 cyBQdHkgTHRkMB4XDTEzMDEyNTEwMzEyOVoXDTEzMDIyNDEwMzEyOVowRTELMAkG A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ANtzIZmqf7h3axO9mzo2VhiF/BF3Y4E/fDTkFy27PgssS/ipFOMe/IxyM/hA/o/b Qb0BY+sH5s1020kNH79umtabWMaDmOi8bvmHWtVCcYhn3mhbRFWcORdTnfQ8uRYX ZGeoupjlhfrKkQCoSAFKh1OzU7aNx4CjMAjSa4pytrMAVNJ37RryhsfMuHAeG8+0 Eo3qmYyaplpurtr8A3HWV65R2VFCwZ5hKG8I9X2F3UrYKHr4xlxOgjD8j2OfYZxp GHI6YexJ28aR0xlsWfzS+TKKFVxy8ntgPGL0ZXL3vss80mAcBl9FfsJzufn4IHOY spX1OEM0M7plMmQw/yNT9B8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHAwoKYl 6g9lUEwBDqm6ZxjgoYQi6V3loCjBcTr5OrMkLvvZrA55xsse0NRH40I/pvCaAZAZ EEna0fr5GPYi+y+I8EoU2W/+ehSqRAU8Fkdm0eR5MjyLWYOwd3ClUND8EpUNNSKH Xw9k9EQmyKsDxVsKWoJoO9rfFkUjooz07jGPCud18QCBs5i5ThbnQ9UP+26D8z5k 1Dii69LIcLXA3Vtm6R5fT57zNusfx8bqA9yy7UThYaXIazNMWNxiJRXfv0J4zFdD RQ+SFdJ3p5jurPkc3oRWWPbn/Lpf0E5XlYTJImXT1WmWnQSaNtME4P+3kEL5x+v/ u8zTLbobG4x0rQ== -----END CERTIFICATE----- ================================================ FILE: test/node/helpers/public-key.pem ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23Mhmap/uHdrE72bOjZW GIX8EXdjgT98NOQXLbs+CyxL+KkU4x78jHIz+ED+j9tBvQFj6wfmzXTbSQ0fv26a 1ptYxoOY6Lxu+Yda1UJxiGfeaFtEVZw5F1Od9Dy5FhdkZ6i6mOWF+sqRAKhIAUqH U7NTto3HgKMwCNJrinK2swBU0nftGvKGx8y4cB4bz7QSjeqZjJqmWm6u2vwDcdZX rlHZUULBnmEobwj1fYXdStgoevjGXE6CMPyPY59hnGkYcjph7EnbxpHTGWxZ/NL5 MooVXHLye2A8YvRlcve+yzzSYBwGX0V+wnO5+fggc5iylfU4QzQzumUyZDD/I1P0 HwIDAQAB -----END PUBLIC KEY----- ================================================ FILE: test/node/helpers/server.ts ================================================ import fs from 'fs' import { MqttServer, MqttSecureServer } from '../server' export function init_server(PORT: number) { const server = new MqttServer((client) => { client.on('connect', () => { client.connack(0) }) 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', (packet) => { client.pubcomp({ messageId: packet.messageId }) }) client.on('pingreq', () => { client.pingresp() }) client.on('disconnect', () => { client.stream.end() }) }) server.listen(PORT) return server } export function init_secure_server(port: number, key: string, cert: string) { 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 } ================================================ FILE: test/node/helpers/server_process.ts ================================================ import { MqttServer } from '../server' new MqttServer((client) => { client.on('connect', () => { client.connack({ returnCode: 0 }) }) }).listen(3481, 'localhost') ================================================ FILE: test/node/helpers/tls-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDkzCCAnugAwIBAgIUKq35JCwofQRXirn9WuUcjNjGt5MwDQYJKoZIhvcNAQEL BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X DTIwMDgyMjE4MDcwNloXDTMwMDgyMDE4MDcwNlowWTELMAkGA1UEBhMCQVUxEzAR BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEArTgcoNC3gV1yIwMJ3geQCO1iGL7E4GwiGL6h+EyPU011w5bAH9+Q ftGy8XaNjTJWMu6E+tFf5r+AWE314s0QJc7NsfSpy8LATcUc/Z3XlyTkHN9IMScn Rmk+J6FVprvi06Ab64LWyIGLd9DC19taw7xF0EO31jA41Vrs3q88jzjH9U6yYMhw GAfAPg5L5f0Q1hIz51mgLbqT5zbOE5h3ahZcfmyeR5+UjbS2LuIBem1FNPYwyUAg jK9AJieb4WVrRgfgIvKEsZQbYtltf9TfWAxVHJVIC0gu+Dhmi6JI6NbZZ1ngYFjJ uY91MN/Zu23NW5iTSE90x5iYJgQg0ot5/QIDAQABo1MwUTAdBgNVHQ4EFgQUNI0h Z+Q1vtev6jjdkYTNOJ9R7TAwHwYDVR0jBBgwFoAUNI0hZ+Q1vtev6jjdkYTNOJ9R 7TAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAclulOFJE7zSo YG0TF2PSc3yHdVYgL6MJnSf1rTQygO4XFIPdxlHtYiWeENDzc3drF2p8qRk2nidv uxzyDJ9L+K83Jl2QC404uD+bHl/N9M5qF+hZHL6pfuMKv3UZUxPt2bDWtzl96wmg XASC+R4AFb54XjRuRwCg8o7U/ILi8A4Q1uyM7dVwmztuy0QQpMJg01c/5Sr3brY0 qAlsl8EYBRtSVVb/c7CwbKT3b5aitqKm25WK3wWvTOE1VVyYxdNHW4IsX+eYB0Z3 dQ7ZQeb9TYp6taaaC5avk7e6J5n6emHhpzbnHk0dNpKjmZeBrI9yfqdXqLJWdEbG AvPDUVfo/g== -----END CERTIFICATE----- ================================================ FILE: test/node/helpers/tls-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCtOByg0LeBXXIj AwneB5AI7WIYvsTgbCIYvqH4TI9TTXXDlsAf35B+0bLxdo2NMlYy7oT60V/mv4BY TfXizRAlzs2x9KnLwsBNxRz9ndeXJOQc30gxJydGaT4noVWmu+LToBvrgtbIgYt3 0MLX21rDvEXQQ7fWMDjVWuzerzyPOMf1TrJgyHAYB8A+Dkvl/RDWEjPnWaAtupPn Ns4TmHdqFlx+bJ5Hn5SNtLYu4gF6bUU09jDJQCCMr0AmJ5vhZWtGB+Ai8oSxlBti 2W1/1N9YDFUclUgLSC74OGaLokjo1tlnWeBgWMm5j3Uw39m7bc1bmJNIT3THmJgm BCDSi3n9AgMBAAECggEAYjCymb52p1BvSMWKLGAhF85okxpgw87ILTqy2eucO15n aS3lTqwOXrVEOHg5mVZ1Yn2ux/cz47ueZ3AZ+CzCAIyQMVY9ghGtrOgVnPaCpVz2 Kh+v7p0BOHqkDxb3VIKg+9GAwiny0soMYyjlqjLf6qCo+nvIlBPVw6u9JiYzsANT EVaC6iEdvwpEG1ZFtzH08Z3/xjhlvDiPDnrfPDFyWZga+J4WJeL0US48vDfufdSJ lQM0NveF7cdkbKjLiizlYgq8CSOHjMz6OWHS0SFT7iR7GlC5ADpyeMmJLRInLgmE HZV3/FC1IYQzSHk1WNG/MP3RnzCA8NTj1ehE5mfDyQKBgQDYcTnpXRBd2sozAxzN dCVf1PtZ5WBmCmk74Ndr/o8wiHkQ7+E3+zee78c45ZQ+P9iEvaygqGTRetItecBA WxlOQ9z0CmAg1xI2hNIpImAR8Qohr1bEHuzQhdO8LkOuxtj6yP/FDxkKYQAI9C6v Lx6zo6o4XD5Et4wbUJVkwamrVwKBgQDM4JaxOPHcIVFuYAqxonQrv9bFB7Eew0Fj qdrQ/flsgz8FyZtThxF9b+7280y5XNJ4tNUKtDcat4cH3jeWfa467DLKjTKkWdJR iR4MGbsONXWoWPHPQ0GJZY/p3iqn9/OvBZh1k7NsXPmfAVRqMWjNws/WcWSb7Mgq dBN3A37EywKBgEg6UKcNhV6smnk3eq8dKTO3sUEoiGjE5KU0vO6u/j2l7TC3vCKg VMlXHtZf1n6Hc8uoOClMyIgXQngmfv965xD1GJDfvYB4BP3oiPFtJT4Xf9gJ2RyN bV2Qqz3K+o8ikFnwJVovVZ3fDNHwGnwfb1FnNnCkZ6sqzTh4RcJf1iz1AoGARwD7 GNaMc+cUKrWcXy3XJyZoT4a36tpuuhSu4kly/RmLaP0TGOKxvBBj+DAgAgnaY70A LKKCin7ajG6GQ2CxVnhvreU7jNwYWOu1fyoXuvfqG/sfat57QxvwwXOewvHbAWhm CzGyODcMx/+U+uy+zrjagQ5xeNyaDqSF7nRGpfsCgYAA7b/GlldodAJkZAiqejIc SArscos57stZfYyNICJq7Ye4qpzuWSrQKa5GtseSbvnz5yLzLuDe3Lr5HmYLypOc wC0JlKeTBMTObsGN0LixrXXRiuyQyCfmuvKu8WfKIlpZMUB5zgHYE8TAvm0BZjq9 +FUHwoRBoG3Qn04Uj9CCNg== -----END PRIVATE KEY----- ================================================ FILE: test/node/helpers/wrong-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIICATCCAWoCCQDEVSSDKkcTdjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 cyBQdHkgTHRkMB4XDTE0MDUxMTE2MzMxMVoXDTE0MDYxMDE2MzMxMVowRTELMAkG A1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0 IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyDMI VS2XSizZT8KeFYFQfKt7CcT5/Pkzw2BDJoMVLmrkfHdddjsTgswqHfzhO8Fmfg6B MxgsEz2aKi24hJxQFuQ1DGhyfKHnjxM5PqSLiOkQDKllnAOgqOBDXpca0jXypCk1 IVhMspM2ylrnBXps3nTBLJxFBkZSBov/JDkkL+cCAwEAATANBgkqhkiG9w0BAQUF AAOBgQA8k93U0VDIpQ8lpScxrCtEu5jLZgB1fw0fdCUtDHaaM1v+LWr1xfCmFKyT kUMcJl4e1pkcSNfXcI7LdNt8EJqMabOi2UpW1+VZJn206D0f3XmNSmZbk8oozGrl qg2wSTZYlZClCTpWO2Y+iYzojY8kmLaQ2xbTxBz1XlshC8HvsA== -----END CERTIFICATE----- ================================================ FILE: test/node/helpers/wrong-csr.pem ================================================ -----BEGIN CERTIFICATE REQUEST----- MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEh MB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB AQUAA4GNADCBiQKBgQDIMwhVLZdKLNlPwp4VgVB8q3sJxPn8+TPDYEMmgxUuauR8 d112OxOCzCod/OE7wWZ+DoEzGCwTPZoqLbiEnFAW5DUMaHJ8oeePEzk+pIuI6RAM qWWcA6Co4ENelxrSNfKkKTUhWEyykzbKWucFemzedMEsnEUGRlIGi/8kOSQv5wID AQABoAAwDQYJKoZIhvcNAQEFBQADgYEAFXqd8jhW+2hRvkRB1CCVBK5e6AQHq1rF s3B36O64hRHIr1KC+dWr8vv1t9Rkud+7E3ELHtxWCORIYpqQ2Ddldt4PP+MTNj2C qgwOpxM0VDxeeWml8fqx2uzfPhVduyHGm0yff2JS2KRVmnIPLTUuz/+udukIFDVO Sc4/W3qY7f8= -----END CERTIFICATE REQUEST----- ================================================ FILE: test/node/helpers/wrong-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDIMwhVLZdKLNlPwp4VgVB8q3sJxPn8+TPDYEMmgxUuauR8d112 OxOCzCod/OE7wWZ+DoEzGCwTPZoqLbiEnFAW5DUMaHJ8oeePEzk+pIuI6RAMqWWc A6Co4ENelxrSNfKkKTUhWEyykzbKWucFemzedMEsnEUGRlIGi/8kOSQv5wIDAQAB AoGBALOzszgaG2I2jb4dmJ7/G4s8tc2YJTlhS4iFgOEx6rJmur/KuXcmIiZXMzsF wftMZ76hMHH3saB3vEk+DxHh6bR6cW/I82Vxts9suz2fRnd2mh5JHI+opXE53LVn hJcQ4k6LJ9MNVxlHwCTrSuJvtikDOOrCARHRvYzFRL4wXvmpAkEA+DFzXGDg8PxX RFp6RLLbqaUT6YXNi+E5ERuumru+rgRj+OF/dxNK3d1lcIJZjqVMDAgOsZ66/bkh GfCzJPREUwJBAM5/HeHmTEM5K5B0X8b6XEHTgWFUNTu4K36Ee5ySd8RYI8zjQ9wS NM1nXnx12npL7DSkShz9xgnTe0f8YmQnc50CQQCgdE/RXCxwf6LnZNsBCOSsIzXh VgiRsxSSs+PI0zGuDNaY8yfV0ponH1fSSeMeLk0gxiDBwg2/tGzq+UrHzEdTAkB1 /U5O0K+MzbLlxIkhgdaLSlYoDdyo9e/sR7j12v8SMqaqIMWajtCa+VCU3yZqMM2T urgaXqr03GEZ3c0+mwhFAkAwWkczV1iwuedmWLKc36iQhoj+FRMUoxWe/fBixQls g0lDvwWiZ3M6hjCsBRckmt8eU2mUh79Odrj5fRWIwXaX -----END RSA PRIVATE KEY----- ================================================ FILE: test/node/keepaliveManager.ts ================================================ import { afterEach, beforeEach, describe, it } from 'node:test' import { assert } from 'chai' import { useFakeTimers, spy, stub } from 'sinon' import { type MqttClient } from 'src' import KeepaliveManager from '../../src/lib/KeepaliveManager' function mockedClient(keepalive: number) { return { options: { keepalive, }, onKeepaliveTimeout: () => {}, sendPing: () => {}, log: () => {}, } as unknown as MqttClient } describe('KeepaliveManager', () => { let clock: sinon.SinonFakeTimers beforeEach(() => { clock = useFakeTimers() }) afterEach(() => { clock.restore() }) it('should schedule and destroy', () => { const keepalive = 10 // seconds const client = mockedClient(keepalive) const manager = new KeepaliveManager(client, 'auto') const spySendPing = spy(client, 'sendPing') const spyTimeout = spy(client, 'onKeepaliveTimeout') const checksEvery = manager.intervalEvery assert.ok(manager['timerId'], 'timer should be created automatically') clock.tick(checksEvery) assert.equal( spySendPing.callCount, 0, 'should not send ping before keepalive seconds', ) clock.tick(checksEvery) assert.equal(spySendPing.callCount, 1, 'should send ping automatically') assert.equal(spyTimeout.callCount, 0, 'should not trigger timeout') clock.tick(checksEvery) assert.equal( spyTimeout.callCount, 1, 'should trigger keepalive timeout after 1.5*keepalive seconds', ) manager.destroy() assert.ok( !manager['timerId'], 'timer should not exists after destroy()', ) assert.ok( manager['destroyed'], 'timer should have `destroyed` set to true after destroy()', ) }) it('should reschedule', () => { const keepalive = 10 // seconds const manager = new KeepaliveManager(mockedClient(keepalive), 'auto') const checksEvery = manager.intervalEvery clock.tick(checksEvery) assert.equal( manager['counter'], 1, 'should increese counter on every check', ) manager.reschedule() assert.equal( manager['counter'], 0, 'should reset counter after reschedule', ) }) it('should validate keepalive', () => { const manager = new KeepaliveManager(mockedClient(1), 'auto') assert.throw( () => manager.setKeepalive(-1), 'Keepalive value must be an integer between 0 and 2147483647. Provided value is -1', ) assert.throw( () => manager.setKeepalive(2147483648), 'Keepalive value must be an integer between 0 and 2147483647. Provided value is 2147483648', ) manager.setKeepalive(10) assert.equal(manager.keepalive, 10000) assert.equal(manager.intervalEvery, 5000) }) it('should use provided Timer object', () => { const keepalive = 10 // seconds const customTimer = { set: stub().returns(123), clear: stub(), } const manager = new KeepaliveManager( mockedClient(keepalive), customTimer, ) assert.equal(manager['timer'], customTimer) assert.equal(customTimer.set.callCount, 1) assert.equal(manager['timerId'], 123) manager.destroy() assert.equal(customTimer.clear.called, true) }) }) ================================================ FILE: test/node/message-id-provider.ts ================================================ import { assert } from 'chai' import { describe, it } from 'node:test' import { DefaultMessageIdProvider, UniqueMessageIdProvider } from '../../src' describe('message id provider', () => { describe('default', () => { it('should return 1 once the internal counter reached limit', (t) => { const provider = new DefaultMessageIdProvider() provider['nextId'] = 65535 assert.equal(provider.allocate(), 65535) assert.equal(provider.allocate(), 1) }) it('should return 65535 for last message id once the internal counter reached limit', (t) => { 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', (t) => { const provider = new DefaultMessageIdProvider() assert.equal(provider.register(10), true) }) }) describe('unique', () => { it('should return 1, 2, 3.., when allocate', (t) => { const provider = new UniqueMessageIdProvider() assert.equal(provider.allocate(), 1) assert.equal(provider.allocate(), 2) assert.equal(provider.allocate(), 3) }) it('should skip registerd messageId', (t) => { 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', (t) => { 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', (t) => { 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', (t) => { 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', (t) => { 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', (t) => { 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) }) }) }) ================================================ FILE: test/node/mqtt.ts ================================================ import fs from 'fs' import path from 'path' import { describe, it } from 'node:test' import mqtt, { type IClientOptions } from '../../src' import 'should' describe('mqtt', () => { describe('#connect', () => { it('should return an MqttClient when connect is called with mqtt:/ url', function _test(t, done) { const c = mqtt.connect('mqtt://localhost:1883') c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should throw an error when called with no protocol specified', () => { ;(() => { mqtt.connect('foo.bar.com') }).should.throw('Missing protocol') }) 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 option set', function _test(t, done) { const c = mqtt.connect('mqtt://user:pass@localhost:1883') c.should.be.instanceOf(mqtt.MqttClient) c.options.should.have.property('username', 'user') c.options.should.have.property('password', 'pass') c.options.should.not.have.property('path') c.end((err) => done(err)) }) it('should return an MqttClient with path set when protocol is ws/wss', function _test(t, done) { const c = mqtt.connect('ws://localhost:1883/mqtt') c.should.be.instanceOf(mqtt.MqttClient) c.options.should.have.property('path', '/mqtt') c.options.should.have.property('unixSocket', false) c.end((err) => done(err)) }) it('should work with unix sockets', function _test(t, done) { const c = mqtt.connect('mqtt+unix:///tmp/mqtt.sock') c.should.be.instanceOf(mqtt.MqttClient) c.options.should.have.property('path', '/tmp/mqtt.sock') c.options.should.have.property('unixSocket', true) c.end((err) => done(err)) }) it('should not set `path` when parsing url', function _test(t, done) { const c = mqtt.connect('mqtt://[::1]') c.should.be.instanceOf(mqtt.MqttClient) c.options.should.not.have.property('path') c.options.should.have.property('host', '::1') c.end((err) => done(err)) }) it('should return an MqttClient with username and password options set', function _test(t, done) { const c = mqtt.connect('mqtt://user@localhost:1883') c.should.be.instanceOf(mqtt.MqttClient) c.options.should.not.have.property('path') c.options.should.have.property('username', 'user') c.end((err) => done(err)) }) it('should return an MqttClient with the clientid with random value', function _test(t, 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)) }) it('should return an MqttClient with the clientid with empty string', function _test(t, done) { const c = mqtt.connect('mqtt://user@localhost:1883?clientId=') 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(t, done) { const c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') c.should.be.instanceOf(mqtt.MqttClient) c.options.should.have.property('clientId', '123') c.end((err) => done(err)) }) it('should return an MqttClient when connect is called with tcp:/ url', function _test(t, done) { const c = mqtt.connect('tcp://localhost') c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return an MqttClient with correct host when called with a host and port', function _test(t, done) { const c = mqtt.connect('tcp://user:pass@localhost:1883') c.options.should.have.property('hostname', 'localhost') c.options.should.have.property('port', 1883) c.end((err) => done(err)) }) const sslOpts: IClientOptions = { keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')], } it('should return an MqttClient when connect is called with mqtts:/ url', function _test(t, done) { const c = mqtt.connect('mqtts://localhost', sslOpts) c.options.should.have.property('protocol', 'mqtts') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return an MqttClient when connect is called with ssl:/ url', function _test(t, done) { const c = mqtt.connect('ssl://localhost', sslOpts) c.options.should.have.property('protocol', 'ssl') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return an MqttClient when connect is called with ws:/ url', function _test(t, done) { const c = mqtt.connect('ws://localhost', sslOpts) c.options.should.have.property('protocol', 'ws') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return an MqttClient when connect is called with wss:/ url', function _test(t, done) { const c = mqtt.connect('wss://localhost', sslOpts) c.options.should.have.property('protocol', 'wss') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) const sslOpts2: IClientOptions = { 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 but no protocol specified', () => { // to do rewrite wrap function ;(() => { mqtt.connect(sslOpts2) }).should.throw('Missing secure protocol key') }) 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 as any).protocol = 'UNKNOWNPROTOCOL' mqtt.connect(sslOpts2) }).should.throw() }) it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function _test(t, done) { sslOpts2.protocol = 'mqtt' const c = mqtt.connect(sslOpts2) c.options.should.have.property('protocol', 'mqtts') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function _test(t, done) { sslOpts2.protocol = 'mqtts' const c = mqtt.connect(sslOpts2) c.options.should.have.property('protocol', 'mqtts') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function _test(t, done) { sslOpts2.protocol = 'ws' const c = mqtt.connect(sslOpts2) c.options.should.have.property('protocol', 'wss') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function _test(t, done) { sslOpts2.protocol = 'wss' const c = mqtt.connect(sslOpts2) c.options.should.have.property('protocol', 'wss') c.on('error', () => {}) c.should.be.instanceOf(mqtt.MqttClient) c.end((err) => done(err)) }) it('should return an MqttClient with the clientid with option of clientId as empty string', function _test(t, done) { const c = mqtt.connect('mqtt://localhost:1883', { clientId: '', }) 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(t, 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)) }) it('should return an MqttClient with the clientid with option of with specific clientId', function _test(t, 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)) }) it('should return an MqttClient with mqtts protocol when connect is called with mqtts:/ url and protocol (mqtts:) is specified in options', function _test(t, done) { const url = 'mqtts://localhost:1883' const parsedUrl = new URL(url) const protocol = parsedUrl.protocol as 'mqtt' | 'mqtts' const c = mqtt.connect(url, { protocol, }) c.should.be.instanceOf(mqtt.MqttClient) c.options.should.have.property('protocol', 'mqtts') c.end((err) => done(err)) }) }) }) ================================================ FILE: test/node/mqtt_store.ts ================================================ import { describe, it } from 'node:test' import { Store } from '../../src' import 'should' describe('store in lib/connect/index.js (webpack entry point)', () => { it('should create store', function test(t, done) { const store = new Store() store.should.be.instanceOf(Store) done() }) }) ================================================ FILE: test/node/secure_client.ts ================================================ import path from 'path' import fs from 'fs' import { assert } from 'chai' import { describe, it, after } from 'node:test' import mqtt from '../../src' import abstractClientTests from './abstract_client' import { MqttSecureServer, type MqttServerListener } from './server' import 'should' import getPorts from './helpers/port_list' const ports = getPorts(5) const port = ports.PORT 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 serverListener: MqttServerListener = (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', () => { const config = { protocol: 'mqtts', port, rejectUnauthorized: false } after(() => { // clean up and make sure the server is no longer listening... if (server.listening) { server.close() } process.exit(0) }) abstractClientTests(server, config, ports) describe('with secure parameters', () => { it('should validate successfully the CA', function _test(t, 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(t, 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(t, 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(t, 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(t, 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(t, done) { const hostname = 'localhost' server.removeAllListeners('secureConnection') // clear eventHandler server.once('secureConnection', (tlsSocket) => { // one time eventHandler assert.equal((tlsSocket as any).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)) }) }) }) }) ================================================ FILE: test/node/server.ts ================================================ import net from 'net' import tls, { type TlsOptions } from 'tls' import Connection from 'mqtt-connection' import { type Duplex } from 'stream' export type MqttServerListener = (client: Connection) => void /** * MqttServer * * @param {Function} listener - fired on client connection */ export class MqttServer extends net.Server { connectionList: Duplex[] constructor(listener: MqttServerListener) { super() this.connectionList = [] this.on('connection', (duplex) => { this.connectionList.push(duplex) const connection = new Connection(duplex, () => { this.emit('client', connection) }) }) if (listener) { this.on('client', listener) } } } /** * MqttServerNoWait (w/o waiting for initialization) * * @param {Function} listener - fired on client connection */ export class MqttServerNoWait extends net.Server { connectionList: Duplex[] constructor(listener: MqttServerListener) { super() this.connectionList = [] 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) } } } /** * MqttSecureServer * * @param {Object} opts - server options * @param {Function} listener */ export class MqttSecureServer extends tls.Server { connectionList: Duplex[] constructor(opts: TlsOptions, listener: MqttServerListener) { if (typeof opts === 'function') { listener = opts opts = {} } // sets a listener for the 'connection' event super(opts) this.connectionList = [] this.on('secureConnection', (socket) => { this.connectionList.push(socket) const connection = new Connection(socket, () => { this.emit('client', connection) }) }) if (listener) { this.on('client', listener) } } setupConnection(duplex: Duplex) { const connection = new Connection(duplex, () => { this.emit('client', connection) }) } } ================================================ FILE: test/node/server_helpers_for_client_tests.ts ================================================ import _debug from 'debug' import path from 'path' import fs from 'fs' import http from 'http' import WebSocket from 'ws' import MQTTConnection from 'mqtt-connection' import { type Server } from 'net' import { MqttServer, MqttSecureServer, type MqttServerListener } from './server' const KEY = path.join(__dirname, 'helpers', 'tls-key.pem') const CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') const debug = _debug('mqttjs:server_helpers_for_client_tests') /** * This will build the client for the server to use during testing, and set up the * server side client based on mqtt-connection for handling MQTT messages. * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' * @param {Function} handler - event handler */ export default function serverBuilder( protocol: string, handler?: MqttServerListener, ): Server { const sockets = [] const defaultHandler: MqttServerListener = (serverClient) => { sockets.push(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.protocolVersion >= 4) { connack['sessionPresent'] = false } 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') const index = sockets.findIndex((s) => s === serverClient) if (index !== -1) { sockets.splice(index, 1) } }) } if (!handler) { handler = defaultHandler } let mqttServer = null if (protocol === 'mqtt') { mqttServer = new MqttServer(handler) } if (protocol === 'mqtts') { mqttServer = 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) mqttServer = httpServer } const originalClose = mqttServer.close mqttServer.close = (cb) => { sockets.forEach((socket) => { socket.destroy() }) originalClose.call(mqttServer, cb) } return mqttServer } ================================================ FILE: test/node/socks.ts ================================================ import assert from 'assert' import { type AddressInfo, createServer, type Server, type Socket } from 'net' import { describe, it, mock, afterEach, beforeEach } from 'node:test' import openSocks from 'src/lib/connect/socks' type State5 = 'new' | 'id' | 'connect' class MockServer5 { readonly connect: Promise responseID = Buffer.from([0x05, 0x00]) responseREQUEST = Buffer.from([ 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, ]) private server = createServer() private onConnect: (socket: Socket) => void private onError: (err: any) => void private socket?: Socket private state: State5 = 'new' private destination?: [string, number] constructor() { this.connect = new Promise((resolve, reject) => { this.onConnect = resolve this.onError = reject }) } start(): Promise { this.server.listen(undefined, 'localhost') this.server.on('connection', this.onConnection) return new Promise((r) => { this.server.once('listening', () => r(this.port())) }) } port(): number { return (this.server.address() as AddressInfo).port } destroy() { this.server.close() this.socket?.end() this.socket?.destroy() } destinationAddress(): string | undefined { return this.destination?.[0] } destinationPort(): number | undefined { return this.destination?.[1] } private onConnection = (socket: Socket) => { if (this.socket) { socket.destroy() return this.onError(new Error('double connect to SOCKS5 server')) } this.socket = socket socket.on('data', this.onData) } private onData = (chunk: Buffer) => { switch (this.state) { case 'new': { const [ver, nmethods] = chunk if ( ver !== 0x05 || nmethods === 0 || chunk.length !== nmethods + 2 ) { return this.onError(new Error('bad ID packet')) } if (chunk.subarray(2, 2 + nmethods).indexOf(0x00) === -1) { return this.onError(new Error('no supported METHOD')) } this.socket?.write?.(this.responseID) this.state = 'id' break } case 'id': this.destination = this.parseConnect(chunk) if (this.destination === undefined) { return this.onError(new Error('bad REQUEST packet')) } this.socket?.write(this.responseREQUEST) this.state = 'connect' this.socket.off('data', this.onData) this.onConnect(this.socket) break } } private parseConnect(buf: Buffer): [string, number] | undefined { const [ver, cmd, rsv, atyp] = buf if (ver !== 0x05 || cmd !== 0x01 || rsv !== 0x00) return undefined const port = (buf[buf.length - 2] << 8) | buf[buf.length - 1] switch (atyp) { case 0x01: if (buf.length !== 10) return undefined return [buf.subarray(4, 8).join('.'), port] case 0x03: if (buf.length !== 7 + buf[4]) return undefined return [buf.subarray(5, 5 + buf[4]).toString('ascii'), port] default: return undefined } } } describe('SOCKS layer', { timeout: 1000 }, () => { let server5!: MockServer5 let server4: Server | undefined beforeEach(() => { server5 = new MockServer5() }) afterEach(() => { server5.destroy() server4?.close() }) it('should resolve hostnames locally for socks5', async () => { const port = await server5.start() const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks5://localhost:${port}`, { lookup, }, ) await server5.connect stream.destroy() await new Promise((r) => { stream.once('close', r) }) assert.strictEqual(lookup.mock.callCount(), 1) assert.strictEqual(lookup.mock.calls[0].arguments[0], 'foo.bar') assert.strictEqual(server5.destinationAddress(), '1.2.3.4') assert.strictEqual(server5.destinationPort(), 1883) }) it('should resolve hostnames remotely for socks5h', async () => { const port = await server5.start() const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks5h://localhost:${port}`, { lookup, }, ) await server5.connect stream.destroy() await new Promise((r) => { stream.once('close', r) }) assert.strictEqual(lookup.mock.callCount(), 0) assert.strictEqual(server5.destinationAddress(), 'foo.bar') assert.strictEqual(server5.destinationPort(), 1883) }) it('errors during name resolution should be emitted on stream', async () => { const ERROR = new Error() const lookup = mock.fn((address) => Promise.reject(ERROR)) const stream = openSocks('foo.bar', 1883, 'socks5://localhost:6666', { lookup, }) const error = await new Promise((r) => { stream.once('error', r) }) assert.strictEqual(error, ERROR) }) it('errors during SOCKS connect should be emitted on stream', async () => { const port = await server5.start() server5.responseID = Buffer.from([0x00, 0x00]) const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks5://localhost:${port}`, { lookup, }, ) const err = await new Promise((r) => { stream.once('error', r) }) stream.destroy() assert(err instanceof Error) }) it('data flows through the stream after SOCKS has connected', async () => { const port = await server5.start() const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks5://localhost:${port}`, { lookup, }, ) const socket = await server5.connect socket.once('data', (chunk) => socket.write(`${chunk.toString()} pong`)) const response = await new Promise((resolve, reject) => { stream.once('error', (err) => { reject(err) }) stream.once('data', (chunk) => { resolve(chunk.toString()) }) stream.write('ping') }) server5.destroy() stream.destroy() assert.strictEqual(response, 'ping pong') }) it('data written to the stream is buffered until SOCKS has connected', async () => { const port = await server5.start() let startNameResolution!: () => undefined const resolutionPromise = new Promise((r) => { startNameResolution = r as () => undefined }) const lookup = mock.fn((_: string) => resolutionPromise.then(() => ({ address: '1.2.3.4', })), ) const stream = openSocks( 'foo.bar', 1883, `socks5://localhost:${port}`, { lookup, }, ) stream.write('ping') startNameResolution() const socket = await server5.connect socket.once('data', (chunk) => socket.write(`${chunk.toString()} pong`)) const response = await new Promise((resolve, reject) => { stream.once('error', (err) => { reject(err) }) stream.once('data', (chunk) => { resolve(chunk.toString()) }) }) server5.destroy() stream.destroy() assert.strictEqual(response, 'ping pong') }) it('closing the stream closes the connection', async () => { const port = await server5.start() const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks5://localhost:${port}`, { lookup, }, ) const socket = await server5.connect stream.destroy() await new Promise((r) => { socket.once('close', r) }) }) it('closing the connection closes the stream', async () => { const port = await server5.start() const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks5://localhost:${port}`, { lookup, }, ) const socket = await server5.connect socket.destroy() await new Promise((r) => { stream.once('close', r) }) }) it('an invalid protocol errors the stream', async () => { const port = await server5.start() const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks('foo.bar', 1883, `socks://localhost:${port}`, { lookup, }) const error = await new Promise((r) => { stream.once('error', r) }) assert(error instanceof Error) }) it('an invalid URL errors the stream', async () => { const port = await server5.start() const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks('foo.bar', 1883, `socks:localhost:${port}`, { lookup, }) const error = await new Promise((r) => { stream.once('error', r) }) assert(error instanceof Error) }) it('should resolve hostnames locally for socks4', async () => { let onConnect!: (socket: Socket) => void const connect = new Promise((r) => { onConnect = mock.fn((socket: Socket) => { socket.destroy() r(socket) }) }) server4 = await new Promise((resolve, reject) => { const server = createServer(onConnect) server.on('listening', () => resolve(server)) server.on('error', reject) server.listen() }) const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks4://localhost:${(server4.address() as AddressInfo).port}`, { lookup, }, ) const socket = await connect socket.destroy() stream.destroy() assert.strictEqual(lookup.mock.callCount(), 1) assert.strictEqual(lookup.mock.calls[0].arguments[0], 'foo.bar') }) it('should resolve hostnames remotely for socks4a', async () => { let onConnect!: (socket: Socket) => void const connect = new Promise((r) => { onConnect = mock.fn((socket: Socket) => { socket.destroy() r(socket) }) }) server4 = await new Promise((resolve, reject) => { const server = createServer(onConnect) server.on('listening', () => resolve(server)) server.on('error', reject) server.listen() }) const lookup = mock.fn((_: string) => Promise.resolve({ address: '1.2.3.4' }), ) const stream = openSocks( 'foo.bar', 1883, `socks4a://localhost:${(server4.address() as AddressInfo).port}`, { lookup, }, ) const socket = await connect socket.destroy() stream.destroy() assert.strictEqual(lookup.mock.callCount(), 0) }) }) ================================================ FILE: test/node/store.ts ================================================ import { describe } from 'node:test' import Store from '../../src/lib/store' import abstractTest from './abstract_store' describe('in-memory store', () => { abstractTest(function test(done) { done(null, new Store()) }) }) ================================================ FILE: test/node/unique_message_id_provider_client.ts ================================================ import { describe, after } from 'node:test' import abstractClientTests from './abstract_client' import serverBuilder from './server_helpers_for_client_tests' import { UniqueMessageIdProvider, type IClientOptions } from '../../src' import getPorts from './helpers/port_list' const ports = getPorts(3) describe('UniqueMessageIdProviderMqttClient', () => { const server = serverBuilder('mqtt') const config: IClientOptions = { protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider(), } server.listen(ports.PORTAND400) after(() => { // clean up and make sure the server is no longer listening... if (server.listening) { server.close() } process.exit(0) }) abstractClientTests(server, config, ports) }) ================================================ FILE: test/node/util.ts ================================================ import { Transform } from 'readable-stream' const testStream = () => { return new Transform({ transform(buf, enc, cb) { setImmediate(() => { this.push(buf) cb() }) }, }) } export default testStream ================================================ FILE: test/node/websocket_client.ts ================================================ import http from 'http' import WebSocket from 'ws' import MQTTConnection from 'mqtt-connection' import assert from 'assert' import { after, describe, it } from 'node:test' import abstractClientTests from './abstract_client' import getPorts from './helpers/port_list' import { MqttServerNoWait } from './server' import mqtt, { type IClientOptions } from '../../src' const ports = getPorts(4) const port = 9999 const httpServer = http.createServer() let lastProcotols = new Set() function attachWebsocketServer(httpServer2) { const webSocketServer = new WebSocket.Server({ server: httpServer2, handleProtocols: (protocols: Set, request: any) => { lastProcotols = protocols return [...protocols][0] }, 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', (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', () => { const baseConfig: IClientOptions = { protocol: 'ws', port } function makeOptions(custom?: IClientOptions): IClientOptions { return { ...baseConfig, ...(custom || {}) } } after(() => { // clean up and make sure the server is no longer listening... if (httpServer.listening) { httpServer.close() } process.exit(0) }) it('should use mqtt as the protocol by default', function _test(t, 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(t, done) { const baseUrl = 'ws://localhost:9999/mqtt' const sig = '?AUTH=token' const expected = baseUrl + sig let actual: string 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', () => { // `url` is set in `connect/ws.ts` `streamBuilder` assert.equal((client.stream as any).url, expected) assert.equal(actual, expected) client.end(true, (err) => done(err)) }) }) it('should be able to create custom Websocket instance', function _test(t, done) { const baseUrl = 'ws://localhost:9999/mqtt' let urlInCallback: string const opts = makeOptions({ path: '/mqtt', createWebsocket( url: string, websocketSubProtocols: string[], options: IClientOptions, ) { urlInCallback = url assert.equal(url, baseUrl) const subProtocols = [ websocketSubProtocols[0], 'myCustomSubprotocol', ] return new WebSocket(url, subProtocols) }, }) const client = mqtt.connect(opts) client.on('connect', () => { assert.equal((client.stream as any).url, urlInCallback) assert.equal(baseUrl, urlInCallback) assert.equal('myCustomSubprotocol', [...lastProcotols][1]) client.end(true, (err) => done(err)) }) }) it('should use mqttv3.1 as the protocol if using v3.1', function _test(t, 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', { timeout: 15000, }, function _test(t, done) { let serverPort42Connected = false const handler = (serverClient) => { serverClient.on('connect', (packet) => { serverClient.connack({ returnCode: 0 }) }) } const actualURL41 = `wss://localhost:${ports.PORTAND41}/` const actualURL42 = `ws://localhost:${ports.PORTAND42}/` 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 as any).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 as any).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(), ports) }) ================================================ FILE: tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "exclude": ["node_modules", "test", "dist", "build", "example.ts"], } ================================================ FILE: tsconfig.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "lib": ["es2023", "DOM"], "pretty": true, "declaration": true, "newLine": "LF", "preserveConstEnums": false, "isolatedModules": false, "sourceMap": true, "allowJs": false, "checkJs": false, "forceConsistentCasingInFileNames": true, "experimentalDecorators": false, "emitDecoratorMetadata": false, "noErrorTruncation": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "alwaysStrict": true, "removeComments": true, "allowSyntheticDefaultImports": true, "outDir": "./build", "baseUrl": "./", "incremental": true, "preserveSymlinks": true, "resolveJsonModule": true, "strict": false, "strictBindCallApply": false, "strictBuiltinIteratorReturn": false, "strictFunctionTypes": false, "strictNullChecks": false, "strictPropertyInitialization": false, "noImplicitAny": false, "noImplicitThis": false, "useUnknownInCatchVariables": false }, "include": [ "src", "test", "example.ts" ], "exclude": [ "dist", "test/browser" ] } ================================================ FILE: web-test-runner.config.mjs ================================================ // Docs: https://modern-web.dev/docs/test-runner/cli-and-configuration/ import { playwrightLauncher } from '@web/test-runner-playwright'; import { start } from 'aedes-cli' const wsPort = 4000 const wssPort = 4443 await start({ protos: ['tcp', 'tls', 'ws', 'wss'], wsPort, wssPort, key: './test/browser/certs/server-key.pem', cert: './test/browser/certs/server-cert.pem', verbose: true, stats: false }) console.log('Broker setup done') /** @type { import('@web/test-runner-playwright').PlaywrightLauncher[] } */ const browsers = ['chromium', 'firefox', 'webkit'].map(product => playwrightLauncher({ product, createBrowserContext: ({ browser, config }) => { // ignore HTTPS errors const context = browser.newContext({ ignoreHTTPSErrors: true }) return context }, launchOptions: { headless: true, devtools: false } })) /** * @type { import('@web/test-runner').TestRunnerConfig } */ export default { // https://modern-web.dev/docs/test-runner/browser-launchers/playwright/#testing-multiple-browsers // Requires: @web/test-runner-playwright browsers, playwright: true, concurrency: 1, files: ['./test/browser/test.js'], nodeResolve: true, testFramework: { config: { timeout: '10000', }, }, // manual: true, // open: true, // rootDir: path.resolve(__dirname) // http2: true, // protocol: 'https:', // sslCert: './test/certs/server-cert.pem', // sslKey: './test/certs/server-key.pem', testRunnerHtml: (testFrameworkImport) => ` ` };