# HG changeset patch # User ymh # Date 1498483266 -7200 # Node ID dbcee57de2c6017ffacff1dcf5e6f87eaa39a81c # Parent fa8ef84a1780f2a66fa7baa1ad75fd88ec8b15fb Add first implementation of network monitor diff -r fa8ef84a1780 -r dbcee57de2c6 client/.env --- a/client/.env Fri Jun 23 18:50:57 2017 +0200 +++ b/client/.env Mon Jun 26 15:21:06 2017 +0200 @@ -1,2 +1,4 @@ REACT_APP_API_ROOT_URL = http://localhost:8000 REACT_APP_BASENAME = +REACT_APP_NETWORK_STATUS_INTERVAL = 2000 +REACT_APP_NETWORK_STATUS_TIMEOUT = 2000 diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/actions/networkActions.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/actions/networkActions.js Mon Jun 26 15:21:06 2017 +0200 @@ -0,0 +1,15 @@ +import * as types from '../constants/actionTypes'; + +export const dataFetchSuccess = (res) => { + return { + type: types.DATA_FETCH_SUCCESS, + res + }; +} + +export const offlineConfigInitialized = (additionalContext) => { + return { + type: types.OFFLINE_CONFIG_INITIALIZED, + additionalContext + } +} diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/api/APIClient.js --- a/client/src/api/APIClient.js Fri Jun 23 18:50:57 2017 +0200 +++ b/client/src/api/APIClient.js Mon Jun 26 15:21:06 2017 +0200 @@ -79,6 +79,9 @@ } else { return response.json().then((data) => reject(data)); } + }) + .catch((error) => { + reject({error}); }); }); } diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/config.js --- a/client/src/config.js Fri Jun 23 18:50:57 2017 +0200 +++ b/client/src/config.js Mon Jun 26 15:21:06 2017 +0200 @@ -1,6 +1,8 @@ // define application configuration export default { - apiRootUrl: process.env.REACT_APP_API_ROOT_URL, - basename: process.env.REACT_APP_BASENAME + apiRootUrl: process.env.REACT_APP_API_ROOT_URL || 'http://localhost:8000', + basename: process.env.REACT_APP_BASENAME || '', + networkStatusTimeout: parseInt(process.env.REACT_APP_NETWORK_STATUS_TIMEOUT, 10) || 2000, + networkStatusInterval: parseInt(process.env.REACT_APP_NETWORK_STATUS_INTERVAL, 10) || 2000, } diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/constants/actionTypes.js --- a/client/src/constants/actionTypes.js Fri Jun 23 18:50:57 2017 +0200 +++ b/client/src/constants/actionTypes.js Mon Jun 26 15:21:06 2017 +0200 @@ -19,3 +19,6 @@ export const USER_UPDATE_SETTINGS_ASYNC = 'USER_UPDATE_SETTINGS_ASYNC'; export const USER_UPDATE_SETTINGS = 'USER_UPDATE_SETTINGS' export const USER_TOGGLE_AUTO_SUBMIT = 'USER_TOGGLE_AUTO_SUBMIT'; + +export const DATA_FETCH_SUCCESS = 'DATA_FETCH_SUCCESS'; +export const OFFLINE_CONFIG_INITIALIZED ='OFFLINE_CONFIG_INITIALIZED'; diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/sagas/authSaga.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/sagas/authSaga.js Mon Jun 26 15:21:06 2017 +0200 @@ -0,0 +1,73 @@ +import { put, take, all } from 'redux-saga/effects' +import * as types from '../constants/actionTypes'; + +// --- + +function* watchLoginSubmit() { + while (true) { + const { username, password } = yield take(types.AUTH_LOGIN_SUBMIT); + yield put({ type: types.AUTH_LOGIN_REQUEST, username, password }); + } +} + +function* watchLoginRequest(context) { + while (true) { + try { + + const { username, password } = yield take(types.AUTH_LOGIN_REQUEST); + const client = context.client; + const response = yield client.post('/api/auth/login/', { username, password }); + + const actions = [{ + type: types.AUTH_STORE_TOKEN_ASYNC, + token: response.token, + }, + { + type: types.AUTH_LOGIN_SUCCESS, + user: response.user, + token: response.token, + }]; + + yield all(actions.map(action => put(action))); + context.history.push('/sessions'); + + } catch(e) { + yield put({ type: types.AUTH_LOGIN_ERROR, error: e }); + } + } +} + +function* watchStoreToken() { + while (true) { + const { token } = yield take(types.AUTH_STORE_TOKEN_ASYNC); + yield put({ type: types.AUTH_STORE_TOKEN, token }); + } +} + +function* watchUpdateSettings(context) { + while (true) { + const { username, firstname, lastname } = yield take(types.USER_UPDATE_SETTINGS_ASYNC); + const client = context.client; + try { + yield client.put('/api/auth/user/', { + username, + first_name: firstname, + last_name: lastname + }); + yield put({ type: types.USER_UPDATE_SETTINGS, firstname, lastname }); + } catch (e) { + + } + } +} + +// --- + +export default function* rootSaga(context) { + yield all([ + watchLoginSubmit(), + watchLoginRequest(context), + watchStoreToken(), + watchUpdateSettings(context), + ]) +} diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/sagas/index.js --- a/client/src/sagas/index.js Fri Jun 23 18:50:57 2017 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -import { put, take, all } from 'redux-saga/effects' -import * as types from '../constants/actionTypes'; - -// --- - -export function* watchLoginSubmit() { - while (true) { - const { username, password } = yield take(types.AUTH_LOGIN_SUBMIT); - yield put({ type: types.AUTH_LOGIN_REQUEST, username, password }); - } -} - -function* watchLoginRequest(context) { - while (true) { - try { - - const { username, password } = yield take(types.AUTH_LOGIN_REQUEST); - const client = context.client; - const response = yield client.post('/api/auth/login/', { username, password }); - - const actions = [{ - type: types.AUTH_LOGIN_SUCCESS, - user: response.user, - token: response.token, - // meta: { - // transition: (prevState, nextState, action) => ({ - // pathname: '/sessions', - // }), - // }, - }, { - type: types.AUTH_STORE_TOKEN_ASYNC, - token: response.token, - }]; - - yield all(actions.map(action => put(action))); - context.history.push('/sessions'); - - } catch(e) { - yield put({ type: types.AUTH_LOGIN_ERROR, error: e }); - } - } -} - -function* watchStoreToken() { - while (true) { - const { token } = yield take(types.AUTH_STORE_TOKEN_ASYNC); - yield put({ type: types.AUTH_STORE_TOKEN, token }); - } -} - -function* watchUpdateSettings(context) { - while (true) { - const { username, firstname, lastname } = yield take(types.USER_UPDATE_SETTINGS_ASYNC); - const client = context.client; - try { - yield client.put('/api/auth/user/', { - username, - first_name: firstname, - last_name: lastname - }); - yield put({ type: types.USER_UPDATE_SETTINGS, firstname, lastname }); - } catch (e) { - - } - } -} - -// --- - -export default function* rootSaga(context) { - yield all([ - watchLoginSubmit(), - watchLoginRequest(context), - watchStoreToken(), - watchUpdateSettings(context), - ]) -} diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/sagas/networkSaga.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/sagas/networkSaga.js Mon Jun 26 15:21:06 2017 +0200 @@ -0,0 +1,79 @@ +import * as types from '../constants/actionTypes'; +import { all, call, fork, race, take, cancelled, put, select } from 'redux-saga/effects' +import config from '../config'; + +// Utility function to delay effects +function delay(millis) { + const promise = new Promise(resolve => { + setTimeout(() => resolve(true), millis) + }); + return promise; +} + +function pingServer(client, token) { + console.log("PING SERVER", token); + if(token) { + const timeout = new Promise((resolve, reject) => { + setTimeout(reject, config.networkStatusTimeout, 'request timed out'); + }); + return Promise + .race([timeout, client.post('/api/auth/refresh/', { token })]); + } else { + return Promise.reject({ error: 'No token in the store'}) + } +} + +// Fetch data every 20 seconds +function* pollData(context) { + try { + yield call(delay, config.networkStatusInterval); + const token = yield select(state => state.token); + const res = yield pingServer(context.client, token); + yield call(context.callback, true); + yield put({ + type: types.AUTH_STORE_TOKEN_ASYNC, + token: res.token, + }); + } catch (error) { + yield call(context.callback, false); + // if the error is that there is no token, then we know we have to wait for a login + if(error.error && error.error === 'No token in the store') { + yield take(types.AUTH_LOGIN_SUCCESS); + } + } finally { + if (yield cancelled()) { + // pollDate cancelled + // if there is a token : this was a LOGIN, set status to ok + // if there is no token : this was a LOGOUT, set status to ko and wait for login + const token = yield select(state => state.token); + if(token) { + yield call(context.callback, true); + } else { + yield call(context.callback, false); + yield take(types.AUTH_LOGIN_SUCCESS); + } + } + } +} + +// Wait for successful response, then fire another request +// Cancel polling if user logs out or log in +function* watchPollData(context) { + while (true) { + yield race([ + call(pollData, context), + take(types.AUTH_LOGOUT), + take(types.AUTH_LOGIN_SUCCESS) + ]); + } +} + +// Daemonize tasks in parallel +export default function* root(baseContext) { + const actionRes = yield take(types.OFFLINE_CONFIG_INITIALIZED); + const context = {...baseContext, ...actionRes.additionalContext}; + yield all([ + fork(watchPollData, context) + // other watchers here + ]); +} diff -r fa8ef84a1780 -r dbcee57de2c6 client/src/store/configureStore.js --- a/client/src/store/configureStore.js Fri Jun 23 18:50:57 2017 +0200 +++ b/client/src/store/configureStore.js Mon Jun 26 15:21:06 2017 +0200 @@ -1,5 +1,6 @@ import rootReducer from '../reducers'; -import rootSaga from '../sagas'; +import rootAuthSaga from '../sagas/authSaga'; +import networkSaga from '../sagas/networkSaga'; import { compose, createStore, applyMiddleware } from 'redux'; import { routerMiddleware } from 'react-router-redux'; import createSagaMiddleware from 'redux-saga' @@ -14,6 +15,7 @@ import APIClient from '../api/APIClient'; import createEffect from '../api'; import config from '../config'; +import { offlineConfigInitialized } from '../actions/networkActions'; // const composeEnhancers = (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ? // window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ @@ -53,7 +55,6 @@ ...offlineDefaultConfig, persistOptions, effect: createEffect(apiClient), -// detectNetwork: callback => callback(true), } const storeInitialState = { ...defaultState }; @@ -64,6 +65,8 @@ const router = routerMiddleware(history); const saga = createSagaMiddleware(); + offlineConfig.detectNetwork = callback => { saga.run(networkSaga, { callback }); }; + const store = offline(offlineConfig)(createStore)(rootReducer, initialState, composeEnhancers( applyMiddleware(router, saga) )); @@ -75,7 +78,9 @@ history } - saga.run(rootSaga, context); + saga.run(rootAuthSaga, context); + + store.dispatch(offlineConfigInitialized({ client: apiClient })) return store; };