├── .gitignore ├── index.js ├── .jshintrc ├── Makefile ├── examples └── subscriptions.js ├── package.json ├── LICENSE-MIT ├── README.md ├── lib └── mqtt-router.js └── test └── router_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | build 4 | npm-debug.log 5 | .DS_Store 6 | *.iml -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Router = require('./lib/mqtt-router.js') 4 | 5 | exports.wrap = function (mqttclient) { 6 | return new Router(mqttclient) 7 | } 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "strict": true, 4 | "laxcomma": true, 5 | "curly": false, 6 | "laxbreak": true, 7 | "expr": true, 8 | "predef": [ 9 | "describe", 10 | "it", 11 | "before", 12 | "beforeEach", 13 | "after", 14 | "afterEach" 15 | ] 16 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = spec 2 | 3 | all: lint test 4 | 5 | test: 6 | @NODE_ENV=test ./node_modules/.bin/mocha --recursive --reporter $(REPORTER) --timeout 5000 7 | 8 | lint: 9 | standard 10 | 11 | tests: test 12 | 13 | tap: 14 | @NODE_ENV=test ./node_modules/.bin/mocha -R tap > results.tap 15 | 16 | unit: 17 | @NODE_ENV=test ./node_modules/.bin/mocha --recursive -R xunit > results.xml --timeout 3000 18 | 19 | .PHONY: test tap unit jshint 20 | -------------------------------------------------------------------------------- /examples/subscriptions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var mqtt = require('mqtt') 3 | var mqttrouter = require('../index.js') 4 | 5 | var client = mqtt.createClient() 6 | var router = mqttrouter.wrap(client) 7 | 8 | router.subscribe('$TEST/dev/request', function (topic, message) { 9 | console.log('request handler', topic, message) 10 | }) 11 | 12 | router.subscribe('$TEST/dev/reply', function (topic, message) { 13 | console.log('reply handler', topic, message) 14 | }) 15 | 16 | setInterval(function () { 17 | client.publish('$TEST/dev/reply', 'hello me!') 18 | client.publish('$TEST/dev/request', 'hello me!') 19 | }, 5000) 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-router", 3 | "version": "0.5.1", 4 | "description": "Router for mqtt subscriptions.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/wolfeidau/mqtt-router.git" 12 | }, 13 | "keywords": [ 14 | "MQTT", 15 | "router", 16 | "subscriptions" 17 | ], 18 | "author": "Mark Wolfe ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wolfeidau/mqtt-router/issues" 22 | }, 23 | "dependencies": { 24 | "mqtt": "~2.9.1", 25 | "houkou": "~0.2.2", 26 | "debug": "~2.6.8" 27 | }, 28 | "devDependencies": { 29 | "chai": "~4.0.2", 30 | "mocha": "~3.4.2", 31 | "mosca": "^2.5.1", 32 | "sinon": "~2.3.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Mark Wolfe 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mqtt-router [![Build Status](https://drone.io/github.com/wolfeidau/mqtt-router/status.png)](https://drone.io/github.com/wolfeidau/mqtt-router/latest) 2 | 3 | This module a router for use with MQTT subscriptions. 4 | 5 | [![NPM](https://nodei.co/npm/mqtt-router.png)](https://nodei.co/npm/mqtt-router/) 6 | [![NPM](https://nodei.co/npm-dl/mqtt-router.png)](https://nodei.co/npm/mqtt-router/) 7 | 8 | ## Installation 9 | 10 | ``` 11 | npm install mqtt-router 12 | ``` 13 | 14 | # TLDR 15 | 16 | If you have just started with [MQTT](https://github.com/adamvr/MQTT.js) the first thing you will notice is there is only callback registered for on Message, 17 | even though you can register multiple subscriptions.. It is therefore up to you the developer to route these to the 18 | correct handler, which is why I wrote this library. 19 | 20 | I have added a simple override for the topic subscription to enable named params, really this is to avoid the 21 | inevitable tokenising of the topic which I do every time I build complex topic structures. 22 | 23 | *NOTE:* I will need to revisit this with some more validation, but for now it works for my simple requirements. 24 | 25 | 26 | # usage 27 | 28 | ```javascript 29 | var mqtt = require('mqtt') 30 | , mqttrouter = require('mqtt-router'); 31 | 32 | var settings = { 33 | reconnectPeriod: 5000 34 | }; 35 | 36 | // client connection 37 | var client = mqtt.connect('mqtt://localhost', settings); 38 | 39 | // enable the subscription router 40 | var router = mqttrouter.wrap(client); 41 | 42 | // subscribe to messages for 'hello/me' 43 | router.subscribe('hello/me', function(topic, message){ 44 | console.log('received', topic, message); 45 | }); 46 | 47 | // subscribe to messages for 'hello/you' 48 | router.subscribe('hello/you', function(topic, message){ 49 | console.log('received', topic, message); 50 | }); 51 | 52 | // subscribe to messages for 'some/+/you' with a named param for that token 53 | router.subscribe('some/+:person/you', function(topic, message, params){ 54 | console.log('received', topic, message); 55 | }); 56 | 57 | ``` 58 | 59 | One thing to note is that subscriptions are refreshed on reconnect, the status of the connection is also 60 | exposed via the `isConnected` method. 61 | 62 | ## License 63 | Copyright (c) 2013 Mark Wolfe 64 | Licensed under the MIT license. 65 | -------------------------------------------------------------------------------- /lib/mqtt-router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* 3 | * mqtt-router 4 | * https://github.com/wolfeidau/mqtt-router 5 | * 6 | * Copyright (c) 2013 Mark Wolfe 7 | * Licensed under the MIT license. 8 | */ 9 | var mqtt = require('mqtt') 10 | var Houkou = require('houkou') 11 | var log = require('debug')('mqtt-router:router') 12 | 13 | /** 14 | * Holds the state of a subscription to a given topic path. 15 | * 16 | * @param topic 17 | * @param route 18 | * @param handler 19 | * @param opts 20 | * @constructor 21 | */ 22 | var Endpoint = function (topic, route, handler, opts) { 23 | log('endpoint', topic) 24 | this.topic = topic 25 | this.route = route ? new Houkou(route, requirements(route)) : new Houkou(topic) 26 | this.handlers = [handler] 27 | this.opts = opts 28 | 29 | function requirements (route) { 30 | var params = route.match(/:[a-zA-Z0-9]+/g) 31 | 32 | if (!params) { 33 | return null 34 | } 35 | 36 | var obj = {requirements: {}} 37 | 38 | params.forEach(function (param) { 39 | obj.requirements[param.replace(':', '')] = '[a-zA-Z0-9_-]+' 40 | }) 41 | log('requirements', obj) 42 | 43 | return obj 44 | } 45 | 46 | this.reset = function reset () { 47 | this.handlers = [] 48 | } 49 | } 50 | 51 | /** 52 | * Wraps the MQTT client and provides a router of sorts for handling which callbacks are invoked when a message is 53 | * received over MQTT. 54 | * 55 | * @param mqttclient 56 | * @constructor 57 | */ 58 | var Router = function (mqttclient) { 59 | this.mqttclient = mqttclient || mqtt.connect() 60 | this.endpoints = [] 61 | 62 | var self = this 63 | 64 | this.mqttclient.on('message', function (topic, message) { 65 | log('message', topic, message) 66 | 67 | self.endpoints.forEach(function (endpoint) { 68 | log('endpoint', endpoint.topic) 69 | var params = endpoint.route.match(topic) 70 | if (params) { 71 | endpoint.handlers.forEach(function (handler) { 72 | handler(topic, message, params) 73 | }) 74 | } 75 | }) 76 | }) 77 | 78 | /** 79 | * Returns whether or not the underlying MQTT connection is up. 80 | * 81 | * @returns {boolean|*} 82 | */ 83 | this.isConnected = function () { 84 | return self.mqttclient.connected 85 | } 86 | 87 | this.mqttclient.on('connect', function () { 88 | log('connect', self.mqttclient.connected) 89 | 90 | // one thing to note here is subscription operations are idempotent, so no need for fancy checking. 91 | self.endpoints.forEach(function (endpoint) { 92 | log('connect', 'subscribe', endpoint.topic, endpoint.opts) 93 | self.mqttclient.subscribe(endpoint.topic, endpoint.opts) 94 | }) 95 | }) 96 | 97 | this.mqttclient.on('close', function (err) { 98 | log('close', self.mqttclient.connected, err) 99 | }) 100 | 101 | /** 102 | * Subscribe to a topic using an optional route to break up the topic. 103 | * 104 | * @param topic - MQTT topic with named params 105 | * @param opts - Options which are passed to the MQTT client subscribe call 106 | * @param handler - Handler function 107 | */ 108 | this.subscribe = function (topic, opts, handler) { 109 | // .subscribe('topic', handler) 110 | if (typeof opts === 'function') { 111 | handler = opts 112 | opts = null 113 | } 114 | 115 | var safeTopic = this._stripParams(topic) 116 | 117 | // clean out the MQTT wild card options 118 | var routeOption = topic.replace(/\$/, '\\$').replace(/\+/, '').replace(/#/, '') 119 | 120 | var endpoint = self._endpointMatches(safeTopic)[0] 121 | 122 | if (endpoint) { 123 | endpoint.handlers.push(handler) 124 | } else { 125 | self.endpoints.push(new Endpoint(safeTopic, routeOption, handler, opts)) 126 | } 127 | log('subscribe', safeTopic) 128 | log('subscribe', 'route', routeOption) 129 | this.mqttclient.subscribe(safeTopic, opts) 130 | } 131 | 132 | this.reset = function reset () { 133 | log('reset', this.endpoints) 134 | // clear out all the handlers 135 | this.endpoints.forEach(function resetEndpoint (endpoint) { 136 | endpoint.reset() 137 | }) 138 | this.endpoints = [] 139 | } 140 | 141 | this._stripParams = function (topic) { 142 | var safeTopic = topic.replace(/\$/, '\\$') 143 | safeTopic = topic.replace(/:[a-zA-Z0-9]+/g, '') 144 | return safeTopic 145 | } 146 | 147 | this._endpointMatches = function (topic) { 148 | return this.endpoints.map(function (endpoint) { 149 | return endpoint.topic === topic ? endpoint : null 150 | }) 151 | } 152 | } 153 | 154 | module.exports = Router 155 | -------------------------------------------------------------------------------- /test/router_test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it, beforeEach, afterEach */ 3 | 4 | var chai = require('chai') 5 | var sinon = require('sinon') 6 | var mqtt = require('mqtt') 7 | var log = require('debug')('test:mqtt-router') 8 | var should = chai.should() // eslint-disable-line 9 | var mqttrouter = require('../index.js') 10 | var mosca = require('mosca') 11 | 12 | describe('router', function () { 13 | var mqttBroker 14 | beforeEach(() => { 15 | mqttBroker = new mosca.Server({ 16 | persistence: { 17 | factory: mosca.persistence.Memory 18 | } 19 | }) 20 | }) 21 | 22 | afterEach(() => { 23 | mqttBroker.close() 24 | }) 25 | it('should route one message to the handler', function (done) { 26 | var mqttclient = mqtt.connect(`mqtt://localhost:${mqttBroker.opts.port}`) 27 | var router = mqttrouter.wrap(mqttclient) 28 | 29 | var firstTopic = 'TEST/localtime/request' 30 | var secondTopic = 'TEST/localtime/reply' 31 | 32 | function check () { 33 | callback.calledOnce.should.equal(true) 34 | callback.getCall(0).args[0].should.equal(firstTopic) 35 | router.reset() 36 | done() 37 | } 38 | 39 | var callback = sinon.spy(function (topic, message) { 40 | log('msg', topic, message) 41 | check() 42 | }) 43 | router.subscribe(firstTopic, callback) 44 | log('publish', firstTopic) 45 | mqttclient.publish(firstTopic, 'hello firstTopic!') 46 | 47 | log('publish', secondTopic) 48 | mqttclient.publish(secondTopic, 'hello secondTopic!') 49 | }) 50 | 51 | it('should route one message to wild card handler', function (done) { 52 | var mqttclient = mqtt.connect(`mqtt://localhost:${mqttBroker.opts.port}`) 53 | var router = mqttrouter.wrap(mqttclient) 54 | 55 | var firstTopic = 'TEST/beertime/request' 56 | var secondTopic = 'TEST/remotetime/reply' 57 | 58 | function check () { 59 | callback.calledOnce.should.equal(true) 60 | callback.getCall(0).args[0].should.equal(firstTopic) 61 | router.reset() 62 | done() 63 | } 64 | 65 | var callback = sinon.spy(function (topic, message, params) { 66 | log('msg', topic, message, params) 67 | check() 68 | }) 69 | 70 | router.subscribe('TEST/beertime/#:type', callback) 71 | 72 | log('publish', firstTopic) 73 | mqttclient.publish(firstTopic, 'hello firstTopic!') 74 | 75 | log('publish', secondTopic) 76 | mqttclient.publish(secondTopic, 'hello secondTopic!') 77 | }) 78 | 79 | it('should route one message to wild card handler with two params', function (done) { 80 | var mqttclient = mqtt.connect(`mqtt://localhost:${mqttBroker.opts.port}`) 81 | var router = mqttrouter.wrap(mqttclient) 82 | 83 | var firstTopic = '$TEST/greentime/request/1' 84 | var secondTopic = '$TEST/sometime/reply' 85 | 86 | function check () { 87 | callback.calledOnce.should.equal(true) 88 | callback.getCall(0).args[0].should.equal(firstTopic) 89 | callback.getCall(0).args[2].type.should.equal('request') 90 | callback.getCall(0).args[2].no.should.equal('1') 91 | router.reset() 92 | done() 93 | } 94 | 95 | var callback = sinon.spy(function (topic, message, params) { 96 | log('msg', topic, message, params) 97 | check() 98 | }) 99 | 100 | router.subscribe('$TEST/greentime/+:type/+:no', callback) 101 | 102 | log('publish', firstTopic) 103 | mqttclient.publish(firstTopic, 'hello firstTopic!') 104 | 105 | log('publish', secondTopic) 106 | mqttclient.publish(secondTopic, 'hello secondTopic!') 107 | }) 108 | 109 | it('should route one message to single level wild card', function (done) { 110 | var mqttclient = mqtt.connect(`mqtt://localhost:${mqttBroker.opts.port}`) 111 | var router = mqttrouter.wrap(mqttclient) 112 | 113 | var firstTopic = 'TEST/winetime/request' 114 | var secondTopic = 'TEST/whiskeytime/reply' 115 | 116 | function check () { 117 | callback.calledOnce.should.equal(true) 118 | callback.getCall(0).args[0].should.equal(firstTopic) 119 | router.reset() 120 | done() 121 | } 122 | 123 | var callback = sinon.spy(function (topic, message, params) { 124 | log('msg', topic, message, params) 125 | check() 126 | }) 127 | 128 | router.subscribe('TEST/+:time/request', callback) 129 | 130 | log('publish', firstTopic) 131 | mqttclient.publish(firstTopic, 'hello firstTopic!') 132 | 133 | log('publish', secondTopic) 134 | mqttclient.publish(secondTopic, 'hello secondTopic!') 135 | }) 136 | 137 | it('should route one message to the handler with $ in topic name', function (done) { 138 | var mqttclient = mqtt.connect(`mqtt://localhost:${mqttBroker.opts.port}`) 139 | var router = mqttrouter.wrap(mqttclient) 140 | 141 | var firstTopic = '$TEST/localtime/request' 142 | var secondTopic = '$TEST/localtime/reply' 143 | 144 | function check () { 145 | callback.calledOnce.should.equal(true) 146 | callback.getCall(0).args[0].should.equal(firstTopic) 147 | router.reset() 148 | done() 149 | } 150 | 151 | var callback = sinon.spy(function (topic, message) { 152 | log('msg', topic, message) 153 | check() 154 | }) 155 | 156 | router.subscribe(firstTopic, callback) 157 | 158 | log('publish', firstTopic) 159 | mqttclient.publish(firstTopic, 'hello firstTopic!') 160 | 161 | log('publish', secondTopic) 162 | mqttclient.publish(secondTopic, 'hello secondTopic!') 163 | }) 164 | }) 165 | --------------------------------------------------------------------------------