Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement control mechanism for queue release #225

Merged
merged 18 commits into from
Oct 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
emanueleDiVizio marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny opt. Let's keep the state in a variable right away since we are calling getState() twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the reason for calling getState inside the releaseQueue loop is that this way the queue release function always has the latest state on every iteration. This way, when CHANGE_QUEUE_SEMAPHORE and CONNECTION_CHANGE are dispatched the new state is reflected straight away inside releaseQueue, since at every iteration we call getState(). On the other hand, if we pass down isQueuePaused and isConnected from middleware function their values will only reflect the state when the releaseQueue was created, and will not update on further state change while the queue is being released. Does that sound right?

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 =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this regrouping, since isBackOnline by itself was already a valid condition?

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I'll implement this 👍

(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';

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { SEMAPHORE_COLOR } from '../utils/constants';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have to keep SemaphoreColor for typing in handleChangeQueueSemaphore (line 88). I'll import SEMAPHORE_COLOR as well.

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';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯


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