├── .npmignore ├── .gitignore ├── .babelrc ├── src ├── index.js ├── ActionTypes.js └── createFetchProcess.js ├── .travis.yml ├── .eslintrc ├── tests ├── mock │ └── dataApi.js └── createFetchProcess.spec.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | temp 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as createFetchProcess } from './createFetchProcess'; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run lint 9 | - npm run test 10 | -------------------------------------------------------------------------------- /src/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOADING_START = '@redux-observable-processes/LOADING_START'; 2 | export const LOADING_END = '@redux-observable-processes/LOADING_END'; 3 | export const LOADING_CANCEL = '@redux-observable-processes/LOADING_CANCEL'; 4 | 5 | export const FETCH_DATA = '@redux-observable-processes/FETCH_DATA'; 6 | export const FETCH_CLEAR_CACHE = '@redux-observable-processes/FETCH_CLEAR_CACHE'; 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "globals": { 9 | "__DEV__": false, 10 | "__DEV_TOOLS__": false 11 | }, 12 | "rules": { 13 | "no-confusing-arrow": 0, 14 | "no-nested-ternary": 0, 15 | "yoda": 0 16 | }, 17 | "parserOptions": { 18 | "ecmaVersion": 6, 19 | "ecmaFeatures": { 20 | "experimentalObjectRestSpread": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/mock/dataApi.js: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | import 'rxjs/add/observable/fromPromise'; 3 | 4 | export const TEST_TIMEOUT = 20; 5 | 6 | // In real case it's just (...args) => Obervable.ajax(...args) 7 | export const dataCallWithCancel = (...args) => 8 | Observable.create(observer => { 9 | const handler = setTimeout( 10 | () => args.length === 1 && args[0] === 'please throw' 11 | ? observer.error(new Error({ message: 'remote error', data: args })) 12 | : (observer.next({ apiResult: args }), observer.complete()), 13 | TEST_TIMEOUT 14 | ); 15 | 16 | return () => { 17 | clearTimeout(handler); 18 | }; 19 | }); 20 | 21 | export const throwAtFirstCall = () => { 22 | let counter = 0; 23 | return (...args) => 24 | Observable.create(observer => { 25 | const handler = setTimeout( 26 | () => counter++ === 0 27 | ? observer.error(new Error({ message: 'remote error', data: args })) 28 | : (observer.next({ apiResult: args }), observer.complete()), 29 | TEST_TIMEOUT 30 | ); 31 | 32 | return () => { 33 | clearTimeout(handler); 34 | }; 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-observable-process-fetch", 3 | "version": "0.1.2", 4 | "description": "processes for redux-observable", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "lint": "eslint src tests", 8 | "test": "mocha --compilers js:babel-register './tests/*.spec.js'", 9 | "test:watch": "mocha --compilers js:babel-register --reporter min --watch './tests/*.spec.js'", 10 | "build": "npm run lint && rimraf lib && babel src -d lib", 11 | "prepublish": "npm run lint && npm run test && npm run build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/istarkov/redux-observable-process-fetch.git" 16 | }, 17 | "keywords": [ 18 | "redux", 19 | "observable", 20 | "middleware", 21 | "process", 22 | "processes" 23 | ], 24 | "author": "Ivan Starkov", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/istarkov/redux-observable-processes/issues" 28 | }, 29 | "homepage": "https://github.com/istarkov/redux-observable-processes#readme", 30 | "dependencies": { 31 | "rxjs": "^5.0.0-beta.8" 32 | }, 33 | "devDependencies": { 34 | "babel-cli": "^6.9.0", 35 | "babel-plugin-transform-runtime": "^6.9.0", 36 | "babel-preset-es2015": "^6.9.0", 37 | "babel-preset-stage-0": "^6.5.0", 38 | "eslint": "^2.10.2", 39 | "eslint-config-airbnb": "^9.0.1", 40 | "eslint-plugin-import": "^1.8.0", 41 | "eslint-plugin-jsx-a11y": "^1.2.2", 42 | "eslint-plugin-react": "^5.1.1", 43 | "expect": "^1.20.1", 44 | "mocha": "^2.5.3", 45 | "redux": "^3.5.2", 46 | "redux-observable": "^0.5.0", 47 | "rimraf": "^2.5.2" 48 | }, 49 | "peerDependencies": { 50 | "redux": "3.*", 51 | "redux-observable": ">=0.5.0", 52 | "rxjs": ">=5.0.0-beta.8" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-observable-process-fetch 2 | 3 | This library is a [redux-observable](https://github.com/redux-observable/redux-observable) middleware. 4 | And as `redux-observable` is a middleware itself it is a middleware for middleware. 5 | 6 | This middleware add caching, refetching, pre and post loading actions support 7 | for async data loading services like `Observable.ajax` or any other service with signature 8 | `(...args: Array) => Observable`. 9 | 10 | # Example 11 | 12 | The best example is a [tests](./tests/createFetchProcess.spec.js) 13 | 14 | Initialize redux with `redux-observable` and `redux-observable-process-fetch` 15 | 16 | ```javascript 17 | import { Observable } from 'rxjs/Observable'; 18 | import 'rxjs/add/observable/dom/ajax'; 19 | import { createStore, applyMiddleware } from 'redux'; 20 | import { reduxObservable, combineDelegators } from 'redux-observable'; 21 | import { createFetchProcess } from 'redux-observable-process-fetch'; 22 | 23 | const ctreateStoreWithReduxObservableMiddleware = (reducer) => { 24 | const processor = combineDelegators( 25 | createFetchProcess({ fetch: (type, id) => Observable.ajax(`http://blbla/${type}/${id}`) }) 26 | ); 27 | const middleware = reduxObservable(processor); 28 | const store = createStore(reducer, applyMiddleware(middleware)); 29 | return store; 30 | }; 31 | ``` 32 | 33 | Create action creator 34 | 35 | ```javascript 36 | export const loadMySuperObject = (...args) => ({ 37 | type: FETCH_DATA, 38 | meta: { type: 'SUPER_OBJECT_LOAD', cache: true }, 39 | payload: args, 40 | }); 41 | ``` 42 | 43 | Use `loadMySuperObject` as usual in redux. 44 | 45 | # What you will get 46 | 47 | For each action with type `FETCH_DATA`, like this 48 | 49 | ```javascript 50 | const fetchDataAction = { 51 | type: FETCH_DATA, 52 | meta: { type: ON_FETCH_SOMETHING_COMPLETE, cache: false }, 53 | payload: [1, 2] 54 | } 55 | ``` 56 | 57 | It dispatches `LOADING_START` and `LOADING_END` actions before data fetch starts and after it end. 58 | 59 | On fetch complete it dispatches action 60 | 61 | ```javascript 62 | { 63 | type: fetchDataAction.meta.type, 64 | payload: fetchResult, 65 | meta: fetchDataAction.meta 66 | } 67 | ``` 68 | 69 | and on fetch error 70 | 71 | ```javascript 72 | { 73 | type: fetchDataAction.meta.type, 74 | payload: error, 75 | error: true, 76 | meta: fetchDataAction.meta 77 | } 78 | ``` 79 | 80 | If `FETCH_DATA` action meta has `cache: false` property, and action with same 81 | `payload` and `meta.type` has already been successfully processed, 82 | it does nothing (_as data is already in redux store_). 83 | 84 | It `fetch` again if there where fetch error at previous call. 85 | 86 | If `FETCH_DATA` action meta has `cache: true` property, it always fetch `data` again, 87 | cancelling previous `FETCH_DATA` action with same `payload` and `meta.type`. 88 | 89 | If such actions are called simultaneously first action does not run at all. 90 | 91 | If sequentially action `LOADING_CANCEL` will be run. 92 | 93 | Look at [tests](./tests/createFetchProcess.spec.js) for more examples. 94 | 95 | # Install 96 | 97 | ``` 98 | npm install --save redux-observable-process-fetch 99 | ``` 100 | -------------------------------------------------------------------------------- /src/createFetchProcess.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import { Observable } from 'rxjs/Observable'; 3 | // In real life I'll use just `import { Observable } from 'rxjs/Rx';` 4 | // BTW it's interesting to compare size of library with and without theese imports 5 | import 'rxjs/add/observable/from'; 6 | import 'rxjs/add/observable/of'; 7 | import 'rxjs/add/observable/empty'; 8 | import 'rxjs/add/operator/filter'; 9 | import 'rxjs/add/operator/map'; 10 | import 'rxjs/add/operator/mergeMap'; 11 | import 'rxjs/add/operator/scan'; 12 | import 'rxjs/add/operator/delay'; 13 | import 'rxjs/add/operator/concat'; 14 | import 'rxjs/add/operator/catch'; 15 | import 'rxjs/add/operator/takeUntil'; 16 | import 'rxjs/add/operator/do'; 17 | 18 | import { 19 | FETCH_DATA, FETCH_CLEAR_CACHE, 20 | LOADING_START, LOADING_END, LOADING_CANCEL, 21 | } from './ActionTypes'; 22 | 23 | 24 | /** 25 | * redux-observable process to support data fetching, caching, pre and post loading events. 26 | * input: `{ fetch: (...args: Array) => Observable }` 27 | * 28 | * For each action with type `FETCH_DATA`, like this 29 | * `fetchDataAction = { 30 | * type: FETCH_DATA, 31 | * meta: { type: ON_FETCH_SOMETHING_COMPLETE, cache: false }, 32 | * payload: [1, 2] 33 | * }` 34 | * 35 | * It dispatches `LOADING_START` and `LOADING_END` actions before data fetch starts and after it end. 36 | * 37 | * On fetch complete it dispatches action 38 | * `{ type: fetchDataAction.meta.type, payload: fetchResult, meta: fetchDataAction.meta }` 39 | * and on fetch error 40 | * `{ type: fetchDataAction.meta.type, payload: error, error: true, meta: fetchDataAction.meta }` 41 | * 42 | * If `FETCH_DATA` action meta has `cache: false` property, and action with same 43 | * `payload` and `meta.type` has already been successfully processed, 44 | * it does nothing (_as data is already in redux store_). 45 | * And it `fetch` again if there where fetch error. 46 | * 47 | * If `FETCH_DATA` action meta has `cache: true` property, it always fetch `data` again, 48 | * cancelling previous `FETCH_DATA` action with same `payload` and `meta.type`. 49 | * If such actions are called simultaneously first action does not run at all, 50 | * If sequentially action LOADING_CANCEL will be run 51 | * Look at tests for examples. 52 | */ 53 | export default (services) => 54 | (actions$, { dispatch }) => { 55 | const prepared$ = actions$ 56 | // filter only needed events 57 | .filter(({ type }) => type === FETCH_DATA || type === FETCH_CLEAR_CACHE) 58 | // precalculate dataKey as it will be used in multiple places 59 | .map((action) => ({ 60 | ...action, 61 | dataKey: JSON.stringify([action.meta.type, ...action.payload]), 62 | })) 63 | // to simplify logic split action on two if we need to refetch item 64 | // as refetch is the same as fetch after cache clean 65 | .mergeMap(action => action.meta.cache 66 | ? Observable.of(action) 67 | : Observable.from([ 68 | { ...action, type: FETCH_CLEAR_CACHE }, 69 | action, 70 | ]) 71 | ); 72 | 73 | return prepared$ 74 | // hold information wich items is fetched already 75 | .scan( 76 | (r, action) => { 77 | r.action = undefined; 78 | if (action.type === FETCH_CLEAR_CACHE) { 79 | delete r[action.dataKey]; 80 | } else if (!(action.dataKey in r)) { 81 | r[action.dataKey] = 1; 82 | r.action = action; // recall this action 83 | } 84 | return r; 85 | }, 86 | {} 87 | ) 88 | .filter(({ action }) => action !== undefined) 89 | // if action is not undefined we should to refetch item 90 | .mergeMap(({ action }) => 91 | Observable.of({}) 92 | .delay(0) // to allow not run first fetch if same actions run simultaneously 93 | .takeUntil( 94 | prepared$ 95 | .filter(({ type, dataKey: key }) => 96 | type === FETCH_CLEAR_CACHE && key === action.dataKey 97 | ) 98 | ) 99 | .mergeMap(() => 100 | Observable.of({ type: LOADING_START, payload: action.payload, meta: action.meta }) 101 | .concat( 102 | (services[action.meta.api] || services)(...action.payload) 103 | .map(payload => ({ type: action.meta.type, payload, meta: action.meta })) 104 | .catch(error => Observable.of({ 105 | type: action.meta.type, payload: error, error: true, meta: action.meta, 106 | })) 107 | .do(({ error }) => { 108 | if (error === true) { 109 | // run dispatch at next step to allow error and LOADING_END actions to 110 | // finish process 111 | Observable.of({ ...action, type: FETCH_CLEAR_CACHE }) 112 | .delay(0) 113 | .subscribe(dispatch); 114 | } 115 | }) 116 | ) 117 | .concat( 118 | Observable.of({ type: LOADING_END, payload: action.payload, meta: action.meta }) 119 | ) 120 | // we should stop sequence if FETCH_CLEAR_CACHE occured (the second action wins) 121 | .takeUntil( 122 | prepared$ 123 | .filter(({ type, dataKey: key }) => 124 | type === FETCH_CLEAR_CACHE && key === action.dataKey 125 | ) 126 | .do(() => dispatch({ 127 | type: LOADING_CANCEL, payload: action.payload, meta: action.meta, 128 | })) 129 | ) 130 | ) 131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /tests/createFetchProcess.spec.js: -------------------------------------------------------------------------------- 1 | // `npm bin`/mocha --compilers js:babel-register --reporter min --watch './tests/*.spec.js' 2 | import expect from 'expect'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import 'rxjs/add/observable/merge'; 5 | // import 'rxjs/add/observable/dom/ajax'; 6 | 7 | import { createStore, applyMiddleware } from 'redux'; 8 | import { reduxObservable } from 'redux-observable'; 9 | 10 | import { createFetchProcess } from '../src'; 11 | import { 12 | FETCH_DATA, FETCH_CLEAR_CACHE, 13 | LOADING_START, LOADING_END, LOADING_CANCEL, 14 | } from '../src/ActionTypes'; 15 | 16 | import { dataCallWithCancel, throwAtFirstCall, TEST_TIMEOUT } from './mock/dataApi'; 17 | 18 | // TODO replace with redux-observable combineDelegators 19 | const combineDelegators = (...delegators) => (actions, store) => 20 | Observable.merge(...(delegators.map((delegator) => delegator(actions, store)))); 21 | 22 | // Initialize redux + redux-observable + redux-observable-processor 23 | const ctreateStoreWithReduxObservableMiddleware = (services) => { 24 | const reducer = (state = [], action) => state 25 | .concat(action) 26 | .filter(({ type }) => ['@@redux/INIT', FETCH_DATA, FETCH_CLEAR_CACHE].indexOf(type) === -1); 27 | // create Processor 28 | const processor = combineDelegators( 29 | createFetchProcess(services) 30 | ); 31 | const middleware = reduxObservable(processor); 32 | 33 | const store = createStore(reducer, applyMiddleware(middleware)); 34 | return store; 35 | }; 36 | 37 | // fetch action creator 38 | const createFetchAction = (actionType, preferCache, meta, ...args) => ({ 39 | type: FETCH_DATA, 40 | meta: { api: 'fetch', type: actionType, cache: preferCache, ...meta }, 41 | payload: args, 42 | }); 43 | 44 | const PREFER_CACHE = true; 45 | const PREFER_REFETCH = false; 46 | 47 | describe('createFetchProcess test', () => { 48 | describe('Loading Actions', () => { 49 | it('should generate LOADING_START LOADING_END events', (done) => { 50 | const store = ctreateStoreWithReduxObservableMiddleware({ fetch: dataCallWithCancel }); 51 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT'; 52 | 53 | store.dispatch( 54 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, {}, 1, 2) 55 | ); 56 | 57 | setTimeout(() => { 58 | const state = store.getState(); 59 | expect(state.map(({ type }) => type)) 60 | .toEqual([LOADING_START, LOAD_MY_OBJECT, LOADING_END]); 61 | done(); 62 | }, 100); 63 | }); 64 | 65 | it('should generate LOADING_START LOADING_END events even on fech Error', (done) => { 66 | const store = ctreateStoreWithReduxObservableMiddleware({ fetch: dataCallWithCancel }); 67 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT'; 68 | 69 | store.dispatch( 70 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, {}, 'please throw') 71 | ); 72 | 73 | setTimeout(() => { 74 | const state = store.getState(); 75 | expect(state.map(({ type, error }) => ({ type, ...(error && { error }) }))) 76 | .toEqual([ 77 | { type: LOADING_START }, 78 | { type: LOAD_MY_OBJECT, error: true }, 79 | { type: LOADING_END }, 80 | ]); 81 | 82 | done(); 83 | }, 100); 84 | }); 85 | }); 86 | 87 | 88 | describe('Prefer Cache', () => { 89 | it('should not dipatch action for same type + paylod', (done) => { 90 | const store = ctreateStoreWithReduxObservableMiddleware({ fetch: dataCallWithCancel }); 91 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT'; 92 | 93 | const [FIRST_CALL, NEXT_CALL] = [1, 2]; 94 | 95 | store.dispatch( 96 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, { test: FIRST_CALL }, 'bar', 'foo') 97 | ); 98 | store.dispatch( 99 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, { test: NEXT_CALL }, 'bar', 'foo') 100 | ); 101 | 102 | setTimeout( 103 | () => store.dispatch( 104 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, { test: NEXT_CALL }, 'bar', 'foo') 105 | ), 106 | TEST_TIMEOUT / 2 107 | ); 108 | 109 | setTimeout( 110 | () => store.dispatch( 111 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, { test: NEXT_CALL }, 'bar', 'foo') 112 | ), 113 | TEST_TIMEOUT * 2 114 | ); 115 | 116 | setTimeout(() => { 117 | const state = store.getState(); 118 | expect(state.map(({ type, meta: { test } }) => ({ type, ...(test && { test }) }))) 119 | .toEqual([ // FIRST_CALL SECOND_CALL wins 120 | { type: LOADING_START, test: FIRST_CALL }, 121 | { type: LOAD_MY_OBJECT, test: FIRST_CALL }, 122 | { type: LOADING_END, test: FIRST_CALL }, 123 | ]); 124 | 125 | done(); 126 | }, 100); 127 | }); 128 | 129 | it('should refetch if previous call ends with error', (done) => { 130 | const store = ctreateStoreWithReduxObservableMiddleware({ throwFirst: throwAtFirstCall() }); 131 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT__'; 132 | 133 | const [FIRST_CALL, NEXT_CALL] = [1, 2]; 134 | 135 | store.dispatch( 136 | createFetchAction( 137 | LOAD_MY_OBJECT, PREFER_CACHE, 138 | { api: 'throwFirst', test: FIRST_CALL }, 139 | 'bar', 'foo' 140 | ) 141 | ); 142 | 143 | setTimeout(() => 144 | store.dispatch( 145 | createFetchAction( 146 | LOAD_MY_OBJECT, PREFER_CACHE, 147 | { api: 'throwFirst', test: NEXT_CALL }, 148 | 'bar', 'foo' 149 | ) 150 | ), 151 | TEST_TIMEOUT * 2 152 | ); 153 | 154 | setTimeout(() => { 155 | const state = store.getState(); 156 | expect( 157 | state 158 | .filter(({ type }) => type === LOAD_MY_OBJECT) 159 | .map(({ error }) => !!error) 160 | ).toEqual([true, false]); 161 | done(); 162 | }, 100); 163 | }); 164 | 165 | it('should dipatch action for different payload or type', (done) => { 166 | const store = ctreateStoreWithReduxObservableMiddleware({ fetch: dataCallWithCancel }); 167 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT'; 168 | const LOAD_MY_OTHER_OBJECT = 'LOAD_MY_OTHER_OBJECT'; 169 | 170 | store.dispatch( 171 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, {}, 'bar', 'foo') 172 | ); 173 | store.dispatch( 174 | createFetchAction(LOAD_MY_OBJECT, PREFER_CACHE, {}, 'foo', 'bar') 175 | ); 176 | store.dispatch( 177 | createFetchAction(LOAD_MY_OTHER_OBJECT, PREFER_CACHE, {}, 'bar', 'foo') 178 | ); 179 | 180 | setTimeout(() => { 181 | const state = store.getState(); 182 | 183 | expect( 184 | state 185 | .filter(({ type }) => type === LOAD_MY_OBJECT || type === LOAD_MY_OTHER_OBJECT) 186 | .map(({ type, payload }) => ({ type, payload })) 187 | ) 188 | .toEqual([ 189 | { type: LOAD_MY_OBJECT, payload: { apiResult: ['bar', 'foo'] } }, 190 | { type: LOAD_MY_OBJECT, payload: { apiResult: ['foo', 'bar'] } }, 191 | { type: LOAD_MY_OTHER_OBJECT, payload: { apiResult: ['bar', 'foo'] } }, 192 | ]); 193 | 194 | done(); 195 | }, 90); 196 | }); 197 | }); 198 | 199 | describe('Prefer Refetch', () => { 200 | it('should not start previous and fetch curent if run simultaneously', (done) => { 201 | const store = ctreateStoreWithReduxObservableMiddleware({ fetch: dataCallWithCancel }); 202 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT'; 203 | const [FIRST_CALL, SECOND_CALL] = [1, 2]; 204 | store.dispatch( 205 | createFetchAction(LOAD_MY_OBJECT, PREFER_REFETCH, { test: FIRST_CALL }, 'bar', 'foo') 206 | ); 207 | store.dispatch( 208 | createFetchAction(LOAD_MY_OBJECT, PREFER_REFETCH, { test: SECOND_CALL }, 'bar', 'foo') 209 | ); 210 | 211 | setTimeout(() => { 212 | const state = store.getState(); 213 | expect(state.map(({ type, meta: { test } }) => ({ type, test }))) 214 | .toEqual([ // SECOND_CALL wins 215 | { type: LOADING_START, test: SECOND_CALL }, 216 | { type: LOAD_MY_OBJECT, test: SECOND_CALL }, 217 | { type: LOADING_END, test: SECOND_CALL }, 218 | ]); 219 | 220 | done(); 221 | }, 100); 222 | }); 223 | 224 | it('should cancel previous and fetch curent if previous is already running', (done) => { 225 | const store = ctreateStoreWithReduxObservableMiddleware({ fetch: dataCallWithCancel }); 226 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT'; 227 | const [FIRST_CALL, SECOND_CALL] = [1, 2]; 228 | store.dispatch( 229 | createFetchAction(LOAD_MY_OBJECT, PREFER_REFETCH, { test: FIRST_CALL }, 'bar', 'foo') 230 | ); 231 | 232 | setTimeout( 233 | () => 234 | store.dispatch( 235 | createFetchAction(LOAD_MY_OBJECT, PREFER_REFETCH, { test: SECOND_CALL }, 'bar', 'foo') 236 | ), 237 | 0 238 | ); 239 | 240 | setTimeout(() => { 241 | const state = store.getState(); 242 | expect( 243 | state 244 | .map(({ type, meta: { test } }) => ({ type, test })) 245 | ).toEqual([ 246 | { type: LOADING_START, test: 1 }, 247 | { type: LOADING_CANCEL, test: 1 }, 248 | { type: LOADING_START, test: 2 }, 249 | { type: LOAD_MY_OBJECT, test: 2 }, 250 | { type: LOADING_END, test: 2 }, 251 | ]); 252 | 253 | done(); 254 | }, 100); 255 | }); 256 | 257 | it('should fetch curent if previous is already fetched', (done) => { 258 | const store = ctreateStoreWithReduxObservableMiddleware({ fetch: dataCallWithCancel }); 259 | const LOAD_MY_OBJECT = 'LOAD_MY_OBJECT'; 260 | const [FIRST_CALL, SECOND_CALL] = [1, 2]; 261 | store.dispatch( 262 | createFetchAction(LOAD_MY_OBJECT, PREFER_REFETCH, { test: FIRST_CALL }, 'bar', 'foo') 263 | ); 264 | 265 | setTimeout( 266 | () => 267 | store.dispatch( 268 | createFetchAction(LOAD_MY_OBJECT, PREFER_REFETCH, { test: SECOND_CALL }, 'bar', 'foo') 269 | ), 270 | TEST_TIMEOUT * 2 271 | ); 272 | 273 | setTimeout(() => { 274 | const state = store.getState(); 275 | expect( 276 | state 277 | .map(({ type, meta: { test } }) => ({ type, test })) 278 | ).toEqual([ 279 | { type: LOADING_START, test: 1 }, 280 | { type: LOAD_MY_OBJECT, test: 1 }, 281 | { type: LOADING_END, test: 1 }, 282 | { type: LOADING_START, test: 2 }, 283 | { type: LOAD_MY_OBJECT, test: 2 }, 284 | { type: LOADING_END, test: 2 }, 285 | ]); 286 | 287 | done(); 288 | }, 100); 289 | }); 290 | }); 291 | }); 292 | --------------------------------------------------------------------------------