Skip to content

Commit

Permalink
Feat: new queueReleaseThrottle optional param for createNetworkMiddle…
Browse files Browse the repository at this point in the history
…ware
  • Loading branch information
rgommezz committed Feb 3, 2019
1 parent 9a2ca4b commit 112cd6e
Show file tree
Hide file tree
Showing 16 changed files with 202 additions and 118 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ createNetworkMiddleware(config: MiddlewareConfig): ReduxMiddleware

type MiddlewareConfig = {
regexActionType?: RegExp = /FETCH.*REQUEST/,
actionTypes?: Array<string> = []
actionTypes?: Array<string> = [],
queueReleaseThrottle?: number = 50,
}
```

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

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

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

##### 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 @@ -316,7 +319,9 @@ import { createNetworkMiddleware } from 'react-native-offline';
import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware();
const networkMiddleware = createNetworkMiddleware();
const networkMiddleware = createNetworkMiddleware({
queueReleaseThrottle: 200,
});

const store = createStore(
rootReducer,
Expand Down
2 changes: 1 addition & 1 deletion example/components/ConnectionToggler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function ConnectionToggler() {
return (
<DummyNetworkContext.Consumer>
{({ toggleConnection }) => (
<View style={{ marginBottom: 30 }}>
<View style={{ marginBottom: 20 }}>
<Button
onPress={toggleConnection}
title="Toggle Internet connection"
Expand Down
4 changes: 2 additions & 2 deletions example/components/OfflineQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React from 'react';
import { View, StyleSheet, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux';

function OfflineQueue({ queue }) {
function OfflineQueue({ queue, title }) {
return (
<View style={{ height: 90, marginVertical: 8 }}>
<Text style={styles.title}>Offline Queue (FIFO)</Text>
<Text style={styles.title}>{title}</Text>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={styles.queue}
Expand Down
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"expo": "^32.0.0",
"react": "16.5.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
"react-native-offline": "^4.2.0",
"react-native-offline": "4.3.0",
"react-navigation": "^3.0.9",
"redux": "^4.0.1",
"redux-saga": "0.16.2"
Expand Down
6 changes: 5 additions & 1 deletion example/redux/createStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import createSagaMiddleware from 'redux-saga';
import counter from './reducer';
import rootSaga from './sagas';

export default function createReduxStore({ withSaga = false } = {}) {
export default function createReduxStore({
withSaga = false,
queueReleaseThrottle = 1000,
} = {}) {
const networkMiddleware = createNetworkMiddleware({
regexActionType: /^OTHER/,
actionTypes: ['ADD_ONE', 'SUB_ONE'],
queueReleaseThrottle,
});

const sagaMiddleware = createSagaMiddleware();
Expand Down
6 changes: 3 additions & 3 deletions example/screens/ReduxScreen.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Platform, View, StyleSheet, Image } from 'react-native';
import { Platform, View, StyleSheet, Image, Text } from "react-native";
import { ReduxNetworkProvider } from 'react-native-offline';
import { Provider } from 'react-redux';

Expand All @@ -11,7 +11,7 @@ import Counter from '../components/Counter';
import OfflineQueue from '../components/OfflineQueue';
import ActionButtons from '../components/ActionButtons';

const store = createStore();
const store = createStore({ queueReleaseThrottle: 1000 });

export default class ReduxScreen extends React.Component {
static navigationOptions = {
Expand Down Expand Up @@ -42,7 +42,7 @@ export default class ReduxScreen extends React.Component {
<View style={styles.secondSection}>
<ActionButtons />
<View style={styles.offlineQueue}>
<OfflineQueue />
<OfflineQueue title="Offline Queue (FIFO), throttle = 1s" />
</View>
</View>
</View>
Expand Down
4 changes: 2 additions & 2 deletions example/screens/SagasScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ActionButtons from '../components/ActionButtons';
import OfflineQueue from '../components/OfflineQueue';
import createStore from '../redux/createStore';

const store = createStore({ withSaga: true });
const store = createStore({ withSaga: true, queueReleaseThrottle: 250 });

export default class SettingsScreen extends React.Component {
static navigationOptions = {
Expand Down Expand Up @@ -42,7 +42,7 @@ export default class SettingsScreen extends React.Component {
<View style={styles.secondSection}>
<ActionButtons />
<View style={styles.offlineQueue}>
<OfflineQueue />
<OfflineQueue title="Offline Queue (FIFO), throttle = 250ms" />
</View>
</View>
</View>
Expand Down
8 changes: 4 additions & 4 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5592,10 +5592,10 @@ react-native-maps@expo/react-native-maps#v0.22.1-exp.0:
version "0.22.1"
resolved "https://codeload.github.com/expo/react-native-maps/tar.gz/e6f98ff7272e5d0a7fe974a41f28593af2d77bb2"

react-native-offline@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/react-native-offline/-/react-native-offline-4.2.0.tgz#baf2337a8126b93e1b9241c6b328bbe1c26a19c0"
integrity sha512-gFEs5oDEcSeUybnGQ/MlOI3Q9K8rH+cXcMWbyAoLDYnY8K6MCRXwOwfhFuTfKaRrcfMsywjTLcYetfNMMVN1ng==
react-native-offline@4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/react-native-offline/-/react-native-offline-4.3.0.tgz#6877e4a4b961e230e0a198bce2d0611ca8af9d89"
integrity sha512-hM+rNGHKpmagseFuhnok/c7uCrMqs70fHRzaqGBCKnTsio6ff3/wb7jomsuRjsR1Iy+DwBws+VUovcfpQAtuBQ==
dependencies:
lodash "^4.17.11"
react-redux "^6.0.0"
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-offline",
"version": "4.2.0",
"version": "4.3.0",
"description": "Handy toolbelt to deal with offline mode in React Native applications. Cross-platform, provides a smooth redux integration.",
"main": "./src/index.js",
"author": "Raul Gomez Acuña <raulgdeveloper@gmail.com> (https://github.com/rgommezz)",
Expand Down Expand Up @@ -71,7 +71,7 @@
"dependencies": {
"lodash": "^4.17.11",
"react-redux": "^6.0.0",
"redux":"4.x",
"redux": "4.x",
"redux-saga": "^0.16.2"
},
"jest": {
Expand Down
11 changes: 1 addition & 10 deletions src/components/ReduxNetworkProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
type Props = {
dispatch: FluxAction => FluxAction,
isConnected: boolean,
actionQueue: Array<FluxAction>,
pingTimeout?: number,
pingServerUrl?: string,
shouldPing?: boolean,
Expand All @@ -36,17 +35,10 @@ class ReduxNetworkProvider extends React.Component<Props> {
};

handleConnectivityChange = (isConnected: boolean) => {
const { isConnected: wasConnected, actionQueue, dispatch } = this.props;

const { isConnected: wasConnected, dispatch } = this.props;
if (isConnected !== wasConnected) {
dispatch(connectionChange(isConnected));
}
// dispatching queued actions in order of arrival (if we have any)
if (!wasConnected && isConnected && actionQueue.length > 0) {
actionQueue.forEach((action: *) => {
dispatch(action);
});
}
};

render() {
Expand All @@ -65,7 +57,6 @@ class ReduxNetworkProvider extends React.Component<Props> {
function mapStateToProps(state: { network: NetworkState }) {
return {
isConnected: state.network.isConnected,
actionQueue: state.network.actionQueue,
};
}

Expand Down
128 changes: 92 additions & 36 deletions src/redux/createNetworkMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
removeActionFromQueue,
dismissActionsFromQueue,
} from './actionCreators';
import getSimilarActionInQueue from '../utils/getSimilarActionInQueue';
import type { NetworkState } from '../types';
import networkActionTypes from './actionTypes';
import wait from '../utils/wait';

type MiddlewareAPI<S> = {
dispatch: (action: any) => void,
Expand All @@ -21,59 +22,114 @@ type State = {
type Arguments = {|
regexActionType: RegExp,
actionTypes: Array<string>,
queueReleaseThrottle: number,
|};

function validateParams(regexActionType, actionTypes) {
if ({}.toString.call(regexActionType) !== '[object RegExp]')
throw new Error('You should pass a regex as regexActionType param');

if ({}.toString.call(actionTypes) !== '[object Array]')
throw new Error('You should pass an array as actionTypes param');
}

function findActionToBeDismissed(action, actionQueue) {
return find(actionQueue, (a: *) => {
const actionsToDismiss = get(a, 'meta.dismiss', []);
return actionsToDismiss.includes(action.type);
});
}

function isObjectAndShouldBeIntercepted(action, regexActionType, actionTypes) {
return (
typeof action === 'object' &&
(regexActionType.test(action.type) || actionTypes.includes(action.type))
);
}

function isThunkAndShouldBeIntercepted(action) {
return typeof action === 'function' && action.interceptInOffline === true;
}

function checkIfActionShouldBeIntercepted(
action,
regexActionType,
actionTypes,
) {
return (
isObjectAndShouldBeIntercepted(action, regexActionType, actionTypes) ||
isThunkAndShouldBeIntercepted(action)
);
}

function didComeBackOnline(action, wasConnected) {
return (
action.type === networkActionTypes.CONNECTION_CHANGE &&
!wasConnected &&
action.payload === true
);
}

export const createReleaseQueue = (getState, next, delay) => async queue => {
// eslint-disable-next-line
for (const action of queue) {
const { isConnected } = getState().network;
if (isConnected) {
next(removeActionFromQueue(action));
next(action);
// eslint-disable-next-line
await wait(delay);
} else {
break;
}
}
};

function createNetworkMiddleware({
regexActionType = /FETCH.*REQUEST/,
actionTypes = [],
queueReleaseThrottle = 50,
}: Arguments = {}) {
return ({ getState }: MiddlewareAPI<State>) => (
next: (action: any) => void,
) => (action: any) => {
if ({}.toString.call(regexActionType) !== '[object RegExp]')
throw new Error('You should pass a regex as regexActionType param');
const { isConnected, actionQueue } = getState().network;
const releaseQueue = createReleaseQueue(
getState,
next,
queueReleaseThrottle,
);
validateParams(regexActionType, actionTypes);

if ({}.toString.call(actionTypes) !== '[object Array]')
throw new Error('You should pass an array as actionTypes param');
const shouldInterceptAction = checkIfActionShouldBeIntercepted(
action,
regexActionType,
actionTypes,
);

const { isConnected, actionQueue } = getState().network;
if (shouldInterceptAction && isConnected === false) {
// Offline, preventing the original action from being dispatched.
// Dispatching an internal action instead.
return next(fetchOfflineMode(action));
}

const isObjectAndMatchCondition =
typeof action === 'object' &&
(regexActionType.test(action.type) || actionTypes.includes(action.type));

const isFunctionAndMatchCondition =
typeof action === 'function' && action.interceptInOffline === true;

if (isObjectAndMatchCondition || isFunctionAndMatchCondition) {
if (isConnected === false) {
// Offline, preventing the original action from being dispatched.
// Dispatching an internal action instead.
return next(fetchOfflineMode(action));
}
const actionQueued =
actionQueue.length > 0
? getSimilarActionInQueue(action, actionQueue)
: null;
if (actionQueued) {
// Back online and the action that was queued is about to be dispatched.
// Removing action from queue, prior to handing over to next middleware or final dispatch
next(removeActionFromQueue(action));

return next(action);
}
const isBackOnline = didComeBackOnline(action, isConnected);
if (isBackOnline) {
// Dispatching queued actions in order of arrival (if we have any)
next(action);
return releaseQueue(actionQueue);
}

// We don't want to dispatch actions all the time, but rather when there is a dismissal case
const isAnyActionToBeDismissed = find(actionQueue, (a: *) => {
const actionsToDismiss = get(a, 'meta.dismiss', []);
return actionsToDismiss.includes(action.type);
});
// Checking if we have a dismissal case
const isAnyActionToBeDismissed = findActionToBeDismissed(
action,
actionQueue,
);
if (isAnyActionToBeDismissed && !isConnected) {
next(dismissActionsFromQueue(action.type));
return next(action);
}

// Proxy the original action to the next middleware on the chain or final dispatch
return next(action);
};
}
Expand Down
8 changes: 1 addition & 7 deletions src/redux/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,10 @@ export function* checkInternetAccessSaga({
export function* handleConnectivityChange(
hasInternetAccess: boolean,
): Generator<*, *, *> {
const { actionQueue, isConnected } = yield select(networkSelector);
const { isConnected } = yield select(networkSelector);
if (isConnected !== hasInternetAccess) {
yield put(connectionChange(hasInternetAccess));
}
if (hasInternetAccess && actionQueue.length > 0) {
// eslint-disable-next-line
for (const action of actionQueue) {
yield put(action);
}
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/utils/wait.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const wait = t => new Promise(resolve => setTimeout(resolve, t));
export default wait;
Loading

0 comments on commit 112cd6e

Please sign in to comment.