Skip to content
This repository was archived by the owner on Oct 28, 2022. It is now read-only.

Commit 75363a3

Browse files
committed
feat(encoders/decoders): add encoder/decoder support, closes #1
- adds `text`, `json`, `binary`, and `base64` encoders and decoders to be used with `publish()` and `subscribe()`, respectively - can set defaults with options to `connect()` - the default is `text` for both encoder and decoder - update docs, tests - `unsubscribe` returns `true` if actual unsubscription occurs; `false` otherwise
1 parent 91128a1 commit 75363a3

File tree

9 files changed

+426
-70
lines changed

9 files changed

+426
-70
lines changed

README.md

Lines changed: 110 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ Also, `Promise`s.
3030
There are two (2) main issues which users of MQTT.js will quickly encounter:
3131

3232
1. You can't just set a listener function for a subscribed topic; you have to stuff logic into a listener on the `message` event, and figure out what to do with the topic.
33-
2. Received messages are all `Buffer`s, and when publishing, your message must be a `string`, `Buffer`.
33+
2. Received messages are all `Buffer` objects, and you can only publish a `Buffer` or `string`.
3434
3. (BONUS ISSUE) It doesn't use `Promise`s, which some people prefer over callbacks.
3535

36-
`mqttletoad` solves the *first* problem and the *third* problem.
36+
`mqttletoad` solves the above problems. These are problems I have, and likely many others have as well.
3737

3838
### Listeners for Specific Topics
3939

@@ -57,42 +57,96 @@ We need something like a *router* (think [express](https://www.npmjs.com/package
5757

5858
What's better is that `EventEmitter`s are standardized. They are easy to consume. Think [RxJs](https://npm.im/rxjs)'s `Observable.fromEvent()`. This should help those using a "reactive" programming model.
5959

60-
### Promise Support
60+
### Encoding and Decoding
6161

62-
[async-mqtt](https://npm.im/async-mqtt) does the same thing here--more or less.
62+
MQTT makes no prescriptions about what a message looks like. It's just a [blob](https://en.wikipedia.org/wiki/Binary_large_object).
63+
64+
But as a developer, you know your data. Maybe that message is JSON, maybe it's base64-encoded, or maybe it's just a string. You are unlikely to be surprised about what you get.
65+
66+
Out-of-the-box, `mqttletoad` provides several common *decoders* for received messages, and *encoders* for publishing messages.
67+
68+
These are:
6369

64-
### WONTFIX: Message Formats
70+
- `json` - Convert to/from a JSON representation of an object
71+
- `text` - Convert to/from a UTF-8 encoded string
72+
- `binary` - Convert to a `Buffer` (all received messages are `Buffer`s, so no decoding necessary)
73+
- `base64` - Convert to/from a base64 (string) representation of just about anything
6574

66-
MQTT makes no prescriptions about what your message looks like. It's just a [blob](https://en.wikipedia.org/wiki/Binary_large_object). A JavaScript object is neither of these! If you need to publish an object, call `JSON.stringify` on it first:
75+
To use these, you can specify a *default* encoder and/or decoder when connecting:
6776

6877
```js
69-
const obj = {goin: 'on'};
70-
client.emit('fever/flavor', JSON.stringify(obj));
78+
const toad = require('mqttletoad');
79+
80+
(async function () {
81+
const client = await toad.connect('wss://test.mosquitto.org', {
82+
encoder: 'json',
83+
decoder: 'json'
84+
});
85+
86+
await client.subscribe('foo/bar', message => {
87+
console.log(message.baz); // quux
88+
});
89+
90+
// see listener above
91+
await client.publish('foo/bar', {baz: 'quux'});
92+
}());
7193
```
7294

73-
When subscribing, you will *always* receive a `Buffer`. You'll need to unwrap it yourself:
95+
Or you can do this on a per-subscription/publish basis:
7496

7597
```js
76-
client.on('a/licky/boom/boom/down', buf => {
77-
const obj = JSON.stringify(String(buf));
78-
});
79-
```
98+
const toad = require('mqttletoad');
99+
100+
(async function () {
101+
// "text" is the default encoder/decoder
102+
const client = await toad.connect('wss://test.mosquitto.org', {
103+
encoder: 'text',
104+
decoder: 'text'
105+
});
106+
107+
await client.subscribe('foo/bar', message => {
108+
console.log(message.baz); // quux
109+
}, {decoder: 'json'});
110+
111+
// see listener above
112+
await client.publish('foo/bar', {baz: 'quux'}, {encoder: 'json'});
113+
}());
114+
```
80115

81-
A better idea may be to put some metadata in your topic about the message format, but again, this is up to you:
116+
You can also provide your own either way:
82117

83118
```js
84-
const formatters = {
85-
json: val => String(JSON.stringify),
86-
text: String,
87-
yaml: parseYaml
88-
};
89-
90-
client.on('radscript/4ever/format/+', (buf, {topic})=> {
91-
const format = topic.split('/').pop();
92-
const result = formatters[format](buf);
93-
});
119+
const toad = require('mqttletoad');
120+
121+
(async function() {
122+
const client = await toad.connect('wss://test.mosquitto.org', {
123+
decoder: parseFloat
124+
});
125+
126+
await client.subscribe('foo/bar', message => {
127+
console.log(message); // 123.4
128+
});
129+
130+
await client.subscribe('foo/bar', message => {
131+
console.log(message); // '123.4'
132+
}, {decoder: 'text'});
133+
134+
// see listeners above
135+
await client.publish('foo/bar', 123.4); // default encoder is "text"
136+
}());
94137
```
95138

139+
### Promise Support
140+
141+
[async-mqtt](https://npm.im/async-mqtt) does the same thing here--more or less.
142+
143+
The following functions are promisified:
144+
145+
- `MqttClient#publish`
146+
- `MqttClient#subscribe`
147+
- `MqttClient#unsubscribe`
148+
- `MqttClient#end`
149+
96150
## Install
97151

98152
**Node.js v7.0.0 or greater required**.
@@ -103,6 +157,8 @@ $ npm install mqttletoad
103157

104158
## Usage
105159

160+
This is a fancypants wrapper around [MQTT.js](https://npm.im/mqtt), so most everything there applies here, except the differences noted above.
161+
106162
```js
107163
const toad = require('mqttletoad');
108164

@@ -116,28 +172,48 @@ const myfunc = async () => {
116172
console.warn('client offline; reconnecting...');
117173
});
118174

119-
// yes, `buf` is still a Buffer.
120-
const suback = await client.subscribe('winken/+/nod', (buf, packet) => {
121-
console.log(`topic: "${packet.topic}", message: "${String(buf)}"`);
175+
// uses default "text" decoder
176+
const suback = await client.subscribe('winken/+/nod', (str, packet) => {
177+
console.log(`topic: "${packet.topic}", message: "${str}"`);
122178
}, {qos: 1});
123179

124180
console.log(`subscribed to ${suback.topic} w/ QoS ${suback.qos}`);
125181

182+
// uses default "text" encoder
126183
await client.publish('winken/blinken/nod', 'foo');
184+
185+
const someOtherListener = (message, packet) => {
186+
// does stuff with MESSAGE
187+
};
188+
189+
// a custom decoder
190+
await client.subscribe('winken/+/nod', someOtherListener, {
191+
decoder: value => String(value).toUpperCase()
192+
});
193+
194+
// remove only this particular listener for this topic;
195+
// no actual unsubscription occurs because this isn't the only listener
196+
// on the topic.
197+
await client.unsubscribe('winken/+/nod', someOtherListener);
198+
199+
// remove ALL listeners from this topic and unsubscribe
200+
await client.unsubscribe('winken/+/nod');
201+
202+
// disconnect
203+
await client.end();
127204
}
128205
```
129206

130-
## API
131-
132-
Basically it's [async-mqtt](https://npm.im/async-mqtt) (which is [mqtt](https://npm.im/mqtt)) except:
133-
134207
- Use `client.subscribe(topic, [opts], listener)` to *register a listener* for the topic.
135-
- `opts` are the standard options `mqtt.Client#subscribe()` supports
136-
- While `mqtt.Client#subscribe()` supports an `Array` of topics, our `topic` is singular, and must be a string.
208+
- `opts` are the standard options `MqttClient#subscribe()` supports, including `decoder`
209+
- While `MqttClient#subscribe()` supports an `Array` of topics, our `topic` is singular, and *must* be a string.
137210
- Standard MQTT topic wildcards are supported, and listeners are executed first in order of specificity; i.e. `foo/bar` will take precedence over `foo/+` and `foo/+` will take precedence over `foo/#`.
138211
- Use `client.unsubscribe(topic, listener)` to remove the listener for the topic.
139212
- This will not necessarily *unsubscribe* from the topic (at the broker level), because there may be other listeners, but it *will* remove the listener.
140-
- `client.end()` won't throw a fit if already disconnected
213+
- If `listener` is omitted, all listeners are removed, which forces unsubscription.
214+
- Use `client.end(force=false)` to disconnect
215+
- Use `client.publish(topic, message, [opts])` with standard `MqttClient#publish()` options, including `encoder`
216+
- Use `connect(url, [opts])` to connect; `url` is a `string`, or you could just pass an `opts` object. Includes `encoder` and `decoder` options, which set the default encoder and decoder, respectively. The default is `text` in both cases.
141217

142218
## Roadmap
143219

lib/decoders.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
const atob = require('atob');
4+
5+
/**
6+
* JSON decoder
7+
* @param {Buffer} value - Value to decode
8+
* @returns {*} JSON representation of value
9+
*/
10+
const json = value => JSON.parse(value.toString('utf-8'));
11+
12+
/**
13+
* Base64 decoder
14+
* @param {Buffer} value - Value to decode
15+
* @returns {string} base64-encoded string
16+
*/
17+
const base64 = value => atob(value.toString('utf-8'));
18+
19+
/**
20+
* Text decoder
21+
* @param {Buffer} value - Value to decode
22+
* @returns {string} utf-8 encoded string
23+
*/
24+
const text = value => value.toString('utf-8');
25+
26+
/**
27+
* Binary decoder (does nothing)
28+
* @param {Buffer} value - Value to decode
29+
* @returns {Buffer} Unmolested `value`
30+
*/
31+
const binary = value => value;
32+
33+
exports.json = json;
34+
exports.base64 = base64;
35+
exports.text = text;
36+
exports.binary = binary;

lib/encoders.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
const btoa = require('btoa');
4+
5+
/**
6+
* JSON encoder
7+
* @param {*} value - Value to encode
8+
* @returns {string} JSON representation of value
9+
*/
10+
const json = JSON.stringify;
11+
12+
/**
13+
* Base64 encoder
14+
* @param {*} value - Value to encode
15+
* @returns {string} base64-encoded string
16+
*/
17+
const base64 = btoa;
18+
19+
/**
20+
* Text encoder
21+
* @param {*} value - Value to encode
22+
* @returns {string} text representation of string
23+
*/
24+
const text = String;
25+
26+
/**
27+
* Binary encoder
28+
* @param {*} value - Value to encode
29+
* @returns {Buffer} A Buffer representing the value
30+
*/
31+
const binary = value => Buffer.from(value);
32+
33+
exports.json = json;
34+
exports.base64 = base64;
35+
exports.text = text;
36+
exports.binary = binary;

0 commit comments

Comments
 (0)