Skip to content
This repository was archived by the owner on Dec 6, 2022. It is now read-only.

Commit a9e3cde

Browse files
author
Andrey Kogut
committed
init
0 parents  commit a9e3cde

File tree

6 files changed

+272
-0
lines changed

6 files changed

+272
-0
lines changed

.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["es2015"]
3+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
*.log
3+
lib

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src

README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
redux-async-flow
2+
================
3+
4+
This [redux middleware](http://rackt.github.io/redux/docs/advanced/Middleware.html) allows dispatching async actions, that may include:
5+
- promise
6+
- array of promises, that will be chained
7+
- functions returning promises
8+
- array of functions returning prommises, that will be chained
9+
- array of other async actions, that will be chained
10+
- functions returning anything, that will be chained
11+
- array of functions returning anything, that will be chained
12+
13+
It will handle errors in promises, dispatching ERROR actions. Exceptions, that occured in your reducer *won't be swallowed* and will be thrown.
14+
15+
16+
Example
17+
-------
18+
19+
Lets say, you have some functions, returning promises for web api requests:
20+
21+
```js
22+
const apiLogin = password => new Promise((resolve, reject) => {
23+
setTimeout(() => {
24+
resolve(15921);
25+
}, 1000);
26+
});
27+
28+
const apiPullUserProfile = userId => new Promise((resolve, reject) => {
29+
if (userId === 15921) {
30+
setTimeout(() => {
31+
resolve({
32+
avatarUrl: "https://img.example.org/15921/Gc2xcYMvRieoA8.png"
33+
});
34+
}, 1000);
35+
}
36+
});
37+
38+
const apiPullAvatar = avatarUrl => new Promise((resolve, reject) => {
39+
if (avatarUrl === "https://img.example.org/15921/Gc2xcYMvRieoA8.png") {
40+
setTimeout(() => {
41+
resolve("Done");
42+
}, 1000);
43+
}
44+
});
45+
```
46+
47+
You can make corresponding actions creators:
48+
```js
49+
const startLoggingIn = password => ({
50+
types: ["LOGGING_IN_STARTED", "LOGGING_IN_SUCCESS", "LOGGING_IN_ERROR"],
51+
payload: apiLogin(password)
52+
});
53+
54+
const startPullingProfile = userId => ({
55+
types: ["PULLING_PROFILE_STARTED", "PULLING_PROFILE_SUCCESS", "PULLING_PROFILE_ERROR"],
56+
payload: apiLogin(userId)
57+
});
58+
59+
const startPullingAvatar = avatarUrl => ({
60+
types: ["PULLING_AVATAR_STARTED", "PULLING_AVATAR_SUCCESS", "PULLING_AVATAR_ERROR"],
61+
payload: apiLogin(avatarUrl)
62+
});
63+
```
64+
65+
Now you can make one action creator to rule them all:
66+
```js
67+
const startEverything = password => ({
68+
types: ["EVERYTHING_STARTED", "EVERYTHING_SUCCESS", "EVERYTHING_ERROR"],
69+
payload: [
70+
startLoggingIn(password),
71+
startPullingProfile,
72+
startPullingAvatar
73+
]
74+
});
75+
```
76+
77+
That will results in creating actions in the following order:
78+
```
79+
{ type: EVERYTHING_STARTED }
80+
{ type: LOGGING_IN_STARTED }
81+
{ type: LOGGING_IN_SUCCESS, payload: 15921 /* userId */ }
82+
{ type: PULLING_PROFILE_STARTED }
83+
{ type: PULLING_PROFILE_SUCCESS, payload: "https://img.example.org/15921/Gc2xcYMvRieoA8.png" /* avatarUrl */ }
84+
{ type: PULLING_AVATAR_STARTED }
85+
{ type: PULLING_AVATAR_SUCCESS, payload: /* avatar */ }
86+
{ type: EVERYTHING_SUCCESS }
87+
```
88+
89+
You can transform data in the midle of chain, lets replace `https` with `http`:
90+
```js
91+
const startEverything = password => ({
92+
types: ["EVERYTHING_STARTED", "EVERYTHING_SUCCESS", "EVERYTHING_ERROR"],
93+
payload: [
94+
startLoggingIn(password),
95+
startPullingProfile,
96+
url => url.replace("https", "http"),
97+
startPullingAvatar
98+
]
99+
});
100+
101+
// Or even asynchronously:
102+
103+
const startEverything = password => ({
104+
types: ["EVERYTHING_STARTED", "EVERYTHING_SUCCESS", "EVERYTHING_ERROR"],
105+
payload: [
106+
startLoggingIn(password),
107+
startPullingProfile,
108+
url => new Promise((resolve, reject) => {
109+
setTimeout(() => {
110+
resolve(url.replace("https", "http"));
111+
}, 500);
112+
}),
113+
startPullingAvatar
114+
]
115+
});
116+
```
117+
118+
Any questions, propositions, pull requests are welcome in [issues](https://github.com/andykog/redux-async-flow/issues).

package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "redux-async-flow",
3+
"version": "0.0.3",
4+
"description": "Async middleware for Redux",
5+
"main": "lib/index.js",
6+
"scripts": {
7+
"prepublish": "rimraf lib && babel src --out-dir lib"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/andykog/redux-async-flow.git"
12+
},
13+
"keywords": [
14+
"redux",
15+
"async",
16+
"promise",
17+
"middleware"
18+
],
19+
"author": "Andrey Kogut <mail@andreykogut.com>",
20+
"license": "MIT",
21+
"devDependencies": {
22+
"babel": "^6.3.13",
23+
"babel-cli": "^6.3.13",
24+
"babel-core": "^6.3.13",
25+
"rimraf": "^2.4.4"
26+
}
27+
}

src/index.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"use strict";
2+
3+
const isPromise = maybePromise => maybePromise && typeof maybePromise.then === 'function';
4+
5+
const isAsyncAction = maybeAsync => maybeAsync && maybeAsync.types instanceof Array;
6+
7+
const isDispatchError = maybeDispatchError => maybeDispatchError && maybeDispatchError.__isDispatchError === true;
8+
9+
const dispatchedPromise = (promise, types, dispatch) => {
10+
11+
const [START, SUCCESS, FAIL] = types;
12+
13+
dispatch({
14+
type: START,
15+
meta: {
16+
async: true
17+
}
18+
});
19+
20+
return promise.then(
21+
result => {
22+
try {
23+
dispatch({
24+
type: SUCCESS,
25+
payload: result,
26+
meta: {
27+
resolves: START
28+
}
29+
});
30+
} catch(dispatchError) {
31+
dispatchError.__isDispatchError = true;
32+
throw dispatchError;
33+
}
34+
return result;
35+
},
36+
error => {
37+
try {
38+
dispatch({
39+
type: FAIL,
40+
payload: error,
41+
error: true,
42+
meta: {
43+
resolves: START
44+
}
45+
});
46+
} catch(dispatchError) {
47+
dispatchError.__isDispatchError = true;
48+
throw dispatchError;
49+
}
50+
throw error;
51+
}
52+
);
53+
54+
};
55+
56+
57+
const promisifiedDispatchedAsyncAction = (asyncAction, dispatch) => {
58+
59+
const payload = typeof asyncAction.payload === "function" ? asyncAction.payload() : asyncAction.payload;
60+
61+
if (isPromise(payload)) {
62+
63+
return dispatchedPromise(payload, asyncAction.types, dispatch)
64+
65+
} else if (isAsyncAction(payload)) {
66+
67+
return dispatchedPromise(
68+
promisifiedDispatchedAsyncAction(payload, dispatch),
69+
payload.types,
70+
dispatch
71+
);
72+
73+
} else if (payload instanceof Array) {
74+
75+
return dispatchedPromise(
76+
payload.reduce(
77+
(acc, nextPart) => acc.then(
78+
previousResult => {
79+
nextPart = typeof nextPart === "function" ? nextPart(previousResult) : nextPart;
80+
if (isPromise(nextPart)) {
81+
return nextPart;
82+
} else if (isAsyncAction(nextPart)) {
83+
return promisifiedDispatchedAsyncAction(nextPart, dispatch)
84+
} else {
85+
return nextPart;
86+
}
87+
}
88+
),
89+
Promise.resolve()
90+
),
91+
asyncAction.types,
92+
dispatch
93+
);
94+
95+
} else {
96+
97+
return(Promise.resolve(payload));
98+
99+
}
100+
101+
};
102+
103+
export default ({ dispatch }) => next => action => {
104+
105+
if (isAsyncAction(action)) {
106+
107+
promisifiedDispatchedAsyncAction(action, dispatch)
108+
.catch(error => {
109+
if (isDispatchError(error)) {
110+
throw error;
111+
}
112+
});
113+
114+
} else {
115+
116+
next(action);
117+
118+
}
119+
120+
};

0 commit comments

Comments
 (0)