refactored from simple built in client to reconnecting client package so client will autoreconnect if sockect server goes down/up.

Minor improvements to test client example.

updated autobind
master
David Kebler 2019-01-01 17:23:46 -08:00
parent 64bbe11774
commit 4d6661f785
14 changed files with 275 additions and 6862 deletions

View File

@ -1,34 +1,26 @@
module.exports = { module.exports = {
"ecmaFeatures": { ecmaFeatures: {
"modules": true, modules: true,
"spread" : true, spread: true,
"restParams" : true restParams: true
}, },
"env": { env: {
"es6": true, es6: true,
"node": true, node: true,
"mocha": true mocha: true
}, },
"parserOptions": { parserOptions: {
"ecmaVersion": 2017, ecmaVersion: 2017,
"sourceType": "module", sourceType: 'module',
"parser": 'babel-eslint', parser: 'babel-eslint'
}, },
"extends": "eslint:recommended", extends: 'eslint:recommended',
"rules": { rules: {
"indent": [ indent: ['error', 2],
"error", 'space-before-function-paren': ['error', 'always'],
2 'no-console': 0,
], semi: ['error', 'never'],
"no-console": 0, 'linebreak-style': ['error', 'unix'],
"semi": ["error", "never"], quotes: ['error', 'single']
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
]
} }
} }

View File

@ -1,99 +0,0 @@
// Websocket is a native global for vanilla JS
/* globals WebSocket:true */
import btc from 'better-try-catch'
import EventEmitter from 'eventemitter3'
import autoBind from 'auto-bind'
class Consumer extends EventEmitter {
constructor (url, opts = {}) {
super()
this.name = opts.name || 'browser'
this.instanceID = new Date().getTime()
this.url = url
this.protocol = opts.protocol
autoBind(this)
}
async connect () {
return new Promise((resolve,reject) => {
const socket = new WebSocket(this.url, this.protocol)
// Connection opened
socket.addEventListener('open', open.bind(this))
function open () {
this.socket = socket
resolve(`socket open to server at : ${this.url}`)
}
setTimeout(function () {
reject(new Error('Socket did not connect in 5 seconds'))
}, 5000)
socket.addEventListener('error', function () {
// console.log('Web Socket error occurred')
reject(new Error('Could not connect to socket server '))
})
})
}
listen (func) {
this.socket.addEventListener('message', handler.bind(this))
this.on('pushed',async function(packet){
// TODO do some extra security here?
let res = await this._packetProcess(packet)
if (!res) { // if process was not promise returning like just logged to console
console.log('warning: consumer process function was not promise returning')
}
})
function handler (event) {
let packet = {}
if (this.socket.readyState === 1) {
let [err, parsed] = btc(JSON.parse)(event.data)
if (err) packet = {error: `Could not parse JSON: ${event.data}`}
else packet = parsed
} else {
packet = {error: `Connection not Ready, CODE:${this.socket.readyState}`}
}
if (func) func(packet)
this.emit(packet._header.id, packet)
}
}
async send (packet) {
return new Promise((resolve, reject) => {
if (this.socket.readyState !== 1) reject(new Error(`Connection not Ready, CODE:${this.socket.readyState}`))
packet._header =
{ id: Math.random().toString().slice(2), // need this for when multiple sends for different consumers use same packet instanceack
sender: { name: this.name, instanceID: this.instanceID },
url: this.url
}
let [err, message] = btc(JSON.stringify)(packet)
if (err) reject(new Error(`Could not JSON stringify: ${packet}`))
// console.log('message to send', message)
this.socket.send(message)
// listen for when packet comes back with unique header id
this.once(packet._header.id, async function (reply) {
let res = await this._packetProcess(reply)
if (!res) { // if process was not promise returning like just logged to console
res = reply
console.log('consumer function was not promise returning - resolving unprocessed')
}
resolve(res)
}) // end reply listener
})
}
registerPacketProcessor (func) {
this._packetProcess = func
}
async _packetProcess (packet) {
return Promise.resolve(packet)
}
} // end Consumer Class
export default Consumer

View File

@ -1,8 +1,8 @@
{ {
"name": "@uci/websocket-client", "name": "@uci/websocket-client",
"version": "0.1.4", "version": "0.1.5",
"description": "JSON packet browser client over web socket", "description": "JSON packet browser client over web socket",
"main": "browser-client", "main": "src",
"scripts": { "scripts": {
"cd": "cd test-client && quasar dev" "cd": "cd test-client && quasar dev"
}, },
@ -28,7 +28,8 @@
"homepage": "https://github.com/uCOMmandIt/websocket-client#readme", "homepage": "https://github.com/uCOMmandIt/websocket-client#readme",
"devDependencies": {}, "devDependencies": {},
"dependencies": { "dependencies": {
"auto-bind": "^1.2.0", "auto-bind": "^2.0.0",
"eventemitter3": "^3.0.1" "eventemitter3": "^3.1.0",
"reconnecting-websocket": "^4.1.10"
} }
} }

View File

@ -9,13 +9,3 @@
## Why Bother ## Why Bother
## Getting Started ## Getting Started
"babel-eslint": "^8.2.1",
"eslint": "^4.18.2",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1"

173
src/WSConsumer.js Normal file
View File

@ -0,0 +1,173 @@
// Websocket is a native global for vanilla JS
// /* globals WebSocket:true */
import btc from 'better-try-catch'
import EventEmitter from 'eventemitter3'
import autoBind from 'auto-bind'
import WS from 'reconnecting-websocket'
/**
* Web Socket Consumer - An in browser consumer/client that can communicate via UCI packets
* extends {@link https://github.com/primus/eventemitter3 event emitter 3} an in browser event emitter
* uses the browser built in vanilla js global {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket Websocket client class}
* @extends EventEmitter
*/
class WSConsumer extends EventEmitter {
/**
* constructor - Description
*
* @param {type} url URL of UCI websocket server
*
*/
constructor (url, opts = {}) {
super()
this.name = opts.name || 'browser'
this.instanceID = new Date().getTime()
this.url = url
this.wsopts = opts.ws || { maxReconnectionDelay: 10000,minReconnectionDelay: 1000 + Math.random() * 4000,reconnectionDelayGrowFactor: 1.3,minUptime: 5000,connectionTimeout: 4000,maxRetries: Infinity,debug: false,}
this.protocol = opts.protocol // available if needed but not documented
this.socket = {}
autoBind(this)
}
/**
* connect - After instantiating this must be called to connect to a UCI WesbSocket Sever
* @required
* as coded will reconnect and set up listeners again if the socket gets closed
* commented code work
* but opted to use https://www.npmjs.com/package/reconnecting-websocket
*/
// async connect () {
// return new Promise((resolve, reject) => {
// const init = (socket) => {
// if (socket) console.error('Disconnected from Server')
// this.socket = new WebSocket(this.url, this.protocol)
// this.socket.addEventListener('close', init)
// this.socket.addEventListener('open', open.bind(this))
// }
//
// setTimeout(function () {
// reject(new Error('Socket did not initially connect in 20 seconds'))
// }, 20000)
//
// init()
//
// function open () {
// this.listen()
// resolve(`socket open to server at : ${this.url}`)
// }
// })
// }
async connect () {
return new Promise((resolve, reject) => {
this.socket = new WS(this.url, this.protocol, this.wsopts)
this.socket.onopen = open.bind(this)
setTimeout(function () {
reject(new Error('Socket did not initially connect in 20 seconds'))
}, 20000)
// this.socket.onerror = (ev) => { reject(`could not connect/reconnect to server : ${ev}`)}
function open () {
this.listen()
resolve(`socket open to server at : ${this.url}`)
}
})
}
/**
* listen - Description
*
* @param {type} func Description
*
* @returns {type} Description
*/
listen (func) {
this.socket.addEventListener('message', handler.bind(this))
this.on('pushed', async function (packet) {
// TODO do some extra security here for 'evil' pushed packets
let res = await this._packetProcess(packet)
if (!res) {
// if process was not promise returning like just logged to console
console.log(
'warning: consumer process function was not promise returning'
)
}
})
function handler (event) {
let packet = {}
if (this.socket.readyState === 1) {
let [err, parsed] = btc(JSON.parse)(event.data)
if (err) packet = { error: `Could not parse JSON: ${event.data}` }
else packet = parsed
} else {
packet = {
error: `Connection not Ready, CODE:${this.socket.readyState}`
}
}
// console.log('in the handler', event.data)
if (func) func(packet)
this.emit(packet._header.id, packet)
}
}
/**
* send - Description
*
* @param {type} packet Description
*
* @returns {type} Description
*/
async send (packet) {
return new Promise((resolve, reject) => {
if (this.socket.readyState !== 1)
reject(
new Error(`Connection not Ready, CODE:${this.socket.readyState}`)
)
packet._header = {
id: Math.random()
.toString()
.slice(2), // need this for when multiple sends for different consumers use same packet instanceack
sender: { name: this.name, instanceID: this.instanceID },
url: this.url
}
let [err, message] = btc(JSON.stringify)(packet)
if (err) reject(new Error(`Could not JSON stringify: ${packet}`))
// console.log('message to send', message)
this.socket.send(message)
// listen for when packet comes back with unique header id
this.once(packet._header.id, async function (reply) {
let res = await this._packetProcess(reply)
if (!res) {
// if process was not promise returning like just logged to console
res = reply
console.log(
'consumer function was not promise returning - resolving unprocessed'
)
}
resolve(res)
}) // end reply listener
})
}
/**
* registerPacketProcessor - attaches the passed packet function as the one to process incoming packets
* the funcion must take a uci packet object and return a promise whose resolution should be a uci packet if further communication is desir
*
* @param {function} func function to do the incoming packet processing
*
*/
registerPacketProcessor (func) {
this._packetProcess = func
}
async _packetProcess (packet) {
return Promise.resolve(packet)
}
} // end Consumer Class
export default WSConsumer

4
src/index.js Normal file
View File

@ -0,0 +1,4 @@
import Consumer from './WSConsumer'
export { Consumer as wsConsumer }
export default Consumer

View File

@ -15,23 +15,20 @@ module.exports = {
'standard' 'standard'
], ],
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: ['vue'],
'vue'
],
globals: { globals: {
'ga': true, // Google Analytics ga: true, // Google Analytics
'cordova': true, cordova: true,
'__statics': true __statics: true
}, },
// add your custom rules here // add your custom rules here
'rules': { rules: {
// allow async-await // allow async-await
'generator-star-spacing': 'off', 'generator-star-spacing': 'off',
'space-before-function-paren': ['error', 'always'],
// allow paren-less arrow functions // allow paren-less arrow functions
'arrow-parens': 0, 'arrow-parens': 0,
'one-var': 0, 'one-var': 0,
'import/first': 0, 'import/first': 0,
'import/named': 2, 'import/named': 2,
'import/namespace': 2, 'import/namespace': 2,

View File

@ -17,3 +17,4 @@ yarn-error.log*
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.lock

View File

@ -8,10 +8,10 @@
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"cd": "quasar dev" "tc": "quasar dev"
}, },
"dependencies": { "dependencies": {
"@uci/websocket-client": "^0.1.2", "@uci/websocket-client": "^0.1.4",
"axios": "^0.18.0", "axios": "^0.18.0",
"better-try-catch": "^0.6.2" "better-try-catch": "^0.6.2"
}, },
@ -26,7 +26,8 @@
"eslint-plugin-promise": "^3.7.0", "eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.3.0", "eslint-plugin-vue": "^4.3.0",
"quasar-cli": "^0.16.0" "quasar-cli": "^0.17.22",
"strip-ansi": "=3.0.1"
}, },
"engines": { "engines": {
"node": ">= 8.9.0", "node": ">= 8.9.0",

View File

@ -3,11 +3,8 @@
module.exports = function (ctx) { module.exports = function (ctx) {
return { return {
// app plugins (/src/plugins) // app plugins (/src/plugins)
plugins: [ plugins: ['socket'],
], css: ['app.styl'],
css: [
'app.styl'
],
extras: [ extras: [
ctx.theme.mat ? 'roboto-font' : null, ctx.theme.mat ? 'roboto-font' : null,
'material-icons', 'material-icons',
@ -57,18 +54,13 @@ module.exports = function (ctx) {
'QInput', 'QInput',
'QField' 'QField'
], ],
directives: [ directives: ['Ripple'],
'Ripple'
],
// Quasar plugins // Quasar plugins
plugins: [ plugins: ['Notify'],
'Notify'
],
iconSet: ctx.theme.mat ? 'material-icons' : 'ionicons' iconSet: ctx.theme.mat ? 'material-icons' : 'ionicons'
}, },
// animations: 'all' --- includes all animations // animations: 'all' --- includes all animations
animations: [ animations: [],
],
pwa: { pwa: {
// workboxPluginMode: 'InjectManifest', // workboxPluginMode: 'InjectManifest',
// workboxOptions: {}, // workboxOptions: {},
@ -82,29 +74,29 @@ module.exports = function (ctx) {
theme_color: '#027be3', theme_color: '#027be3',
icons: [ icons: [
{ {
'src': 'statics/icons/icon-128x128.png', src: 'statics/icons/icon-128x128.png',
'sizes': '128x128', sizes: '128x128',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'statics/icons/icon-192x192.png', src: 'statics/icons/icon-192x192.png',
'sizes': '192x192', sizes: '192x192',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'statics/icons/icon-256x256.png', src: 'statics/icons/icon-256x256.png',
'sizes': '256x256', sizes: '256x256',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'statics/icons/icon-384x384.png', src: 'statics/icons/icon-384x384.png',
'sizes': '384x384', sizes: '384x384',
'type': 'image/png' type: 'image/png'
}, },
{ {
'src': 'statics/icons/icon-512x512.png', src: 'statics/icons/icon-512x512.png',
'sizes': '512x512', sizes: '512x512',
'type': 'image/png' type: 'image/png'
} }
] ]
} }
@ -119,19 +111,16 @@ module.exports = function (ctx) {
}, },
packager: { packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store // OS X / Mac App Store
// appBundleId: '', // appBundleId: '',
// appCategoryType: '', // appCategoryType: '',
// osxSign: '', // osxSign: '',
// protocol: 'myapp://path', // protocol: 'myapp://path',
// Window only // Window only
// win32metadata: { ... } // win32metadata: { ... }
}, },
builder: { builder: {
// https://www.electron.build/configuration/configuration // https://www.electron.build/configuration/configuration
// appId: 'quasar-app' // appId: 'quasar-app'
} }
} }

View File

@ -24,13 +24,13 @@ orientation="vertical"
<q-btn :class="switches[3]" @click="toggle(4)">Switch 4</q-btn> <q-btn :class="switches[3]" @click="toggle(4)">Switch 4</q-btn>
<q-input class="row" rows="2" v-model="packet" readonly inverted type="textarea" float-label="Sending Packet:" /> <q-input class="row" rows="2" v-model="packet" readonly inverted type="textarea" float-label="Sending Packet:" />
<q-input class="row" rows="2" v-model="response" readonly inverted color="secondary" type="textarea" float-label="Socket/Server Response:" /> <q-input class="row" rows="2" v-model="response" readonly inverted color="secondary" type="textarea" float-label="Socket/Server Response:" />
<q-input class="row" rows="2" v-model="respayload" readonly inverted color="secondary" type="textarea" float-label="Socket/Server Response Payload:" />
<q-input class="row" rows="2" v-model="pushed" readonly inverted color="tertiary" type="textarea" float-label="Pushed from Socket/Server:" /> <q-input class="row" rows="2" v-model="pushed" readonly inverted color="tertiary" type="textarea" float-label="Pushed from Socket/Server:" />
</q-page> </q-page>
</template> </template>
<script> <script>
import btc from 'better-try-catch' import btc from 'better-try-catch'
import socket from '../socket.js' // import socket from '../socket.js'
export default { export default {
data () { data () {
@ -38,6 +38,7 @@ export default {
cmd: '', cmd: '',
packet: '', packet: '',
response: '', response: '',
respayload: {},
key: [], key: [],
value: [], value: [],
pushed: '', pushed: '',
@ -46,22 +47,40 @@ export default {
}, },
methods: { methods: {
async send () { async send () {
let packet = {cmd: this.cmd, [this.key[0]]: this.value[0], [this.key[1]]: this.value[1], [this.key[2]]: this.value[2]} let packet = {
cmd: this.cmd,
[this.key[0]]: this.value[0],
[this.key[1]]: this.value[1],
[this.key[2]]: this.value[2]
}
this.packet = JSON.stringify(packet) this.packet = JSON.stringify(packet)
let [err, res] = await btc(socket.send)(packet) let [err, res] = await btc(this.$socket.send)(packet)
if (err) console.log('error ', err) if (err) console.log('error ', err)
else { else {
delete res._header delete res._header
this.response = JSON.stringify(res) this.response = res.log
delete res.Response
this.packet_process(res)
this.respayload = JSON.stringify(res)
} }
}, },
setcolor (id) { setcolor (id) {
return this.switches[id] return this.switches[id]
}, },
packet_process (packet) {
if (packet.cmd === 'switch/status') {
this.switches[packet.id - 1] = packet.status
this.pushed = ''
}
},
async toggle (id) { async toggle (id) {
let packet = { cmd: 'switch/toggle', id: id, status: this.switches[id - 1] } let packet = {
cmd: 'switch/toggle',
id: id,
status: this.switches[id - 1]
}
this.packet = JSON.stringify(packet) this.packet = JSON.stringify(packet)
let [err, res] = await btc(socket.send)(packet) let [err, res] = await btc(this.$socket.send)(packet)
if (err) console.log('error ', err) if (err) console.log('error ', err)
else { else {
delete res._header delete res._header
@ -71,21 +90,27 @@ export default {
}, },
async mounted () { async mounted () {
console.log('mounting') console.log('mounting')
this.$q.notify({type: 'info', message: `Client connecting to', ${socket.url}`}) this.$q.notify({
let [err] = await btc(socket.connect)() type: 'info',
if (err) { this.$q.notify({type: 'negative', message: 'Websocket Server Not Available'}) } else { message: `Client connecting to', ${this.$socket.url}`
this.$q.notify({type: 'positive', message: 'Ready'}) })
socket.listen() let [err] = await btc(this.$socket.connect)()
if (err) {
this.$q.notify({
type: 'negative',
message: 'Websocket Server Not Available'
})
} else {
this.$q.notify({ type: 'positive', message: 'Ready' })
this.$socket.listen()
} }
socket.on('pushed', (packet) => { this.$socket.on('pushed', packet => {
delete packet._header delete packet._header
let status = packet.cmd.split('/') this.packet_process(packet)
this.switches[packet.id - 1] = status[1]
this.pushed = JSON.stringify(packet) this.pushed = JSON.stringify(packet)
}) })
} }
} }
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@ -1,5 +1,5 @@
// import WebSocket from '@uci/websocket-client' // import WebSocket from '@uci/websocket-client'
import WebSocket from '../../../browser-client' import WebSocket from '../../../src'
const ws = new WebSocket(process.env.WSS || 'ws://0.0.0.0:8090') const ws = new WebSocket(process.env.WSS || 'ws://0.0.0.0:8090')

View File

@ -1,10 +0,0 @@
import axios from 'axios'
export default ({ Vue }) => {
// we add it to Vue prototype
// so we can reference it in Vue files
// without the need to import axios
Vue.prototype.$axios = axios
// Example: this.$axios will reference Axios now so you don't need stuff like vue-axios
}

File diff suppressed because it is too large Load Diff