├── .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 |
--------------------------------------------------------------------------------