├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── config.js ├── index.js ├── jsdoc └── sandbox.hbs ├── lib ├── backend │ └── file.js ├── core.js └── worker.js ├── package.json ├── test ├── create-example-data.js └── test.js └── ui ├── hgrip.png ├── index.html ├── script.js ├── style.css └── vgrip.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .travis.yml 3 | test 4 | jsdoc 5 | coverage 6 | .nyc_output 7 | .gitignore 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | before_install: 5 | - sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa -y 6 | - sudo apt-get update -y 7 | - sudo apt-get install mosquitto -y 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Sebastian Raff (https://hobbyquaker.github.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a NoSQL Database, to be precise: a JSON document store with MQTT interface, CouchDB/MapReduce inspired views, 2 | implemented in Node.js. 🤠 3 | 4 | 5 | [![NPM version](https://badge.fury.io/js/mqttdb.svg)](http://badge.fury.io/js/mqttdb) 6 | [![Dependencies Status](https://david-dm.org/hobbyquaker/mqttDB/status.svg)](https://david-dm.org/hobbyquaker/mqttDB) 7 | [![Build Status](https://travis-ci.org/hobbyquaker/mqttDB.svg?branch=master)](https://travis-ci.org/hobbyquaker/mqttDB) 8 | [![Coverage Status](https://coveralls.io/repos/github/hobbyquaker/mqttDB/badge.svg?branch=master)](https://coveralls.io/github/hobbyquaker/mqttDB?branch=master) 9 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 10 | [![License][mit-badge]][mit-url] 11 | 12 | 13 | It's intended to be used as a database for storing metadata in systems that use MQTT as message bus, I'm using it in 14 | conjunction with [mqtt-smarthome](https://github.com/mqtt-smarthome/mqtt-smarthome), but I think it could be useful in 15 | other MQTT based environments also. 16 | 17 | You can create and modify documents by publishing JSON payloads to MQTT and receive document changes by simply 18 | subscribing to certain topics. You can create views by defining map and reduce functions and filter document ids with 19 | MQTT style wildcards. 20 | 21 | _Note that this project is not associated with or endorsed by http://mqtt.org_ 22 | 23 | ## Documentation 24 | 25 | * [Introduction](https://github.com/hobbyquaker/mqttDB/wiki/Introduction) 26 | * [Install](https://github.com/hobbyquaker/mqttDB/wiki/Install) 27 | * [Command Line Parameters](https://github.com/hobbyquaker/mqttDB/wiki/Command-Line-Parameters) 28 | * [Connecting to the MQTT Broker](https://github.com/hobbyquaker/mqttDB/wiki/Command-Line-Parameters#connecting-to-the-mqtt-broker) 29 | * [WebUI](https://github.com/hobbyquaker/mqttDB/wiki/WebUI) 30 | * [Documents](https://github.com/hobbyquaker/mqttDB/wiki/Documents) 31 | * [Document IDs](https://github.com/hobbyquaker/mqttDB/wiki/Documents#ids) 32 | * [Create or Overwrite Documents](https://github.com/hobbyquaker/mqttDB/wiki/Documents#create-or-overwrite-a-document) 33 | * [Extend Documents](https://github.com/hobbyquaker/mqttDB/wiki/Documents#extend-a-document) 34 | * [Delete Documents](https://github.com/hobbyquaker/mqttDB/wiki/Documents#deletion-of-documents) 35 | * [Property Access](https://github.com/hobbyquaker/mqttDB/wiki/Documents#property-access) 36 | * [Internal Properties](https://github.com/hobbyquaker/mqttDB/wiki/Documents#internal-properties) 37 | * [Views](https://github.com/hobbyquaker/mqttDB/wiki/Views) 38 | * [What are Views?](https://github.com/hobbyquaker/mqttDB/wiki/Views#what-are-views) 39 | * [Create Views](https://github.com/hobbyquaker/mqttDB/wiki/Views#create-views) 40 | * [Delete Views](https://github.com/hobbyquaker/mqttDB/wiki/Views#delete-views) 41 | * [Javascript](https://github.com/hobbyquaker/mqttDB/wiki/Views#javascript) 42 | * [How Views are Composed](https://github.com/hobbyquaker/mqttDB/wiki/Views#how-views-are-composed) 43 | * [View Examples](https://github.com/hobbyquaker/mqttDB/wiki/View-Examples) 44 | * [Topic Reference](https://github.com/hobbyquaker/mqttDB/wiki/Topics) 45 | * [Topics on which mqttDB publishes](https://github.com/hobbyquaker/mqttDB/wiki/Topics#topics-on-which-mqttdb-publishes) 46 | * [Topics subscribed by mqttDB](https://github.com/hobbyquaker/mqttDB/wiki/Topics#topics-subscribed-by-mqttdb) 47 | * [Sandbox Reference](https://github.com/hobbyquaker/mqttDB/wiki/Sandbox) 48 | * [Performance](https://github.com/hobbyquaker/mqttDB/wiki/Performance) 49 | 50 | 51 | ## Contributing 52 | 53 | Any form of feedback is highly appreciated, may it be questions, suggestions, feature requests, bug reports, critics, 54 | salutes or rants! 😉 Feel free to [create an Issue](https://github.com/hobbyquaker/mqttDB/issues/new)! 55 | 56 | Pull Requests Welcome! 57 | 58 | 59 | ## License 60 | 61 | MIT (c) 2017 [Sebastian Raff](https://github.com/hobbyquaker) 62 | 63 | [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 64 | [mit-url]: LICENSE 65 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const config = require('yargs') 3 | .usage('Usage: $0 [options]') 4 | .describe('v', 'possible values: "error", "warn", "info", "debug"') 5 | .describe('n', 'instance name. used as topic prefix') 6 | .describe('u', 'mqtt broker url.') 7 | .describe('p', 'web server port') 8 | .describe('i', 'web server interface') 9 | .describe('x', 'diable web server') 10 | .describe('w', 'number of worker processes') 11 | .describe('r', 'disable retained publish of docs and views') 12 | .describe('s', 'timeout in milliseconds for map/reduce script execution') 13 | .describe('h', 'show help') 14 | .alias({ 15 | h: 'help', 16 | i: 'web-interface', 17 | n: 'name', 18 | p: 'web-port', 19 | r: 'retain-disable', 20 | s: 'script-timeout', 21 | u: 'url', 22 | v: 'verbosity', 23 | w: 'workers', 24 | x: 'web-disable' 25 | }) 26 | .default({ 27 | u: 'mqtt://127.0.0.1', 28 | i: '0.0.0.0', 29 | n: '$db', 30 | v: 'info', 31 | p: 8092, 32 | w: os.cpus().length, 33 | s: 10000 34 | }) 35 | .version() 36 | .help('help') 37 | .argv; 38 | 39 | module.exports = config; 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const http = require('http'); 5 | const express = require('express'); 6 | 7 | const app = express(); 8 | 9 | const server = http.Server(app); // eslint-disable-line new-cap 10 | const io = require('socket.io')(server); 11 | 12 | const log = require('yalm'); 13 | const Mqtt = require('mqtt'); 14 | const mw = require('mqtt-wildcard'); 15 | const config = require('./config.js'); 16 | const pkg = require('./package.json'); 17 | const Core = require('./lib/core.js'); 18 | 19 | log.setLevel(config.verbosity); 20 | 21 | const core = new Core(config, log); 22 | 23 | let mqttConnected = false; 24 | 25 | log.info(pkg.name + ' ' + pkg.version + ' starting'); 26 | log.info('mqtt trying to connect', config.url); 27 | 28 | const mqtt = Mqtt.connect(config.url, {will: {topic: config.name + '/connected', payload: '0', retain: true}}); 29 | 30 | mqtt.on('connect', () => { 31 | mqttConnected = true; 32 | 33 | log.info('mqtt connected', config.url); 34 | mqtt.publish(config.name + '/connected', '1', {retain: true}); 35 | 36 | log.debug('mqtt subscribe', config.name + '/set/#'); 37 | mqtt.subscribe(config.name + '/set/#'); 38 | log.debug('mqtt subscribe', config.name + '/extend/#'); 39 | mqtt.subscribe(config.name + '/extend/#'); 40 | log.debug('mqtt subscribe', config.name + '/prop/#'); 41 | mqtt.subscribe(config.name + '/prop/#'); 42 | log.debug('mqtt subscribe', config.name + '/query/#'); 43 | mqtt.subscribe(config.name + '/query/#'); 44 | log.debug('mqtt subscribe', config.name + '/get/+/#'); 45 | mqtt.subscribe(config.name + '/get/+/#'); 46 | }); 47 | 48 | mqtt.on('close', () => { 49 | if (mqttConnected) { 50 | mqttConnected = false; 51 | log.info('mqtt closed ' + config.url); 52 | } 53 | }); 54 | 55 | /* istanbul ignore next */ 56 | mqtt.on('error', err => { 57 | log.error('mqtt', err); 58 | }); 59 | 60 | mqtt.on('offline', () => { 61 | log.warn('mqtt offline'); 62 | }); 63 | 64 | mqtt.on('reconnect', () => { 65 | log.debug('mqtt reconnect'); 66 | }); 67 | 68 | mqtt.on('message', (topic, payload) => { 69 | payload = payload.toString(); 70 | log.debug('mqtt <', topic, payload); 71 | const match = mw(topic, config.name + '/+/#'); 72 | const [cmd, id] = match; 73 | 74 | if (!match || !id) { 75 | log.error('malformed topic', topic); 76 | return; 77 | } 78 | 79 | /* eslint-disable no-case-declarations */ 80 | switch (cmd) { 81 | case 'set': 82 | case 'extend': 83 | case 'query': 84 | case 'prop': 85 | let data; 86 | if (payload === '') { 87 | data = payload; 88 | } else { 89 | try { 90 | data = JSON.parse(payload); 91 | } catch (err) { 92 | log.error('malformed payload', err); 93 | return; 94 | } 95 | } 96 | try { 97 | core[cmd](id, data); 98 | } catch (err) { 99 | /* istanbul ignore next */ 100 | log.error('error in mqtt message handler', err.message); 101 | } 102 | 103 | break; 104 | 105 | case 'get': 106 | const [type, gid] = mw(topic, config.name + '/get/+/#'); 107 | get(type, gid); 108 | break; 109 | 110 | /* istanbul ignore next */ 111 | default: 112 | log.error('unknown cmd', cmd); 113 | } 114 | }); 115 | 116 | function get(type, id) { 117 | switch (type) { 118 | case 'doc': 119 | mqtt.publish(config.name + '/doc/' + id, core.db[id] ? JSON.stringify(core.db[id]) : ''); 120 | break; 121 | case 'view': 122 | mqtt.publish(config.name + '/view/' + id, core.views[id] ? JSON.stringify(core.views[id]) : ''); 123 | break; 124 | default: 125 | log.error('unknown get type', type); 126 | } 127 | } 128 | 129 | core.on('ready', () => { 130 | /* istanbul ignore else */ 131 | if (!config.retainDisable) { 132 | const did = Object.keys(core.db); 133 | did.forEach(id => { 134 | mqtt.publish(config.name + '/doc/' + id, JSON.stringify(core.db[id]), {retain: true}); 135 | }); 136 | if (did.length > 0) { 137 | log.info('published ' + did.length + ' docs'); 138 | } 139 | const vid = Object.keys(core.views); 140 | vid.forEach(id => { 141 | mqtt.publish(config.name + '/view/' + id, JSON.stringify(core.views[id]), {retain: true}); 142 | }); 143 | if (vid.length > 0) { 144 | log.info('published ' + vid.length + ' views'); 145 | } 146 | } 147 | mqtt.publish(config.name + '/rev', String(core.rev), {retain: true}); 148 | mqtt.publish(config.name + '/connected', '2', {retain: true}); 149 | }); 150 | 151 | core.on('update', (id, data) => { 152 | log.debug('mqtt >', id, 'rev', data.rev); 153 | mqtt.publish(config.name + '/doc/' + id, JSON.stringify(data), {retain: !config.retainDisable}); 154 | log.debug('mqtt > rev', core.rev); 155 | mqtt.publish(config.name + '/rev', String(core.rev), {retain: true}); 156 | io.emit('objectIds', Object.keys(core.db)); 157 | }); 158 | 159 | core.on('view', (id, data) => { 160 | io.emit('updateView', id); 161 | if (data && data.error) { 162 | log.error('view', id, ':', data); 163 | } else { 164 | log.debug('view', id, 'rev', core.views[id] && core.views[id]._rev); 165 | } 166 | const payload = data ? JSON.stringify(data) : ''; 167 | mqtt.publish(config.name + '/view/' + id, payload, {retain: !config.retainDisable}); 168 | io.emit('viewIds', Object.keys(core.views)); 169 | }); 170 | 171 | /* istanbul ignore next */ 172 | core.on('error', err => { 173 | log.error(err); 174 | }); 175 | 176 | /* istanbul ignore else */ 177 | if (!config.webDisable) { 178 | server.listen(config.webPort, config.webInterface); 179 | log.info('http server listening on ' + config.webInterface + ':' + config.webPort); 180 | 181 | app.get('/', (req, res) => { 182 | res.redirect(301, '/ui'); 183 | }); 184 | app.use('/ui', express.static(path.join(__dirname, '/ui'))); 185 | app.use('/node_modules', express.static(path.join(__dirname, '/node_modules'))); 186 | 187 | io.on('connection', socket => { 188 | io.emit('objectIds', Object.keys(core.db)); 189 | io.emit('viewIds', Object.keys(core.views)); 190 | 191 | socket.on('getObject', (id, cb) => { 192 | cb(core.db[id]); 193 | }); 194 | 195 | socket.on('getView', (id, cb) => { 196 | cb({id, query: core.queries[id], view: core.views[id]}); 197 | }); 198 | 199 | socket.on('set', (id, data, cb) => { 200 | if (data._rev === null || !core.db[id] || data._rev === core.db[id]._rev) { 201 | core.set(id, data); 202 | socket.emit('objectIds', Object.keys(core.db).sort()); 203 | cb('ok'); 204 | } else { 205 | cb('rev mismatch ' + core.db[id]._rev); 206 | } 207 | }); 208 | 209 | socket.on('del', (id, cb) => { 210 | core.del(id); 211 | cb('ok'); 212 | socket.emit('objectIds', Object.keys(core.db).sort()); 213 | }); 214 | 215 | socket.on('query', (id, payload, cb) => { 216 | core.query(id, payload); 217 | cb('ok'); 218 | socket.emit('viewIds', Object.keys(core.views).sort()); 219 | }); 220 | }); 221 | } 222 | -------------------------------------------------------------------------------- /jsdoc/sandbox.hbs: -------------------------------------------------------------------------------- 1 | **Documentation of the sandbox in which the _map_ and _reduce_ scripts are executed.** 2 | 3 | Keep in mind that you don't have access to Node.js globals like e.g. `require`, `process` or `console` and that the 4 | documents in the sandbox are frozen, so no change on the database contents is possible by the _map_ and _reduce_ 5 | scripts. 6 | 7 | --- 8 | 9 | {{>main}} 10 | -------------------------------------------------------------------------------- /lib/backend/file.js: -------------------------------------------------------------------------------- 1 | module.exports = function (core) { 2 | const pjson = require('persist-json')('mqttdb', {secure: true}); 3 | const timeout = 250; 4 | 5 | let timerDb; 6 | let timerViews; 7 | let timerQueries; 8 | 9 | let countDb = 0; 10 | let countViews = 0; 11 | let countQueries = 0; 12 | 13 | pjson.load(core.name + '.docs', (err, data) => { 14 | if (err) { 15 | core.log.warn(err.message); 16 | } else { 17 | core.log.info('database loaded'); 18 | } 19 | core.rev = (data && data.rev) || 0; 20 | core.db = (data && data.db) || {}; 21 | 22 | pjson.load(core.name + '.views', (err, data) => { 23 | if (err) { 24 | core.log.warn(err.message); 25 | } else { 26 | core.log.info('views loaded'); 27 | } 28 | core.views = (data && data.views) || {}; 29 | 30 | pjson.load(core.name + '.queries', (err, data) => { 31 | if (err) { 32 | core.log.warn(err.message); 33 | } else { 34 | core.log.info('queries loaded'); 35 | } 36 | core.queries = (data && data.queries) || {}; 37 | core.emit('ready'); 38 | }); 39 | }); 40 | }); 41 | 42 | function updateDb() { 43 | if (timerDb && (countDb < 10)) { 44 | clearTimeout(timerDb); 45 | countDb += 1; 46 | timerDb = setTimeout(() => { 47 | timerDb = null; 48 | pjson.save(core.name + '.docs', {rev: core.rev, db: core.db}, () => { 49 | core.log.debug('saved', core.name + '.docs rev', core.rev); 50 | }); 51 | }, timeout); 52 | } else if (timerDb) { 53 | clearTimeout(timerDb); 54 | timerDb = null; 55 | countDb = 0; 56 | pjson.save(core.name + '.docs', {rev: core.rev, db: core.db}, () => { 57 | core.log.debug('saved', core.name + '.docs rev', core.rev); 58 | }); 59 | } else { 60 | timerDb = setTimeout(() => { 61 | timerDb = null; 62 | pjson.save(core.name + '.docs', {rev: core.rev, db: core.db}, () => { 63 | core.log.debug('saved', core.name + '.docs rev', core.rev); 64 | }); 65 | }, timeout); 66 | } 67 | } 68 | 69 | function updateViews() { 70 | if (timerViews && (countViews < 10)) { 71 | clearTimeout(timerViews); 72 | countViews += 1; 73 | timerViews = setTimeout(() => { 74 | timerViews = null; 75 | pjson.save(core.name + '.views', {views: core.views}, () => {}); 76 | }, timeout); 77 | } else if (timerViews) { 78 | clearTimeout(timerViews); 79 | timerViews = null; 80 | countViews = 0; 81 | pjson.save(core.name + '.views', {views: core.views}, () => {}); 82 | } else { 83 | timerViews = setTimeout(() => { 84 | timerViews = null; 85 | pjson.save(core.name + '.views', {views: core.views}, () => {}); 86 | }, timeout); 87 | } 88 | } 89 | 90 | function updateQueries() { 91 | /* istanbul ignore else */ 92 | if (timerQueries && (countQueries < 10)) { 93 | clearTimeout(timerQueries); 94 | countQueries += 1; 95 | timerQueries = setTimeout(() => { 96 | timerQueries = null; 97 | pjson.save(core.name + '.queries', {queries: core.queries}, () => {}); 98 | }, timeout); 99 | } else if (timerQueries) { 100 | clearTimeout(timerQueries); 101 | timerQueries = null; 102 | countQueries = 0; 103 | pjson.save(core.name + '.queries', {queries: core.queries}, () => {}); 104 | } else { 105 | timerQueries = setTimeout(() => { 106 | timerQueries = null; 107 | pjson.save(core.name + '.queries', {queries: core.queries}, () => {}); 108 | }, timeout); 109 | } 110 | } 111 | 112 | core.on('update', () => { 113 | core.rev += 1; 114 | updateDb(); 115 | }); 116 | 117 | core.on('view', () => { 118 | updateViews(); 119 | }); 120 | 121 | core.on('query', () => { 122 | updateQueries(); 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const cluster = require('cluster'); 4 | const {EventEmitter} = require('events'); 5 | const oe = require('obj-ease'); 6 | 7 | let time; 8 | let timeEnd; 9 | 10 | class Core extends EventEmitter { 11 | constructor(config, log) { 12 | super(); 13 | /* istanbul ignore next */ 14 | config = config || {}; 15 | this.log = log; 16 | this.db = {}; 17 | this.queries = {}; 18 | this.views = {}; 19 | this.viewsEnv = {}; 20 | /* istanbul ignore next */ 21 | this.name = config.name || 'db'; 22 | /* istanbul ignore next */ 23 | this.backend = config.backend || 'file'; 24 | /* istanbul ignore next */ 25 | this.scriptTimeout = config.scriptTimeout || 10000; 26 | 27 | /* istanbul ignore else */ 28 | if (config.verbosity === 'debug') { 29 | time = console.time; // eslint-disable-line prefer-destructuring 30 | timeEnd = console.timeEnd; // eslint-disable-line prefer-destructuring 31 | } else { 32 | time = () => {}; 33 | timeEnd = () => {}; 34 | } 35 | 36 | require(path.join(__dirname, 'backend', this.backend + '.js'))(this); 37 | 38 | this.workers = []; 39 | 40 | /* istanbul ignore next */ 41 | this.numWorkers = config.workers || os.cpus().length; 42 | 43 | cluster.setupMaster({ 44 | exec: path.join(__dirname, 'worker.js'), 45 | args: [], 46 | silent: false 47 | }); 48 | 49 | cluster.on('online', worker => { 50 | this.workers.push(worker); 51 | log.info('worker ' + worker.process.pid + ' is online'); 52 | worker.on('message', msg => { 53 | log.debug('message from worker', worker.process.pid, msg.type, msg.id); 54 | switch (msg.type) { 55 | case 'view': 56 | if (msg.payload === '') { 57 | delete this.views[msg.id]; 58 | } else { 59 | this.views[msg.id] = msg.payload; 60 | } 61 | this.emit('view', msg.id, msg.payload); 62 | break; 63 | 64 | /* istanbul ignore next */ 65 | default: 66 | } 67 | }); 68 | /* istanbul ignore next */ 69 | worker.on('disconnect', () => { 70 | log.error('worker', worker.process.pid, 'died'); 71 | log.error('exiting'); 72 | process.exit(1); // eslint-disable-line unicorn/no-process-exit 73 | }); 74 | if (this.workers.length === this.numWorkers) { 75 | log.info('all workers ready'); 76 | startInit(); 77 | } 78 | }); 79 | 80 | const env = {}; 81 | 82 | /* istanbul ignore else */ 83 | if (config.verbosity === 'debug') { 84 | env.DEBUG = '1'; 85 | } 86 | /* istanbul ignore else */ 87 | if (config.scriptTimeout !== 'undefined') { 88 | env.SCRIPT_TIMEOUT = config.scriptTimeout; 89 | } 90 | for (let i = 0; i < this.numWorkers; i++) { 91 | cluster.fork(env); 92 | } 93 | 94 | this.on('update', (id, payload) => { 95 | this.workers.forEach(worker => { 96 | time('sending update to worker ' + worker.process.pid); 97 | worker.send({type: 'update', id, payload, rev: this.rev}); 98 | timeEnd('sending update to worker ' + worker.process.pid); 99 | }); 100 | }); 101 | 102 | this.on('ready', () => { 103 | startInit(); 104 | }); 105 | 106 | let eventCount = 0; 107 | function startInit() { 108 | eventCount += 1; 109 | if (eventCount === 2) { 110 | init(); 111 | } 112 | } 113 | 114 | const that = this; 115 | 116 | function init() { 117 | that.workers.forEach(worker => { 118 | time('sending db to worker ' + worker.process.pid); 119 | worker.send({type: 'db', db: that.db, rev: that.rev}); 120 | timeEnd('sending db to worker ' + worker.process.pid); 121 | }); 122 | Object.keys(that.queries).forEach(id => { 123 | that.query(id, that.queries[id], true); 124 | }); 125 | log.info('init complete'); 126 | } 127 | } 128 | 129 | getRev(id) { 130 | if (this.db[id] && (typeof this.db[id]._rev !== 'undefined')) { 131 | const rev = this.db[id]._rev; 132 | delete this.db[id]._rev; 133 | return rev; 134 | } 135 | return -1; 136 | } 137 | 138 | setRev(id, rev) { 139 | /* istanbul ignore else */ 140 | if (this.db[id]) { 141 | this.db[id]._rev = rev; 142 | } 143 | } 144 | 145 | incRev(id, rev) { 146 | /* istanbul ignore else */ 147 | if (this.db[id]) { 148 | this.db[id]._rev = rev + 1; 149 | } 150 | } 151 | 152 | set(id, payload) { 153 | if (payload === '') { 154 | return this.del(id); 155 | } 156 | /* istanbul ignore if */ 157 | if (typeof payload !== 'object') { 158 | return false; 159 | } 160 | delete payload._rev; 161 | delete payload._id; 162 | const rev = this.getRev(id); 163 | if (this.db[id]) { 164 | delete this.db[id]._id; 165 | } 166 | if (!oe.equal(this.db[id], payload)) { 167 | this.db[id] = payload; 168 | this.db[id]._id = id; 169 | this.incRev(id, rev); 170 | this.emit('update', id, this.db[id]); 171 | return true; 172 | } 173 | this.db[id]._id = id; 174 | 175 | this.setRev(id, rev); 176 | return false; 177 | } 178 | 179 | prop(id, payload) { 180 | let rev; 181 | switch (payload.method) { 182 | case 'set': 183 | rev = this.getRev(id); 184 | if (oe.setProp(this.db[id], payload.prop, payload.val)) { 185 | delete this.db[id]._id; 186 | this.db[id]._id = id; 187 | this.incRev(id, rev); 188 | this.emit('update', id, this.db[id]); 189 | return true; 190 | } 191 | this.setRev(id, rev); 192 | return false; 193 | 194 | case 'create': 195 | if (typeof oe.getProp(this.db[id], payload.prop) === 'undefined') { 196 | rev = this.getRev(id); 197 | oe.setProp(this.db[id], payload.prop, payload.val); 198 | this.db[id]._id = id; 199 | this.incRev(id, rev); 200 | this.emit('update', id, this.db[id]); 201 | return true; 202 | } 203 | return false; 204 | 205 | case 'del': 206 | rev = this.getRev(id); 207 | /* istanbul ignore else */ 208 | if (oe.delProp(this.db[id], payload.prop)) { 209 | this.db[id]._id = id; 210 | this.incRev(id, rev); 211 | this.emit('update', id, this.db[id]); 212 | return true; 213 | } 214 | /* istanbul ignore next */ 215 | this.setRev(id, rev); 216 | /* istanbul ignore next */ 217 | return false; 218 | 219 | /* istanbul ignore next */ 220 | default: 221 | } 222 | } 223 | 224 | extend(id, payload) { 225 | /* istanbul ignore if */ 226 | if (!payload || typeof payload !== 'object') { 227 | return false; 228 | } 229 | const rev = this.getRev(id); 230 | delete payload._id; 231 | delete payload._rev; 232 | if (this.db[id]) { 233 | delete this.db[id]._id; 234 | } else { 235 | this.db[id] = {}; 236 | } 237 | const change = oe.extend(this.db[id], payload); 238 | this.db[id]._id = id; 239 | if (change) { 240 | this.incRev(id, rev); 241 | this.emit('update', id, this.db[id]); 242 | return true; 243 | } 244 | this.setRev(id, rev); 245 | return false; 246 | } 247 | 248 | del(id) { 249 | delete this.db[id]; 250 | this.emit('update', id, ''); 251 | } 252 | 253 | getWorker() { 254 | this.nextWorker = this.nextWorker || 0; 255 | const id = this.nextWorker; 256 | this.nextWorker += 1; 257 | /* istanbul ignore if */ 258 | if (this.nextWorker >= this.numWorkers) { 259 | this.nextWorker = 0; 260 | } 261 | return id; 262 | } 263 | 264 | query(id, payload, init) { 265 | if (!this.viewsEnv[id]) { 266 | this.viewsEnv[id] = {}; 267 | } 268 | let workerId; 269 | if (!this.viewsEnv[id] || typeof this.viewsEnv[id].worker === 'undefined' || this.viewsEnv[id].worker >= this.numWorkers) { 270 | workerId = this.getWorker(); 271 | this.viewsEnv[id].worker = workerId; 272 | } else { 273 | workerId = this.viewsEnv[id].worker; 274 | } 275 | 276 | if (payload === '') { 277 | delete this.queries[id]; 278 | delete this.viewsEnv[id]; 279 | delete this.views[id]; 280 | } else { 281 | this.queries[id] = payload; 282 | } 283 | 284 | if (!init) { 285 | this.emit('query'); 286 | } 287 | 288 | this.log.debug('sending query', id, 'to worker', workerId, payload); 289 | this.workers[workerId].send({type: 'query', id, payload, init: init ? this.views[id] : undefined}); 290 | } 291 | } 292 | 293 | module.exports = Core; 294 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | const domain = require('domain'); 2 | const vm = require('vm'); 3 | const oe = require('obj-ease'); 4 | const mqttWildcard = require('mqtt-wildcard'); 5 | 6 | /* istanbul ignore next */ 7 | const time = process.env.DEBUG ? console.time : () => {}; 8 | /* istanbul ignore next */ 9 | const timeEnd = process.env.DEBUG ? console.timeEnd : () => {}; 10 | 11 | class Views { 12 | constructor() { 13 | this.db = {}; 14 | this.queries = {}; 15 | this.views = {}; 16 | this.viewsEnv = {}; 17 | this.updateQueue = []; 18 | 19 | /* istanbul ignore next */ 20 | this.scriptTimeout = (typeof process.env.SCRIPT_TIMEOUT === 'undefined') ? 0 : process.env.SCRIPT_TIMEOUT; 21 | 22 | process.on('message', msg => { 23 | switch (msg.type) { 24 | case 'db': 25 | this.db = msg.db; 26 | this.rev = msg.rev; 27 | Object.keys(this.db).forEach(id => { 28 | Object.freeze(this.db[id]); 29 | }); 30 | break; 31 | case 'update': 32 | if (msg.payload === '') { 33 | delete this.db[msg.id]; 34 | } else { 35 | this.db[msg.id] = msg.payload; 36 | Object.freeze(this.db[msg.id]); 37 | } 38 | this.rev = msg.rev; 39 | this.updateViews(); 40 | break; 41 | 42 | case 'query': 43 | if (msg.init) { 44 | this.views[msg.id] = msg.init; 45 | } 46 | this.query(msg.id, msg.payload, Boolean(msg.init)); 47 | break; 48 | 49 | /* istanbul ignore next */ 50 | default: 51 | } 52 | }); 53 | } 54 | 55 | query(id, payload) { 56 | if (payload === '') { 57 | // Delete view 58 | delete this.queries[id]; 59 | delete this.views[id]; 60 | delete this.viewsEnv[id]; 61 | process.send({type: 'view', id, payload: ''}); 62 | } else { 63 | // Create/overwrite view 64 | this.queries[id] = payload; 65 | 66 | const {map, reduce, filter} = payload; 67 | 68 | if (!this.viewsEnv[id]) { 69 | this.viewsEnv[id] = {}; 70 | } 71 | this.viewsEnv[id].rev = -1; 72 | 73 | if (!this.views[id]) { 74 | this.views[id] = { 75 | _id: id, 76 | _rev: -1 77 | }; 78 | } 79 | 80 | let src = ` 81 | api.map = function () { 82 | ${map} 83 | }; 84 | api._result = []; 85 | api.forEachDocument(id => {`; 86 | 87 | if (filter) { 88 | src += ` 89 | if (api.mqttWildcard(id, ${JSON.stringify(filter)})) { 90 | api.map.apply(api.getDocument(id)); 91 | }`; 92 | } else { 93 | src += ` 94 | api.map.apply(api.getDocument(id));`; 95 | } 96 | src += ` 97 | }); 98 | `; 99 | if (reduce) { 100 | src += ` 101 | api.reduce = function (result) { 102 | ${reduce} 103 | } 104 | api._result = api.reduce(api._result); 105 | `; 106 | } 107 | delete this.viewsEnv[id].context; 108 | delete this.viewsEnv[id].script; 109 | delete this.views[id].serror; 110 | try { 111 | time(process.pid + ' create script ' + id); 112 | this.viewsEnv[id].script = new vm.Script(src, { 113 | filename: 'view-' + id 114 | }); 115 | timeEnd(process.pid + ' create script ' + id); 116 | } catch (err) { 117 | this.views[id].error = 'script creation: ' + err.message; 118 | delete this.views[id].result; 119 | delete this.views[id].length; 120 | delete this.viewsEnv[id].script; 121 | process.send({type: 'view', id, payload: this.views[id]}); 122 | } 123 | 124 | /* istanbul ignore else */ 125 | if (!this.viewsEnv[id].context) { 126 | time(process.pid + ' createContext ' + id); 127 | const Sandbox = { 128 | /** 129 | * @class api 130 | */ 131 | api: { 132 | /** 133 | * @method api.forEachDocument 134 | * @param {forEachDocumentCallback} callback function called for each document 135 | * @returns {undefined} 136 | * @description executes a provided function once for each document. Mind that using this 137 | * function inside a map script can result in O(n²) complexity and could ruin view 138 | * composition performance. If you need it you should instead use it in the reduce script. 139 | */ 140 | forEachDocument: callback => Object.keys(this.db).forEach(callback), 141 | /** 142 | * @callback forEachDocumentCallback 143 | * @param {string} id the id of the current document 144 | */ 145 | 146 | /** 147 | * @method api.getDocument 148 | * @param {string} id a document id 149 | * @returns {object} the document 150 | * @description get a document by id 151 | */ 152 | getDocument: id => this.db[id], 153 | 154 | /** 155 | * @method api.getProp 156 | * @param {object} document 157 | * @param {string} property 158 | * @returns {*} the properties value or undefined 159 | * @description get a properties value 160 | * @see {@link https://github.com/hobbyquaker/obj-ease#getprop|obj-ease documentation} 161 | */ 162 | getProp: oe.getProp, 163 | 164 | /** 165 | * @method api.mqttWildcard 166 | * @param {string} id 167 | * @param {string} pattern 168 | * @returns {array|null} 169 | * @description match a string against a MQTT wildcard pattern 170 | * @see {@link https://github.com/hobbyquaker/mqtt-wildcard#api|mqtt-wildcard documentation} 171 | */ 172 | mqttWildcard, 173 | _result: [] 174 | }, 175 | /** 176 | * @method emit 177 | * @param {*} item data that gets pushed to the result array 178 | * @returns {undefined} 179 | * @description push an item to the result array 180 | */ 181 | emit: item => Sandbox.api._result.push(item) 182 | }; 183 | this.viewsEnv[id].context = vm.createContext(Sandbox); 184 | timeEnd(process.pid + ' createContext ' + id); 185 | } 186 | 187 | if (this.viewsEnv[id].script) { 188 | this.updateQueue.push(id); 189 | this.shiftQueue(); 190 | } 191 | } 192 | } 193 | 194 | execView(id, callback) { 195 | time(process.pid + ' execView ' + id); 196 | const {script} = this.viewsEnv[id]; 197 | time(process.pid + ' create domain ' + id); 198 | const viewDomain = domain.create(); 199 | timeEnd(process.pid + ' create domain ' + id); 200 | const {context} = this.viewsEnv[id]; 201 | 202 | viewDomain.on('error', err => { 203 | this.views[id].error = 'script execution: ' + err.message; 204 | delete this.views[id].result; 205 | delete this.views[id].length; 206 | process.send({type: 'view', id, payload: this.views[id]}); 207 | callback(); 208 | }); 209 | 210 | viewDomain.run(() => { 211 | setImmediate(() => { 212 | const {rev} = this; 213 | time(process.pid + ' runInContext ' + id); 214 | script.runInContext(context, { 215 | filename: 'view-' + id, 216 | timeout: this.scriptTimeout, 217 | lineOffset: 2 218 | }); 219 | timeEnd(process.pid + ' runInContext ' + id); 220 | timeEnd(process.pid + ' execView ' + id); 221 | 222 | this.viewsEnv[id].rev = rev; 223 | 224 | const res = context.api._result; 225 | /* istanbul ignore if */ 226 | if (!this.views[id]) { 227 | this.views[id] = { 228 | _rev: -1 229 | }; 230 | } 231 | delete this.views[id].error; 232 | delete this.views[id].serror; 233 | if (!oe.equal(res, this.views[id].result)) { 234 | this.views[id]._rev += 1; 235 | this.views[id].result = res; 236 | this.views[id].length = res.length; 237 | process.send({type: 'view', id, payload: this.views[id]}); 238 | } 239 | callback(); 240 | }); 241 | }); 242 | } 243 | 244 | updateViews() { 245 | Object.keys(this.queries).forEach(id => { 246 | this.updateQueue.push(id); 247 | }); 248 | setImmediate(() => { 249 | this.shiftQueue(); 250 | }); 251 | } 252 | 253 | shiftQueue() { 254 | if (this.viewRunning || this.updateQueue.length === 0) { 255 | return; 256 | } 257 | const id = this.updateQueue.shift(); 258 | if (this.viewsEnv[id].rev === this.rev) { 259 | setImmediate(() => { 260 | this.shiftQueue(); 261 | }); 262 | } else { 263 | this.viewRunning = true; 264 | this.execView(id, () => { 265 | this.viewRunning = false; 266 | setImmediate(() => { 267 | this.shiftQueue(); 268 | }); 269 | }); 270 | } 271 | } 272 | } 273 | 274 | new Views(); // eslint-disable-line no-new 275 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqttdb", 3 | "version": "1.4.2", 4 | "description": "JSON Store with MQTT Interface", 5 | "main": "index.js", 6 | "preferGlobal": true, 7 | "bin": { 8 | "mqttdb": "./index.js" 9 | }, 10 | "scripts": { 11 | "test": "camo-purge ; xo && nyc mocha --exit test/test.js && nyc report --reporter=text-lcov | coveralls --force", 12 | "lint": "xo", 13 | "lintfix": "xo --fix", 14 | "testonly": "mocha test/test.js", 15 | "testcov": "nyc mocha test/test.js", 16 | "doc": "jsdoc2md -f lib/worker.js -t jsdoc/sandbox.hbs > ../mqttDB.wiki/Sandbox.md" 17 | }, 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "repository": "https://github.com/hobbyquaker/mqttDB", 22 | "engineStrict": true, 23 | "author": "Sebastian Raff https://github.com/hobbyquaker", 24 | "license": "MIT", 25 | "dependencies": { 26 | "async": "^2.5.0", 27 | "bootstrap": "^4.1.1", 28 | "bootstrap-3-typeahead": "^4.0.2", 29 | "codemirror": "^5.29.0", 30 | "express": "^4.15.4", 31 | "font-awesome": "^4.7.0", 32 | "jquery": "^3.2.1", 33 | "jquery-resizable-dom": "^0.32.0", 34 | "json-stringify-pretty-compact": "https://github.com/hobbyquaker/json-stringify-pretty-compact", 35 | "mqtt": "^2.18.0", 36 | "mqtt-wildcard": "^3.0.9", 37 | "obj-ease": "^1.0.1", 38 | "persist-json": "^1.2.0", 39 | "socket.io": "^2.0.3", 40 | "yalm": "^4.1.0", 41 | "yargs": "^11.0.0" 42 | }, 43 | "devDependencies": { 44 | "camo-purge": "latest", 45 | "coveralls": "latest", 46 | "jsdoc-to-markdown": "latest", 47 | "mocha": "latest", 48 | "nyc": "latest", 49 | "request": "latest", 50 | "should": "latest", 51 | "socket.io-client": "^2.0.3", 52 | "stream-splitter": "latest", 53 | "xo": "latest" 54 | }, 55 | "xo": { 56 | "space": 4, 57 | "ignore": [ 58 | "test/test.js" 59 | ], 60 | "rules": { 61 | "no-case-declarations": "warn", 62 | "no-restricted-modules": "warn", 63 | "node/no-deprecated-api": "warn" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/create-example-data.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | const Mqtt = require('mqtt'); 3 | 4 | const mqtt = Mqtt.connect('mqtt://127.0.0.1'); 5 | 6 | const objects = [ 7 | {type: 'Minion', name: 'Stuart', eyes: 1}, 8 | {type: 'Minion', name: 'Carl', eyes: 1}, 9 | {type: 'Minion', name: 'Kevin', eyes: 2}, 10 | {type: 'Minion', name: 'Bob', eyes: 2}, 11 | {type: 'Minion', name: 'Jerry', eyes: 2}, 12 | {type: 'Minion', name: 'Phil', eyes: 2}, 13 | {type: 'Minion', name: 'Tom', eyes: 2}, 14 | {type: 'Minion', name: 'Tim', eyes: 2}, 15 | 16 | {type: 'Simpson', name: 'Homer'}, 17 | {type: 'Simpson', name: 'Marge'}, 18 | {type: 'Simpson', name: 'Lisa'}, 19 | {type: 'Simpson', name: 'Bart'}, 20 | {type: 'Simpson', name: 'Maggie'}, 21 | 22 | {type: 'Moon', name: 'Phobos', planet: 'Mars', diameter: 26}, 23 | {type: 'Moon', name: 'Deimos', planet: 'Mars', diameter: 15}, 24 | {type: 'Moon', name: 'Moon', planet: 'Earth', diameter: 3476}, 25 | 26 | {type: 'Moon', order: 'I', name: 'Io', desc: '', diameter: 3643, year: 1610, planet: 'Jupiter'}, 27 | {type: 'Moon', order: 'II', name: 'Europa', desc: '', diameter: 3122, year: 1610, planet: 'Jupiter'}, 28 | {type: 'Moon', order: 'III', name: 'Ganymed', desc: '', diameter: 5262, year: 1610, planet: 'Jupiter'}, 29 | {type: 'Moon', order: 'IV', name: 'Kallisto', desc: '', diameter: 4821, year: 1610, planet: 'Jupiter'}, 30 | {type: 'Moon', order: 'V', name: 'Amalthea', desc: '', diameter: 168, year: 1892, planet: 'Jupiter'}, 31 | {type: 'Moon', order: 'VI', name: 'Himalia', desc: '', diameter: 160, year: 1904, planet: 'Jupiter'}, 32 | {type: 'Moon', order: 'VII', name: 'Elara', desc: '', diameter: 78, year: 1905, planet: 'Jupiter'}, 33 | {type: 'Moon', order: 'VIII', name: 'Pasiphae', desc: '', diameter: 56, year: 1908, planet: 'Jupiter'}, 34 | {type: 'Moon', order: 'IX', name: 'Sinope', desc: '', diameter: 38, year: 1914, planet: 'Jupiter'}, 35 | {type: 'Moon', order: 'X', name: 'Lysithea', desc: '', diameter: 38, year: 1938, planet: 'Jupiter'}, 36 | {type: 'Moon', order: 'XI', name: 'Carme', desc: '', diameter: 46, year: 1938, planet: 'Jupiter'}, 37 | {type: 'Moon', order: 'XII', name: 'Ananke', desc: '', diameter: 28, year: 1951, planet: 'Jupiter'}, 38 | {type: 'Moon', order: 'XIII', name: 'Leda', desc: '', diameter: 18, year: 1973, planet: 'Jupiter'}, 39 | {type: 'Moon', order: 'XIV', name: 'Thebe', desc: 'S/1979 J 2', diameter: 98, year: 1979, planet: 'Jupiter'}, 40 | {type: 'Moon', order: 'XV', name: 'Adrastea', desc: 'S/1979 J 1', diameter: 16, year: 1979, planet: 'Jupiter'}, 41 | {type: 'Moon', order: 'XVI', name: 'Metis', desc: 'S/1979 J 3', diameter: 44, year: 1979, planet: 'Jupiter'}, 42 | {type: 'Moon', order: 'XVII', name: 'Callirrhoe', desc: 'S/1999 J 1', diameter: 9, year: 2000, planet: 'Jupiter'}, 43 | {type: 'Moon', order: 'XVIII', name: 'Themisto', desc: 'S/2000 J 1', diameter: 9, year: 1975, planet: 'Jupiter'}, 44 | {type: 'Moon', order: 'XIX', name: 'Megaclite', desc: 'S/2000 J 8', diameter: 6, year: 2001, planet: 'Jupiter'}, 45 | {type: 'Moon', order: 'XX', name: 'Taygete', desc: 'S/2000 J 9', diameter: 5, year: 2001, planet: 'Jupiter'}, 46 | {type: 'Moon', order: 'XXI', name: 'Chaldene', desc: 'S/2000 J 10', diameter: 4, year: 2001, planet: 'Jupiter'}, 47 | {type: 'Moon', order: 'XXII', name: 'Harpalyke', desc: 'S/2000 J 5', diameter: 4, year: 2001, planet: 'Jupiter'}, 48 | {type: 'Moon', order: 'XXIII', name: 'Kalyke', desc: 'S/2000 J 2', diameter: 5, year: 2001, planet: 'Jupiter'}, 49 | {type: 'Moon', order: 'XXIV', name: 'Iocaste', desc: 'S/2000 J 3', diameter: 5, year: 2001, planet: 'Jupiter'}, 50 | {type: 'Moon', order: 'XXV', name: 'Erinome', desc: 'S/2000 J 4', diameter: 3, year: 2003, planet: 'Jupiter'}, 51 | {type: 'Moon', order: 'XXVI', name: 'Isonoe', desc: 'S/2000 J 6', diameter: 4, year: 2001, planet: 'Jupiter'}, 52 | {type: 'Moon', order: 'XXVII', name: 'Praxidike', desc: 'S/2000 J 7', diameter: 7, year: 2003, planet: 'Jupiter'}, 53 | {type: 'Moon', order: 'XXVIII', name: 'Autonoe', desc: 'S/2001 J 1', diameter: 4, year: 2002, planet: 'Jupiter'}, 54 | {type: 'Moon', order: 'XXIX', name: 'Thyone', desc: 'S/2001 J 2', diameter: 4, year: 2002, planet: 'Jupiter'}, 55 | {type: 'Moon', order: 'XXX', name: 'Hermippe', desc: 'S/2001 J 3', diameter: 4, year: 2002, planet: 'Jupiter'}, 56 | {type: 'Moon', order: 'XXXI', name: 'Aitne', desc: 'S/2001 J 11', diameter: 3, year: 2002, planet: 'Jupiter'}, 57 | {type: 'Moon', order: 'XXXII', name: 'Eurydome', desc: 'S/2001 J 4', diameter: 3, year: 2002, planet: 'Jupiter'}, 58 | {type: 'Moon', order: 'XXXIII', name: 'Euanthe', desc: 'S/2001 J 7', diameter: 3, year: 2002, planet: 'Jupiter'}, 59 | {type: 'Moon', order: 'XXXIV', name: 'Euporie', desc: 'S/2001 J 10', diameter: 2, year: 2002, planet: 'Jupiter'}, 60 | {type: 'Moon', order: 'XXXV', name: 'Orthosie', desc: 'S/2001 J 9', diameter: 2, year: 2002, planet: 'Jupiter'}, 61 | {type: 'Moon', order: 'XXXVI', name: 'Sponde', desc: 'S/2001 J 5', diameter: 2, year: 2002, planet: 'Jupiter'}, 62 | {type: 'Moon', order: 'XXXVII', name: 'Kale', desc: 'S/2001 J 8', diameter: 2, year: 2002, planet: 'Jupiter'}, 63 | {type: 'Moon', order: 'XXXVIII', name: 'Pasithee', desc: 'S/2001 J 6', diameter: 2, year: 2002, planet: 'Jupiter'}, 64 | {type: 'Moon', order: 'XXXIX', name: 'Hegemone', desc: 'S/2003 J 8', diameter: 3, year: 2003, planet: 'Jupiter'}, 65 | {type: 'Moon', order: 'XL', name: 'Mneme', desc: 'S/2003 J 21', diameter: 2, year: 2003, planet: 'Jupiter'}, 66 | {type: 'Moon', order: 'XLI', name: 'Aoede', desc: 'S/2003 J 7', diameter: 4, year: 2003, planet: 'Jupiter'}, 67 | {type: 'Moon', order: 'XLII', name: 'Thelxinoe', desc: 'S/2003 J 22', diameter: 2, year: 2004, planet: 'Jupiter'}, 68 | {type: 'Moon', order: 'XLIII', name: 'Arche', desc: 'S/2002 J 1', diameter: 3, year: 2002, planet: 'Jupiter'}, 69 | {type: 'Moon', order: 'XLIV', name: 'Kallichore', desc: 'S/2003 J 11', diameter: 2, year: 2003, planet: 'Jupiter'}, 70 | {type: 'Moon', order: 'XLV', name: 'Helike', desc: 'S/2003 J 6', diameter: 4, year: 2003, planet: 'Jupiter'}, 71 | {type: 'Moon', order: 'XLVI', name: 'Carpo', desc: 'S/2003 J 20', diameter: 3, year: 2003, planet: 'Jupiter'}, 72 | {type: 'Moon', order: 'XLVII', name: 'Eukelade', desc: 'S/2003 J 1', diameter: 4, year: 2003, planet: 'Jupiter'}, 73 | {type: 'Moon', order: 'XLVIII', name: 'Cyllene', desc: 'S/2003 J 13', diameter: 2, year: 2003, planet: 'Jupiter'}, 74 | {type: 'Moon', order: 'XLIX', name: 'Kore', desc: 'S/2003 J 14', diameter: 2, year: 2003, planet: 'Jupiter'}, 75 | {type: 'Moon', order: 'L', name: 'Herse', desc: 'S/2003 J 17', diameter: 2, year: 2003, planet: 'Jupiter'}, 76 | {type: 'Moon', order: 'LI', name: '', desc: 'S/2010 J 1', diameter: 723, year: 2010, planet: 'Jupiter'}, 77 | {type: 'Moon', order: 'LII', name: '', desc: 'S/2010 J 2', diameter: 588, year: 2010, planet: 'Jupiter'}, 78 | {type: 'Moon', order: 'LIII', name: 'Dia', desc: 'S/2000 J 11', diameter: 4, year: 2000, planet: 'Jupiter'}, 79 | {type: 'Moon', order: 'LIV', name: '', desc: 'S/2016 J 1', year: 2016, planet: 'Jupiter'}, 80 | {type: 'Moon', order: 'LV', name: '', desc: 'S/2003 J 18', diameter: 2, year: 2003, planet: 'Jupiter'}, 81 | {type: 'Moon', order: 'LVI', name: '', desc: 'S/2011 J 2', year: 2011, planet: 'Jupiter'}, 82 | {type: 'Moon', order: 'LVII', name: '', desc: 'S/2003 J 5', diameter: 4, year: 2003, planet: 'Jupiter'}, 83 | {type: 'Moon', order: 'LVIII', name: '', desc: 'S/2003 J 15', diameter: 2, year: 2003, planet: 'Jupiter'}, 84 | {type: 'Moon', order: 'LIX', name: '', desc: 'S/2017 J 1', year: 2017, planet: 'Jupiter'}, 85 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 2', diameter: 2, year: 2003, planet: 'Jupiter'}, 86 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 3', diameter: 2, year: 2003, planet: 'Jupiter'}, 87 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 4', diameter: 2, year: 2003, planet: 'Jupiter'}, 88 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 9', diameter: 1, year: 2003, planet: 'Jupiter'}, 89 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 10', diameter: 2, year: 2003, planet: 'Jupiter'}, 90 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 12', diameter: 1, year: 2003, planet: 'Jupiter'}, 91 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 16', diameter: 2, year: 2003, planet: 'Jupiter'}, 92 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 19', diameter: 2, year: 2003, planet: 'Jupiter'}, 93 | {type: 'Moon', order: '', name: '', desc: 'S/2003 J 23', diameter: 2, year: 2003, planet: 'Jupiter'}, 94 | {type: 'Moon', order: '', name: '', desc: 'S/2011 J', diameter: 163, year: 2011, planet: 'Jupiter'}, 95 | 96 | {type: 'Moon', order: 'I', name: 'Mimas', desc: '', diameter: 396.4, year: 1789, planet: 'Saturn'}, 97 | {type: 'Moon', order: 'II', name: 'Enceladus', desc: '', diameter: 504.2, year: 1789, planet: 'Saturn'}, 98 | {type: 'Moon', order: 'III', name: 'Tethys', desc: '', diameter: 1066, year: 1684, planet: 'Saturn'}, 99 | {type: 'Moon', order: 'IV', name: 'Dione', desc: '', diameter: 1123.4, year: 1684, planet: 'Saturn'}, 100 | {type: 'Moon', order: 'V', name: 'Rhea', desc: '', diameter: 1529, year: 1672, planet: 'Saturn'}, 101 | {type: 'Moon', order: 'VI', name: 'Titan', desc: '', diameter: 5150, year: 1655, planet: 'Saturn'}, 102 | {type: 'Moon', order: 'VII', name: 'Hyperion', desc: '', diameter: 266, year: 1848, planet: 'Saturn'}, 103 | {type: 'Moon', order: 'VIII', name: 'Iapetus', desc: '', diameter: 1436, year: 1671, planet: 'Saturn'}, 104 | {type: 'Moon', order: 'IX', name: 'Phoebe', desc: '', diameter: 240, year: 1899, planet: 'Saturn'}, 105 | {type: 'Moon', order: 'X', name: 'Janus', desc: 'S/1980 S 1', diameter: 178, year: 1966, planet: 'Saturn'}, 106 | {type: 'Moon', order: 'XI', name: 'Epimetheus', desc: 'S/1980 S 3', diameter: 119, year: 1980, planet: 'Saturn'}, 107 | {type: 'Moon', order: 'XII', name: 'Helene', desc: 'S/1980 S 6', diameter: 35.2, year: 1980, planet: 'Saturn'}, 108 | {type: 'Moon', order: 'XIII', name: 'Telesto', desc: 'S/1980 S 13', diameter: 24.8, year: 1980, planet: 'Saturn'}, 109 | {type: 'Moon', order: 'XIV', name: 'Calypso', desc: 'S/1980 S 25', diameter: 21.4, year: 1980, planet: 'Saturn'}, 110 | {type: 'Moon', order: 'XV', name: 'Atlas', desc: 'S/1980 S 28', diameter: 32, year: 1980, planet: 'Saturn'}, 111 | {type: 'Moon', order: 'XVI', name: 'Prometheus', desc: 'S/1980 S 27', diameter: 100, year: 1980, planet: 'Saturn'}, 112 | {type: 'Moon', order: 'XVII', name: 'Pandora', desc: 'S/1980 S 26', diameter: 84, year: 1980, planet: 'Saturn'}, 113 | {type: 'Moon', order: 'XVIII', name: 'Pan', desc: 'S/1981 S 13', diameter: 20, year: 1990, planet: 'Saturn'}, 114 | {type: 'Moon', order: 'XIX', name: 'Ymir', desc: 'S/2000 S 1', diameter: 18, year: 2000, planet: 'Saturn'}, 115 | {type: 'Moon', order: 'XX', name: 'Paaliaq', desc: 'S/2000 S 2', diameter: 22, year: 2000, planet: 'Saturn'}, 116 | {type: 'Moon', order: 'XXI', name: 'Tarvos', desc: 'S/2000 S 4', diameter: 15, year: 2000, planet: 'Saturn'}, 117 | {type: 'Moon', order: 'XXII', name: 'Ijiraq', desc: 'S/2000 S 6', diameter: 12, year: 2000, planet: 'Saturn'}, 118 | {type: 'Moon', order: 'XXIII', name: 'Suttungr', desc: 'S/2000 S 12', diameter: 7, year: 2000, planet: 'Saturn'}, 119 | {type: 'Moon', order: 'XXIV', name: 'Kiviuq', desc: 'S/2000 S 5', diameter: 16, year: 2000, planet: 'Saturn'}, 120 | {type: 'Moon', order: 'XXV', name: 'Mundilfari', desc: 'S/2000 S 9', diameter: 7, year: 2000, planet: 'Saturn'}, 121 | {type: 'Moon', order: 'XXVI', name: 'Albiorix', desc: 'S/2000 S 11', diameter: 32, year: 2000, planet: 'Saturn'}, 122 | {type: 'Moon', order: 'XXVII', name: 'Skathi', desc: 'S/2000 S 8', diameter: 8, year: 2000, planet: 'Saturn'}, 123 | {type: 'Moon', order: 'XXVIII', name: 'Erriapus', desc: 'S/2000 S 10', diameter: 10, year: 2000, planet: 'Saturn'}, 124 | {type: 'Moon', order: 'XXIX', name: 'Siarnaq', desc: 'S/2000 S 3', diameter: 40, year: 2000, planet: 'Saturn'}, 125 | {type: 'Moon', order: 'XXX', name: 'Thrymr', desc: 'S/2000 S 7', diameter: 7, year: 2000, planet: 'Saturn'}, 126 | {type: 'Moon', order: 'XXXI', name: 'Narvi', desc: 'S/2003 S 1', diameter: 7, year: 2003, planet: 'Saturn'}, 127 | {type: 'Moon', order: 'XXXII', name: 'Methone', desc: 'S/2004 S 1', diameter: 3, year: 2004, planet: 'Saturn'}, 128 | {type: 'Moon', order: 'XXXIII', name: 'Pallene', desc: 'S/2004 S 2', diameter: 4, year: 2004, planet: 'Saturn'}, 129 | {type: 'Moon', order: 'XXXIV', name: 'Polydeuces', desc: 'S/2004 S 5', diameter: 4, year: 2004, planet: 'Saturn'}, 130 | {type: 'Moon', order: 'XXXV', name: 'Daphnis', desc: 'S/2005 S 1', diameter: 7, year: 2005, planet: 'Saturn'}, 131 | {type: 'Moon', order: 'XXXVI', name: 'Aegir', desc: 'S/2004 S 10', diameter: 6, year: 2004, planet: 'Saturn'}, 132 | {type: 'Moon', order: 'XXXVII', name: 'Bebhionn', desc: 'S/2004 S 11', diameter: 6, year: 2004, planet: 'Saturn'}, 133 | {type: 'Moon', order: 'XXXVIII', name: 'Bergelmir', desc: 'S/2004 S 15', diameter: 6, year: 2004, planet: 'Saturn'}, 134 | {type: 'Moon', order: 'XXXIX', name: 'Bestla', desc: 'S/2004 S 18', diameter: 7, year: 2004, planet: 'Saturn'}, 135 | {type: 'Moon', order: 'XL', name: 'Farbauti', desc: 'S/2004 S 9', diameter: 5, year: 2004, planet: 'Saturn'}, 136 | {type: 'Moon', order: 'XLI', name: 'Fenrir', desc: 'S/2004 S 16', diameter: 4, year: 2004, planet: 'Saturn'}, 137 | {type: 'Moon', order: 'XLII', name: 'Fornjot', desc: 'S/2004 S 8', diameter: 6, year: 2004, planet: 'Saturn'}, 138 | {type: 'Moon', order: 'XLIII', name: 'Hati', desc: 'S/2004 S 14', diameter: 6, year: 2004, planet: 'Saturn'}, 139 | {type: 'Moon', order: 'XLIV', name: 'Hyrrokkin', desc: 'S/2004 S 19', diameter: 8, year: 2004, planet: 'Saturn'}, 140 | {type: 'Moon', order: 'XLV', name: 'Kari', desc: 'S/2006 S 2', diameter: 7, year: 2006, planet: 'Saturn'}, 141 | {type: 'Moon', order: 'XLVI', name: 'Loge', desc: 'S/2006 S 5', diameter: 6, year: 2006, planet: 'Saturn'}, 142 | {type: 'Moon', order: 'XLVII', name: 'Skoll', desc: 'S/2006 S 8', diameter: 6, year: 2006, planet: 'Saturn'}, 143 | {type: 'Moon', order: 'XLVIII', name: 'Surtur', desc: 'S/2006 S 7', diameter: 6, year: 2006, planet: 'Saturn'}, 144 | {type: 'Moon', order: 'XLIX', name: 'Anthe', desc: 'S/2007 S 4', diameter: 2, year: 2007, planet: 'Saturn'}, 145 | {type: 'Moon', order: 'L', name: 'Jarnsaxa', desc: 'S/2006 S 6', diameter: 6, year: 2006, planet: 'Saturn'}, 146 | {type: 'Moon', order: 'LI', name: 'Greip', desc: 'S/2006 S 4', diameter: 6, year: 2006, planet: 'Saturn'}, 147 | {type: 'Moon', order: 'LII', name: 'Tarqeq', desc: 'S/2007 S 1', diameter: 7, year: 2007, planet: 'Saturn'}, 148 | {type: 'Moon', order: 'LIII', name: 'Aegaeon', desc: 'S/2008 S 1', diameter: 0.5, year: 2008, planet: 'Saturn'}, 149 | {type: 'Moon', order: '', name: '', desc: 'S/2004 S 7', diameter: 6, year: 2004, planet: 'Saturn'}, 150 | {type: 'Moon', order: '', name: '', desc: 'S/2004 S 12', diameter: 5, year: 2004, planet: 'Saturn'}, 151 | {type: 'Moon', order: '', name: '', desc: 'S/2004 S 13', diameter: 6, year: 2004, planet: 'Saturn'}, 152 | {type: 'Moon', order: '', name: '', desc: 'S/2004 S 17', diameter: 4, year: 2004, planet: 'Saturn'}, 153 | {type: 'Moon', order: '', name: '', desc: 'S/2006 S 1', diameter: 6, year: 2006, planet: 'Saturn'}, 154 | {type: 'Moon', order: '', name: '', desc: 'S/2006 S 3', diameter: 6, year: 2006, planet: 'Saturn'}, 155 | {type: 'Moon', order: '', name: '', desc: 'S/2007 S 2', diameter: 6, year: 2007, planet: 'Saturn'}, 156 | {type: 'Moon', order: '', name: '', desc: 'S/2007 S 3', diameter: 5, year: 2007, planet: 'Saturn'}, 157 | {type: 'Moon', order: '', name: '', desc: 'S/2009 S 1', diameter: 0.3, year: '2009', planet: 'Saturn'}, 158 | 159 | {type: 'Moon', order: 'I', name: 'Ariel', desc: '', diameter: 1158, year: 1851, planet: 'Uranus'}, 160 | {type: 'Moon', order: 'II', name: 'Umbriel', desc: '', diameter: 1169, year: 1851, planet: 'Uranus'}, 161 | {type: 'Moon', order: 'III', name: 'Titania', desc: '', diameter: 1578, year: 1787, planet: 'Uranus'}, 162 | {type: 'Moon', order: 'IV', name: 'Oberon', desc: '', diameter: 1523, year: 1787, planet: 'Uranus'}, 163 | {type: 'Moon', order: 'V', name: 'Miranda', desc: '', diameter: 472, year: 1948, planet: 'Uranus'}, 164 | {type: 'Moon', order: 'VI', name: 'Cordelia', desc: 'S/1986 U 7', diameter: 40, year: 1986, planet: 'Uranus'}, 165 | {type: 'Moon', order: 'VII', name: 'Ophelia', desc: 'S/1986 U 8', diameter: 43, year: 1986, planet: 'Uranus'}, 166 | {type: 'Moon', order: 'VIII', name: 'Bianca', desc: 'S/1986 U 9', diameter: 51, year: 1986, planet: 'Uranus'}, 167 | {type: 'Moon', order: 'IX', name: 'Cressida', desc: 'S/1986 U 3', diameter: 80, year: 1986, planet: 'Uranus'}, 168 | {type: 'Moon', order: 'X', name: 'Desdemona', desc: 'S/1986 U 6', diameter: 64, year: 1986, planet: 'Uranus'}, 169 | {type: 'Moon', order: 'XI', name: 'Juliet', desc: 'S/1986 U 2', diameter: 94, year: 1986, planet: 'Uranus'}, 170 | {type: 'Moon', order: 'XII', name: 'Portia', desc: 'S/1986 U 1', diameter: 135, year: 1986, planet: 'Uranus'}, 171 | {type: 'Moon', order: 'XIII', name: 'Rosalind', desc: 'S/1986 U 4', diameter: 72, year: 1986, planet: 'Uranus'}, 172 | {type: 'Moon', order: 'XIV', name: 'Belinda', desc: 'S/1986 U 5', diameter: 81, year: 1986, planet: 'Uranus'}, 173 | {type: 'Moon', order: 'XV', name: 'Puck', desc: 'S/1985 U 1', diameter: 162, year: 1985, planet: 'Uranus'}, 174 | {type: 'Moon', order: 'XVI', name: 'Caliban', desc: 'S/1997 U 1', diameter: 72, year: 1997, planet: 'Uranus'}, 175 | {type: 'Moon', order: 'XVII', name: 'Sycorax', desc: 'S/1997 U 2', diameter: 150, year: 1997, planet: 'Uranus'}, 176 | {type: 'Moon', order: 'XVIII', name: 'Prospero', desc: 'S/1999 U 3', diameter: 50, year: 1999, planet: 'Uranus'}, 177 | {type: 'Moon', order: 'XIX', name: 'Setebos', desc: 'S/1999 U 1', diameter: 47, year: 1999, planet: 'Uranus'}, 178 | {type: 'Moon', order: 'XX', name: 'Stephano', desc: 'S/1999 U 2', diameter: 32, year: 1999, planet: 'Uranus'}, 179 | {type: 'Moon', order: 'XXI', name: 'Trinculo', desc: 'S/2001 U 1', diameter: 18, year: 2001, planet: 'Uranus'}, 180 | {type: 'Moon', order: 'XXII', name: 'Francisco', desc: 'S/2001 U 3', diameter: 22, year: 2001, planet: 'Uranus'}, 181 | {type: 'Moon', order: 'XXIII', name: 'Margaret', desc: 'S/2003 U 3', diameter: 20, year: 2003, planet: 'Uranus'}, 182 | {type: 'Moon', order: 'XXIV', name: 'Ferdinand', desc: 'S/2001 U 2', diameter: 21, year: 2001, planet: 'Uranus'}, 183 | {type: 'Moon', order: 'XXV', name: 'Perdita', desc: 'S/1986 U 10', diameter: 30, year: 1986, planet: 'Uranus'}, 184 | {type: 'Moon', order: 'XXVI', name: 'Mab', desc: 'S/2003 U 1', diameter: 16, year: 2003, planet: 'Uranus'}, 185 | {type: 'Moon', order: 'XXVII', name: 'Cupid', desc: 'S/2003 U 2', diameter: 18, year: 2003, planet: 'Uranus'}, 186 | 187 | {type: 'Moon', order: 'I', name: 'Triton', desc: '', diameter: 2707, year: 1846, planet: 'Neptun'}, 188 | {type: 'Moon', order: 'II', name: 'Nereid', desc: '', diameter: 340, year: 1949, planet: 'Neptun'}, 189 | {type: 'Moon', order: 'III', name: 'Naiad', desc: 'S/1989 N 6', diameter: 67, year: 1989, planet: 'Neptun'}, 190 | {type: 'Moon', order: 'IV', name: 'Thalassa', desc: 'S/1989 N 5', diameter: 81, year: 1989, planet: 'Neptun'}, 191 | {type: 'Moon', order: 'V', name: 'Despina', desc: 'S/1989 N 3', diameter: 150, year: 1989, planet: 'Neptun'}, 192 | {type: 'Moon', order: 'VI', name: 'Galatea', desc: 'S/1989 N 4', diameter: 175, year: 1989, planet: 'Neptun'}, 193 | {type: 'Moon', order: 'VII', name: 'Larissa', desc: 'S/1989 N 2', diameter: 195, year: 1981, planet: 'Neptun'}, 194 | {type: 'Moon', order: 'VIII', name: 'Proteus', desc: 'S/1989 N 1', diameter: 420, year: 1989, planet: 'Neptun'}, 195 | {type: 'Moon', order: 'IX', name: 'Halimede', desc: 'S/2002 N 1', diameter: 48, year: 2002, planet: 'Neptun'}, 196 | {type: 'Moon', order: 'X', name: 'Psamathe', desc: 'S/2003 N 1', diameter: 38, year: 2003, planet: 'Neptun'}, 197 | {type: 'Moon', order: 'XI', name: 'Sao', desc: 'S/2002 N 2', diameter: 44, year: 2002, planet: 'Neptun'}, 198 | {type: 'Moon', order: 'XII', name: 'Laomedeia', desc: 'S/2002 N 3', diameter: 42, year: 2002, planet: 'Neptun'}, 199 | {type: 'Moon', order: 'XIII', name: 'Neso', desc: 'S/2002 N 4', diameter: 60, year: 2002, planet: 'Neptun'}, 200 | {type: 'Moon', order: 'XIV', name: '', desc: 'S/2004 N 1', diameter: 18, year: 2013, planet: 'Neptun'} 201 | 202 | ]; 203 | 204 | const queue = []; 205 | let count = 0; 206 | objects.forEach(obj => { 207 | const id = 'test/' + ('0000' + (count++)).slice(-5); 208 | queue.push({id, payload: JSON.stringify(obj)}); 209 | }); 210 | 211 | function publish({id, payload}, callback) { 212 | mqtt.publish('db/set/' + id, payload, {qos: 2}, err => { 213 | if (err) { 214 | console.error('error', id, err); 215 | } else { 216 | console.log('published', id); 217 | } 218 | setTimeout(callback, 20); 219 | }); 220 | } 221 | 222 | async.mapSeries(queue, publish, () => { 223 | console.log('done'); 224 | mqtt.end(); 225 | }); 226 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, should */ 2 | 3 | const debug = false; 4 | 5 | require('should'); 6 | 7 | const path = require('path'); 8 | const cp = require('child_process'); 9 | const streamSplitter = require('stream-splitter'); 10 | const request = require('request'); 11 | const io = require('socket.io-client'); 12 | 13 | if (process.platform === 'darwin') { 14 | cp.spawn('/usr/local/bin/brew', ['services', 'start', 'mosquitto']); 15 | } 16 | 17 | const Mqtt = require('mqtt'); 18 | const mqtt = Mqtt.connect('mqtt://127.0.0.1'); 19 | 20 | const dbId = 'db' + Math.random().toString(16).substr(2, 8); 21 | 22 | const cmd = path.join(__dirname, '..', 'index.js'); 23 | const cmdArgs = ['-v', 'debug', '-n', dbId]; 24 | 25 | let proc; 26 | let procPipeOut; 27 | let procPipeErr; 28 | let subIndex = 0; 29 | const procSubscriptions = {}; 30 | const procBuffer = []; 31 | 32 | function procSubscribe(rx, cb) { 33 | subIndex += 1; 34 | procSubscriptions[subIndex] = {rx, cb}; 35 | matchSubscriptions(); 36 | return subIndex; 37 | } 38 | 39 | function procUnsubscribe(subIndex) { 40 | delete procSubscriptions[subIndex]; 41 | } 42 | 43 | function matchSubscriptions(data) { 44 | let subs = procSubscriptions; 45 | let buf = procBuffer; 46 | if (data) { 47 | buf.push(data); 48 | } 49 | buf.forEach((line, index) => { 50 | Object.keys(subs).forEach(key => { 51 | const sub = subs[key]; 52 | if (line.match(sub.rx)) { 53 | sub.cb(line); 54 | delete subs[key]; 55 | buf.splice(index, 1); 56 | } 57 | }); 58 | }); 59 | } 60 | 61 | function startDb() { 62 | proc = cp.spawn(cmd, cmdArgs); 63 | procPipeOut = proc.stdout.pipe(streamSplitter('\n')); 64 | procPipeErr = proc.stderr.pipe(streamSplitter('\n')); 65 | 66 | procPipeOut.on('token', data => { 67 | if (debug) console.log('db', data.toString()); 68 | matchSubscriptions(data.toString()); 69 | }); 70 | procPipeErr.on('token', data => { 71 | if (debug) console.log('db', data.toString()); 72 | matchSubscriptions(data.toString()); 73 | }); 74 | } 75 | 76 | const mqttSubscriptions = []; 77 | 78 | mqtt.on('message', (topic, payload, options) => { 79 | payload = payload.toString(); 80 | mqttSubscriptions.forEach((sub, index) => { 81 | if (sub.topic === topic) { 82 | const callback = sub.callback; 83 | if (!sub.retain && options.retain) { 84 | return; 85 | } 86 | if (sub.once) { 87 | mqttSubscriptions.splice(index, 1); 88 | if (debug) console.log('mqtt unsubscribe', sub.topic); 89 | mqtt.unsubscribe(sub.topic); 90 | } 91 | if (payload !== '') { 92 | payload = JSON.parse(payload) 93 | } 94 | callback(payload); 95 | } 96 | }); 97 | }); 98 | 99 | function mqttSubscribeOnce(topic, callback) { 100 | mqttSubscriptions.push({topic, callback, once: true}); 101 | if (debug) console.log('mqtt subscribe', topic); 102 | mqtt.subscribe(topic); 103 | } 104 | 105 | function mqttSubscribeOnceRetain(topic, callback) { 106 | mqttSubscriptions.push({topic, callback, once: true, retain: true}); 107 | if (debug) console.log('mqtt subscribe', topic); 108 | mqtt.subscribe(topic); 109 | } 110 | 111 | 112 | 113 | describe('start daemons', () => { 114 | it('mqttDB should start without error', function (done) { 115 | this.timeout(20000); 116 | procSubscribe(/mqttdb [0-9.]+ starting/, data => { 117 | done(); 118 | }); 119 | startDb(); 120 | }); 121 | it('connect to the mqtt broker', function (done) { 122 | this.timeout(20000); 123 | procSubscribe(/mqtt connected/, data => { 124 | done(); 125 | }); 126 | }); 127 | it('spawn workers', function (done) { 128 | this.timeout(20000); 129 | procSubscribe(/all workers ready/, data => { 130 | done(); 131 | }); 132 | }); 133 | it('complete init', function (done) { 134 | this.timeout(20000); 135 | procSubscribe(/init complete/, data => { 136 | done(); 137 | }); 138 | }); 139 | it('subscribe to set', function (done) { 140 | procSubscribe(/mqtt subscribe db[0-9a-f]+\/set\/#/, () => { 141 | done(); 142 | }); 143 | }); 144 | it('subscribe to extend', function (done) { 145 | procSubscribe(/mqtt subscribe db[0-9a-f]+\/extend\/#/, () => { 146 | done(); 147 | }); 148 | }); 149 | it('subscribe to prop', function (done) { 150 | procSubscribe(/mqtt subscribe db[0-9a-f]+\/prop\/#/, () => { 151 | done(); 152 | }); 153 | }); 154 | it('subscribe to query', function (done) { 155 | procSubscribe(/mqtt subscribe db[0-9a-f]+\/query\/#/, () => { 156 | done(); 157 | }); 158 | }); 159 | }); 160 | 161 | describe('document test1', () => { 162 | it('create a document', function (done) { 163 | this.timeout(20000); 164 | const doc = {type: 'test'}; 165 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 166 | should.deepEqual({type: 'test', _id: 'test1', _rev: 0}, payload); 167 | done(); 168 | }); 169 | setTimeout(() => { 170 | mqtt.publish(dbId + '/set/test1', JSON.stringify(doc)); 171 | }, 500); 172 | }); 173 | it('get a document', function (done) { 174 | this.timeout(20000); 175 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 176 | should.deepEqual({type: 'test', _id: 'test1', _rev: 0}, payload); 177 | done(); 178 | }); 179 | setTimeout(() => { 180 | mqtt.publish(dbId + '/get/doc/test1'); 181 | }, 500); 182 | }); 183 | it('get a non-existing document', function (done) { 184 | this.timeout(20000); 185 | mqttSubscribeOnce(dbId + '/doc/test0', payload => { 186 | payload.should.equal(''); 187 | done(); 188 | }); 189 | setTimeout(() => { 190 | mqtt.publish(dbId + '/get/doc/test0'); 191 | }, 500); 192 | }); 193 | it('log error on unknown get type', function (done) { 194 | this.timeout(20000); 195 | procSubscribe(/unknown get type/, () => { 196 | done(); 197 | }); 198 | setTimeout(() => { 199 | mqtt.publish(dbId + '/get/foo/bar'); 200 | }, 500); 201 | }); 202 | it('log error on malformed payload', function (done) { 203 | this.timeout(20000); 204 | procSubscribe(/malformed payload/, () => { 205 | done(); 206 | }); 207 | setTimeout(() => { 208 | mqtt.publish(dbId + '/set/test5', '}{'); 209 | }, 500); 210 | }); 211 | it('log error on malformed topic', function (done) { 212 | this.timeout(20000); 213 | procSubscribe(/malformed topic/, () => { 214 | done(); 215 | }); 216 | setTimeout(() => { 217 | mqtt.publish(dbId + '/set/', '{}'); 218 | }, 500); 219 | }); 220 | it('extend a document', function (done) { 221 | this.timeout(20000); 222 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 223 | should.deepEqual({type: 'test', _id: 'test1', _rev: 1, muh: 'kuh'}, payload); 224 | done(); 225 | }); 226 | setTimeout(() => { 227 | mqtt.publish(dbId + '/extend/test1', JSON.stringify({muh: 'kuh'})); 228 | }, 500); 229 | }); 230 | it('extend a non-existing document', function (done) { 231 | this.timeout(20000); 232 | mqttSubscribeOnce(dbId + '/doc/doc6', payload => { 233 | should.deepEqual({_id: 'doc6', _rev: 0, muh: 'kuh'}, payload); 234 | done(); 235 | }); 236 | setTimeout(() => { 237 | mqtt.publish(dbId + '/extend/doc6', JSON.stringify({muh: 'kuh'})); 238 | }, 500); 239 | }); 240 | it('do nothing on extending existing document', function (done) { 241 | mqttSubscribeOnceRetain(dbId + '/doc/doc6', payload => { 242 | should.deepEqual({_id: 'doc6', _rev: 0, muh: 'kuh'}, payload); 243 | done(); 244 | }); 245 | setTimeout(() => { 246 | mqtt.publish(dbId + '/extend/doc6', JSON.stringify({muh: 'kuh'})); 247 | }, 500); 248 | }); 249 | it('do nothing on setting existing document', function (done) { 250 | mqttSubscribeOnceRetain(dbId + '/doc/doc6', payload => { 251 | should.deepEqual({_id: 'doc6', _rev: 0, muh: 'kuh'}, payload); 252 | done(); 253 | }); 254 | setTimeout(() => { 255 | mqtt.publish(dbId + '/set/doc6', JSON.stringify({muh: 'kuh'})); 256 | }, 500); 257 | }); 258 | it('set a property', function (done) { 259 | this.timeout(20000); 260 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 261 | should.deepEqual({type: 'test', _id: 'test1', _rev: 2, muh: 'kuh', bla: 'blubb'}, payload); 262 | done(); 263 | }); 264 | setTimeout(() => { 265 | mqtt.publish(dbId + '/prop/test1', JSON.stringify({method: 'set', prop: 'bla', val: 'blubb'})); 266 | }, 500); 267 | }); 268 | it('do nothing on setting existing property', function (done) { 269 | mqttSubscribeOnceRetain(dbId + '/doc/doc6', payload => { 270 | should.deepEqual({_id: 'doc6', _rev: 0, muh: 'kuh'}, payload); 271 | done(); 272 | }); 273 | setTimeout(() => { 274 | mqtt.publish(dbId + '/prop/doc6', JSON.stringify({method: 'set', prop: 'muh', val: 'kuh'})); 275 | }, 500); 276 | }); 277 | it('overwrite a property', function (done) { 278 | this.timeout(20000); 279 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 280 | should.deepEqual({type: 'test', _id: 'test1', _rev: 3, muh: 'kuh', bla: 'bla'}, payload); 281 | done(); 282 | }); 283 | setTimeout(() => { 284 | mqtt.publish(dbId + '/prop/test1', JSON.stringify({method: 'set', prop: 'bla', val: 'bla'})); 285 | }, 500); 286 | }); 287 | it('create a property', function (done) { 288 | this.timeout(20000); 289 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 290 | should.deepEqual({type: 'test', _id: 'test1', _rev: 4, muh: 'kuh', bla: 'bla', foo: 'bar'}, payload); 291 | done(); 292 | }); 293 | setTimeout(() => { 294 | mqtt.publish(dbId + '/prop/test1', JSON.stringify({method: 'create', prop: 'foo', val: 'bar'})); 295 | }, 500); 296 | }); 297 | 298 | it('should not overwrite a property', function (done) { 299 | this.timeout(20000); 300 | 301 | setTimeout(() => { 302 | mqtt.publish(dbId + '/prop/test1', JSON.stringify({method: 'create', prop: 'muh', val: 'no!'})); 303 | }, 500); 304 | 305 | setTimeout(() => { 306 | mqttSubscribeOnceRetain(dbId + '/doc/test1', payload => { 307 | should.deepEqual({type: 'test', _id: 'test1', _rev: 4, muh: 'kuh', bla: 'bla', foo: 'bar'}, payload); 308 | done(); 309 | }); 310 | }, 2000); 311 | }); 312 | 313 | it('should delete a property', function (done) { 314 | this.timeout(20000); 315 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 316 | should.deepEqual({type: 'test', _id: 'test1', _rev: 5, muh: 'kuh', bla: 'bla'}, payload); 317 | done(); 318 | }); 319 | setTimeout(() => { 320 | mqtt.publish(dbId + '/prop/test1', JSON.stringify({method: 'del', prop: 'foo'})); 321 | }, 500); 322 | }); 323 | it('delete a document', function (done) { 324 | this.timeout(20000); 325 | mqttSubscribeOnce(dbId + '/doc/test1', payload => { 326 | payload.should.equal(''); 327 | done(); 328 | }); 329 | setTimeout(() => { 330 | mqtt.publish(dbId + '/set/test1', ''); 331 | }, 500); 332 | }); 333 | it('delete a document', function (done) { 334 | this.timeout(20000); 335 | mqttSubscribeOnce(dbId + '/doc/doc6', payload => { 336 | payload.should.equal(''); 337 | done(); 338 | }); 339 | setTimeout(() => { 340 | mqtt.publish(dbId + '/set/doc6', ''); 341 | }, 500); 342 | }); 343 | }); 344 | 345 | describe('view test1', () => { 346 | it('create a view', function (done) { 347 | this.timeout(20000); 348 | mqttSubscribeOnce(dbId + '/view/test1', payload => { 349 | should.deepEqual({ _id: 'test1', _rev: 0, result: [], length: 0 }, payload); 350 | done(); 351 | }); 352 | mqtt.publish(dbId + '/query/test1', JSON.stringify({filter: '#', map: 'if (this.type === "muh") emit(this._id)', reduce: 'return result'})); 353 | }); 354 | it('get a view', function (done) { 355 | this.timeout(20000); 356 | mqttSubscribeOnce(dbId + '/view/test1', payload => { 357 | should.deepEqual({ _id: 'test1', _rev: 0, result: [], length: 0 }, payload); 358 | done(); 359 | }); 360 | mqtt.publish(dbId + '/get/view/test1'); 361 | }); 362 | it('get a non-existing view', function (done) { 363 | this.timeout(20000); 364 | mqttSubscribeOnce(dbId + '/view/test0', payload => { 365 | payload.should.equal(''); 366 | done(); 367 | }); 368 | mqtt.publish(dbId + '/get/view/test0'); 369 | }); 370 | it('publish the new view after adding a document', function (done) { 371 | this.timeout(20000); 372 | mqttSubscribeOnce(dbId + '/view/test1', payload => { 373 | should.deepEqual({ _id: 'test1', _rev: 1, result: [ 'doc1' ], length: 1 }, payload); 374 | done(); 375 | }); 376 | setTimeout(() => { 377 | mqtt.publish(dbId + '/set/doc1', JSON.stringify({type: 'muh'})); 378 | }, 2000); 379 | }); 380 | it('not change the view after adding a document not matching the query', function (done) { 381 | this.timeout(20000); 382 | mqtt.publish(dbId + '/set/doc2', JSON.stringify({type: 'foo'})); 383 | setTimeout(() => { 384 | mqttSubscribeOnceRetain(dbId + '/view/test1', payload => { 385 | should.deepEqual({ _id: 'test1', _rev: 1, result: [ 'doc1' ], length: 1 }, payload); 386 | done(); 387 | }); 388 | }, 2000); 389 | }); 390 | it('publish the new view after altering the query', function (done) { 391 | this.timeout(20000); 392 | mqttSubscribeOnce(dbId + '/view/test1', payload => { 393 | should.deepEqual({ _id: 'test1', _rev: 2, result: [ 'doc2' ], length: 1 }, payload); 394 | done(); 395 | }); 396 | mqtt.publish(dbId + '/query/test1', JSON.stringify({filter: '#', map: 'if (this.type === "foo") emit(this._id)', reduce: 'return result'})); 397 | }); 398 | it('queue view execution', function (done) { 399 | this.timeout(20000); 400 | for (let i = 3; i < 50; i++) { 401 | mqtt.publish(dbId + '/set/doc' + i, JSON.stringify({type: 'foo'})); 402 | } 403 | setTimeout(() => { 404 | mqttSubscribeOnceRetain(dbId + '/view/test1', payload => { 405 | delete payload._rev; 406 | payload.should.deepEqual({ 407 | _id: 'test1', 408 | result: [ 409 | 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', 'doc10', 'doc11', 'doc12', 410 | 'doc13', 'doc14', 'doc15', 'doc16', 'doc17', 'doc18', 'doc19', 'doc20', 'doc21', 'doc22', 411 | 'doc23', 'doc24', 'doc25', 'doc26', 'doc27', 'doc28', 'doc29', 'doc30', 'doc31', 'doc32', 412 | 'doc33', 'doc34', 'doc35', 'doc36', 'doc37', 'doc38', 'doc39', 'doc40', 'doc41', 'doc42', 413 | 'doc43', 'doc44', 'doc45', 'doc46', 'doc47', 'doc48', 'doc49' 414 | ], 415 | length: 48 416 | }); 417 | 418 | for (let i = 3; i < 50; i++) { 419 | mqtt.publish(dbId + '/set/doc' + i, ''); 420 | } 421 | setTimeout(() => { 422 | done(); 423 | }, 5000); 424 | }); 425 | }, 5000); 426 | 427 | }); 428 | it('delete the view', function (done) { 429 | this.timeout(20000); 430 | mqttSubscribeOnce(dbId + '/view/test1', payload => { 431 | payload.should.equal(''); 432 | done(); 433 | }); 434 | mqtt.publish(dbId + '/query/test1', ''); 435 | }); 436 | }); 437 | 438 | describe('view test2 script creation error', () => { 439 | it('publish an error', function (done) { 440 | this.timeout(20000); 441 | mqttSubscribeOnce(dbId + '/view/test2', payload => { 442 | should.deepEqual({ _id: 'test2', _rev: -1, error: 'script creation: Unexpected identifier'}, payload); 443 | done(); 444 | }); 445 | mqtt.publish(dbId + '/query/test2', JSON.stringify({map: 'ERROR (this.type === "muh") emit(this._id)'})); 446 | }); 447 | }); 448 | 449 | describe('view test3 script execution error', () => { 450 | it('publish an error', function (done) { 451 | this.timeout(20000); 452 | mqttSubscribeOnce(dbId + '/view/test3', payload => { 453 | should.deepEqual({ _id: 'test3', _rev: -1, error: 'script execution: Cannot read property \'type\' of undefined'}, payload); 454 | done(); 455 | }); 456 | mqtt.publish(dbId + '/query/test3', JSON.stringify({map: 'if (this.doesNotExist.type === "muh") emit(this._id)'})); 457 | }); 458 | }); 459 | 460 | describe('stop daemon', () => { 461 | it('stop mqttDB', function (done) { 462 | this.timeout(20000); 463 | proc.on('close', () => { 464 | done(); 465 | }); 466 | setTimeout(() => { 467 | proc.kill('SIGTERM'); 468 | }, 2000); 469 | }); 470 | }); 471 | 472 | describe('restart daemon', () => { 473 | it('mqttDB should start without error', function (done) { 474 | this.timeout(20000); 475 | procSubscribe(/mqttdb [0-9.]+ starting/, data => { 476 | done(); 477 | }); 478 | startDb(); 479 | }); 480 | it('load the previous database', function (done) { 481 | this.timeout(20000); 482 | procSubscribe(/database loaded/, data => { 483 | done(); 484 | }); 485 | }); 486 | it('publish 2 documents', function (done) { 487 | this.timeout(20000); 488 | procSubscribe(/published 2 docs/, data => { 489 | done(); 490 | }); 491 | }); 492 | }); 493 | 494 | describe('webserver', () => { 495 | it('response with http 200 on /', function (done) { 496 | this.timeout(20000); 497 | request('http://127.0.0.1:8092/', (err, res, body) => { 498 | if (res.statusCode) { 499 | done(); 500 | } 501 | }); 502 | }); 503 | }); 504 | 505 | describe('socket.io', () => { 506 | it('connect', function (done) { 507 | const client = io.connect('http://127.0.0.1:8092'); 508 | client.on('connect', () => { 509 | done(); 510 | client.disconnect(); 511 | }); 512 | }); 513 | it('receive objectIds on connect', function (done) { 514 | const client = io.connect('http://127.0.0.1:8092'); 515 | client.on('objectIds', (data) => { 516 | client.disconnect(); 517 | data.should.deepEqual(['doc1', 'doc2']); 518 | done(); 519 | }); 520 | }); 521 | it('receive viewIds on connect', function (done) { 522 | const client = io.connect('http://127.0.0.1:8092'); 523 | client.on('viewIds', (data) => { 524 | client.disconnect(); 525 | data.should.deepEqual(['test2', 'test3']); 526 | done(); 527 | }); 528 | }); 529 | it('get an object', function (done) { 530 | const client = io.connect('http://127.0.0.1:8092'); 531 | client.emit('getObject', 'doc1', data => { 532 | client.disconnect(); 533 | data.should.deepEqual({type: 'muh', _id: 'doc1', _rev: 0}); 534 | done(); 535 | }); 536 | }); 537 | it('create a view', function (done) { 538 | this.timeout(20000); 539 | const client = io.connect('http://127.0.0.1:8092'); 540 | mqttSubscribeOnce(dbId + '/view/test4', data => { 541 | data.should.deepEqual({_id: 'test4', _rev: 0, result: [ 'doc1', 'doc2' ], length: 2}); 542 | done(); 543 | }); 544 | client.emit('query', 'test4', {map: 'emit(this._id)'}, () => { 545 | client.disconnect(); 546 | }); 547 | }); 548 | it('get a view', function (done) { 549 | this.timeout(20000); 550 | const client = io.connect('http://127.0.0.1:8092'); 551 | setTimeout(() => { 552 | client.emit('getView', 'test4', data => { 553 | data.should.deepEqual({ 554 | id: 'test4', 555 | query: {map: 'emit(this._id)'}, 556 | view: {_id: 'test4', _rev: 0, result: [ 'doc1', 'doc2' ], length: 2} 557 | }); 558 | client.disconnect(); 559 | done(); 560 | }); 561 | }, 2000); 562 | }); 563 | it('create a document', function (done) { 564 | this.timeout(20000); 565 | const client = io.connect('http://127.0.0.1:8092'); 566 | mqttSubscribeOnce(dbId + '/doc/doc3', data => { 567 | data.should.deepEqual({_id: 'doc3', _rev: 0, foo: 'bar'}); 568 | done(); 569 | }); 570 | client.emit('set', 'doc3', {foo: 'bar'}, () => { 571 | client.disconnect(); 572 | }); 573 | }); 574 | it('respond with error on revision conflict', function (done) { 575 | this.timeout(20000); 576 | const client = io.connect('http://127.0.0.1:8092'); 577 | client.emit('set', 'doc3', {foo: 'bar', _rev: -1}, (data) => { 578 | data.should.equal('rev mismatch 0'); 579 | client.disconnect(); 580 | done(); 581 | }); 582 | }); 583 | it('delete a document', function (done) { 584 | this.timeout(20000); 585 | const client = io.connect('http://127.0.0.1:8092'); 586 | mqttSubscribeOnce(dbId + '/doc/doc3', data => { 587 | data.should.equal(''); 588 | done(); 589 | }); 590 | client.emit('del', 'doc3', () => { 591 | client.disconnect(); 592 | }); 593 | }); 594 | }); 595 | 596 | describe('mqtt connection', () => { 597 | it('log disconnection from broker', function (done) { 598 | this.timeout(20000); 599 | procSubscribe(/mqtt close/, () => { 600 | done(); 601 | }); 602 | if (process.platform === 'darwin') { 603 | cp.spawn('/usr/local/bin/brew', ['services', 'stop', 'mosquitto']); 604 | } else { 605 | cp.spawn('/usr/bin/sudo', ['/etc/init.d/mosquitto', 'stop']); 606 | } 607 | }); 608 | 609 | it('try to reconnect to the broker', function (done) { 610 | this.timeout(30000); 611 | procSubscribe(/mqtt reconnect/, () => { 612 | done(); 613 | }); 614 | }); 615 | 616 | it('log reconnection to broker', function (done) { 617 | this.timeout(20000); 618 | procSubscribe(/mqtt connected/, () => { 619 | done(); 620 | }); 621 | if (process.platform === 'darwin') { 622 | cp.spawn('/usr/local/bin/brew', ['services', 'start', 'mosquitto']); 623 | } else { 624 | cp.spawn('/usr/bin/sudo', ['/etc/init.d/mosquitto', 'start']); 625 | } 626 | }); 627 | }); 628 | 629 | 630 | 631 | describe('stop daemon', () => { 632 | it('stop mqttDB', function (done) { 633 | this.timeout(20000); 634 | proc.on('close', () => { 635 | done(); 636 | }); 637 | setTimeout(() => { 638 | proc.kill('SIGTERM'); 639 | }, 2000); 640 | }); 641 | }); 642 | -------------------------------------------------------------------------------- /ui/hgrip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hobbyquaker/mqttDB/58c8b1df452c68ad584f81494ca82256355bac54/ui/hgrip.png -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | mqttDB 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 46 | 47 | 66 | 67 | 87 | 88 | 89 | 97 |
98 |
99 |
100 | 101 |
102 |
103 | 104 |
105 |
106 |
107 | 108 | 109 | JSON 110 | _rev: 111 | 112 |
113 |
114 | 117 |
118 |
119 |
120 |
121 | 122 |
123 | 124 |
125 | 126 |
127 | 128 |
129 | 130 | 131 |
132 | 133 |
134 |
135 |
136 |
137 | 138 |
139 |
140 |
141 |
142 |
143 | 144 |
145 |
146 |
147 | 148 |
149 |
150 | 151 | 152 |
153 |
154 | 157 |
158 |
159 |
160 | 161 |
162 |
163 | 164 |
165 |
166 | 167 |
168 |
169 |
170 | 171 |
172 |
173 | 174 | -------------------------------------------------------------------------------- /ui/script.js: -------------------------------------------------------------------------------- 1 | /* global $, stringify, CodeMirror, document, io */ 2 | 3 | const $inputObjectId = $('#input-object-id'); 4 | 5 | const $indicatorObjectId = $('#indicator-object-id'); 6 | const $indicatorViewId = $('#indicator-view-id'); 7 | 8 | const $buttonSaveObject = $('#button-save-object'); 9 | const $buttonDelObject = $('#button-del-object'); 10 | const $buttonConfirmDelObject = $('#button-confirm-del-object'); 11 | 12 | const $inputViewId = $('#input-view-id'); 13 | const $inputViewFilter = $('#input-view-filter'); 14 | 15 | const $buttonSaveView = $('#button-save-view'); 16 | const $buttonDelView = $('#button-del-view'); 17 | const $buttonConfirmDelView = $('#button-confirm-del-view'); 18 | 19 | const $indicatorObjectSaved = $('#indicator-object-saved'); 20 | const $indicatorViewSaved = $('#indicator-view-saved'); 21 | 22 | const $objectRev = $('#object-rev'); 23 | const $objectRevServer = $('#object-rev-server'); 24 | const $objectRevEditor = $('#object-rev-editor'); 25 | 26 | const $objectConflictId = $('#object-conflict-id'); 27 | const $buttonSaveObjectForce = $('#button-save-object-force'); 28 | const $buttonReloadObject = $('#button-reload-object'); 29 | 30 | const $dialogConfirmConflict = $('#dialog-confirm-conflict'); 31 | const $dialogConfirmDelObject = $('#dialog-confirm-del-object'); 32 | const $dialogConfirmDelView = $('#dialog-confirm-del-view'); 33 | 34 | const $jsonlint = $('#jsonlint'); 35 | 36 | let objectIds = []; 37 | let currentObjectId; 38 | let currentViewId; 39 | 40 | const cmMap = CodeMirror.fromTextArea(document.querySelector('#input-view-map'), { 41 | autoCloseBrackets: true, 42 | lineNumbers: true, 43 | mode: 'javascript', 44 | foldGutter: true, 45 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] 46 | }); 47 | 48 | const cmReduce = CodeMirror.fromTextArea(document.querySelector('#input-view-reduce'), { 49 | autoCloseBrackets: true, 50 | lineNumbers: true, 51 | mode: 'javascript', 52 | foldGutter: true, 53 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] 54 | }); 55 | 56 | const cmResult = CodeMirror.fromTextArea(document.querySelector('#input-view-result'), { 57 | autoCloseBrackets: true, 58 | lineNumbers: true, 59 | mode: 'javascript', 60 | readOnly: 'nocursor', 61 | foldGutter: true, 62 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] 63 | }); 64 | 65 | const cmObject = CodeMirror.fromTextArea(document.querySelector('#input-object'), { 66 | autoCloseBrackets: true, 67 | lineNumbers: true, 68 | mode: 'javascript', 69 | foldGutter: true, 70 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] 71 | }); 72 | 73 | $('a[aria-controls="views"][data-toggle="tab"]').on('shown.bs.tab', () => { 74 | cmMap.refresh(); 75 | cmReduce.refresh(); 76 | cmResult.refresh(); 77 | }); 78 | 79 | const socket = io.connect(); 80 | 81 | socket.on('objectIds', data => { 82 | objectIds = data; 83 | 84 | if ($inputObjectId.typeahead) { 85 | $inputObjectId.typeahead('destroy'); 86 | } 87 | 88 | $inputObjectId.typeahead({ 89 | source: objectIds.sort(), 90 | minLength: 0, 91 | items: 12, 92 | afterSelect: () => { 93 | getObject($inputObjectId.val()); 94 | } 95 | }); 96 | 97 | $inputObjectId.keyup(e => { 98 | const id = $inputObjectId.val(); 99 | if (id !== currentObjectId) { 100 | $inputObjectId.css({color: 'grey'}); 101 | } 102 | if (e.which === 13) { 103 | if (id !== currentObjectId || !$indicatorObjectSaved.is(':visible')) { 104 | getObject(id); 105 | } 106 | } 107 | }); 108 | 109 | $inputObjectId.blur(() => { 110 | const id = $inputObjectId.val(); 111 | if (currentObjectId !== id) { 112 | getObject(id); 113 | } 114 | }); 115 | }); 116 | 117 | socket.on('viewIds', viewIds => { 118 | if ($inputViewId.typeahead) { 119 | $inputViewId.typeahead('destroy'); 120 | } 121 | 122 | $inputViewId.typeahead({ 123 | source: viewIds.sort(), 124 | minLength: 0, 125 | items: 12, 126 | afterSelect: () => { 127 | getView($inputViewId.val()); 128 | }}); 129 | 130 | $inputViewId.keyup(e => { 131 | const id = $inputViewId.val(); 132 | if (id !== currentViewId) { 133 | $inputViewId.css({color: 'grey'}); 134 | } 135 | if (e.which === 13) { 136 | if (id !== currentViewId || !$indicatorViewSaved.is(':visible')) { 137 | getView(id); 138 | } 139 | } 140 | }); 141 | 142 | $inputViewId.blur(() => { 143 | if (currentViewId !== $inputViewId.val()) { 144 | getView($inputViewId.val(), true); 145 | } 146 | }); 147 | }); 148 | 149 | socket.on('updateView', id => { 150 | if (id === currentViewId) { 151 | getView(id); 152 | } 153 | }); 154 | 155 | function clearObject() { 156 | $objectRev.html(''); 157 | cmObject.setValue(''); 158 | $buttonDelObject.attr('disabled', true); 159 | $buttonSaveObject.attr('disabled', true); 160 | $indicatorObjectSaved.hide(); 161 | jsonLint(); 162 | } 163 | 164 | function clearView() { 165 | // $viewsRev.html(''); 166 | cmMap.setValue(''); 167 | cmReduce.setValue(''); 168 | cmResult.setValue(''); 169 | $inputViewFilter.val(''); 170 | $buttonDelView.attr('disabled', true); 171 | $buttonSaveView.attr('disabled', true); 172 | $indicatorViewSaved.hide(); 173 | } 174 | 175 | function getObject(id) { 176 | currentObjectId = id; 177 | $inputObjectId.css({color: 'black'}); 178 | 179 | socket.emit('getObject', id, data => { 180 | if (data) { 181 | $objectRev.html(data._rev); 182 | cmObject.setValue(stringify(data)); 183 | $buttonDelObject.removeAttr('disabled'); 184 | $indicatorObjectSaved.show(); 185 | } else { 186 | clearObject(); 187 | } 188 | 189 | jsonLint(); 190 | }); 191 | } 192 | 193 | function getView(id) { 194 | currentViewId = id; 195 | 196 | $inputViewId.css({color: 'black'}); 197 | 198 | socket.emit('getView', id, data => { 199 | console.log('getView', id, data); 200 | if (data.query) { 201 | cmMap.setValue(data.query.map || ''); 202 | cmReduce.setValue(data.query.reduce || ''); 203 | $inputViewFilter.val(data.query.filter || '#'); 204 | // $viewsRev.html(data.rev); 205 | const obj = {}; 206 | if (data.view.error) { 207 | obj.error = data.view.error; 208 | obj._id = id; 209 | } else { 210 | obj.result = data.view.result; 211 | obj.length = data.view.result.length; 212 | obj._id = id; 213 | obj._rev = data.view._rev; 214 | } 215 | 216 | cmResult.setValue(stringify(obj)); 217 | $('.object-link').click(function () { 218 | console.log($(this).data('object')); 219 | const id = JSON.parse('"' + $(this).data('object') + '"'); 220 | console.log(id); 221 | $inputObjectId.val(id); 222 | getObject(id); 223 | $('#tablist a[href="#tab-objects"]').tab('show'); 224 | }); 225 | $indicatorViewSaved.show(); 226 | $buttonDelView.removeAttr('disabled'); 227 | $buttonSaveView.removeAttr('disabled'); 228 | } else { 229 | clearView(); 230 | if (id) { 231 | $buttonSaveView.removeAttr('disabled'); 232 | } 233 | } 234 | }); 235 | } 236 | 237 | function jsonLint() { 238 | try { 239 | JSON.parse(cmObject.getValue()); 240 | $jsonlint.html(''); 241 | $buttonSaveObject.removeAttr('disabled'); 242 | } catch (err) { 243 | $jsonlint.html(''); 244 | $buttonSaveObject.attr('disabled', true); 245 | } 246 | } 247 | 248 | $buttonSaveObject.attr('disabled', true); 249 | $buttonDelObject.attr('disabled', true); 250 | $indicatorObjectSaved.hide(); 251 | 252 | $buttonSaveView.attr('disabled', true); 253 | $buttonDelView.attr('disabled', true); 254 | $indicatorViewSaved.hide(); 255 | 256 | cmObject.on('change', () => { 257 | $indicatorObjectSaved.hide(); 258 | jsonLint(); 259 | }); 260 | 261 | $inputViewFilter.keyup(() => { 262 | $indicatorViewSaved.hide(); 263 | }); 264 | 265 | cmMap.on('change', () => { 266 | $indicatorViewSaved.hide(); 267 | }); 268 | 269 | cmReduce.on('change', () => { 270 | $indicatorViewSaved.hide(); 271 | }); 272 | 273 | $buttonSaveObject.click(() => { 274 | const data = JSON.parse(cmObject.getValue()); 275 | data._rev = parseInt($objectRev.html(), 10); 276 | socket.emit('set', $inputObjectId.val(), data, res => { 277 | if (res === 'ok') { 278 | $indicatorObjectSaved.show(); 279 | getObject($inputObjectId.val()); 280 | } else if (res.startsWith('rev mismatch')) { 281 | const [, revServer] = res.match(/rev mismatch (\d+)/); 282 | $objectRevServer.html(revServer); 283 | $objectRevEditor.html($objectRev.html()); 284 | $objectConflictId.html($inputObjectId.val()); 285 | $dialogConfirmConflict.modal('show'); 286 | } 287 | }); 288 | }); 289 | 290 | $buttonSaveObjectForce.click(() => { 291 | const data = JSON.parse(cmObject.getValue()); 292 | data._rev = null; 293 | socket.emit('set', $inputObjectId.val(), data, () => { 294 | $indicatorObjectSaved.show(); 295 | getObject($inputObjectId.val()); 296 | }); 297 | $dialogConfirmConflict.modal('hide'); 298 | }); 299 | 300 | $buttonReloadObject.click(() => { 301 | getObject($inputObjectId.val()); 302 | $dialogConfirmConflict.modal('hide'); 303 | }); 304 | 305 | $buttonDelObject.click(() => { 306 | $indicatorObjectId.html($inputObjectId.val()); 307 | $dialogConfirmDelObject.modal('show'); 308 | }); 309 | 310 | $buttonConfirmDelObject.click(() => { 311 | socket.emit('del', $inputObjectId.val(), () => { 312 | $inputObjectId.val(''); 313 | clearObject(); 314 | }); 315 | $dialogConfirmDelObject.modal('hide'); 316 | }); 317 | 318 | $buttonSaveView.click(() => { 319 | socket.emit('query', $inputViewId.val(), { 320 | map: cmMap.getValue(), 321 | reduce: cmReduce.getValue(), 322 | filter: $inputViewFilter.val() 323 | }, () => { 324 | $indicatorViewSaved.show(); 325 | }); 326 | }); 327 | 328 | $buttonDelView.click(() => { 329 | $indicatorViewId.html($inputViewId.val()); 330 | $dialogConfirmDelView.modal('show'); 331 | }); 332 | 333 | $buttonConfirmDelView.click(() => { 334 | socket.emit('query', $inputViewId.val(), '', () => { 335 | $inputViewId.val(''); 336 | clearView(); 337 | }); 338 | $dialogConfirmDelView.modal('hide'); 339 | }); 340 | 341 | $('.panel-top').resizable({ 342 | handleSelector: '.splitter-horizontal', 343 | resizeWidth: false 344 | }); 345 | 346 | $('.panel-left').resizable({ 347 | handleSelector: '.splitter-vertical', 348 | resizeHeight: false 349 | }); 350 | -------------------------------------------------------------------------------- /ui/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 2px; 5 | } 6 | 7 | #container { 8 | height: calc(100% - 46px); 9 | } 10 | 11 | #tab-objects, #tab-views { 12 | layout: flex; 13 | flex-direction: column; 14 | height: 100%; 15 | } 16 | 17 | .input-line { 18 | padding: 4px; 19 | height: 40px; 20 | } 21 | 22 | .view-config { 23 | padding: 4px; 24 | } 25 | 26 | #map-editor-container, #reduce-editor-container { 27 | height: calc(100% - 8px); 28 | } 29 | 30 | .editor-container { 31 | border: 1px solid #ccc; 32 | border-radius: 4px; 33 | margin: 4px; 34 | } 35 | 36 | .editor-container:focus-within { 37 | border-color: #66afe9; 38 | outline: 0; 39 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); 40 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); 41 | } 42 | 43 | .code { 44 | white-space: pre; 45 | font-family: monospace; 46 | } 47 | 48 | .CodeMirror { 49 | height: 100%; 50 | width: 100%; 51 | border-radius: 4px; 52 | } 53 | 54 | #map-editor-container .CodeMirror, #reduce-editor-container .CodeMirror { 55 | height: 100%; 56 | } 57 | 58 | #result-editor-container { 59 | position: absolute; 60 | width: calc(100% - 8px); 61 | height: calc(100% - 8px); 62 | } 63 | 64 | #result-editor-container .CodeMirror { 65 | height: 100%; 66 | } 67 | 68 | .CodeMirror-scroll { 69 | border-radius: 4px; 70 | } 71 | 72 | 73 | #topic { 74 | width: 100%; 75 | } 76 | 77 | #object-editor-container { 78 | height: calc(100% - 84px); 79 | } 80 | 81 | #input-view-result { 82 | white-space: pre; 83 | display: block; 84 | overflow-y: scroll; 85 | height: 100% 86 | } 87 | 88 | #jsonlint { 89 | display: inline-block; 90 | width: 20px; 91 | } 92 | 93 | #object-rev { 94 | display: inline-block; 95 | width: 60px; 96 | } 97 | 98 | #object-buttons, #view-buttons { 99 | padding: 2px 4px 0px 4px; 100 | height: 38px; 101 | width: 100%; 102 | display: flex; 103 | flex-direction: row; 104 | } 105 | 106 | .buttons-left { 107 | flex-grow: 1; 108 | } 109 | 110 | #indicator-object-saved, #indicator-view-saved { 111 | height: 34px; 112 | line-height: 23px; 113 | padding: 6px; 114 | margin: 0; 115 | } 116 | 117 | .modal-title { 118 | display: inline-block; 119 | } 120 | 121 | .topic { 122 | font-weight: bold; 123 | } 124 | 125 | .panel-container-vertical { 126 | height: calc(100% - 78px); 127 | display: flex; 128 | flex-direction: column; 129 | overflow: hidden; 130 | } 131 | 132 | .panel-container-horizontal { 133 | height: 100%; 134 | width: 100%; 135 | display: flex; 136 | flex-direction: row; 137 | overflow: hidden; 138 | } 139 | 140 | .panel-left { 141 | flex: 0 0 auto; 142 | height: 100%; 143 | width: calc(50% - 6px); 144 | min-width: 12px; 145 | max-width: calc(100% - 24px); 146 | } 147 | 148 | .panel-right { 149 | height: 100%; 150 | flex-grow: 1; 151 | overflow: hidden; 152 | } 153 | 154 | .panel-top { 155 | flex: 0 0 auto; 156 | min-height: 37px; 157 | max-height: calc(100% - 77px); 158 | height: 78px; 159 | width: 100%; 160 | white-space: nowrap; 161 | } 162 | 163 | .splitter-horizontal { 164 | flex: 0 0 auto; 165 | height: 38px; 166 | background: url(./hgrip.png) center center no-repeat; 167 | cursor: row-resize; 168 | margin-left: 160px; 169 | margin-right: 160px; 170 | } 171 | 172 | 173 | .splitter-vertical { 174 | flex: 0 0 auto; 175 | width: 12px; 176 | background: url(./vgrip.png) center center no-repeat; 177 | cursor: col-resize; 178 | } 179 | 180 | 181 | .panel-bottom { 182 | position: relative; 183 | flex: 1 1 auto; 184 | } 185 | -------------------------------------------------------------------------------- /ui/vgrip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hobbyquaker/mqttDB/58c8b1df452c68ad584f81494ca82256355bac54/ui/vgrip.png --------------------------------------------------------------------------------