├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── logo.png └── logo.svg ├── examples └── basic │ ├── README.md │ ├── client │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── actions.js │ │ ├── components │ │ │ ├── App.js │ │ │ ├── MakeRequests.js │ │ │ ├── RequestsQueue.js │ │ │ └── SyncStatus.js │ │ ├── index.css │ │ ├── index.js │ │ ├── registerServiceWorker.js │ │ └── store.js │ └── yarn.lock │ ├── package.json │ └── server.js ├── flow-typed └── npm │ ├── babel-cli_vx.x.x.js │ ├── babel-core_vx.x.x.js │ ├── babel-eslint_vx.x.x.js │ ├── babel-loader_vx.x.x.js │ ├── babel-plugin-flow-react-proptypes_vx.x.x.js │ ├── babel-plugin-transform-class-properties_vx.x.x.js │ ├── babel-plugin-transform-flow-strip-types_vx.x.x.js │ ├── babel-plugin-transform-object-rest-spread_vx.x.x.js │ ├── babel-preset-latest_vx.x.x.js │ ├── babel-preset-react_vx.x.x.js │ ├── eslint-config-formidable_vx.x.x.js │ ├── eslint-plugin-babel_vx.x.x.js │ ├── eslint-plugin-filenames_vx.x.x.js │ ├── eslint-plugin-import_vx.x.x.js │ ├── eslint_vx.x.x.js │ ├── flow-bin_v0.x.x.js │ ├── redux-logger_vx.x.x.js │ └── redux_v3.x.x.js ├── package.json ├── src ├── __tests__ │ ├── defaults │ │ └── effect.js │ ├── index.js │ ├── middleware.js │ └── send.js ├── actions.js ├── config.js ├── constants.js ├── defaults │ ├── defaultCommit.js │ ├── defaultRollback.js │ ├── detectNetwork.js │ ├── detectNetwork.native.js │ ├── detectNetwork.native.legacy.js │ ├── discard.js │ ├── effect.js │ ├── index.js │ ├── offlineStateLens.js │ ├── persist.js │ ├── persist.native.js │ ├── persistAutoRehydrate.js │ └── retry.js ├── index.js ├── middleware.js ├── send.js ├── types.js └── updater.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "latest", 4 | "stage-3" 5 | ], 6 | "plugins": [ 7 | "transform-flow-strip-types", 8 | ["transform-class-properties", { "spec": true }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/__tests__/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: ['airbnb-base', 'prettier'], 4 | plugins: ['prettier'], 5 | globals: { 6 | Promise: true 7 | }, 8 | env: { 9 | jest: true, // https://stackoverflow.com/a/40265356/586382 10 | node: true 11 | }, 12 | rules: { 13 | 'comma-dangle': 'off', 14 | 'import/prefer-default-export': ['warn'], 15 | 'function-paren-newline': ['error', 'consistent'], 16 | 'prettier/prettier': ['error', { 17 | 'singleQuote': true, 18 | 'parser': 'flow' 19 | }] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [options] 4 | suppress_comment=.*\\$FlowIssue 5 | suppress_comment=.*\\$FlowFixMe 6 | suppress_comment=.*\\$FlowIgnore 7 | suppress_comment=.*\\$FlowInvalidInputTest 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Redux Offline specific ignores 2 | lib/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | .DS_Store 43 | 44 | # yarn.lock exists so don't commit package-lock.json 45 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc.js 3 | .flowconfig 4 | .travis.yml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | script: 5 | - node --version 6 | - yarn --version 7 | - yarn run lint && yarn run flow && yarn run test 8 | notifications: 9 | email: 10 | on_failure: change 11 | cache: 12 | yarn: true 13 | directories: 14 | - node_modules 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [2.2.0](https://github.com/redux-offline/redux-offline/releases/tag/v2.2.0) (2017-11-06) 3 | 4 | ### Bug Fixes 5 | 6 | * Prevent outdated connectivity info from updating the online status. (@birgernass in #58 ) 7 | * Updated deprecated Net Info. (@kbrandwijk in #52 ) 8 | * Fix failing test. (@wacii in #51 ) 9 | * Fix payload/meta inconsistency in `README.MD`. (@elf-pavlik in #38 ) 10 | * Remove console statements. (@wacii in #34 ) 11 | 12 | ### Features 13 | 14 | * Allow to use promises in discard function. (@piranna in #53 ) 15 | * Add basic example (@wacii in #46 ) 16 | * Tests for default effector (@piranna in #43 ) 17 | * Immutable js root state (@fabriziomoscon and @Ashmalech in #42 ) 18 | * Support HMR (@wacii in #32 ) 19 | * Optional commit/rollback. (@sebasgarcep in #30 ) 20 | * Alternative API: Give user control over where the middleware is applied. (@wacii in #26 ) 21 | * Add prettier as eslint plugin (@wacii in #24 ) 22 | * Extract send from middleware. (@wacii in #23 ) 23 | 24 | 25 | ## [v2.1.0](https://github.com/redux-offline/redux-offline/releases/tag/v2.1.0) (2017-09-11) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * Fix regression in `detectNetwork()`. (@jaulz in #20 ) 31 | * Fix package version. (@EvanWillms in #18 ) 32 | * Use `completeRetry()` correctly. (@wacii in #15 ) 33 | * Removes unsupported code. (@wacii in #12 ) 34 | * Fixes redux dev-tools integration. (@wacii in #11 ) 35 | * Fixes offline not being restored correctly on persist rehydrate actions. (@wacii in #5 ) 36 | * Prevents js errors on commit reducers bubbling up to the rollback. Expose the error with a redux action for handling. (@sorodrigo in #3 ) 37 | * Adds fallback to prevent crashes when no Content-Type header is found on `effect.js` . (@sorodrigo in #2 ) 38 | 39 | 40 | ### Features 41 | 42 | * Adds tests. (@wacii in #6 ) 43 | * Changes eslint config to airbnb. (@sorodrigo in #21 ) 44 | * Adds netInfo to `detectNetwork.native.js` . (@sorodrigo in #1 ) 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jani Eväkallio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | redux-offline 3 |

4 |

5 | 6 | 7 | 8 | 9 | npm version 10 | 11 | 12 | travis 13 | 14 |

15 | 16 | Persistent Redux store for _Reasonaboutable_:tm: Offline-First applications, with first-class support for optimistic UI. Use with React, React Native, or as standalone state container for any web app. 17 | 18 | _To get started, take a moment to read through the **[Offline Guide](#offline-guide)** to understand the architecture and tradeoffs behind Redux Offline, and for further context why Offline matters, read [this blog post](https://hackernoon.com/introducing-redux-offline-offline-first-architecture-for-progressive-web-applications-and-react-68c5167ecfe0)_ 19 | 20 | ## Contents 21 | 22 | * [Quick start](#quick-start) 23 | * [Offline Guide](#offline-guide) 24 | * [Configuration](#configuration) 25 | * [Contributing](#contributing) 26 | * [Miscellanea](#miscellanea) 27 | 28 | ## Full disclosure 29 | 30 | This is a community maintained fork. The original repo can be found in [here](https://github.com/jevakallio/redux-offline). 31 | 32 | Redux Offline is very, very new. If you find a bug, good job for being an early adopter! (And there will be issues.) If you find a problem, please submit an issue and I will get to them. 😇 33 | 34 | ## Quick start 35 | 36 | ##### 1. Install with npm (or [Yarn](https://yarnpkg.com)) 37 | ```sh 38 | npm install --save @redux-offline/redux-offline 39 | ``` 40 | 41 | ##### 2. Add the `offline` [store enhancer](http://redux.js.org/docs/Glossary.html#store-enhancer) with `compose` 42 | ```diff 43 | 44 | - import { applyMiddleware, createStore } from 'redux'; 45 | + import { applyMiddleware, createStore, compose } from 'redux'; 46 | + import { offline } from 'redux-offline'; 47 | + import offlineConfig from 'redux-offline/lib/defaults'; 48 | 49 | // ... 50 | 51 | const store = createStore( 52 | reducer, 53 | preloadedState, 54 | - applyMiddleware(middleware) 55 | + compose( 56 | + applyMiddleware(middleware), 57 | + offline(offlineConfig) 58 | + ) 59 | ); 60 | ``` 61 | 62 | See [Configuration](#configuration) for overriding default configurations. 63 | 64 | Looking for `createOfflineStore` from redux-offline 1.x? See migration instructions in the [2.0.0 release notes](https://github.com/redux-offline/redux-offline/releases/tag/v2.0.0). 65 | 66 | ##### 3. Decorate actions with offline metadata 67 | 68 | ```js 69 | const followUser = userId => ({ 70 | type: 'FOLLOW_USER_REQUEST', 71 | payload: { userId }, 72 | meta: { 73 | offline: { 74 | // the network action to execute: 75 | effect: { url: '/api/follow', method: 'POST', body: { userId } }, 76 | // action to dispatch when effect succeeds: 77 | commit: { type: 'FOLLOW_USER_COMMIT', meta: { userId } }, 78 | // action to dispatch if network action fails permanently: 79 | rollback: { type: 'FOLLOW_USER_ROLLBACK', meta: { userId } } 80 | } 81 | } 82 | }); 83 | ``` 84 | 85 | Read the [Offline Guide](#offline-guide) to understand how effects are executed, and how the actions are dispatched. 86 | 87 | ##### 4. (React Native Android) Ask permission to read network status 88 | 89 | If writing a native app for Android, you'll need to make sure to request the permission to access network state in your `AndroidManifest.xml`: 90 | 91 | ```xml 92 | 93 | ``` 94 | 95 | 96 | ## Offline Guide 97 | 98 | Making offline-friendly apps is not rocket science, but to make them work well involves dealing with finicky details around persisting state, resilience against flaky networks, optimistically updating user interface state, reliably reverting it back in case of failures, synchronising state in the background, and managing the evolution of the persistent state over long, long periods of time. 99 | 100 | **Redux Offline** is a battle-tested offline-first architecture, and an _experimental_ library that implements it. To make use of the library, it'll be helpful to understand the architecture behind it. 101 | 102 | ### Progressive Web Apps 103 | 104 | Redux Offline helps you with offline state management, but it **does not** automatically make your web site available offline. For caching assets (HTML pages, scripts, images, and other resources) your website needs to implement a ServiceWorker. To get started with PWAs and React, [this article provides great list of resources](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-3-offline-support-and-network-resilience-c84db889162c) to begin with. 105 | 106 | ### Persistence is key 107 | In order to be able to render meaningful content when the user opens your application offline, your application state needs to be persisted to disk. 108 | 109 | Instead of reinventing the wheel, Redux Offline uses the excellent [redux-persist](https://github.com/rt2zz/redux-persist) library. Your Redux store is saved to disk on every change, and reloaded automatically on startup. By default, browser environments will use [IndexedDB](https://developer.mozilla.org/en/docs/Web/API/IndexedDB_API) or WebSQL/localStorage fallbacks via [localForage](https://github.com/localForage/localForage), and [AsyncStorage](https://facebook.github.io/react-native/docs/asyncstorage.html) in React Native. 110 | 111 | You can [configure every aspect of how your state is persisted](#configuration). 112 | 113 | ### That's all she wrote 114 | 115 | Persisting and rehydrating state (which is a term we use for reading the state back from the disk and into our store) will get us **Read**-resilience. Our app will work offline as long as the user only wants to read from the state. We also want to support **Write**-resilience: The user should be able to do (some) actions while offline, and be able to safely assume that they will eventually be reconciled and sent to our backend. 116 | 117 | In order to support Write-resilience, we will store all network-bound actions in a queue inside our store. Redux Offline creates a state subtree called `offline`, where among other internal state needed by the library, it manages an array called `outbox`. 118 | 119 | To be able to perform the network-bound actions after we come back online, we need to store all necessary data to perform the action, and metadata about what should happen afterwards. Redux Offline understands the following metadata: 120 | ```diff 121 | type OfflineAction = { 122 | type: string, 123 | payload: any, 124 | + meta: { 125 | + offline: { 126 | + effect: any, 127 | + commit: Action, 128 | + rollback: Action 129 | + } 130 | + } 131 | ``` 132 | 133 | * `meta.offline.effect` is any data you want to send to the reconciler 134 | * `meta.offline.commit` action will be fired once the network effect has been successfully sent 135 | * `meta.offline.rollback` action will be fired if the network effect **permanently** fails (does not count network-related failures, which will be automatically retried). 136 | 137 | ### Optimism will get you places 138 | 139 | When the initial action has been dispatched, you can update your application state in your reducers as you normally would. 140 | 141 | A common pattern for offline-friendly apps is to *optimistically update UI state*. In practice, this means that as soon as user performs an action, we update the UI to look as if the action had already succeeded. This makes our applications resilient to network latency, and improves the perceived performance of our app. 142 | 143 | When we optimistically update state, we need to ensure that if the action does permanently fail, the user is appropriately notified and the application state is rolled back. To allow you this opportunity, Redux Offline will fire the action you specified in `meta.offline.rollback`. If the rollback action does not have a payload, an error object returned by the effects reconciler will be set as the payload. 144 | 145 | An example of an optimistic update: 146 | ```js 147 | const action = userId => ({ 148 | type: 'FOLLOW_USER', 149 | payload: { userId }, 150 | meta: { 151 | offline: { 152 | effect: //..., 153 | rollback: { type: 'FOLLOW_USER_ROLLBACK', meta: { userId }} 154 | } 155 | } 156 | }); 157 | 158 | // optimistically update the state, revert on rollback 159 | const followingUsersReducer = (state, action) { 160 | switch(action.type) { 161 | case 'FOLLOW_USER': 162 | return { ...state, [action.payload.userId]: true }; 163 | case 'FOLLOW_USER_ROLLBACK': 164 | return omit(state, [action.meta.userId]); 165 | default: 166 | return state; 167 | } 168 | } 169 | ``` 170 | 171 | ### A pessimist is never disappointed 172 | 173 | Sometimes it's important that the user knows that the action actually went through, so you can't optimistically update your state until the effect has been executed. Or sometimes, in order to render the final UI state, you need some data from the server response. For these cases, you can use the `meta.offline.commit` action: 174 | 175 | ```js 176 | const completeOrder = (orderId, lineItems) => ({ 177 | type: 'COMPLETE_ORDER', 178 | payload: { orderId, lineItems }, 179 | meta: { 180 | offline: { 181 | effect: //..., 182 | commit: { type: 'COMPLETE_ORDER_COMMIT', meta: { orderId }}, 183 | rollback: { type: 'COMPLETE_ORDER_ROLLBACK', meta: { orderId }} 184 | } 185 | } 186 | }); 187 | 188 | const ordersReducer = (state, action) { 189 | switch(action.type) { 190 | case 'COMPLETE_ORDER': 191 | return { 192 | ...state, 193 | submitting: {...state.submitting, [action.payload.orderId]: true 194 | }; 195 | case 'COMPLETE_ORDER_COMMIT': 196 | return { 197 | ...state, 198 | receipts: { ...state.receipts, [action.meta.orderId]: action.payload }, 199 | submitting: omit(state.submitting, [action.meta.orderId]) 200 | }; 201 | case 'COMPLETE_ORDER_ROLLBACK': 202 | return { 203 | ...state, 204 | error: action.payload, 205 | submitting: omit(state.submitting, [action.meta.orderId]) 206 | }; 207 | default: 208 | return state; 209 | } 210 | } 211 | ``` 212 | 213 | ### Executor of our will 214 | 215 | The last part of the offline metadata is `meta.offline.effect`. This property can contain anything, and will be passed as-is to the effects reconciler. 216 | 217 | The **effects reconciler** is a function that you pass to offline enhancer configuration, whose responsibility it is to take the effect payload, send it over the network, and return a Promise that resolves if sending was successful or rejects if the sending failed. The method is passed the full action as a second parameter: 218 | 219 | ```js 220 | type EffectsReconciler = (effect: any, action: OfflineAction) => Promise 221 | ``` 222 | 223 | The default reconciler is simply a paper-thin wrapper around [fetch](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) that rejects non-OK HTTP status codes, and assumes the response will be valid JSON. 224 | ```js 225 | const effectReconciler = ({url, ...opts}) => 226 | fetch(url, opts).then(res => res.ok 227 | ? res.json() 228 | : Promise.reject(res.text().then(msg => new Error(msg))); 229 | ``` 230 | So the default effect format expected by the reconciler is something like: 231 | ```js 232 | { 233 | type: 'ACTION', 234 | meta: { 235 | offline: { 236 | effect: { url: '/api/endpoint', method: 'POST'} 237 | } 238 | } 239 | } 240 | ``` 241 | 242 | That said, you'll probably want to [use your own method](#change-how-network-requests-are-made) - it can be anything, as long as it returns a Promise. 243 | 244 | ### Is this thing even on? 245 | 246 | A library that aims to support offline usage, it would be useful to know whether or not the device is online or offline. Unfortunately, network availability is not a binary "Yes" or "No": It can also be "Yes, but not really". The network receiver on your mobile device may report connectivity, but if you can't reach the remote server, are we really connected? 247 | 248 | Redux Offline uses the browser [Network Information APIs](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) and React Native [NetInfo](https://facebook.github.io/react-native/docs/netinfo.html) to be notified when the device *thinks* it's online to orchestrate synchronisation and retries. The current reported online state is stored in your store state as boolean `state.offline.online` and can be used to display a network indicator in your app, if desired. 249 | 250 | Sometimes, it's more reliable to check network connectivity by actually making sure you can exchange data with a remote server by periodically making a HEAD request, or keeping an open WebSocket connection with a heartbeat. This, too, [can be configured](#change-how-network-status-is-detected). 251 | 252 | 253 | ### Giving up is hard to do 254 | 255 | Networks are flaky. Your backend could be down. Sometimes, when the moon is in waxing crescent and the sixth-degree leylines are obstructed by passing birds, things mysteriously fail. If you are processing your offline actions queue in serial, you will need a reliable mechanism to decide when to retry the requests, and when to give up to prevent blocking the rest of the queue from being flushed. 256 | 257 | Building an offline-friendly app, you should never give up because a *network* connection failed. You *may* want to give up if the server reports a *server error*. And if the server tells you that your *request cannot be processed*, you need to give up immediately. 258 | 259 | Modelled after this principle, the default discard strategy is: 260 | * If server was not reached, always retry 261 | * If server responded with HTTP `4xx` client error, always discard 262 | * If server responded with HTTP `5xx` server error, retry with a decaying schedule configured by the [retry strategy](#change-how-network-requests-are-retried). 263 | 264 | If your backend doesn't conform to this standard, or you've [changed the effects reconciler](#change-how-network-requests-are-made) to return errors that don't expose a HTTP `status` field, you'll want to [configure the error detection strategy](#change-how-irreconcilable-errors-are-detected), too. 265 | 266 | When a message is discarded, the `meta.offline.rollback` action defined in the message metadata is fired, and you can respond accordingly. 267 | 268 | 269 | ### And if you don't at first succeed, try, try again 270 | 271 | When a network request has failed, and you've [chosen not to discard the message](#giving-up-is-hard-to-do), you need to decide when to retry the request. If your requests are failing due to an overloaded backend, retrying too often will make the problem worse and effectively DDoS your own service. Never kick a man when he's down. 272 | 273 | By default, we will always retry the first message in the queue when the [network detector](#is-this-thing-even-on) reports a change from offline to online. Otherwise, we will retry the request on a decaying schedule: 274 | * After 1 seconds 275 | * After 5 seconds 276 | * After 15 seconds 277 | * After 30 seconds 278 | * After 1 minute 279 | * After 3 minutes 280 | * After 5 minutes 281 | * After 10 minutes 282 | * After 30 minutes 283 | * After 1 hour 284 | 285 | After these 10 timed attempts, if the message is still failing due to a server error, it will be discarded. 286 | 287 | Retrying a request for this long may seem excessive, and for some use cases it can be. You can [configure the retry strategy](#change-how-network-requests-are-retried) to suit yours. 288 | 289 | The reason the default behaviour is to desperately try to make the requests succeed is that we really, really want to avoid having to deal with conflict resolution... 290 | 291 | 292 | ## Configuration 293 | 294 | 295 | ### Configuration object 296 | 297 | Redux Offline supports the following configuration properties: 298 | ```js 299 | export type Config = { 300 | detectNetwork: (callback: NetworkCallback) => void, 301 | effect: (effect: any, action: OfflineAction) => Promise<*>, 302 | retry: (action: OfflineAction, retries: number) => ?number, 303 | discard: (error: any, action: OfflineAction, retries: number) => boolean|Promise, 304 | defaultCommit: { type: string }, 305 | defaultRollback: { type: string }, 306 | persist: (store: any) => any, 307 | persistOptions: {}, 308 | persistCallback: (callback: any) => any, 309 | persistAutoRehydrate: (config: ?{}) => (next: any) => any, 310 | offlineStateLens: (state: any) => { get: OfflineState, set: (offlineState: ?OfflineState) => any } 311 | }; 312 | ``` 313 | 314 | #### Passing configuration to the enhancer 315 | The `offline` store enhancer takes the [configuration object](#configuration-object) as a final parameter: 316 | ```diff 317 | + import { offline } from 'redux-offline'; 318 | + import defaultConfig from 'redux-offline/lib/defaults'; 319 | 320 | const store = createStore( 321 | reducer, 322 | preloadedState, 323 | - middleware 324 | + compose(middleware, offline(defaultConfig)) 325 | ); 326 | ``` 327 | 328 | #### Overriding default properties 329 | You can override any individual property in the default configuration: 330 | ```diff 331 | import { offline } from 'redux-offline'; 332 | import defaultConfig from 'redux-offline/lib/defaults'; 333 | 334 | const customConfig = { 335 | ...defaultConfig, 336 | effect: (effect, _action) => Api.send(effect) 337 | } 338 | 339 | const store = createStore( 340 | reducer, 341 | preloadedState, 342 | - middleware 343 | + compose(middleware, offline(customConfig)) 344 | ); 345 | ``` 346 | 347 | #### Only import what you need 348 | The reason for default config is defined as a separate import is, that it pulls in the [redux-persist](https://github.com/rt2zz/redux-persist) dependency and a limited, but non-negligible amount of library code. If you want to minimize your bundle size, you'll want to avoid importing any code you don't use, and bring in only the pieces you need: 349 | 350 | ```diff 351 | import { offline } from 'redux-offline'; 352 | import retry from 'redux-offline/lib/defaults/retry'; 353 | import discard from 'redux-offline/lib/defaults/discard'; 354 | 355 | const myConfig = { 356 | retry, 357 | discard, 358 | effect: (effect, action) => MyCustomApiService.send(effect, action), 359 | detectNetwork: (callback) => MyCustomPingService.startPing(callback), 360 | persist: (store) => MyCustomPersistence.persist(store) 361 | }; 362 | 363 | const store = createStore( 364 | reducer, 365 | preloadedState, 366 | - middleware 367 | + compose(middleware, offline(myConfig)) 368 | myConfig 369 | ); 370 | ``` 371 | 372 | 373 | ### I want to... 374 | 375 | #### Change how network requests are made 376 | 377 | Probably the first thing you will want to do is to replace the default `fetch` effects handler. Do this by overriding `config.effect`: 378 | ```js 379 | const config = { 380 | effect: (effect, action) => { 381 | console.log(`Executing effect for ${action.type}`); 382 | return MyApi.send(effect) 383 | } 384 | } 385 | ``` 386 | 387 | The first parameter is whatever value is set in `action.meta.offline.effect`. The second parameter is the full action, which may be useful for context. The method is expected to return a Promise. The full signature of the effect handler is: `(effect: any, action: OfflineAction) => Promise`. 388 | 389 | 390 | #### Change how state is saved to disk 391 | 392 | By default, persistence is handled by [redux-persist](https://github.com/rt2zz/redux-persist). The recommended way of customizing 393 | persistence is to configure redux-persist. You can pass any valid configuration 394 | to redux-persist by defining it `config.persistOptions`: 395 | ```js 396 | const config = { 397 | persistOptions: { /*...*/ } 398 | }; 399 | ``` 400 | 401 | You can pass the callback for redux-persist as well. This function would be called when rehydration is complete. It's useful if you want to delay rendering until rehydration is complete. You can define it in `config.persistCallback`: 402 | ```js 403 | const config = { 404 | persistCallback: () => { /*...*/ } 405 | }; 406 | ``` 407 | 408 | You can pass your persistAutoRehydrate method. For example in this way you can add a logger to the persistor. 409 | ```js 410 | import { autoRehydrate } from 'redux-persist'; 411 | 412 | const config = { 413 | persistAutoRehydrate: () => autoRehydrate({log: true}) 414 | }; 415 | ``` 416 | 417 | If you want to replace redux-persist entirely **(not recommended)**, you can override `config.persist`. The function receives the store instance as a first parameter, and is responsible for setting any subscribers to listen for store changes to persist it. 418 | ```js 419 | const config = { 420 | persist: (store) => 421 | store.subscribe(() => console.log(store.getState())) 422 | ) 423 | } 424 | ``` 425 | 426 | If you override `config.store`, you will also need to manage the rehydration of your state manually. 427 | 428 | #### Change how network status is detected 429 | 430 | To replace the default network status detector, override the `config.detectNetwork` method: 431 | ```js 432 | const config = { 433 | detectNetwork: callback => MyCustomDetector.on('change', callback) 434 | } 435 | ``` 436 | 437 | The function is passed a callback, which you should call with boolean `true` when the app gets back online, and `false` when it goes offline. 438 | 439 | #### Change how irreconcilable errors are detected 440 | 441 | Actions in the queue are by default discarded when a server returns 442 | a HTTP `4xx` error. To change this, set override the `config.discard` method: 443 | ```js 444 | const config = { 445 | discard: (error, action, retries) => error.permanent || retries > 10; 446 | } 447 | ``` 448 | 449 | The method receives the Error returned by the effect reconciler, the action being processed, and a number representing how many times the action has been retried. If the method returns `true`, the action will be discarded; `false`, and it will be retried. The full signature of the method is `(error: any, action: OfflineAction, retries: number) => boolean`. Alternatively, you can return a Promise object that resolve to a boolean, allowing you to detect when to discard asynchronously (for example, doing a request to a server to refresh a token and try again). 450 | 451 | #### Change how network requests are retried 452 | 453 | By default, sending actions is retried on a decaying schedule starting with retries every few seconds, eventually slowing down to an hour before the last retry. These retry delays only apply to scenarios where the device reports being online but the server cannot be reached, or the server is reached but is responding with a non-permanent error. 454 | 455 | To configure the retry duration, override `config.retry`: 456 | ```js 457 | const config = { 458 | retry: (action, retries) => action.meta.urgent ? 100 : 1000 * (retries + 1) 459 | } 460 | ``` 461 | 462 | The function receives the action and a number representing how many times the 463 | action has been retried, and should reply with a number representing the amount 464 | of milliseconds to wait until the next retry. If this method returns `null` or 465 | `undefined`, the action will not be retried until the next time the app comes 466 | online, is started, or you manually fire an `Offline/SEND` action. 467 | 468 | #### Change how errors are handled 469 | 470 | Granular error handling is not yet implemented. You can use discard/retry, and 471 | if necessary to purge messages from your queue, you can filter `state.offline.outbox` 472 | in your reducers. Official support coming soon. 473 | 474 | #### Synchronise my state while the app is not open 475 | 476 | Background sync is not yet supported. Coming soon. 477 | 478 | #### Use an [Immutable](https://facebook.github.io/immutable-js/) store 479 | 480 | The `offline` state branch created by Redux Offline needs to be a vanilla JavaScript object. 481 | If your entire store is immutable you should check out [`redux-offline-immutable-config`](https://github.com/anyjunk/redux-offline-immutable-config) which provides drop-in configurations using immutable counterparts and code examples. 482 | If you use Immutable in the rest of your store, but the root object, you should not need extra configurations. 483 | 484 | [Contributions welcome](#contributing). 485 | 486 | #### Choose where the offline middleware is added 487 | 488 | By default, the offline middleware is inserted right before the offline store enhancer as part of its own middleware chain. If you want more control over where the middleware is inserted, consider using the alternative api, `createOffline()`. 489 | 490 | ```js 491 | import { createOffline } from "@redux-offline/redux-offline"; 492 | const { middleware, enhanceReducer, enhanceStore } = createOffline(config); 493 | const store = createStore( 494 | enhanceReducer(rootReducer), 495 | initialStore, 496 | compose(applyMiddleware(middleware), enhanceStore) 497 | ); 498 | ``` 499 | 500 | 501 | ## Contributing 502 | 503 | Improvements and additions welcome. For large changes, please submit a discussion issue before jumping to coding; we'd hate you to waste the effort. 504 | 505 | In lieu of a formal style guide, follow the included eslint rules, and use Prettier to format your code. 506 | 507 | ## Miscellanea 508 | 509 | ### Prior art 510 | 511 | Redux Offline is a distillation of patterns discovered while building apps using previously existing libraries: 512 | 513 | * Forbes Lindesay's [redux-optimist](https://github.com/ForbesLindesay/redux-optimist) 514 | * Zack Story's [redux-persist](https://github.com/rt2zz/redux-persist) 515 | 516 | Without their work, Redux Offline wouldn't exist. If you like the ideas behind Redux Offline, but want to build your own stack from lower-level components, these are good places to start. 517 | 518 | ### License 519 | 520 | MIT 521 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorodrigo/redux-offline/911129aa73d1eefa0ef51237fb3f4eb36bc0d4b5/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logo 5 | Created with Sketch. 6 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | Visualization of Redux Offline's request resolution. 2 | 3 | ## Installation 4 | 5 | Install and start the server: 6 | 7 | ```bash 8 | yarn && yarn start 9 | ``` 10 | 11 | Then, start the client: 12 | 13 | ```bash 14 | cd client 15 | yarn && yarn start 16 | ``` 17 | 18 | This launches a Webpack dev server, which hosts the static content. Other requests are proxied to the server launched previously. 19 | 20 | See [Create React App](https://github.com/facebookincubator/create-react-app) for more information. 21 | -------------------------------------------------------------------------------- /examples/basic/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/basic/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@redux-offline/redux-offline": "^2.1.0", 7 | "react": "^16.0.0", 8 | "react-dom": "^16.0.0", 9 | "react-redux": "^5.0.6", 10 | "react-scripts": "1.0.14", 11 | "redux": "^3.7.2" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "alternative": "REACT_APP_OFFLINE_API=alternative react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | }, 20 | "proxy": "http://localhost:4000" 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorodrigo/redux-offline/911129aa73d1eefa0ef51237fb3f4eb36bc0d4b5/examples/basic/client/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/basic/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/basic/client/src/actions.js: -------------------------------------------------------------------------------- 1 | function succeedAlways() { 2 | return { 3 | type: 'SUCCEED_ALWAYS', 4 | meta: { 5 | offline: { 6 | effect: { url: '/succeed-always' }, 7 | commit: { type: 'SUCCEED_ALWAYS_SUCCESS' }, 8 | rollback: { type: 'SUCCEED_ALWAYS_FAILURE' } 9 | } 10 | } 11 | }; 12 | } 13 | 14 | function succeedSometimes() { 15 | return { 16 | type: 'SUCCEED_SOMETIMES', 17 | meta: { 18 | offline: { 19 | effect: { url: '/succeed-sometimes' }, 20 | commit: { type: 'SUCCEED_SOMETIMES_SUCCESS' }, 21 | rollback: { type: 'SUCCEED_SOMETIMES_FAILURE' } 22 | } 23 | } 24 | }; 25 | } 26 | 27 | function failSometimes() { 28 | return { 29 | type: 'FAIL_SOMETIMES', 30 | meta: { 31 | offline: { 32 | effect: { url: '/fail-sometimes' }, 33 | commit: { type: 'FAIL_SOMETIMES_SUCCESS' }, 34 | rollback: { type: 'FAIL_SOMETIMES_FAILURE' } 35 | } 36 | } 37 | }; 38 | } 39 | 40 | export { succeedAlways, succeedSometimes, failSometimes }; 41 | -------------------------------------------------------------------------------- /examples/basic/client/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import MakeRequests from './MakeRequests'; 4 | import RequestsQueue from './RequestsQueue'; 5 | import SyncStatus from './SyncStatus'; 6 | import store from '../store'; 7 | 8 | function App() { 9 | return ( 10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /examples/basic/client/src/components/MakeRequests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { succeedAlways, succeedSometimes, failSometimes } from '../actions'; 5 | 6 | function MakeRequests({ succeedAlways, succeedSometimes, failSometimes }) { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | 16 | function mapDispatchToProps(dispatch) { 17 | return bindActionCreators( 18 | { 19 | succeedAlways, 20 | succeedSometimes, 21 | failSometimes 22 | }, 23 | dispatch 24 | ); 25 | } 26 | 27 | const ConnectedComponent = connect(null, mapDispatchToProps)(MakeRequests); 28 | 29 | export { MakeRequests as RawComponent }; 30 | export default ConnectedComponent; 31 | -------------------------------------------------------------------------------- /examples/basic/client/src/components/RequestsQueue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | function RequestsQueue({ actions }) { 5 | if (actions.length === 0) { 6 | return

There are no requests

; 7 | } 8 | 9 | return ( 10 |
    11 | {actions.map(action => ( 12 |
  • 13 | {action.type} 14 | #{action.meta.transaction} 15 |
  • 16 | ))} 17 |
18 | ); 19 | } 20 | 21 | function mapStateToProps(state) { 22 | return { 23 | actions: state.offline.outbox 24 | }; 25 | } 26 | 27 | const ConnectedComponent = connect(mapStateToProps)(RequestsQueue); 28 | 29 | export { RequestsQueue as RawComponent }; 30 | export default ConnectedComponent; 31 | -------------------------------------------------------------------------------- /examples/basic/client/src/components/SyncStatus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | function SyncStatus({ timer, busy, retryScheduled, attempt }) { 5 | if (!busy && !retryScheduled) { 6 | return

Synced

; 7 | } else if (busy) { 8 | return

Waiting on request - Attempt #{attempt}

; 9 | } 10 | return ( 11 |

12 | Waiting on retry: {timer}s - Attempt #{attempt} 13 |

14 | ); 15 | } 16 | 17 | function mapStateToProps(state) { 18 | return { 19 | timer: state.timer, 20 | busy: state.offline.busy, 21 | retryScheduled: state.offline.retryScheduled, 22 | attempt: state.offline.retryCount + 1 23 | }; 24 | } 25 | 26 | const ConnectedComponent = connect(mapStateToProps)(SyncStatus); 27 | 28 | export { SyncStatus as RawComponent }; 29 | export default ConnectedComponent; 30 | -------------------------------------------------------------------------------- /examples/basic/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/basic/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './components/App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /examples/basic/client/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /examples/basic/client/src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import { offline, createOffline } from '@redux-offline/redux-offline'; 3 | import defaultConfig from '@redux-offline/redux-offline/lib/defaults'; 4 | 5 | const initialState = { 6 | timer: 0 7 | }; 8 | function reducer(state = initialState, action) { 9 | if (action.type === 'Offline/SCHEDULE_RETRY') { 10 | return { 11 | ...state, 12 | timer: action.payload.delay / 1000 13 | }; 14 | } 15 | if (action.type === 'TICK') { 16 | return { 17 | ...state, 18 | timer: state.timer === 0 ? 0 : state.timer - 1 19 | }; 20 | } 21 | return state; 22 | } 23 | 24 | const config = { 25 | ...defaultConfig, 26 | retry(_action, retries) { 27 | return (retries + 1) * 1000; 28 | } 29 | }; 30 | 31 | function tickMiddleware(store) { 32 | return next => action => { 33 | if (action.type === 'Offline/SCHEDULE_RETRY') { 34 | const intervalId = setInterval(() => { 35 | store.dispatch({ type: 'TICK' }); 36 | }, 1000); 37 | setTimeout(() => clearInterval(intervalId), action.payload.delay); 38 | } 39 | return next(action); 40 | }; 41 | } 42 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 43 | 44 | let store; 45 | if (process.env.REACT_APP_OFFLINE_API === 'alternative') { 46 | const { middleware, enhanceReducer, enhanceStore } = createOffline(config); 47 | store = createStore( 48 | enhanceReducer(reducer), 49 | undefined, 50 | composeEnhancers(applyMiddleware(middleware, tickMiddleware), enhanceStore) 51 | ); 52 | } else { 53 | store = createStore( 54 | reducer, 55 | composeEnhancers(offline(config), applyMiddleware(tickMiddleware)) 56 | ); 57 | } 58 | 59 | export default store; 60 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "dependencies": { 9 | "express": "^4.16.1" 10 | }, 11 | "main": "index.js", 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /examples/basic/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | app.use((_req, _res, next) => setTimeout(next, 400)); 6 | 7 | app.get('/succeed-always', (_req, res) => { 8 | res.status(200).json({ some: 'data' }); 9 | }); 10 | 11 | app.get('/succeed-sometimes', (_req, res) => { 12 | if (Math.random() < 0.5) { 13 | res.status(500).json({ error: 'server' }); 14 | } else { 15 | res.status(200).json({ some: 'data' }); 16 | } 17 | }); 18 | 19 | app.get('/fail-sometimes', (_req, res) => { 20 | if (Math.random() < 0.5) { 21 | res.status(500).json({ error: 'server' }); 22 | } else { 23 | res.status(400).json({ error: 'client' }); 24 | } 25 | }); 26 | 27 | app.listen(4000); 28 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-cli_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 4ed046e8a59cab80808a45c9f5b25815 2 | // flow-typed version: <>/babel-cli_v^6.18.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-cli' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-cli' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-cli/bin/babel-doctor' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-cli/bin/babel-external-helpers' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-cli/bin/babel-node' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-cli/bin/babel' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-cli/lib/_babel-node' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-cli/lib/babel-external-helpers' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'babel-cli/lib/babel-node' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'babel-cli/lib/babel/dir' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'babel-cli/lib/babel/file' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'babel-cli/lib/babel/index' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'babel-cli/lib/babel/util' { 66 | declare module.exports: any; 67 | } 68 | 69 | // Filename aliases 70 | declare module 'babel-cli/bin/babel-doctor.js' { 71 | declare module.exports: $Exports<'babel-cli/bin/babel-doctor'>; 72 | } 73 | declare module 'babel-cli/bin/babel-external-helpers.js' { 74 | declare module.exports: $Exports<'babel-cli/bin/babel-external-helpers'>; 75 | } 76 | declare module 'babel-cli/bin/babel-node.js' { 77 | declare module.exports: $Exports<'babel-cli/bin/babel-node'>; 78 | } 79 | declare module 'babel-cli/bin/babel.js' { 80 | declare module.exports: $Exports<'babel-cli/bin/babel'>; 81 | } 82 | declare module 'babel-cli/index' { 83 | declare module.exports: $Exports<'babel-cli'>; 84 | } 85 | declare module 'babel-cli/index.js' { 86 | declare module.exports: $Exports<'babel-cli'>; 87 | } 88 | declare module 'babel-cli/lib/_babel-node.js' { 89 | declare module.exports: $Exports<'babel-cli/lib/_babel-node'>; 90 | } 91 | declare module 'babel-cli/lib/babel-external-helpers.js' { 92 | declare module.exports: $Exports<'babel-cli/lib/babel-external-helpers'>; 93 | } 94 | declare module 'babel-cli/lib/babel-node.js' { 95 | declare module.exports: $Exports<'babel-cli/lib/babel-node'>; 96 | } 97 | declare module 'babel-cli/lib/babel/dir.js' { 98 | declare module.exports: $Exports<'babel-cli/lib/babel/dir'>; 99 | } 100 | declare module 'babel-cli/lib/babel/file.js' { 101 | declare module.exports: $Exports<'babel-cli/lib/babel/file'>; 102 | } 103 | declare module 'babel-cli/lib/babel/index.js' { 104 | declare module.exports: $Exports<'babel-cli/lib/babel/index'>; 105 | } 106 | declare module 'babel-cli/lib/babel/util.js' { 107 | declare module.exports: $Exports<'babel-cli/lib/babel/util'>; 108 | } 109 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-core_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ba3cc9e95abaf3c9239b928223c62a9c 2 | // flow-typed version: <>/babel-core_v^6.18.2/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-core' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-core' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-core/lib/api/browser' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-core/lib/api/node' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-core/lib/helpers/get-possible-plugin-names' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-core/lib/helpers/get-possible-preset-names' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-core/lib/helpers/merge' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-core/lib/helpers/normalize-ast' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'babel-core/lib/helpers/resolve-from-possible-names' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'babel-core/lib/helpers/resolve-plugin' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'babel-core/lib/helpers/resolve-preset' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'babel-core/lib/helpers/resolve' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'babel-core/lib/store' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'babel-core/lib/tools/build-external-helpers' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'babel-core/lib/transformation/file/index' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'babel-core/lib/transformation/file/logger' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'babel-core/lib/transformation/file/metadata' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'babel-core/lib/transformation/file/options/build-config-chain' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'babel-core/lib/transformation/file/options/config' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'babel-core/lib/transformation/file/options/index' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'babel-core/lib/transformation/file/options/option-manager' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'babel-core/lib/transformation/file/options/parsers' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'babel-core/lib/transformation/file/options/removed' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'babel-core/lib/transformation/internal-plugins/block-hoist' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'babel-core/lib/transformation/internal-plugins/shadow-functions' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'babel-core/lib/transformation/pipeline' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'babel-core/lib/transformation/plugin-pass' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'babel-core/lib/transformation/plugin' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'babel-core/lib/util' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'babel-core/register' { 134 | declare module.exports: any; 135 | } 136 | 137 | // Filename aliases 138 | declare module 'babel-core/index' { 139 | declare module.exports: $Exports<'babel-core'>; 140 | } 141 | declare module 'babel-core/index.js' { 142 | declare module.exports: $Exports<'babel-core'>; 143 | } 144 | declare module 'babel-core/lib/api/browser.js' { 145 | declare module.exports: $Exports<'babel-core/lib/api/browser'>; 146 | } 147 | declare module 'babel-core/lib/api/node.js' { 148 | declare module.exports: $Exports<'babel-core/lib/api/node'>; 149 | } 150 | declare module 'babel-core/lib/helpers/get-possible-plugin-names.js' { 151 | declare module.exports: $Exports<'babel-core/lib/helpers/get-possible-plugin-names'>; 152 | } 153 | declare module 'babel-core/lib/helpers/get-possible-preset-names.js' { 154 | declare module.exports: $Exports<'babel-core/lib/helpers/get-possible-preset-names'>; 155 | } 156 | declare module 'babel-core/lib/helpers/merge.js' { 157 | declare module.exports: $Exports<'babel-core/lib/helpers/merge'>; 158 | } 159 | declare module 'babel-core/lib/helpers/normalize-ast.js' { 160 | declare module.exports: $Exports<'babel-core/lib/helpers/normalize-ast'>; 161 | } 162 | declare module 'babel-core/lib/helpers/resolve-from-possible-names.js' { 163 | declare module.exports: $Exports<'babel-core/lib/helpers/resolve-from-possible-names'>; 164 | } 165 | declare module 'babel-core/lib/helpers/resolve-plugin.js' { 166 | declare module.exports: $Exports<'babel-core/lib/helpers/resolve-plugin'>; 167 | } 168 | declare module 'babel-core/lib/helpers/resolve-preset.js' { 169 | declare module.exports: $Exports<'babel-core/lib/helpers/resolve-preset'>; 170 | } 171 | declare module 'babel-core/lib/helpers/resolve.js' { 172 | declare module.exports: $Exports<'babel-core/lib/helpers/resolve'>; 173 | } 174 | declare module 'babel-core/lib/store.js' { 175 | declare module.exports: $Exports<'babel-core/lib/store'>; 176 | } 177 | declare module 'babel-core/lib/tools/build-external-helpers.js' { 178 | declare module.exports: $Exports<'babel-core/lib/tools/build-external-helpers'>; 179 | } 180 | declare module 'babel-core/lib/transformation/file/index.js' { 181 | declare module.exports: $Exports<'babel-core/lib/transformation/file/index'>; 182 | } 183 | declare module 'babel-core/lib/transformation/file/logger.js' { 184 | declare module.exports: $Exports<'babel-core/lib/transformation/file/logger'>; 185 | } 186 | declare module 'babel-core/lib/transformation/file/metadata.js' { 187 | declare module.exports: $Exports<'babel-core/lib/transformation/file/metadata'>; 188 | } 189 | declare module 'babel-core/lib/transformation/file/options/build-config-chain.js' { 190 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/build-config-chain'>; 191 | } 192 | declare module 'babel-core/lib/transformation/file/options/config.js' { 193 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/config'>; 194 | } 195 | declare module 'babel-core/lib/transformation/file/options/index.js' { 196 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/index'>; 197 | } 198 | declare module 'babel-core/lib/transformation/file/options/option-manager.js' { 199 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/option-manager'>; 200 | } 201 | declare module 'babel-core/lib/transformation/file/options/parsers.js' { 202 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/parsers'>; 203 | } 204 | declare module 'babel-core/lib/transformation/file/options/removed.js' { 205 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/removed'>; 206 | } 207 | declare module 'babel-core/lib/transformation/internal-plugins/block-hoist.js' { 208 | declare module.exports: $Exports<'babel-core/lib/transformation/internal-plugins/block-hoist'>; 209 | } 210 | declare module 'babel-core/lib/transformation/internal-plugins/shadow-functions.js' { 211 | declare module.exports: $Exports<'babel-core/lib/transformation/internal-plugins/shadow-functions'>; 212 | } 213 | declare module 'babel-core/lib/transformation/pipeline.js' { 214 | declare module.exports: $Exports<'babel-core/lib/transformation/pipeline'>; 215 | } 216 | declare module 'babel-core/lib/transformation/plugin-pass.js' { 217 | declare module.exports: $Exports<'babel-core/lib/transformation/plugin-pass'>; 218 | } 219 | declare module 'babel-core/lib/transformation/plugin.js' { 220 | declare module.exports: $Exports<'babel-core/lib/transformation/plugin'>; 221 | } 222 | declare module 'babel-core/lib/util.js' { 223 | declare module.exports: $Exports<'babel-core/lib/util'>; 224 | } 225 | declare module 'babel-core/register.js' { 226 | declare module.exports: $Exports<'babel-core/register'>; 227 | } 228 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-eslint_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 78bf0c72d7a781ce282de76ea9658d5a 2 | // flow-typed version: <>/babel-eslint_v^7.1.1/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-eslint' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-eslint' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-eslint/babylon-to-espree/attachComments' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-eslint/babylon-to-espree/convertTemplateType' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-eslint/babylon-to-espree/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-eslint/babylon-to-espree/toAST' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-eslint/babylon-to-espree/toToken' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-eslint/babylon-to-espree/toTokens' { 46 | declare module.exports: any; 47 | } 48 | 49 | // Filename aliases 50 | declare module 'babel-eslint/babylon-to-espree/attachComments.js' { 51 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/attachComments'>; 52 | } 53 | declare module 'babel-eslint/babylon-to-espree/convertTemplateType.js' { 54 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/convertTemplateType'>; 55 | } 56 | declare module 'babel-eslint/babylon-to-espree/index.js' { 57 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/index'>; 58 | } 59 | declare module 'babel-eslint/babylon-to-espree/toAST.js' { 60 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toAST'>; 61 | } 62 | declare module 'babel-eslint/babylon-to-espree/toToken.js' { 63 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toToken'>; 64 | } 65 | declare module 'babel-eslint/babylon-to-espree/toTokens.js' { 66 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toTokens'>; 67 | } 68 | declare module 'babel-eslint/index' { 69 | declare module.exports: $Exports<'babel-eslint'>; 70 | } 71 | declare module 'babel-eslint/index.js' { 72 | declare module.exports: $Exports<'babel-eslint'>; 73 | } 74 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-loader_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 958f321748bba5e53bcf8aa662b4a827 2 | // flow-typed version: <>/babel-loader_v^6.2.8/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-loader' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-loader' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-loader/lib/fs-cache' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-loader/lib/index' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-loader/lib/resolve-rc' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-loader/lib/utils/exists' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-loader/lib/utils/read' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-loader/lib/utils/relative' { 46 | declare module.exports: any; 47 | } 48 | 49 | // Filename aliases 50 | declare module 'babel-loader/lib/fs-cache.js' { 51 | declare module.exports: $Exports<'babel-loader/lib/fs-cache'>; 52 | } 53 | declare module 'babel-loader/lib/index.js' { 54 | declare module.exports: $Exports<'babel-loader/lib/index'>; 55 | } 56 | declare module 'babel-loader/lib/resolve-rc.js' { 57 | declare module.exports: $Exports<'babel-loader/lib/resolve-rc'>; 58 | } 59 | declare module 'babel-loader/lib/utils/exists.js' { 60 | declare module.exports: $Exports<'babel-loader/lib/utils/exists'>; 61 | } 62 | declare module 'babel-loader/lib/utils/read.js' { 63 | declare module.exports: $Exports<'babel-loader/lib/utils/read'>; 64 | } 65 | declare module 'babel-loader/lib/utils/relative.js' { 66 | declare module.exports: $Exports<'babel-loader/lib/utils/relative'>; 67 | } 68 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-flow-react-proptypes_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 55569fcfc9cea7240793ba21edcd1750 2 | // flow-typed version: <>/babel-plugin-flow-react-proptypes_v^0.18.1/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-flow-react-proptypes' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-flow-react-proptypes' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-flow-react-proptypes/lib/convertToPropTypes' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-plugin-flow-react-proptypes/lib/index' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-plugin-flow-react-proptypes/lib/makePropTypesAst' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-plugin-flow-react-proptypes/lib/util' { 38 | declare module.exports: any; 39 | } 40 | 41 | // Filename aliases 42 | declare module 'babel-plugin-flow-react-proptypes/lib/convertToPropTypes.js' { 43 | declare module.exports: $Exports<'babel-plugin-flow-react-proptypes/lib/convertToPropTypes'>; 44 | } 45 | declare module 'babel-plugin-flow-react-proptypes/lib/index.js' { 46 | declare module.exports: $Exports<'babel-plugin-flow-react-proptypes/lib/index'>; 47 | } 48 | declare module 'babel-plugin-flow-react-proptypes/lib/makePropTypesAst.js' { 49 | declare module.exports: $Exports<'babel-plugin-flow-react-proptypes/lib/makePropTypesAst'>; 50 | } 51 | declare module 'babel-plugin-flow-react-proptypes/lib/util.js' { 52 | declare module.exports: $Exports<'babel-plugin-flow-react-proptypes/lib/util'>; 53 | } 54 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-class-properties_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e5044bdb64cc3189dd13e3a6a4e88e59 2 | // flow-typed version: <>/babel-plugin-transform-class-properties_v^6.19.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-class-properties' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-class-properties' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-class-properties/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-class-properties/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-class-properties/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-flow-strip-types_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ee65546d193f4ebf75093922d76833ec 2 | // flow-typed version: <>/babel-plugin-transform-flow-strip-types_v^6.18.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-flow-strip-types' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-flow-strip-types' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-flow-strip-types/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-flow-strip-types/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-flow-strip-types/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-object-rest-spread_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 466f592e1f789690c0a90a5c3cc07e69 2 | // flow-typed version: <>/babel-plugin-transform-object-rest-spread_v^6.19.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-object-rest-spread' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-object-rest-spread' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-object-rest-spread/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-object-rest-spread/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-object-rest-spread/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-preset-latest_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 633229b4c101f73875920d7eb308ec36 2 | // flow-typed version: <>/babel-preset-latest_v^6.16.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-preset-latest' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-preset-latest' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-preset-latest/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-preset-latest/lib/index.js' { 31 | declare module.exports: $Exports<'babel-preset-latest/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-preset-react_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 084a38d54a422566a87df612e48873e2 2 | // flow-typed version: <>/babel-preset-react_v^6.16.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-preset-react' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-preset-react' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-preset-react/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-preset-react/lib/index.js' { 31 | declare module.exports: $Exports<'babel-preset-react/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-config-formidable_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ae00b941adf06d4eddcf3a1ed80ca422 2 | // flow-typed version: <>/eslint-config-formidable_v^2.0.1/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-config-formidable' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-config-formidable' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-config-formidable/configurations/es5-browser' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-config-formidable/configurations/es5-node' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-config-formidable/configurations/es5-test' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-config-formidable/configurations/es5' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-config-formidable/configurations/es6-browser' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-config-formidable/configurations/es6-node-test' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-config-formidable/configurations/es6-node' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-config-formidable/configurations/es6-react-test' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-config-formidable/configurations/es6-react' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-config-formidable/configurations/es6-test' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-config-formidable/configurations/es6' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-config-formidable/configurations/off' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-config-formidable/rules/eslint/best-practices/off' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-config-formidable/rules/eslint/best-practices/on' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-config-formidable/rules/eslint/errors/off' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-config-formidable/rules/eslint/errors/on' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-config-formidable/rules/eslint/es6/off' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-config-formidable/rules/eslint/es6/on' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'eslint-config-formidable/rules/eslint/node/off' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'eslint-config-formidable/rules/eslint/node/on' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'eslint-config-formidable/rules/eslint/strict/off' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'eslint-config-formidable/rules/eslint/strict/on' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'eslint-config-formidable/rules/eslint/style/off' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'eslint-config-formidable/rules/eslint/style/on' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'eslint-config-formidable/rules/eslint/variables/off' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'eslint-config-formidable/rules/eslint/variables/on' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'eslint-config-formidable/rules/filenames/off' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'eslint-config-formidable/rules/filenames/on' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'eslint-config-formidable/rules/import/off' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'eslint-config-formidable/rules/import/on' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'eslint-config-formidable/rules/react/off' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'eslint-config-formidable/rules/react/on' { 150 | declare module.exports: any; 151 | } 152 | 153 | // Filename aliases 154 | declare module 'eslint-config-formidable/configurations/es5-browser.js' { 155 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es5-browser'>; 156 | } 157 | declare module 'eslint-config-formidable/configurations/es5-node.js' { 158 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es5-node'>; 159 | } 160 | declare module 'eslint-config-formidable/configurations/es5-test.js' { 161 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es5-test'>; 162 | } 163 | declare module 'eslint-config-formidable/configurations/es5.js' { 164 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es5'>; 165 | } 166 | declare module 'eslint-config-formidable/configurations/es6-browser.js' { 167 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es6-browser'>; 168 | } 169 | declare module 'eslint-config-formidable/configurations/es6-node-test.js' { 170 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es6-node-test'>; 171 | } 172 | declare module 'eslint-config-formidable/configurations/es6-node.js' { 173 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es6-node'>; 174 | } 175 | declare module 'eslint-config-formidable/configurations/es6-react-test.js' { 176 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es6-react-test'>; 177 | } 178 | declare module 'eslint-config-formidable/configurations/es6-react.js' { 179 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es6-react'>; 180 | } 181 | declare module 'eslint-config-formidable/configurations/es6-test.js' { 182 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es6-test'>; 183 | } 184 | declare module 'eslint-config-formidable/configurations/es6.js' { 185 | declare module.exports: $Exports<'eslint-config-formidable/configurations/es6'>; 186 | } 187 | declare module 'eslint-config-formidable/configurations/off.js' { 188 | declare module.exports: $Exports<'eslint-config-formidable/configurations/off'>; 189 | } 190 | declare module 'eslint-config-formidable/rules/eslint/best-practices/off.js' { 191 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/best-practices/off'>; 192 | } 193 | declare module 'eslint-config-formidable/rules/eslint/best-practices/on.js' { 194 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/best-practices/on'>; 195 | } 196 | declare module 'eslint-config-formidable/rules/eslint/errors/off.js' { 197 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/errors/off'>; 198 | } 199 | declare module 'eslint-config-formidable/rules/eslint/errors/on.js' { 200 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/errors/on'>; 201 | } 202 | declare module 'eslint-config-formidable/rules/eslint/es6/off.js' { 203 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/es6/off'>; 204 | } 205 | declare module 'eslint-config-formidable/rules/eslint/es6/on.js' { 206 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/es6/on'>; 207 | } 208 | declare module 'eslint-config-formidable/rules/eslint/node/off.js' { 209 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/node/off'>; 210 | } 211 | declare module 'eslint-config-formidable/rules/eslint/node/on.js' { 212 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/node/on'>; 213 | } 214 | declare module 'eslint-config-formidable/rules/eslint/strict/off.js' { 215 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/strict/off'>; 216 | } 217 | declare module 'eslint-config-formidable/rules/eslint/strict/on.js' { 218 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/strict/on'>; 219 | } 220 | declare module 'eslint-config-formidable/rules/eslint/style/off.js' { 221 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/style/off'>; 222 | } 223 | declare module 'eslint-config-formidable/rules/eslint/style/on.js' { 224 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/style/on'>; 225 | } 226 | declare module 'eslint-config-formidable/rules/eslint/variables/off.js' { 227 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/variables/off'>; 228 | } 229 | declare module 'eslint-config-formidable/rules/eslint/variables/on.js' { 230 | declare module.exports: $Exports<'eslint-config-formidable/rules/eslint/variables/on'>; 231 | } 232 | declare module 'eslint-config-formidable/rules/filenames/off.js' { 233 | declare module.exports: $Exports<'eslint-config-formidable/rules/filenames/off'>; 234 | } 235 | declare module 'eslint-config-formidable/rules/filenames/on.js' { 236 | declare module.exports: $Exports<'eslint-config-formidable/rules/filenames/on'>; 237 | } 238 | declare module 'eslint-config-formidable/rules/import/off.js' { 239 | declare module.exports: $Exports<'eslint-config-formidable/rules/import/off'>; 240 | } 241 | declare module 'eslint-config-formidable/rules/import/on.js' { 242 | declare module.exports: $Exports<'eslint-config-formidable/rules/import/on'>; 243 | } 244 | declare module 'eslint-config-formidable/rules/react/off.js' { 245 | declare module.exports: $Exports<'eslint-config-formidable/rules/react/off'>; 246 | } 247 | declare module 'eslint-config-formidable/rules/react/on.js' { 248 | declare module.exports: $Exports<'eslint-config-formidable/rules/react/on'>; 249 | } 250 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-babel_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: cd853adfb9d273360a0eb4742f04c8e7 2 | // flow-typed version: <>/eslint-plugin-babel_v^4.1.1/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-babel' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-babel' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-babel/ast-utils' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-babel/rules/array-bracket-spacing' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-babel/rules/arrow-parens' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-babel/rules/flow-object-type' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-babel/rules/func-params-comma-dangle' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-babel/rules/generator-star-spacing' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-babel/rules/new-cap' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-babel/rules/no-await-in-loop' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-babel/rules/no-invalid-this' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-babel/rules/object-curly-spacing' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-babel/rules/object-shorthand' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-babel/rules/semi' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-babel/tests/rules/new-cap' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-babel/tests/rules/no-invalid-this' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-babel/tests/rules/object-curly-spacing' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-babel/tests/rules/semi' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-babel/tests/RuleTester' { 90 | declare module.exports: any; 91 | } 92 | 93 | // Filename aliases 94 | declare module 'eslint-plugin-babel/ast-utils.js' { 95 | declare module.exports: $Exports<'eslint-plugin-babel/ast-utils'>; 96 | } 97 | declare module 'eslint-plugin-babel/index' { 98 | declare module.exports: $Exports<'eslint-plugin-babel'>; 99 | } 100 | declare module 'eslint-plugin-babel/index.js' { 101 | declare module.exports: $Exports<'eslint-plugin-babel'>; 102 | } 103 | declare module 'eslint-plugin-babel/rules/array-bracket-spacing.js' { 104 | declare module.exports: $Exports<'eslint-plugin-babel/rules/array-bracket-spacing'>; 105 | } 106 | declare module 'eslint-plugin-babel/rules/arrow-parens.js' { 107 | declare module.exports: $Exports<'eslint-plugin-babel/rules/arrow-parens'>; 108 | } 109 | declare module 'eslint-plugin-babel/rules/flow-object-type.js' { 110 | declare module.exports: $Exports<'eslint-plugin-babel/rules/flow-object-type'>; 111 | } 112 | declare module 'eslint-plugin-babel/rules/func-params-comma-dangle.js' { 113 | declare module.exports: $Exports<'eslint-plugin-babel/rules/func-params-comma-dangle'>; 114 | } 115 | declare module 'eslint-plugin-babel/rules/generator-star-spacing.js' { 116 | declare module.exports: $Exports<'eslint-plugin-babel/rules/generator-star-spacing'>; 117 | } 118 | declare module 'eslint-plugin-babel/rules/new-cap.js' { 119 | declare module.exports: $Exports<'eslint-plugin-babel/rules/new-cap'>; 120 | } 121 | declare module 'eslint-plugin-babel/rules/no-await-in-loop.js' { 122 | declare module.exports: $Exports<'eslint-plugin-babel/rules/no-await-in-loop'>; 123 | } 124 | declare module 'eslint-plugin-babel/rules/no-invalid-this.js' { 125 | declare module.exports: $Exports<'eslint-plugin-babel/rules/no-invalid-this'>; 126 | } 127 | declare module 'eslint-plugin-babel/rules/object-curly-spacing.js' { 128 | declare module.exports: $Exports<'eslint-plugin-babel/rules/object-curly-spacing'>; 129 | } 130 | declare module 'eslint-plugin-babel/rules/object-shorthand.js' { 131 | declare module.exports: $Exports<'eslint-plugin-babel/rules/object-shorthand'>; 132 | } 133 | declare module 'eslint-plugin-babel/rules/semi.js' { 134 | declare module.exports: $Exports<'eslint-plugin-babel/rules/semi'>; 135 | } 136 | declare module 'eslint-plugin-babel/tests/rules/new-cap.js' { 137 | declare module.exports: $Exports<'eslint-plugin-babel/tests/rules/new-cap'>; 138 | } 139 | declare module 'eslint-plugin-babel/tests/rules/no-invalid-this.js' { 140 | declare module.exports: $Exports<'eslint-plugin-babel/tests/rules/no-invalid-this'>; 141 | } 142 | declare module 'eslint-plugin-babel/tests/rules/object-curly-spacing.js' { 143 | declare module.exports: $Exports<'eslint-plugin-babel/tests/rules/object-curly-spacing'>; 144 | } 145 | declare module 'eslint-plugin-babel/tests/rules/semi.js' { 146 | declare module.exports: $Exports<'eslint-plugin-babel/tests/rules/semi'>; 147 | } 148 | declare module 'eslint-plugin-babel/tests/RuleTester.js' { 149 | declare module.exports: $Exports<'eslint-plugin-babel/tests/RuleTester'>; 150 | } 151 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-filenames_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f54017c519a3c3b093e3b989e911c602 2 | // flow-typed version: <>/eslint-plugin-filenames_v^1.1.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-filenames' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-filenames' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-filenames/lib/common/getExportedName' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-filenames/lib/common/isIgnoredFilename' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-filenames/lib/common/isIndexFile' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-filenames/lib/common/parseFilename' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-filenames/lib/rules/match-exported' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-filenames/lib/rules/match-regex' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-filenames/lib/rules/no-index' { 50 | declare module.exports: any; 51 | } 52 | 53 | // Filename aliases 54 | declare module 'eslint-plugin-filenames/index' { 55 | declare module.exports: $Exports<'eslint-plugin-filenames'>; 56 | } 57 | declare module 'eslint-plugin-filenames/index.js' { 58 | declare module.exports: $Exports<'eslint-plugin-filenames'>; 59 | } 60 | declare module 'eslint-plugin-filenames/lib/common/getExportedName.js' { 61 | declare module.exports: $Exports<'eslint-plugin-filenames/lib/common/getExportedName'>; 62 | } 63 | declare module 'eslint-plugin-filenames/lib/common/isIgnoredFilename.js' { 64 | declare module.exports: $Exports<'eslint-plugin-filenames/lib/common/isIgnoredFilename'>; 65 | } 66 | declare module 'eslint-plugin-filenames/lib/common/isIndexFile.js' { 67 | declare module.exports: $Exports<'eslint-plugin-filenames/lib/common/isIndexFile'>; 68 | } 69 | declare module 'eslint-plugin-filenames/lib/common/parseFilename.js' { 70 | declare module.exports: $Exports<'eslint-plugin-filenames/lib/common/parseFilename'>; 71 | } 72 | declare module 'eslint-plugin-filenames/lib/rules/match-exported.js' { 73 | declare module.exports: $Exports<'eslint-plugin-filenames/lib/rules/match-exported'>; 74 | } 75 | declare module 'eslint-plugin-filenames/lib/rules/match-regex.js' { 76 | declare module.exports: $Exports<'eslint-plugin-filenames/lib/rules/match-regex'>; 77 | } 78 | declare module 'eslint-plugin-filenames/lib/rules/no-index.js' { 79 | declare module.exports: $Exports<'eslint-plugin-filenames/lib/rules/no-index'>; 80 | } 81 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-import_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6aa72b267b236a84ef1428e7844f0925 2 | // flow-typed version: <>/eslint-plugin-import_v^2.2.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-import' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-import' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-import/config/electron' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-plugin-import/config/errors' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-plugin-import/config/react-native' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-plugin-import/config/react' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-plugin-import/config/recommended' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-plugin-import/config/stage-0' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-plugin-import/config/warnings' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-plugin-import/lib/core/importType' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-plugin-import/lib/core/staticRequire' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-plugin-import/lib/ExportMap' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'eslint-plugin-import/lib/importDeclaration' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'eslint-plugin-import/lib/index' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'eslint-plugin-import/lib/rules/default' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'eslint-plugin-import/lib/rules/export' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'eslint-plugin-import/lib/rules/extensions' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'eslint-plugin-import/lib/rules/first' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'eslint-plugin-import/lib/rules/imports-first' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'eslint-plugin-import/lib/rules/max-dependencies' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'eslint-plugin-import/lib/rules/named' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'eslint-plugin-import/lib/rules/namespace' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'eslint-plugin-import/lib/rules/newline-after-import' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'eslint-plugin-import/lib/rules/no-absolute-path' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'eslint-plugin-import/lib/rules/no-amd' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'eslint-plugin-import/lib/rules/no-commonjs' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'eslint-plugin-import/lib/rules/no-deprecated' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'eslint-plugin-import/lib/rules/no-duplicates' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'eslint-plugin-import/lib/rules/no-dynamic-require' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'eslint-plugin-import/lib/rules/no-extraneous-dependencies' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'eslint-plugin-import/lib/rules/no-internal-modules' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'eslint-plugin-import/lib/rules/no-mutable-exports' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default-member' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default' { 150 | declare module.exports: any; 151 | } 152 | 153 | declare module 'eslint-plugin-import/lib/rules/no-named-default' { 154 | declare module.exports: any; 155 | } 156 | 157 | declare module 'eslint-plugin-import/lib/rules/no-namespace' { 158 | declare module.exports: any; 159 | } 160 | 161 | declare module 'eslint-plugin-import/lib/rules/no-nodejs-modules' { 162 | declare module.exports: any; 163 | } 164 | 165 | declare module 'eslint-plugin-import/lib/rules/no-restricted-paths' { 166 | declare module.exports: any; 167 | } 168 | 169 | declare module 'eslint-plugin-import/lib/rules/no-unassigned-import' { 170 | declare module.exports: any; 171 | } 172 | 173 | declare module 'eslint-plugin-import/lib/rules/no-unresolved' { 174 | declare module.exports: any; 175 | } 176 | 177 | declare module 'eslint-plugin-import/lib/rules/no-webpack-loader-syntax' { 178 | declare module.exports: any; 179 | } 180 | 181 | declare module 'eslint-plugin-import/lib/rules/order' { 182 | declare module.exports: any; 183 | } 184 | 185 | declare module 'eslint-plugin-import/lib/rules/prefer-default-export' { 186 | declare module.exports: any; 187 | } 188 | 189 | declare module 'eslint-plugin-import/lib/rules/unambiguous' { 190 | declare module.exports: any; 191 | } 192 | 193 | declare module 'eslint-plugin-import/memo-parser/index' { 194 | declare module.exports: any; 195 | } 196 | 197 | // Filename aliases 198 | declare module 'eslint-plugin-import/config/electron.js' { 199 | declare module.exports: $Exports<'eslint-plugin-import/config/electron'>; 200 | } 201 | declare module 'eslint-plugin-import/config/errors.js' { 202 | declare module.exports: $Exports<'eslint-plugin-import/config/errors'>; 203 | } 204 | declare module 'eslint-plugin-import/config/react-native.js' { 205 | declare module.exports: $Exports<'eslint-plugin-import/config/react-native'>; 206 | } 207 | declare module 'eslint-plugin-import/config/react.js' { 208 | declare module.exports: $Exports<'eslint-plugin-import/config/react'>; 209 | } 210 | declare module 'eslint-plugin-import/config/recommended.js' { 211 | declare module.exports: $Exports<'eslint-plugin-import/config/recommended'>; 212 | } 213 | declare module 'eslint-plugin-import/config/stage-0.js' { 214 | declare module.exports: $Exports<'eslint-plugin-import/config/stage-0'>; 215 | } 216 | declare module 'eslint-plugin-import/config/warnings.js' { 217 | declare module.exports: $Exports<'eslint-plugin-import/config/warnings'>; 218 | } 219 | declare module 'eslint-plugin-import/lib/core/importType.js' { 220 | declare module.exports: $Exports<'eslint-plugin-import/lib/core/importType'>; 221 | } 222 | declare module 'eslint-plugin-import/lib/core/staticRequire.js' { 223 | declare module.exports: $Exports<'eslint-plugin-import/lib/core/staticRequire'>; 224 | } 225 | declare module 'eslint-plugin-import/lib/ExportMap.js' { 226 | declare module.exports: $Exports<'eslint-plugin-import/lib/ExportMap'>; 227 | } 228 | declare module 'eslint-plugin-import/lib/importDeclaration.js' { 229 | declare module.exports: $Exports<'eslint-plugin-import/lib/importDeclaration'>; 230 | } 231 | declare module 'eslint-plugin-import/lib/index.js' { 232 | declare module.exports: $Exports<'eslint-plugin-import/lib/index'>; 233 | } 234 | declare module 'eslint-plugin-import/lib/rules/default.js' { 235 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/default'>; 236 | } 237 | declare module 'eslint-plugin-import/lib/rules/export.js' { 238 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/export'>; 239 | } 240 | declare module 'eslint-plugin-import/lib/rules/extensions.js' { 241 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/extensions'>; 242 | } 243 | declare module 'eslint-plugin-import/lib/rules/first.js' { 244 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/first'>; 245 | } 246 | declare module 'eslint-plugin-import/lib/rules/imports-first.js' { 247 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/imports-first'>; 248 | } 249 | declare module 'eslint-plugin-import/lib/rules/max-dependencies.js' { 250 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/max-dependencies'>; 251 | } 252 | declare module 'eslint-plugin-import/lib/rules/named.js' { 253 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/named'>; 254 | } 255 | declare module 'eslint-plugin-import/lib/rules/namespace.js' { 256 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/namespace'>; 257 | } 258 | declare module 'eslint-plugin-import/lib/rules/newline-after-import.js' { 259 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/newline-after-import'>; 260 | } 261 | declare module 'eslint-plugin-import/lib/rules/no-absolute-path.js' { 262 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-absolute-path'>; 263 | } 264 | declare module 'eslint-plugin-import/lib/rules/no-amd.js' { 265 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-amd'>; 266 | } 267 | declare module 'eslint-plugin-import/lib/rules/no-commonjs.js' { 268 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-commonjs'>; 269 | } 270 | declare module 'eslint-plugin-import/lib/rules/no-deprecated.js' { 271 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-deprecated'>; 272 | } 273 | declare module 'eslint-plugin-import/lib/rules/no-duplicates.js' { 274 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-duplicates'>; 275 | } 276 | declare module 'eslint-plugin-import/lib/rules/no-dynamic-require.js' { 277 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-dynamic-require'>; 278 | } 279 | declare module 'eslint-plugin-import/lib/rules/no-extraneous-dependencies.js' { 280 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-extraneous-dependencies'>; 281 | } 282 | declare module 'eslint-plugin-import/lib/rules/no-internal-modules.js' { 283 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-internal-modules'>; 284 | } 285 | declare module 'eslint-plugin-import/lib/rules/no-mutable-exports.js' { 286 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-mutable-exports'>; 287 | } 288 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default-member.js' { 289 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-as-default-member'>; 290 | } 291 | declare module 'eslint-plugin-import/lib/rules/no-named-as-default.js' { 292 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-as-default'>; 293 | } 294 | declare module 'eslint-plugin-import/lib/rules/no-named-default.js' { 295 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-named-default'>; 296 | } 297 | declare module 'eslint-plugin-import/lib/rules/no-namespace.js' { 298 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-namespace'>; 299 | } 300 | declare module 'eslint-plugin-import/lib/rules/no-nodejs-modules.js' { 301 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-nodejs-modules'>; 302 | } 303 | declare module 'eslint-plugin-import/lib/rules/no-restricted-paths.js' { 304 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-restricted-paths'>; 305 | } 306 | declare module 'eslint-plugin-import/lib/rules/no-unassigned-import.js' { 307 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-unassigned-import'>; 308 | } 309 | declare module 'eslint-plugin-import/lib/rules/no-unresolved.js' { 310 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-unresolved'>; 311 | } 312 | declare module 'eslint-plugin-import/lib/rules/no-webpack-loader-syntax.js' { 313 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/no-webpack-loader-syntax'>; 314 | } 315 | declare module 'eslint-plugin-import/lib/rules/order.js' { 316 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/order'>; 317 | } 318 | declare module 'eslint-plugin-import/lib/rules/prefer-default-export.js' { 319 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/prefer-default-export'>; 320 | } 321 | declare module 'eslint-plugin-import/lib/rules/unambiguous.js' { 322 | declare module.exports: $Exports<'eslint-plugin-import/lib/rules/unambiguous'>; 323 | } 324 | declare module 'eslint-plugin-import/memo-parser/index.js' { 325 | declare module.exports: $Exports<'eslint-plugin-import/memo-parser/index'>; 326 | } 327 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/redux-logger_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 62bf4b1b7b6d07346f0aa2b8c5e0d94a 2 | // flow-typed version: <>/redux-logger_v^2.8.2/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'redux-logger' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'redux-logger' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'redux-logger/dist/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'redux-logger/dist/index.min' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'redux-logger/lib/core' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'redux-logger/lib/defaults' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'redux-logger/lib/diff' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'redux-logger/lib/helpers' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'redux-logger/lib/index' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'redux-logger/src/core' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'redux-logger/src/defaults' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'redux-logger/src/diff' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'redux-logger/src/helpers' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'redux-logger/src/index' { 70 | declare module.exports: any; 71 | } 72 | 73 | // Filename aliases 74 | declare module 'redux-logger/dist/index.js' { 75 | declare module.exports: $Exports<'redux-logger/dist/index'>; 76 | } 77 | declare module 'redux-logger/dist/index.min.js' { 78 | declare module.exports: $Exports<'redux-logger/dist/index.min'>; 79 | } 80 | declare module 'redux-logger/lib/core.js' { 81 | declare module.exports: $Exports<'redux-logger/lib/core'>; 82 | } 83 | declare module 'redux-logger/lib/defaults.js' { 84 | declare module.exports: $Exports<'redux-logger/lib/defaults'>; 85 | } 86 | declare module 'redux-logger/lib/diff.js' { 87 | declare module.exports: $Exports<'redux-logger/lib/diff'>; 88 | } 89 | declare module 'redux-logger/lib/helpers.js' { 90 | declare module.exports: $Exports<'redux-logger/lib/helpers'>; 91 | } 92 | declare module 'redux-logger/lib/index.js' { 93 | declare module.exports: $Exports<'redux-logger/lib/index'>; 94 | } 95 | declare module 'redux-logger/src/core.js' { 96 | declare module.exports: $Exports<'redux-logger/src/core'>; 97 | } 98 | declare module 'redux-logger/src/defaults.js' { 99 | declare module.exports: $Exports<'redux-logger/src/defaults'>; 100 | } 101 | declare module 'redux-logger/src/diff.js' { 102 | declare module.exports: $Exports<'redux-logger/src/diff'>; 103 | } 104 | declare module 'redux-logger/src/helpers.js' { 105 | declare module.exports: $Exports<'redux-logger/src/helpers'>; 106 | } 107 | declare module 'redux-logger/src/index.js' { 108 | declare module.exports: $Exports<'redux-logger/src/index'>; 109 | } 110 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ba132c96664f1a05288f3eb2272a3c35 2 | // flow-typed version: c4bbd91cfc/redux_v3.x.x/flow_>=v0.33.x 3 | 4 | declare module 'redux' { 5 | 6 | /* 7 | 8 | S = State 9 | A = Action 10 | 11 | */ 12 | 13 | declare type Dispatch }> = (action: A) => A; 14 | 15 | declare type MiddlewareAPI = { 16 | dispatch: Dispatch; 17 | getState(): S; 18 | }; 19 | 20 | declare type Store = { 21 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 22 | dispatch: Dispatch; 23 | getState(): S; 24 | subscribe(listener: () => void): () => void; 25 | replaceReducer(nextReducer: Reducer): void 26 | }; 27 | 28 | declare type Reducer = (state: S, action: A) => S; 29 | 30 | declare type Middleware = 31 | (api: MiddlewareAPI) => 32 | (next: Dispatch) => Dispatch; 33 | 34 | declare type StoreCreator = { 35 | (reducer: Reducer, enhancer?: StoreEnhancer): Store; 36 | (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; 37 | }; 38 | 39 | declare type StoreEnhancer = (next: StoreCreator) => StoreCreator; 40 | 41 | declare function createStore(reducer: Reducer, enhancer?: StoreEnhancer): Store; 42 | declare function createStore(reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; 43 | 44 | declare function applyMiddleware(...middlewares: Array>): StoreEnhancer; 45 | 46 | declare type ActionCreator = (...args: Array) => A; 47 | declare type ActionCreators = { [key: K]: ActionCreator }; 48 | 49 | declare function bindActionCreators>(actionCreator: C, dispatch: Dispatch): C; 50 | declare function bindActionCreators>(actionCreators: C, dispatch: Dispatch): C; 51 | 52 | declare function combineReducers(reducers: O): Reducer<$ObjMap(r: Reducer) => S>, A>; 53 | 54 | declare function compose(...fns: Array>): Function; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@redux-offline/redux-offline", 3 | "version": "2.2.0", 4 | "description": "Redux Offline-First Architecture", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib --ignore '**/__tests__/**'", 8 | "flow:start": "flow server", 9 | "flow:stop": "flow stop", 10 | "flow": "flow; test $? -eq 0 -o $? -eq 2", 11 | "lint": "eslint src/", 12 | "prepublish": "npm run lint && npm run flow && npm run test && npm run build", 13 | "prettier": "eslint src --fix", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "watch": "npm run build -- --watch" 17 | }, 18 | "jest": { 19 | "rootDir": "src" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/redux-offline/redux-offline" 24 | }, 25 | "author": "", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/redux-offline/redux-offline/issues" 29 | }, 30 | "homepage": "https://github.com/redux-offline/redux-offline#readme", 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-core": "^6.26.0", 34 | "babel-eslint": "^7.2.3", 35 | "babel-plugin-transform-class-properties": "^6.24.1", 36 | "babel-plugin-transform-flow-strip-types": "^6.18.0", 37 | "babel-preset-latest": "^6.24.1", 38 | "babel-preset-stage-3": "^6.24.1", 39 | "eslint": "^4.6.1", 40 | "eslint-config-airbnb-base": "^12.0.0", 41 | "eslint-config-prettier": "^2.4.0", 42 | "eslint-plugin-babel": "^4.1.1", 43 | "eslint-plugin-import": "^2.7.0", 44 | "eslint-plugin-prettier": "^2.2.0", 45 | "flow-bin": "^0.42.0", 46 | "jest": "^19.0.2", 47 | "prettier": "^1.6.1", 48 | "react": "^15.4.2", 49 | "redux": "^3.6.0", 50 | "redux-devtools-instrument": "^1.8.2", 51 | "redux-logger": "^2.8.2", 52 | "redux-persist-node-storage": "^1.0.2" 53 | }, 54 | "dependencies": { 55 | "redux-persist": "^4.5.0" 56 | }, 57 | "peerDependencies": { 58 | "redux": ">=3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/__tests__/defaults/effect.js: -------------------------------------------------------------------------------- 1 | import effectReconciler from '../../defaults/effect'; 2 | 3 | function fetch(body) { 4 | return Promise.resolve({ 5 | ok: true, 6 | headers: { get: jest.fn(() => 'application/json') }, 7 | text: jest.fn(() => Promise.resolve(body)) 8 | }); 9 | } 10 | 11 | let globalFetch; 12 | 13 | beforeAll(() => { 14 | globalFetch = global.fetch; 15 | }); 16 | afterAll(() => { 17 | global.fetch = globalFetch; 18 | }); 19 | 20 | test('effector accept JSON stringified object', () => { 21 | const body = { 22 | email: 'email@example.com', 23 | password: 'p4ssw0rd' 24 | }; 25 | 26 | global.fetch = jest.fn((url, options) => { 27 | expect(options.headers['content-type']).toEqual('application/json'); 28 | expect(JSON.parse(options.body)).toEqual(body); 29 | 30 | return fetch(''); 31 | }); 32 | 33 | return effectReconciler({ body: JSON.stringify(body) }).then(body2 => { 34 | expect(body2).toEqual(null); 35 | }); 36 | }); 37 | 38 | test('effector receive JSON and response objects', () => { 39 | const body = { id: 1234 }; 40 | 41 | global.fetch = jest.fn(() => fetch(JSON.stringify(body))); 42 | 43 | return effectReconciler({}).then(body2 => { 44 | expect(body2).toEqual(body); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from "redux"; 2 | import { KEY_PREFIX } from "redux-persist/lib/constants" 3 | import { AsyncNodeStorage } from "redux-persist-node-storage"; 4 | import instrument from "redux-devtools-instrument"; 5 | import { createOffline, offline } from "../index"; 6 | import { applyDefaults } from "../config"; 7 | 8 | const storage = new AsyncNodeStorage("/tmp/storageDir"); 9 | const storageKey = `${KEY_PREFIX}offline`; 10 | function noop() {} 11 | 12 | beforeEach(() => storage.removeItem(storageKey, noop) ); 13 | 14 | const defaultConfig = applyDefaults({ 15 | effect: jest.fn(() => Promise.resolve()), 16 | persistOptions: { storage } 17 | }); 18 | 19 | function defaultReducer(state = { 20 | offline: { 21 | busy: false, 22 | lastTransaction: 0, 23 | online: true, 24 | outbox: [], 25 | receipts: [], 26 | retryToken: 0, 27 | retryCount: 0, 28 | retryScheduled: false, 29 | netInfo: { 30 | isConnectionExpensive: null, 31 | reach: 'none' 32 | } 33 | } 34 | }) { 35 | return state; 36 | } 37 | 38 | test("offline() creates storeEnhancer", () => { 39 | const storeEnhancer = offline(defaultConfig); 40 | 41 | const store = storeEnhancer(createStore)(defaultReducer); 42 | expect(store.dispatch).toEqual(expect.any(Function)); 43 | expect(store.getState).toEqual(expect.any(Function)); 44 | }); 45 | 46 | test("createOffline() creates storeEnhancer", () => { 47 | const { middleware, enhanceReducer, enhanceStore } = 48 | createOffline(defaultConfig); 49 | const reducer = enhanceReducer(defaultReducer); 50 | const store = createStore(reducer, compose( 51 | applyMiddleware(middleware), 52 | enhanceStore 53 | )); 54 | expect(store.dispatch).toEqual(expect.any(Function)); 55 | expect(store.getState).toEqual(expect.any(Function)); 56 | }); 57 | 58 | // see https://github.com/redux-offline/redux-offline/issues/31 59 | test("supports HMR by overriding `replaceReducer()`", () => { 60 | const store = offline(defaultConfig)(createStore)(defaultReducer); 61 | store.replaceReducer(defaultReducer); 62 | store.dispatch({ type: "SOME_ACTION" }); 63 | expect(store.getState()).toHaveProperty("offline"); 64 | }); 65 | 66 | // see https://github.com/redux-offline/redux-offline/issues/4 67 | test("restores offline outbox when rehydrates", () => { 68 | const actions = [{ 69 | type: "SOME_OFFLINE_ACTION", 70 | meta: { offline: { effect: {} } } 71 | }]; 72 | storage.setItem( 73 | storageKey, 74 | JSON.stringify({ outbox: actions }), 75 | noop 76 | ); 77 | 78 | expect.assertions(1); 79 | return new Promise(resolve => { 80 | const store = offline({ 81 | ...defaultConfig, 82 | persistCallback() { 83 | const { offline: { outbox } } = store.getState(); 84 | expect(outbox).toEqual(actions); 85 | resolve(); 86 | } 87 | })(createStore)(defaultReducer); 88 | }); 89 | }); 90 | 91 | // see https://github.com/jevakallio/redux-offline/pull/91 92 | test("works with devtools store enhancer", () => { 93 | const monitorReducer = state => state; 94 | const store = createStore( 95 | defaultReducer, 96 | compose(offline(defaultConfig), instrument(monitorReducer)) 97 | ); 98 | 99 | expect(() => { 100 | store.dispatch({ type: "SOME_ACTION" }); 101 | }).not.toThrow(); 102 | }); 103 | -------------------------------------------------------------------------------- /src/__tests__/middleware.js: -------------------------------------------------------------------------------- 1 | import { createOfflineMiddleware } from '../middleware'; 2 | import { completeRetry, scheduleRetry } from '../actions'; 3 | import { OFFLINE_SEND } from '../constants'; 4 | import send from '../send'; 5 | 6 | import offlineStateLens from '../defaults/offlineStateLens' 7 | 8 | const offlineAction = { 9 | type: 'OFFLINE_ACTION_REQUEST', 10 | meta: { 11 | offline: { 12 | effect: { url: '/api/endpoint', method: 'POST' }, 13 | commit: { type: 'OFFLINE_ACTION_COMMIT' }, 14 | rollback: { type: 'OFFLINE_ACTION_ROLLBACK' } 15 | } 16 | } 17 | }; 18 | 19 | const defaultOfflineState = { 20 | busy: false, 21 | lastTransaction: 0, 22 | online: true, 23 | outbox: [offlineAction], 24 | receipts: [], 25 | retryToken: 0, 26 | retryCount: 0, 27 | retryScheduled: false, 28 | netInfo: { 29 | isConnectionExpensive: null, 30 | reach: 'NONE' 31 | } 32 | }; 33 | 34 | function setup(offlineState = {}) { 35 | const state = { 36 | offline: { ...defaultOfflineState, ...offlineState } 37 | }; 38 | return { 39 | config: { 40 | rehydrate: false, 41 | persist: null, 42 | detectNetwork: null, 43 | batch: jest.fn(outbox => outbox.slice(0, 1)), 44 | effect: jest.fn(), 45 | retry: jest.fn(), 46 | discard: jest.fn(), 47 | offlineStateLens, 48 | }, 49 | store: { 50 | getState: jest.fn(() => state), 51 | dispatch: jest.fn() 52 | }, 53 | next: jest.fn(action => ({ actions: [action] })), 54 | action: { type: 'NOT_OFFLINE_ACTION' } 55 | }; 56 | } 57 | 58 | // NOTE: there is not currently an action creator for this 59 | function offlineSend() { 60 | return { type: OFFLINE_SEND }; 61 | } 62 | 63 | jest.mock('../send', () => jest.fn(() => Promise.resolve())); 64 | beforeEach(send.mockClear); 65 | 66 | test('creates middleware', () => { 67 | const { config, store, next, action } = setup(); 68 | const middleware = createOfflineMiddleware(config); 69 | 70 | const result = middleware(store)(next)(action); 71 | expect(next).toBeCalled(); 72 | expect(result).toEqual(next(action)); 73 | }); 74 | 75 | describe('on any action', () => { 76 | it('processes outbox when idle', () => { 77 | const { config, store, next, action } = setup(); 78 | createOfflineMiddleware(config)(store)(next)(action); 79 | expect(send).toBeCalled(); 80 | }); 81 | 82 | it('does not process outbox when busy', () => { 83 | const { config, store, next, action } = setup({ busy: true }); 84 | createOfflineMiddleware(config)(store)(next)(action); 85 | expect(send).not.toBeCalled(); 86 | }); 87 | 88 | it('does not process outbox when retry scheduled', () => { 89 | const { config, store, next, action } = setup({ retryScheduled: true }); 90 | createOfflineMiddleware(config)(store)(next)(action); 91 | expect(send).not.toBeCalled(); 92 | }); 93 | 94 | it('does not process outbox when offline', () => { 95 | const { config, store, next, action } = setup({ online: false }); 96 | createOfflineMiddleware(config)(store)(next)(action); 97 | expect(send).not.toBeCalled(); 98 | }); 99 | }); 100 | 101 | // TODO: test for double dispatch 102 | describe('on OFFLINE_SEND', () => { 103 | it('processes outbox when idle', () => { 104 | const { config, store, next } = setup(); 105 | createOfflineMiddleware(config)(store)(next)(offlineSend()); 106 | expect(send).toBeCalled(); 107 | }); 108 | 109 | it('does not process outbox when busy', () => { 110 | const { config, store, next } = setup({ busy: true }); 111 | createOfflineMiddleware(config)(store)(next)(offlineSend()); 112 | expect(send).not.toBeCalled(); 113 | }); 114 | 115 | it('processes outbox when retry scheduled', () => { 116 | const { config, store, next } = setup({ retryScheduled: true }); 117 | createOfflineMiddleware(config)(store)(next)(offlineSend()); 118 | expect(send).toBeCalled(); 119 | }); 120 | 121 | it('processes outbox when offline', () => { 122 | const { config, store, next } = setup({ online: false }); 123 | createOfflineMiddleware(config)(store)(next)(offlineSend()); 124 | expect(send).toBeCalled(); 125 | }); 126 | }); 127 | 128 | // FIXME: completeRetry is supposed to be called with an action 129 | // TODO: wrapping `setTimeout()` in a promise in `after()` is pointless 130 | describe('on OFFLINE_SCHEDULE_RETRY', () => { 131 | jest.useFakeTimers(); 132 | const delay = 15000; 133 | 134 | test('dispatches COMPLETE_RETRY after delay', () => { 135 | const { config, store, next } = setup(); 136 | createOfflineMiddleware(config)(store)(next)(scheduleRetry(delay)); 137 | jest.runTimersToTime(delay); 138 | 139 | expect.assertions(1); 140 | const nextAction = store.getState().offline.outbox[0]; 141 | return Promise.resolve().then(() => 142 | expect(store.dispatch).toBeCalledWith(completeRetry(nextAction))); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/__tests__/send.js: -------------------------------------------------------------------------------- 1 | import send from '../send'; 2 | import { busy, scheduleRetry } from '../actions'; 3 | import defaultCommitAction from '../defaults/defaultCommit'; 4 | import defaultRollbackAction from '../defaults/defaultRollback'; 5 | 6 | const DELAY = 1000; 7 | const completedMeta = { 8 | meta: expect.objectContaining({ completed: expect.any(Boolean) }) 9 | }; 10 | 11 | function setup(partialConfig) { 12 | const defaultConfig = { 13 | effect: jest.fn(() => Promise.resolve()), 14 | discard: () => false, 15 | retry: () => DELAY, 16 | defaultCommit: defaultCommitAction, 17 | defaultRollback: defaultRollbackAction, 18 | }; 19 | 20 | return { 21 | action: { 22 | type: 'REQUEST', 23 | meta: { 24 | offline: { 25 | effect: { url: '/api/resource', method: 'get' }, 26 | commit: { type: 'COMMIT' }, 27 | rollback: { type: 'ROLLBACK' } 28 | } 29 | } 30 | }, 31 | config: { ...defaultConfig, ...partialConfig }, 32 | dispatch: jest.fn() 33 | }; 34 | } 35 | 36 | test('dispatches busy action', () => { 37 | const { action, config, dispatch } = setup(); 38 | send(action, dispatch, config); 39 | expect(dispatch).toBeCalledWith(busy(true)); 40 | }); 41 | 42 | test('requests resource using effects reconciler', () => { 43 | const { action, config, dispatch } = setup(); 44 | send(action, dispatch, config); 45 | expect(config.effect).toBeCalledWith(action.meta.offline.effect, action); 46 | }); 47 | 48 | describe('when request succeeds', () => { 49 | test('dispatches complete action', () => { 50 | const effect = () => Promise.resolve(); 51 | const { action, config, dispatch } = setup({ effect }); 52 | const promise = send(action, dispatch, config); 53 | 54 | const { commit } = action.meta.offline; 55 | expect.assertions(2); 56 | return promise.then(() => { 57 | expect(dispatch).toBeCalledWith(expect.objectContaining(commit)); 58 | expect(dispatch).toBeCalledWith(expect.objectContaining(completedMeta)); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('when request fails', () => { 64 | test('dispatches schedule retry action', () => { 65 | const effect = () => Promise.reject(); 66 | const { action, config, dispatch } = setup({ effect }); 67 | const promise = send(action, dispatch, config); 68 | 69 | expect.assertions(1); 70 | return promise.then(() => { 71 | expect(dispatch).toBeCalledWith(scheduleRetry(DELAY)); 72 | }); 73 | }); 74 | 75 | test('dispatches complete action on discard', () => { 76 | const effect = () => Promise.reject(); 77 | const discard = () => true; 78 | const { action, config, dispatch } = setup({ effect, discard }); 79 | const promise = send(action, dispatch, config); 80 | 81 | const { rollback } = action.meta.offline; 82 | expect.assertions(2); 83 | return promise.then(() => { 84 | expect(dispatch).toBeCalledWith(expect.objectContaining(rollback)); 85 | expect(dispatch).toBeCalledWith(expect.objectContaining(completedMeta)); 86 | }); 87 | }); 88 | 89 | test('dispatches complete action with promised discard', () => { 90 | const effect = () => Promise.reject(); 91 | const discard = () => Promise.resolve(true); 92 | const { action, config, dispatch } = setup({ effect, discard }); 93 | const promise = send(action, dispatch, config); 94 | 95 | const { rollback } = action.meta.offline; 96 | expect.assertions(2); 97 | return promise.then(() => { 98 | expect(dispatch).toBeCalledWith(expect.objectContaining(rollback)); 99 | expect(dispatch).toBeCalledWith(expect.objectContaining(completedMeta)); 100 | }); 101 | }); 102 | 103 | test('dispatches complete action when discard throw an exception', () => { 104 | const effect = () => Promise.reject(); 105 | const discard = () => {throw new Error}; 106 | const { action, config, dispatch } = setup({ effect, discard }); 107 | const promise = send(action, dispatch, config); 108 | 109 | const { rollback } = action.meta.offline; 110 | expect.assertions(2); 111 | return promise.then(() => { 112 | expect(dispatch).toBeCalledWith(expect.objectContaining(rollback)); 113 | expect(dispatch).toBeCalledWith(expect.objectContaining(completedMeta)); 114 | }); 115 | }); 116 | }); 117 | 118 | describe('when request succeeds and commit is undefined', () => { 119 | test('dispatches default commit action', () => { 120 | const effect = () => Promise.resolve(); 121 | 122 | const action = { 123 | type: 'REQUEST', 124 | meta: { 125 | offline: { 126 | effect: { type: 'MOCK' }, 127 | }, 128 | }, 129 | }; 130 | 131 | const { config, dispatch } = setup({ effect }); 132 | 133 | const promise = send(action, dispatch, config) 134 | 135 | return promise.then(() => { 136 | expect(dispatch).toBeCalledWith(expect.objectContaining(defaultCommitAction)); 137 | expect(dispatch).toBeCalledWith(expect.objectContaining(completedMeta)); 138 | }); 139 | }); 140 | }); 141 | 142 | describe('when request is to be discarded and rollback is undefined', () => { 143 | test('dispatches default rollback action', () => { 144 | const effect = () => Promise.reject(); 145 | const discard = () => true; 146 | 147 | const action = { 148 | type: 'REQUEST', 149 | meta: { 150 | offline: { 151 | effect: { type: 'MOCK' }, 152 | }, 153 | }, 154 | }; 155 | 156 | const { config, dispatch } = setup({ effect, discard }); 157 | 158 | const promise = send(action, dispatch, config) 159 | 160 | return promise.then(() => { 161 | expect(dispatch).toBeCalledWith(expect.objectContaining(defaultRollbackAction)); 162 | expect(dispatch).toBeCalledWith(expect.objectContaining(completedMeta)); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | OFFLINE_STATUS_CHANGED, 3 | OFFLINE_SCHEDULE_RETRY, 4 | OFFLINE_COMPLETE_RETRY, 5 | OFFLINE_BUSY 6 | } from './constants'; 7 | 8 | export const networkStatusChanged = ({ online, netInfo }) => ({ 9 | type: OFFLINE_STATUS_CHANGED, 10 | payload: { 11 | online, 12 | netInfo 13 | } 14 | }); 15 | 16 | export const scheduleRetry = (delay = 0) => ({ 17 | type: OFFLINE_SCHEDULE_RETRY, 18 | payload: { 19 | delay 20 | } 21 | }); 22 | 23 | export const completeRetry = action => ({ 24 | type: OFFLINE_COMPLETE_RETRY, 25 | payload: action 26 | }); 27 | 28 | export const busy = isBusy => ({ 29 | type: OFFLINE_BUSY, 30 | payload: { busy: isBusy } 31 | }); 32 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* global $Shape */ 3 | import type { Config } from './types'; 4 | import defaults from './defaults'; 5 | 6 | export const applyDefaults = (config: $Shape = {}): Config => ({ 7 | ...defaults, 8 | ...config 9 | }); 10 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const OFFLINE_STATUS_CHANGED = 'Offline/STATUS_CHANGED'; 2 | export const OFFLINE_SCHEDULE_RETRY = 'Offline/SCHEDULE_RETRY'; 3 | export const OFFLINE_COMPLETE_RETRY = 'Offline/COMPLETE_RETRY'; 4 | export const OFFLINE_SEND = 'Offline/SEND'; 5 | export const OFFLINE_BUSY = 'Offline/BUSY'; 6 | export const RESET_STATE = 'Offline/RESET_STATE'; 7 | export const PERSIST_REHYDRATE = 'persist/REHYDRATE'; 8 | export const JS_ERROR = 'Offline/JS_ERROR'; 9 | export const DEFAULT_COMMIT = 'Offline/DEFAULT_COMMIT'; 10 | export const DEFAULT_ROLLBACK = 'Offline/DEFAULT_ROLLBACK'; 11 | -------------------------------------------------------------------------------- /src/defaults/defaultCommit.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_COMMIT } from '../constants'; 2 | 3 | const defaultCommit = { 4 | type: DEFAULT_COMMIT 5 | }; 6 | 7 | export default defaultCommit; 8 | -------------------------------------------------------------------------------- /src/defaults/defaultRollback.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_ROLLBACK } from '../constants'; 2 | 3 | const defaultRollback = { 4 | type: DEFAULT_ROLLBACK 5 | }; 6 | 7 | export default defaultRollback; 8 | -------------------------------------------------------------------------------- /src/defaults/detectNetwork.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | 3 | const handle = (callback, online) => { 4 | // NetInfo is not supported in browsers, hence we only pass online status 5 | if (window.requestAnimationFrame) { 6 | window.requestAnimationFrame(() => callback({ online })); 7 | } else { 8 | setTimeout(() => callback({ online }), 0); 9 | } 10 | }; 11 | 12 | export default callback => { 13 | if (typeof window !== 'undefined' && window.addEventListener) { 14 | window.addEventListener('online', () => handle(callback, true)); 15 | window.addEventListener('offline', () => handle(callback, false)); 16 | handle(callback, window.navigator.onLine); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/defaults/detectNetwork.native.js: -------------------------------------------------------------------------------- 1 | /* eslint no-underscore-dangle: 0 */ 2 | import { AppState, NetInfo } from 'react-native'; // eslint-disable-line 3 | import LegacyDetectNetwork from './detectNetwork.native.legacy'; 4 | 5 | class DetectNetwork { 6 | constructor(callback) { 7 | this._reach = null; 8 | this._isConnected = null; 9 | this._isConnectionExpensive = null; 10 | this._callback = callback; 11 | this._shouldInitUpdateReach = true; 12 | 13 | this._init(); 14 | this._addListeners(); 15 | } 16 | 17 | /** 18 | * Check props for changes 19 | * @param {string} reach - connection reachability. 20 | * - Cross-platform: [none, wifi, cellular, unknown] 21 | * - Android: [bluetooth, ethernet, wimax] 22 | * @returns {boolean} - Whether the connection reachability or the connection props have changed 23 | * @private 24 | */ 25 | _hasChanged = reach => { 26 | if (this._reach !== reach) { 27 | return true; 28 | } 29 | if (this._isConnected !== this._getConnection(reach)) { 30 | return true; 31 | } 32 | return false; 33 | }; 34 | /** 35 | * Sets the connection reachability prop 36 | * @param {string} reach - connection reachability. 37 | * - Cross-platform: [none, wifi, cellular, unknown] 38 | * - Android: [bluetooth, ethernet, wimax] 39 | * @returns {void} 40 | * @private 41 | */ 42 | _setReach = reach => { 43 | this._reach = reach; 44 | this._isConnected = this._getConnection(reach); 45 | }; 46 | /** 47 | * Gets the isConnected prop depending on the connection reachability's value 48 | * @param {string} reach - connection reachability. 49 | * - Cross-platform: [none, wifi, cellular, unknown] 50 | * - Android: [bluetooth, ethernet, wimax] 51 | * @returns {void} 52 | * @private 53 | */ 54 | _getConnection = reach => reach !== 'none' && reach !== 'unknown'; 55 | /** 56 | * Sets the isConnectionExpensive prop 57 | * @returns {Promise.} Resolves to true if connection is expensive, 58 | * false if not, and null if not supported. 59 | * @private 60 | */ 61 | _setIsConnectionExpensive = async () => { 62 | try { 63 | this._isConnectionExpensive = await NetInfo.isConnectionExpensive(); 64 | } catch (err) { 65 | // err means that isConnectionExpensive is not supported in iOS 66 | this._isConnectionExpensive = null; 67 | } 68 | }; 69 | /** 70 | * Sets the shouldInitUpdateReach flag 71 | * @param {boolean} shouldUpdate - Whether the init method should update the reach prop 72 | * @returns {void} 73 | * @private 74 | */ 75 | _setShouldInitUpdateReach = shouldUpdate => { 76 | this._shouldInitUpdateReach = shouldUpdate; 77 | }; 78 | /** 79 | * Fetches and sets the connection reachability and the isConnected props, 80 | * if neither of the AppState and NetInfo event listeners have been called 81 | * @returns {Promise.} Resolves when the props have been 82 | * initialized and update. 83 | * @private 84 | */ 85 | _init = async () => { 86 | const connectionInfo = await NetInfo.getConnectionInfo(); 87 | if (this._shouldInitUpdateReach) { 88 | this._update(connectionInfo.type); 89 | } 90 | }; 91 | /** 92 | * Check changes on props and store and dispatch if neccesary 93 | * @param {string} reach - connection reachability. 94 | * - Cross-platform: [none, wifi, cellular, unknown] 95 | * - Android: [bluetooth, ethernet, wimax] 96 | * @returns {void} 97 | * @private 98 | */ 99 | _update = reach => { 100 | if (this._hasChanged(reach)) { 101 | this._setReach(reach); 102 | this._dispatch(); 103 | } 104 | }; 105 | 106 | /** 107 | * Adds listeners for when connection reachability and app state changes to update props 108 | * @returns {void} 109 | * @private 110 | */ 111 | _addListeners() { 112 | NetInfo.addEventListener('connectionChange', connectionInfo => { 113 | this._setShouldInitUpdateReach(false); 114 | this._update(connectionInfo.type); 115 | }); 116 | AppState.addEventListener('change', async () => { 117 | this._setShouldInitUpdateReach(false); 118 | const connectionInfo = await NetInfo.getConnectionInfo(); 119 | this._update(connectionInfo.type); 120 | }); 121 | } 122 | 123 | /** 124 | * Executes the given callback to update redux's store with the new internal props 125 | * @returns {Promise.} Resolves after fetching the isConnectionExpensive 126 | * and dispatches actions 127 | * @private 128 | */ 129 | _dispatch = async () => { 130 | await this._setIsConnectionExpensive(); 131 | this._callback({ 132 | online: this._isConnected, 133 | netInfo: { 134 | isConnectionExpensive: this._isConnectionExpensive, 135 | reach: this._reach 136 | } 137 | }); 138 | }; 139 | } 140 | 141 | const isLegacy = typeof NetInfo.getConnectionInfo === 'undefined'; 142 | export default callback => 143 | isLegacy ? new LegacyDetectNetwork(callback) : new DetectNetwork(callback); 144 | -------------------------------------------------------------------------------- /src/defaults/detectNetwork.native.legacy.js: -------------------------------------------------------------------------------- 1 | /* eslint no-underscore-dangle: 0 */ 2 | import { AppState, NetInfo } from 'react-native'; // eslint-disable-line 3 | 4 | class LegacyDetectNetwork { 5 | constructor(callback) { 6 | this._reach = null; 7 | this._isConnected = null; 8 | this._isConnectionExpensive = null; 9 | this._callback = callback; 10 | this._shouldInitUpdateReach = true; 11 | 12 | this._init(); 13 | this._addListeners(); 14 | } 15 | 16 | /** 17 | * Check props for changes 18 | * @param {string} reach - connection reachability. 19 | * - iOS: [none, wifi, cell, unknown] 20 | * - Android: [NONE, BLUETOOTH, DUMMY, ETHERNET, MOBILE, MOBILE_DUN, MOBILE_HIPRI, 21 | * MOBILE_MMS, MOBILE_SUPL, VPN, WIFI, WIMAX, UNKNOWN] 22 | * @returns {boolean} - Whether the connection reachability or the connection props have changed 23 | * @private 24 | */ 25 | _hasChanged = reach => { 26 | if (this._reach !== reach) { 27 | return true; 28 | } 29 | if (this._isConnected !== this._getConnection(reach)) { 30 | return true; 31 | } 32 | return false; 33 | }; 34 | /** 35 | * Sets the connection reachability prop 36 | * @param {string} reach - connection reachability. 37 | * - iOS: [none, wifi, cell, unknown] 38 | * - Android: [NONE, BLUETOOTH, DUMMY, ETHERNET, MOBILE, MOBILE_DUN, MOBILE_HIPRI, 39 | * MOBILE_MMS, MOBILE_SUPL, VPN, WIFI, WIMAX, UNKNOWN] 40 | * @returns {void} 41 | * @private 42 | */ 43 | _setReach = reach => { 44 | this._reach = reach; 45 | this._isConnected = this._getConnection(reach); 46 | }; 47 | /** 48 | * Gets the isConnected prop depending on the connection reachability's value 49 | * @param {string} reach - connection reachability. 50 | * - iOS: [none, wifi, cell, unknown] 51 | * - Android: [NONE, BLUETOOTH, DUMMY, ETHERNET, MOBILE, MOBILE_DUN, MOBILE_HIPRI, 52 | * MOBILE_MMS, MOBILE_SUPL, VPN, WIFI, WIMAX, UNKNOWN] 53 | * @returns {void} 54 | * @private 55 | */ 56 | _getConnection = reach => reach !== 'NONE' && reach !== 'UNKNOWN'; 57 | /** 58 | * Sets the isConnectionExpensive prop 59 | * @returns {Promise.} Resolves to true if connection is expensive, 60 | * false if not, and null if not supported. 61 | * @private 62 | */ 63 | _setIsConnectionExpensive = async () => { 64 | try { 65 | this._isConnectionExpensive = await NetInfo.isConnectionExpensive(); 66 | } catch (err) { 67 | // err means that isConnectionExpensive is not supported in iOS 68 | this._isConnectionExpensive = null; 69 | } 70 | }; 71 | /** 72 | * Sets the shouldInitUpdateReach flag 73 | * @param {boolean} shouldUpdate - Whether the init method should update the reach prop 74 | * @returns {void} 75 | * @private 76 | */ 77 | _setShouldInitUpdateReach = shouldUpdate => { 78 | this._shouldInitUpdateReach = shouldUpdate; 79 | }; 80 | /** 81 | * Fetches and sets the connection reachability and the isConnected props, 82 | * if neither of the AppState and NetInfo event listeners have been called 83 | * @returns {Promise.} Resolves when the props have been 84 | * initialized and update. 85 | * @private 86 | */ 87 | _init = async () => { 88 | const reach = await NetInfo.fetch(); 89 | if (this._shouldInitUpdateReach) { 90 | this._update(reach); 91 | } 92 | }; 93 | /** 94 | * Check changes on props and store and dispatch if neccesary 95 | * @param {string} reach - connection reachability. 96 | * - iOS: [none, wifi, cell, unknown] 97 | * - Android: [NONE, BLUETOOTH, DUMMY, ETHERNET, MOBILE, MOBILE_DUN, MOBILE_HIPRI, 98 | * MOBILE_MMS, MOBILE_SUPL, VPN, WIFI, WIMAX, UNKNOWN] 99 | * @returns {void} 100 | * @private 101 | */ 102 | _update = reach => { 103 | const normalizedReach = reach.toUpperCase(); 104 | if (this._hasChanged(normalizedReach)) { 105 | this._setReach(normalizedReach); 106 | this._dispatch(); 107 | } 108 | }; 109 | 110 | /** 111 | * Adds listeners for when connection reachability and app state changes to update props 112 | * @returns {void} 113 | * @private 114 | */ 115 | _addListeners() { 116 | NetInfo.addEventListener('change', reach => { 117 | this._setShouldInitUpdateReach(false); 118 | this._update(reach); 119 | }); 120 | AppState.addEventListener('change', async () => { 121 | this._setShouldInitUpdateReach(false); 122 | const reach = await NetInfo.fetch(); 123 | this._update(reach); 124 | }); 125 | } 126 | 127 | /** 128 | * Executes the given callback to update redux's store with the new internal props 129 | * @returns {Promise.} Resolves after fetching the isConnectionExpensive 130 | * and dispatches actions 131 | * @private 132 | */ 133 | _dispatch = async () => { 134 | await this._setIsConnectionExpensive(); 135 | this._callback({ 136 | online: this._isConnected, 137 | netInfo: { 138 | isConnectionExpensive: this._isConnectionExpensive, 139 | reach: this._reach 140 | } 141 | }); 142 | }; 143 | } 144 | 145 | export default LegacyDetectNetwork; 146 | -------------------------------------------------------------------------------- /src/defaults/discard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { OfflineAction } from '../types'; 4 | import { NetworkError } from './effect'; 5 | 6 | export default ( 7 | error: NetworkError, 8 | action: OfflineAction, 9 | _retries: number = 0 // eslint-disable-line no-unused-vars 10 | ): boolean => { 11 | // not a network error -> discard 12 | if (!('status' in error)) { 13 | return true; 14 | } 15 | 16 | // discard http 4xx errors 17 | // $FlowFixMe 18 | return error.status >= 400 && error.status < 500; 19 | }; 20 | -------------------------------------------------------------------------------- /src/defaults/effect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* global fetch */ 3 | 4 | import type { OfflineAction } from '../types'; 5 | 6 | export function NetworkError(response: {} | string, status: number) { 7 | this.name = 'NetworkError'; 8 | this.status = status; 9 | this.response = response; 10 | } 11 | 12 | // $FlowFixMe 13 | NetworkError.prototype = Error.prototype; 14 | NetworkError.prototype.status = null; 15 | 16 | const tryParseJSON = (json: string): ?{} => { 17 | if (!json) { 18 | return null; 19 | } 20 | try { 21 | return JSON.parse(json); 22 | } catch (e) { 23 | throw new Error(`Failed to parse unexpected JSON response: ${json}`); 24 | } 25 | }; 26 | 27 | const getResponseBody = (res: any): Promise<{} | string> => { 28 | const contentType = res.headers.get('content-type') || false; 29 | if (contentType && contentType.indexOf('json') >= 0) { 30 | return res.text().then(tryParseJSON); 31 | } 32 | return res.text(); 33 | }; 34 | 35 | // eslint-disable-next-line no-unused-vars 36 | export default (effect: any, _action: OfflineAction): Promise => { 37 | const { url, ...options } = effect; 38 | const headers = { 'content-type': 'application/json', ...options.headers }; 39 | return fetch(url, { ...options, headers }).then(res => { 40 | if (res.ok) { 41 | return getResponseBody(res); 42 | } 43 | return getResponseBody(res).then(body => { 44 | throw new NetworkError(body || '', res.status); 45 | }); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/defaults/index.js: -------------------------------------------------------------------------------- 1 | import persist from './persist'; 2 | import detectNetwork from './detectNetwork'; 3 | import effect from './effect'; 4 | import retry from './retry'; 5 | import discard from './discard'; 6 | import defaultCommit from './defaultCommit'; 7 | import defaultRollback from './defaultRollback'; 8 | import persistAutoRehydrate from './persistAutoRehydrate'; 9 | import offlineStateLens from './offlineStateLens'; 10 | 11 | export default { 12 | rehydrate: true, // backward compatibility, TODO remove in the next breaking change version 13 | persist, 14 | detectNetwork, 15 | effect, 16 | retry, 17 | discard, 18 | defaultCommit, 19 | defaultRollback, 20 | persistAutoRehydrate, 21 | offlineStateLens 22 | }; 23 | -------------------------------------------------------------------------------- /src/defaults/offlineStateLens.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default (state: any) => { 3 | const { offline, ...rest } = state; 4 | return { 5 | get: offline, 6 | set: (offlineState: any) => 7 | typeof offlineState === 'undefined' 8 | ? rest 9 | : { offline: offlineState, ...rest } 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/defaults/persist.js: -------------------------------------------------------------------------------- 1 | // flow 2 | import { persistStore } from 'redux-persist'; 3 | 4 | export default persistStore; 5 | -------------------------------------------------------------------------------- /src/defaults/persist.native.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // $FlowIgnore 4 | import { AsyncStorage } from 'react-native'; // eslint-disable-line 5 | import { persistStore } from 'redux-persist'; 6 | 7 | export default (store: any, options: {}, callback: any) => 8 | persistStore(store, { storage: AsyncStorage, ...options }, callback); 9 | -------------------------------------------------------------------------------- /src/defaults/persistAutoRehydrate.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { autoRehydrate } from 'redux-persist'; 3 | 4 | export default autoRehydrate; 5 | -------------------------------------------------------------------------------- /src/defaults/retry.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { OfflineAction } from '../types'; 3 | 4 | const decaySchedule = [ 5 | 1000, // After 1 seconds 6 | 1000 * 5, // After 5 seconds 7 | 1000 * 15, // After 15 seconds 8 | 1000 * 30, // After 30 seconds 9 | 1000 * 60, // After 1 minute 10 | 1000 * 60 * 3, // After 3 minutes 11 | 1000 * 60 * 5, // After 5 minutes 12 | 1000 * 60 * 10, // After 10 minutes 13 | 1000 * 60 * 30, // After 30 minutes 14 | 1000 * 60 * 60 // After 1 hour 15 | ]; 16 | 17 | export default (action: OfflineAction, retries: number): ?number => 18 | decaySchedule[retries] || null; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* global $Shape */ 3 | import { applyMiddleware, compose } from 'redux'; 4 | import type { Config } from './types'; 5 | import { createOfflineMiddleware } from './middleware'; 6 | import { enhanceReducer } from './updater'; 7 | import { applyDefaults } from './config'; 8 | import { networkStatusChanged } from './actions'; 9 | 10 | // @TODO: Take createStore as config? 11 | const warnIfNotReduxAction = (config: $Shape, key: string) => { 12 | const maybeAction = config[key]; 13 | 14 | const isNotReduxAction = 15 | maybeAction === null || 16 | typeof maybeAction !== 'object' || 17 | typeof maybeAction.type !== 'string' || 18 | maybeAction.type === ''; 19 | 20 | if (isNotReduxAction && console.warn) { 21 | const msg = 22 | `${key} must be a proper redux action, ` + 23 | `i.e. it must be an object and have a non-empty string type. ` + 24 | `Instead you provided: ${JSON.stringify(maybeAction, null, 2)}`; 25 | console.warn(msg); 26 | } 27 | }; 28 | 29 | // eslint-disable-next-line no-unused-vars 30 | let persistor; 31 | 32 | export const offline = (userConfig: $Shape = {}) => ( 33 | createStore: any 34 | ) => (reducer: any, preloadedState: any, enhancer: any = x => x) => { 35 | const config = applyDefaults(userConfig); 36 | 37 | warnIfNotReduxAction(config, 'defaultCommit'); 38 | warnIfNotReduxAction(config, 'defaultRollback'); 39 | 40 | // wraps userland reducer with a top-level 41 | // reducer that handles offline state updating 42 | const offlineReducer = enhanceReducer(reducer, config); 43 | 44 | const offlineMiddleware = applyMiddleware(createOfflineMiddleware(config)); 45 | 46 | // create autoRehydrate enhancer if required 47 | const offlineEnhancer = 48 | config.persist && config.rehydrate && config.persistAutoRehydrate 49 | ? compose(offlineMiddleware, config.persistAutoRehydrate()) 50 | : offlineMiddleware; 51 | 52 | // create store 53 | const store = offlineEnhancer(createStore)( 54 | offlineReducer, 55 | preloadedState, 56 | enhancer 57 | ); 58 | 59 | const baseReplaceReducer = store.replaceReducer.bind(store); 60 | store.replaceReducer = function replaceReducer(nextReducer) { 61 | return baseReplaceReducer(enhanceReducer(nextReducer, config)); 62 | }; 63 | 64 | // launch store persistor 65 | if (config.persist) { 66 | persistor = config.persist( 67 | store, 68 | config.persistOptions, 69 | config.persistCallback 70 | ); 71 | } 72 | 73 | // launch network detector 74 | if (config.detectNetwork) { 75 | config.detectNetwork(online => { 76 | store.dispatch(networkStatusChanged(online)); 77 | }); 78 | } 79 | 80 | return store; 81 | }; 82 | 83 | export const createOffline = (userConfig: $Shape = {}) => { 84 | const config = applyDefaults(userConfig); 85 | 86 | warnIfNotReduxAction(config, 'defaultCommit'); 87 | warnIfNotReduxAction(config, 'defaultRollback'); 88 | 89 | const enhanceStore = (next: any) => ( 90 | reducer: any, 91 | preloadedState: any, 92 | enhancer: any 93 | ) => { 94 | // create autoRehydrate enhancer if required 95 | const createStore = 96 | config.persist && config.rehydrate && config.persistAutoRehydrate 97 | ? config.persistAutoRehydrate()(next) 98 | : next; 99 | 100 | // create store 101 | const store = createStore(reducer, preloadedState, enhancer); 102 | 103 | const baseReplaceReducer = store.replaceReducer.bind(store); 104 | store.replaceReducer = function replaceReducer(nextReducer) { 105 | return baseReplaceReducer(enhanceReducer(nextReducer, config)); 106 | }; 107 | 108 | // launch store persistor 109 | if (config.persist) { 110 | persistor = config.persist( 111 | store, 112 | config.persistOptions, 113 | config.persistCallback 114 | ); 115 | } 116 | 117 | // launch network detector 118 | if (config.detectNetwork) { 119 | config.detectNetwork(online => { 120 | store.dispatch(networkStatusChanged(online)); 121 | }); 122 | } 123 | 124 | return store; 125 | }; 126 | 127 | return { 128 | middleware: createOfflineMiddleware(config), 129 | enhanceReducer(reducer) { 130 | return enhanceReducer(reducer, config); 131 | }, 132 | enhanceStore 133 | }; 134 | }; 135 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { AppState, Config } from './types'; 4 | import { OFFLINE_SEND, OFFLINE_SCHEDULE_RETRY } from './constants'; 5 | import { completeRetry } from './actions'; 6 | import send from './send'; 7 | 8 | const after = (timeout = 0) => 9 | new Promise(resolve => setTimeout(resolve, timeout)); 10 | 11 | export const createOfflineMiddleware = (config: Config) => (store: any) => ( 12 | next: any 13 | ) => (action: any) => { 14 | // allow other middleware to do their things 15 | const result = next(action); 16 | 17 | // find any actions to send, if any 18 | const state: AppState = store.getState(); 19 | const offline = config.offlineStateLens(state).get; 20 | const offlineAction = offline.outbox[0]; 21 | 22 | // if the are any actions in the queue that we are not 23 | // yet processing, send those actions 24 | if ( 25 | offlineAction && 26 | !offline.busy && 27 | !offline.retryScheduled && 28 | offline.online 29 | ) { 30 | send(offlineAction, store.dispatch, config, offline.retryCount); 31 | } 32 | 33 | if (action.type === OFFLINE_SCHEDULE_RETRY) { 34 | after(action.payload.delay).then(() => { 35 | store.dispatch(completeRetry(offlineAction)); 36 | }); 37 | } 38 | 39 | if (action.type === OFFLINE_SEND && offlineAction && !offline.busy) { 40 | send(offlineAction, store.dispatch, config, offline.retryCount); 41 | } 42 | 43 | return result; 44 | }; 45 | -------------------------------------------------------------------------------- /src/send.js: -------------------------------------------------------------------------------- 1 | import { busy, scheduleRetry } from './actions'; 2 | import { JS_ERROR } from './constants'; 3 | import type { Config, OfflineAction, ResultAction } from './types'; 4 | 5 | const complete = ( 6 | action: ResultAction, 7 | success: boolean, 8 | payload: {} 9 | ): ResultAction => ({ 10 | ...action, 11 | payload, 12 | meta: { ...action.meta, success, completed: true } 13 | }); 14 | 15 | const send = (action: OfflineAction, dispatch, config: Config, retries = 0) => { 16 | const metadata = action.meta.offline; 17 | dispatch(busy(true)); 18 | return config 19 | .effect(metadata.effect, action) 20 | .then(result => { 21 | const commitAction = metadata.commit || { 22 | ...config.defaultCommit, 23 | meta: { ...config.defaultCommit.meta, offlineAction: action } 24 | }; 25 | try { 26 | dispatch(complete(commitAction, true, result)); 27 | } catch (e) { 28 | dispatch(complete({ type: JS_ERROR, payload: e }, false)); 29 | } 30 | }) 31 | .catch(async error => { 32 | const rollbackAction = metadata.rollback || { 33 | ...config.defaultRollback, 34 | meta: { ...config.defaultRollback.meta, offlineAction: action } 35 | }; 36 | 37 | // discard 38 | let mustDiscard = true; 39 | try { 40 | mustDiscard = await config.discard(error, action, retries); 41 | } catch (e) { 42 | console.warn(e); 43 | } 44 | 45 | if (!mustDiscard) { 46 | const delay = config.retry(action, retries); 47 | if (delay != null) { 48 | dispatch(scheduleRetry(delay)); 49 | return; 50 | } 51 | } 52 | 53 | dispatch(complete(rollbackAction, false, error)); 54 | }); 55 | }; 56 | 57 | export default send; 58 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type ResultAction = { 4 | type: string, 5 | payload: ?{}, 6 | meta: { 7 | success: boolean, 8 | completed: boolean 9 | } 10 | }; 11 | 12 | export type OfflineMetadata = { 13 | effect: {}, 14 | commit?: ResultAction, 15 | rollback?: ResultAction 16 | }; 17 | 18 | export type OfflineAction = { 19 | type: string, 20 | payload?: {}, 21 | meta: { 22 | transaction?: number, 23 | offline: OfflineMetadata 24 | } 25 | }; 26 | 27 | export type Outbox = Array; 28 | 29 | export type OfflineState = { 30 | lastTransaction: number, 31 | online: boolean, 32 | outbox: Outbox, 33 | retryCount: number, 34 | retryScheduled: boolean 35 | }; 36 | 37 | export type AppState = { 38 | offline: OfflineState 39 | }; 40 | 41 | type NetworkCallback = (result: boolean) => void; 42 | 43 | export type Config = { 44 | detectNetwork: (callback: NetworkCallback) => void, 45 | persist: (store: any, options: {}, callback: () => void) => any, 46 | effect: (effect: any, action: OfflineAction) => Promise<*>, 47 | retry: (action: OfflineAction, retries: number) => ?number, 48 | discard: (error: any, action: OfflineAction, retries: number) => boolean, 49 | persistOptions: {}, 50 | persistCallback: (callback: any) => any, 51 | defaultCommit: { type: string }, 52 | defaultRollback: { type: string }, 53 | persistAutoRehydrate: (config: ?{}) => (next: any) => any, 54 | offlineStateLens: ( 55 | state: any 56 | ) => { get: OfflineState, set: (offlineState: ?OfflineState) => any } 57 | }; 58 | -------------------------------------------------------------------------------- /src/updater.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* global $Shape */ 3 | 4 | import type { 5 | OfflineState, 6 | OfflineAction, 7 | ResultAction, 8 | Config 9 | } from './types'; 10 | import { 11 | OFFLINE_STATUS_CHANGED, 12 | OFFLINE_SCHEDULE_RETRY, 13 | OFFLINE_COMPLETE_RETRY, 14 | OFFLINE_BUSY, 15 | RESET_STATE, 16 | PERSIST_REHYDRATE 17 | } from './constants'; 18 | 19 | type ControlAction = 20 | | { type: OFFLINE_STATUS_CHANGED, payload: { online: boolean } } 21 | | { type: OFFLINE_SCHEDULE_RETRY }; 22 | 23 | const enqueue = (state: OfflineState, action: any): OfflineState => { 24 | const transaction = state.lastTransaction + 1; 25 | const stamped = { ...action, meta: { ...action.meta, transaction } }; 26 | const { outbox } = state; 27 | return { 28 | ...state, 29 | lastTransaction: transaction, 30 | outbox: [...outbox, stamped] 31 | }; 32 | }; 33 | 34 | const dequeue = (state: OfflineState): OfflineState => { 35 | const [, ...rest] = state.outbox; 36 | return { 37 | ...state, 38 | outbox: rest, 39 | retryCount: 0, 40 | busy: false 41 | }; 42 | }; 43 | 44 | const initialState: OfflineState = { 45 | busy: false, 46 | lastTransaction: 0, 47 | online: false, 48 | outbox: [], 49 | retryCount: 0, 50 | retryScheduled: false, 51 | netInfo: { 52 | isConnectionExpensive: null, 53 | reach: 'NONE' 54 | } 55 | }; 56 | 57 | // @TODO: the typing of this is all kinds of wack 58 | 59 | const offlineUpdater = function offlineUpdater( 60 | state: OfflineState = initialState, 61 | action: ControlAction | OfflineAction | ResultAction 62 | ): OfflineState { 63 | // Update online/offline status 64 | if ( 65 | action.type === OFFLINE_STATUS_CHANGED && 66 | action.payload && 67 | typeof action.payload.online === 'boolean' 68 | ) { 69 | return { 70 | ...state, 71 | online: action.payload.online, 72 | netInfo: action.payload.netInfo 73 | }; 74 | } 75 | 76 | if (action.type === PERSIST_REHYDRATE) { 77 | return { 78 | ...state, 79 | ...action.payload.offline, 80 | online: state.online, 81 | netInfo: state.netInfo, 82 | retryScheduled: initialState.retryScheduled, 83 | retryCount: initialState.retryCount, 84 | busy: initialState.busy 85 | }; 86 | } 87 | 88 | if (action.type === OFFLINE_SCHEDULE_RETRY) { 89 | return { 90 | ...state, 91 | busy: false, 92 | retryScheduled: true, 93 | retryCount: state.retryCount + 1 94 | }; 95 | } 96 | 97 | if (action.type === OFFLINE_COMPLETE_RETRY) { 98 | return { ...state, retryScheduled: false }; 99 | } 100 | 101 | if ( 102 | action.type === OFFLINE_BUSY && 103 | action.payload && 104 | typeof action.payload.busy === 'boolean' 105 | ) { 106 | return { ...state, busy: action.payload.busy }; 107 | } 108 | 109 | // Add offline actions to queue 110 | if (action.meta && action.meta.offline) { 111 | return enqueue(state, action); 112 | } 113 | 114 | // Remove completed actions from queue (success or fail) 115 | if (action.meta && action.meta.completed === true) { 116 | return dequeue(state); 117 | } 118 | 119 | if (action.type === RESET_STATE) { 120 | return { ...initialState, online: state.online, netInfo: state.netInfo }; 121 | } 122 | 123 | return state; 124 | }; 125 | 126 | export const enhanceReducer = (reducer: any, config: $Shape) => ( 127 | state: any, 128 | action: any 129 | ) => { 130 | let offlineState; 131 | let restState; 132 | if (typeof state !== 'undefined') { 133 | offlineState = config.offlineStateLens(state).get; 134 | restState = config.offlineStateLens(state).set(); 135 | } 136 | 137 | return config 138 | .offlineStateLens(reducer(restState, action)) 139 | .set(offlineUpdater(offlineState, action)); 140 | }; 141 | --------------------------------------------------------------------------------