Skip to content

Commit

Permalink
Merge pull request #31 from kouhin/release/v1.1.0
Browse files Browse the repository at this point in the history
Release/v1.1.0
  • Loading branch information
kouhin authored Feb 15, 2017
2 parents 228349b + 6037ddb commit 67d76c5
Show file tree
Hide file tree
Showing 26 changed files with 911 additions and 715 deletions.
5 changes: 5 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"presets": [
"es2015"
]
}
39 changes: 13 additions & 26 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "redux-dataloader",
"version": "1.0.0",
"version": "1.1.0",
"description": "Loads async data for Redux apps focusing on preventing duplicated requests and dealing with async dependencies.",
"main": "lib/index.js",
"jsnext:main": "src/index.js",
"repository": {
"type": "git",
"url": "https://github.com/kouhin/redux-dataloader.git"
Expand All @@ -12,17 +13,14 @@
},
"homepage": "https://github.com/kouhin/redux-dataloader",
"scripts": {
"clean": "rimraf lib dist coverage",
"build": "npm run clean && npm run build:commonjs && npm run build:umd",
"build:commonjs": "babel src --out-dir lib",
"build:umd": "webpack",
"prepublish": "npm run clean && npm run lint && npm test && npm run build",
"prepare": "npm run clean && npm run lint && npm test && npm run build",
"clean": "rimraf lib coverage",
"build": "babel src --out-dir lib",
"test": "mocha test/*.js --opts mocha.opts",
"test:cov": "babel-node $(npm bin)/isparta cover $(npm bin)/_mocha test/*.js -- --opts mocha.opts",
"lint": "eslint src test"
},
"files": [
"dist",
"lib",
"src"
],
Expand All @@ -42,29 +40,26 @@
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-core": "^6.21.0",
"babel-core": "^6.20.0",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-polyfill": "^6.20.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.16.0",
"chai": "^3.5.0",
"eslint-config-airbnb-deps": "^13.0.0",
"eslint-config-airbnb-deps": "^14.0.0",
"isparta": "^4.0.0",
"istanbul": "^0.4.5",
"mocha": "^3.2.0",
"redux": "^3.6.0",
"rimraf": "^2.5.4",
"sinon": "^1.17.6",
"webpack": "^1.14.0"
"sinon": "^1.17.6"
},
"dependencies": {
"babel-runtime": "^6.20.0",
"async": "^2.1.4",
"lodash": "^4.17.2"
},
"eslintConfig": {
"parser": "babel-eslint",
"extends": "eslint-config-airbnb",
"env": {
"browser": true,
Expand All @@ -79,19 +74,11 @@
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
"devDependencies": [
"**/*.test.js"
]
}
]
}
},
"babel": {
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": [
"transform-runtime"
]
}
}
87 changes: 87 additions & 0 deletions src/Task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import asyncify from 'async/asyncify';
import retry from 'async/retry';
import assign from 'lodash/assign';

import { loadFailure, loadSuccess } from './actions';
import { isAction } from './utils';
import { DEFAULT_OPTIONS } from './constants';

export default class Task {
constructor(context, monitoredAction, params = {}) {
if (!isAction(monitoredAction)) {
throw new Error('action must be a plain object');
}

this.context = assign({}, context, {
action: monitoredAction,
});

this.params = assign({}, {
success({ action }) {
throw new Error('success() is not implemented', action.type);
},
error({ action }) {
throw new Error('error() is not implemented', action.type);
},
loading({ action }) {
return action;
},
shouldFetch() {
return true;
},
fetch({ action }) {
throw new Error('Not implemented', action);
},
}, params);
}

execute(options = {}, callback) {
const opts = assign({}, DEFAULT_OPTIONS, options);

const context = this.context;
const dispatch = context.dispatch;
const {
success,
error,
loading,
shouldFetch,
fetch,
} = this.params;

const disableInternalAction = options.disableInternalAction;

if (!shouldFetch(context)) {
callback(null, null); // load nothing
if (!disableInternalAction) {
const successAction = loadSuccess(context.action);
dispatch(successAction);
}
return;
}

dispatch(loading(context));

// Retry
const asyncFetch = asyncify(fetch);
retry({
times: opts.retryTimes,
interval: opts.retryWait,
}, (retryCb) => {
asyncFetch(context, retryCb);
}, (err, result) => {
if (err) {
const errorAction = error(context, err);
if (!disableInternalAction) {
dispatch(loadFailure(context.action, err));
}
callback(null, dispatch(errorAction));
return;
}
const successAction = success(context, result);
callback(null, dispatch(successAction));
if (!disableInternalAction) {
dispatch(loadSuccess(context.action, result));
}
});
}
}
31 changes: 31 additions & 0 deletions src/TaskDescriptor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import isEqual from 'lodash/isEqual';
import assign from 'lodash/assign';

import Task from './Task';
import { DEFAULT_OPTIONS } from './constants';

export default class TaskDescriptor {
constructor(pattern, params, options = {}) {
this.pattern = pattern;
this.params = params;
this.options = assign({}, DEFAULT_OPTIONS, options);
if (this.options.retryTimes < 1) {
this.options.retryTimes = 1;
}
}

supports(action) {
switch (typeof this.pattern) {
case 'object':
return isEqual(this.pattern, action);
case 'function':
return this.pattern(action) === true;
default:
return this.pattern === action.type;
}
}

newTask(context, action) {
return new Task(context, action, this.params);
}
}
File renamed without changes.
9 changes: 9 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { fixedWait } from './waitStrategies';

export const DEFAULT_OPTIONS = {
ttl: 10000, // Default TTL: 10s
retryTimes: 1,
retryWait: fixedWait(0),
};

export const REDUX_DATALOADER_ACTION_ID = () => {};
88 changes: 88 additions & 0 deletions src/createDataLoaderMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import findKey from 'lodash/findKey';
import find from 'lodash/find';
import isEqual from 'lodash/isEqual';
import assign from 'lodash/assign';
import flattenDeep from 'lodash/flattenDeep';
import get from 'lodash/get';
import isInteger from 'lodash/isInteger';

import { REDUX_DATALOADER_ACTION_ID } from './constants';

function findTaskKey(runningTasksMap, action) {
return findKey(runningTasksMap, o =>
(o.action.type === action.type && isEqual(o.action, action)));
}

export default function createDataLoaderMiddleware(
loaders = [],
withArgs = {},
middlewareOpts = {},
) {
const flattenedLoaders = flattenDeep(loaders);
let currentId = 1;
const uniqueId = (prefix) => {
currentId += 1;
return `${prefix}${currentId}`;
};

const middleware = ({ dispatch, getState }) => {
middleware.runningTasks = {};
const ctx = assign({}, withArgs, {
dispatch,
getState,
});

return next => (receivedAction) => {
// eslint-disable-next-line no-underscore-dangle
if (receivedAction._id !== REDUX_DATALOADER_ACTION_ID) {
return next(receivedAction);
}
return receivedAction.then((asyncAction) => {
// dispatch data loader request action
next(asyncAction);

const { action } = asyncAction.meta;
const taskKey = findTaskKey(middleware.runningTasks, action);
if (taskKey) {
return middleware.runningTasks[taskKey].promise;
}

const taskDescriptor = find(flattenedLoaders, loader => loader.supports(action));
if (!taskDescriptor) {
throw new Error('No loader for action', action);
}

// Priority: Action Meta Options > TaskDescriptor Options > Middleware Options
const options = assign(
{},
middlewareOpts,
taskDescriptor.options,
get(asyncAction, 'meta.options', {}),
);

const task = taskDescriptor.newTask(ctx, action);
const runningTask = new Promise((resolve, reject) => {
task.execute(options, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});

if (isInteger(options.ttl) && options.ttl > 0) {
const key = uniqueId(`${action.type}__`);
middleware.runningTasks[key] = { action, promise: runningTask };
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
setTimeout(() => {
delete middleware.runningTasks[key];
}, options.ttl);
}
}
return runningTask;
});
};
};
return middleware;
}
13 changes: 13 additions & 0 deletions src/createLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import TaskDescriptor from './TaskDescriptor';

/**
* Create a new TaskDescriptor
*
* @param {string|object|function} pattern pattern to match action
* @param {object} params parameters
* @param {object} options options
* @returns {TaskDescriptor} a descriptor object for creating data loader
*/
export default function createLoader(pattern, params, options) {
return new TaskDescriptor(pattern, params, options);
}
Loading

0 comments on commit 67d76c5

Please sign in to comment.