├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src ├── actions.js ├── config.js ├── consumeActionMiddleware.js ├── index.js ├── initialState.js ├── offlineActions.js ├── offlineMiddleware.js ├── offlinePersistenceTransform.js ├── reducer.js └── suspendSaga.js ├── tests └── offlineActions.spec.js ├── typings.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "semi": [2, "never"], 6 | "arrow-body-style": [0, "as-needed"] 7 | }, 8 | "env": { 9 | "jest": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules 3 | yarn-error.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | .babelrc 4 | .eslintrc 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Original work Copyright (c) 2018-2019 Inspire Innovation BV (Utrecht, The Netherlands). 4 | Continued work Copyright (c) 2019 Roberto Pando. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-offline-queue 2 | 3 | This package is a simple solution for handling actions or requests with redux while the app is in an offline state by queueing these, and dispatching them once connectivity is re-established. **Works perfect with react-native** 4 | 5 | Motivation: Provide a better user experience. 6 | 7 | - [Installation](#installation) 8 | - [Usage](#usage) 9 | - [Compatibility](#compatibility) 10 | - [Additional Configuration](#additional-configuration) 11 | 12 | ## Installation 13 | 14 | `yarn add redux-offline-queue` 15 | 16 | OR (old school) 17 | 18 | `npm install --save redux-offline-queue` 19 | 20 | ## Usage 21 | 22 | **See example project here:** [offlineTweet](https://github.com/RobPando/offlineTweet) 23 | 24 | Get up and running in 4 easy steps: 25 | 26 | ### Step 1: Add the redux-offline-queue reducer to your combine reducers 27 | 28 | Either import the `{ reducer as offline }` from `redux-offline-queue` and add it to the `combineReducers` or require it like so (whatever floats your boat): 29 | 30 | ```javascript 31 | import { combineReducers } from 'redux' 32 | 33 | export default combineReducers({ 34 | offline: require('redux-offline-queue').reducer, 35 | yourOtherReducer: require('~App/yourOtherReducer').reducer, 36 | }) 37 | ``` 38 | 39 | ### Step 2: Add the offlineMiddleware 40 | 41 | ```javascript 42 | import { offlineMiddleware } from 'redux-offline-queue' 43 | 44 | const composeStoreWithMiddleware = applyMiddleware( 45 | offlineMiddleware() 46 | )(createStore) 47 | ``` 48 | 49 | **Note** that this queue is not persisted by itself. One should provide a persistence config by using e.g. `redux-persist` to keep the offline queue persisted. 50 | 51 | ### Step 3: Declare the actions to be queued 52 | 53 | #### With `reduxsauce` 54 | 55 | ```javascript 56 | import { createReducer, createActions } from 'reduxsauce' 57 | import { markActionsOffline } from 'redux-offline-queue' 58 | 59 | const { Types, Creators } = createActions({ 60 | requestBlogs: null, 61 | createBlog: ['blog'], 62 | }) 63 | 64 | markActionsOffline(Creators, ['createBlog']) 65 | ... 66 | ``` 67 | 68 | #### Without 69 | 70 | ```javascript 71 | import { markActionsOffline } from 'redux-offline-queue' 72 | 73 | const Creators = { 74 | createBlog: blog => ({ 75 | type: 'CREATE_BLOG', 76 | blog, 77 | }), 78 | } 79 | 80 | markActionsOffline(Creators, ['createBlog']) 81 | ... 82 | ``` 83 | 84 | Last but not least... 85 | 86 | ### Step 4: Monitor the connectivity and let the library know. 87 | 88 | ```javascript 89 | import { OFFLINE, ONLINE } from 'redux-offline-queue' 90 | 91 | if (appIsConnected) { 92 | dispatch({ type: ONLINE }) 93 | } else { 94 | dispatch({ type: OFFLINE }) 95 | } 96 | ``` 97 | 98 | Works perfect with React Native's `NetInfo` 99 | 100 | ```javascript 101 | import { put, take, call } from 'redux-saga/effects' 102 | import NetInfo from '@react-native-community/netinfo' 103 | import { OFFLINE, ONLINE } from 'redux-offline-queue' 104 | 105 | function* startWatchingNetworkConnectivity() { 106 | const channel = eventChannel((emitter) => { 107 | NetInfo.isConnected.addEventListener('connectionChange', emitter) 108 | return () => NetInfo.isConnected.removeEventListener('connectionChange', emitter) 109 | }) 110 | try { 111 | while(true) { 112 | const isConnected = yield take(channel) 113 | if (isConnected) { 114 | yield put({ type: ONLINE }) 115 | } else { 116 | yield put({ type: OFFLINE }) 117 | } 118 | } 119 | } finally { 120 | channel.close() 121 | } 122 | } 123 | ``` 124 | 125 | **Android** 126 | 127 | If react native's `NetInfo` is intended to be used, for android don't forget to add the following to the `AndroidManifest.xml` : 128 | ```xml 129 | 130 | ``` 131 | 132 | Inspired by redux-queue-offline(mathieudutour) 133 | 134 | Developed by Krzysztof Ciombor 135 | 136 | ## Compatibility 137 | 138 | ### with `redux-saga` 139 | 140 | If you are using `redux-sagas` for http requests and want to fire your redux actions normally, but suspend(queue) sagas, for Step 2, do the following instead: 141 | 142 | ```javascript 143 | import { applyMiddleware } from 'redux' 144 | import createSagaMiddleware from 'redux-saga' 145 | import { 146 | offlineMiddleware, 147 | suspendSaga, 148 | consumeActionMiddleware, 149 | } from 'redux-offline-queue' 150 | 151 | const middleware = [] 152 | 153 | middleware.push(offlineMiddleware()) 154 | const suspendSagaMiddleware = suspendSaga(createSagaMiddleware()) 155 | middleware.push(suspendSagaMiddleware) 156 | middleware.push(consumeActionMiddleware()) 157 | 158 | applyMiddleware(...middleware) 159 | ``` 160 | 161 | It is **IMPORTANT** that the `consumeActionMiddleware` is placed last, so you can allow the previous middlewares to react first before eventually getting consumed. 162 | 163 | ## Additional Configuration 164 | 165 | Additional configuration can be passed with `offlineMiddleware()`, such as adding additional triggers that will trigger the offline queue to dispatch its actions: 166 | 167 | ```javascript 168 | ... 169 | import { REHYDRATE } from 'redux-persist' 170 | 171 | applyMiddleware(offlineMiddleware({ 172 | additionalTriggers: REHYDRATE, 173 | })) 174 | ... 175 | ``` 176 | 177 | ## Contributing 178 | 179 | Bug reports and pull requests are welcome. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://www.contributor-covenant.org) code of conduct. 180 | 181 | ## License 182 | 183 | Original work copyright 2018-2019 [Inspire Innovation BV](https://inspire.nl). 184 | Continued work copyright 2019 Roberto Pando. 185 | 186 | Read [LICENSE](LICENSE) for details. 187 | 188 | The development of this package has been sponsored by Inspire Innovation BV (Utrecht, The Netherlands). 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-offline-queue", 3 | "version": "1.1.2", 4 | "description": "Simple offline queue for redux, inspired by redux-queue-offline.", 5 | "main": "lib/index.js", 6 | "types": "./typings.d.ts", 7 | "scripts": { 8 | "build": "babel src --out-dir lib", 9 | "test": "jest" 10 | }, 11 | "keywords": [ 12 | "redux", 13 | "offline", 14 | "queue", 15 | "react native offline" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/RobPando/redux-offline-queue.git" 20 | }, 21 | "author": "Rob Pando", 22 | "license": "MIT", 23 | "dependencies": { 24 | "lodash": "^4.17.21", 25 | "redux-persist": "^6.0.0", 26 | "reduxsauce": "^1.1.2", 27 | "uuid": "^8.3.2" 28 | }, 29 | "devDependencies": { 30 | "@types/redux": "^3.6.0", 31 | "babel-cli": "^6.26.0", 32 | "babel-core": "^6.26.3", 33 | "babel-eslint": "^10.0.3", 34 | "babel-jest": "^25.1.0", 35 | "babel-preset-env": "^1.7.0", 36 | "babel-preset-stage-0": "^6.24.1", 37 | "eslint": "^6.8.0", 38 | "eslint-config-airbnb": "^18.0.1", 39 | "eslint-plugin-import": "^2.20.1", 40 | "eslint-plugin-jsx-a11y": "^6.2.3", 41 | "eslint-plugin-react": "^7.18.3", 42 | "jest": "^25.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | const ACTION_PREFIX = 'redux-offline-queue/' 2 | 3 | /** 4 | * External actions. 5 | * Should be called from the outside to property set the connection state. 6 | * 7 | * We're doing it this way to not couple tighly with react-native and make it possible 8 | * to use the queue in a different environment. 9 | */ 10 | export const ONLINE = `${ACTION_PREFIX}ONLINE` 11 | export const OFFLINE = `${ACTION_PREFIX}OFFLINE` 12 | 13 | /** 14 | * Internal actions. 15 | * These are fired to manage the internal offline queue state. 16 | */ 17 | export const QUEUE_ACTION = `${ACTION_PREFIX}QUEUE_ACTION` 18 | export const REMOVE_ACTION = `${ACTION_PREFIX}REMOVE_ACTION` 19 | export const RESET_QUEUE = `${ACTION_PREFIX}RESET_QUEUE` 20 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default config for the offline queue. 3 | * 4 | * @param {String} stateName Redux store key for offline queue state. 5 | * @param {Array} additionalTriggers An array of action types 6 | * that will trigger the offline queue to dispatch its actions if possible. 7 | */ 8 | const DEFAULT_CONFIG = { 9 | stateName: 'offline', 10 | additionalTriggers: [], 11 | } 12 | 13 | /** 14 | * Returns a configuration options with passed config or default values. 15 | * 16 | * @param {Object} userConfig A config object that can be used to override default values. 17 | */ 18 | export default function getConfig(userConfig = {}) { 19 | return { ...DEFAULT_CONFIG, ...userConfig } 20 | } 21 | -------------------------------------------------------------------------------- /src/consumeActionMiddleware.js: -------------------------------------------------------------------------------- 1 | import { get as _get } from 'lodash' 2 | 3 | /** 4 | * Custom middleware that can consume the action before it can reach the reducer. 5 | * 6 | * This is useful when we want to optimistically update the local state, 7 | * but the same action will be dispatched again when it is fired from the offline queue. 8 | * To avoid updating the state again we change its type to one no reducer reacts to. 9 | * 10 | * For the action to be consumed it should have: 11 | * ``` 12 | * consume: true 13 | * ``` 14 | * property set. 15 | * 16 | * Note: For this to work correctly it should be placed as the last middleware in the chain. 17 | * For example, we do want the saga or logger to react to this action. 18 | */ 19 | export default function consumeActionMiddleware() { 20 | return (store) => (next) => (action) => { 21 | const shouldConsumeAction = _get(action, 'consume', false) 22 | if (shouldConsumeAction) { 23 | return next({ type: '@@CONSUME@@', payload: { ...action } }) 24 | } 25 | return next(action) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import offlineMiddleware from './offlineMiddleware' 3 | import { createOfflineActions, markActionsOffline, queueAction, removeAction } from './offlineActions' 4 | import reducer from './reducer' 5 | import suspendSaga from './suspendSaga' 6 | import consumeActionMiddleware from './consumeActionMiddleware' 7 | import offlinePersistenceTransform from './offlinePersistenceTransform' 8 | 9 | module.exports = { 10 | ONLINE: actions.ONLINE, 11 | OFFLINE: actions.OFFLINE, 12 | createOfflineActions, 13 | offlineMiddleware, 14 | markActionsOffline, 15 | queueAction, 16 | removeAction, 17 | reducer, 18 | suspendSaga, 19 | consumeActionMiddleware, 20 | offlinePersistenceTransform, 21 | } 22 | -------------------------------------------------------------------------------- /src/initialState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initial state for the offline queue. 3 | * 4 | * @param {Array} queue Keeps an array of redux actions that are queued in the offline mode. 5 | * @param {Boolean} isConnected Boolean indicating if the device is connected to the Internet. 6 | */ 7 | export default { 8 | queue: [], 9 | isConnected: true, 10 | } 11 | -------------------------------------------------------------------------------- /src/offlineActions.js: -------------------------------------------------------------------------------- 1 | import { createActions } from 'reduxsauce' 2 | import { 3 | mapValues as _mapValues, 4 | forEach as _forEach, 5 | has as _has, 6 | } from 'lodash' 7 | import { v4 as uuid } from 'uuid' 8 | 9 | import { QUEUE_ACTION, REMOVE_ACTION } from "./actions" 10 | 11 | /** 12 | * Wraps reduxsauce's creator function to append offline metadata. 13 | * 14 | * @param {Function} creator Reduxsauce's creator function. 15 | */ 16 | const appendOfflineMeta = (creator) => { 17 | return (...rest) => { 18 | const creatorResult = creator(...rest) 19 | 20 | return { 21 | ...creatorResult, 22 | meta: { 23 | uuid: uuid(), 24 | ...creatorResult.meta, 25 | queueIfOffline: true, 26 | }, 27 | } 28 | } 29 | } 30 | 31 | /** 32 | * Custom wrapper around reduxsauce's `createActions` that automatically appends 33 | * offline meta required by offline queue. 34 | * 35 | * Sample usage: 36 | * ``` 37 | * const { Types: OfflineTypes, Creators: OfflineCreators } = createOfflineActions({ 38 | * updateUser: ['userId'], 39 | * }) 40 | * ``` 41 | * 42 | * @param {Object} config Reduxsauce configuration object with action definitions. 43 | */ 44 | export function createOfflineActions(config) { 45 | const { Types, Creators } = createActions(config) 46 | 47 | const OfflineCreators = _mapValues(Creators, (creator) => { 48 | return appendOfflineMeta(creator) 49 | }) 50 | 51 | return { 52 | Types, 53 | Creators: OfflineCreators, 54 | } 55 | } 56 | 57 | /** 58 | * Provides an alternative way to mark an action as offline action. 59 | * 60 | * Modifies given action creators object 61 | * by appending offline meta to specified action names. 62 | * 63 | * This is useful as it does not require merging back Creators and OfflineCreators. 64 | * 65 | * @param {Object} creators Reduxsauce's action creators. 66 | * @param {Array} offlineActions An array of action names. 67 | */ 68 | export function markActionsOffline(creators, offlineActions) { 69 | _forEach(offlineActions, (offlineAction) => { 70 | if (_has(creators, offlineAction)) { 71 | // eslint-disable-next-line no-param-reassign 72 | creators[offlineAction] = appendOfflineMeta(creators[offlineAction]) 73 | } 74 | }) 75 | } 76 | 77 | /** 78 | * Provides an object with the action type that is utilized to queue request actions. 79 | * The action provided should include a type and the payload. 80 | * 81 | * @param {Object} action An action that needs to be queued. 82 | */ 83 | export const queueAction = (action) => { 84 | return { 85 | type: QUEUE_ACTION, 86 | payload: { 87 | ...action, 88 | meta: { 89 | uuid: uuid(), 90 | ...action.meta, 91 | }, 92 | } 93 | } 94 | } 95 | 96 | export const removeAction = (action) => { 97 | return { 98 | type: REMOVE_ACTION, 99 | payload: { 100 | uuid: action.uuid, 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/offlineMiddleware.js: -------------------------------------------------------------------------------- 1 | import { 2 | includes as _includes, 3 | get as _get, 4 | } from 'lodash' 5 | import { v4 as uuid } from 'uuid' 6 | 7 | import INITIAL_STATE from './initialState' 8 | import { QUEUE_ACTION, ONLINE, RESET_QUEUE } from './actions' 9 | import getConfig from './config' 10 | 11 | /** 12 | * Helper method to dispatch the queued action again when the connection is available. 13 | * 14 | * It will modify the original action by adding: 15 | * ``` 16 | * consume: true 17 | * ``` 18 | * to skip firing the reducer 19 | * and: 20 | * ``` 21 | * meta: { 22 | * queueIfOffline: false 23 | * } 24 | * ``` 25 | * to avoid putting it back to the queue. 26 | * 27 | * @param {Array} queue An array of queued Redux actions. 28 | * @param {Function} dispatch Redux's dispatch function. 29 | */ 30 | function fireQueuedActions(queue, dispatch) { 31 | queue.forEach((actionInQueue) => { 32 | dispatch({ 33 | ...actionInQueue, 34 | consume: true, 35 | meta: { 36 | ...actionInQueue.meta, 37 | queueIfOffline: false, 38 | }, 39 | }) 40 | }) 41 | } 42 | 43 | /** 44 | * Custom Redux middleware for providing an offline queue functionality. 45 | * 46 | * Every action that should be queued if the device is offline should have: 47 | * ``` 48 | * meta: { 49 | * queueIfOffline: true 50 | * } 51 | * ``` 52 | * property set. 53 | * 54 | * When the device is online this just passes the action to the next middleware as is. 55 | * 56 | * When the device is offline this action will be placed in an offline queue. 57 | * Those actions are later dispatched again when the device comes online. 58 | * Note that this action is still dispatched to make the optimistic updates possible. 59 | * However it wil have `skipSaga: true` property set 60 | * for the `suspendSaga` wrapper to skip the corresponding saga. 61 | * 62 | * Note that this queue is not persisted by itself. 63 | * One should provide a persistence config by using e.g. 64 | * `redux-persist` to keep the offline queue persisted. 65 | * 66 | * @param {Object} userConfig See: config.js for the configuration options. 67 | */ 68 | export default function offlineMiddleware(userConfig = {}) { 69 | return ({ getState, dispatch }) => (next) => (action) => { 70 | const config = getConfig(userConfig) 71 | const { stateName, additionalTriggers } = config 72 | 73 | const state = _get(getState(), stateName, INITIAL_STATE) 74 | 75 | const { isConnected } = state 76 | 77 | if (action.type === ONLINE || _includes(additionalTriggers, action.type)) { 78 | const result = next(action) 79 | const { queue } = _get(getState(), stateName) 80 | const canFireQueue = isConnected || action.type === ONLINE 81 | if (canFireQueue) { 82 | fireQueuedActions(queue, dispatch) 83 | dispatch({ type: RESET_QUEUE }) 84 | } 85 | return result 86 | } 87 | 88 | const shouldQueue = _get(action, ['meta', 'queueIfOffline'], false) 89 | 90 | if (isConnected || !shouldQueue) { 91 | return next(action) 92 | } 93 | 94 | const actionToQueue = { 95 | type: QUEUE_ACTION, 96 | payload: { 97 | ...action, 98 | meta: { 99 | uuid: uuid(), 100 | ...action.meta, 101 | } 102 | }, 103 | } 104 | 105 | dispatch(actionToQueue) 106 | 107 | const skipSagaAction = { 108 | ...action, 109 | skipSaga: true, 110 | } 111 | 112 | return next(skipSagaAction) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/offlinePersistenceTransform.js: -------------------------------------------------------------------------------- 1 | import { createTransform } from 'redux-persist' 2 | import { omit as _omit } from 'lodash' 3 | 4 | const OMIT_KEYS = ['isConnected'] 5 | 6 | /** 7 | * Custom redux-persist transformation 8 | * to omit persisting `isConnected` key from offline queue. 9 | */ 10 | export default createTransform( 11 | (inboundState) => _omit(inboundState, OMIT_KEYS), 12 | (outboundState) => outboundState, 13 | { whitelist: ['offline'] }, 14 | ) 15 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { REHYDRATE } from 'redux-persist' 2 | 3 | import INITIAL_STATE from './initialState' 4 | import { 5 | QUEUE_ACTION, 6 | ONLINE, 7 | OFFLINE, 8 | RESET_QUEUE, 9 | REMOVE_ACTION, 10 | } from './actions' 11 | 12 | /** 13 | * Reducer for the offline queue. 14 | * 15 | * @param {Object} state Offline queue Redux store state. 16 | * @param {Object} action Action that was dispatched to the store. 17 | */ 18 | export default function reducer(state = INITIAL_STATE, action = {}) { 19 | switch (action.type) { 20 | case REHYDRATE: { 21 | // Handle rehydrating with custom shallow merge. 22 | if (action.payload && action.payload.offline) { 23 | return { ...state, ...action.payload.offline } 24 | } 25 | 26 | return state 27 | } 28 | case QUEUE_ACTION: 29 | return { ...state, queue: state.queue.concat(action.payload) } 30 | case ONLINE: 31 | return { ...state, isConnected: true } 32 | case OFFLINE: 33 | return { ...state, isConnected: false } 34 | case REMOVE_ACTION: { 35 | if (action.payload.uuid) { 36 | const filteredQueue = state.queue.filter(queuedAction => queuedAction.meta.uuid !== action.payload.uuid) 37 | return { ...state, queue: [...filteredQueue] } 38 | } 39 | } 40 | case RESET_QUEUE: 41 | return { ...state, queue: [] } 42 | default: 43 | return state 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/suspendSaga.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom wrapper for the saga middleware that can skip firing the saga. 3 | * 4 | * In case of the offline action we do want it to be dispatched 5 | * so that the reducer updates the local state in a optimistic manner. 6 | * 7 | * However since we know for sure that the device is offline 8 | * the corresponding saga should not be fired. 9 | * 10 | * For the action to skip the saga it should have: 11 | * ``` 12 | * skipSaga: true 13 | * ``` 14 | * property set. 15 | * 16 | * Note: One should wrap the existing saga middleware for this to work correctly, 17 | * for example: 18 | * ``` 19 | * const sagaMiddleware = createSagaMiddleware() 20 | * const suspendSagaMiddleware = suspendSaga(sagaMiddleware) 21 | * ``` 22 | * 23 | * @param {Function} middleware Saga middleware. 24 | */ 25 | export default function suspendSaga(middleware) { 26 | return (store) => (next) => { 27 | const delegate = middleware(store)(next) 28 | 29 | return (action) => { 30 | const { skipSaga } = action 31 | if (skipSaga) { 32 | return next(action) 33 | } 34 | return delegate(action) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/offlineActions.spec.js: -------------------------------------------------------------------------------- 1 | import { createOfflineActions, markActionsOffline } from '../src/offlineActions' 2 | 3 | test('createOfflineActions returns enchanced actions with offline meta keys', () => { 4 | const { Creators } = createOfflineActions({ 5 | test: ['testId'], 6 | }) 7 | 8 | expect(Creators.test(1)).toEqual({ 9 | type: 'TEST', 10 | testId: 1, 11 | meta: { 12 | queueIfOffline: true, 13 | }, 14 | }) 15 | }) 16 | 17 | test('markActionsOffline modifies Creators object', () => { 18 | const Creators = { 19 | test: testId => ({ 20 | type: 'TEST', 21 | testId, 22 | }), 23 | } 24 | 25 | markActionsOffline(Creators, ['test']) 26 | 27 | expect(Creators.test(1)).toEqual({ 28 | type: 'TEST', 29 | testId: 1, 30 | meta: { 31 | queueIfOffline: true, 32 | }, 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "redux-offline-queue" { 2 | import { Middleware } from "redux"; 3 | 4 | export const reducer: (state: any, action: any) => any; 5 | export const offlineMiddleware: (next: any) => (userConfig: any) => any; 6 | export const suspendSaga: (middlewar: Middleware) => any; 7 | export const consumeActionMiddleware: () => any; 8 | export const offlinePersistenceTransform: any; 9 | export const createOfflineActions: (config: any) => any; 10 | export const markActionsOffline: (creator: any, offlineActions: any) => void; 11 | export const queueAction: (action: any) => any; 12 | export const removeAction: (action: any) => any; 13 | 14 | export const ONLINE: string; 15 | export const OFFLINE: string; 16 | } 17 | --------------------------------------------------------------------------------