Skip to content

Commit

Permalink
feat: Implement control mechanism for queue release (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
emanueleDiVizio authored and rgommezz committed Oct 20, 2019
1 parent 6b37ce3 commit 35c48b8
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 41 deletions.
55 changes: 31 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ type MiddlewareConfig = {
regexActionType?: RegExp = /FETCH.*REQUEST/,
actionTypes?: Array<string> = [],
queueReleaseThrottle?: number = 50,
shouldDequeueSelector: (state: RootReduxState) => boolean = () => true
}
```

Expand All @@ -360,6 +361,9 @@ By default it's configured to intercept actions for fetching data following the

`queueReleaseThrottle`: waiting time in ms between dispatches when flushing the offline queue. Useful to reduce the server pressure when coming back online. Defaults to 50ms.

`shouldDequeueSelector`: function that receives the redux application state and returns a boolean. It'll be executed every time an action is dispatched, before it reaches the reducer. This is useful to control if the queue should be released when the connection is regained and there were actions queued up. Returning `true` (the default behaviour) releases the queue, whereas returning `false` prevents queue release. For example, you may wanna perform some authentication checks, prior to releasing the queue. Note, if the result of `shouldDequeueSelector` changes *while* the queue is being released, the queue will not halt. If you want to halt the queue *while* is being released, please see relevant FAQ section.


##### Thunks Config
For `redux-thunk` library, the async flow is wrapped inside functions that will be lazily evaluated when dispatched, so our store is able to dispatch functions as well. Therefore, the configuration differs:

Expand Down Expand Up @@ -551,16 +555,14 @@ checkInternetConnection(
##### Example

```js
import { checkInternetConnection, offlineActionTypes } from 'react-native-offline';
import { checkInternetConnection, offlineActionCreators } from 'react-native-offline';

async function internetChecker(dispatch) {
const isConnected = await checkInternetConnection();
const { connectionChange } = offlineActionCreators;
// Dispatching can be done inside a connected component, a thunk (where dispatch is injected), saga, or any sort of middleware
// In this example we are using a thunk
dispatch({
type: offlineActionTypes.CONNECTION_CHANGE,
payload: isConnected,
});
dispatch(connectionChange(isConnected));
}
```

Expand All @@ -584,21 +586,19 @@ As you can see in the snippets below, we create the `store` instance as usual an
// configureStore.js
import { createStore, applyMiddleware } from 'redux';
import { persistStore } from 'redux-persist';
import { createNetworkMiddleware, offlineActionTypes, checkInternetConnection } from 'react-native-offline';
import { createNetworkMiddleware, offlineActionCreators, checkInternetConnection } from 'react-native-offline';
import rootReducer from '../reducers';

const networkMiddleware = createNetworkMiddleware();

export default function configureStore(callback) {
const store = createStore(rootReducer, applyMiddleware(networkMiddleware));
const { connectionChange } = offlineActionCreators;
// https://github.com/rt2zz/redux-persist#persiststorestore-config-callback
persistStore(store, null, () => {
// After rehydration completes, we detect initial connection
checkInternetConnection().then(isConnected => {
store.dispatch({
type: offlineActionTypes.CONNECTION_CHANGE,
payload: isConnected,
});
store.dispatch(connectionChange(isConnected));
callback(); // Notify our root component we are good to go, so that we can render our app
});
});
Expand Down Expand Up @@ -641,25 +641,32 @@ export default App;

This way, we make sure the right actions are dispatched before anything else can be.

#### How to intercept and queue actions when the server responds with client (4xx) or server (5xx) errors
You can do that by dispatching yourself an action of type `@@network-connectivity/FETCH_OFFLINE_MODE`. The action types the library uses are exposed under `offlineActionTypes` property.
#### How do I stop the queue *while* it is being released?

You can do that by dispatching a `CHANGE_QUEUE_SEMAPHORE` action using `changeQueueSemaphore` action creator. This action is used to manually stop and resume the queue even if it's being released.

Unfortunately, the action creators are not exposed yet, so I'll release soon a new version with that fixed. In the meantime, you can check that specific action creator in [here](https://github.com/rgommezz/react-native-offline/blob/master/src/actionCreators.js#L18), so that you can emulate its payload. That should queue up your action properly.
It works in the following way: if a `changeQueueSemaphore('RED')` action is dispatched, queue release is now halted. It will only resume if another if `changeQueueSemaphore('GREEN')` is dispatched.

```js
import { offlineActionTypes } from 'react-native-offline';
import { offlineActionCreators } from 'react-native-offline';
...
async function weHaltQeueeReleaseHere(){
const { changeQueueSemaphore } = offlineActionCreators;
dispatch(changeQueueSemaphore('RED')) // The queue is now halted and it won't continue dispatching actions
await somePromise();
dispatch(changeQueueSemaphore('GREEN')) // The queue is now resumed and it will continue dispatching actions
}
```


#### How to intercept and queue actions when the server responds with client (4xx) or server (5xx) errors
You can do that by dispatching a `FETCH_OFFLINE_MODE` action using `fetchOfflineMode` action creator.

```js
import { offlineActionCreators } from 'react-native-offline';
...
fetch('someurl/data').catch(error => {
dispatch({
type: actionTypes.FETCH_OFFLINE_MODE,
payload: {
prevAction: {
type: action.type, // <-- action is the one that triggered your api call
payload: action.payload,
},
},
meta: { retry: true }
})
dispatch(offlineActionCreators.fetchOfflineMode(action)) // <-- action is the one that triggered your api call
);
```
Expand Down
2 changes: 1 addition & 1 deletion src/components/NetworkConnectivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function validateProps(props: Props) {
throw new Error('httpMethod parameter should be either HEAD or OPTIONS');
}
}

/* eslint-disable react/default-props-match-prop-types */
class NetworkConnectivity extends React.PureComponent<Props, State> {
static defaultProps = {
onConnectivityChange: () => undefined,
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ module.exports = {
get offlineActionTypes() {
return require('./redux/actionTypes').default;
},
get offlineActionCreators() {
return require('./redux/actionCreators').default;
},
get networkSaga() {
return require('./redux/sagas').default;
},
Expand Down
17 changes: 17 additions & 0 deletions src/redux/actionCreators.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
FluxActionWithPreviousIntent,
FluxActionForRemoval,
FluxActionForDismissal,
FluxActionForChangeQueueSemaphore,
SemaphoreColor,
} from '../types';

type EnqueuedAction = FluxAction | Function;
Expand Down Expand Up @@ -53,3 +55,18 @@ export const dismissActionsFromQueue = (
type: actionTypes.DISMISS_ACTIONS_FROM_QUEUE,
payload: actionTrigger,
});

export const changeQueueSemaphore = (
semaphoreColor: SemaphoreColor,
): FluxActionForChangeQueueSemaphore => ({
type: actionTypes.CHANGE_QUEUE_SEMAPHORE,
payload: semaphoreColor,
});

export default {
changeQueueSemaphore,
dismissActionsFromQueue,
removeActionFromQueue,
fetchOfflineMode,
connectionChange,
};
2 changes: 2 additions & 0 deletions src/redux/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type ActionTypes = {|
FETCH_OFFLINE_MODE: '@@network-connectivity/FETCH_OFFLINE_MODE',
REMOVE_FROM_ACTION_QUEUE: '@@network-connectivity/REMOVE_FROM_ACTION_QUEUE',
DISMISS_ACTIONS_FROM_QUEUE: '@@network-connectivity/DISMISS_ACTIONS_FROM_QUEUE',
CHANGE_QUEUE_SEMAPHORE: '@@network-connectivity/CHANGE_QUEUE_SEMAPHORE',
|};

const actionTypes: ActionTypes = {
Expand All @@ -13,6 +14,7 @@ const actionTypes: ActionTypes = {
REMOVE_FROM_ACTION_QUEUE: '@@network-connectivity/REMOVE_FROM_ACTION_QUEUE',
DISMISS_ACTIONS_FROM_QUEUE:
'@@network-connectivity/DISMISS_ACTIONS_FROM_QUEUE',
CHANGE_QUEUE_SEMAPHORE: '@@network-connectivity/CHANGE_QUEUE_SEMAPHORE',
};

export default actionTypes;
37 changes: 32 additions & 5 deletions src/redux/createNetworkMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type { NetworkState } from '../types';
import networkActionTypes from './actionTypes';
import wait from '../utils/wait';
import { SEMAPHORE_COLOR } from '../utils/constants';

type MiddlewareAPI<S> = {
dispatch: (action: any) => void,
Expand All @@ -23,6 +24,7 @@ type Arguments = {|
regexActionType: RegExp,
actionTypes: Array<string>,
queueReleaseThrottle: number,
shouldDequeueSelector: (state: State) => boolean,
|};

function validateParams(regexActionType, actionTypes) {
Expand Down Expand Up @@ -70,11 +72,28 @@ function didComeBackOnline(action, wasConnected) {
);
}

export const createReleaseQueue = (getState, next, delay) => async queue => {
function didQueueResume(action, isQueuePaused) {
return (
action.type === networkActionTypes.CHANGE_QUEUE_SEMAPHORE &&
isQueuePaused &&
action.payload === SEMAPHORE_COLOR.GREEN
);
}

export const createReleaseQueue = (
getState,
next,
delay,
shouldDequeueSelector,
) => async queue => {
// eslint-disable-next-line
for (const action of queue) {
const { isConnected } = getState().network;
if (isConnected) {
const state = getState();
const {
network: { isConnected, isQueuePaused },
} = state;

if (isConnected && !isQueuePaused && shouldDequeueSelector(state)) {
next(removeActionFromQueue(action));
next(action);
// eslint-disable-next-line
Expand All @@ -89,15 +108,17 @@ function createNetworkMiddleware({
regexActionType = /FETCH.*REQUEST/,
actionTypes = [],
queueReleaseThrottle = 50,
shouldDequeueSelector = () => true,
}: Arguments = {}) {
return ({ getState }: MiddlewareAPI<State>) => (
next: (action: any) => void,
) => (action: any) => {
const { isConnected, actionQueue } = getState().network;
const { isConnected, actionQueue, isQueuePaused } = getState().network;
const releaseQueue = createReleaseQueue(
getState,
next,
queueReleaseThrottle,
shouldDequeueSelector,
);
validateParams(regexActionType, actionTypes);

Expand All @@ -114,7 +135,13 @@ function createNetworkMiddleware({
}

const isBackOnline = didComeBackOnline(action, isConnected);
if (isBackOnline) {
const hasQueueBeenResumed = didQueueResume(action, isQueuePaused);

const shouldDequeue =
(isBackOnline || (isConnected && hasQueueBeenResumed)) &&
shouldDequeueSelector(getState());

if (shouldDequeue) {
// Dispatching queued actions in order of arrival (if we have any)
next(action);
return releaseQueue(actionQueue);
Expand Down
19 changes: 18 additions & 1 deletion src/redux/createReducer.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
/* @flow */

import { get, without } from 'lodash';
import { SEMAPHORE_COLOR } from '../utils/constants';
import actionTypes from './actionTypes';
import getSimilarActionInQueue from '../utils/getSimilarActionInQueue';
import type {
FluxAction,
FluxActionWithPreviousIntent,
FluxActionForRemoval,
NetworkState,
SemaphoreColor,
} from '../types';

export const initialState = {
isConnected: true,
actionQueue: [],
isQueuePaused: false,
};

function handleOfflineAction(
Expand Down Expand Up @@ -81,8 +84,20 @@ function handleDismissActionsFromQueue(
};
}

function handleChangeQueueSemaphore(
state: NetworkState,
semaphoreColor: SemaphoreColor,
): NetworkState {
return {
...state,
isQueuePaused: semaphoreColor === SEMAPHORE_COLOR.RED,
};
}

export default (comparisonFn: Function = getSimilarActionInQueue) => (
state: NetworkState = initialState,
state: NetworkState = {
...initialState,
},
action: *,
): NetworkState => {
switch (action.type) {
Expand All @@ -97,6 +112,8 @@ export default (comparisonFn: Function = getSimilarActionInQueue) => (
return handleRemoveActionFromQueue(state, action.payload);
case actionTypes.DISMISS_ACTIONS_FROM_QUEUE:
return handleDismissActionsFromQueue(state, action.payload);
case actionTypes.CHANGE_QUEUE_SEMAPHORE:
return handleChangeQueueSemaphore(state, action.payload);
default:
return state;
}
Expand Down
8 changes: 8 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export type State = {
isConnected: boolean,
};

export type SemaphoreColor = 'RED' | 'GREEN';

export type FluxAction = {
type: string,
payload: any,
Expand Down Expand Up @@ -35,9 +37,15 @@ export type FluxActionForDismissal = {
payload: string,
};

export type FluxActionForChangeQueueSemaphore = {
type: string,
payload: SemaphoreColor,
};

export type NetworkState = {
isConnected: boolean,
actionQueue: Array<*>,
isQueuePaused: boolean,
};

export type HTTPMethod = 'HEAD' | 'OPTIONS';
1 change: 1 addition & 0 deletions src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
export const DEFAULT_TIMEOUT = 10000;
export const DEFAULT_PING_SERVER_URL = 'https://www.google.com/';
export const DEFAULT_HTTP_METHOD = 'HEAD';
export const SEMAPHORE_COLOR = { RED: 'RED', GREEN: 'GREEN' };
Loading

0 comments on commit 35c48b8

Please sign in to comment.