decouple streaming client from app comp
This commit is contained in:
parent
cb5ffadcba
commit
df3f5e170b
5 changed files with 105 additions and 94 deletions
5
priv/frontend/package-lock.json
generated
5
priv/frontend/package-lock.json
generated
|
@ -6926,6 +6926,11 @@
|
||||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
|
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"nanoevents": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoevents/-/nanoevents-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-e6wsmRiGoBKaHHavsaKeVZRfxzH6y7evHdbZMTeRPyA0JKChr32quvabb6qPH6zDfEwy2/v/RABRdz8MuHXLJw=="
|
||||||
|
},
|
||||||
"nanomatch": {
|
"nanomatch": {
|
||||||
"version": "1.2.13",
|
"version": "1.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
|
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
|
||||||
"@fortawesome/react-fontawesome": "0.1.0-11",
|
"@fortawesome/react-fontawesome": "0.1.0-11",
|
||||||
"ms": "^2.1.1",
|
"ms": "^2.1.1",
|
||||||
|
"nanoevents": "^1.0.5",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.4.0",
|
"react": "^16.4.0",
|
||||||
"react-dom": "^16.4.0",
|
"react-dom": "^16.4.0",
|
||||||
|
|
|
@ -4,14 +4,12 @@ import './App.css'
|
||||||
import Service from './Service.js'
|
import Service from './Service.js'
|
||||||
import ServicePlaceholder from './ServicePlaceholder.js'
|
import ServicePlaceholder from './ServicePlaceholder.js'
|
||||||
import DegradedNotice from './DegradedNotice.js'
|
import DegradedNotice from './DegradedNotice.js'
|
||||||
import OP from '../ws/op.js'
|
import StreamingClient from '../ws/client'
|
||||||
import { log, objectFromEntries } from '../util.js'
|
import { log, objectFromEntries } from '../util.js'
|
||||||
import { domain as DOMAIN } from '../config.json'
|
import { domain as DOMAIN } from '../config.json'
|
||||||
|
|
||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
websocket = null
|
client = null
|
||||||
|
|
||||||
reconnectionTime = 1000
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -21,31 +19,15 @@ export default class App extends Component {
|
||||||
|
|
||||||
async componentDidMount () {
|
async componentDidMount () {
|
||||||
await this.loadMetrics()
|
await this.loadMetrics()
|
||||||
this.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeToChannels () {
|
const endpoint = `${DOMAIN}/api/streaming`
|
||||||
if (this.state.metrics == null) {
|
.replace('https', 'wss')
|
||||||
// fetching state failed, bail
|
.replace('http', 'ws')
|
||||||
log('not going to subscribe to realtime updates due to fetch failure')
|
this.client = new StreamingClient(endpoint, this.state.metrics)
|
||||||
return
|
this.client.connect()
|
||||||
}
|
|
||||||
|
|
||||||
const { graph } = this.state.metrics
|
this.client.on('status', this.handleStatus.bind(this))
|
||||||
|
this.client.on('latency', this.handleLatency.bind(this))
|
||||||
const channels = Object.keys(graph).reduce(
|
|
||||||
(channels, name) => [...channels, `latency:${name}`, `status:${name}`],
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
log(
|
|
||||||
`subscribing to ${channels.length} channels: ${channels.join(', ')}`
|
|
||||||
)
|
|
||||||
|
|
||||||
this._send({
|
|
||||||
op: OP.SUBSCRIBE,
|
|
||||||
channels,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStatus (name, [, status]) {
|
handleStatus (name, [, status]) {
|
||||||
|
@ -92,68 +74,6 @@ export default class App extends Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePacket (packet) {
|
|
||||||
const { op, c: channel, d: data } = packet
|
|
||||||
|
|
||||||
if (op !== OP.DATA) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const [type, name] = channel.split(':')
|
|
||||||
|
|
||||||
log('updating from channel:', channel)
|
|
||||||
|
|
||||||
if (type === 'latency') {
|
|
||||||
this.handleLatency(name, data)
|
|
||||||
} else if (type === 'status') {
|
|
||||||
this.handleStatus(name, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connect () {
|
|
||||||
log('connecting to ws')
|
|
||||||
|
|
||||||
const endpoint = `${DOMAIN}/api/streaming`
|
|
||||||
.replace('https', 'wss')
|
|
||||||
.replace('http', 'ws')
|
|
||||||
|
|
||||||
this.websocket = new WebSocket(endpoint)
|
|
||||||
|
|
||||||
this.websocket.onopen = () => {
|
|
||||||
log('ws opened')
|
|
||||||
this.subscribeToChannels()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.websocket.onclose = ({ code, reason }) => {
|
|
||||||
log(`ws closed with code ${code} (reason: ${reason || '<none>'}); ` +
|
|
||||||
`attempting to reconnect in ${this.reconnectionTime}ms`)
|
|
||||||
setTimeout(() => this.connect(), this.reconnectionTime)
|
|
||||||
this.reconnectionTime *= 2
|
|
||||||
}
|
|
||||||
|
|
||||||
this.websocket.onmessage = (message) => {
|
|
||||||
const { data } = message
|
|
||||||
const parsed = JSON.parse(data)
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'%c>>>%c',
|
|
||||||
'color: hsla(320, 100%, 50%, 1); font-weight: bold',
|
|
||||||
'color: inherit; font-weight: inherit',
|
|
||||||
parsed,
|
|
||||||
)
|
|
||||||
|
|
||||||
this.handlePacket(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.websocket.onerror = (event) => {
|
|
||||||
log('ws error:', event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_send (payload) {
|
|
||||||
this.websocket.send(JSON.stringify(payload))
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadMetrics () {
|
async loadMetrics () {
|
||||||
log('loading metrics')
|
log('loading metrics')
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
export function log (...args) {
|
export const logger = ({ name, color }) => (...args) =>
|
||||||
console.log(
|
console.log(
|
||||||
'%c[elstat]%c',
|
`%c[${name}]%c`,
|
||||||
'color: purple; font-weight: bold',
|
`color: ${color}; font-weight: bold`,
|
||||||
'color: inherit; font-weight: inherit',
|
'color: inherit; font-weight: inherit',
|
||||||
...args,
|
...args
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
export const log = logger({ name: 'elstat', color: 'purple' })
|
||||||
|
|
||||||
export function objectFromEntries (entries) {
|
export function objectFromEntries (entries) {
|
||||||
return entries.reduce(
|
return entries.reduce(
|
||||||
|
|
84
priv/frontend/src/ws/client.js
Normal file
84
priv/frontend/src/ws/client.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import NanoEvents from 'nanoevents'
|
||||||
|
|
||||||
|
import OP from './op'
|
||||||
|
import { logger } from '../util'
|
||||||
|
const log = logger({ name: 'ws', color: 'green' })
|
||||||
|
|
||||||
|
export default class StreamingClient extends NanoEvents {
|
||||||
|
constructor (url, metrics) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.url = url
|
||||||
|
this.metrics = metrics
|
||||||
|
this.ws = null
|
||||||
|
this.delay = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
send (packet) {
|
||||||
|
this.ws.send(JSON.stringify(packet))
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe () {
|
||||||
|
if (!this.metrics) {
|
||||||
|
log('not subscribing to channels -- no initial metrics')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log('subscribing to channels')
|
||||||
|
|
||||||
|
const { status } = this.metrics
|
||||||
|
const channels = Object.keys(status).reduce(
|
||||||
|
(channels, name) => [...channels, `latency:${name}`, `status:${name}`],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
log(`subscribing to ${channels.length} channels: ${channels.join(', ')}`)
|
||||||
|
this.send({ op: OP.SUBSCRIBE, channels })
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (packet) {
|
||||||
|
const { op, c: channel, d: data } = packet
|
||||||
|
|
||||||
|
if (op !== OP.DATA) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [type, name] = channel.split(':')
|
||||||
|
|
||||||
|
this.emit(type, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect () {
|
||||||
|
log('connecting')
|
||||||
|
|
||||||
|
const ws = this.ws = new WebSocket(this.url)
|
||||||
|
window.ws = ws
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
log('connected')
|
||||||
|
this.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = ({ code, reason }) => {
|
||||||
|
log(
|
||||||
|
`ws closed with code ${code} (reason: ${reason || '<none>'}); ` +
|
||||||
|
`attempting to reconnect in ${this.delay}ms`
|
||||||
|
)
|
||||||
|
|
||||||
|
setTimeout(() => this.connect(), this.delay)
|
||||||
|
this.delay *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = ({ data }) => {
|
||||||
|
log(`recv: ${data}`)
|
||||||
|
const packet = JSON.parse(data)
|
||||||
|
|
||||||
|
this.emit('packet', packet)
|
||||||
|
this.handle(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
log(`error: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue