Skip to content

Commit 35c48b8

Browse files
emanueleDiViziorgommezz
authored andcommitted
feat: Implement control mechanism for queue release (#225)
1 parent 6b37ce3 commit 35c48b8

11 files changed

+231
-41
lines changed

README.md

+31-24
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ type MiddlewareConfig = {
347347
regexActionType?: RegExp = /FETCH.*REQUEST/,
348348
actionTypes?: Array<string> = [],
349349
queueReleaseThrottle?: number = 50,
350+
shouldDequeueSelector: (state: RootReduxState) => boolean = () => true
350351
}
351352
```
352353

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

361362
`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.
362363

364+
`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.
365+
366+
363367
##### Thunks Config
364368
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:
365369

@@ -551,16 +555,14 @@ checkInternetConnection(
551555
##### Example
552556

553557
```js
554-
import { checkInternetConnection, offlineActionTypes } from 'react-native-offline';
558+
import { checkInternetConnection, offlineActionCreators } from 'react-native-offline';
555559

556560
async function internetChecker(dispatch) {
557561
const isConnected = await checkInternetConnection();
562+
const { connectionChange } = offlineActionCreators;
558563
// Dispatching can be done inside a connected component, a thunk (where dispatch is injected), saga, or any sort of middleware
559564
// In this example we are using a thunk
560-
dispatch({
561-
type: offlineActionTypes.CONNECTION_CHANGE,
562-
payload: isConnected,
563-
});
565+
dispatch(connectionChange(isConnected));
564566
}
565567
```
566568

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

590592
const networkMiddleware = createNetworkMiddleware();
591593

592594
export default function configureStore(callback) {
593595
const store = createStore(rootReducer, applyMiddleware(networkMiddleware));
596+
const { connectionChange } = offlineActionCreators;
594597
// https://github.com/rt2zz/redux-persist#persiststorestore-config-callback
595598
persistStore(store, null, () => {
596599
// After rehydration completes, we detect initial connection
597600
checkInternetConnection().then(isConnected => {
598-
store.dispatch({
599-
type: offlineActionTypes.CONNECTION_CHANGE,
600-
payload: isConnected,
601-
});
601+
store.dispatch(connectionChange(isConnected));
602602
callback(); // Notify our root component we are good to go, so that we can render our app
603603
});
604604
});
@@ -641,25 +641,32 @@ export default App;
641641

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

644-
#### How to intercept and queue actions when the server responds with client (4xx) or server (5xx) errors
645-
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.
644+
#### How do I stop the queue *while* it is being released?
645+
646+
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.
646647

647-
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.
648+
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.
648649

649650
```js
650-
import { offlineActionTypes } from 'react-native-offline';
651+
import { offlineActionCreators } from 'react-native-offline';
652+
...
653+
async function weHaltQeueeReleaseHere(){
654+
const { changeQueueSemaphore } = offlineActionCreators;
655+
dispatch(changeQueueSemaphore('RED')) // The queue is now halted and it won't continue dispatching actions
656+
await somePromise();
657+
dispatch(changeQueueSemaphore('GREEN')) // The queue is now resumed and it will continue dispatching actions
658+
}
659+
```
660+
661+
662+
#### How to intercept and queue actions when the server responds with client (4xx) or server (5xx) errors
663+
You can do that by dispatching a `FETCH_OFFLINE_MODE` action using `fetchOfflineMode` action creator.
664+
665+
```js
666+
import { offlineActionCreators } from 'react-native-offline';
651667
...
652668
fetch('someurl/data').catch(error => {
653-
dispatch({
654-
type: actionTypes.FETCH_OFFLINE_MODE,
655-
payload: {
656-
prevAction: {
657-
type: action.type, // <-- action is the one that triggered your api call
658-
payload: action.payload,
659-
},
660-
},
661-
meta: { retry: true }
662-
})
669+
dispatch(offlineActionCreators.fetchOfflineMode(action)) // <-- action is the one that triggered your api call
663670
);
664671
```
665672

src/components/NetworkConnectivity.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function validateProps(props: Props) {
5656
throw new Error('httpMethod parameter should be either HEAD or OPTIONS');
5757
}
5858
}
59-
59+
/* eslint-disable react/default-props-match-prop-types */
6060
class NetworkConnectivity extends React.PureComponent<Props, State> {
6161
static defaultProps = {
6262
onConnectivityChange: () => undefined,

src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ module.exports = {
2020
get offlineActionTypes() {
2121
return require('./redux/actionTypes').default;
2222
},
23+
get offlineActionCreators() {
24+
return require('./redux/actionCreators').default;
25+
},
2326
get networkSaga() {
2427
return require('./redux/sagas').default;
2528
},

src/redux/actionCreators.js

+17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type {
66
FluxActionWithPreviousIntent,
77
FluxActionForRemoval,
88
FluxActionForDismissal,
9+
FluxActionForChangeQueueSemaphore,
10+
SemaphoreColor,
911
} from '../types';
1012

1113
type EnqueuedAction = FluxAction | Function;
@@ -53,3 +55,18 @@ export const dismissActionsFromQueue = (
5355
type: actionTypes.DISMISS_ACTIONS_FROM_QUEUE,
5456
payload: actionTrigger,
5557
});
58+
59+
export const changeQueueSemaphore = (
60+
semaphoreColor: SemaphoreColor,
61+
): FluxActionForChangeQueueSemaphore => ({
62+
type: actionTypes.CHANGE_QUEUE_SEMAPHORE,
63+
payload: semaphoreColor,
64+
});
65+
66+
export default {
67+
changeQueueSemaphore,
68+
dismissActionsFromQueue,
69+
removeActionFromQueue,
70+
fetchOfflineMode,
71+
connectionChange,
72+
};

src/redux/actionTypes.js

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type ActionTypes = {|
55
FETCH_OFFLINE_MODE: '@@network-connectivity/FETCH_OFFLINE_MODE',
66
REMOVE_FROM_ACTION_QUEUE: '@@network-connectivity/REMOVE_FROM_ACTION_QUEUE',
77
DISMISS_ACTIONS_FROM_QUEUE: '@@network-connectivity/DISMISS_ACTIONS_FROM_QUEUE',
8+
CHANGE_QUEUE_SEMAPHORE: '@@network-connectivity/CHANGE_QUEUE_SEMAPHORE',
89
|};
910

1011
const actionTypes: ActionTypes = {
@@ -13,6 +14,7 @@ const actionTypes: ActionTypes = {
1314
REMOVE_FROM_ACTION_QUEUE: '@@network-connectivity/REMOVE_FROM_ACTION_QUEUE',
1415
DISMISS_ACTIONS_FROM_QUEUE:
1516
'@@network-connectivity/DISMISS_ACTIONS_FROM_QUEUE',
17+
CHANGE_QUEUE_SEMAPHORE: '@@network-connectivity/CHANGE_QUEUE_SEMAPHORE',
1618
};
1719

1820
export default actionTypes;

src/redux/createNetworkMiddleware.js

+32-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import type { NetworkState } from '../types';
1010
import networkActionTypes from './actionTypes';
1111
import wait from '../utils/wait';
12+
import { SEMAPHORE_COLOR } from '../utils/constants';
1213

1314
type MiddlewareAPI<S> = {
1415
dispatch: (action: any) => void,
@@ -23,6 +24,7 @@ type Arguments = {|
2324
regexActionType: RegExp,
2425
actionTypes: Array<string>,
2526
queueReleaseThrottle: number,
27+
shouldDequeueSelector: (state: State) => boolean,
2628
|};
2729

2830
function validateParams(regexActionType, actionTypes) {
@@ -70,11 +72,28 @@ function didComeBackOnline(action, wasConnected) {
7072
);
7173
}
7274

73-
export const createReleaseQueue = (getState, next, delay) => async queue => {
75+
function didQueueResume(action, isQueuePaused) {
76+
return (
77+
action.type === networkActionTypes.CHANGE_QUEUE_SEMAPHORE &&
78+
isQueuePaused &&
79+
action.payload === SEMAPHORE_COLOR.GREEN
80+
);
81+
}
82+
83+
export const createReleaseQueue = (
84+
getState,
85+
next,
86+
delay,
87+
shouldDequeueSelector,
88+
) => async queue => {
7489
// eslint-disable-next-line
7590
for (const action of queue) {
76-
const { isConnected } = getState().network;
77-
if (isConnected) {
91+
const state = getState();
92+
const {
93+
network: { isConnected, isQueuePaused },
94+
} = state;
95+
96+
if (isConnected && !isQueuePaused && shouldDequeueSelector(state)) {
7897
next(removeActionFromQueue(action));
7998
next(action);
8099
// eslint-disable-next-line
@@ -89,15 +108,17 @@ function createNetworkMiddleware({
89108
regexActionType = /FETCH.*REQUEST/,
90109
actionTypes = [],
91110
queueReleaseThrottle = 50,
111+
shouldDequeueSelector = () => true,
92112
}: Arguments = {}) {
93113
return ({ getState }: MiddlewareAPI<State>) => (
94114
next: (action: any) => void,
95115
) => (action: any) => {
96-
const { isConnected, actionQueue } = getState().network;
116+
const { isConnected, actionQueue, isQueuePaused } = getState().network;
97117
const releaseQueue = createReleaseQueue(
98118
getState,
99119
next,
100120
queueReleaseThrottle,
121+
shouldDequeueSelector,
101122
);
102123
validateParams(regexActionType, actionTypes);
103124

@@ -114,7 +135,13 @@ function createNetworkMiddleware({
114135
}
115136

116137
const isBackOnline = didComeBackOnline(action, isConnected);
117-
if (isBackOnline) {
138+
const hasQueueBeenResumed = didQueueResume(action, isQueuePaused);
139+
140+
const shouldDequeue =
141+
(isBackOnline || (isConnected && hasQueueBeenResumed)) &&
142+
shouldDequeueSelector(getState());
143+
144+
if (shouldDequeue) {
118145
// Dispatching queued actions in order of arrival (if we have any)
119146
next(action);
120147
return releaseQueue(actionQueue);

src/redux/createReducer.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
/* @flow */
22

33
import { get, without } from 'lodash';
4+
import { SEMAPHORE_COLOR } from '../utils/constants';
45
import actionTypes from './actionTypes';
56
import getSimilarActionInQueue from '../utils/getSimilarActionInQueue';
67
import type {
78
FluxAction,
89
FluxActionWithPreviousIntent,
910
FluxActionForRemoval,
1011
NetworkState,
12+
SemaphoreColor,
1113
} from '../types';
1214

1315
export const initialState = {
1416
isConnected: true,
1517
actionQueue: [],
18+
isQueuePaused: false,
1619
};
1720

1821
function handleOfflineAction(
@@ -81,8 +84,20 @@ function handleDismissActionsFromQueue(
8184
};
8285
}
8386

87+
function handleChangeQueueSemaphore(
88+
state: NetworkState,
89+
semaphoreColor: SemaphoreColor,
90+
): NetworkState {
91+
return {
92+
...state,
93+
isQueuePaused: semaphoreColor === SEMAPHORE_COLOR.RED,
94+
};
95+
}
96+
8497
export default (comparisonFn: Function = getSimilarActionInQueue) => (
85-
state: NetworkState = initialState,
98+
state: NetworkState = {
99+
...initialState,
100+
},
86101
action: *,
87102
): NetworkState => {
88103
switch (action.type) {
@@ -97,6 +112,8 @@ export default (comparisonFn: Function = getSimilarActionInQueue) => (
97112
return handleRemoveActionFromQueue(state, action.payload);
98113
case actionTypes.DISMISS_ACTIONS_FROM_QUEUE:
99114
return handleDismissActionsFromQueue(state, action.payload);
115+
case actionTypes.CHANGE_QUEUE_SEMAPHORE:
116+
return handleChangeQueueSemaphore(state, action.payload);
100117
default:
101118
return state;
102119
}

src/types.js

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export type State = {
44
isConnected: boolean,
55
};
66

7+
export type SemaphoreColor = 'RED' | 'GREEN';
8+
79
export type FluxAction = {
810
type: string,
911
payload: any,
@@ -35,9 +37,15 @@ export type FluxActionForDismissal = {
3537
payload: string,
3638
};
3739

40+
export type FluxActionForChangeQueueSemaphore = {
41+
type: string,
42+
payload: SemaphoreColor,
43+
};
44+
3845
export type NetworkState = {
3946
isConnected: boolean,
4047
actionQueue: Array<*>,
48+
isQueuePaused: boolean,
4149
};
4250

4351
export type HTTPMethod = 'HEAD' | 'OPTIONS';

src/utils/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
export const DEFAULT_TIMEOUT = 10000;
33
export const DEFAULT_PING_SERVER_URL = 'https://www.google.com/';
44
export const DEFAULT_HTTP_METHOD = 'HEAD';
5+
export const SEMAPHORE_COLOR = { RED: 'RED', GREEN: 'GREEN' };

0 commit comments

Comments
 (0)