├── .babelrc
├── .eslintrc
├── .gitignore
├── .mocharc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── index.js
├── package.json
├── scripts
├── build.sh
├── bump-package-version.js
└── release.sh
└── src
├── __tests__
├── asyncDispatch.js
├── index.js
├── setup.js
└── splitAsyncKeys.js
├── asyncDispatch.js
├── index.js
├── splitAsyncKeys.js
└── version.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": ["transform-object-rest-spread"]
4 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "algolia",
3 | "globals": {
4 | "describe": true,
5 | "context": true,
6 | "it": true,
7 | "beforeEach": true,
8 | "afterEach": true,
9 | "expect": true,
10 | "sinon": true,
11 | "after": true,
12 | "before": true
13 | }
14 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | npm-debug.log
4 | docs/.sass-cache/
5 | docs/.webpack/
6 | .DS_Store
7 | .coverage/
--------------------------------------------------------------------------------
/.mocharc:
--------------------------------------------------------------------------------
1 | --require source-map-support/register
2 | --require babel-register
3 | --require ./src/__tests__/setup.js
4 | --reporter dot
5 | --colors
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | - "5"
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # 0.1.0 (2016-07-11)
3 |
4 |
5 |
6 |
7 | ## [0.1.1](https://github.com/algolia/redux-updeep/compare/v0.1.0...v0.1.1) (2016-07-11)
8 |
9 |
10 |
11 |
12 | # 0.1.0 (2016-07-11)
13 |
14 |
15 |
16 |
17 | ## [0.1.2](https://github.com/algolia/eventual-values/compare/v0.1.1...v0.1.2) (2016-07-07)
18 |
19 |
20 |
21 |
22 | ## [0.1.1](https://github.com/algolia/eventual-values/compare/v0.1.0...v0.1.1) (2016-07-07)
23 |
24 |
25 |
26 |
27 | # 0.1.0 (2016-07-07)
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Algolia
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # redux-magic-async-middleware
2 |
3 | `redux-magic-async-middleware` is a middleware which makes it easy to handle asynchronous data with redux. Combine it with [redux-updeep] for increased awesomeness and reduced boilerplate !
4 |
5 | [![Version][version-svg]][package-url] [![Build Status][travis-svg]][travis-url] [![License][license-image]][license-url]
6 |
7 |
8 | ## Installation
9 |
10 | `redux-magic-async-middleware` is available on npm. You'll need [redux-updeep] to take full advantage of it.
11 |
12 | ```bash
13 | npm install -S redux-magic-async-middleware redux-updeep
14 | ```
15 |
16 | ## Getting started
17 |
18 | ### 1. Add as a middleware
19 |
20 | Just add it as you would add any middleware
21 |
22 | ```js
23 | import {createStore, applyMiddleware, combineReducers} from 'redux';
24 | import magicAsyncMiddleware from 'redux-magic-async-middleware';
25 |
26 | let combinedReducers = combineReducers(...);
27 | let otherMiddlewares = [...];
28 |
29 | let store = createStore(
30 | combinedReducers,
31 | applyMiddleware(
32 | magicAsyncMiddleware,
33 | ...otherMiddlewares
34 | )
35 | );
36 | ```
37 |
38 | ### 2. Dispatch promises in your action payload as if they were synchronous values
39 |
40 | Remember how [redux-updeep] lets you easily dispatch actions and merges the payload automatically in your state ? Well, using `redux-magic-async-middleware` you can do the same with promises. While they are loading, they are represented by [eventual values](https://github.com/algolia/eventual-values), so you can check their status easily. Once they are resolved the data will automatically be updated.
41 |
42 | ```js
43 | const initialState = {
44 | user: undefined,
45 | projectList: undefined
46 | };
47 |
48 | export default createReducer('USER', initialState);
49 |
50 | export function loadUserData() {
51 | return {
52 | type: 'USER/LOAD_USER_DATA',
53 | payload: {
54 | user: fetch('/user_data'),
55 | projectList: fetch('/projectList').then(data => data.projects)
56 | }
57 | };
58 | }
59 | ```
60 |
61 | _Note that the middleware will only look for promises in the first level of the payload. If you need more, use a [path](#using-a-path-to-merge-asynchrounous-data-deeply-in-your-reducer-state)_
62 |
63 | ### 3. Use the data in your components
64 |
65 | ```js
66 | import React from 'react';
67 | import {connect} from 'react-redux'
68 | import {isReady} from 'eventual-values';
69 |
70 | function MyComponent({user}) {
71 | if (isReady(user)) {
72 | return
Hello {user.name}
;
73 | }
74 |
75 | return Loading;
76 | }
77 |
78 | export default connect(({myReducer: {user}}) => ({user}))(MyComponent);
79 | ```
80 |
81 | ## Advanced usage
82 |
83 | ### Using a path to merge asynchronous data deeply in your reducer state
84 |
85 | As mentioned the middleware will only look for promises at the first level in the payload. If the asynchronous data needs to be updated deeper into the reducer state, use a [path](https://github.com/algolia/redux-updeep#specifying-a-path).
86 |
87 | ```js
88 | const initialState = {
89 | user: undefined,
90 | projectList: undefined
91 | };
92 |
93 | export default createReducer('USER', initialState);
94 |
95 | export function loadName() {
96 | return {
97 | type: 'USER/LOAD_NAME',
98 | payload: {
99 | name: fetch('/user_name')
100 | },
101 | path: ['user']
102 | };
103 | }
104 | ```
105 |
106 | ### Refreshing data & loading state
107 |
108 | By default, the middleware will only mark values as loading if they were `undefined` when the actions were dispatched.
109 |
110 | This means that if a key on the reducer's state has a value, and if then a promised is dispatched at this key, then nothing will happen to the reducer state until the promise is resolved or rejected.
111 |
112 | It is possible to override this behaviour and reset the value to a loading eventual value at every dispatch by using the overrideStatus key in the action:
113 |
114 | ```js
115 | const initialState = {
116 | user: undefined,
117 | projectList: undefined
118 | };
119 |
120 | export default createReducer('USER', initialState);
121 |
122 | export function loadUserData() {
123 | return {
124 | type: 'USER/LOAD_USER_DATA',
125 | overrideStatus: true,
126 | payload: {
127 | user: fetch('/user_data')
128 | }
129 | };
130 | }
131 | ```
132 |
133 |
134 | [version-svg]: https://img.shields.io/npm/v/redux-magic-async-middleware.svg?style=flat-square
135 | [package-url]: https://npmjs.org/package/redux-magic-async-middleware
136 | [travis-svg]: https://img.shields.io/travis/algolia/redux-magic-async-middleware/master.svg?style=flat-square
137 | [travis-url]: https://travis-ci.org/algolia/redux-magic-async-middleware
138 | [license-image]: http://img.shields.io/badge/license-MIT-green.svg?style=flat-square
139 | [license-url]: LICENSE
140 | [downloads-image]: https://img.shields.io/npm/dm/redux-matic-async-middleware.svg?style=flat-square
141 | [downloads-url]: http://npm-stat.com/charts.html?package=redux-magic-async-middleware
142 | [redux-updeep]: https://github.com/algolia/redux-updeep
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/index.js').default;
2 | module.exports.version = require('./src/version.js');
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-magic-async-middleware",
3 | "version": "0.1.0",
4 | "description": "redux-magic-async-middleware is a reducer generator that uses magic-async-middleware to handle state updates",
5 | "main": "index.js",
6 | "repository": "https://github.com/algolia/redux-magic-async-middleware",
7 | "scripts": {
8 | "build": "LIBRARY_NAME=redux-magic-async-middleware ./scripts/build.sh",
9 | "release": "./scripts/release.sh",
10 | "test": "mocha --opts .mocharc ./src/**/__tests__/*.js"
11 | },
12 | "author": "Alexandre Meunier ",
13 | "license": "MIT",
14 | "dependencies": {
15 | "eventual-values": "^0.1.2",
16 | "lodash": "^4.13.1",
17 | "updeep": "^0.16.1"
18 | },
19 | "peerDependencies": {
20 | "redux-updeep": "^0.1.1"
21 | },
22 | "devDependencies": {
23 | "babel-cli": "^6.10.1",
24 | "babel-plugin-transform-object-rest-spread": "^6.8.0",
25 | "babel-preset-es2015": "^6.9.0",
26 | "babel-register": "^6.9.0",
27 | "chai": "^3.5.0",
28 | "chai-as-promised": "^5.3.0",
29 | "conventional-changelog": "^1.1.0",
30 | "conventional-changelog-cli": "^1.2.0",
31 | "json": "^9.0.4",
32 | "mocha": "^2.5.3",
33 | "mversion": "^1.10.1",
34 | "promise-defer": "^1.0.0",
35 | "semver": "^5.2.0",
36 | "sinon": "^1.17.4",
37 | "sinon-chai": "^2.8.0",
38 | "source-map-support": "^0.4.1"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e # exit when error, no verbose
4 |
5 | printf "\nBuilding ${LIBRARY_NAME} library\n"
6 |
7 | dist_dir="dist"
8 |
9 | mkdir -p "${dist_dir}"
10 | rm -rf "${dist_dir:?}"/*
11 |
12 | BABEL_DISABLE_CACHE=1 BABEL_ENV=npm babel -q index.js -o "$dist_dir/index.js"
13 | BABEL_DISABLE_CACHE=1 BABEL_ENV=npm babel -q src/ --out-dir "$dist_dir/src/" --ignore __tests__
--------------------------------------------------------------------------------
/scripts/bump-package-version.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 max-len:0 */
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | import mversion from 'mversion';
6 |
7 | import semver from 'semver';
8 | import currentVersion from '../src/version.js';
9 |
10 | if (!process.env.VERSION) {
11 | throw new Error('bump: Usage is VERSION=MAJOR.MINOR.PATCH scripts/bump-package-version.js');
12 | }
13 | let newVersion = process.env.VERSION;
14 |
15 | if (!semver.valid(newVersion)) {
16 | throw new Error('bump: Provided new version (' + newVersion + ') is not a valid version per semver');
17 | }
18 |
19 | if (semver.gte(currentVersion, newVersion)) {
20 | throw new Error('bump: Provided new version is not higher than current version (' + newVersion + ' <= ' + currentVersion + ')');
21 | }
22 |
23 | console.log('Bumping ' + newVersion);
24 |
25 | console.log('..Updating src/version.js');
26 |
27 | let versionFile = path.join(__dirname, '../src/version.js');
28 | let newContent = "export default '" + newVersion + "';\n";
29 | fs.writeFileSync(versionFile, newContent);
30 |
31 | console.log('..Updating package.json');
32 |
33 | mversion.update(newVersion);
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e # exit when error
4 |
5 | printf "\nReleasing\n"
6 |
7 | if ! npm owner ls | grep -q "$(npm whoami)"
8 | then
9 | printf "Release: Not an owner of the npm repo, ask a contributor for access"
10 | exit 1
11 | fi
12 |
13 | currentBranch=`git rev-parse --abbrev-ref HEAD`
14 | if [ $currentBranch != 'master' ]; then
15 | printf "Release: You must be on master"
16 | exit 1
17 | fi
18 |
19 | if [[ -n $(git status --porcelain) ]]; then
20 | printf "Release: Working tree is not clean (git status)"
21 | exit 1
22 | fi
23 |
24 | printf "\n\nRelease: update working tree"
25 | git pull origin master
26 | git fetch origin --tags
27 |
28 | printf "Release: npm install"
29 | npm install
30 |
31 | currentVersion=`cat package.json | json version`
32 |
33 | # header
34 | printf "\n\nRelease: current version is %s" $currentVersion
35 | printf "\nRelease: a changelog will be generated only if a fix/feat/performance/breaking token is found in git log"
36 | printf "\nRelease: you must choose a new ve.rs.ion (semver)"
37 | printf "\nRelease: press q to exit the next screen\n\n"
38 |
39 | # preview changelog
40 | read -p "=> Release: press [ENTER] to view changes since latest version.."
41 |
42 | conventional-changelog --preset angular --output-unreleased | less
43 |
44 | # choose and bump new version
45 | # printf "\n\nRelease: Please enter the new chosen version > "
46 | printf "\n=> Release: please type the new chosen version > "
47 | read -e newVersion
48 | VERSION=$newVersion babel-node ./scripts/bump-package-version.js
49 |
50 | # build new version
51 | NODE_ENV=production VERSION=$newVersion npm run build
52 |
53 | # update changelog
54 | printf "\n\nRelease: update changelog"
55 | changelog=`conventional-changelog -p angular`
56 | conventional-changelog --preset angular --infile CHANGELOG.md --same-file
57 |
58 | # git add and tag
59 | commitMessage="v$newVersion\n\n$changelog"
60 | git add src/version.js package.json CHANGELOG.md README.md
61 | printf "%s" "$commitMessage" | git commit --file -
62 | git tag "v$newVersion"
63 |
64 | printf "\n\nRelease: almost done, check everything in another terminal tab.\n"
65 | read -p "=> Release: when ready, press [ENTER] to push to github and publish the package"
66 |
67 | printf "\n\nRelease: push to github, publish on npm\n"
68 | git push origin master
69 | git push origin --tags
70 |
71 | # We are gonna publish the package to npm, in a way
72 | # where only the dist cdn and npm are available
73 | cp package.json README.md LICENSE dist/
74 | mkdir dist/dist
75 | cd dist
76 | npm publish
77 | rm package.json README.md LICENSE
78 | cd ..
79 |
80 | printf "\n\nRelease:
81 | Package was published to npm.
82 | A job on travis-ci will be automatically launched to finalize the release."
83 |
--------------------------------------------------------------------------------
/src/__tests__/asyncDispatch.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import updeep from 'updeep';
3 | import deferred from 'promise-defer';
4 |
5 | import asyncDispatch from '../asyncDispatch';
6 | import {isPending, isReady} from 'eventual-values';
7 |
8 | describe('modux/asyncDispatch', () => {
9 | var dispatch;
10 | const type = 'NAME_SPACE/COMMIT';
11 | const key = 'test';
12 | var promise = new Promise(() => {});
13 |
14 | beforeEach(() => {
15 | dispatch = sinon.spy();
16 | });
17 |
18 | it('should return the loading action', () => {
19 | var actual = asyncDispatch(dispatch, type, key, promise);
20 |
21 | expect(actual.type).to.eql('NAME_SPACE/COMMIT');
22 | expect(actual.payload.test).to.be.a('function');
23 | expect(isPending(updeep(actual.payload, {}).test)).to.eq(true);
24 | expect(isPending(updeep(actual.payload, {test: 'value'}).test)).to.eq(false);
25 | });
26 |
27 | context('when overrideStatus is set to true', () => {
28 | it('should return the loading action', () => {
29 | var actual = asyncDispatch(dispatch, type, key, promise, true);
30 |
31 | expect(actual.type).to.eql('NAME_SPACE/COMMIT');
32 | expect(isPending(updeep(actual.payload, {}).test)).to.eq(true);
33 | expect(isPending(updeep(actual.payload, {test: 'value'}).test)).to.eq(true);
34 | });
35 | });
36 |
37 | context('when the promise is resolved', () => {
38 | it('should dispatch the success action', () => {
39 | const def = deferred();
40 |
41 | asyncDispatch(dispatch, type, key, def.promise);
42 |
43 | def.resolve({name: 'test'});
44 |
45 | return def.promise.then(() => {
46 | expect(dispatch.args[0][0]).to.eql({
47 | type: 'NAME_SPACE/COMMIT_SUCCESS',
48 | payload: {
49 | [key]: {name: 'test'}
50 | }
51 | });
52 |
53 | expect(isReady(dispatch.args[0][0])).to.eql(true);
54 | });
55 | });
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/__tests__/index.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import deferred from 'promise-defer';
3 | import omit from 'lodash/omit';
4 | import updeep from 'updeep';
5 |
6 | import middleware from '../';
7 | import {isPending} from 'eventual-values';
8 |
9 |
10 | describe('modux/magicAsyncMiddleware', () => {
11 | var store;
12 | var next;
13 | var partialMiddleware;
14 |
15 | before(() => {
16 | store = {
17 | dispatch: sinon.spy(),
18 | getState: sinon.spy()
19 | };
20 |
21 | next = sinon.spy();
22 |
23 | partialMiddleware = middleware(store)(next);
24 | });
25 |
26 | describe('when dispatching a non-COMMIT action', () => {
27 | it('should just call next on the action', () => {
28 | const action = {
29 | type: 'NAME_SPACE/UNKNOWN',
30 | payload: {
31 | test: 'true'
32 | }
33 | };
34 |
35 | partialMiddleware(action);
36 |
37 | expect(next).to.have.been.calledWithExactly(action);
38 | });
39 | });
40 |
41 | describe('when dispatching a COMMIT action', () => {
42 | const type = 'NAME_SPACE/COMMIT';
43 | describe('when there are only sync payload keys', () => {
44 | it('should just call next on the action', () => {
45 | const action = {
46 | type,
47 | payload: {
48 | test: 'true',
49 | test2: ['test3']
50 | }
51 | };
52 |
53 | partialMiddleware(action);
54 |
55 | expect(next).to.have.been.calledWithExactly(action);
56 | });
57 | });
58 |
59 | describe('when there are async payload keys', () => {
60 | var def1;
61 | var def2;
62 |
63 | before(() => {
64 | def1 = deferred();
65 | def2 = deferred();
66 | });
67 |
68 | it('should call next with the async keys action', () => {
69 | const action = {
70 | type,
71 | path: 'path',
72 | payload: {
73 | test: 'true',
74 | test2: ['test3'],
75 | async1: def1.promise,
76 | async2: def2.promise
77 | }
78 | };
79 |
80 | partialMiddleware(action);
81 |
82 | const args = next.args[2][0];
83 |
84 | expect(args.type).to.eql('NAME_SPACE/COMMIT');
85 | expect(args.path).to.eql('path');
86 | expect(omit(args.payload, 'async1', 'async2')).to.eql({
87 | test: 'true',
88 | test2: ['test3']
89 | });
90 |
91 | expect(isPending(updeep(args.payload, {}).async1)).to.eq(true);
92 | expect(isPending(updeep(args.payload, {}).async2)).to.eq(true);
93 | });
94 |
95 | it('should call next on the async key when they resolve', () => {
96 | def1.resolve({newValue: 'true'});
97 |
98 | return def1.promise.then(() => {
99 | expect(store.dispatch).to.have.been.calledWithExactly({
100 | type: `${type}_SUCCESS`,
101 | path: 'path',
102 | payload: {
103 | async1: {newValue: 'true'}
104 | }
105 | });
106 | });
107 | });
108 |
109 | it('should call next on the async key when they are rejected', () => {
110 | def2.reject('error message');
111 |
112 | return def1.promise.catch(() => {
113 | expect(store.dispatch).to.have.been.calledWithExactly({
114 | type: `${type}_ERROR`,
115 | error: 'error message',
116 | path: 'path',
117 | payload: {
118 | async2Status: 'error'
119 | }
120 | });
121 | });
122 | });
123 | });
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/src/__tests__/setup.js:
--------------------------------------------------------------------------------
1 | var chai = require('chai');
2 | global.sinon = require('sinon');
3 | var sinonChai = require('sinon-chai');
4 | chai.use(sinonChai);
5 | var chaiAsPromised = require('chai-as-promised');
6 | chai.use(chaiAsPromised);
7 |
--------------------------------------------------------------------------------
/src/__tests__/splitAsyncKeys.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import pick from 'lodash/pick';
3 | import omit from 'lodash/omit';
4 |
5 | import splitAsyncKeys from '../splitAsyncKeys';
6 |
7 | describe('modux/splitAsyncKeys', () => {
8 | it('should properly bucket keys', () => {
9 | var data = {
10 | asyncKey: new Promise(() => {}),
11 | syncKey1: 'test',
12 | syncKey2: ['test2', 'test3'],
13 | nullKey: null
14 | };
15 |
16 | var {syncDiff, asyncDiff} = splitAsyncKeys(data);
17 |
18 | expect(asyncDiff).to.eql(pick(data, 'asyncKey'));
19 | expect(syncDiff).to.eql(omit(data, 'asyncKey'));
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/asyncDispatch.js:
--------------------------------------------------------------------------------
1 | import eventual from 'eventual-values';
2 | import updeep from 'updeep';
3 |
4 | function isNotReadyOrUndefined(value) {
5 | return value === undefined || !eventual.isReady(value);
6 | }
7 |
8 | function makeAction(type, key, path, status, overrideStatus, payload = null) {
9 | const action = {
10 | type: status === 'pending' ? type : `${type}_${status.toUpperCase()}`
11 | };
12 |
13 | switch (status) {
14 | case 'success':
15 | action.payload = {
16 | [key]: eventual.resolve(payload)
17 | };
18 | break;
19 | case 'error':
20 | action.payload = {
21 | [key]: eventual.reject(payload)
22 | };
23 |
24 | break;
25 | default:
26 | action.payload = {
27 | [key]: overrideStatus ?
28 | eventual() :
29 | updeep.ifElse(isNotReadyOrUndefined, eventual, eventual.resolve)
30 | };
31 |
32 | break;
33 | }
34 |
35 | if (path !== null) {
36 | action.path = path;
37 | }
38 |
39 | return action;
40 | }
41 |
42 | export default function asyncDispatch(
43 | dispatch,
44 | type,
45 | key,
46 | promise,
47 | overrideStatus = false,
48 | path = null
49 | ) {
50 | promise.then((data) => {
51 | dispatch(makeAction(type, key, path, 'success', overrideStatus, data));
52 | }).catch((error) => {
53 | dispatch(makeAction(type, key, path, 'error', overrideStatus, error));
54 | });
55 |
56 | return makeAction(type, key, path, 'pending', overrideStatus);
57 | }
58 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import splitAsyncKeys from './splitAsyncKeys';
2 | import asyncDispatch from './asyncDispatch';
3 |
4 | export default function magicAsyncMiddleware(store) {
5 | return next => action => {
6 | if (
7 | action.payload &&
8 | typeof action.payload === 'object' &&
9 | action.payload.promise === undefined &&
10 | action.payload.length === undefined
11 | ) {
12 | const {syncDiff, asyncDiff} = splitAsyncKeys(action.payload);
13 |
14 | if (Object.keys(asyncDiff).length > 0) {
15 | const asyncAction = Object.keys(asyncDiff).reduce((memo, key) => {
16 | const promise = asyncDiff[key];
17 |
18 | const values = asyncDispatch(
19 | store.dispatch,
20 | action.type,
21 | key,
22 | promise,
23 | action.overrideStatus,
24 | action.path
25 | );
26 |
27 | return {
28 | ...memo,
29 | payload: {
30 | ...memo.payload,
31 | ...values.payload
32 | }
33 | };
34 | }, {
35 | type: action.type,
36 | path: action.path,
37 | payload: syncDiff
38 | });
39 |
40 | next(asyncAction);
41 | } else {
42 | next(action);
43 | }
44 | } else {
45 | next(action);
46 | }
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/src/splitAsyncKeys.js:
--------------------------------------------------------------------------------
1 | export default function splitAsyncKeys(stateDiff) {
2 | if (typeof stateDiff === 'function') {
3 | return {syncDiff: stateDiff, asyncDiff: {}};
4 | }
5 |
6 | let syncDiff = {};
7 | let asyncDiff = {};
8 |
9 | Object.keys(stateDiff).forEach((key) => {
10 | const value = stateDiff[key];
11 |
12 | if (
13 | value !== null &&
14 | typeof value === 'object' &&
15 | typeof value.then === 'function'
16 | ) {
17 | asyncDiff[key] = value;
18 | } else {
19 | syncDiff[key] = value;
20 | }
21 | });
22 |
23 | return {syncDiff, asyncDiff};
24 | }
25 |
--------------------------------------------------------------------------------
/src/version.js:
--------------------------------------------------------------------------------
1 | export default '0.1.0';
2 |
--------------------------------------------------------------------------------