├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .travis.yml ├── README-PAHO.md ├── README.md ├── about.html ├── edl-v10 ├── epl-v10 ├── index.js ├── notice.html ├── package.json ├── src ├── Client.js ├── ClientImplementation.js ├── ConnectMessage.js ├── Message.js ├── Pinger.js ├── PublishMessage.js ├── WireMessage.js ├── constants.js ├── header.txt ├── oldtests │ ├── BasicTest-spec.js │ ├── hacks.js │ ├── interops-spec.js │ ├── live-take-over-spec.js │ └── send-receive-spec.js └── util.js ├── test ├── .support.js ├── Client.test.js ├── Message.test.js ├── integration.test.js └── util.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | quote_type = single 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/oldtests 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "ecmaFeatures": { 5 | }, 6 | 7 | "env": { 8 | "es6": true, 9 | "jasmine": true 10 | }, 11 | 12 | "plugins": [ 13 | ], 14 | 15 | // Map from global var to bool specifying if it can be redefined 16 | "globals": { 17 | "__DEV__": true, 18 | "__dirname": false, 19 | "__fbBatchedBridgeConfig": false, 20 | "alert": false, 21 | "cancelAnimationFrame": false, 22 | "cancelIdleCallback": false, 23 | "Class": true, 24 | "clearImmediate": true, 25 | "clearInterval": false, 26 | "clearTimeout": false, 27 | "console": false, 28 | "document": false, 29 | "escape": false, 30 | "Event": false, 31 | "EventTarget": false, 32 | "exports": false, 33 | "fetch": false, 34 | "FormData": false, 35 | "global": false, 36 | "jest": false, 37 | "Map": true, 38 | "module": false, 39 | "navigator": false, 40 | "process": false, 41 | "Promise": true, 42 | "requestAnimationFrame": true, 43 | "requestIdleCallback": true, 44 | "require": false, 45 | "Set": true, 46 | "setImmediate": true, 47 | "setInterval": false, 48 | "setTimeout": false, 49 | "test": true, 50 | "window": false, 51 | "XMLHttpRequest": false, 52 | "WebSocket": true, 53 | "pit": false, 54 | 55 | // Flow global types. 56 | "ReactComponent": false, 57 | "ReactClass": false, 58 | "ReactElement": false, 59 | "ReactPropsCheckType": false, 60 | "ReactPropsChainableTypeChecker": false, 61 | "ReactPropTypes": false, 62 | "SyntheticEvent": false, 63 | "$Either": false, 64 | "$All": false, 65 | "$ArrayBufferView": false, 66 | "$Tuple": false, 67 | "$Supertype": false, 68 | "$Subtype": false, 69 | "$Shape": false, 70 | "$Diff": false, 71 | "$Keys": false, 72 | "$Enum": false, 73 | "$Exports": false, 74 | "$FlowIssue": false, 75 | "$FlowFixMe": false, 76 | "$FixMe": false 77 | }, 78 | 79 | "rules": { 80 | "comma-dangle": 0, // disallow trailing commas in object literals 81 | "no-cond-assign": 1, // disallow assignment in conditional expressions 82 | "no-console": 0, // disallow use of console (off by default in the node environment) 83 | "no-const-assign": 2, // disallow assignment to const-declared variables 84 | "no-constant-condition": 0, // disallow use of constant expressions in conditions 85 | "no-control-regex": 1, // disallow control characters in regular expressions 86 | "no-debugger": 1, // disallow use of debugger 87 | "no-dupe-keys": 1, // disallow duplicate keys when creating object literals 88 | "no-empty": 0, // disallow empty statements 89 | "no-ex-assign": 1, // disallow assigning to the exception in a catch block 90 | "no-extra-boolean-cast": 1, // disallow double-negation boolean casts in a boolean context 91 | "no-extra-parens": 0, // disallow unnecessary parentheses (off by default) 92 | "no-extra-semi": 1, // disallow unnecessary semicolons 93 | "no-func-assign": 1, // disallow overwriting functions written as function declarations 94 | "no-inner-declarations": 0, // disallow function or variable declarations in nested blocks 95 | "no-invalid-regexp": 1, // disallow invalid regular expression strings in the RegExp constructor 96 | "no-negated-in-lhs": 1, // disallow negation of the left operand of an in expression 97 | "no-obj-calls": 1, // disallow the use of object properties of the global object (Math and JSON) as functions 98 | "no-regex-spaces": 1, // disallow multiple spaces in a regular expression literal 99 | "no-reserved-keys": 0, // disallow reserved words being used as object literal keys (off by default) 100 | "no-sparse-arrays": 1, // disallow sparse arrays 101 | "no-unreachable": 1, // disallow unreachable statements after a return, throw, continue, or break statement 102 | "use-isnan": 1, // disallow comparisons with the value NaN 103 | "valid-jsdoc": 0, // Ensure JSDoc comments are valid (off by default) 104 | "valid-typeof": 1, // Ensure that the results of typeof are compared against a valid string 105 | 106 | // Best Practices 107 | // These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns. 108 | 109 | "block-scoped-var": 0, // treat var statements as if they were block scoped (off by default) 110 | "complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default) 111 | "consistent-return": 0, // require return statements to either always or never specify values 112 | "curly": 1, // specify curly brace conventions for all control statements 113 | "default-case": 0, // require default case in switch statements (off by default) 114 | "dot-notation": 1, // encourages use of dot notation whenever possible 115 | "eqeqeq": [1, "allow-null"], // require the use of === and !== 116 | "guard-for-in": 0, // make sure for-in loops have an if statement (off by default) 117 | "no-alert": 1, // disallow the use of alert, confirm, and prompt 118 | "no-caller": 1, // disallow use of arguments.caller or arguments.callee 119 | "no-div-regex": 1, // disallow division operators explicitly at beginning of regular expression (off by default) 120 | "no-else-return": 0, // disallow else after a return in an if (off by default) 121 | "no-eq-null": 0, // disallow comparisons to null without a type-checking operator (off by default) 122 | "no-eval": 1, // disallow use of eval() 123 | "no-extend-native": 1, // disallow adding to native types 124 | "no-extra-bind": 1, // disallow unnecessary function binding 125 | "no-fallthrough": 1, // disallow fallthrough of case statements 126 | "no-floating-decimal": 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default) 127 | "no-implied-eval": 1, // disallow use of eval()-like methods 128 | "no-labels": 1, // disallow use of labeled statements 129 | "no-iterator": 1, // disallow usage of __iterator__ property 130 | "no-lone-blocks": 1, // disallow unnecessary nested blocks 131 | "no-loop-func": 0, // disallow creation of functions within loops 132 | "no-multi-str": 0, // disallow use of multiline strings 133 | "no-native-reassign": 0, // disallow reassignments of native objects 134 | "no-new": 1, // disallow use of new operator when not part of the assignment or comparison 135 | "no-new-func": 1, // disallow use of new operator for Function object 136 | "no-new-wrappers": 1, // disallows creating new instances of String,Number, and Boolean 137 | "no-octal": 1, // disallow use of octal literals 138 | "no-octal-escape": 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; 139 | "no-proto": 1, // disallow usage of __proto__ property 140 | "no-redeclare": 0, // disallow declaring the same variable more then once 141 | "no-return-assign": 1, // disallow use of assignment in return statement 142 | "no-script-url": 1, // disallow use of javascript: urls. 143 | "no-self-compare": 1, // disallow comparisons where both sides are exactly the same (off by default) 144 | "no-sequences": 1, // disallow use of comma operator 145 | "no-unused-expressions": 0, // disallow usage of expressions in statement position 146 | "no-void": 1, // disallow use of void operator (off by default) 147 | "no-warning-comments": 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default) 148 | "no-with": 1, // disallow use of the with statement 149 | "radix": 1, // require use of the second argument for parseInt() (off by default) 150 | "semi-spacing": 1, // require a space after a semi-colon 151 | "vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default) 152 | "wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default) 153 | "yoda": 1, // require or disallow Yoda conditions 154 | 155 | // Variables 156 | // These rules have to do with variable declarations. 157 | 158 | "no-catch-shadow": 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) 159 | "no-delete-var": 1, // disallow deletion of variables 160 | "no-label-var": 1, // disallow labels that share a name with a variable 161 | "no-shadow": 1, // disallow declaration of variables already declared in the outer scope 162 | "no-shadow-restricted-names": 1, // disallow shadowing of names such as arguments 163 | "no-undef": 2, // disallow use of undeclared variables unless mentioned in a /*global */ block 164 | "no-undefined": 0, // disallow use of undefined variable (off by default) 165 | "no-undef-init": 1, // disallow use of undefined when initializing variables 166 | "no-unused-vars": [1, {"vars": "all", "args": "none"}], // disallow declaration of variables that are not used in the code 167 | "no-use-before-define": 0, // disallow use of variables before they are defined 168 | 169 | // Node.js 170 | // These rules are specific to JavaScript running on Node.js. 171 | 172 | "handle-callback-err": 1, // enforces error handling in callbacks (off by default) (on by default in the node environment) 173 | "no-mixed-requires": 1, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment) 174 | "no-new-require": 1, // disallow use of new operator with the require function (off by default) (on by default in the node environment) 175 | "no-path-concat": 1, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment) 176 | "no-process-exit": 0, // disallow process.exit() (on by default in the node environment) 177 | "no-restricted-modules": 1, // restrict usage of specified node modules (off by default) 178 | "no-sync": 0, // disallow use of synchronous methods (off by default) 179 | 180 | // Stylistic Issues 181 | // These rules are purely matters of style and are quite subjective. 182 | 183 | "key-spacing": 0, 184 | "keyword-spacing": 1, // enforce spacing before and after keywords 185 | "comma-spacing": 0, 186 | "no-multi-spaces": 0, 187 | "brace-style": 0, // enforce one true brace style (off by default) 188 | "camelcase": 0, // require camel case names 189 | "consistent-this": [1, "self"], // enforces consistent naming when capturing the current execution context (off by default) 190 | "eol-last": 1, // enforce newline at the end of file, with no multiple empty lines 191 | "func-names": 0, // require function expressions to have a name (off by default) 192 | "func-style": 0, // enforces use of function declarations or expressions (off by default) 193 | "new-cap": 0, // require a capital letter for constructors 194 | "new-parens": 1, // disallow the omission of parentheses when invoking a constructor with no arguments 195 | "no-nested-ternary": 0, // disallow nested ternary expressions (off by default) 196 | "no-array-constructor": 1, // disallow use of the Array constructor 197 | "no-lonely-if": 0, // disallow if as the only statement in an else block (off by default) 198 | "no-new-object": 1, // disallow use of the Object constructor 199 | "no-spaced-func": 1, // disallow space between function identifier and application 200 | "no-ternary": 0, // disallow the use of ternary operators (off by default) 201 | "no-trailing-spaces": 1, // disallow trailing whitespace at the end of lines 202 | "no-underscore-dangle": 0, // disallow dangling underscores in identifiers 203 | "no-mixed-spaces-and-tabs": 1, // disallow mixed spaces and tabs for indentation 204 | "quotes": [1, "single", "avoid-escape"], // specify whether double or single quotes should be used 205 | "quote-props": 0, // require quotes around object literal property names (off by default) 206 | "semi": 1, // require or disallow use of semicolons instead of ASI 207 | "sort-vars": 0, // sort variables within the same declaration block (off by default) 208 | "space-in-brackets": 0, // require or disallow spaces inside brackets (off by default) 209 | "space-in-parens": 0, // require or disallow spaces inside parentheses (off by default) 210 | "space-infix-ops": 1, // require spaces around operators 211 | "space-unary-ops": [1, { "words": true, "nonwords": false }], // require or disallow spaces before/after unary operators (words on by default, nonwords off by default) 212 | "max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default) 213 | "one-var": 0, // allow just one var statement per function (off by default) 214 | "wrap-regex": 0 // require regex literals to be wrapped in parentheses (off by default) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | unsafe.enable_getters_and_setters=true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npm 3 | persistence 4 | tmp 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: yarn 4 | services: mongodb 5 | node_js: 6 | - stable 7 | -------------------------------------------------------------------------------- /README-PAHO.md: -------------------------------------------------------------------------------- 1 | # Eclipse Paho JavaScript client 2 | 3 | The Paho JavaScript Client is an MQTT browser-based client library written in Javascript that uses WebSockets to connect to an MQTT Broker. 4 | 5 | ## Project description: 6 | 7 | The Paho project has been created to provide reliable open-source implementations of open and standard messaging protocols aimed at new, existing, and emerging applications for Machine-to-Machine (M2M) and Internet of Things (IoT). 8 | Paho reflects the inherent physical and cost constraints of device connectivity. Its objectives include effective levels of decoupling between devices and applications, designed to keep markets open and encourage the rapid growth of scalable Web and Enterprise middleware and applications. 9 | 10 | ## Links 11 | 12 | - Project Website: [https://www.eclipse.org/paho](https://www.eclipse.org/paho) 13 | - Eclipse Project Information: [https://projects.eclipse.org/projects/iot.paho](https://projects.eclipse.org/projects/iot.paho) 14 | - Paho Javascript Client Page: [https://eclipse.org/paho/clients/js/](https://eclipse.org/paho/clients/js) 15 | - GitHub: [https://github.com/eclipse/paho.mqtt.javascript](https://github.com/eclipse/paho.mqtt.javascript) 16 | - Twitter: [@eclipsepaho](https://twitter.com/eclipsepaho) 17 | - Issues: [github.com/eclipse/paho.mqtt.javascript/issues](https://github.com/eclipse/paho.mqtt.javascript/issues) 18 | - Mailing-list: [https://dev.eclipse.org/mailman/listinfo/paho-dev](https://dev.eclipse.org/mailman/listinfo/paho-dev 19 | 20 | 21 | ## Using the Paho Javascript Client 22 | 23 | 24 | ### Downloading 25 | 26 | A zip file containing the full and a minified version the Javascript client can be downloaded from the [Paho downloads page](https://projects.eclipse.org/projects/technology.paho/downloads) 27 | 28 | Alternatively the Javascript client can be downloaded directly from the projects git repository: [https://raw.githubusercontent.com/eclipse/paho.mqtt.javascript/master/src/mqttws31.js](https://raw.githubusercontent.com/eclipse/paho.mqtt.javascript/master/src/mqttws31.js). 29 | 30 | Please **do not** link directly to this url from your application. 31 | 32 | ### Building from source 33 | 34 | There are two active branches on the Paho Java git repository, ```master``` which is used to produce stable releases, and ```develop``` where active development is carried out. By default cloning the git repository will download the ```master``` branch, to build from develop make sure you switch to the remote branch: ```git checkout -b develop remotes/origin/develop``` 35 | 36 | The project contains a maven based build that produces a minified version of the client, runs unit tests and generates it's documentation. 37 | 38 | To run the build: 39 | 40 | ``` 41 | $ mvn 42 | ``` 43 | 44 | The output of the build is copied to the ```target``` directory. 45 | 46 | ### Tests 47 | 48 | The client uses the [Jasmine](http://jasmine.github.io/) test framework. The tests for the client are in: 49 | 50 | ``` 51 | src/tests 52 | ``` 53 | 54 | To run the tests with maven, use the following command: 55 | ``` 56 | $ mvn test -Dtest.server=iot.eclipse.com -Dtest.server.port=80 -Dtest.server.path=/ws 57 | ``` 58 | The parameters passed in should be modified to match the broker instance being tested against. 59 | 60 | ### Documentation 61 | 62 | Reference documentation is online at: [http://www.eclipse.org/paho/files/jsdoc/index.html](http://www.eclipse.org/paho/files/jsdoc/index.html) 63 | 64 | ### Compatibility 65 | 66 | The client should work in any browser fully supporting WebSockets, [http://caniuse.com/websockets](http://caniuse.com/websockets) lists browser compatibility. 67 | 68 | ## Getting Started 69 | 70 | The included code below is a very basic sample that connects to a server using WebSockets and subscribes to the topic ```World```, once subscribed, it then publishes the message ```Hello``` to that topic. Any messages that come into the subscribed topic will be printed to the Javascript console. 71 | 72 | This requires the use of a broker that supports WebSockets natively, or the use of a gateway that can forward between WebSockets and TCP. 73 | 74 | ```JS 75 | // Create a client instance 76 | client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId"); 77 | 78 | // set callback handlers 79 | client.onConnectionLost = onConnectionLost; 80 | client.onMessageArrived = onMessageArrived; 81 | 82 | // connect the client 83 | client.connect({onSuccess:onConnect}); 84 | 85 | 86 | // called when the client connects 87 | function onConnect() { 88 | // Once a connection has been made, make a subscription and send a message. 89 | console.log("onConnect"); 90 | client.subscribe("World"); 91 | message = new Paho.MQTT.Message("Hello"); 92 | message.destinationName = "World"; 93 | client.send(message); 94 | } 95 | 96 | // called when the client loses its connection 97 | function onConnectionLost(responseObject) { 98 | if (responseObject.errorCode !== 0) { 99 | console.log("onConnectionLost:"+responseObject.errorMessage); 100 | } 101 | } 102 | 103 | // called when a message arrives 104 | function onMessageArrived(message) { 105 | console.log("onMessageArrived:"+message.payloadString); 106 | } 107 | ``` 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eclipse Paho JavaScript client forked for React Native 2 | [![npm version](https://badge.fury.io/js/react-native-paho-mqtt.svg)](https://badge.fury.io/js/react-native-paho-mqtt) [![Build Status](https://travis-ci.org/rh389/react-native-paho-mqtt.svg?branch=master)](https://travis-ci.org/rh389/react-native-paho-mqtt) 3 | 4 | A fork of [paho-client](https://www.npmjs.com/package/paho-client), this project exists to provide an ES6-ready, Promise-based, react-native compatible version of the Eclipse Paho client 5 | 6 | ### Compatibility note 7 | 8 | Due to a React Native binary websocket bug, this library will *not work* with React Native 0.46.0 on Android, but should be ok on other platforms. RN 0.47 and RN<=0.45 are fine on all platforms as far as I know. 9 | 10 | ### Documentation 11 | 12 | Reference documentation (for the base Paho client) is online at: [http://www.eclipse.org/paho/files/jsdoc/index.html](http://www.eclipse.org/paho/files/jsdoc/index.html) 13 | 14 | ## Getting Started 15 | 16 | The included code below is a very basic sample that connects to a server using WebSockets and subscribes to the topic ```World```, once subscribed, it then publishes the message ```Hello``` to that topic. Any messages that come into the subscribed topic will be printed to the Javascript console. 17 | 18 | This requires the use of a broker that supports WebSockets natively, or the use of a gateway that can forward between WebSockets and TCP. 19 | 20 | ```js 21 | import { Client, Message } from 'react-native-paho-mqtt'; 22 | 23 | //Set up an in-memory alternative to global localStorage 24 | const myStorage = { 25 | setItem: (key, item) => { 26 | myStorage[key] = item; 27 | }, 28 | getItem: (key) => myStorage[key], 29 | removeItem: (key) => { 30 | delete myStorage[key]; 31 | }, 32 | }; 33 | 34 | // Create a client instance 35 | const client = new Client({ uri: 'ws://iot.eclipse.org:80/ws', clientId: 'clientId', storage: myStorage }); 36 | 37 | // set event handlers 38 | client.on('connectionLost', (responseObject) => { 39 | if (responseObject.errorCode !== 0) { 40 | console.log(responseObject.errorMessage); 41 | } 42 | }); 43 | client.on('messageReceived', (message) => { 44 | console.log(message.payloadString); 45 | }); 46 | 47 | // connect the client 48 | client.connect() 49 | .then(() => { 50 | // Once a connection has been made, make a subscription and send a message. 51 | console.log('onConnect'); 52 | return client.subscribe('World'); 53 | }) 54 | .then(() => { 55 | const message = new Message('Hello'); 56 | message.destinationName = 'World'; 57 | client.send(message); 58 | }) 59 | .catch((responseObject) => { 60 | if (responseObject.errorCode !== 0) { 61 | console.log('onConnectionLost:' + responseObject.errorMessage); 62 | } 63 | }) 64 | ; 65 | 66 | ``` 67 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About 5 | 6 | 7 |

About This Content

8 | 9 |

December 9, 2013

10 |

License

11 | 12 |

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise 13 | indicated below, the Content is provided to you under the terms and conditions of the 14 | Eclipse Public License Version 1.0 ("EPL") and Eclipse Distribution License Version 1.0 ("EDL"). 15 | A copy of the EPL is available at 16 | http://www.eclipse.org/legal/epl-v10.html 17 | and a copy of the EDL is available at 18 | http://www.eclipse.org/org/documents/edl-v10.php. 19 | For purposes of the EPL, "Program" will mean the Content.

20 | 21 |

If you did not receive this Content directly from the Eclipse Foundation, the Content is 22 | being redistributed by another party ("Redistributor") and different terms and conditions may 23 | apply to your use of any object code in the Content. Check the Redistributor's license that was 24 | provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise 25 | indicated below, the terms and conditions of the EPL still apply to any source code in the Content 26 | and such source code may be obtained at http://www.eclipse.org.

27 | 28 | 29 | -------------------------------------------------------------------------------- /edl-v10: -------------------------------------------------------------------------------- 1 | 2 | Eclipse Distribution License - v 1.0 3 | 4 | Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 9 | 10 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 11 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | 16 | -------------------------------------------------------------------------------- /epl-v10: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and 10 | b) in the case of each subsequent Contributor: 11 | i) changes to the Program, and 12 | ii) additions to the Program; 13 | where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. 14 | "Contributor" means any person or entity that distributes the Program. 15 | 16 | "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. 17 | 18 | "Program" means the Contributions distributed in accordance with this Agreement. 19 | 20 | "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 21 | 22 | 2. GRANT OF RIGHTS 23 | 24 | a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. 25 | b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. 26 | c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. 27 | d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 28 | 3. REQUIREMENTS 29 | 30 | A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: 31 | 32 | a) it complies with the terms and conditions of this Agreement; and 33 | b) its license agreement: 34 | i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; 35 | ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; 36 | iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and 37 | iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. 38 | When the Program is made available in source code form: 39 | 40 | a) it must be made available under this Agreement; and 41 | b) a copy of this Agreement must be included with each copy of the Program. 42 | Contributors may not remove or alter any copyright notices contained within the Program. 43 | 44 | Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 45 | 46 | 4. COMMERCIAL DISTRIBUTION 47 | 48 | Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. 49 | 50 | For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 51 | 52 | 5. NO WARRANTY 53 | 54 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 55 | 56 | 6. DISCLAIMER OF LIABILITY 57 | 58 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 59 | 60 | 7. GENERAL 61 | 62 | If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. 63 | 64 | If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. 65 | 66 | All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. 67 | 68 | Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. 69 | 70 | This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Message from './src/Message'; 2 | import Client from './src/Client'; 3 | 4 | export { 5 | Message, 6 | Client 7 | }; 8 | -------------------------------------------------------------------------------- /notice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Eclipse Foundation Software User Agreement 7 | 8 | 9 | 10 |

Eclipse Foundation Software User Agreement

11 |

February 1, 2011

12 | 13 |

Usage Of Content

14 | 15 |

THE ECLIPSE FOUNDATION MAKES AVAILABLE SOFTWARE, DOCUMENTATION, INFORMATION AND/OR OTHER MATERIALS FOR OPEN SOURCE PROJECTS 16 | (COLLECTIVELY "CONTENT"). USE OF THE CONTENT IS GOVERNED BY THE TERMS AND CONDITIONS OF THIS AGREEMENT AND/OR THE TERMS AND 17 | CONDITIONS OF LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW. BY USING THE CONTENT, YOU AGREE THAT YOUR USE 18 | OF THE CONTENT IS GOVERNED BY THIS AGREEMENT AND/OR THE TERMS AND CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR 19 | NOTICES INDICATED OR REFERENCED BELOW. IF YOU DO NOT AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT AND THE TERMS AND 20 | CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW, THEN YOU MAY NOT USE THE CONTENT.

21 | 22 |

Applicable Licenses

23 | 24 |

Unless otherwise indicated, all Content made available by the Eclipse Foundation is provided to you under the terms and conditions of the Eclipse Public License Version 1.0 25 | ("EPL"). A copy of the EPL is provided with this Content and is also available at http://www.eclipse.org/legal/epl-v10.html. 26 | For purposes of the EPL, "Program" will mean the Content.

27 | 28 |

Content includes, but is not limited to, source code, object code, documentation and other files maintained in the Eclipse Foundation source code 29 | repository ("Repository") in software modules ("Modules") and made available as downloadable archives ("Downloads").

30 | 31 | 38 | 39 |

The terms and conditions governing Plug-ins and Fragments should be contained in files named "about.html" ("Abouts"). The terms and conditions governing Features and 40 | Included Features should be contained in files named "license.html" ("Feature Licenses"). Abouts and Feature Licenses may be located in any directory of a Download or Module 41 | including, but not limited to the following locations:

42 | 43 | 50 | 51 |

Note: if a Feature made available by the Eclipse Foundation is installed using the Provisioning Technology (as defined below), you must agree to a license ("Feature Update License") during the 52 | installation process. If the Feature contains Included Features, the Feature Update License should either provide you with the terms and conditions governing the Included Features or 53 | inform you where you can locate them. Feature Update Licenses may be found in the "license" property of files named "feature.properties" found within a Feature. 54 | Such Abouts, Feature Licenses, and Feature Update Licenses contain the terms and conditions (or references to such terms and conditions) that govern your use of the associated Content in 55 | that directory.

56 | 57 |

THE ABOUTS, FEATURE LICENSES, AND FEATURE UPDATE LICENSES MAY REFER TO THE EPL OR OTHER LICENSE AGREEMENTS, NOTICES OR TERMS AND CONDITIONS. SOME OF THESE 58 | OTHER LICENSE AGREEMENTS MAY INCLUDE (BUT ARE NOT LIMITED TO):

59 | 60 | 68 | 69 |

IT IS YOUR OBLIGATION TO READ AND ACCEPT ALL SUCH TERMS AND CONDITIONS PRIOR TO USE OF THE CONTENT. If no About, Feature License, or Feature Update License is provided, please 70 | contact the Eclipse Foundation to determine what terms and conditions govern that particular Content.

71 | 72 | 73 |

Use of Provisioning Technology

74 | 75 |

The Eclipse Foundation makes available provisioning software, examples of which include, but are not limited to, p2 and the Eclipse 76 | Update Manager ("Provisioning Technology") for the purpose of allowing users to install software, documentation, information and/or 77 | other materials (collectively "Installable Software"). This capability is provided with the intent of allowing such users to 78 | install, extend and update Eclipse-based products. Information about packaging Installable Software is available at http://eclipse.org/equinox/p2/repository_packaging.html 80 | ("Specification").

81 | 82 |

You may use Provisioning Technology to allow other parties to install Installable Software. You shall be responsible for enabling the 83 | applicable license agreements relating to the Installable Software to be presented to, and accepted by, the users of the Provisioning Technology 84 | in accordance with the Specification. By using Provisioning Technology in such a manner and making it available in accordance with the 85 | Specification, you further acknowledge your agreement to, and the acquisition of all necessary rights to permit the following:

86 | 87 |
    88 |
  1. A series of actions may occur ("Provisioning Process") in which a user may execute the Provisioning Technology 89 | on a machine ("Target Machine") with the intent of installing, extending or updating the functionality of an Eclipse-based 90 | product.
  2. 91 |
  3. During the Provisioning Process, the Provisioning Technology may cause third party Installable Software or a portion thereof to be 92 | accessed and copied to the Target Machine.
  4. 93 |
  5. Pursuant to the Specification, you will provide to the user the terms and conditions that govern the use of the Installable 94 | Software ("Installable Software Agreement") and such Installable Software Agreement shall be accessed from the Target 95 | Machine in accordance with the Specification. Such Installable Software Agreement must inform the user of the terms and conditions that govern 96 | the Installable Software and must solicit acceptance by the end user in the manner prescribed in such Installable Software Agreement. Upon such 97 | indication of agreement by the user, the provisioning Technology will complete installation of the Installable Software.
  6. 98 |
99 | 100 |

Cryptography

101 | 102 |

Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to 103 | another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, 104 | possession, or use, and re-export of encryption software, to see if this is permitted.

105 | 106 |

Java and all Java-based trademarks are trademarks of Oracle Corporation in the United States, other countries, or both.

107 | 108 | 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-paho-mqtt", 3 | "description": "A fork of the Paho javascript client for use in React Native", 4 | "version": "0.1.1", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "flow": "flow", 8 | "jest": "jest", 9 | "test": "eslint . && flow && jest" 10 | }, 11 | "homepage": "https://github.com/rh389/react-native-paho-mqtt", 12 | "license": "SEE LICENSE IN epl-v10", 13 | "keywords": [ 14 | "mqtt", 15 | "react-native", 16 | "paho", 17 | "client" 18 | ], 19 | "repository": "rh389/react-native-paho-mqtt", 20 | "dependencies": { 21 | "events": "^1.1.1" 22 | }, 23 | "devDependencies": { 24 | "babel": "^6.5.2", 25 | "babel-cli": "^6.22.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-jest": "^18.0.0", 28 | "babel-preset-react-native": "^1.9.1", 29 | "eslint": "^3.15.0", 30 | "flow-bin": "^0.52.0", 31 | "jest-cli": "^18.1.0", 32 | "mosca": "^2.2.0", 33 | "node-localstorage": "^1.3.0", 34 | "websocket": "^1.0.24" 35 | }, 36 | "jest": { 37 | "transform": { 38 | ".*": "/node_modules/babel-jest" 39 | }, 40 | "moduleFileExtensions": [ 41 | "js", 42 | "json" 43 | ], 44 | "testRegex": "/test/.*\\.test\\.js$" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Client.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2013 IBM Corp. 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Eclipse Distribution License v1.0 which accompany this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * and the Eclipse Distribution License is available at 11 | * http://www.eclipse.org/org/documents/edl-v10.php. 12 | * 13 | * Contributors: 14 | * Andrew Banks - initial API and implementation and initial documentation 15 | *******************************************************************************/ 16 | /* @flow */ 17 | import ClientImplementation from './ClientImplementation'; 18 | import Message from './Message'; 19 | import { format, validate } from './util'; 20 | import { DEFAULT_KEEPALIVE_SECONDS, ERROR } from './constants'; 21 | import EventEmitter from 'events'; 22 | 23 | // ------------------------------------------------------------------------ 24 | // Public API. 25 | // ------------------------------------------------------------------------ 26 | 27 | type ConstructorOptions = { 28 | uri: string, 29 | clientId: string, 30 | storage: any, 31 | webSocket?: Class 32 | } 33 | 34 | type ConnectOptions = { 35 | userName?: string, 36 | password?: string, 37 | willMessage?: Message, 38 | timeout?: number, 39 | keepAliveInterval: number, 40 | useSSL: boolean, 41 | cleanSession: boolean, 42 | mqttVersion: number, 43 | allowMqttVersionFallback: boolean, 44 | uris?: string[] 45 | } 46 | 47 | /** 48 | * The JavaScript application communicates to the server using a {@link Client} object. 49 | * 50 | * Most applications will create just one Client object and then call its connect() method, 51 | * however applications can create more than one Client object if they wish. 52 | * In this case the combination of uri and clientId attributes must be different for each Client object. 53 | * 54 | * @name Client 55 | * 56 | * @fires Client#connectionLost 57 | * @fires Client#messageReceived 58 | * @fires Client#messageDelivered 59 | */ 60 | export default class Client extends EventEmitter { 61 | 62 | 63 | _client: ClientImplementation; 64 | 65 | /** 66 | * 67 | * @param {string} [uri] - the address of the messaging server, as a fully qualified WebSocket URI 68 | * @param {string} [clientId] - the Messaging client identifier, between 1 and 23 characters in length. 69 | * @param {object} [storage] - object implementing getItem, setItem, removeItem in a manner compatible with localStorage 70 | * @param {object} [webSocket] - object implementing the W3C websocket spec 71 | */ 72 | constructor({ uri, clientId, storage, webSocket }: ConstructorOptions) { 73 | super(); 74 | 75 | if (!/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(uri)) { 76 | throw new Error(format(ERROR.INVALID_ARGUMENT, [typeof uri, 'uri'])); 77 | } 78 | 79 | let clientIdLength = 0; 80 | for (let i = 0; i < clientId.length; i++) { 81 | let charCode = clientId.charCodeAt(i); 82 | if (charCode >= 0xD800 && charCode <= 0xDBFF) { 83 | i++; // Surrogate pair. 84 | } 85 | clientIdLength++; 86 | } 87 | if (typeof clientId !== 'string' || clientIdLength > 65535) { 88 | throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, 'clientId'])); 89 | } 90 | 91 | this._client = new ClientImplementation(uri, clientId, storage, webSocket); 92 | 93 | /** 94 | * @event Client#messageDelivered 95 | * @type {Message} 96 | */ 97 | this._client.onMessageDelivered = (message) => this.emit('messageDelivered', message); 98 | 99 | /** 100 | * @event Client#messageReceived 101 | * @type {Message} 102 | */ 103 | this._client.onMessageArrived = (message) => this.emit('messageReceived', message); 104 | 105 | /** 106 | * @event Client#connectionLost 107 | * @type {Error} 108 | */ 109 | this._client.onConnectionLost = (e) => this.emit('connectionLost', e); 110 | } 111 | 112 | /** 113 | * Connect this Messaging client to its server. 114 | * 115 | * @name Client#connect 116 | * @function 117 | * @param {number} [timeout=30000] - Fail if not connected within this time 118 | * @param {string} [userName] - Authentication username for this connection. 119 | * @param {string} [password] - Authentication password for this connection. 120 | * @param {Message} [willMessage] - sent by the server when the client disconnects abnormally. 121 | * @param {number} [keepAliveInterval=60] - ping the server every n ms to avoid being disconnected by the remote end. 122 | * @param {number} [mqttVersion=4] - protocol version to use (3 or 4). 123 | * @param {boolean} [cleanSession=true] - if true the client and server persistent state is deleted on successful connect. 124 | */ 125 | connect({ 126 | userName, 127 | password, 128 | willMessage, 129 | timeout = 30000, 130 | keepAliveInterval = DEFAULT_KEEPALIVE_SECONDS, 131 | cleanSession = true, 132 | mqttVersion = 4 133 | }: ConnectOptions = {}) { 134 | validate({ 135 | userName, 136 | password, 137 | willMessage, 138 | timeout, 139 | keepAliveInterval, 140 | cleanSession, 141 | mqttVersion 142 | }, { 143 | timeout: 'number', 144 | userName: '?string', 145 | password: '?string', 146 | willMessage: '?object', 147 | keepAliveInterval: 'number', 148 | cleanSession: 'boolean', 149 | mqttVersion: 'number' 150 | }); 151 | 152 | return new Promise((resolve, reject) => { 153 | 154 | if (mqttVersion > 4 || mqttVersion < 3) { 155 | throw new Error(format(ERROR.INVALID_ARGUMENT, [mqttVersion, 'mqttVersion'])); 156 | } 157 | 158 | //Check that if password is set, so is username 159 | if (password !== undefined && userName === undefined) { 160 | throw new Error(format(ERROR.INVALID_ARGUMENT, [password, 'password'])); 161 | } 162 | 163 | if (willMessage) { 164 | if (!(willMessage instanceof Message)) { 165 | throw new Error(format(ERROR.INVALID_TYPE, [willMessage, 'willMessage'])); 166 | } 167 | // The will message must have a payload that can be represented as a string. 168 | // Cause the willMessage to throw an exception if this is not the case. 169 | willMessage.payloadString; 170 | 171 | if (typeof willMessage.destinationName === 'undefined') { 172 | throw new Error(format(ERROR.INVALID_TYPE, [typeof willMessage.destinationName, 'willMessage.destinationName'])); 173 | } 174 | } 175 | 176 | this._client.connect({ 177 | userName, 178 | password, 179 | willMessage: willMessage || null, 180 | timeout, 181 | keepAliveInterval, 182 | cleanSession, 183 | mqttVersion: mqttVersion === 4 ? 4 : 3, 184 | onSuccess: resolve, 185 | onFailure: reject 186 | }); 187 | }); 188 | } 189 | 190 | /** 191 | * Subscribe for messages, request receipt of a copy of messages sent to the destinations described by the filter. 192 | * 193 | * @param {string} [filter] the topic to subscribe to 194 | * @param {number} [qos=0] - the maximum qos of any publications sent as a result of making this subscription. 195 | * @param {number} [timeout=30000] - milliseconds after which the call will fail 196 | * @returns {Promise} 197 | */ 198 | subscribe(filter: string, { qos = 0, timeout = 30000 }:{ qos: 0 | 1 | 2, timeout: number } = {}) { 199 | return new Promise((resolve, reject) => { 200 | if (typeof filter !== 'string') { 201 | throw new Error('Invalid argument:' + filter); 202 | } 203 | if (typeof timeout !== 'number') { 204 | throw new Error('Invalid argument:' + timeout); 205 | } 206 | if ([0, 1, 2].indexOf(qos) === -1) { 207 | throw new Error('Invalid argument:' + qos); 208 | } 209 | 210 | this._client.subscribe(filter, { 211 | timeout, 212 | qos, 213 | onSuccess: resolve, 214 | onFailure: reject 215 | }); 216 | }); 217 | } 218 | 219 | /** 220 | * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter. 221 | * 222 | * @param {string} [filter] the topic to unsubscribe from 223 | * @param {number} [timeout=30000] MS after which the promise will be rejected 224 | * @returns {Promise} 225 | */ 226 | unsubscribe(filter: string, { timeout = 30000 }:{ timeout: number } = {}) { 227 | return new Promise((resolve, reject) => { 228 | if (typeof filter !== 'string') { 229 | throw new Error('Invalid argument:' + filter); 230 | } 231 | if (typeof timeout !== 'number') { 232 | throw new Error('Invalid argument:' + timeout); 233 | } 234 | 235 | this._client.unsubscribe(filter, { 236 | timeout, 237 | onSuccess: resolve, 238 | onFailure: reject 239 | }); 240 | }); 241 | } 242 | 243 | /** 244 | * Send a message to the consumers of the destination in the Message. 245 | * 246 | * @name Client#send 247 | * @function 248 | * @param {string|Message} topic - mandatory The name of the destination to which the message is to be sent. 249 | * - If it is the only parameter, used as Message object. 250 | * @param {String|ArrayBuffer} payload - The message data to be sent. 251 | * @param {number} qos The Quality of Service used to deliver the message. 252 | *
253 | *
0 Best effort (default). 254 | *
1 At least once. 255 | *
2 Exactly once. 256 | *
257 | * @param {Boolean} retained If true, the message is to be retained by the server and delivered 258 | * to both current and future subscriptions. 259 | * If false the server only delivers the message to current subscribers, this is the default for new Messages. 260 | * A received message has the retained boolean set to true if the message was published 261 | * with the retained boolean set to true 262 | * and the subscrption was made after the message has been published. 263 | * @throws {InvalidState} if the client is not connected. 264 | */ 265 | send(topic: string | Message, payload: string, qos: 0 | 1 | 2, retained: boolean) { 266 | let message; 267 | 268 | if (arguments.length === 0) { 269 | throw new Error('Invalid argument.' + 'length'); 270 | 271 | } else if (arguments.length === 1) { 272 | 273 | if (!(topic instanceof Message)) { 274 | throw new Error('Invalid argument:' + typeof topic); 275 | } 276 | 277 | message = topic; 278 | if (typeof message.destinationName === 'undefined') { 279 | throw new Error(format(ERROR.INVALID_ARGUMENT, [message.destinationName, 'Message.destinationName'])); 280 | } 281 | this._client.send(message); 282 | 283 | } else if (typeof topic === 'string') { 284 | //parameter checking in Message object 285 | message = new Message(payload); 286 | message.destinationName = topic; 287 | if (arguments.length >= 3) { 288 | message.qos = qos; 289 | } 290 | if (arguments.length >= 4) { 291 | message.retained = retained; 292 | } 293 | this._client.send(message); 294 | } 295 | } 296 | 297 | /** 298 | * Normal disconnect of this Messaging client from its server. 299 | * 300 | * @name Client#disconnect 301 | * @function 302 | * @throws {InvalidState} if the client is already disconnected. 303 | */ 304 | disconnect() { 305 | return new Promise((resolve, reject) => { 306 | this.once('connectionLost', (error) => { 307 | if (error && error.errorCode !== 0) { 308 | return reject(error); 309 | } 310 | resolve(); 311 | }); 312 | this._client.disconnect(); 313 | }); 314 | } 315 | 316 | /** 317 | * Get the contents of the trace log. 318 | * 319 | * @name Client#getTraceLog 320 | * @function 321 | * @return {Object[]} tracebuffer containing the time ordered trace records. 322 | */ 323 | getTraceLog() { 324 | return this._client.getTraceLog(); 325 | } 326 | 327 | /** 328 | * Start tracing. 329 | * 330 | * @name Client#startTrace 331 | * @function 332 | */ 333 | startTrace() { 334 | this._client.startTrace(); 335 | } 336 | 337 | /** 338 | * Stop tracing. 339 | * 340 | * @name Client#stopTrace 341 | * @function 342 | */ 343 | stopTrace() { 344 | this._client.stopTrace(); 345 | } 346 | 347 | isConnected() { 348 | return this._client.connected; 349 | } 350 | 351 | get uri(): string { 352 | return this._client.uri; 353 | } 354 | 355 | get clientId(): ?string { 356 | return this._client.clientId; 357 | } 358 | 359 | get trace(): ?Function { 360 | return this._client.traceFunction; 361 | } 362 | 363 | set trace(trace: Function) { 364 | if (typeof trace === 'function') { 365 | this._client.traceFunction = trace; 366 | } else { 367 | throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, 'onTrace'])); 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/ClientImplementation.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | 3 | import Message from './Message'; 4 | import { decodeMessage, format, invariant } from './util'; 5 | import { CONNACK_RC, ERROR, MESSAGE_TYPE } from './constants'; 6 | import Pinger from './Pinger'; 7 | import WireMessage from './WireMessage'; 8 | import PublishMessage from './PublishMessage'; 9 | import ConnectMessage from './ConnectMessage'; 10 | 11 | type ConnectOptions = { 12 | timeout?: number, 13 | mqttVersion: 3 | 4, 14 | keepAliveInterval: number, 15 | onSuccess: ?() => void, 16 | onFailure: ?(Error) => void, 17 | userName?: string, 18 | password?: string, 19 | willMessage: ?Message, 20 | cleanSession: boolean 21 | } 22 | 23 | type Storage = Object & { setItem: (key: string, item: any) => void, getItem: (key: string) => any }; 24 | 25 | /* 26 | * Internal implementation of the Websockets MQTT V3.1/V4 client. 27 | */ 28 | class ClientImplementation { 29 | 30 | uri: string; 31 | clientId: string; 32 | storage: Storage; 33 | webSocket: Class; 34 | socket: ?WebSocket; 35 | 36 | // We have received a CONNACK and not subsequently disconnected 37 | connected = false; 38 | 39 | // The largest permitted message ID, effectively number of in-flight outbound messages. 40 | // Must be <=65536, the maximum permitted by the protocol 41 | maxOutboundInFlight = 65536; 42 | connectOptions: ?ConnectOptions; 43 | onConnectionLost: ?Function; 44 | onMessageDelivered: ?Function; 45 | onMessageArrived: ?Function; 46 | traceFunction: ?Function; 47 | _msg_queue = null; 48 | _connectTimeout: ?number; 49 | /* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */ 50 | sendPinger = null; 51 | 52 | receiveBuffer: ?Uint8Array = null; 53 | 54 | _traceBuffer = null; 55 | _MAX_TRACE_ENTRIES = 100; 56 | 57 | // Internal queue of messages to be sent, in sending order. 58 | _messagesAwaitingDispatch: (WireMessage | PublishMessage)[] = []; 59 | 60 | // Messages we have sent and are expecting a response for, indexed by their respective message ids. 61 | _outboundMessagesInFlight: { [key: string]: WireMessage | PublishMessage } = {}; 62 | 63 | // Messages we have received and acknowleged and are expecting a confirm message for indexed by their respective message ids. 64 | _receivedMessagesAwaitingAckConfirm: { [key: string]: WireMessage | PublishMessage } = {}; 65 | 66 | // Unique identifier for outbound messages, incrementing counter as messages are sent. 67 | _nextMessageId: number = 1; 68 | 69 | // Used to determine the transmission sequence of stored sent messages. 70 | _sequence: number = 0; 71 | 72 | // Local storage keys are qualified with this. 73 | _localKey: string; 74 | 75 | /** 76 | * 77 | * @param {string} [uri] the FQDN (with protocol and path suffix) of the host 78 | * @param {string} [clientId] the MQ client identifier. 79 | * @param {object} [storage] object implementing getItem, setItem, removeItem in a manner compatible with localStorage 80 | * @param {object} [ws] object implementing the W3C websockets spec 81 | */ 82 | constructor(uri: string, clientId: string, storage?: Object, ws?: Class) { 83 | // Check dependencies are satisfied in this browser. 84 | if (!ws && !(window && window.WebSocket)) { 85 | throw new Error(format(ERROR.UNSUPPORTED, ['WebSocket'])); 86 | } 87 | if (!storage && !(window && window.localStorage)) { 88 | throw new Error(format(ERROR.UNSUPPORTED, ['localStorage'])); 89 | } 90 | 91 | if (!(window && window.ArrayBuffer)) { 92 | throw new Error(format(ERROR.UNSUPPORTED, ['ArrayBuffer'])); 93 | } 94 | this._trace('Client', uri, clientId); 95 | 96 | this.uri = uri; 97 | this.clientId = clientId; 98 | this.storage = storage || window.localStorage; 99 | this.webSocket = ws || window.WebSocket; 100 | 101 | // Local storagekeys are qualified with the following string. 102 | // The conditional inclusion of path in the key is for backward 103 | // compatibility to when the path was not configurable and assumed to 104 | // be /mqtt 105 | this._localKey = uri + ':' + clientId + ':'; 106 | 107 | 108 | // Load the local state, if any, from the saved version, only restore state relevant to this client. 109 | Object.keys(this.storage).forEach(key => { 110 | if (key.indexOf('Sent:' + this._localKey) === 0 || key.indexOf('Received:' + this._localKey) === 0) { 111 | this.restore(key); 112 | } 113 | }); 114 | } 115 | 116 | 117 | connect(connectOptions: ConnectOptions) { 118 | const maskedCopy = { ...connectOptions }; 119 | if (maskedCopy.password) { 120 | maskedCopy.password = 'REDACTED'; 121 | } 122 | this._trace('Client.connect', maskedCopy, this.socket, this.connected); 123 | 124 | if (this.connected) { 125 | throw new Error(format(ERROR.INVALID_STATE, ['already connected'])); 126 | } 127 | if (this.socket) { 128 | throw new Error(format(ERROR.INVALID_STATE, ['already connected'])); 129 | } 130 | 131 | this.connectOptions = connectOptions; 132 | this.connected = false; 133 | this.socket = new this.webSocket(this.uri, ['mqtt' + (connectOptions.mqttVersion === 3 ? 'v3.1' : '')]); 134 | this.socket.binaryType = 'arraybuffer'; 135 | 136 | // When the socket is open, this client will send the CONNECT WireMessage using the saved parameters. 137 | this.socket.onopen = () => { 138 | const { willMessage, mqttVersion, userName, password, cleanSession, keepAliveInterval } = connectOptions; 139 | // Send the CONNECT message object. 140 | 141 | const wireMessage = new ConnectMessage({ 142 | cleanSession, 143 | userName, 144 | password, 145 | mqttVersion, 146 | willMessage, 147 | keepAliveInterval, 148 | clientId: this.clientId 149 | }); 150 | 151 | this._trace('socket.send', { ...wireMessage, options: maskedCopy }); 152 | 153 | this.socket && (this.socket.onopen = () => null); 154 | this.socket && (this.socket.send(wireMessage.encode())); 155 | }; 156 | 157 | this.socket.onmessage = (event) => { 158 | this._trace('socket.onmessage', event.data); 159 | const messages = this._deframeMessages(((event.data:any):ArrayBuffer)); 160 | messages && messages.forEach(message => this._handleMessage(message)); 161 | }; 162 | this.socket.onerror = (error: { data?: string }) => 163 | this._disconnected(ERROR.SOCKET_ERROR.code, format(ERROR.SOCKET_ERROR, [error.data || ' Unknown socket error'])); 164 | this.socket.onclose = () => this._disconnected(ERROR.SOCKET_CLOSE.code, format(ERROR.SOCKET_CLOSE)); 165 | 166 | if (connectOptions.keepAliveInterval > 0) { 167 | //Cast this to any to deal with flow/IDE bug: https://github.com/facebook/flow/issues/2235#issuecomment-239357626 168 | this.sendPinger = new Pinger((this: any), connectOptions.keepAliveInterval); 169 | } 170 | 171 | if (connectOptions.timeout) { 172 | this._connectTimeout = setTimeout(() => { 173 | this._disconnected(ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)); 174 | }, connectOptions.timeout); 175 | } 176 | } 177 | 178 | subscribe(filter: string, subscribeOptions: { onSuccess: ({ grantedQos: number }) => void, onFailure: (Error) => void, qos: 0 | 1 | 2, timeout: number }) { 179 | this._trace('Client.subscribe', filter, subscribeOptions); 180 | 181 | if (!this.connected) { 182 | throw new Error(format(ERROR.INVALID_STATE, ['not connected'])); 183 | } 184 | 185 | const wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE); 186 | wireMessage.topics = [filter]; 187 | wireMessage.requestedQos = [subscribeOptions.qos || 0]; 188 | 189 | if (subscribeOptions.onSuccess) { 190 | wireMessage.subAckReceived = function (grantedQos) { 191 | subscribeOptions.onSuccess({ grantedQos: grantedQos }); 192 | }; 193 | } 194 | 195 | if (subscribeOptions.onFailure) { 196 | wireMessage.onFailure = subscribeOptions.onFailure; 197 | } 198 | 199 | if (subscribeOptions.timeout && subscribeOptions.onFailure) { 200 | wireMessage.timeOut = setTimeout(() => { 201 | subscribeOptions.onFailure(new Error(format(ERROR.SUBSCRIBE_TIMEOUT))); 202 | }, subscribeOptions.timeout); 203 | } 204 | 205 | // All subscriptions return a SUBACK. 206 | this._requires_ack(wireMessage); 207 | this._scheduleMessage(wireMessage); 208 | } 209 | 210 | /** @ignore */ 211 | unsubscribe(filter: string, unsubscribeOptions: { onSuccess: () => void, onFailure: (Error) => void, timeout: number }) { 212 | this._trace('Client.unsubscribe', filter, unsubscribeOptions); 213 | 214 | if (!this.connected) { 215 | throw new Error(format(ERROR.INVALID_STATE, ['not connected'])); 216 | } 217 | 218 | const wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE); 219 | wireMessage.topics = [filter]; 220 | 221 | if (unsubscribeOptions.onSuccess) { 222 | wireMessage.unSubAckReceived = function () { 223 | unsubscribeOptions.onSuccess(); 224 | }; 225 | } 226 | if (unsubscribeOptions.timeout) { 227 | wireMessage.timeOut = setTimeout(() => { 228 | unsubscribeOptions.onFailure(new Error(format(ERROR.UNSUBSCRIBE_TIMEOUT))); 229 | }, unsubscribeOptions.timeout); 230 | } 231 | 232 | // All unsubscribes return a SUBACK. 233 | this._requires_ack(wireMessage); 234 | this._scheduleMessage(wireMessage); 235 | } 236 | 237 | send(message: Message) { 238 | this._trace('Client.send', message); 239 | 240 | if (!this.connected) { 241 | throw new Error(format(ERROR.INVALID_STATE, ['not connected'])); 242 | } 243 | 244 | const wireMessage = new PublishMessage(message); 245 | 246 | if (message.qos > 0) { 247 | this._requires_ack(wireMessage); 248 | } else if (this.onMessageDelivered) { 249 | const onMessageDelivered = this.onMessageDelivered; 250 | wireMessage.onDispatched = () => onMessageDelivered(wireMessage.payloadMessage); 251 | } 252 | this._scheduleMessage(wireMessage); 253 | } 254 | 255 | disconnect() { 256 | this._trace('Client.disconnect'); 257 | 258 | if (!this.socket) { 259 | throw new Error(format(ERROR.INVALID_STATE, ['not connecting or connected'])); 260 | } 261 | 262 | const wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT); 263 | 264 | // Run the disconnected call back as soon as the message has been sent, 265 | // in case of a failure later on in the disconnect processing. 266 | // as a consequence, the _disconnected call back may be run several times. 267 | wireMessage.onDispatched = () => { 268 | this._disconnected(); 269 | }; 270 | 271 | this._scheduleMessage(wireMessage); 272 | } 273 | 274 | getTraceLog() { 275 | if (this._traceBuffer !== null) { 276 | this._trace('Client.getTraceLog', new Date()); 277 | this._trace('Client.getTraceLog in flight messages', Object.keys(this._outboundMessagesInFlight).length); 278 | Object.keys(this._outboundMessagesInFlight).forEach((key) => { 279 | this._trace('_outboundMessagesInFlight ', key, this._outboundMessagesInFlight[key]); 280 | }); 281 | Object.keys(this._receivedMessagesAwaitingAckConfirm).forEach((key) => { 282 | this._trace('_receivedMessagesAwaitingAckConfirm ', key, this._receivedMessagesAwaitingAckConfirm[key]); 283 | }); 284 | return this._traceBuffer; 285 | } 286 | } 287 | 288 | startTrace() { 289 | if (this._traceBuffer === null) { 290 | this._traceBuffer = []; 291 | } 292 | this._trace('Client.startTrace', new Date()); 293 | } 294 | 295 | stopTrace() { 296 | this._traceBuffer = null; 297 | } 298 | 299 | 300 | // Schedule a new message to be sent over the WebSockets 301 | // connection. CONNECT messages cause WebSocket connection 302 | // to be started. All other messages are queued internally 303 | // until this has happened. When WS connection starts, process 304 | // all outstanding messages. 305 | _scheduleMessage(message: WireMessage | PublishMessage) { 306 | this._messagesAwaitingDispatch.push(message); 307 | // Process outstanding messages in the queue if we have an open socket, and have received CONNACK. 308 | if (this.connected) { 309 | this._process_queue(); 310 | } 311 | } 312 | 313 | store(prefix: string, wireMessage: PublishMessage) { 314 | const messageIdentifier = wireMessage.messageIdentifier; 315 | invariant(messageIdentifier, format(ERROR.INVALID_STATE, ['Cannot store a WireMessage with no messageIdentifier'])); 316 | const storedMessage: any = { type: wireMessage.type, messageIdentifier, version: 1 }; 317 | 318 | switch (wireMessage.type) { 319 | case MESSAGE_TYPE.PUBLISH: 320 | const payloadMessage = wireMessage.payloadMessage; 321 | invariant(payloadMessage, format(ERROR.INVALID_STATE, ['PUBLISH WireMessage with no payloadMessage'])); 322 | if (wireMessage.pubRecReceived) { 323 | storedMessage.pubRecReceived = true; 324 | } 325 | 326 | // Convert the payload to a hex string. 327 | storedMessage.payloadMessage = {}; 328 | let hex = ''; 329 | const messageBytes = payloadMessage.payloadBytes; 330 | for (let i = 0; i < messageBytes.length; i++) { 331 | if (messageBytes[i] <= 0xF) { 332 | hex = hex + '0' + messageBytes[i].toString(16); 333 | } else { 334 | hex = hex + messageBytes[i].toString(16); 335 | } 336 | } 337 | storedMessage.payloadMessage.payloadHex = hex; 338 | 339 | storedMessage.payloadMessage.qos = payloadMessage.qos; 340 | storedMessage.payloadMessage.destinationName = payloadMessage.destinationName; 341 | if (payloadMessage.duplicate) { 342 | storedMessage.payloadMessage.duplicate = true; 343 | } 344 | if (payloadMessage.retained) { 345 | storedMessage.payloadMessage.retained = true; 346 | } 347 | 348 | // Add a sequence number to sent messages. 349 | if (prefix.indexOf('Sent:') === 0) { 350 | if (wireMessage.sequence === undefined) { 351 | wireMessage.sequence = ++this._sequence; 352 | } 353 | storedMessage.sequence = wireMessage.sequence; 354 | } 355 | break; 356 | 357 | default: 358 | throw Error(format(ERROR.INVALID_STORED_DATA, [prefix + this._localKey + messageIdentifier, storedMessage])); 359 | } 360 | this.storage.setItem(prefix + this._localKey + messageIdentifier, JSON.stringify(storedMessage)); 361 | } 362 | 363 | restore(key: string) { 364 | const value = this.storage.getItem(key); 365 | const storedMessage = JSON.parse(value); 366 | 367 | let wireMessage: PublishMessage; 368 | 369 | switch (storedMessage.type) { 370 | case MESSAGE_TYPE.PUBLISH: 371 | // Replace the payload message with a Message object. 372 | let hex = storedMessage.payloadMessage.payloadHex; 373 | const buffer = new ArrayBuffer((hex.length) / 2); 374 | const byteStream = new Uint8Array(buffer); 375 | let i = 0; 376 | while (hex.length >= 2) { 377 | const x = parseInt(hex.substring(0, 2), 16); 378 | hex = hex.substring(2, hex.length); 379 | byteStream[i++] = x; 380 | } 381 | const payloadMessage = new Message(byteStream); 382 | 383 | payloadMessage.qos = storedMessage.payloadMessage.qos; 384 | payloadMessage.destinationName = storedMessage.payloadMessage.destinationName; 385 | if (storedMessage.payloadMessage.duplicate) { 386 | payloadMessage.duplicate = true; 387 | } 388 | if (storedMessage.payloadMessage.retained) { 389 | payloadMessage.retained = true; 390 | } 391 | wireMessage = new PublishMessage(payloadMessage, storedMessage.messageIdentifier); 392 | 393 | break; 394 | 395 | default: 396 | throw Error(format(ERROR.INVALID_STORED_DATA, [key, value])); 397 | } 398 | 399 | if (key.indexOf('Sent:' + this._localKey) === 0) { 400 | wireMessage.payloadMessage.duplicate = true; 401 | invariant(wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['Stored WireMessage with no messageIdentifier'])); 402 | this._outboundMessagesInFlight[wireMessage.messageIdentifier.toString()] = wireMessage; 403 | } else if (key.indexOf('Received:' + this._localKey) === 0) { 404 | invariant(wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['Stored WireMessage with no messageIdentifier'])); 405 | this._receivedMessagesAwaitingAckConfirm[wireMessage.messageIdentifier.toString()] = wireMessage; 406 | } 407 | } 408 | 409 | _process_queue() { 410 | // Process messages in order they were added 411 | // Send all queued messages down socket connection 412 | const socket = this.socket; 413 | 414 | // Consume each message and remove it from the queue 415 | let wireMessage; 416 | while ((wireMessage = this._messagesAwaitingDispatch.shift())) { 417 | this._trace('Client._socketSend', wireMessage); 418 | socket && socket.send(wireMessage.encode()); 419 | wireMessage.onDispatched && wireMessage.onDispatched(); 420 | } 421 | 422 | this.sendPinger && this.sendPinger.reset(); 423 | } 424 | 425 | /** 426 | * Expect an ACK response for this message. Add message to the set of in progress 427 | * messages and set an unused identifier in this message. 428 | * @ignore 429 | */ 430 | _requires_ack(wireMessage: WireMessage | PublishMessage) { 431 | const messageCount = Object.keys(this._outboundMessagesInFlight).length; 432 | if (messageCount > this.maxOutboundInFlight) { 433 | throw Error('Too many messages:' + messageCount); 434 | } 435 | 436 | while (this._outboundMessagesInFlight[this._nextMessageId.toString()] !== undefined) { 437 | this._nextMessageId++; 438 | } 439 | wireMessage.messageIdentifier = this._nextMessageId; 440 | this._outboundMessagesInFlight[wireMessage.messageIdentifier.toString()] = wireMessage; 441 | if (wireMessage instanceof PublishMessage) { 442 | this.store('Sent:', wireMessage); 443 | } 444 | if (this._nextMessageId === this.maxOutboundInFlight) { 445 | // Next time we will search for the first unused ID up from 1 446 | this._nextMessageId = 1; 447 | } 448 | } 449 | 450 | _deframeMessages(data: ArrayBuffer): ?Array<(WireMessage | PublishMessage)> { 451 | let byteArray = new Uint8Array(data); 452 | if (this.receiveBuffer) { 453 | const receiveBufferLength = this.receiveBuffer.length; 454 | const newData = new Uint8Array(receiveBufferLength + byteArray.length); 455 | newData.set(this.receiveBuffer); 456 | newData.set(byteArray, receiveBufferLength); 457 | byteArray = newData; 458 | this.receiveBuffer = null; 459 | } 460 | try { 461 | let offset = 0; 462 | let messages: (WireMessage | PublishMessage)[] = []; 463 | while (offset < byteArray.length) { 464 | const result = decodeMessage(byteArray, offset); 465 | const wireMessage = result[0]; 466 | offset = result[1]; 467 | if (wireMessage) { 468 | messages.push(wireMessage); 469 | } else { 470 | break; 471 | } 472 | } 473 | if (offset < byteArray.length) { 474 | this.receiveBuffer = byteArray.subarray(offset); 475 | } 476 | return messages; 477 | } catch (error) { 478 | this._disconnected(ERROR.INTERNAL_ERROR.code, format(ERROR.INTERNAL_ERROR, [error.message, error.stack.toString()])); 479 | } 480 | } 481 | 482 | _handleMessage(wireMessage: WireMessage | PublishMessage) { 483 | 484 | this._trace('Client._handleMessage', wireMessage); 485 | const connectOptions = this.connectOptions; 486 | invariant(connectOptions, format(ERROR.INVALID_STATE, ['_handleMessage invoked but connectOptions not set'])); 487 | 488 | try { 489 | if (wireMessage instanceof PublishMessage) { 490 | this._receivePublish(wireMessage); 491 | return; 492 | } 493 | 494 | let sentMessage: WireMessage | PublishMessage, receivedMessage: WireMessage | PublishMessage, messageIdentifier; 495 | 496 | switch (wireMessage.type) { 497 | case MESSAGE_TYPE.CONNACK: 498 | clearTimeout(this._connectTimeout); 499 | 500 | // If we have started using clean session then clear up the local state. 501 | if (connectOptions.cleanSession) { 502 | Object.keys(this._outboundMessagesInFlight).forEach((key) => { 503 | sentMessage = this._outboundMessagesInFlight[key]; 504 | invariant(messageIdentifier = sentMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['Stored WireMessage with no messageIdentifier'])); 505 | this.storage.removeItem('Sent:' + this._localKey + messageIdentifier); 506 | }); 507 | this._outboundMessagesInFlight = {}; 508 | 509 | Object.keys(this._receivedMessagesAwaitingAckConfirm).forEach((key) => { 510 | receivedMessage = this._receivedMessagesAwaitingAckConfirm[key]; 511 | invariant(messageIdentifier = receivedMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['Stored WireMessage with no messageIdentifier'])); 512 | this.storage.removeItem('Received:' + this._localKey + messageIdentifier); 513 | }); 514 | this._receivedMessagesAwaitingAckConfirm = {}; 515 | } 516 | // Client connected and ready for business. 517 | if (wireMessage.returnCode === 0) { 518 | this.connected = true; 519 | } else { 520 | this._disconnected(ERROR.CONNACK_RETURNCODE.code, format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]])); 521 | break; 522 | } 523 | 524 | // Resend messages. Sort sentMessages into the original sent order. 525 | const sequencedMessages = Object.keys(this._outboundMessagesInFlight).map(key => this._outboundMessagesInFlight[key]).sort((a, b) => (a.sequence || 0) - (b.sequence || 0)); 526 | 527 | sequencedMessages.forEach((sequencedMessage) => { 528 | invariant(messageIdentifier = sequencedMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['PUBREL WireMessage with no messageIdentifier'])); 529 | if (sequencedMessage instanceof PublishMessage && sequencedMessage.pubRecReceived) { 530 | this._scheduleMessage(new WireMessage(MESSAGE_TYPE.PUBREL, { messageIdentifier })); 531 | } else { 532 | this._scheduleMessage(sequencedMessage); 533 | } 534 | }); 535 | 536 | // Execute the connectOptions.onSuccess callback if there is one. 537 | connectOptions.onSuccess && connectOptions.onSuccess(); 538 | 539 | // Process all queued messages now that the connection is established. 540 | this._process_queue(); 541 | break; 542 | 543 | case MESSAGE_TYPE.PUBACK: 544 | invariant(messageIdentifier = wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['PUBACK WireMessage with no messageIdentifier'])); 545 | sentMessage = this._outboundMessagesInFlight[messageIdentifier.toString()]; 546 | // If this is a re flow of a PUBACK after we have restarted receivedMessage will not exist. 547 | if (sentMessage) { 548 | delete this._outboundMessagesInFlight[messageIdentifier.toString()]; 549 | this.storage.removeItem('Sent:' + this._localKey + messageIdentifier); 550 | if (this.onMessageDelivered && sentMessage instanceof PublishMessage) { 551 | this.onMessageDelivered(sentMessage.payloadMessage); 552 | } 553 | } 554 | break; 555 | 556 | case MESSAGE_TYPE.PUBREC: 557 | invariant(messageIdentifier = wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['PUBREC WireMessage with no messageIdentifier'])); 558 | sentMessage = this._outboundMessagesInFlight[messageIdentifier.toString()]; 559 | // If this is a re flow of a PUBREC after we have restarted receivedMessage will not exist. 560 | if (sentMessage && sentMessage instanceof PublishMessage) { 561 | sentMessage.pubRecReceived = true; 562 | const pubRelMessage = new WireMessage(MESSAGE_TYPE.PUBREL, { messageIdentifier }); 563 | this.store('Sent:', sentMessage); 564 | this._scheduleMessage(pubRelMessage); 565 | } 566 | break; 567 | 568 | case MESSAGE_TYPE.PUBREL: 569 | invariant(messageIdentifier = wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['PUBREL WireMessage with no messageIdentifier'])); 570 | receivedMessage = this._receivedMessagesAwaitingAckConfirm[messageIdentifier.toString()]; 571 | this.storage.removeItem('Received:' + this._localKey + messageIdentifier); 572 | // If this is a re flow of a PUBREL after we have restarted receivedMessage will not exist. 573 | if (receivedMessage && receivedMessage instanceof PublishMessage) { 574 | this._receiveMessage(receivedMessage); 575 | delete this._receivedMessagesAwaitingAckConfirm[messageIdentifier.toString()]; 576 | } 577 | // Always flow PubComp, we may have previously flowed PubComp but the server lost it and restarted. 578 | const pubCompMessage = new WireMessage(MESSAGE_TYPE.PUBCOMP, { messageIdentifier }); 579 | this._scheduleMessage(pubCompMessage); 580 | break; 581 | 582 | case MESSAGE_TYPE.PUBCOMP: 583 | invariant(messageIdentifier = wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['PUBCOMP WireMessage with no messageIdentifier'])); 584 | sentMessage = this._outboundMessagesInFlight[messageIdentifier.toString()]; 585 | delete this._outboundMessagesInFlight[messageIdentifier.toString()]; 586 | this.storage.removeItem('Sent:' + this._localKey + messageIdentifier); 587 | if (this.onMessageDelivered && sentMessage instanceof PublishMessage) { 588 | this.onMessageDelivered(sentMessage.payloadMessage); 589 | } 590 | break; 591 | 592 | case MESSAGE_TYPE.SUBACK: 593 | invariant(messageIdentifier = wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['SUBACK WireMessage with no messageIdentifier'])); 594 | sentMessage = this._outboundMessagesInFlight[messageIdentifier.toString()]; 595 | if (sentMessage) { 596 | if (sentMessage.timeOut) { 597 | clearTimeout(sentMessage.timeOut); 598 | } 599 | invariant(wireMessage.returnCode instanceof Uint8Array, format(ERROR.INVALID_STATE, ['SUBACK WireMessage with invalid returnCode'])); 600 | // This will need to be fixed when we add multiple topic support 601 | if (wireMessage.returnCode[0] === 0x80) { 602 | if ((sentMessage instanceof WireMessage) && sentMessage.onFailure) { 603 | sentMessage.onFailure(new Error('Suback error')); 604 | } 605 | } else if ((sentMessage instanceof WireMessage) && sentMessage.subAckReceived) { 606 | sentMessage.subAckReceived(wireMessage.returnCode[0]); 607 | } 608 | delete this._outboundMessagesInFlight[messageIdentifier.toString()]; 609 | } 610 | break; 611 | 612 | case MESSAGE_TYPE.UNSUBACK: 613 | invariant(messageIdentifier = wireMessage.messageIdentifier, format(ERROR.INVALID_STATE, ['UNSUBACK WireMessage with no messageIdentifier'])); 614 | sentMessage = this._outboundMessagesInFlight[messageIdentifier.toString()]; 615 | if (sentMessage && (sentMessage instanceof WireMessage) && sentMessage.type === MESSAGE_TYPE.UNSUBSCRIBE) { 616 | if (sentMessage.timeOut) { 617 | clearTimeout(sentMessage.timeOut); 618 | } 619 | sentMessage.unSubAckReceived && sentMessage.unSubAckReceived(); 620 | delete this._outboundMessagesInFlight[messageIdentifier.toString()]; 621 | } 622 | 623 | break; 624 | 625 | case MESSAGE_TYPE.PINGRESP: 626 | // We don't care whether the server is still there (yet) 627 | break; 628 | 629 | case MESSAGE_TYPE.DISCONNECT: 630 | // Clients do not expect to receive disconnect packets. 631 | this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code, format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type])); 632 | break; 633 | 634 | default: 635 | this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code, format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type])); 636 | } 637 | } catch (error) { 638 | this._disconnected(ERROR.INTERNAL_ERROR.code, format(ERROR.INTERNAL_ERROR, [error.message, error.stack.toString()])); 639 | } 640 | } 641 | 642 | /** @ignore */ 643 | _socketSend(wireMessage: WireMessage) { 644 | this._trace('Client._socketSend', wireMessage); 645 | this.socket && this.socket.send(wireMessage.encode()); 646 | // We have proved to the server we are alive. 647 | this.sendPinger && this.sendPinger.reset(); 648 | } 649 | 650 | /** @ignore */ 651 | _receivePublish(wireMessage: PublishMessage) { 652 | const { payloadMessage, messageIdentifier } = wireMessage; 653 | invariant(payloadMessage, format(ERROR.INVALID_STATE, ['PUBLISH WireMessage with no payloadMessage'])); 654 | switch (payloadMessage.qos) { 655 | case 0: 656 | this._receiveMessage(wireMessage); 657 | break; 658 | 659 | case 1: 660 | invariant(messageIdentifier, format(ERROR.INVALID_STATE, ['QoS 1 WireMessage with no messageIdentifier'])); 661 | this._scheduleMessage(new WireMessage(MESSAGE_TYPE.PUBACK, { messageIdentifier })); 662 | this._receiveMessage(wireMessage); 663 | break; 664 | 665 | case 2: 666 | invariant(messageIdentifier, format(ERROR.INVALID_STATE, ['QoS 2 WireMessage with no messageIdentifier'])); 667 | this._receivedMessagesAwaitingAckConfirm[messageIdentifier.toString()] = wireMessage; 668 | this.store('Received:', wireMessage); 669 | this._scheduleMessage(new WireMessage(MESSAGE_TYPE.PUBREC, { messageIdentifier })); 670 | break; 671 | 672 | default: 673 | throw Error('Invaild qos=' + payloadMessage.qos); 674 | } 675 | } 676 | 677 | /** @ignore */ 678 | _receiveMessage(wireMessage: PublishMessage) { 679 | if (this.onMessageArrived) { 680 | this.onMessageArrived(wireMessage.payloadMessage); 681 | } 682 | } 683 | 684 | /** 685 | * Client has disconnected either at its own request or because the server 686 | * or network disconnected it. Remove all non-durable state. 687 | * @param {number} [errorCode] the error number. 688 | * @param {string} [errorText] the error text. 689 | * @ignore 690 | */ 691 | _disconnected(errorCode?: number, errorText?: string) { 692 | this._trace('Client._disconnected', errorCode, errorText); 693 | 694 | this.sendPinger && this.sendPinger.cancel(); 695 | if (this._connectTimeout) { 696 | clearTimeout(this._connectTimeout); 697 | } 698 | // Clear message buffers. 699 | this._messagesAwaitingDispatch = []; 700 | 701 | if (this.socket) { 702 | // Cancel all socket callbacks so that they cannot be driven again by this socket. 703 | this.socket.onopen = () => null; 704 | this.socket.onmessage = () => null; 705 | this.socket.onerror = () => null; 706 | this.socket.onclose = () => null; 707 | if (this.socket.readyState === 1) { 708 | this.socket.close(); 709 | } 710 | this.socket = null; 711 | } 712 | 713 | if (errorCode === undefined) { 714 | errorCode = ERROR.OK.code; 715 | errorText = format(ERROR.OK); 716 | } 717 | 718 | // Run any application callbacks last as they may attempt to reconnect and hence create a new socket. 719 | if (this.connected) { 720 | this.connected = false; 721 | // Execute the onConnectionLost callback if there is one, and we were connected. 722 | this.onConnectionLost && this.onConnectionLost({ errorCode: errorCode, errorMessage: errorText }); 723 | } else { 724 | // Otherwise we never had a connection, so indicate that the connect has failed. 725 | if (this.connectOptions && this.connectOptions.onFailure) { 726 | this.connectOptions.onFailure(new Error(errorText)); 727 | } 728 | } 729 | } 730 | 731 | /** @ignore */ 732 | _trace(...args: any) { 733 | // Pass trace message back to client's callback function 734 | const traceFunction = this.traceFunction; 735 | if (traceFunction) { 736 | traceFunction({ severity: 'Debug', message: args.map(a => JSON.stringify(a)).join('')}); 737 | } 738 | 739 | //buffer style trace 740 | if (this._traceBuffer !== null) { 741 | for (let i = 0, max = args.length; i < max; i++) { 742 | if (this._traceBuffer.length === this._MAX_TRACE_ENTRIES) { 743 | this._traceBuffer.shift(); 744 | } 745 | if (i === 0 || typeof args[i] === 'undefined') { 746 | this._traceBuffer.push(args[i]); 747 | } else { 748 | this._traceBuffer.push(' ' + JSON.stringify(args[i])); 749 | } 750 | } 751 | } 752 | } 753 | } 754 | 755 | export default ClientImplementation; 756 | -------------------------------------------------------------------------------- /src/ConnectMessage.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { encodeMultiByteInteger, lengthOfUTF8, writeString, writeUint16 } from './util'; 4 | import { MESSAGE_TYPE, MqttProtoIdentifierv3, MqttProtoIdentifierv4 } from './constants'; 5 | import Message from './Message'; 6 | 7 | type ConnectOptions = { 8 | cleanSession: boolean; 9 | userName: ?string; 10 | password: ?string; 11 | mqttVersion: 3 | 4; 12 | willMessage: ?Message; 13 | keepAliveInterval: number; 14 | clientId: string; 15 | } 16 | 17 | export default class { 18 | options: ConnectOptions; 19 | 20 | constructor(options: ConnectOptions) { 21 | this.options = options; 22 | } 23 | 24 | encode(): ArrayBuffer { 25 | const options = this.options; 26 | 27 | // Compute the first byte of the fixed header 28 | const first = ((MESSAGE_TYPE.CONNECT & 0x0f) << 4); 29 | 30 | /* 31 | * Now calculate the length of the variable header + payload by adding up the lengths 32 | * of all the component parts 33 | */ 34 | 35 | let remLength = 0; 36 | let willMessagePayloadBytes; 37 | 38 | 39 | switch (options.mqttVersion) { 40 | case 3: 41 | remLength += MqttProtoIdentifierv3.length + 3; 42 | break; 43 | case 4: 44 | remLength += MqttProtoIdentifierv4.length + 3; 45 | break; 46 | } 47 | 48 | remLength += lengthOfUTF8(options.clientId) + 2; 49 | if (options.willMessage) { 50 | willMessagePayloadBytes = options.willMessage.payloadBytes; 51 | // Will message is always a string, sent as UTF-8 characters with a preceding length. 52 | remLength += lengthOfUTF8(options.willMessage.destinationName) + 2; 53 | if (!(willMessagePayloadBytes instanceof Uint8Array)) { 54 | willMessagePayloadBytes = new Uint8Array(willMessagePayloadBytes); 55 | } 56 | remLength += willMessagePayloadBytes.byteLength + 2; 57 | } 58 | if (options.userName) { 59 | remLength += lengthOfUTF8(options.userName) + 2; 60 | if (options.password) { 61 | remLength += lengthOfUTF8(options.password) + 2; 62 | } 63 | } 64 | 65 | // Now we can allocate a buffer for the message 66 | 67 | const mbi = encodeMultiByteInteger(remLength); // Convert the length to MQTT MBI format 68 | let pos = mbi.length + 1; // Offset of start of variable header 69 | const buffer = new ArrayBuffer(remLength + pos); 70 | const byteStream = new Uint8Array(buffer); // view it as a sequence of bytes 71 | 72 | //Write the fixed header into the buffer 73 | byteStream[0] = first; 74 | byteStream.set(mbi, 1); 75 | 76 | switch (options.mqttVersion) { 77 | case 3: 78 | byteStream.set(MqttProtoIdentifierv3, pos); 79 | pos += MqttProtoIdentifierv3.length; 80 | break; 81 | case 4: 82 | byteStream.set(MqttProtoIdentifierv4, pos); 83 | pos += MqttProtoIdentifierv4.length; 84 | break; 85 | } 86 | let connectFlags = 0; 87 | if (options.cleanSession) { 88 | connectFlags = 0x02; 89 | } 90 | if (options.willMessage) { 91 | connectFlags |= 0x04; 92 | connectFlags |= (options.willMessage.qos << 3); 93 | if (options.willMessage.retained) { 94 | connectFlags |= 0x20; 95 | } 96 | } 97 | if (options.userName) { 98 | connectFlags |= 0x80; 99 | } 100 | if (options.password) { 101 | connectFlags |= 0x40; 102 | } 103 | byteStream[pos++] = connectFlags; 104 | pos = writeUint16(options.keepAliveInterval, byteStream, pos); 105 | 106 | pos = writeString(options.clientId, lengthOfUTF8(options.clientId), byteStream, pos); 107 | if (options.willMessage) { 108 | willMessagePayloadBytes = options.willMessage.payloadBytes; 109 | pos = writeString(options.willMessage.destinationName, lengthOfUTF8(options.willMessage.destinationName), byteStream, pos); 110 | pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos); 111 | byteStream.set(willMessagePayloadBytes, pos); 112 | pos += willMessagePayloadBytes.byteLength; 113 | 114 | } 115 | if (options.userName) { 116 | pos = writeString(options.userName, lengthOfUTF8(options.userName), byteStream, pos); 117 | if (options.password) { 118 | writeString(options.password, lengthOfUTF8(options.password), byteStream, pos); 119 | } 120 | } 121 | return buffer; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Message.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { format, lengthOfUTF8, parseUTF8, stringToUTF8 } from './util'; 4 | import { ERROR } from './constants'; 5 | 6 | type Payload = string | Uint8Array; 7 | 8 | /** 9 | * An application message, sent or received. 10 | *

11 | * All attributes may be null, which implies the default values. 12 | * 13 | * @name Message 14 | * @constructor 15 | * @param {String|ArrayBuffer} newPayload The message data to be sent. 16 | *

17 | * @property {string} payloadString read only The payload as a string if the payload consists of valid UTF-8 characters. 18 | * @property {ArrayBuffer} payloadBytes read only The payload as an ArrayBuffer. 19 | *

20 | * @property {string} destinationName mandatory The name of the destination to which the message is to be sent 21 | * (for messages about to be sent) or the name of the destination from which the message has been received. 22 | * (for messages received by the onMessage function). 23 | *

24 | * @property {number} qos The Quality of Service used to deliver the message. 25 | *

26 | *
0 Best effort (default). 27 | *
1 At least once. 28 | *
2 Exactly once. 29 | *
30 | *

31 | * @property {Boolean} retained If true, the message is to be retained by the server and delivered 32 | * to both current and future subscriptions. 33 | * If false the server only delivers the message to current subscribers, this is the default for new Messages. 34 | * A received message has the retained boolean set to true if the message was published 35 | * with the retained boolean set to true 36 | * and the subscrption was made after the message has been published. 37 | *

38 | * @property {Boolean} duplicate read only If true, this message might be a duplicate of one which has already been received. 39 | * This is only set on messages received from the server. 40 | * 41 | */ 42 | export default class { 43 | _payload: Payload; 44 | _destinationName: string; 45 | _qos: 0 | 1 | 2 = 0; 46 | _retained: boolean = false; 47 | _duplicate: boolean = false; 48 | 49 | constructor(newPayload: Payload) { 50 | if (!(typeof newPayload === 'string' 51 | || newPayload instanceof Uint8Array 52 | )) { 53 | throw (format(ERROR.INVALID_ARGUMENT, [newPayload.toString(), 'newPayload'])); 54 | } 55 | this._payload = newPayload; 56 | } 57 | 58 | get payloadString(): string { 59 | if (typeof this._payload === 'string') { 60 | return this._payload; 61 | } else { 62 | return parseUTF8(this._payload, 0, this._payload.byteLength); 63 | } 64 | } 65 | 66 | get payloadBytes(): Uint8Array { 67 | const payload = this._payload; 68 | if (typeof payload === 'string') { 69 | const buffer = new ArrayBuffer(lengthOfUTF8(payload)); 70 | const byteStream = new Uint8Array(buffer); 71 | stringToUTF8(payload, byteStream, 0); 72 | 73 | return byteStream; 74 | } else { 75 | return payload; 76 | } 77 | } 78 | 79 | get destinationName(): string { 80 | return this._destinationName; 81 | } 82 | 83 | set destinationName(newDestinationName: string) { 84 | if (typeof newDestinationName === 'string') { 85 | this._destinationName = newDestinationName; 86 | } else { 87 | throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, 'newDestinationName'])); 88 | } 89 | } 90 | 91 | get qos(): 0 | 1 | 2 { 92 | return this._qos; 93 | } 94 | 95 | set qos(newQos: 0 | 1 | 2) { 96 | if (newQos === 0 || newQos === 1 || newQos === 2) { 97 | this._qos = newQos; 98 | } else { 99 | throw new Error('Invalid argument:' + newQos); 100 | } 101 | } 102 | 103 | get retained(): boolean { 104 | return this._retained; 105 | } 106 | 107 | set retained(newRetained: boolean) { 108 | if (typeof newRetained === 'boolean') { 109 | this._retained = newRetained; 110 | } else { 111 | throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, 'newRetained'])); 112 | } 113 | } 114 | 115 | get duplicate(): boolean { 116 | return this._duplicate; 117 | } 118 | 119 | set duplicate(newDuplicate: boolean) { 120 | if (typeof newDuplicate === 'boolean') { 121 | this._duplicate = newDuplicate; 122 | } else { 123 | throw new Error(format(ERROR.INVALID_ARGUMENT, [newDuplicate, 'newDuplicate'])); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Pinger.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | 3 | import WireMessage from './WireMessage'; 4 | import { MESSAGE_TYPE } from './constants'; 5 | import ClientImplementation from './ClientImplementation'; 6 | 7 | /** 8 | * Repeat keepalive requests, monitor responses. 9 | * @ignore 10 | */ 11 | export default class { 12 | _client: ClientImplementation; 13 | _keepAliveIntervalMs: number; 14 | pingReq: ArrayBuffer = new WireMessage(MESSAGE_TYPE.PINGREQ).encode(); 15 | timeout: ?number; 16 | 17 | constructor(client: ClientImplementation, keepAliveIntervalSeconds: number) { 18 | this._client = client; 19 | this._keepAliveIntervalMs = keepAliveIntervalSeconds * 1000; 20 | this.reset(); 21 | } 22 | 23 | _doPing() { 24 | this._client._trace('Pinger.doPing', 'send PINGREQ'); 25 | this._client.socket && this._client.socket.send(this.pingReq); 26 | this.timeout = setTimeout(() => this._doPing(), this._keepAliveIntervalMs); 27 | } 28 | 29 | reset() { 30 | if (this.timeout) { 31 | clearTimeout(this.timeout); 32 | this.timeout = null; 33 | } 34 | if (this._keepAliveIntervalMs > 0) { 35 | this.timeout = setTimeout(() => this._doPing(), this._keepAliveIntervalMs); 36 | } 37 | } 38 | 39 | cancel() { 40 | clearTimeout(this.timeout); 41 | this.timeout = null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PublishMessage.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { encodeMultiByteInteger, lengthOfUTF8, writeString, writeUint16 } from './util'; 4 | import { MESSAGE_TYPE } from './constants'; 5 | import Message from './Message'; 6 | 7 | /** 8 | * Construct an MQTT wire protocol message. 9 | * @param type MQTT packet type. 10 | * @param options optional wire message attributes. 11 | * 12 | * Optional properties 13 | * 14 | * messageIdentifier: message ID in the range [0..65535] 15 | * payloadMessage: Application Message - PUBLISH only 16 | * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE) 17 | * requestQoS: array of QoS values [0..2] 18 | * 19 | * @private 20 | * @ignore 21 | */ 22 | export default class { 23 | type: number; 24 | messageIdentifier: ?number = null; 25 | payloadMessage: Message; 26 | onDispatched: ?() => void; 27 | pubRecReceived: ?boolean; 28 | sequence: ?number; 29 | 30 | constructor(payloadMessage: Message, messageIdentifier: ?number = null) { 31 | this.type = MESSAGE_TYPE.PUBLISH; 32 | this.payloadMessage = payloadMessage; 33 | this.messageIdentifier = messageIdentifier; 34 | } 35 | 36 | encode(): ArrayBuffer { 37 | // Compute the first byte of the fixed header 38 | let first = ((this.type & 0x0f) << 4); 39 | 40 | /* 41 | * Now calculate the length of the variable header + payload by adding up the lengths 42 | * of all the component parts 43 | */ 44 | 45 | let remLength = 0; 46 | let payloadBytes: ?Uint8Array; 47 | 48 | // if the message contains a messageIdentifier then we need two bytes for that 49 | if (this.messageIdentifier) { 50 | remLength += 2; 51 | } 52 | 53 | //Publish 54 | if (this.payloadMessage.duplicate) { 55 | first |= 0x08; 56 | } 57 | first = first |= (this.payloadMessage.qos << 1); 58 | if (this.payloadMessage.retained) { 59 | first |= 0x01; 60 | } 61 | payloadBytes = this.payloadMessage.payloadBytes; 62 | const destinationNameLength = lengthOfUTF8(this.payloadMessage.destinationName); 63 | remLength += destinationNameLength + 2; 64 | remLength += payloadBytes.byteLength; 65 | //End publish 66 | 67 | // Now we can allocate a buffer for the message 68 | 69 | const mbi = encodeMultiByteInteger(remLength); // Convert the length to MQTT MBI format 70 | let pos = mbi.length + 1; // Offset of start of variable header 71 | const buffer = new ArrayBuffer(remLength + pos); 72 | const byteStream = new Uint8Array(buffer); // view it as a sequence of bytes 73 | 74 | //Write the fixed header into the buffer 75 | byteStream[0] = first; 76 | byteStream.set(mbi, 1); 77 | 78 | pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos); 79 | 80 | // Output the messageIdentifier - if there is one 81 | if (this.messageIdentifier) { 82 | pos = writeUint16(this.messageIdentifier, byteStream, pos); 83 | } 84 | 85 | // PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters. 86 | payloadBytes && byteStream.set(payloadBytes, pos); 87 | 88 | return buffer; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/WireMessage.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { encodeMultiByteInteger, format, lengthOfUTF8, writeString, writeUint16 } from './util'; 4 | import { ERROR, MESSAGE_TYPE } from './constants'; 5 | 6 | /** 7 | * Construct an MQTT wire protocol message. 8 | * @param type MQTT packet type. 9 | * @param options optional wire message attributes. 10 | * 11 | * Optional properties 12 | * 13 | * messageIdentifier: message ID in the range [0..65535] 14 | * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE) 15 | * requestQoS: array of QoS values [0..2] 16 | * 17 | * @private 18 | * @ignore 19 | */ 20 | export default class { 21 | type: number; 22 | messageIdentifier: ?number = null; 23 | clientId: ?string; 24 | 25 | //CONNACK only 26 | returnCode: ?(number | Uint8Array); 27 | sessionPresent: ?boolean; 28 | onDispatched: ?() => void; 29 | 30 | //PUB/SUB flows only 31 | subAckReceived: ?(grantedQos: number) => void; 32 | onFailure: ?(Error) => void; 33 | timeOut: ?number; 34 | unSubAckReceived: ?() => void; 35 | topics: ?string[]; 36 | requestedQos: ?(0 | 1 | 2)[]; 37 | 38 | sequence: ?number; 39 | 40 | constructor(type: number, options: { 41 | messageIdentifier?: number; 42 | topics?: string[]; 43 | requestedQos?: (0 | 1 | 2)[]; 44 | clientId?: string 45 | } = {}) { 46 | this.type = type; 47 | const self: Object = this; 48 | Object.keys(options).forEach((name) => { 49 | self[name] = options[name]; 50 | }); 51 | } 52 | 53 | encode(): ArrayBuffer { 54 | // Compute the first byte of the fixed header 55 | let first = ((this.type & 0x0f) << 4); 56 | 57 | /* 58 | * Now calculate the length of the variable header + payload by adding up the lengths 59 | * of all the component parts 60 | */ 61 | 62 | let remLength = 0; 63 | const topicStrLength = []; 64 | 65 | // if the message contains a messageIdentifier then we need two bytes for that 66 | if (this.messageIdentifier) { 67 | remLength += 2; 68 | } 69 | 70 | switch (this.type) { 71 | // Subscribe, Unsubscribe can both contain topic strings 72 | case MESSAGE_TYPE.SUBSCRIBE: 73 | first |= 0x02; // Qos = 1; 74 | if (!this.topics) { 75 | throw new Error(format(ERROR.INVALID_STATE, ['SUBSCRIBE WireMessage with no topics'])); 76 | } 77 | if (!this.requestedQos) { 78 | throw new Error(format(ERROR.INVALID_STATE, ['SUBSCRIBE WireMessage with no requestedQos'])); 79 | } 80 | for (let i = 0; i < this.topics.length; i++) { 81 | topicStrLength[i] = lengthOfUTF8(this.topics[i]); 82 | remLength += topicStrLength[i] + 2; 83 | } 84 | remLength += this.requestedQos.length; // 1 byte for each topic's Qos 85 | // QoS on Subscribe only 86 | break; 87 | 88 | case MESSAGE_TYPE.UNSUBSCRIBE: 89 | first |= 0x02; // Qos = 1; 90 | if (!this.topics) { 91 | throw new Error(format(ERROR.INVALID_STATE, ['UNSUBSCRIBE WireMessage with no topics'])); 92 | } 93 | for (let i = 0; i < this.topics.length; i++) { 94 | topicStrLength[i] = lengthOfUTF8(this.topics[i]); 95 | remLength += topicStrLength[i] + 2; 96 | } 97 | break; 98 | 99 | case MESSAGE_TYPE.PUBREL: 100 | first |= 0x02; // Qos = 1; 101 | break; 102 | 103 | case MESSAGE_TYPE.DISCONNECT: 104 | break; 105 | 106 | default: 107 | } 108 | 109 | // Now we can allocate a buffer for the message 110 | 111 | const mbi = encodeMultiByteInteger(remLength); // Convert the length to MQTT MBI format 112 | let pos = mbi.length + 1; // Offset of start of variable header 113 | const buffer = new ArrayBuffer(remLength + pos); 114 | const byteStream = new Uint8Array(buffer); // view it as a sequence of bytes 115 | 116 | //Write the fixed header into the buffer 117 | byteStream[0] = first; 118 | byteStream.set(mbi, 1); 119 | 120 | // Output the messageIdentifier - if there is one 121 | if (this.messageIdentifier) { 122 | pos = writeUint16(this.messageIdentifier, byteStream, pos); 123 | } 124 | 125 | switch (this.type) { 126 | case MESSAGE_TYPE.SUBSCRIBE: 127 | if (!this.topics) { 128 | throw new Error(format(ERROR.INVALID_STATE, ['SUBSCRIBE WireMessage with no topics'])); 129 | } 130 | // SUBSCRIBE has a list of topic strings and request QoS 131 | for (let i = 0; i < this.topics.length; i++) { 132 | pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos); 133 | if (!this.requestedQos || typeof this.requestedQos[i] === 'undefined') { 134 | throw new Error(format(ERROR.INVALID_STATE, ['SUBSCRIBE WireMessage topic with no corresponding requestedQos'])); 135 | } 136 | byteStream[pos++] = this.requestedQos[i]; 137 | } 138 | break; 139 | 140 | case MESSAGE_TYPE.UNSUBSCRIBE: 141 | if (!this.topics) { 142 | throw new Error(format(ERROR.INVALID_STATE, ['UNSUBSCRIBE WireMessage with no topics'])); 143 | } 144 | // UNSUBSCRIBE has a list of topic strings 145 | for (let i = 0; i < this.topics.length; i++) { 146 | pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos); 147 | } 148 | break; 149 | 150 | default: 151 | // Do nothing. 152 | } 153 | 154 | return buffer; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_KEEPALIVE_SECONDS = 60; 2 | 3 | /** 4 | * Unique message type identifiers, with associated 5 | * associated integer values. 6 | * @private 7 | */ 8 | export const ERROR = { 9 | OK: { code: 0, text: 'AMQJSC0000I OK.' }, 10 | CONNECT_TIMEOUT: { code: 1, text: 'AMQJSC0001E Connect timed out.' }, 11 | SUBSCRIBE_TIMEOUT: { code: 2, text: 'AMQJS0002E Subscribe timed out.' }, 12 | UNSUBSCRIBE_TIMEOUT: { code: 3, text: 'AMQJS0003E Unsubscribe timed out.' }, 13 | PING_TIMEOUT: { code: 4, text: 'AMQJS0004E Ping timed out.' }, 14 | INTERNAL_ERROR: { code: 5, text: 'AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}' }, 15 | CONNACK_RETURNCODE: { code: 6, text: 'AMQJS0006E Bad Connack return code:{0} {1}.' }, 16 | SOCKET_ERROR: { code: 7, text: 'AMQJS0007E Socket error:{0}.' }, 17 | SOCKET_CLOSE: { code: 8, text: 'AMQJS0008I Socket closed.' }, 18 | MALFORMED_UTF: { code: 9, text: 'AMQJS0009E Malformed UTF data:{0} {1} {2}.' }, 19 | UNSUPPORTED: { code: 10, text: 'AMQJS0010E {0} is not supported by this browser.' }, 20 | INVALID_STATE: { code: 11, text: 'AMQJS0011E Invalid state {0}.' }, 21 | INVALID_TYPE: { code: 12, text: 'AMQJS0012E Invalid type {0} for {1}.' }, 22 | INVALID_ARGUMENT: { code: 13, text: 'AMQJS0013E Invalid argument {0} for {1}.' }, 23 | UNSUPPORTED_OPERATION: { code: 14, text: 'AMQJS0014E Unsupported operation.' }, 24 | INVALID_STORED_DATA: { code: 15, text: 'AMQJS0015E Invalid data in local storage key={0} value={1}.' }, 25 | INVALID_MQTT_MESSAGE_TYPE: { code: 16, text: 'AMQJS0016E Invalid MQTT message type {0}.' }, 26 | MALFORMED_UNICODE: { code: 17, text: 'AMQJS0017E Malformed Unicode string:{0} {1}.' } 27 | }; 28 | 29 | /** 30 | * Unique message type identifiers, with associated 31 | * associated integer values. 32 | * @private 33 | */ 34 | export const MESSAGE_TYPE = { 35 | CONNECT: 1, 36 | CONNACK: 2, 37 | PUBLISH: 3, 38 | PUBACK: 4, 39 | PUBREC: 5, 40 | PUBREL: 6, 41 | PUBCOMP: 7, 42 | SUBSCRIBE: 8, 43 | SUBACK: 9, 44 | UNSUBSCRIBE: 10, 45 | UNSUBACK: 11, 46 | PINGREQ: 12, 47 | PINGRESP: 13, 48 | DISCONNECT: 14 49 | }; 50 | 51 | /** CONNACK RC Meaning. */ 52 | export const CONNACK_RC = { 53 | 0: 'Connection Accepted', 54 | 1: 'Connection Refused: unacceptable protocol version', 55 | 2: 'Connection Refused: identifier rejected', 56 | 3: 'Connection Refused: server unavailable', 57 | 4: 'Connection Refused: bad user name or password', 58 | 5: 'Connection Refused: not authorized' 59 | }; 60 | 61 | //MQTTv3.1 protocol and version 6 M Q I s d p 3 62 | export const MqttProtoIdentifierv3 = [0x00, 0x06, 0x4d, 0x51, 0x49, 0x73, 0x64, 0x70, 0x03]; 63 | //MQTTv4 protocol and version 4 M Q T T 4 64 | export const MqttProtoIdentifierv4 = [0x00, 0x04, 0x4d, 0x51, 0x54, 0x54, 0x04]; 65 | -------------------------------------------------------------------------------- /src/header.txt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2013, 2014 IBM Corp. 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Eclipse Distribution License v1.0 which accompany this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * and the Eclipse Distribution License is available at 11 | * http://www.eclipse.org/org/documents/edl-v10.php. 12 | * 13 | *******************************************************************************/ 14 | 15 | -------------------------------------------------------------------------------- /src/oldtests/BasicTest-spec.js: -------------------------------------------------------------------------------- 1 | require('./hacks'); 2 | var settings = require('./client-harness'); 3 | 4 | var testServer = settings.server; 5 | var testPort = settings.port; 6 | var testPath = settings.path; 7 | var testMqttVersion = settings.mqttVersion; 8 | 9 | var genStr = function (str) { 10 | var time = new Date(); 11 | return str + '.' + time.getTime(); 12 | }; 13 | 14 | describe('BasicTest', function () { 15 | //var client = null; 16 | var clientId = this.description; 17 | var connected = false; 18 | var failure = false; 19 | var disconnectError = null; 20 | var disconnectErrorMsg = null; 21 | 22 | var subscribed = false; 23 | var isMessageReceived = false; 24 | var isMessageDelivered = false; 25 | var strTopic = '/' + makeid() + '/World'; 26 | var strMessageReceived = ''; 27 | var strMessageSend = 'Hello'; 28 | var strTopicReceived = ''; 29 | 30 | 31 | function makeid() { 32 | var text = ''; 33 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 34 | 35 | for (var i = 0; i < 5; i++) { 36 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 37 | } 38 | 39 | return text; 40 | } 41 | 42 | function onConnectSuccess(responseObj) { 43 | //console.log("connected %s",responseObj); 44 | connected = true; 45 | }; 46 | 47 | function onConnectionLost(err) { 48 | connected = false; 49 | if (err) { 50 | disconnectError = err.errorCode; 51 | disconnectErrorMsg = err.errorMessage; 52 | } 53 | console.log('connection lost. ErrorCode: %s, ErrorMsg: %s', disconnectError, disconnectErrorMsg); 54 | }; 55 | 56 | function onConnectFailure(err) { 57 | connected = false; 58 | console.log('Connect fail. ErrorCode: %s, ErrorMsg: %s', err.errCode, err.errorMessage); 59 | }; 60 | 61 | function onSubscribeSuccess() { 62 | subscribed = true; 63 | //console.log("subscribed",subscribed); 64 | }; 65 | 66 | function onSubscribeFailure(err) { 67 | subscribed = false; 68 | console.log('subscribe fail. ErrorCode: %s, ErrorMsg: %s', err.errCode, err.errorMessage); 69 | }; 70 | function onUnsubscribeSuccess() { 71 | subscribed = false; 72 | //console.log("unsubscribed",subscribed); 73 | }; 74 | 75 | function messageDelivered(response) { 76 | console.log('messageDelivered. topic:%s, duplicate:%s, payloadString:%s,qos:%s,retained:%s', response.destinationName, response.duplicate, response.payloadString, response.qos, response.retained); 77 | isMessageDelivered = true; 78 | //reponse.invocationContext.onMessageArrived = null; 79 | }; 80 | 81 | function messageArrived(message) { 82 | console.log('messageArrived.', 'topic:', message.destinationName, ' ;content:', message.payloadString); 83 | isMessageReceived = true; 84 | strMessageReceived = message.payloadString; 85 | strTopicReceived = message.destinationName; 86 | 87 | //reponse.invocationContext.onMessageArrived = null; 88 | }; 89 | 90 | beforeEach(function () { 91 | connected = false; 92 | failure = false; 93 | disconnectError = null; 94 | disconnectErrorMsg = null; 95 | //if(!client){ 96 | // client = new Paho.MQTT.Client(testServer, testPort, clientId); 97 | //} 98 | }); 99 | 100 | it('it should connect to a list of server(HA connection).', function () { 101 | var defaultServer = testServer; 102 | var defaultPort = testPort; 103 | var arrHosts = ['localhost', testServer,]; 104 | var arrPorts = [2000, testPort]; 105 | 106 | var client = new Paho.MQTT.Client({ 107 | host: defaultServer, 108 | port: defaultPort, 109 | path: testPath, 110 | clientId: genStr(clientId) 111 | }); 112 | client.onConnectionLost = onConnectionLost; 113 | expect(client).not.toBe(null); 114 | 115 | console.log('should connect to a available server from list'); 116 | runs(function () { 117 | client.connect({ 118 | onSuccess: onConnectSuccess, 119 | onFailure: onConnectFailure, 120 | hosts: arrHosts, 121 | ports: arrPorts, 122 | mqttVersion: testMqttVersion 123 | }); 124 | }); 125 | 126 | waitsFor(function () { 127 | return connected; 128 | }, 'the client should connect', 2000); 129 | 130 | runs(function () { 131 | expect(connected).toBe(true); 132 | //client.disconnect(); 133 | }); 134 | 135 | }); 136 | 137 | 138 | it('it should publish and subscribe.', function () { 139 | 140 | var client = new Paho.MQTT.Client({ host: testServer, port: testPort, path: testPath, clientId: genStr(clientId) }); 141 | client.onMessageArrived = messageArrived; 142 | client.onMessageDelivered = messageDelivered; 143 | 144 | runs(function () { 145 | client.connect({ onSuccess: onConnectSuccess, onFailure: onConnectFailure, mqttVersion: testMqttVersion }); 146 | }); 147 | waitsFor(function () { 148 | return connected; 149 | }, 'the client should connect', 2000); 150 | runs(function () { 151 | expect(connected).toBe(true); 152 | }); 153 | 154 | console.log('Subscribe a topic...'); 155 | runs(function () { 156 | client.subscribe(strTopic, { onSuccess: onSubscribeSuccess, onFailure: onSubscribeFailure }); 157 | }); 158 | 159 | waitsFor(function () { 160 | return subscribed; 161 | }, 'the client should subscribe', 2000); 162 | 163 | runs(function () { 164 | expect(subscribed).toBe(true); 165 | }); 166 | 167 | console.log('Send and receive message...'); 168 | runs(function () { 169 | var message = new Paho.MQTT.Message(strMessageSend); 170 | message.destinationName = strTopic; 171 | client.send(message); 172 | }); 173 | waitsFor(function () { 174 | return isMessageReceived; 175 | }, 'the client should send and receive a message', 2000); 176 | runs(function () { 177 | //to do Check message sent 178 | expect(isMessageDelivered).toBe(true); 179 | //Check msg received 180 | expect(isMessageReceived).toBe(true); 181 | //Check message 182 | expect(strMessageReceived).toEqual(strMessageSend); 183 | //Check topic 184 | expect(strTopicReceived).toEqual(strTopic); 185 | 186 | //disconnect 187 | //client.disconnect(); 188 | 189 | }); 190 | 191 | console.log('Unsubscribe a topic...'); 192 | runs(function () { 193 | client.unsubscribe(strTopic, { onSuccess: onUnsubscribeSuccess }); 194 | }); 195 | waitsFor(function () { 196 | return !subscribed; 197 | }, 'the client should subscribe', 2000); 198 | runs(function () { 199 | expect(subscribed).toBe(false); 200 | //disconnect 201 | //client.disconnect(); 202 | }); 203 | 204 | //should not receive a message after unsubscribe 205 | runs(function () { 206 | //console.log('isMessageReceived',isMessageReceived); 207 | isMessageDelivered = false; 208 | isMessageReceived = false; 209 | strMessageReceived = ''; 210 | strTopicReceived = ''; 211 | 212 | var message = new Paho.MQTT.Message(genStr(strMessageSend)); 213 | message.destinationName = strTopic; 214 | client.send(message); 215 | }); 216 | waitsFor(function () { 217 | return isMessageDelivered; 218 | }, 'the client should send and receive a message', 2000); 219 | 220 | runs(function () { 221 | //to do Check message sent 222 | expect(isMessageDelivered).toBe(true); 223 | //Check msg received 224 | expect(isMessageReceived).toBe(false); 225 | //Check message 226 | expect(strMessageReceived).toEqual(''); 227 | //Check topic 228 | expect(strTopicReceived).toEqual(''); 229 | 230 | //disconnect 231 | //client.disconnect(); 232 | 233 | }); 234 | }); 235 | 236 | }); 237 | -------------------------------------------------------------------------------- /src/oldtests/hacks.js: -------------------------------------------------------------------------------- 1 | import { Client, Message } from '../..'; 2 | 3 | //global.window = global; 4 | global.Paho = { 5 | MQTT: { 6 | Client: Client, 7 | Message: Message 8 | } 9 | }; 10 | 11 | // This is the equivalent of the old waitsFor/runs syntax 12 | // which was removed from Jasmine 2 13 | global.runs = global.waitsFor = function (escapeFunction, runFunction, escapeTime) { 14 | // check the escapeFunction every millisecond so as soon as it is met we can escape the function 15 | var interval = setInterval(function () { 16 | if (escapeFunction()) { 17 | clearMe(); 18 | runFunction(); 19 | } 20 | }, 1); 21 | 22 | // in case we never reach the escapeFunction, we will time out 23 | // at the escapeTime 24 | var timeOut = setTimeout(function () { 25 | clearMe(); 26 | runFunction(); 27 | }, escapeTime); 28 | 29 | // clear the interval and the timeout 30 | function clearMe() { 31 | clearInterval(interval); 32 | clearTimeout(timeOut); 33 | } 34 | }; 35 | 36 | global.waits = function (ms) { 37 | var start = new Date().getTime(), expire = start + ms; 38 | while (new Date().getTime() < expire) { 39 | } 40 | return; 41 | }; 42 | 43 | global.WebSocket = require('websocket').w3cwebsocket; 44 | 45 | 46 | var LocalStorage = require('node-localstorage').LocalStorage; 47 | global.localStorage = new LocalStorage('./persistence'); 48 | 49 | require('../Client'); 50 | -------------------------------------------------------------------------------- /src/oldtests/interops-spec.js: -------------------------------------------------------------------------------- 1 | require('./hacks'); 2 | var settings = require('./client-harness'); 3 | 4 | var testServer = settings.interopServer; 5 | var testPort = settings.interopPort; 6 | var testPath = settings.interopPath; 7 | var testMqttVersion = 4; 8 | 9 | var genStr = function (str) { 10 | var time = new Date(); 11 | return str + '.' + time.getTime(); 12 | }; 13 | 14 | var topics = ['TopicA', 'TopicA/B', 'Topic/C', 'TopicA/C', '/TopicA']; 15 | var wildtopics = ['TopicA/+', '+/C', '#', '/#', '/+', '+/+', 'TopicA/#']; 16 | var nosubscribetopics = ['test/nosubscribe',]; 17 | 18 | describe('InteropsTests', function () { 19 | var clientId = this.description; 20 | var client = null; 21 | var failure = false; 22 | var subscribed = false; 23 | var disconnectError = null; 24 | var disconnectErrorMsg = null; 25 | 26 | var subscribed = false; 27 | var messageReceivedCount = 0; 28 | var messagePublishedCount = 0; 29 | var sendingComplete = false; 30 | var receivingComplete = false; 31 | 32 | beforeEach(function () { 33 | failure = false; 34 | subscribed = false; 35 | disconnectError = null; 36 | disconnectErrorMsg = null; 37 | messageReceivedCount = 0; 38 | messagePublishedCount = 0; 39 | sendingComplete = false; 40 | receivingComplete = false; 41 | }); 42 | 43 | afterEach(function () { 44 | if (client !== null && client.isConnected()) { 45 | client.disconnect(); 46 | } 47 | client = null; 48 | }); 49 | 50 | var callbacks = { 51 | onConnectionLost: function (err) { 52 | console.log('connectionLost ' + err.errorMessage); 53 | }, 54 | onMessageArrived: function (message) { 55 | console.log('messageArrived %s %s %s %s', message.destinationName, message.payloadString, message.qos, message.retained); 56 | messageReceivedCount++; 57 | if (messageReceivedCount == 3) { 58 | receivingComplete = true; 59 | } 60 | }, 61 | onConnectSuccess: function (response) { 62 | connected = true; 63 | }, 64 | onConnectFailure: function (err) { 65 | console.log('Connect failed %s %s', err.errCode, err.errorMessage); 66 | }, 67 | onDisconnectSuccess: function (response) { 68 | connected = false; 69 | console.log('Disconnected from server'); 70 | }, 71 | onDisconnectFailure: function (err) { 72 | console.log('Disconnect failed %s %s', err.errCode, err.errorMessage); 73 | }, 74 | onMessageDelivered: function (reponse) { 75 | messagePublishedCount++; 76 | if (messagePublishedCount == 3) { 77 | sendingComplete = true; 78 | } 79 | }, 80 | onSubscribeSuccess: function () { 81 | subscribed = true; 82 | }, 83 | }; 84 | 85 | it('should connect, disconnect, subscribe, publish and receive messages', function () { 86 | client = new Paho.MQTT.Client({ host: testServer, port: testPort, path: testPath, clientId: 'testclientid-js' }); 87 | client.onMessageArrived = callbacks.onMessageArrived; 88 | client.onMessageDelivered = callbacks.onMessageDelivered; 89 | 90 | expect(client).not.toBe(null); 91 | 92 | runs(function () { 93 | client.connect({ onSuccess: callbacks.onConnectSuccess, mqttVersion: testMqttVersion }); 94 | }); 95 | waitsFor(function () { 96 | return client.isConnected(); 97 | }, 'the client should connect', 5000); 98 | runs(function () { 99 | expect(client.isConnected()).toBe(true); 100 | }); 101 | 102 | runs(function () { 103 | client.disconnect(); 104 | }); 105 | waitsFor(function () { 106 | return true; 107 | }, 'the client should disconnect', 5000); 108 | runs(function () { 109 | expect(client.isConnected()).toBe(false); 110 | }); 111 | 112 | runs(function () { 113 | client.connect({ onSuccess: callbacks.onConnectSuccess, mqttVersion: testMqttVersion }); 114 | }); 115 | waitsFor(function () { 116 | return client.isConnected(); 117 | }, 'the client should connect again', 5000); 118 | runs(function () { 119 | expect(client.isConnected()).toBe(true); 120 | }); 121 | 122 | runs(function () { 123 | client.subscribe(topics[0], { qos: 2, onSuccess: callbacks.onSubscribeSuccess }); 124 | }); 125 | waitsFor(function () { 126 | return subscribed; 127 | }, 'the client should subscribe', 2000); 128 | runs(function () { 129 | expect(subscribed).toBe(true); 130 | }); 131 | 132 | runs(function () { 133 | for (var i = 0; i < 3; i++) { 134 | var message = new Paho.MQTT.Message('qos ' + i); 135 | message.destinationName = topics[0]; 136 | message.qos = i; 137 | client.send(message); 138 | } 139 | }); 140 | waitsFor(function () { 141 | return sendingComplete; 142 | }, 'the client should send 3 messages', 5000); 143 | waitsFor(function () { 144 | return receivingComplete; 145 | }, 'the client should receive 3 messages', 5000); 146 | runs(function () { 147 | expect(messagePublishedCount).toBe(3); 148 | expect(messageReceivedCount).toBe(3); 149 | }); 150 | 151 | runs(function () { 152 | client.disconnect({ onSuccess: callbacks.onDisconnectSuccess }); 153 | }); 154 | waitsFor(function () { 155 | return connected; 156 | }, 'the client should disconnect', 5000); 157 | runs(function () { 158 | expect(client.isConnected()).toBe(false); 159 | }); 160 | }); 161 | 162 | it('should connect, attempt to connect again and fail', function () { 163 | var exception = false; 164 | client = new Paho.MQTT.Client({ host: testServer, port: testPort, path: testPath, clientId: 'testclientid-js' }); 165 | expect(client).not.toBe(null); 166 | 167 | runs(function () { 168 | client.connect({ onSuccess: callbacks.onConnectSuccess, mqttVersion: testMqttVersion }); 169 | }); 170 | waitsFor(function () { 171 | return client.isConnected(); 172 | }, 'the client should connect', 5000); 173 | runs(function () { 174 | expect(client.isConnected()).toBe(true); 175 | }); 176 | 177 | runs(function () { 178 | try { 179 | client.connect({ onSuccess: callbacks.onConnectSuccess, mqttVersion: testMqttVersion }); 180 | } catch (e) { 181 | console.log(e.message); 182 | if (e.message == 'AMQJS0011E Invalid state already connected.') { 183 | exception = true; 184 | } 185 | } 186 | }); 187 | runs(function () { 188 | expect(exception).toBe(true); 189 | }); 190 | }); 191 | 192 | it('should connect successfully with a 0 length clientid with cleansession true', function () { 193 | client = new Paho.MQTT.Client({ host: testServer, port: testPort, path: testPath, clientId: '' }); 194 | expect(client).not.toBe(null); 195 | 196 | runs(function () { 197 | client.connect({ cleanSession: true, onSuccess: callbacks.onConnectSuccess, mqttVersion: testMqttVersion }); 198 | }); 199 | waitsFor(function () { 200 | return client.isConnected(); 201 | }, 'the client should connect', 5000); 202 | runs(function () { 203 | expect(client.isConnected()).toBe(true); 204 | }); 205 | 206 | runs(function () { 207 | client.disconnect(); 208 | }); 209 | waitsFor(function () { 210 | return true; 211 | }, 'the client should disconnect', 5000); 212 | runs(function () { 213 | expect(client.isConnected()).toBe(false); 214 | }); 215 | }); 216 | 217 | it('should fail to connect successfully with a 0 length clientid with cleansession false', function () { 218 | var connectFail = false; 219 | var failCallback = function (err) { 220 | connectFail = true; 221 | }; 222 | client = new Paho.MQTT.Client({ host: testServer, port: testPort, path: testPath, clientId: '' }); 223 | expect(client).not.toBe(null); 224 | 225 | runs(function () { 226 | client.connect({ cleanSession: false, onFailure: failCallback, mqttVersion: testMqttVersion }); 227 | }); 228 | waitsFor(function () { 229 | return connectFail; 230 | }, 'the client should fail to connect', 5000); 231 | runs(function () { 232 | expect(client.isConnected()).toBe(false); 233 | }); 234 | }); 235 | /* 236 | it('should queue up messages on the server for offline clients', function() { 237 | client = new Paho.MQTT.Client(testServer, testPort, testPath, "testclientid-js"); 238 | client.onMessageArrived = callbacks.onMessageArrived; 239 | 240 | expect(client).not.toBe(null); 241 | 242 | runs(function() { 243 | client.connect({onSuccess: callbacks.onConnectSuccess, mqttVersion:testMqttVersion, cleanSession:false}); 244 | }); 245 | waitsFor(function() { 246 | return client.isConnected(); 247 | }, "the client should connect", 5000); 248 | runs(function() { 249 | expect(client.isConnected()).toBe(true); 250 | }); 251 | 252 | runs(function() { 253 | client.subscribe(wildtopics[5], {qos:2, onSuccess: callbacks.onSubscribeSuccess}); 254 | }); 255 | waitsFor(function() { 256 | return subscribed; 257 | }, "the client should subscribe", 2000); 258 | runs(function() { 259 | expect(subscribed).toBe(true); 260 | }); 261 | 262 | runs(function() { 263 | client.disconnect(); 264 | }); 265 | waitsFor(function() { 266 | return true; 267 | }, "the client should disconnect", 5000); 268 | runs(function() { 269 | expect(client.isConnected()).toBe(false); 270 | }); 271 | 272 | bClient = new Paho.MQTT.Client(testServer, testPort, testPath, "testclientid-js-b"); 273 | bClient.onMessageDelivered = callbacks.onMessageDelivered; 274 | 275 | runs(function() { 276 | bClient.connect({onSuccess: callbacks.onConnectSuccess, mqttVersion:testMqttVersion, cleanSession:true}); 277 | }); 278 | waitsFor(function() { 279 | return bClient.isConnected(); 280 | }, "the client should connect again", 5000); 281 | runs(function() { 282 | expect(bClient.isConnected()).toBe(true); 283 | }); 284 | 285 | runs(function (){ 286 | for (var i = 0; i < 3; i++) { 287 | var message = new Paho.MQTT.Message("qos " + i); 288 | message.destinationName = topics[i+1]; 289 | message.qos=i; 290 | bClient.send(message); 291 | } 292 | }); 293 | waitsFor(function() { 294 | return sendingComplete; 295 | }, "the client should send 3 messages", 5000); 296 | runs(function() { 297 | expect(messagePublishedCount).toBe(3); 298 | }); 299 | 300 | runs(function() { 301 | bClient.disconnect({onSuccess: callbacks.onDisconnectSuccess}); 302 | }); 303 | waitsFor(function() { 304 | return connected; 305 | }, "the client should disconnect", 5000); 306 | runs(function() { 307 | expect(bClient.isConnected()).toBe(false); 308 | }); 309 | 310 | runs(function() { 311 | client.connect({onSuccess: callbacks.onConnectSuccess, mqttVersion:testMqttVersion, cleanSession:false}); 312 | }); 313 | waitsFor(function() { 314 | return client.isConnected(); 315 | }, "the client should connect", 5000); 316 | runs(function() { 317 | expect(client.isConnected()).toBe(true); 318 | }); 319 | waitsFor(function() { 320 | return (messageReceivedCount > 1); 321 | }, "the client should receive 2/3 messages", 5000); 322 | runs(function() { 323 | expect(messageReceivedCount).toBeGreaterThan(1); 324 | }); 325 | runs(function() { 326 | client.disconnect(); 327 | }); 328 | waitsFor(function() { 329 | return true; 330 | }, "the client should disconnect", 5000); 331 | runs(function() { 332 | expect(client.isConnected()).toBe(false); 333 | }); 334 | }); 335 | 336 | 337 | // This test has been commented out as it is only valid for a messagesight 338 | // server and behaviour differs between mqtt server implementations. 339 | it('should get a return code for failure to subscribe', function() { 340 | client = new Paho.MQTT.Client(testServer, testPort, testPath, "testclientid-js"); 341 | client.onMessageArrived = callbacks.onMessageArrived; 342 | 343 | var subFailed = false; 344 | var failSubscribe = function(response) { 345 | if (response.errorCode.get(0) == 0x80) { 346 | subFailed = true; 347 | } 348 | } 349 | 350 | expect(client).not.toBe(null); 351 | 352 | runs(function() { 353 | client.connect({onSuccess: callbacks.onConnectSuccess, mqttVersion:testMqttVersion, cleanSession:true}); 354 | }); 355 | waitsFor(function() { 356 | return client.isConnected(); 357 | }, "the client should connect", 5000); 358 | runs(function() { 359 | expect(client.isConnected()).toBe(true); 360 | }); 361 | 362 | runs(function() { 363 | client.subscribe(nosubscribetopics[0], {qos:2, onFailure: failSubscribe}); 364 | }); 365 | waitsFor(function() { 366 | return subFailed; 367 | }, "the client should fail to subscribe", 2000); 368 | runs(function() { 369 | expect(subFailed).toBe(true); 370 | }); 371 | }); 372 | */ 373 | }); 374 | -------------------------------------------------------------------------------- /src/oldtests/live-take-over-spec.js: -------------------------------------------------------------------------------- 1 | require('./hacks'); 2 | var settings = require('./client-harness'); 3 | 4 | var testServer = settings.server; 5 | var testPort = settings.port; 6 | var testPath = settings.path; 7 | var testMqttVersion = settings.mqttVersion; 8 | 9 | describe('LiveTakeOver', function () { 10 | 11 | //************************************************************************* 12 | // Client wrapper - define a client wrapper to ease testing 13 | //************************************************************************* 14 | var MqttClient = function (clientId) { 15 | var client = new Paho.MQTT.Client({ host: testServer, port: testPort, path: testPath, clientId }); 16 | //states 17 | var connected = false; 18 | var subscribed = false; 19 | var messageReceived = false; 20 | var messageDelivered = false; 21 | var receivedMessage = null; 22 | 23 | this.states = { connected: connected }; 24 | 25 | //reset all states 26 | this.resetStates = function () { 27 | connected = false; 28 | subscribed = false; 29 | messageReceived = false; 30 | messageDelivered = false; 31 | receivedMessage = null; 32 | }; 33 | 34 | //callbacks 35 | var onConnect = function () { 36 | console.log('%s connected', clientId); 37 | connected = true; 38 | }; 39 | 40 | var onDisconnect = function (response) { 41 | console.log('%s disconnected', clientId); 42 | connected = false; 43 | }; 44 | 45 | var onSubscribe = function () { 46 | console.log('%s subscribed', clientId); 47 | subscribed = true; 48 | }; 49 | 50 | var onUnsubscribe = function () { 51 | console.log('%s unsubscribed', clientId); 52 | subscribed = false; 53 | }; 54 | 55 | var onMessageArrived = function (msg) { 56 | console.log('%s received msg: %s', clientId, msg.payloadString); 57 | messageReceived = true; 58 | receivedMessage = msg; 59 | }; 60 | 61 | var onMessageDelivered = function (msg) { 62 | console.log('%s delivered message: %s', clientId, msg.payloadString); 63 | messageDelivered = true; 64 | }; 65 | 66 | //set callbacks 67 | client.onMessageArrived = onMessageArrived; 68 | client.onConnectionLost = onDisconnect; 69 | client.onMessageDelivered = onMessageDelivered; 70 | 71 | //functions 72 | //connect and verify 73 | this.connect = function (connectOptions) { 74 | connectOptions = connectOptions || {}; 75 | if (!connectOptions.hasOwnProperty('onSuccess')) { 76 | connectOptions.onSuccess = onConnect; 77 | connectOptions.mqttVersion = testMqttVersion; 78 | } 79 | runs(function () { 80 | client.connect(connectOptions); 81 | }); 82 | 83 | waitsFor(function () { 84 | return connected; 85 | }, 'the client should connect', 10000); 86 | 87 | runs(function () { 88 | expect(connected).toBe(true); 89 | //reset state 90 | connected = false; 91 | }); 92 | }; 93 | 94 | //disconnect and verify 95 | this.disconnect = function () { 96 | runs(function () { 97 | client.disconnect(); 98 | }); 99 | 100 | waitsFor(function () { 101 | return !connected; 102 | }, 'the client should disconnect', 10000); 103 | 104 | runs(function () { 105 | expect(connected).not.toBe(true); 106 | }); 107 | }; 108 | 109 | //subscribe and verify 110 | this.subscribe = function (topic, qos) { 111 | runs(function () { 112 | client.subscribe(topic, { qos: qos, onSuccess: onSubscribe }); 113 | }); 114 | 115 | waitsFor(function () { 116 | return subscribed; 117 | }, 'the client should subscribe', 2000); 118 | 119 | runs(function () { 120 | expect(subscribed).toBe(true); 121 | //reset state 122 | subscribed = false; 123 | }); 124 | }; 125 | 126 | //unsubscribe and verify 127 | this.unsubscribe = function (topic) { 128 | runs(function () { 129 | client.unsubscribe(topic, { onSuccess: onUnsubscribe }); 130 | }); 131 | 132 | waitsFor(function () { 133 | return !subscribed; 134 | }, 'the client should subscribe', 2000); 135 | 136 | runs(function () { 137 | expect(subscribed).not.toBe(true); 138 | }); 139 | }; 140 | 141 | //publish and verify 142 | this.publish = function (topic, qos, payload) { 143 | runs(function () { 144 | var message = new Paho.MQTT.Message(payload); 145 | message.destinationName = topic; 146 | message.qos = qos; 147 | client.send(message); 148 | }); 149 | 150 | waitsFor(function () { 151 | return messageDelivered; 152 | }, 'the client should delivered a message', 10000); 153 | 154 | runs(function () { 155 | //reset state 156 | messageDelivered = false; 157 | }); 158 | }; 159 | 160 | 161 | //verify no message received 162 | this.receiveNone = function () { 163 | waits(2000); 164 | runs(function () { 165 | expect(messageReceived).toBe(false); 166 | expect(receivedMessage).toBeNull(); 167 | }); 168 | }; 169 | 170 | //verify the receive message 171 | this.receive = function (expectedTopic, publishedQoS, subscribedQoS, expectedPayload) { 172 | 173 | waitsFor(function () { 174 | return messageReceived; 175 | }, 'the client should send and receive a message', 10000); 176 | 177 | runs(function () { 178 | expect(messageReceived).toBe(true); 179 | expect(receivedMessage).not.toBeNull(); 180 | expect(receivedMessage.qos).toBe(Math.min(publishedQoS, subscribedQoS)); 181 | expect(receivedMessage.destinationName).toBe(expectedTopic); 182 | if (typeof expectedPayload === 'string') { 183 | expect(receivedMessage.payloadString).toEqual(expectedPayload); 184 | } else { 185 | expect(receivedMessage.payloadBytes).toEqual(expectedPayload); 186 | } 187 | 188 | //reset state after each publish 189 | messageReceived = false; 190 | receivedMessage = null; 191 | }); 192 | }; 193 | }; 194 | 195 | //************************************************************************* 196 | // Tests 197 | //************************************************************************* 198 | 199 | it('should be taken over by another client for the actively doing work.', function () { 200 | var clientId = 'TakeOverClient1'; 201 | var testTopic = 'FirstClient/Topic'; 202 | var subscribedQoS = 2; 203 | var publishQoS = 1; 204 | var payload = 'TakeOverPayload'; 205 | 206 | //will msg 207 | var willMessage = new Paho.MQTT.Message('will-payload'); 208 | willMessage.destinationName = 'willTopic'; 209 | willMessage.qos = 2; 210 | willMessage.retained = true; 211 | 212 | var client1 = new MqttClient(clientId); 213 | client1.connect({ cleanSession: false, willMessage: willMessage, mqttVersion: testMqttVersion }); 214 | 215 | //subscribe 216 | client1.subscribe(testTopic, subscribedQoS); 217 | 218 | //publish some messwage 219 | for (var i = 0; i < 9; i++) { 220 | client1.publish(testTopic, publishQoS, payload); 221 | client1.receive(testTopic, publishQoS, subscribedQoS, payload); 222 | } 223 | 224 | // Now lets take over the connection 225 | // Create a second MQTT client connection with the same clientid. The 226 | // server should spot this and kick the first client connection off. 227 | var client2 = new MqttClient(clientId); 228 | client2.connect({ cleanSession: false, willMessage: willMessage, mqttVersion: testMqttVersion }); 229 | 230 | waitsFor(function () { 231 | return !client1.states.connected; 232 | }, 'the previous client should be disconnected', 10000); 233 | 234 | // We should have taken over the first Client's subscription... 235 | //Now check we have grabbed his subscription by publishing. 236 | client2.publish(testTopic, publishQoS, payload); 237 | client2.receive(testTopic, publishQoS, subscribedQoS, payload); 238 | 239 | //disconnect 240 | client2.disconnect(); 241 | }); 242 | 243 | 244 | }); 245 | -------------------------------------------------------------------------------- /src/oldtests/send-receive-spec.js: -------------------------------------------------------------------------------- 1 | require('./hacks'); 2 | var settings = require('./client-harness'); 3 | 4 | var testServer = settings.server; 5 | var testPort = settings.port; 6 | var testPath = settings.path; 7 | var testMqttVersion = settings.mqttVersion; 8 | 9 | //define a default clientID 10 | var clientId = 'testClient1'; 11 | 12 | describe('SendReceive', function () { 13 | 14 | //************************************************************************* 15 | // Client wrapper - define a client wrapper to ease testing 16 | //************************************************************************* 17 | var MqttClient = function (clientId) { 18 | var client = new Paho.MQTT.Client({ host: testServer, port: testPort, path: testPath, clientId }); 19 | //states 20 | var connected = false; 21 | var subscribed = false; 22 | var messageReceived = false; 23 | var messageDelivered = false; 24 | var receivedMessage = null; 25 | 26 | //reset all states 27 | this.resetStates = function () { 28 | connected = false; 29 | subscribed = false; 30 | messageReceived = false; 31 | messageDelivered = false; 32 | receivedMessage = null; 33 | }; 34 | 35 | //callbacks 36 | var onConnect = function () { 37 | console.log('%s connected', clientId); 38 | connected = true; 39 | }; 40 | 41 | var onDisconnect = function (response) { 42 | console.log('%s disconnected', clientId); 43 | connected = false; 44 | }; 45 | 46 | var onSubscribe = function () { 47 | console.log('%s subscribed', clientId); 48 | subscribed = true; 49 | }; 50 | 51 | var onUnsubscribe = function () { 52 | console.log('%s unsubscribed', clientId); 53 | subscribed = false; 54 | }; 55 | 56 | var onMessageArrived = function (msg) { 57 | console.log('%s received msg: %s', clientId, msg.payloadString); 58 | messageReceived = true; 59 | receivedMessage = msg; 60 | }; 61 | 62 | var onMessageDelivered = function (msg) { 63 | console.log('%s delivered message: %s', clientId, msg.payloadString); 64 | messageDelivered = true; 65 | }; 66 | 67 | //set callbacks 68 | client.onMessageArrived = onMessageArrived; 69 | client.onConnectionLost = onDisconnect; 70 | client.onMessageDelivered = onMessageDelivered; 71 | 72 | //functions 73 | //connect and verify 74 | this.connect = function (connectOptions) { 75 | connectOptions = connectOptions || {}; 76 | if (!connectOptions.hasOwnProperty('onSuccess')) { 77 | connectOptions.onSuccess = onConnect; 78 | connectOptions.mqttVersion = testMqttVersion; 79 | } 80 | runs(function () { 81 | client.connect(connectOptions); 82 | }); 83 | 84 | waitsFor(function () { 85 | return connected; 86 | }, 'the client should connect', 10000); 87 | 88 | runs(function () { 89 | expect(connected).toBe(true); 90 | //reset state 91 | connected = false; 92 | }); 93 | }; 94 | 95 | //disconnect and verify 96 | this.disconnect = function () { 97 | runs(function () { 98 | client.disconnect(); 99 | }); 100 | 101 | waitsFor(function () { 102 | return !connected; 103 | }, 'the client should disconnect', 10000); 104 | 105 | runs(function () { 106 | expect(connected).not.toBe(true); 107 | }); 108 | }; 109 | 110 | //subscribe and verify 111 | this.subscribe = function (topic, qos) { 112 | runs(function () { 113 | client.subscribe(topic, { qos: qos, onSuccess: onSubscribe }); 114 | }); 115 | 116 | waitsFor(function () { 117 | return subscribed; 118 | }, 'the client should subscribe', 2000); 119 | 120 | runs(function () { 121 | expect(subscribed).toBe(true); 122 | //reset state 123 | subscribed = false; 124 | }); 125 | }; 126 | 127 | //unsubscribe and verify 128 | this.unsubscribe = function (topic) { 129 | runs(function () { 130 | client.unsubscribe(topic, { onSuccess: onUnsubscribe }); 131 | }); 132 | 133 | waitsFor(function () { 134 | return !subscribed; 135 | }, 'the client should subscribe', 2000); 136 | 137 | runs(function () { 138 | expect(subscribed).not.toBe(true); 139 | }); 140 | }; 141 | 142 | //publish and verify 143 | this.publish = function (topic, qos, payload) { 144 | runs(function () { 145 | var message = new Paho.MQTT.Message(payload); 146 | message.destinationName = topic; 147 | message.qos = qos; 148 | client.send(message); 149 | }); 150 | 151 | waitsFor(function () { 152 | return messageDelivered; 153 | }, 'the client should delivered a message', 10000); 154 | 155 | runs(function () { 156 | //reset state 157 | messageDelivered = false; 158 | }); 159 | }; 160 | 161 | 162 | //verify no message received 163 | this.receiveNone = function () { 164 | waits(2000); 165 | runs(function () { 166 | expect(messageReceived).toBe(false); 167 | expect(receivedMessage).toBeNull(); 168 | }); 169 | }; 170 | 171 | //verify the receive message 172 | this.receive = function (expectedTopic, publishedQoS, subscribedQoS, expectedPayload) { 173 | 174 | waitsFor(function () { 175 | return messageReceived; 176 | }, 'the client should send and receive a message', 10000); 177 | 178 | runs(function () { 179 | expect(messageReceived).toBe(true); 180 | expect(receivedMessage).not.toBeNull(); 181 | expect(receivedMessage.qos).toBe(Math.min(publishedQoS, subscribedQoS)); 182 | expect(receivedMessage.destinationName).toBe(expectedTopic); 183 | if (typeof expectedPayload === 'string') { 184 | expect(receivedMessage.payloadString).toEqual(expectedPayload); 185 | } else { 186 | expect(receivedMessage.payloadBytes).toEqual(expectedPayload); 187 | } 188 | 189 | //reset state after each publish 190 | messageReceived = false; 191 | receivedMessage = null; 192 | }); 193 | }; 194 | }; 195 | 196 | //************************************************************************* 197 | // Tests 198 | //************************************************************************* 199 | 200 | it('should connect to a server and disconnect from a server', function () { 201 | var client = new MqttClient(clientId); 202 | 203 | //connect and verify 204 | client.connect({ mqttVersion: testMqttVersion }); 205 | 206 | //disconnect and verify 207 | client.disconnect(); 208 | }); 209 | 210 | 211 | it('should pub/sub using largish messages', function () { 212 | var client = new MqttClient(clientId); 213 | 214 | //connect and verify 215 | client.connect({ mqttVersion: testMqttVersion }); 216 | 217 | //subscribe and verify 218 | var testTopic = 'pubsub/topic'; 219 | var subscribedQoS = 0; 220 | client.subscribe(testTopic, subscribedQoS); 221 | 222 | //unsubscribe and verify 223 | client.unsubscribe(testTopic); 224 | 225 | //subscribe again 226 | client.subscribe(testTopic, subscribedQoS); 227 | 228 | //publish a large message to the topic and verify 229 | var publishQoS = 0; 230 | var payload = ''; 231 | var largeSize = 10000; 232 | for (var i = 0; i < largeSize; i++) { 233 | payload += 's'; 234 | } 235 | client.publish(testTopic, publishQoS, payload); 236 | 237 | //receive and verify 238 | client.receive(testTopic, publishQoS, subscribedQoS, payload); 239 | 240 | //disconnect and verify 241 | client.disconnect(); 242 | }); 243 | 244 | 245 | it('should preserve QOS values between publishers and subscribers', function () { 246 | var client = new MqttClient(clientId); 247 | 248 | //connect and verify 249 | client.connect({ mqttVersion: testMqttVersion }); 250 | 251 | //subscribe and verify 252 | var testTopics = ['pubsub/topic1', 'pubsub/topic2', 'pubsub/topic3']; 253 | var subscribedQoSs = [0, 1, 2]; 254 | for (var i = 0; i < testTopics.length; i++) { 255 | client.subscribe(testTopics[i], subscribedQoSs[i]); 256 | } 257 | 258 | //publish, receive and verify 259 | for (var i = 0; i < testTopics.length; i++) { 260 | var payload = 'msg-' + i; 261 | for (var qos = 0; qos < 3; qos++) { 262 | client.publish(testTopics[i], qos, payload); 263 | //receive and verify 264 | client.receive(testTopics[i], qos, subscribedQoSs[i], payload); 265 | } 266 | } 267 | 268 | //disconnect and verify 269 | client.disconnect(); 270 | }); 271 | 272 | it('should work using multiple publishers and subscribers.', function () { 273 | //topic to publish 274 | var topic = 'multiplePubSub/topic'; 275 | 276 | //create publishers and connect 277 | var publishers = []; 278 | var publishersNum = 2; 279 | for (var i = 0; i < publishersNum; i++) { 280 | publishers[i] = new MqttClient('publisher-' + i); 281 | publishers[i].connect({ mqttVersion: testMqttVersion }); 282 | } 283 | 284 | //create subscribers and connect 285 | var subscribedQoS = 0; 286 | var subscribers = []; 287 | var subscribersNum = 10; 288 | for (var i = 0; i < subscribersNum; i++) { 289 | subscribers[i] = new MqttClient('subscriber-' + i); 290 | subscribers[i].connect({ mqttVersion: testMqttVersion }); 291 | subscribers[i].subscribe(topic, subscribedQoS); 292 | } 293 | 294 | //do publish and receive with verify 295 | var publishQoS = 0; 296 | var pubishMsgNum = 10; 297 | for (var m = 0; m < pubishMsgNum; m++) { 298 | var payload = 'multi-pub-sub-msg-' + m; 299 | for (var i = 0; i < publishersNum; i++) { 300 | publishers[i].publish(topic, publishQoS, payload); 301 | for (var j = 0; j < subscribersNum; j++) { 302 | subscribers[j].receive(topic, publishQoS, subscribedQoS, payload); 303 | } 304 | } 305 | } 306 | 307 | //disconnect publishers and subscribers 308 | for (var i = 0; i < publishersNum; i++) { 309 | publishers[i].disconnect(); 310 | } 311 | for (var i = 0; i < subscribersNum; i++) { 312 | subscribers[i].disconnect(); 313 | } 314 | 315 | }); 316 | 317 | it('should clean up before re-connecting if cleanSession flag is set.', function () { 318 | //connect with cleanSession flag=false and verify 319 | var client = new MqttClient('client-1'); 320 | client.connect({ cleanSession: false, mqttVersion: testMqttVersion }); 321 | 322 | //subscribe and verify 323 | var testTopic = 'cleanSession/topic1'; 324 | var subscribedQoS = 0; 325 | client.subscribe(testTopic, subscribedQoS); 326 | 327 | //publish and verify 328 | var publishQoS = 1; 329 | var payload = 'cleanSession-msg'; 330 | client.publish(testTopic, publishQoS, payload); 331 | client.receive(testTopic, publishQoS, subscribedQoS, payload); 332 | //disconnect 333 | client.disconnect(); 334 | 335 | // Send a message from another client, to our durable subscription. 336 | var anotherClient = new MqttClient('anotherClient-1'); 337 | anotherClient.connect({ cleanSession: true, mqttVersion: testMqttVersion }); 338 | anotherClient.subscribe(testTopic, subscribedQoS); 339 | anotherClient.publish(testTopic, publishQoS, payload); 340 | anotherClient.receive(testTopic, publishQoS, subscribedQoS, payload); 341 | anotherClient.disconnect(); 342 | 343 | //reconnect 344 | client.connect({ cleanSession: true, mqttVersion: testMqttVersion }); 345 | //check no msg is received 346 | client.receiveNone(); 347 | 348 | //do another publish and check if msg is received, because subscription should be cancelled 349 | client.publish(testTopic, publishQoS, payload); 350 | //check no msg is received 351 | client.receiveNone(); 352 | //disconnect 353 | client.disconnect(); 354 | }); 355 | 356 | 357 | }); 358 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ERROR, MESSAGE_TYPE } from './constants'; 4 | import WireMessage from './WireMessage'; 5 | import PublishMessage from './PublishMessage'; 6 | import Message from './Message'; 7 | 8 | export function invariant(condition: boolean, message: string) { 9 | if (!condition) { 10 | throw new Error(message); 11 | } 12 | } 13 | 14 | /** 15 | * Format an error message text. 16 | * 17 | * @param error ERROR.KEY value above. 18 | * @param substitutions [array] substituted into the text. 19 | * @return the text with the substitutions made. 20 | */ 21 | export function format(error: { text: string }, substitutions?: (?(string | number | Uint8Array))[]) { 22 | let text = error.text; 23 | if (substitutions) { 24 | let field; 25 | substitutions.forEach((substitution, i) => { 26 | if (substitution === null) { 27 | substitution = 'null'; 28 | } 29 | if (substitution === undefined) { 30 | substitution = 'undefined'; 31 | } 32 | field = '{' + i + '}'; 33 | text = text.replace(field, substitution.toString()); 34 | }); 35 | } 36 | return text; 37 | } 38 | 39 | /** 40 | * Validate an object's parameter names to ensure they 41 | * match a list of expected letiables name for this option 42 | * type. Used to ensure option object passed into the API don't 43 | * contain erroneous parameters. 44 | * @param {Object} obj - User options object 45 | * @param {Object} keys - valid keys and types that may exist in obj. 46 | * @throws {Error} Invalid option parameter found. 47 | * @private 48 | */ 49 | export function validate(obj: {}, keys: { [key: string]: string }) { 50 | Object.keys(obj).forEach(key => { 51 | if (keys.hasOwnProperty(key)) { 52 | let desiredType = keys[key]; 53 | if (!(desiredType.indexOf('?') === 0 && (typeof obj[key] === 'undefined' || obj[key] === null))) { 54 | desiredType = desiredType.replace(/^\?/, ''); 55 | if (typeof obj[key] !== desiredType) { 56 | throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key])); 57 | } 58 | } 59 | } else { 60 | throw new Error('Unknown property, ' + key + '. Valid properties are: ' + Object.keys(keys).join(' ')); 61 | } 62 | }); 63 | } 64 | 65 | //Write a 16-bit number into two bytes of Uint8Array, starting at offset 66 | export function writeUint16(input: number, buffer: Uint8Array, offset: number): number { 67 | buffer[offset++] = input >> 8; //MSB 68 | buffer[offset++] = input % 256; //LSB 69 | return offset; 70 | } 71 | 72 | export function writeString(input: string, utf8Length: number, buffer: Uint8Array, offset: number) { 73 | offset = writeUint16(utf8Length, buffer, offset); 74 | stringToUTF8(input, buffer, offset); 75 | return offset + utf8Length; 76 | } 77 | 78 | //Read a 16-bit number out of two bytes of a Uint8Array 79 | export function readUint16(buffer: Uint8Array, offset: number): number { 80 | return 256 * buffer[offset] + buffer[offset + 1]; 81 | } 82 | 83 | /** 84 | * Encodes an MQTT Multi-Byte Integer 85 | * @private 86 | */ 87 | export function encodeMultiByteInteger(number: number) { 88 | let output = new Array(1); 89 | let numBytes = 0; 90 | 91 | do { 92 | let digit = number % 128; 93 | number = number >> 7; 94 | if (number > 0) { 95 | digit |= 0x80; 96 | } 97 | output[numBytes++] = digit; 98 | } while ((number > 0) && (numBytes < 4)); 99 | 100 | return output; 101 | } 102 | 103 | /** 104 | * Takes a String and calculates its length in bytes when encoded in UTF8. 105 | * @private 106 | */ 107 | export function lengthOfUTF8(input: string): number { 108 | let output = 0; 109 | for (let i = 0; i < input.length; i++) { 110 | const charCode = input.charCodeAt(i); 111 | if (charCode > 0x7FF) { 112 | // Surrogate pair means its a 4 byte character 113 | if (charCode >= 0xD800 && charCode <= 0xDBFF) { 114 | i++; 115 | output++; 116 | } 117 | output += 3; 118 | } 119 | else if (charCode > 0x7F) { 120 | output += 2; 121 | } else { 122 | output++; 123 | } 124 | } 125 | return output; 126 | } 127 | 128 | /** 129 | * Takes a String and writes it into an array as UTF8 encoded bytes. 130 | * @private 131 | */ 132 | export function stringToUTF8(input: string, output: Uint8Array, start: number): Uint8Array { 133 | let pos = start; 134 | for (let i = 0; i < input.length; i++) { 135 | let charCode = input.charCodeAt(i); 136 | 137 | // Check for a surrogate pair. 138 | if (charCode >= 0xD800 && charCode <= 0xDBFF) { 139 | const lowCharCode = input.charCodeAt(++i); 140 | if (isNaN(lowCharCode)) { 141 | throw new Error(format(ERROR.MALFORMED_UNICODE, [charCode, lowCharCode])); 142 | } 143 | charCode = ((charCode - 0xD800) << 10) + (lowCharCode - 0xDC00) + 0x10000; 144 | 145 | } 146 | 147 | if (charCode <= 0x7F) { 148 | output[pos++] = charCode; 149 | } else if (charCode <= 0x7FF) { 150 | output[pos++] = charCode >> 6 & 0x1F | 0xC0; 151 | output[pos++] = charCode & 0x3F | 0x80; 152 | } else if (charCode <= 0xFFFF) { 153 | output[pos++] = charCode >> 12 & 0x0F | 0xE0; 154 | output[pos++] = charCode >> 6 & 0x3F | 0x80; 155 | output[pos++] = charCode & 0x3F | 0x80; 156 | } else { 157 | output[pos++] = charCode >> 18 & 0x07 | 0xF0; 158 | output[pos++] = charCode >> 12 & 0x3F | 0x80; 159 | output[pos++] = charCode >> 6 & 0x3F | 0x80; 160 | output[pos++] = charCode & 0x3F | 0x80; 161 | } 162 | } 163 | return output; 164 | } 165 | 166 | export function parseUTF8(input: Uint8Array, offset: number, length: number): string { 167 | let output = ''; 168 | let utf16; 169 | let pos = offset; 170 | 171 | while (pos < offset + length) { 172 | let byte1 = input[pos++]; 173 | if (byte1 < 128) { 174 | utf16 = byte1; 175 | } else { 176 | let byte2 = input[pos++] - 128; 177 | if (byte2 < 0) { 178 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), ''])); 179 | } 180 | if (byte1 < 0xE0) // 2 byte character 181 | { 182 | utf16 = 64 * (byte1 - 0xC0) + byte2; 183 | } else { 184 | let byte3 = input[pos++] - 128; 185 | if (byte3 < 0) { 186 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)])); 187 | } 188 | if (byte1 < 0xF0) // 3 byte character 189 | { 190 | utf16 = 4096 * (byte1 - 0xE0) + 64 * byte2 + byte3; 191 | } else { 192 | let byte4 = input[pos++] - 128; 193 | if (byte4 < 0) { 194 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); 195 | } 196 | if (byte1 < 0xF8) // 4 byte character 197 | { 198 | utf16 = 262144 * (byte1 - 0xF0) + 4096 * byte2 + 64 * byte3 + byte4; 199 | } else // longer encodings are not supported 200 | { 201 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); 202 | } 203 | } 204 | } 205 | } 206 | 207 | if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate pair 208 | { 209 | utf16 -= 0x10000; 210 | output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character 211 | utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character 212 | } 213 | output += String.fromCharCode(utf16); 214 | } 215 | return output; 216 | } 217 | 218 | export function decodeMessage(input: Uint8Array, pos: number): [?WireMessage | PublishMessage, number] { 219 | const startingPos = pos; 220 | let first = input[pos]; 221 | const type = first >> 4; 222 | const messageInfo = (first & 0x0f); 223 | pos += 1; 224 | 225 | 226 | // Decode the remaining length (MBI format) 227 | 228 | let digit; 229 | let remLength = 0; 230 | let multiplier = 1; 231 | do { 232 | if (pos === input.length) { 233 | return [null, startingPos]; 234 | } 235 | digit = input[pos++]; 236 | remLength += ((digit & 0x7F) * multiplier); 237 | multiplier *= 128; 238 | } while ((digit & 0x80) !== 0); 239 | 240 | const endPos = pos + remLength; 241 | if (endPos > input.length) { 242 | return [null, startingPos]; 243 | } 244 | 245 | let wireMessage; 246 | switch (type) { 247 | case MESSAGE_TYPE.CONNACK: 248 | wireMessage = new WireMessage(type); 249 | const connectAcknowledgeFlags = input[pos++]; 250 | const sessionPresent = connectAcknowledgeFlags & 0x01; 251 | if (sessionPresent) { 252 | wireMessage.sessionPresent = true; 253 | } 254 | wireMessage.returnCode = input[pos++]; 255 | break; 256 | 257 | case MESSAGE_TYPE.PUBLISH: 258 | const qos: any = (messageInfo >> 1) & 0x03; 259 | 260 | const len = readUint16(input, pos); 261 | pos += 2; 262 | const topicName = parseUTF8(input, pos, len); 263 | pos += len; 264 | let messageIdentifier; 265 | // If QoS 1 or 2 there will be a messageIdentifier 266 | if (qos > 0) { 267 | messageIdentifier = readUint16(input, pos); 268 | pos += 2; 269 | } 270 | 271 | const message = new Message(input.subarray(pos, endPos)); 272 | if ((messageInfo & 0x01) === 0x01) { 273 | message.retained = true; 274 | } 275 | if ((messageInfo & 0x08) === 0x08) { 276 | message.duplicate = true; 277 | } 278 | message.qos = qos; 279 | message.destinationName = topicName; 280 | return [new PublishMessage(message, messageIdentifier), endPos]; 281 | 282 | case MESSAGE_TYPE.PUBACK: 283 | case MESSAGE_TYPE.PUBREC: 284 | case MESSAGE_TYPE.PUBREL: 285 | case MESSAGE_TYPE.PUBCOMP: 286 | case MESSAGE_TYPE.UNSUBACK: 287 | wireMessage = new WireMessage(type); 288 | wireMessage.messageIdentifier = readUint16(input, pos); 289 | break; 290 | 291 | case MESSAGE_TYPE.SUBACK: 292 | wireMessage = new WireMessage(type); 293 | wireMessage.messageIdentifier = readUint16(input, pos); 294 | pos += 2; 295 | wireMessage.returnCode = input.subarray(pos, endPos); 296 | break; 297 | 298 | default: 299 | wireMessage = new WireMessage(type); 300 | } 301 | return [wireMessage, endPos]; 302 | } 303 | -------------------------------------------------------------------------------- /test/.support.js: -------------------------------------------------------------------------------- 1 | import { Server } from 'mosca'; 2 | 3 | let broker = null; 4 | 5 | const PORT = 3005; 6 | 7 | export const startBroker = () => new Promise((resolve, reject) => { 8 | broker = new Server({ 9 | port: 18830, 10 | backend: { 11 | //using ascoltatore 12 | type: 'mongo', 13 | url: 'mongodb://localhost:27017/mqtt', 14 | pubsubCollection: 'ascoltatori', 15 | mongo: {} 16 | }, 17 | http: { 18 | port: PORT, 19 | bundle: true, 20 | static: './' 21 | } 22 | }); 23 | broker.on('ready', resolve); 24 | }); 25 | 26 | export const stopBroker = () => { 27 | if (broker) { 28 | return new Promise((resolve, reject) => { 29 | broker.close(resolve); 30 | broker = null; 31 | }); 32 | } 33 | return Promise.resolve(); 34 | }; 35 | 36 | export const uri = 'ws://localhost:'+PORT+'/'; 37 | export const webSocket = require('websocket').w3cwebsocket; 38 | export const storage = require('node-localstorage'); 39 | export const mqttVersion = 3; 40 | -------------------------------------------------------------------------------- /test/Client.test.js: -------------------------------------------------------------------------------- 1 | import { Client } from '../'; 2 | import { uri } from './.support'; 3 | 4 | import { w3cwebsocket as webSocket } from 'websocket'; 5 | import { LocalStorage } from 'node-localstorage'; 6 | 7 | const storage = new LocalStorage('./tmp'); 8 | 9 | describe('client-uris', function () { 10 | 11 | test('should create a new client with a uri', function () { 12 | const client = new Client({ 13 | uri, 14 | clientId: 'testclientid', 15 | webSocket, 16 | storage 17 | }); 18 | 19 | expect(client).not.toBe(null); 20 | expect(client.uri).toBe(uri); 21 | }); 22 | 23 | test('should fail to create a new client with an invalid ws uri', function () { 24 | let client = null; 25 | let error; 26 | try { 27 | client = new Client({ uri: 'http://example.com', clientId: 'testclientid', webSocket, storage }); 28 | } catch (err) { 29 | error = err; 30 | } 31 | expect(client).toBe(null); 32 | expect(error).not.toBe(null); 33 | }); 34 | 35 | /* 36 | // We don't yet expose setting the path element with the arrays of hosts/ports 37 | // If you want a path other than /mqtt, you need to use the array of hosts-as-uris. 38 | // Leaving this test here to remember this fact in case we add an array of paths to connopts 39 | it('should connect and disconnect to a server using connectoptions hosts and ports', function() { 40 | client = new Paho.MQTT.Client(testServer, testPort, "testclientid"); 41 | expect(client).not.toBe(null); 42 | 43 | client.onMessageArrived = messageArrived; 44 | client.onConnectionLost = onDisconnect; 45 | 46 | runs(function() { 47 | client.connect({onSuccess:onConnect,hosts:[host],ports:[port]}); 48 | }); 49 | 50 | waitsFor(function() { 51 | return connected; 52 | }, "the client should connect", 10000); 53 | 54 | runs(function() { 55 | expect(connected).toBe(true); 56 | }); 57 | runs(function() { 58 | client.disconnect(); 59 | }); 60 | waitsFor(function() { 61 | return !connected; 62 | }, "the client should disconnect",1000); 63 | runs(function() { 64 | expect(connected).toBe(false); 65 | expect(disconnectError).not.toBe(null); 66 | expect(disconnectError.errorCode).toBe(0); 67 | }); 68 | }); 69 | */ 70 | }); 71 | -------------------------------------------------------------------------------- /test/Message.test.js: -------------------------------------------------------------------------------- 1 | import Message from '../src/Message'; 2 | 3 | test('check message properties.', function () { 4 | const strMsg = 'test Msg'; 5 | const strDes = '/test'; 6 | const message = new Message(strMsg); 7 | message.destinationName = strDes; 8 | 9 | expect(message.qos).toBe(0); 10 | expect(message.duplicate).toBe(false); 11 | expect(message.retained).toBe(false); 12 | expect(message.payloadString).toEqual(strMsg); 13 | expect(message.payloadBytes.length).toBeGreaterThan(0); 14 | expect(message.destinationName).toEqual(strDes); 15 | 16 | expect(function () { 17 | Message(); 18 | }).toThrow(); 19 | 20 | message.qos = 0; 21 | expect(message.qos).toBe(0); 22 | message.qos = 1; 23 | expect(message.qos).toBe(1); 24 | message.qos = 2; 25 | expect(message.qos).toBe(2); 26 | 27 | //illegal argument exception 28 | expect(function () { 29 | message.qos = -1; 30 | }).toThrow(); 31 | expect(function () { 32 | message.qos = 1; 33 | }).not.toThrow(); 34 | 35 | const strPayload = 'payload is a string'; 36 | message.payloadString = strPayload; 37 | expect(message.payloadString).not.toEqual(strPayload); 38 | 39 | message.retained = false; 40 | expect(message.retained).toBe(false); 41 | message.retained = true; 42 | expect(message.retained).toBe(true); 43 | 44 | message.duplicate = false; 45 | expect(message.duplicate).toBe(false); 46 | message.duplicate = true; 47 | expect(message.duplicate).toBe(true); 48 | 49 | //to do , check payload 50 | /* 51 | var buffer = new ArrayBuffer(4); 52 | var uintArr = new Uint8Array(buffer); 53 | dataView = new DataView(buffer); 54 | dataView.setInt32(0,0x48656c6c); 55 | //dataView.setInt 56 | console.log(dataView.getInt32(0).toString(16)); 57 | //var arrbufPayload = new ArrayBuffer 58 | var msg = new Paho.MQTT.Message(buffer); 59 | console.log(msg.payloadBytes,msg.payloadString); 60 | */ 61 | }); 62 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | import { Client } from '../'; 2 | import { uri, mqttVersion, startBroker, stopBroker, storage, webSocket } from './.support'; 3 | import Message from '../src/Message'; 4 | 5 | const client = new Client({ 6 | uri, 7 | clientId: 'testclientid', 8 | webSocket, 9 | storage, 10 | }); 11 | 12 | test('client is set up correctly', function () { 13 | expect(client.uri).toBe(uri); 14 | }); 15 | 16 | describe('Basic integration tests', () => { 17 | beforeAll(() => startBroker().then(() => client.connect({ mqttVersion }))); 18 | 19 | test('should send and receive a message', (done) => { 20 | client.on('messageReceived', (message) => { 21 | expect(message.payloadString).toEqual('Hello'); 22 | done(); 23 | }); 24 | const message = new Message('Hello'); 25 | message.destinationName = 'World'; 26 | client.subscribe('World').then(() => client.send(message)); 27 | }); 28 | 29 | test('should disconnect and reconnect cleanly', () => client.disconnect().then(() => client.connect({ mqttVersion }))); 30 | 31 | afterAll(() => client.disconnect().then(() => stopBroker())); 32 | }); 33 | 34 | describe('Keep alive mechanism', () => { 35 | beforeAll(() => startBroker().then(() => client.connect({ mqttVersion, keepAliveInterval: 1 }))); 36 | 37 | test('should send PINGREQ messages so that the server does not disconnect', () => { 38 | return new Promise((resolve, reject) => { 39 | const successTimer = setTimeout(resolve, 3000); 40 | const onFail = (e) => { 41 | clearTimeout(successTimer); 42 | client.removeListener('connectionLost', onFail); 43 | stopBroker().then(() => reject(new Error('Unexpected disconnection'))); 44 | }; 45 | client.on('connectionLost', onFail); 46 | }); 47 | }, 5000); 48 | 49 | afterAll(() => client.disconnect().then(() => stopBroker())); 50 | }); 51 | -------------------------------------------------------------------------------- /test/util.test.js: -------------------------------------------------------------------------------- 1 | import { format } from '../src/util'; 2 | 3 | test('format applies substitutions', () => { 4 | expect(format({ text: 'Hi {0}, formatting is working {1}' }, ['Rob', 'OK'])).toBe('Hi Rob, formatting is working OK'); 5 | }); 6 | --------------------------------------------------------------------------------