Handle connection loss gracefully

This commit is contained in:
Drew DeVault 2017-09-08 13:55:47 +09:00
parent 54030bb341
commit a1aa140848
7 changed files with 61 additions and 23 deletions

View File

@ -1,5 +1,10 @@
export const SOCKET_CONNECTED = "SOCKET_CONNECTED"; export const SOCKET_STATE = {
export const SOCKET_DISCONNECTED = "SOCKET_DISCONNECTED"; CONNECTED: "SOCKET_CONNECTED",
CONNECTING: "SOCKET_CONNECTING",
DISCONNECTED: "SOCKET_DISCONNECTED"
};
export const socket_connected = () => ({ type: SOCKET_CONNECTED }); export const SOCKET_UPDATE = "SOCKET_UPDATE";
export const socket_disconnected = () => ({ type: SOCKET_DISCONNECTED });
export const socket_update = (state, reason=null) =>
({ type: SOCKET_UPDATE, state, reason });

View File

@ -6,21 +6,25 @@ import { ConnectedRouter } from 'react-router-redux';
import 'preact/devtools'; import 'preact/devtools';
import './polyfills'; import './polyfills';
import store, { history } from './store'; import store, { create, history } from './store';
import scss from '../scss/main.scss'; import scss from '../scss/main.scss';
import { ws_init } from './socket'; import { ws_init } from './socket';
import { filter_subscribe } from './actions/filter_subscribe'; import { filter_subscribe } from './actions/filter_subscribe';
import { socket_connected, socket_disconnected } from './actions/socket'; import { socket_update, SOCKET_STATE } from './actions/socket';
import Nav from './ui/navigation'; import Nav from './ui/navigation';
import Main from './ui/main'; import Main from './ui/main';
import Connection from './ui/connection'; import Connection from './ui/connection';
export function initialize(uri) { export function initialize(uri) {
store.dispatch(socket_update(SOCKET_STATE.CONNECTING));
ws_init(uri, () => { ws_init(uri, () => {
store.dispatch(socket_connected()); store.dispatch(socket_update(SOCKET_STATE.CONNECTED));
store.dispatch(filter_subscribe()); store.dispatch(filter_subscribe());
store.dispatch(filter_subscribe('server')); store.dispatch(filter_subscribe('server'));
}, () => {
store.dispatch(socket_update(SOCKET_STATE.DISCONNECTED,
"You were disconnected."));
}); });
} }

View File

@ -1,4 +1,5 @@
import { UPDATE_RESOURCES, RESOURCES_REMOVED } from '../actions/resources'; import { UPDATE_RESOURCES, RESOURCES_REMOVED } from '../actions/resources';
import { SOCKET_STATE, SOCKET_UPDATE } from '../actions/socket';
export default function resourceReducer(type) { export default function resourceReducer(type) {
return (state = {}, action) => { return (state = {}, action) => {
@ -17,6 +18,9 @@ export default function resourceReducer(type) {
return Object.values(state) return Object.values(state)
.filter(r => action.ids.indexOf(r.id) === -1) .filter(r => action.ids.indexOf(r.id) === -1)
.reduce((s, r) => ({ ...s, [r.id]: r }), {}); .reduce((s, r) => ({ ...s, [r.id]: r }), {});
case SOCKET_UPDATE:
const _state = action.state;
return _state === SOCKET_STATE.CONNECTING ? {} : state;
} }
return state; return state;
}; };

View File

@ -1,12 +1,14 @@
import { SOCKET_CONNECTED, SOCKET_DISCONNECTED } from '../actions/socket'; import { SOCKET_STATE, SOCKET_UPDATE } from '../actions/socket';
export default function socket(state = { connected: false }, action) { export default function socket(_state = {
state: SOCKET_STATE.DISCONNECTED,
reason: null
}, action) {
const { state, reason } = action;
switch (action.type) { switch (action.type) {
case SOCKET_CONNECTED: case SOCKET_UPDATE:
return { ...state, connected: true }; return { ..._state, state, reason };
case SOCKET_DISCONNECTED:
return { ...state, connected: false };
default: default:
return state; return _state;
} }
} }

View File

@ -35,9 +35,9 @@ function ws_recv(e) {
handler && handler(msg); handler && handler(msg);
} }
export function ws_init(uri, cb) { export function ws_init(uri, open, close) {
ws = new WebSocket(uri); ws = new WebSocket(uri);
ws.addEventListener("open", cb); ws.addEventListener("open", open);
ws.addEventListener("message", ws_recv); ws.addEventListener("message", ws_recv);
ws.addEventListener("close", () => console.log("ws closed")); ws.addEventListener("close", close);
} }

View File

@ -13,10 +13,16 @@ export const history = createHistory();
const _compose = const _compose =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|| compose; || compose;
const store = createStore(
let store;
export const create = () => {
store = createStore(
reducer, reducer,
_compose(applyMiddleware(thunk, routerMiddleware(history))), _compose(applyMiddleware(thunk, routerMiddleware(history))),
); );
};
create();
if (module.hot) { if (module.hot) {
// Enable webpack hot module replacement for reducers // Enable webpack hot module replacement for reducers

View File

@ -1,14 +1,17 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
Alert,
Card, Card,
CardHeader, CardHeader,
CardBlock, CardBlock,
FormGroup, FormGroup,
Progress,
Label, Label,
Input Input
} from 'reactstrap'; } from 'reactstrap';
import { initialize } from '..'; import { initialize } from '..';
import { SOCKET_STATE } from '../actions/socket';
class ConnectionOverlay extends Component { class ConnectionOverlay extends Component {
constructor() { constructor() {
@ -25,15 +28,29 @@ class ConnectionOverlay extends Component {
render() { render() {
const { socket } = this.props; const { socket } = this.props;
if (socket.connected) { if (socket.state === SOCKET_STATE.CONNECTED) {
return null; return null;
} }
if (socket.state === SOCKET_STATE.CONNECTING) {
return (
<div className="connection-overlay">
<Card>
<CardHeader>Connect to synapse</CardHeader>
<CardBlock>
<p className="text-center">Connecting...</p>
<Progress value={100} animated />
</CardBlock>
</Card>
</div>
);
}
const { uri, autoconnect } = this.state; const { uri, autoconnect } = this.state;
return ( return (
<div className="connection-overlay"> <div className="connection-overlay">
<Card> <Card>
<CardHeader>Connect to synapse</CardHeader> <CardHeader>Connect to synapse</CardHeader>
<CardBlock> <CardBlock>
{socket.reason && <Alert color="info">{socket.reason}</Alert>}
<FormGroup> <FormGroup>
<Label for="socket-uri">Server URI</Label> <Label for="socket-uri">Server URI</Label>
<Input <Input
@ -56,7 +73,7 @@ class ConnectionOverlay extends Component {
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={() => initialize(this.state.uri)} onClick={() => initialize(this.state.uri)}
>Connect</button> >{socket.reason ? "Reconnect" : "Connect"}</button>
</CardBlock> </CardBlock>
</Card> </Card>
</div> </div>