From 25fc276d5e3f754f915500e91229b8607febc478 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Tue, 9 Apr 2019 01:20:35 +0200 Subject: Add settings websocket --- src/features/settingsWS/actions.js | 10 ++++ src/features/settingsWS/index.js | 35 ++++++++++++ src/features/settingsWS/state.js | 13 +++++ src/features/settingsWS/store.js | 107 +++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100755 src/features/settingsWS/actions.js create mode 100755 src/features/settingsWS/index.js create mode 100755 src/features/settingsWS/state.js create mode 100755 src/features/settingsWS/store.js (limited to 'src/features') diff --git a/src/features/settingsWS/actions.js b/src/features/settingsWS/actions.js new file mode 100755 index 000000000..631670c8a --- /dev/null +++ b/src/features/settingsWS/actions.js @@ -0,0 +1,10 @@ +import PropTypes from 'prop-types'; +import { createActionsFromDefinitions } from '../../actions/lib/actions'; + +export const settingsWSActions = createActionsFromDefinitions({ + greet: { + name: PropTypes.string.isRequired, + }, +}, PropTypes.checkPropTypes); + +export default settingsWSActions; diff --git a/src/features/settingsWS/index.js b/src/features/settingsWS/index.js new file mode 100755 index 000000000..1e268f184 --- /dev/null +++ b/src/features/settingsWS/index.js @@ -0,0 +1,35 @@ +import { reaction, runInAction } from 'mobx'; +import { SettingsWSStore } from './store'; +import state, { resetState } from './state'; + +const debug = require('debug')('Franz:feature:settingsWS'); + +let store = null; + +export default function initAnnouncements(stores, actions) { + const { features } = stores; + + // Toggle workspace feature + reaction( + () => ( + features.features.isSettingsWSEnabled + ), + (isEnabled) => { + if (isEnabled) { + debug('Initializing `settingsWS` feature'); + store = new SettingsWSStore(stores, null, actions, state); + store.initialize(); + runInAction(() => { state.isFeatureActive = true; }); + } else if (store) { + debug('Disabling `settingsWS` feature'); + runInAction(() => { state.isFeatureActive = false; }); + store.teardown(); + store = null; + resetState(); // Reset state to default + } + }, + { + fireImmediately: true, + }, + ); +} diff --git a/src/features/settingsWS/state.js b/src/features/settingsWS/state.js new file mode 100755 index 000000000..7b16b2b6e --- /dev/null +++ b/src/features/settingsWS/state.js @@ -0,0 +1,13 @@ +import { observable } from 'mobx'; + +const defaultState = { + isFeatureActive: false, +}; + +export const settingsWSState = observable(defaultState); + +export function resetState() { + Object.assign(settingsWSState, defaultState); +} + +export default settingsWSState; diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js new file mode 100755 index 000000000..21fd3440e --- /dev/null +++ b/src/features/settingsWS/store.js @@ -0,0 +1,107 @@ +import { observable, reaction } from 'mobx'; +import WebSocket from 'ws'; +import ms from 'ms'; + +import Store from '../../stores/lib/Store'; +import { WS_API } from '../../environment'; + +const debug = require('debug')('Franz:feature:settingsWS:store'); + +export class SettingsWSStore extends Store { + ws = null; + + @observable connected = false; + + pingTimeout = null; + + reconnectTimeout = null; + + constructor(stores, api, actions, state) { + super(stores, api, actions); + this.state = state; + } + + setup() { + this.connect(); + + reaction(() => !this.connected, this.reconnect.bind(this)); + } + + connect() { + try { + const wsURL = `${WS_API}/ws/${this.stores.user.data.id}`; + debug('Setting up WebSocket to', wsURL); + + this.ws = new WebSocket(wsURL); + + this.ws.on('open', () => { + debug('Opened WebSocket'); + this.send({ + action: 'authorize', + token: this.stores.user.authToken, + }); + + this.connected = true; + + this.heartbeat(); + }); + + this.ws.on('message', (data) => { + const resp = JSON.parse(data); + debug('Received message', resp); + + if (resp.id) { + this.stores.user.getUserInfoRequest.patch((result) => { + if (!result) return; + + debug('Patching user object with new values'); + Object.assign(result, resp); + }); + } + }); + + this.ws.on('ping', this.heartbeat.bind(this)); + } catch (err) { + console.err(err); + } + } + + heartbeat() { + debug('Heartbeat'); + clearTimeout(this.pingTimeout); + + // Use `WebSocket#terminate()` and not `WebSocket#close()`. Delay should be + // equal to the interval at which your server sends out pings plus a + // conservative assumption of the latency. + this.pingTimeout = setTimeout(() => { + debug('Terminating connection reconnecting in 35'); + this.ws.terminate(); + + this.connected = false; + }, ms('35s')); + } + + send(data) { + if (this.ws) { + this.ws.send(JSON.stringify(data)); + debug('Sending data', data); + } else { + debug('WebSocket is not initialized'); + } + } + + reconnect() { + if (!this.connected) { + debug('Trying to reconnect in 30s'); + this.reconnectTimeout = setInterval(() => { + debug('Trying to reconnect'); + this.connect(); + }, ms('30s')); + } else { + debug('Clearing reconnect interval'); + clearInterval(this.reconnectTimeout); + } + } +} + +export default SettingsWSStore; -- cgit v1.2.3-70-g09d2 From 70563e50ca4cfb0bf4d8c111069b2ad2e64a7b59 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Wed, 10 Apr 2019 21:38:03 +0200 Subject: minor clean up --- src/features/settingsWS/store.js | 3 --- 1 file changed, 3 deletions(-) (limited to 'src/features') diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js index 21fd3440e..3cc74d8ef 100755 --- a/src/features/settingsWS/store.js +++ b/src/features/settingsWS/store.js @@ -70,9 +70,6 @@ export class SettingsWSStore extends Store { debug('Heartbeat'); clearTimeout(this.pingTimeout); - // Use `WebSocket#terminate()` and not `WebSocket#close()`. Delay should be - // equal to the interval at which your server sends out pings plus a - // conservative assumption of the latency. this.pingTimeout = setTimeout(() => { debug('Terminating connection reconnecting in 35'); this.ws.terminate(); -- cgit v1.2.3-70-g09d2 From 8063f11cae51ac738dbbeeec5961e346ab2dd3db Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Sat, 13 Apr 2019 13:50:53 +0200 Subject: Only connect when user is already initialized --- src/features/settingsWS/store.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/features') diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js index 3cc74d8ef..8e1d455f1 100755 --- a/src/features/settingsWS/store.js +++ b/src/features/settingsWS/store.js @@ -22,8 +22,9 @@ export class SettingsWSStore extends Store { } setup() { - this.connect(); - + reaction(() => this.stores.user.data.id, this.connect.bind(this), { + fireImmediately: true, + }); reaction(() => !this.connected, this.reconnect.bind(this)); } -- cgit v1.2.3-70-g09d2 From 6aea1d4c7d3f7ca44356bc8186fb662483580fa9 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Sat, 13 Apr 2019 13:52:14 +0200 Subject: Remove copy & paste issues --- src/features/settingsWS/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/features') diff --git a/src/features/settingsWS/index.js b/src/features/settingsWS/index.js index 1e268f184..4049ae814 100755 --- a/src/features/settingsWS/index.js +++ b/src/features/settingsWS/index.js @@ -6,10 +6,10 @@ const debug = require('debug')('Franz:feature:settingsWS'); let store = null; -export default function initAnnouncements(stores, actions) { +export default function initSettingsWebSocket(stores, actions) { const { features } = stores; - // Toggle workspace feature + // Toggle SettingsWebSocket feature reaction( () => ( features.features.isSettingsWSEnabled -- cgit v1.2.3-70-g09d2 From 6cf3217f7f5da5b8ecfbb52e7c761ab91b1c4c97 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Sat, 13 Apr 2019 20:58:00 +0200 Subject: Use store reactions --- src/features/settingsWS/store.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) (limited to 'src/features') diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js index 8e1d455f1..0f1feebb9 100755 --- a/src/features/settingsWS/store.js +++ b/src/features/settingsWS/store.js @@ -1,4 +1,4 @@ -import { observable, reaction } from 'mobx'; +import { observable } from 'mobx'; import WebSocket from 'ws'; import ms from 'ms'; @@ -19,13 +19,12 @@ export class SettingsWSStore extends Store { constructor(stores, api, actions, state) { super(stores, api, actions); this.state = state; - } - setup() { - reaction(() => this.stores.user.data.id, this.connect.bind(this), { - fireImmediately: true, - }); - reaction(() => !this.connected, this.reconnect.bind(this)); + this.registerReactions([ + this._initialize.bind(this), + this._reconnect.bind(this), + this._close.bind(this), + ]); } connect() { @@ -72,7 +71,7 @@ export class SettingsWSStore extends Store { clearTimeout(this.pingTimeout); this.pingTimeout = setTimeout(() => { - debug('Terminating connection reconnecting in 35'); + debug('Terminating connection, reconnecting in 35'); this.ws.terminate(); this.connected = false; @@ -88,7 +87,15 @@ export class SettingsWSStore extends Store { } } - reconnect() { + // Reactions + + _initialize() { + if (this.stores.user.data.id) { + this.connect(); + } + } + + _reconnect() { if (!this.connected) { debug('Trying to reconnect in 30s'); this.reconnectTimeout = setInterval(() => { @@ -100,6 +107,14 @@ export class SettingsWSStore extends Store { clearInterval(this.reconnectTimeout); } } + + _close() { + if (!this.stores.user.isLoggedIn && this.ws) { + debug('Terminating connection'); + this.ws.terminate(); + this.ws = null; + } + } } export default SettingsWSStore; -- cgit v1.2.3-70-g09d2