Add electron

This commit is contained in:
Thomas Nordquist
2019-01-01 15:31:33 +01:00
parent 4e09ea3d30
commit b2badfd43f
37 changed files with 3900 additions and 2578 deletions

2644
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
backend/package.json Normal file
View File

@@ -0,0 +1,56 @@
{
"name": "mqtt-explorer",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"test": "mocha",
"test-inspect": "mocha --inspect-brk",
"coverage": "nyc mocha",
"debug": "ts-node --inspect ./src/index.ts"
},
"author": "",
"license": "ISC",
"nyc": {
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"src/**/spec/*.spec.ts"
],
"extension": [
".ts",
".tsx"
],
"require": [
"ts-node/register"
],
"reporter": [
"text-summary",
"html"
],
"sourceMap": true,
"instrument": true
},
"dependencies": {
"@types/sha1": "^1.1.1",
"@types/socket.io": "^2.1.2",
"mqtt": "^2.18.8",
"sha1": "^1.1.1",
"socket.io": "^2.2.0",
"tslint": "^5.12.0",
"typescript": "^3.2.2"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.18",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",
"source-map-support": "^0.5.9",
"ts-node": "^7.0.1",
"tslint-strict-null-checks": "^1.0.1"
}
}

View File

@@ -0,0 +1,13 @@
import { DataSourceState } from './'
type MessageCallback = (topic: string, payload: Buffer) => void
// A DataSource should automatically reconnect if connection was broken
interface DataSource<DataSourceOptions> {
connect(options: DataSourceOptions): DataSourceState
disconnect(): void
onMessage(messageCallback: MessageCallback): void
topicSeparator: string
}
export { DataSource, MessageCallback }

View File

@@ -0,0 +1,41 @@
interface InternalState {
connecting: boolean
connected: boolean
error?: Error
}
export class DataSourceState {
private state: InternalState = {
error: undefined,
connected: false,
connecting: false
}
public setConnected(connected: boolean) {
this.state = {
error: undefined,
connected: connected,
connecting: false
}
}
public setError(error: Error) {
this.state = {
error: error,
connected: false,
connecting: false
}
}
public setConnecting() {
this.state = {
error: undefined,
connected: false,
connecting: true
}
}
public toJSON() {
return this.state
}
}

View File

@@ -0,0 +1,58 @@
import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource, DataSourceState } from './'
export interface MqttOptions {
url: string
}
export class MqttSource implements DataSource<MqttOptions> {
private client: Client | undefined
private messageCallback?: (topic: string, message: Buffer) => void
private rootSubscription = '#'
public topicSeparator = '/'
public onMessage(messageCallback: (topic: string, message: Buffer) => void) {
this.messageCallback = messageCallback
}
public connect(options: MqttOptions): DataSourceState {
const state = new DataSourceState()
const client = mqttConnect(options.url, {
resubscribe: false
})
this.client = client
client.on('error', (error: Error) => {
state.setError(error)
})
client.on('close', () => {
state.setConnected(false)
})
client.on('reconnect', () => {
state.setConnecting()
})
client.on('connect', () => {
state.setConnected(true)
client.subscribe(this.rootSubscription, (err: Error) => {
if (err) {
throw new Error('mqtt subscription failed')
}
})
})
client.on('message', (topic, message) => {
this.messageCallback && this.messageCallback(topic, message)
})
return state
}
public disconnect() {
this.client && this.client.end()
}
}

View File

@@ -0,0 +1,10 @@
import { DataSource } from './DataSource'
import { DataSourceState } from './DataSourceState'
import { MqttOptions, MqttSource } from './MqttSource'
export {
DataSource,
DataSourceState,
MqttOptions,
MqttSource,
}

61
backend/src/DotExport.ts Normal file
View File

@@ -0,0 +1,61 @@
import { Tree, TreeNode } from './Model'
export class DotExport {
public static renderNodeInformation(node: TreeNode): string {
return `\t${node.sourceEdge.hash()} [label=${this.renderLabel(node.value)}]`
}
public static toDot(tree: Tree): string {
let i = 1
let leaveEdges = Object.values(tree.edges)
.map(e => e.node)
.map(node => node.leafes())
.reduce((a, b) => a.concat(b), [])
.map(leave => leave.branch())
const allEdges: Array<string> = []
const nodeInformation: {[s: string]: string} = {}
leaveEdges.map(edges => edges.reduce( (prev, current) => {
let currentHash = current.sourceEdge.hash()
nodeInformation[currentHash] = this.renderNodeInformation(current)
if (current && prev) {
allEdges.push(`\t${prev.sourceEdge.hash()} -> ${currentHash} [label=${this.renderLabel(current.sourceEdge.name)}]`)
}
return current
}))
return `strict digraph ethane {
${
[this.renderNodeInformation(tree)]
.concat(Object.values(nodeInformation))
.concat(allEdges)
.join('\n')
}
}`;
}
private static renderLabel(value: any): string {
let str;
if(!isNaN(value)) {
str = value
} else {
str = JSON.stringify(value)
if(str && str.length > 0) {
str = str.slice(1, -1)
}
}
if (!str) {
return '""'
}
if(str.length > 20) {
str = str.slice(0, 20)+'…'
}
if (str[0] !== '"') {
str = `"${str}"`
}
return str
}
}

35
backend/src/Model/Edge.ts Normal file
View File

@@ -0,0 +1,35 @@
import { Hashable, TreeNode } from './'
const sha1 = require('sha1')
export class Edge implements Hashable {
public name: string
public node!: TreeNode
public source?: TreeNode | undefined
private cachedHash?: string
constructor(name: string) {
this.name = name
}
public edges() {
return this.node ? Object.values(this.node.edges) : []
}
public hash(): string {
if (!this.cachedHash) {
let previousHash = (this.source && this.source.sourceEdge) ? this.source.sourceEdge.hash() : ''
this.cachedHash = 'H' + sha1(previousHash + this.name)
}
return this.cachedHash
}
public firstEdge(): Edge {
if (this.source && this.source.sourceEdge) {
return this.source.sourceEdge.firstEdge()
} else {
return this
}
}
}

View File

@@ -0,0 +1,3 @@
export interface Hashable {
hash(): string
}

View File

@@ -0,0 +1,4 @@
export class TopicProperties {
public topicSeparator: string = '/'
public multilevelWildcard: string | null = '#'
}

View File

@@ -0,0 +1,7 @@
import { Edge, TreeNode } from './'
export class Tree extends TreeNode {
constructor() {
super(undefined, undefined)
}
}

View File

@@ -0,0 +1,83 @@
import { Edge } from './'
import { EventEmitter } from 'events'
export class TreeNode extends EventEmitter {
public sourceEdge?: Edge
public value?: any | null
public edges: {[s: string]: Edge} = {}
public collapsed = false
constructor(sourceEdge?: Edge, value?: any) {
super()
if (sourceEdge) {
this.sourceEdge = sourceEdge
sourceEdge.node = this
}
this.value = value
}
public hash(): string {
return 'N' + (this.sourceEdge ? this.sourceEdge.hash() : '')
}
public firstNode(): TreeNode {
return this.sourceEdge ? this.sourceEdge.firstEdge().node : this
}
public path(): string {
return this.branch()
.map(node => (node.sourceEdge && node.sourceEdge.name))
.filter(name => name !== undefined)
.join('/')
}
private previous(): TreeNode | undefined {
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
}
public addEdge(edge: Edge) {
this.edges[edge.name] = edge
edge.source = this
this.emit('update')
}
public branch(): Array<TreeNode> {
let previous = this.previous()
if (!previous) {
return [this]
}
return previous.branch().concat([this])
}
public updateWithNode(node: TreeNode) {
if (node.value !== undefined) {
this.value = node.value
}
this.mergeEdges(node)
this.emit('update')
}
public leafes(): Array<TreeNode> {
if (Object.values(this.edges).length === 0) {
return [this]
}
return Object.values(this.edges)
.map(e => e.node.leafes())
.reduce((a, b) => a.concat(b), [])
}
private mergeEdges(node: TreeNode) {
let edgeKeys = Object.keys(node.edges)
for (let edgeKey of edgeKeys) {
let matchingEdge = this.edges[edgeKey]
if (matchingEdge) {
matchingEdge.node.updateWithNode(node.edges[edgeKey].node)
} else {
this.addEdge(node.edges[edgeKey])
}
}
}
}

View File

@@ -0,0 +1,32 @@
import { Edge, Tree, TreeNode } from './'
export abstract class TreeNodeFactory {
public static fromEdgesAndValue(edgeNames: Array<string>, value: any): TreeNode {
const lastEdgeIndex = edgeNames.length - 1
var edges = edgeNames
.map((name, idx) => {
const edge = new Edge(name)
const nodeValue = lastEdgeIndex == idx ? value : undefined
const node = new TreeNode(edge, nodeValue)
edge.node = node
return edge
})
let reversed: Array<Edge> = edges.reverse()
let previous: Edge | undefined = undefined;
for (let edge of reversed) {
if (previous) {
edge.node.addEdge(previous)
}
previous = edge;
}
let leaf = reversed[0].node
let sourceTree = new Tree()
sourceTree.updateWithNode(leaf.firstNode())
return leaf
}
}

View File

@@ -0,0 +1,10 @@
import { Edge } from './Edge'
import { TreeNode } from './TreeNode'
import { TreeNodeFactory } from './TreeNodeFactory'
import { Tree } from './Tree'
import { TopicProperties } from './TopicProperties'
import { Hashable } from './Hashable'
export {
Edge, TreeNode, TreeNodeFactory, Tree, TopicProperties, Hashable
}

View File

@@ -0,0 +1,31 @@
import { Edge, TreeNode } from '../'
import { expect } from 'chai';
import 'mocha';
describe('Edge', () => {
it('should contain a name', () => {
let e = new Edge('foo')
expect(e.name).to.equal('foo')
});
it('hash should not be empty', () => {
let e = new Edge('bar')
expect(e.hash().length).to.be.gt(0)
});
it('hash should be stable', () => {
let e = new Edge('bar')
let previousHash = e.hash()
expect(e.hash()).to.eq(previousHash)
});
it('hash should change when parent is present', () => {
let foo = new Edge('foo')
let bar = new Edge('bar')
var previousHash = bar.hash()
bar.source = new TreeNode(foo, undefined)
expect(bar.hash()).to.not.eq(previousHash)
});
});

View File

@@ -0,0 +1,18 @@
import { Edge, Tree, TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai';
import 'mocha';
import './TreeNode.findNode'
describe('Tree', () => {
it('node can be merged into a tree', () => {
const tree = new Tree()
const topics = 'foo/bar'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 3)
debugger
tree.updateWithNode(leaf.firstNode())
let expectedNode = tree.findNode('foo/bar')
expect(expectedNode).to.eq(leaf)
})
});

View File

@@ -0,0 +1,29 @@
import { TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai';
import 'mocha';
import './TreeNode.findNode'
describe('TreeNode.findNode', () => {
it('findNode should retrieve node', () => {
const topics = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 5)
let root = leaf.firstNode()
expect(root.sourceEdge.name).to.eq('')
let barNode = root.findNode('foo/bar')
if (!barNode) {
expect.fail('did not find node')
return
}
expect(barNode.sourceEdge.name).to.eq('bar')
let bazNode = root.findNode('foo/bar/baz')
if (!bazNode) {
expect.fail('did not find node')
return
}
expect(bazNode.sourceEdge.name).to.eq('baz')
})
})

View File

@@ -0,0 +1,20 @@
import { TreeNode } from '../'
declare module "../" {
interface TreeNode {
findNode(path: String): TreeNode | undefined
}
}
TreeNode.prototype.findNode = function(path: String): TreeNode | undefined {
const topics = path.split('/')
let edge = this.edges[topics[0]]
let remainingTopics = topics.slice(1, topics.length)
if (edge && remainingTopics.length === 0) {
return edge.node
} else if (edge) {
return edge.node.findNode(remainingTopics.join('/'))
}
return undefined
}

View File

@@ -0,0 +1,51 @@
import { Edge, Tree, TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai';
import 'mocha';
import './TreeNode.findNode'
describe('TreeNode', () => {
it('updateWithNode should update value', () => {
const topics = 'foo/bar'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 3)
expect(leaf.value).to.eq(3)
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics, 5)
leaf.firstNode().updateWithNode(updateLeave.firstNode())
expect(leaf.firstNode().sourceEdge.name).to.eq(updateLeave.firstNode().sourceEdge.name)
expect(leaf.value).to.eq(5)
})
it('updateWithNode should update intermediate nodes', () => {
const topics1 = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics1, 3)
expect(leaf.value).to.eq(3)
const topics2 = 'foo/bar'.split('/')
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics2, 5)
leaf.firstNode().updateWithNode(updateLeave.firstNode())
let barNode = leaf.firstNode().findNode('foo/bar')
expect(barNode && barNode.sourceEdge.name).to.eq('bar')
expect(barNode && barNode.value).to.eq(5)
expect(leaf.sourceEdge.name).to.eq('baz')
expect(leaf.value).to.eq(3)
})
it('updateWithNode should add nodes to the tree', () => {
const topics1 = 'foo/bar'.split('/')
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, 3)
const topics2 = 'foo/bar/baz'.split('/')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, 5)
leaf1.firstNode().updateWithNode(leaf2.firstNode())
let expectedNode = leaf1.firstNode().findNode('foo/bar/baz')
if (!expectedNode) {
expect.fail('merge seems to have failed')
return
}
})
});

View File

@@ -0,0 +1,46 @@
import { Edge, TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai';
import 'mocha';
describe('TreeNodeFactory', () => {
it('should create node', () => {
let topic = 'foo/bar'
let edges = topic.split('/')
let node = TreeNodeFactory.fromEdgesAndValue(edges, 5)
expect(node).to.not.eq(undefined)
expect(node.sourceEdge.name).to.eq('bar')
expect(node.value).to.eq(5)
if (!node.sourceEdge.source) {
expect.fail('should not happen')
return
}
let foo = node.sourceEdge.source.sourceEdge
expect(foo.name).to.eq('foo')
});
it('node should contain edges in order', () => {
let topic = 'foo/bar/baz'
let edges = topic.split('/')
let node = TreeNodeFactory.fromEdgesAndValue(edges, 5)
expect(node.value).to.eq(5)
expect(node.sourceEdge.name).to.eq('baz')
const barNode = node.sourceEdge.source
if (!barNode) {
expect.fail('should not fail')
return
}
expect(barNode.sourceEdge.name).to.eq('bar')
const fooNode = barNode.sourceEdge.source
if (!fooNode) {
expect.fail('should not fail')
return
}
expect(fooNode.sourceEdge.name).to.eq('foo')
});
});

37
backend/src/index.ts Normal file
View File

@@ -0,0 +1,37 @@
import { TopicProperties, Tree, TreeNodeFactory } from './Model'
import { MqttSource, DataSource } from './DataSource'
import * as socketIO from 'socket.io'
const server = require('http').createServer();
let tree = new Tree()
let options = {url: 'mqtt://nodered'}
let dataSource = new MqttSource()
let count = 200
const a: Array<any> = []
const io = socketIO(server)
io.on('connection', client => {
console.log('connection')
a.forEach(b => {
io.emit('message', b)
})
client.on('event', data => { /* … */ });
client.on('disconnect', () => { /* … */ });
});
server.listen(3000);
let state = dataSource.connect(options)
dataSource.onMessage((topic: string, payload: Buffer) => {
if (payload.length > 10000) {
payload = payload.slice(0, 10000)
}
io.emit('message', { topic, payload: payload.toString('base64') })
})
setTimeout(() => {
dataSource.disconnect()
}, 1000000)

3
backend/test/mocha.opts Normal file
View File

@@ -0,0 +1,3 @@
--require ts-node/register
--require source-map-support/register
--recursive ./src/**/*.spec.ts

19
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"outDir": "./build",
"strict": true,
"lib": ["es2017"],
"sourceMap": true
},
"includes": [
"src/**/*.ts"
],
"exclude": [
"app",
"node_modules",
"**/*.spec.ts"
]
}