Skip to content

Commit 25111a6

Browse files
authored
Merge pull request #28 from kouhin/feature/async
(Refactoring) use async.js instead of async/await
2 parents 10e31aa + f77ced3 commit 25111a6

26 files changed

+909
-713
lines changed

.babelrc

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

package.json

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "1.0.0",
44
"description": "Loads async data for Redux apps focusing on preventing duplicated requests and dealing with async dependencies.",
55
"main": "lib/index.js",
6+
"jsnext:main": "src/index.js",
67
"repository": {
78
"type": "git",
89
"url": "https://github.com/kouhin/redux-dataloader.git"
@@ -12,17 +13,14 @@
1213
},
1314
"homepage": "https://github.com/kouhin/redux-dataloader",
1415
"scripts": {
15-
"clean": "rimraf lib dist coverage",
16-
"build": "npm run clean && npm run build:commonjs && npm run build:umd",
17-
"build:commonjs": "babel src --out-dir lib",
18-
"build:umd": "webpack",
16+
"clean": "rimraf lib coverage",
17+
"build": "babel src --out-dir lib",
1918
"prepublish": "npm run clean && npm run lint && npm test && npm run build",
2019
"test": "mocha test/*.js --opts mocha.opts",
2120
"test:cov": "babel-node $(npm bin)/isparta cover $(npm bin)/_mocha test/*.js -- --opts mocha.opts",
2221
"lint": "eslint src test"
2322
},
2423
"files": [
25-
"dist",
2624
"lib",
2725
"src"
2826
],
@@ -42,29 +40,26 @@
4240
"license": "MIT",
4341
"devDependencies": {
4442
"babel-cli": "^6.18.0",
45-
"babel-core": "^6.21.0",
43+
"babel-core": "^6.20.0",
4644
"babel-eslint": "^7.1.1",
4745
"babel-loader": "^6.2.10",
4846
"babel-plugin-transform-runtime": "^6.15.0",
49-
"babel-polyfill": "^6.20.0",
5047
"babel-preset-es2015": "^6.18.0",
51-
"babel-preset-react": "^6.16.0",
5248
"babel-preset-stage-0": "^6.16.0",
5349
"chai": "^3.5.0",
54-
"eslint-config-airbnb-deps": "^13.0.0",
50+
"eslint-config-airbnb-deps": "^14.0.0",
5551
"isparta": "^4.0.0",
5652
"istanbul": "^0.4.5",
5753
"mocha": "^3.2.0",
54+
"redux": "^3.6.0",
5855
"rimraf": "^2.5.4",
59-
"sinon": "^1.17.6",
60-
"webpack": "^1.14.0"
56+
"sinon": "^1.17.6"
6157
},
6258
"dependencies": {
63-
"babel-runtime": "^6.20.0",
59+
"async": "^2.1.4",
6460
"lodash": "^4.17.2"
6561
},
6662
"eslintConfig": {
67-
"parser": "babel-eslint",
6863
"extends": "eslint-config-airbnb",
6964
"env": {
7065
"browser": true,
@@ -79,19 +74,11 @@
7974
"import/no-extraneous-dependencies": [
8075
"error",
8176
{
82-
"devDependencies": true
77+
"devDependencies": [
78+
"**/*.test.js"
79+
]
8380
}
8481
]
8582
}
86-
},
87-
"babel": {
88-
"presets": [
89-
"es2015",
90-
"react",
91-
"stage-0"
92-
],
93-
"plugins": [
94-
"transform-runtime"
95-
]
9683
}
9784
}

src/Task.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import asyncify from 'async/asyncify';
2+
import retry from 'async/retry';
3+
import assign from 'lodash/assign';
4+
5+
import { loadFailure, loadSuccess } from './actions';
6+
import { isAction } from './utils';
7+
import { DEFAULT_OPTIONS } from './constants';
8+
9+
export default class Task {
10+
constructor(context, monitoredAction, params = {}) {
11+
if (!isAction(monitoredAction)) {
12+
throw new Error('action must be a plain object');
13+
}
14+
15+
this.context = assign({}, context, {
16+
action: monitoredAction,
17+
});
18+
19+
this.params = assign({}, {
20+
success({ action }) {
21+
throw new Error('success() is not implemented', action.type);
22+
},
23+
error({ action }) {
24+
throw new Error('error() is not implemented', action.type);
25+
},
26+
loading({ action }) {
27+
return action;
28+
},
29+
shouldFetch() {
30+
return true;
31+
},
32+
fetch({ action }) {
33+
throw new Error('Not implemented', action);
34+
},
35+
}, params);
36+
}
37+
38+
execute(options = {}, callback) {
39+
const opts = assign({}, DEFAULT_OPTIONS, options);
40+
41+
const context = this.context;
42+
const dispatch = context.dispatch;
43+
const {
44+
success,
45+
error,
46+
loading,
47+
shouldFetch,
48+
fetch,
49+
} = this.params;
50+
51+
const disableInternalAction = options.disableInternalAction;
52+
53+
if (!shouldFetch(context)) {
54+
callback(null, null); // load nothing
55+
if (!disableInternalAction) {
56+
const successAction = loadSuccess(context.action);
57+
dispatch(successAction);
58+
}
59+
return;
60+
}
61+
62+
dispatch(loading(context));
63+
64+
// Retry
65+
const asyncFetch = asyncify(fetch);
66+
retry({
67+
times: opts.retryTimes,
68+
interval: opts.retryWait,
69+
}, (retryCb) => {
70+
asyncFetch(context, retryCb);
71+
}, (err, result) => {
72+
if (err) {
73+
const errorAction = error(context, err);
74+
if (!disableInternalAction) {
75+
dispatch(loadFailure(context.action, err));
76+
}
77+
callback(null, dispatch(errorAction));
78+
return;
79+
}
80+
const successAction = success(context, result);
81+
callback(null, dispatch(successAction));
82+
if (!disableInternalAction) {
83+
dispatch(loadSuccess(context.action, result));
84+
}
85+
});
86+
}
87+
}

src/TaskDescriptor.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import isEqual from 'lodash/isEqual';
2+
import assign from 'lodash/assign';
3+
4+
import Task from './Task';
5+
import { DEFAULT_OPTIONS } from './constants';
6+
7+
export default class TaskDescriptor {
8+
constructor(pattern, params, options = {}) {
9+
this.pattern = pattern;
10+
this.params = params;
11+
this.options = assign({}, DEFAULT_OPTIONS, options);
12+
if (this.options.retryTimes < 1) {
13+
this.options.retryTimes = 1;
14+
}
15+
}
16+
17+
supports(action) {
18+
switch (typeof this.pattern) {
19+
case 'object':
20+
return isEqual(this.pattern, action);
21+
case 'function':
22+
return this.pattern(action) === true;
23+
default:
24+
return this.pattern === action.type;
25+
}
26+
}
27+
28+
newTask(context, action) {
29+
return new Task(context, action, this.params);
30+
}
31+
}
File renamed without changes.

src/constants.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { fixedWait } from './waitStrategies';
2+
3+
export const DEFAULT_OPTIONS = {
4+
ttl: 10000, // Default TTL: 10s
5+
retryTimes: 1,
6+
retryWait: fixedWait(0),
7+
};
8+
9+
export const REDUX_DATALOADER_ACTION_ID = () => {};

src/createDataLoaderMiddleware.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import findKey from 'lodash/findKey';
2+
import find from 'lodash/find';
3+
import isEqual from 'lodash/isEqual';
4+
import assign from 'lodash/assign';
5+
import flattenDeep from 'lodash/flattenDeep';
6+
import get from 'lodash/get';
7+
import isInteger from 'lodash/isInteger';
8+
9+
import { REDUX_DATALOADER_ACTION_ID } from './constants';
10+
11+
function findTaskKey(runningTasksMap, action) {
12+
return findKey(runningTasksMap, o =>
13+
(o.action.type === action.type && isEqual(o.action, action)));
14+
}
15+
16+
export default function createDataLoaderMiddleware(
17+
loaders = [],
18+
withArgs = {},
19+
middlewareOpts = {},
20+
) {
21+
const flattenedLoaders = flattenDeep(loaders);
22+
let currentId = 1;
23+
const uniqueId = (prefix) => {
24+
currentId += 1;
25+
return `${prefix}${currentId}`;
26+
};
27+
28+
const middleware = ({ dispatch, getState }) => {
29+
middleware.cache = {};
30+
const ctx = assign({}, withArgs, {
31+
dispatch,
32+
getState,
33+
});
34+
35+
return next => (receivedAction) => {
36+
// eslint-disable-next-line no-underscore-dangle
37+
if (receivedAction._id !== REDUX_DATALOADER_ACTION_ID) {
38+
return next(receivedAction);
39+
}
40+
return receivedAction.then((asyncAction) => {
41+
// dispatch data loader request action
42+
next(asyncAction);
43+
44+
const { action } = asyncAction.meta;
45+
const taskKey = findTaskKey(middleware.cache, action);
46+
if (taskKey) {
47+
return middleware.cache[taskKey].promise;
48+
}
49+
50+
const taskDescriptor = find(flattenedLoaders, loader => loader.supports(action));
51+
if (!taskDescriptor) {
52+
throw new Error('No loader for action', action);
53+
}
54+
55+
// Priority: Action Meta Options > TaskDescriptor Options > Middleware Options
56+
const options = assign(
57+
{},
58+
middlewareOpts,
59+
taskDescriptor.options,
60+
get(asyncAction, 'meta.options', {}),
61+
);
62+
63+
const task = taskDescriptor.newTask(ctx, action);
64+
const runningTask = new Promise((resolve, reject) => {
65+
task.execute(options, (err, result) => {
66+
if (err) {
67+
reject(err);
68+
} else {
69+
resolve(result);
70+
}
71+
});
72+
});
73+
74+
if (isInteger(options.ttl) && options.ttl > 0) {
75+
const key = uniqueId(`${action.type}__`);
76+
middleware.cache[key] = { action, promise: runningTask };
77+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
78+
setTimeout(() => {
79+
delete middleware.cache[key];
80+
}, options.ttl);
81+
}
82+
}
83+
return runningTask;
84+
});
85+
};
86+
};
87+
return middleware;
88+
}

src/createLoader.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import TaskDescriptor from './TaskDescriptor';
2+
3+
/**
4+
* Create a new TaskDescriptor
5+
*
6+
* @param {string|object|function} pattern pattern to match action
7+
* @param {object} params parameters
8+
* @param {object} options options
9+
* @returns {TaskDescriptor} a descriptor object for creating data loader
10+
*/
11+
export default function createLoader(pattern, params, options) {
12+
return new TaskDescriptor(pattern, params, options);
13+
}

0 commit comments

Comments
 (0)