Skip to content

Commit 112cd6e

Browse files
authored
Feat: new queueReleaseThrottle optional param for createNetworkMiddleware
1 parent 9a2ca4b commit 112cd6e

16 files changed

+202
-118
lines changed

README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,8 @@ createNetworkMiddleware(config: MiddlewareConfig): ReduxMiddleware
269269

270270
type MiddlewareConfig = {
271271
regexActionType?: RegExp = /FETCH.*REQUEST/,
272-
actionTypes?: Array<string> = []
272+
actionTypes?: Array<string> = [],
273+
queueReleaseThrottle?: number = 50,
273274
}
274275
```
275276

@@ -281,6 +282,8 @@ By default it's configured to intercept actions for fetching data following the
281282

282283
`actionTypes`: array with additional action types to intercept that don't fulfil the RegExp criteria. For instance, it's useful for actions that carry along refreshing data, such as `REFRESH_LIST`.
283284

285+
`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.
286+
284287
##### Thunks Config
285288
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:
286289

@@ -316,7 +319,9 @@ import { createNetworkMiddleware } from 'react-native-offline';
316319
import createSagaMiddleware from 'redux-saga';
317320

318321
const sagaMiddleware = createSagaMiddleware();
319-
const networkMiddleware = createNetworkMiddleware();
322+
const networkMiddleware = createNetworkMiddleware({
323+
queueReleaseThrottle: 200,
324+
});
320325

321326
const store = createStore(
322327
rootReducer,

example/components/ConnectionToggler.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function ConnectionToggler() {
66
return (
77
<DummyNetworkContext.Consumer>
88
{({ toggleConnection }) => (
9-
<View style={{ marginBottom: 30 }}>
9+
<View style={{ marginBottom: 20 }}>
1010
<Button
1111
onPress={toggleConnection}
1212
title="Toggle Internet connection"

example/components/OfflineQueue.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import React from 'react';
22
import { View, StyleSheet, Text, ScrollView } from 'react-native';
33
import { connect } from 'react-redux';
44

5-
function OfflineQueue({ queue }) {
5+
function OfflineQueue({ queue, title }) {
66
return (
77
<View style={{ height: 90, marginVertical: 8 }}>
8-
<Text style={styles.title}>Offline Queue (FIFO)</Text>
8+
<Text style={styles.title}>{title}</Text>
99
<ScrollView
1010
style={{ flex: 1 }}
1111
contentContainerStyle={styles.queue}

example/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"expo": "^32.0.0",
1616
"react": "16.5.0",
1717
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
18-
"react-native-offline": "^4.2.0",
18+
"react-native-offline": "4.3.0",
1919
"react-navigation": "^3.0.9",
2020
"redux": "^4.0.1",
2121
"redux-saga": "0.16.2"

example/redux/createStore.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import createSagaMiddleware from 'redux-saga';
77
import counter from './reducer';
88
import rootSaga from './sagas';
99

10-
export default function createReduxStore({ withSaga = false } = {}) {
10+
export default function createReduxStore({
11+
withSaga = false,
12+
queueReleaseThrottle = 1000,
13+
} = {}) {
1114
const networkMiddleware = createNetworkMiddleware({
1215
regexActionType: /^OTHER/,
1316
actionTypes: ['ADD_ONE', 'SUB_ONE'],
17+
queueReleaseThrottle,
1418
});
1519

1620
const sagaMiddleware = createSagaMiddleware();

example/screens/ReduxScreen.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Platform, View, StyleSheet, Image } from 'react-native';
2+
import { Platform, View, StyleSheet, Image, Text } from "react-native";
33
import { ReduxNetworkProvider } from 'react-native-offline';
44
import { Provider } from 'react-redux';
55

@@ -11,7 +11,7 @@ import Counter from '../components/Counter';
1111
import OfflineQueue from '../components/OfflineQueue';
1212
import ActionButtons from '../components/ActionButtons';
1313

14-
const store = createStore();
14+
const store = createStore({ queueReleaseThrottle: 1000 });
1515

1616
export default class ReduxScreen extends React.Component {
1717
static navigationOptions = {
@@ -42,7 +42,7 @@ export default class ReduxScreen extends React.Component {
4242
<View style={styles.secondSection}>
4343
<ActionButtons />
4444
<View style={styles.offlineQueue}>
45-
<OfflineQueue />
45+
<OfflineQueue title="Offline Queue (FIFO), throttle = 1s" />
4646
</View>
4747
</View>
4848
</View>

example/screens/SagasScreen.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ActionButtons from '../components/ActionButtons';
77
import OfflineQueue from '../components/OfflineQueue';
88
import createStore from '../redux/createStore';
99

10-
const store = createStore({ withSaga: true });
10+
const store = createStore({ withSaga: true, queueReleaseThrottle: 250 });
1111

1212
export default class SettingsScreen extends React.Component {
1313
static navigationOptions = {
@@ -42,7 +42,7 @@ export default class SettingsScreen extends React.Component {
4242
<View style={styles.secondSection}>
4343
<ActionButtons />
4444
<View style={styles.offlineQueue}>
45-
<OfflineQueue />
45+
<OfflineQueue title="Offline Queue (FIFO), throttle = 250ms" />
4646
</View>
4747
</View>
4848
</View>

example/yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -5592,10 +5592,10 @@ react-native-maps@expo/react-native-maps#v0.22.1-exp.0:
55925592
version "0.22.1"
55935593
resolved "https://codeload.github.com/expo/react-native-maps/tar.gz/e6f98ff7272e5d0a7fe974a41f28593af2d77bb2"
55945594

5595-
react-native-offline@^4.2.0:
5596-
version "4.2.0"
5597-
resolved "https://registry.yarnpkg.com/react-native-offline/-/react-native-offline-4.2.0.tgz#baf2337a8126b93e1b9241c6b328bbe1c26a19c0"
5598-
integrity sha512-gFEs5oDEcSeUybnGQ/MlOI3Q9K8rH+cXcMWbyAoLDYnY8K6MCRXwOwfhFuTfKaRrcfMsywjTLcYetfNMMVN1ng==
5595+
react-native-offline@4.3.0:
5596+
version "4.3.0"
5597+
resolved "https://registry.yarnpkg.com/react-native-offline/-/react-native-offline-4.3.0.tgz#6877e4a4b961e230e0a198bce2d0611ca8af9d89"
5598+
integrity sha512-hM+rNGHKpmagseFuhnok/c7uCrMqs70fHRzaqGBCKnTsio6ff3/wb7jomsuRjsR1Iy+DwBws+VUovcfpQAtuBQ==
55995599
dependencies:
56005600
lodash "^4.17.11"
56015601
react-redux "^6.0.0"

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-offline",
3-
"version": "4.2.0",
3+
"version": "4.3.0",
44
"description": "Handy toolbelt to deal with offline mode in React Native applications. Cross-platform, provides a smooth redux integration.",
55
"main": "./src/index.js",
66
"author": "Raul Gomez Acuña <[email protected]> (https://github.com/rgommezz)",
@@ -71,7 +71,7 @@
7171
"dependencies": {
7272
"lodash": "^4.17.11",
7373
"react-redux": "^6.0.0",
74-
"redux":"4.x",
74+
"redux": "4.x",
7575
"redux-saga": "^0.16.2"
7676
},
7777
"jest": {

src/components/ReduxNetworkProvider.js

+1-10
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
type Props = {
1414
dispatch: FluxAction => FluxAction,
1515
isConnected: boolean,
16-
actionQueue: Array<FluxAction>,
1716
pingTimeout?: number,
1817
pingServerUrl?: string,
1918
shouldPing?: boolean,
@@ -36,17 +35,10 @@ class ReduxNetworkProvider extends React.Component<Props> {
3635
};
3736

3837
handleConnectivityChange = (isConnected: boolean) => {
39-
const { isConnected: wasConnected, actionQueue, dispatch } = this.props;
40-
38+
const { isConnected: wasConnected, dispatch } = this.props;
4139
if (isConnected !== wasConnected) {
4240
dispatch(connectionChange(isConnected));
4341
}
44-
// dispatching queued actions in order of arrival (if we have any)
45-
if (!wasConnected && isConnected && actionQueue.length > 0) {
46-
actionQueue.forEach((action: *) => {
47-
dispatch(action);
48-
});
49-
}
5042
};
5143

5244
render() {
@@ -65,7 +57,6 @@ class ReduxNetworkProvider extends React.Component<Props> {
6557
function mapStateToProps(state: { network: NetworkState }) {
6658
return {
6759
isConnected: state.network.isConnected,
68-
actionQueue: state.network.actionQueue,
6960
};
7061
}
7162

src/redux/createNetworkMiddleware.js

+92-36
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
removeActionFromQueue,
77
dismissActionsFromQueue,
88
} from './actionCreators';
9-
import getSimilarActionInQueue from '../utils/getSimilarActionInQueue';
109
import type { NetworkState } from '../types';
10+
import networkActionTypes from './actionTypes';
11+
import wait from '../utils/wait';
1112

1213
type MiddlewareAPI<S> = {
1314
dispatch: (action: any) => void,
@@ -21,59 +22,114 @@ type State = {
2122
type Arguments = {|
2223
regexActionType: RegExp,
2324
actionTypes: Array<string>,
25+
queueReleaseThrottle: number,
2426
|};
2527

28+
function validateParams(regexActionType, actionTypes) {
29+
if ({}.toString.call(regexActionType) !== '[object RegExp]')
30+
throw new Error('You should pass a regex as regexActionType param');
31+
32+
if ({}.toString.call(actionTypes) !== '[object Array]')
33+
throw new Error('You should pass an array as actionTypes param');
34+
}
35+
36+
function findActionToBeDismissed(action, actionQueue) {
37+
return find(actionQueue, (a: *) => {
38+
const actionsToDismiss = get(a, 'meta.dismiss', []);
39+
return actionsToDismiss.includes(action.type);
40+
});
41+
}
42+
43+
function isObjectAndShouldBeIntercepted(action, regexActionType, actionTypes) {
44+
return (
45+
typeof action === 'object' &&
46+
(regexActionType.test(action.type) || actionTypes.includes(action.type))
47+
);
48+
}
49+
50+
function isThunkAndShouldBeIntercepted(action) {
51+
return typeof action === 'function' && action.interceptInOffline === true;
52+
}
53+
54+
function checkIfActionShouldBeIntercepted(
55+
action,
56+
regexActionType,
57+
actionTypes,
58+
) {
59+
return (
60+
isObjectAndShouldBeIntercepted(action, regexActionType, actionTypes) ||
61+
isThunkAndShouldBeIntercepted(action)
62+
);
63+
}
64+
65+
function didComeBackOnline(action, wasConnected) {
66+
return (
67+
action.type === networkActionTypes.CONNECTION_CHANGE &&
68+
!wasConnected &&
69+
action.payload === true
70+
);
71+
}
72+
73+
export const createReleaseQueue = (getState, next, delay) => async queue => {
74+
// eslint-disable-next-line
75+
for (const action of queue) {
76+
const { isConnected } = getState().network;
77+
if (isConnected) {
78+
next(removeActionFromQueue(action));
79+
next(action);
80+
// eslint-disable-next-line
81+
await wait(delay);
82+
} else {
83+
break;
84+
}
85+
}
86+
};
87+
2688
function createNetworkMiddleware({
2789
regexActionType = /FETCH.*REQUEST/,
2890
actionTypes = [],
91+
queueReleaseThrottle = 50,
2992
}: Arguments = {}) {
3093
return ({ getState }: MiddlewareAPI<State>) => (
3194
next: (action: any) => void,
3295
) => (action: any) => {
33-
if ({}.toString.call(regexActionType) !== '[object RegExp]')
34-
throw new Error('You should pass a regex as regexActionType param');
96+
const { isConnected, actionQueue } = getState().network;
97+
const releaseQueue = createReleaseQueue(
98+
getState,
99+
next,
100+
queueReleaseThrottle,
101+
);
102+
validateParams(regexActionType, actionTypes);
35103

36-
if ({}.toString.call(actionTypes) !== '[object Array]')
37-
throw new Error('You should pass an array as actionTypes param');
104+
const shouldInterceptAction = checkIfActionShouldBeIntercepted(
105+
action,
106+
regexActionType,
107+
actionTypes,
108+
);
38109

39-
const { isConnected, actionQueue } = getState().network;
110+
if (shouldInterceptAction && isConnected === false) {
111+
// Offline, preventing the original action from being dispatched.
112+
// Dispatching an internal action instead.
113+
return next(fetchOfflineMode(action));
114+
}
40115

41-
const isObjectAndMatchCondition =
42-
typeof action === 'object' &&
43-
(regexActionType.test(action.type) || actionTypes.includes(action.type));
44-
45-
const isFunctionAndMatchCondition =
46-
typeof action === 'function' && action.interceptInOffline === true;
47-
48-
if (isObjectAndMatchCondition || isFunctionAndMatchCondition) {
49-
if (isConnected === false) {
50-
// Offline, preventing the original action from being dispatched.
51-
// Dispatching an internal action instead.
52-
return next(fetchOfflineMode(action));
53-
}
54-
const actionQueued =
55-
actionQueue.length > 0
56-
? getSimilarActionInQueue(action, actionQueue)
57-
: null;
58-
if (actionQueued) {
59-
// Back online and the action that was queued is about to be dispatched.
60-
// Removing action from queue, prior to handing over to next middleware or final dispatch
61-
next(removeActionFromQueue(action));
62-
63-
return next(action);
64-
}
116+
const isBackOnline = didComeBackOnline(action, isConnected);
117+
if (isBackOnline) {
118+
// Dispatching queued actions in order of arrival (if we have any)
119+
next(action);
120+
return releaseQueue(actionQueue);
65121
}
66122

67-
// We don't want to dispatch actions all the time, but rather when there is a dismissal case
68-
const isAnyActionToBeDismissed = find(actionQueue, (a: *) => {
69-
const actionsToDismiss = get(a, 'meta.dismiss', []);
70-
return actionsToDismiss.includes(action.type);
71-
});
123+
// Checking if we have a dismissal case
124+
const isAnyActionToBeDismissed = findActionToBeDismissed(
125+
action,
126+
actionQueue,
127+
);
72128
if (isAnyActionToBeDismissed && !isConnected) {
73129
next(dismissActionsFromQueue(action.type));
74-
return next(action);
75130
}
76131

132+
// Proxy the original action to the next middleware on the chain or final dispatch
77133
return next(action);
78134
};
79135
}

src/redux/sagas.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,10 @@ export function* checkInternetAccessSaga({
208208
export function* handleConnectivityChange(
209209
hasInternetAccess: boolean,
210210
): Generator<*, *, *> {
211-
const { actionQueue, isConnected } = yield select(networkSelector);
211+
const { isConnected } = yield select(networkSelector);
212212
if (isConnected !== hasInternetAccess) {
213213
yield put(connectionChange(hasInternetAccess));
214214
}
215-
if (hasInternetAccess && actionQueue.length > 0) {
216-
// eslint-disable-next-line
217-
for (const action of actionQueue) {
218-
yield put(action);
219-
}
220-
}
221215
}
222216

223217
/**

src/utils/wait.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const wait = t => new Promise(resolve => setTimeout(resolve, t));
2+
export default wait;

0 commit comments

Comments
 (0)