--- 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;
};