Add first implementation of network monitor
authorymh <ymh.work@gmail.com>
Mon, 26 Jun 2017 15:21:06 +0200
changeset 87 dbcee57de2c6
parent 86 fa8ef84a1780
child 88 2a861fed6bde
Add first implementation of network monitor
client/.env
client/src/actions/networkActions.js
client/src/api/APIClient.js
client/src/config.js
client/src/constants/actionTypes.js
client/src/sagas/authSaga.js
client/src/sagas/index.js
client/src/sagas/networkSaga.js
client/src/store/configureStore.js
--- 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
--- /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
+  }
+}
--- 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});
         });
     });
   }
--- 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,
 }
--- 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';
--- /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),
+  ])
+}
--- 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),
-  ])
-}
--- /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
+  ]);
+}
--- 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;
 };