From f77ced373dd2e9ee39929a93cd59cae2db7b8090 Mon Sep 17 00:00:00 2001 From: kou_hin Date: Wed, 11 Jan 2017 20:31:06 +0900 Subject: [PATCH 1/4] use async.js instead of async/await --- .babelrc | 5 + package.json | 35 ++-- src/Task.js | 87 +++++++++ src/TaskDescriptor.js | 31 +++ src/{action.js => actions.js} | 0 src/constants.js | 9 + src/createDataLoaderMiddleware.js | 88 +++++++++ src/createLoader.js | 13 ++ src/data-loader.js | 171 ---------------- src/index.js | 8 +- src/load.js | 11 +- src/middleware.js | 74 ------- src/utils.js | 9 +- src/wait-strategies.js | 75 ------- src/waitStrategies.js | 64 ++++++ test/TaskDescriptor.test.js | 144 ++++++++++++++ test/{action.test.js => actions.test.js} | 4 +- test/createDataLoaderMiddleware.test.js | 237 +++++++++++++++++++++++ test/createLoader.test.js | 68 +++++++ test/data-loader.test.js | 202 ------------------- test/load.test.js | 4 +- test/middleware.test.js | 22 --- test/utils.test.js | 1 - test/wait-strategies.test.js | 119 ------------ test/waitStrategies.test.js | 119 ++++++++++++ webpack.config.js | 22 +-- 26 files changed, 909 insertions(+), 713 deletions(-) create mode 100644 .babelrc create mode 100644 src/Task.js create mode 100644 src/TaskDescriptor.js rename src/{action.js => actions.js} (100%) create mode 100644 src/constants.js create mode 100644 src/createDataLoaderMiddleware.js create mode 100644 src/createLoader.js delete mode 100644 src/data-loader.js delete mode 100644 src/middleware.js delete mode 100644 src/wait-strategies.js create mode 100644 src/waitStrategies.js create mode 100644 test/TaskDescriptor.test.js rename test/{action.test.js => actions.test.js} (94%) create mode 100644 test/createDataLoaderMiddleware.test.js create mode 100644 test/createLoader.test.js delete mode 100644 test/data-loader.test.js delete mode 100644 test/middleware.test.js delete mode 100644 test/wait-strategies.test.js create mode 100644 test/waitStrategies.test.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..3c078e9 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "es2015" + ] +} diff --git a/package.json b/package.json index 8fcfc07..399bc88 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.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" @@ -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", + "clean": "rimraf lib coverage", + "build": "babel src --out-dir lib", "prepublish": "npm run clean && npm run lint && npm test && npm run build", "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" ], @@ -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, @@ -79,19 +74,11 @@ "import/no-extraneous-dependencies": [ "error", { - "devDependencies": true + "devDependencies": [ + "**/*.test.js" + ] } ] } - }, - "babel": { - "presets": [ - "es2015", - "react", - "stage-0" - ], - "plugins": [ - "transform-runtime" - ] } } diff --git a/src/Task.js b/src/Task.js new file mode 100644 index 0000000..5c51167 --- /dev/null +++ b/src/Task.js @@ -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)); + } + }); + } +} diff --git a/src/TaskDescriptor.js b/src/TaskDescriptor.js new file mode 100644 index 0000000..18dc138 --- /dev/null +++ b/src/TaskDescriptor.js @@ -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); + } +} diff --git a/src/action.js b/src/actions.js similarity index 100% rename from src/action.js rename to src/actions.js diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..93f9552 --- /dev/null +++ b/src/constants.js @@ -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 = () => {}; diff --git a/src/createDataLoaderMiddleware.js b/src/createDataLoaderMiddleware.js new file mode 100644 index 0000000..f6a3dd6 --- /dev/null +++ b/src/createDataLoaderMiddleware.js @@ -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.cache = {}; + 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.cache, action); + if (taskKey) { + return middleware.cache[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.cache[key] = { action, promise: runningTask }; + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + setTimeout(() => { + delete middleware.cache[key]; + }, options.ttl); + } + } + return runningTask; + }); + }; + }; + return middleware; +} diff --git a/src/createLoader.js b/src/createLoader.js new file mode 100644 index 0000000..57a1c6b --- /dev/null +++ b/src/createLoader.js @@ -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); +} diff --git a/src/data-loader.js b/src/data-loader.js deleted file mode 100644 index c1ab059..0000000 --- a/src/data-loader.js +++ /dev/null @@ -1,171 +0,0 @@ -import isEqual from 'lodash/isEqual'; - -import { loadFailure, loadSuccess } from './action'; -import { isAction } from './utils'; -import { fixedWait } from './wait-strategies'; - -const DEFAULT_OPTIONS = { - ttl: 10000, // Default TTL: 10s - retryTimes: 1, - retryWait: fixedWait(0), -}; - -function sleep(ms = 0) { - return new Promise(r => setTimeout(r, ms)); -} - -class DataLoaderTask { - constructor(context, monitoredAction, params = {}) { - if (!isAction(monitoredAction)) { - throw new Error('action must be a plain object'); - } - - this.context = { - ...context, - action: monitoredAction, - }; - - this.params = { - 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; - }, - async fetch({ action }) { - throw new Error('Not implemented', action); - }, - ...params, - }; - } - - async execute(options = {}) { - const opts = { - ...DEFAULT_OPTIONS, - ...options, - }; - - const disableInternalAction = !!options.disableInternalAction; - - if (!this.params.shouldFetch(this.context)) { - if (!disableInternalAction) { - const successAction = loadSuccess(this.context.action); - this.context.dispatch(successAction); // load nothing - } - return null; - } - const loadingAction = this.params.loading(this.context); - this.context.dispatch(loadingAction); - - let currentRetry = 0; - let result; - let error; - - for (;;) { - try { - result = await this.params.fetch(this.context); - break; - } catch (ex) { - currentRetry += 1; - if (options.retryTimes && currentRetry < opts.retryTimes) { - const sleepTime = opts.retryWait.next().value; - await sleep(sleepTime); - } else { - error = ex; - break; - } - } - } - - if (!error) { - const successAction = this.params.success(this.context, result); - - // Check successAction - if (successAction.type === this.context.action.type) { - const errorAction = this.params.error( - this.context, - new Error('Result action type equals origial action type', this.context.action), - ); - this.context.dispatch(errorAction); - if (!disableInternalAction) { - this.context.dispatch(loadFailure(this.context.action, error)); - } - return errorAction; - } - - this.context.dispatch(successAction); - if (!disableInternalAction) { - this.context.dispatch(loadSuccess(this.context.action, result)); - } - return successAction; - } - - const errorAction = this.params.error(this.context, error); - - // Check errorAction - if (errorAction.type === this.context.action.type) { - this.context.dispatch(errorAction); - if (!disableInternalAction) { - this.context.dispatch(loadFailure(this.context.action, error)); - } - return errorAction; - } - this.context.dispatch(errorAction); - if (!disableInternalAction) { - this.context.dispatch(loadFailure(this.context.action, error)); - } - return errorAction; - } - -} - -class DataLoaderTaskDescriptor { - constructor(pattern, params, options) { - this.pattern = pattern; - this.params = params; - this.options = { - ...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) { - const worker = new DataLoaderTask(context, action, this.params); - return worker; - } -} - -/** - * Create a new DataLoaderDescriptor - * - * @param {string|object|function} pattern pattern to match action - * @param {object} params parameters - * @param {object} options options - * @returns {DataLoaderTaskDescriptor} a descriptor object for creating data loader - */ -function createLoader(pattern, params, options) { - const dataLoaderDescriptor = new DataLoaderTaskDescriptor(pattern, params, options); - return dataLoaderDescriptor; -} - -export default createLoader; diff --git a/src/index.js b/src/index.js index ec32bb9..add7cd6 100644 --- a/src/index.js +++ b/src/index.js @@ -2,13 +2,13 @@ export { LOAD_DATA_REQUEST_ACTION, LOAD_DATA_SUCCESS_ACTION, LOAD_DATA_FAILURE_ACTION, -} from './action'; +} from './actions'; export { default as load } from './load'; -export { default as createLoader } from './data-loader'; +export { default as createLoader } from './createLoader'; -export { default as createDataLoaderMiddleware } from './middleware'; +export { default as createDataLoaderMiddleware } from './createDataLoaderMiddleware'; export { fixedWait, @@ -17,4 +17,4 @@ export { incrementingWait, noWait, randomWait, -} from './wait-strategies'; +} from './waitStrategies'; diff --git a/src/load.js b/src/load.js index e530feb..e469207 100644 --- a/src/load.js +++ b/src/load.js @@ -1,14 +1,15 @@ -import { loadRequest } from './action'; +import { loadRequest } from './actions'; import { isAction } from './utils'; - -export const DATALOADER_ACTION_ID = () => {}; +import { REDUX_DATALOADER_ACTION_ID } from './constants'; export default function load(action) { if (!isAction(action)) { throw new Error('action must be object', action); } - const asyncAction = Promise.resolve(loadRequest(action)); + const asyncAction = new Promise((resolve) => { + resolve(loadRequest(action)); + }); // eslint-disable-next-line no-underscore-dangle - asyncAction._id = DATALOADER_ACTION_ID; + asyncAction._id = REDUX_DATALOADER_ACTION_ID; return asyncAction; } diff --git a/src/middleware.js b/src/middleware.js deleted file mode 100644 index b7632c4..0000000 --- a/src/middleware.js +++ /dev/null @@ -1,74 +0,0 @@ -import findKey from 'lodash/findKey'; -import find from 'lodash/find'; -import isEqual from 'lodash/isEqual'; -import isInteger from 'lodash/isInteger'; - -import { DATALOADER_ACTION_ID } from './load'; - -function findRunningTaskKey(runningTasksMap, action) { - return findKey(runningTasksMap, o => isEqual(o.action, action)); -} - -export default function createDataLoaderMiddleware(loaders, args, opts) { - const runningTasks = {}; - - let currentId = 1; - currentId += 1; - const uniqueId = prefix => `${prefix}${currentId}`; - - const middleware = ({ dispatch, getState }) => { - const ctx = { - ...args, - dispatch, - getState, - }; - - return next => (receivedAction) => { - // eslint-disable-next-line no-underscore-dangle - if (!receivedAction._id || receivedAction._id !== DATALOADER_ACTION_ID) { - return next(receivedAction); - } - - return receivedAction.then((asyncAction) => { - next(asyncAction); // dispatch data loader request action - const { action } = asyncAction.meta; - const runningTaskKey = findRunningTaskKey(runningTasks, action); - if (runningTaskKey) { - return runningTasks[runningTaskKey].promise; - } - - const taskDescriptor = find(loaders, loader => loader.supports(action)); - - if (!taskDescriptor) { - throw new Error('No loader for action', action); - } - - const options = { - ...opts, - ...taskDescriptor.options, - ...(asyncAction.meta.options || {}), - }; - - const key = uniqueId(`${action.type}__`); - const runningTask = taskDescriptor.newTask(ctx, action).execute(options); - - if (isInteger(options.ttl) && options.ttl > 0) { - runningTasks[key] = { - action, - promise: runningTask, - }; - if (typeof window !== 'undefined' && typeof document !== 'undefined') { - setTimeout(() => { - delete runningTasks[key]; - }, options.ttl); - } - } - return runningTask; - }); - }; - }; - - middleware.runningTasks = runningTasks; - - return middleware; -} diff --git a/src/utils.js b/src/utils.js index a580900..de5e541 100644 --- a/src/utils.js +++ b/src/utils.js @@ -5,7 +5,14 @@ export function isAction(action) { } export function formatError(err) { - const error = (err instanceof Error) ? err : new Error(err); + let error; + if (err instanceof Error) { + error = err; + } else if (typeof err === 'object') { + error = new Error(JSON.stringify(err)); + } else { + error = new Error(err); + } const result = {}; Object.getOwnPropertyNames(error).forEach((key) => { result[key] = error[key]; diff --git a/src/wait-strategies.js b/src/wait-strategies.js deleted file mode 100644 index a09c23b..0000000 --- a/src/wait-strategies.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @fileOverview WaitStrategy creators inspired by [guava-retrying](https://github.com/rholder/guava-retrying) - * @name wait-strategies.js - * @license MIT - */ - -/** - * Returns a wait strategy that sleeps a fixed amount of time before retrying (in millisecond). - */ -export function* fixedWait(sleepTime) { - for (;;) { - yield sleepTime; - } -} - -/** - * Returns a strategy which sleeps for an exponential amount of time after the first failed attempt, - * and in exponentially incrementing amounts after each failed attempt up to the maximumTime. - */ -export function* exponentialWait(multiplier = 1, max = Number.MAX_VALUE) { - let current = 2 * multiplier; - for (;;) { - const next = 2 * current; - if (next > max) { - yield current; - } else { - yield current; - current = next; - } - } -} - -/** - * Returns a strategy which sleeps for an increasing amount of time after the first failed attempt - * and in Fibonacci increments after each failed attempt up to the maximumTime. - */ -export function* fibonacciWait(multiplier = 1, max = Number.MAX_VALUE) { - let fn1 = 1 * multiplier; - let fn2 = 1 * multiplier; - for (;;) { - const current = fn2; - if (fn1 > max) { - yield current; - } else { - fn2 = fn1; - fn1 += current; - yield current; - } - } -} - -/** - * Returns a strategy that sleeps a fixed amount of time after the first failed attempt - * and in incrementing amounts of time after each additional failed attempt. - */ -export function* incrementingWait(initialSleepTime = 0, increment = 1000, max = Number.MAX_VALUE) { - let current = initialSleepTime; - yield current; - for (;;) { - if (current + increment > max) { - return current; - } - current += increment; - yield current; - } -} - -/** - * Returns a strategy that sleeps a random amount of time before retrying. - */ -export function* randomWait(min, max) { - for (;;) { - yield parseInt(min + ((max - min) * Math.random()), 10); - } -} diff --git a/src/waitStrategies.js b/src/waitStrategies.js new file mode 100644 index 0000000..02eb89b --- /dev/null +++ b/src/waitStrategies.js @@ -0,0 +1,64 @@ +/** + * @fileOverview WaitStrategy creators inspired by [guava-retrying](https://github.com/rholder/guava-retrying) + * @name waitStrategies.js + * @license MIT + */ + +/** + * Returns a wait strategy that sleeps a fixed amount of time before retrying (in millisecond). + */ +export function fixedWait(interval = 0) { + return () => interval; +} + +/** + * Returns a strategy which sleeps for an exponential amount of time after the first failed attempt, + * and in exponentially incrementing amounts after each failed attempt up to the maximumTime. + */ +export function exponentialWait(multiplier = 1, max = Number.MAX_VALUE) { + return (count) => { + const next = multiplier * Math.pow(2, count); // eslint-disable-line no-restricted-properties + return next > max ? max : next; + }; +} + +/** + * Returns a strategy which sleeps for an increasing amount of time after the first failed attempt + * and in Fibonacci increments after each failed attempt up to the maximumTime. + */ +export function fibonacciWait(multiplier = 1, max = Number.MAX_VALUE) { + const cache = { + 1: 1 * multiplier, + 2: 1 * multiplier, + }; + + const fabonacci = (count) => { + const n = count < 1 ? 1 : count; + if (typeof cache[n] === 'number') { + return cache[n] > max ? max : cache[n]; + } + const result = fabonacci(n - 1) + fabonacci(n - 2); + cache[n] = result > max ? max : result; + return cache[n]; + }; + return fabonacci; +} + +/** + * Returns a strategy that sleeps a fixed amount of time after the first failed attempt + * and in incrementing amounts of time after each additional failed attempt. + */ +export function incrementingWait(initialSleepTime = 0, increment = 1000, max = Number.MAX_VALUE) { + return (count) => { + const n = count < 1 ? 0 : count - 1; + const next = initialSleepTime + (n * increment); + return next > max ? max : next; + }; +} + +/** + * Returns a strategy that sleeps a random amount of time before retrying. + */ +export function randomWait(min = 0, max = 0) { + return () => parseInt(min + ((max - min) * Math.random()), 10); +} diff --git a/test/TaskDescriptor.test.js b/test/TaskDescriptor.test.js new file mode 100644 index 0000000..dc0b71e --- /dev/null +++ b/test/TaskDescriptor.test.js @@ -0,0 +1,144 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import sinon from 'sinon'; + +import { createLoader } from '../src'; + +describe('DataLoderTaskDescriptor', () => { + const loader = { + success: () => null, + error: () => null, + fetch: () => null, + }; + const descriptor = createLoader(loader); + + it('newTask() should return a task', () => { + expect(descriptor.newTask({}, { + type: 'USER_REQUEST', + })).to.be.ok; + }); + + it('newTask() should throw an error when action is invalid', () => { + expect(() => descriptor.newTask({}, {})).to.throw(Error); + }); + + it('loading -> shouldFetch -> fetch -> success', (done) => { + const loaderObj = { + fetch: () => 20, + success: () => ({ type: 'USER_SUCCESS' }), + error: () => ({ type: 'USER_FAILURE' }), + loading: () => {}, + shouldFetch: () => true, + }; + + const loadingSpy = sinon.spy(loaderObj, 'loading'); + const shouldFetchSpy = sinon.spy(loaderObj, 'shouldFetch'); + const fetchSpy = sinon.spy(loaderObj, 'fetch'); + const successSpy = sinon.spy(loaderObj, 'success'); + const errorSpy = sinon.spy(loaderObj, 'error'); + + const dispatchSpy = sinon.spy(); + + const newLoader = createLoader('USER_REQUEST', loaderObj); + newLoader.newTask({ + dispatch: dispatchSpy, + }, { + type: 'USER_REQUEST', + payload: { + userId: 25, + }, + }).execute({}, (err) => { + if (err) { + done(err); + return; + } + expect(loadingSpy).to.have.been.calledOnce; + expect(shouldFetchSpy).to.have.been.calledOnce; + expect(fetchSpy).to.have.been.calledOnce; + expect(successSpy).to.have.been.calledOnce; + expect(errorSpy).to.have.not.been.called; + sinon.assert.callOrder(shouldFetchSpy, fetchSpy, successSpy); + done(); + }).catch(done); + }); + + it('loading -> shouldFetch(return false) -> noop', (done) => { + const loaderObj = { + fetch: () => 20, + success: () => ({ type: 'USER_SUCCESS' }), + error: () => ({ type: 'USER_FAILURE' }), + loading: () => {}, + shouldFetch: () => false, + }; + + const loadingSpy = sinon.spy(loaderObj, 'loading'); + const shouldFetchSpy = sinon.spy(loaderObj, 'shouldFetch'); + const fetchSpy = sinon.spy(loaderObj, 'fetch'); + const successSpy = sinon.spy(loaderObj, 'success'); + const errorSpy = sinon.spy(loaderObj, 'error'); + + const dispatchSpy = sinon.spy(); + + const newLoader = createLoader('USER_REQUEST', loaderObj); + newLoader.newTask({ + dispatch: dispatchSpy, + }, { + type: 'USER_REQUEST', + payload: { + userId: 25, + }, + }).execute({}, (err, result) => { + if (err) { + done(err); + return; + } + expect(shouldFetchSpy).to.have.been.calledOnce; + expect(loadingSpy).to.have.not.been.called; + expect(fetchSpy).to.have.not.been.calledOnce; + expect(successSpy).to.have.not.been.called; + expect(errorSpy).to.have.not.been.called; + done(null, result); + }); + }); + + it('loading -> shouldFetch -> fetch -> error', (done) => { + const loaderObj = { + fetch: () => Promise.reject('NotFoundError'), + success: () => {}, + error: (context, err) => ({ type: 'USER_FAILURE', error: err }), + loading: () => {}, + shouldFetch: () => true, + }; + + const loadingSpy = sinon.spy(loaderObj, 'loading'); + const shouldFetchSpy = sinon.spy(loaderObj, 'shouldFetch'); + const fetchSpy = sinon.spy(loaderObj, 'fetch'); + const successSpy = sinon.spy(loaderObj, 'success'); + const errorSpy = sinon.spy(loaderObj, 'error'); + + const dispatchSpy = sinon.spy(); + + const newLoader = createLoader('USER_REQUEST', loaderObj); + newLoader.newTask({ + dispatch: dispatchSpy, + }, { + type: 'USER_REQUEST', + payload: { + userId: 25, + }, + }).execute({}, (err, result) => { + if (err) { + done(err); + return; + } + expect(loadingSpy).to.have.been.calledOnce; + expect(shouldFetchSpy).to.have.been.calledOnce; + expect(fetchSpy).to.have.been.calledOnce; + expect(successSpy).to.have.not.been.called; + expect(errorSpy).to.have.been.calledOnce; + sinon.assert.callOrder(shouldFetchSpy, fetchSpy, errorSpy); + done(null, result); + }); + }); +}); diff --git a/test/action.test.js b/test/actions.test.js similarity index 94% rename from test/action.test.js rename to test/actions.test.js index fa65c72..0e79cbe 100644 --- a/test/action.test.js +++ b/test/actions.test.js @@ -2,9 +2,9 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { formatError } from '../src/utils'; -import * as action from '../src/action'; +import * as action from '../src/actions'; -describe('test actions', () => { +describe('actions', () => { const requestAction = { type: 'LOAD_USER_REQUEST', payload: { diff --git a/test/createDataLoaderMiddleware.test.js b/test/createDataLoaderMiddleware.test.js new file mode 100644 index 0000000..70d74bc --- /dev/null +++ b/test/createDataLoaderMiddleware.test.js @@ -0,0 +1,237 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { createStore, applyMiddleware } from 'redux'; + +import { load, createLoader, fixedWait, createDataLoaderMiddleware } from '../src'; + +export const FETCH_USER_REQUEST = 'myapp/user/FETCH_USER/REQUEST'; +export const FETCH_USER_SUCCESS = 'myapp/user/FETCH_USER/SUCCESS'; +export const FETCH_USER_FAILURE = 'myapp/user/FETCH_USER/FAILURE'; + +/* API */ +const users = { + tom: { + age: 21, + givenName: 'Tom', + familyName: 'TomFamilyName', + }, + bob: { + age: 30, + givenName: 'Bob', + familyName: 'BobFamilyName', + }, +}; + +function findUserByUsername(username) { + return new Promise((resolve, reject) => { + setTimeout(() => { + const result = users[username]; + if (!result) { + reject('notFound'); + return; + } + resolve(users[username]); + }, 100); + }); +} + +/* Actions */ +const userActions = { + fetchUserRequest: (username, ver = Date.now()) => load({ + type: FETCH_USER_REQUEST, + payload: { + username, + ver, + }, + }), + fetchUserSuccess: (username, data) => ({ + type: FETCH_USER_SUCCESS, + payload: { + username, + data, + }, + }), + fetchUserFailure: (username, error) => ({ + type: FETCH_USER_FAILURE, + payload: { + username, + error, + }, + error: true, + }), +}; + +/* Dataloader */ +const userLoader = createLoader(FETCH_USER_REQUEST, { + success: ({ action }, result) => { + const username = action.payload.username; + return userActions.fetchUserSuccess(username, result); + }, + error: ({ action }, error) => { + const username = action.payload.username; + return userActions.fetchUserFailure(username, error); + }, + fetch: ({ action, api }) => { + const { username } = action.payload; + return api.read(username); + }, + shouldFetch: ({ action, getState }) => { + const username = action.payload.username; + return !getState()[username]; + }, +}, { + ttl: 10000, + retryTimes: 3, + retryWait: fixedWait(500), +}); + + +/* Reducer */ +const initState = {}; + +function reducer(state = initState, action) { + switch (action.type) { + case FETCH_USER_SUCCESS: { + const { username, data } = action.payload; + return Object.assign({}, state, { + [username]: data, + }); + } + case FETCH_USER_FAILURE: { + const { username } = action.payload; + return Object.assign({}, state, { + [username]: { + error: true, + }, + }); + } + default: + return state; + } +} + +describe('createDataLoaderMiddleware', () => { + it('dispatch a request action with exist username, data should be stored successfully', (done) => { + const dataLoaderMiddleware = createDataLoaderMiddleware([userLoader], { + api: { + read: findUserByUsername, + }, + }); + const store = createStore( + reducer, + applyMiddleware(dataLoaderMiddleware), + ); + store + .dispatch(userActions.fetchUserRequest('tom')) + .then(() => { + expect(store.getState()).to.be.deep.equal({ + tom: { + age: 21, + givenName: 'Tom', + familyName: 'TomFamilyName', + }, + }); + done(); + }).catch(done); + }); + + it('dispatch two request actions with exist username at the same time, data should be stored successfully', (done) => { + const dataLoaderMiddleware = createDataLoaderMiddleware([userLoader], { + api: { + read: findUserByUsername, + }, + }); + const store = createStore( + reducer, + applyMiddleware(dataLoaderMiddleware), + ); + Promise.all([ + store.dispatch(userActions.fetchUserRequest('tom')), + store.dispatch(userActions.fetchUserRequest('bob')), + ]) + .then(() => { + expect(store.getState()).to.be.deep.equal({ + bob: { + age: 30, + givenName: 'Bob', + familyName: 'BobFamilyName', + }, + tom: { + age: 21, + givenName: 'Tom', + familyName: 'TomFamilyName', + }, + }); + done(); + }).catch(done); + }); + + it('dispatch a request action and cause an error, data error action should be dispatched', (done) => { + const dataLoaderMiddleware = createDataLoaderMiddleware([userLoader], { + api: { + read: findUserByUsername, + }, + }); + const store = createStore( + reducer, + applyMiddleware(dataLoaderMiddleware), + ); + store + .dispatch(userActions.fetchUserRequest('lucy')) + .then(() => { + expect(store.getState().lucy.error).to.be.equal(true); + done(); + }).catch(done); + }); + + it('use shouldFetch to prevent fetch()', (done) => { + let count = 0; + const dataLoaderMiddleware = createDataLoaderMiddleware([userLoader], { + api: { + read: (username) => { + count += 1; + return findUserByUsername(username); + }, + }, + }); + const store = createStore( + reducer, + applyMiddleware(dataLoaderMiddleware), + ); + store + .dispatch(userActions.fetchUserRequest('tom')) + .then((result) => { + expect(result).to.be.deep.equal({ + type: 'myapp/user/FETCH_USER/SUCCESS', + payload: { + username: 'tom', + data: { + age: 21, + givenName: 'Tom', + familyName: 'TomFamilyName', + }, + }, + }); + return store.dispatch(userActions.fetchUserRequest('tom')); + }) + .then((result) => { + expect(result).to.be.equal(null); + return store.dispatch(userActions.fetchUserRequest('tom')); + }) + .then(() => { + expect(count).to.be.equal(1); + expect(store.getState()).to.be.deep.equal({ + tom: { + age: 21, + givenName: 'Tom', + familyName: 'TomFamilyName', + }, + }); + done(); + }) + .catch(done); + }); + + it('prevent duplicated call', () => { + }); +}); diff --git a/test/createLoader.test.js b/test/createLoader.test.js new file mode 100644 index 0000000..5d9f41d --- /dev/null +++ b/test/createLoader.test.js @@ -0,0 +1,68 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { createLoader } from '../src'; + +describe('createLoader', () => { + const loader = { + success: () => null, + error: () => null, + fetch: () => null, + }; + const requestAction = { + type: 'USER_REQUEST', + payload: { + userId: 25, + }, + }; + it('create a loader that matches string', () => { + const descriptor = createLoader('USER_REQUEST', loader); + expect(descriptor.supports(requestAction)).to.be.equal(true); + expect(descriptor.supports('USER_REQUEST')).to.not.be.equal(true); + expect(descriptor.supports({ + type: 'USER_SUCCESS', + })).to.not.be.equal(true); + }); + + it('create a data loader that matches action', () => { + const descriptor = createLoader(requestAction, loader); + expect(descriptor.supports(requestAction)).to.be.equal(true); + expect(descriptor.supports('USER_REQUEST')).to.not.be.equal(true); + expect(descriptor.supports({ + type: 'USER_SUCCESS', + })).to.not.be.equal(true); + expect(descriptor.supports({ + type: 'USER_REQUEST', + payload: { + userId: 25, + }, + })).to.be.equal(true); + }); + + it('create a data loader that uses function to match', () => { + const descriptor = createLoader(action => + (action.payload && action.type && action.type === 'USER_REQUEST'), loader); + expect(descriptor.supports(requestAction)).to.be.equal(true); + expect(descriptor.supports('USER_REQUEST')).to.not.be.equal(true); + expect(descriptor.supports({ + type: 'USER_SUCCESS', + })).to.not.be.equal(true); + expect(descriptor.supports({ + type: 'USER_REQUEST', + })).to.not.be.equal(true); + expect(descriptor.supports({ + type: 'USER_REQUEST', + payload: { + }, + })).to.be.equal(true); + }); + + it('Descriptor should has default options', () => { + const descriptor = createLoader(requestAction, loader); + expect(descriptor.options.ttl).to.equal(10000); + expect(descriptor.options.retryTimes).to.equal(1); + expect(descriptor.options.retryWait(1)).to.equal(0); + }); +}); +/* eslint-enable no-unused-expressions */ diff --git a/test/data-loader.test.js b/test/data-loader.test.js deleted file mode 100644 index 7adc92a..0000000 --- a/test/data-loader.test.js +++ /dev/null @@ -1,202 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; -import sinon from 'sinon'; - -import { createLoader } from '../src'; - -describe('test createLoader: action matcher', () => { - const loader = { - success: () => null, - error: () => null, - fetch: () => null, - }; - const requestAction = { - type: 'USER_REQUEST', - payload: { - userId: 25, - }, - }; - it('create a loader that matches string', () => { - const descriptor = createLoader('USER_REQUEST', loader); - expect(descriptor.supports(requestAction)).to.be.equal(true); - expect(descriptor.supports('USER_REQUEST')).to.not.be.equal(true); - expect(descriptor.supports({ - type: 'USER_SUCCESS', - })).to.not.be.equal(true); - }); - - it('create a data loader that matches action', () => { - const descriptor = createLoader(requestAction, loader); - expect(descriptor.supports(requestAction)).to.be.equal(true); - expect(descriptor.supports('USER_REQUEST')).to.not.be.equal(true); - expect(descriptor.supports({ - type: 'USER_SUCCESS', - })).to.not.be.equal(true); - expect(descriptor.supports({ - type: 'USER_REQUEST', - payload: { - userId: 25, - }, - })).to.be.equal(true); - }); - - it('create a data loader that uses function to match', () => { - const descriptor = createLoader(action => - (action.payload && action.type && action.type === 'USER_REQUEST'), loader); - expect(descriptor.supports(requestAction)).to.be.equal(true); - expect(descriptor.supports('USER_REQUEST')).to.not.be.equal(true); - expect(descriptor.supports({ - type: 'USER_SUCCESS', - })).to.not.be.equal(true); - expect(descriptor.supports({ - type: 'USER_REQUEST', - })).to.not.be.equal(true); - expect(descriptor.supports({ - type: 'USER_REQUEST', - payload: { - }, - })).to.be.equal(true); - }); - - it('Descriptor should has default options', () => { - const descriptor = createLoader(requestAction, loader); - expect(descriptor.options.ttl).to.equal(10000); - expect(descriptor.options.retryTimes).to.equal(1); - expect(descriptor.options.retryWait.next().value).to.equal(0); - }); -}); - -describe('test createLoader: DataLoderTask', () => { - const loader = { - success: () => null, - error: () => null, - fetch: () => null, - }; - const descriptor = createLoader(loader); - - it('newTask() should return a task', () => { - expect(descriptor.newTask({}, { - type: 'USER_REQUEST', - })).to.be.ok; - }); - - it('newTask() should throw an error when action is invalid', () => { - expect(() => descriptor.newTask({}, {})).to.throw(Error); - }); - - it('loading -> shouldFetch -> fetch -> success', (done) => { - const loaderObj = { - fetch: () => 20, - success: () => ({ type: 'USER_SUCCESS' }), - error: () => ({ type: 'USER_FAILURE' }), - loading: () => {}, - shouldFetch: () => true, - }; - - const loadingSpy = sinon.spy(loaderObj, 'loading'); - const shouldFetchSpy = sinon.spy(loaderObj, 'shouldFetch'); - const fetchSpy = sinon.spy(loaderObj, 'fetch'); - const successSpy = sinon.spy(loaderObj, 'success'); - const errorSpy = sinon.spy(loaderObj, 'error'); - - const dispatchSpy = sinon.spy(); - - const newLoader = createLoader('USER_REQUEST', loaderObj); - const promise = newLoader.newTask({ - dispatch: dispatchSpy, - }, { - type: 'USER_REQUEST', - payload: { - userId: 25, - }, - }).execute(); - - promise.then(() => { - expect(loadingSpy.calledOnce).to.be.true; - expect(shouldFetchSpy.calledOnce).to.be.true; - expect(shouldFetchSpy.calledOnce).to.be.true; - expect(fetchSpy.calledOnce).to.be.true; - expect(successSpy.calledOnce).to.be.true; - expect(errorSpy.notCalled).to.be.true; - sinon.assert.callOrder(shouldFetchSpy, fetchSpy, successSpy); - done(); - }, done); - }); - - it('loading -> shouldFetch(return false) -> noop', (done) => { - const loaderObj = { - fetch: () => 20, - success: () => ({ type: 'USER_SUCCESS' }), - error: () => ({ type: 'USER_FAILURE' }), - loading: () => {}, - shouldFetch: () => false, - }; - - const loadingSpy = sinon.spy(loaderObj, 'loading'); - const shouldFetchSpy = sinon.spy(loaderObj, 'shouldFetch'); - const fetchSpy = sinon.spy(loaderObj, 'fetch'); - const successSpy = sinon.spy(loaderObj, 'success'); - const errorSpy = sinon.spy(loaderObj, 'error'); - - const dispatchSpy = sinon.spy(); - - const newLoader = createLoader('USER_REQUEST', loaderObj); - const promise = newLoader.newTask({ - dispatch: dispatchSpy, - }, { - type: 'USER_REQUEST', - payload: { - userId: 25, - }, - }).execute(); - - promise.then(() => { - expect(shouldFetchSpy.calledOnce).to.be.true; - expect(loadingSpy.notCalled).to.be.true; - expect(fetchSpy.notCalled).to.be.true; - expect(successSpy.notCalled).to.be.true; - expect(errorSpy.notCalled).to.be.true; - done(); - }, done); - }); - - it('loading -> shouldFetch -> fetch -> error', (done) => { - const loaderObj = { - fetch: () => Promise.reject('NotFoundError'), - success: () => {}, - error: (context, err) => ({ type: 'USER_FAILURE', error: err }), - loading: () => {}, - shouldFetch: () => true, - }; - - const loadingSpy = sinon.spy(loaderObj, 'loading'); - const shouldFetchSpy = sinon.spy(loaderObj, 'shouldFetch'); - const fetchSpy = sinon.spy(loaderObj, 'fetch'); - const successSpy = sinon.spy(loaderObj, 'success'); - const errorSpy = sinon.spy(loaderObj, 'error'); - - const dispatchSpy = sinon.spy(); - - const newLoader = createLoader('USER_REQUEST', loaderObj); - const promise = newLoader.newTask({ - dispatch: dispatchSpy, - }, { - type: 'USER_REQUEST', - payload: { - userId: 25, - }, - }).execute(); - - promise.then(() => { - expect(loadingSpy.calledOnce).to.be.true; - expect(shouldFetchSpy.calledOnce).to.be.true; - expect(fetchSpy.calledOnce).to.be.true; - expect(successSpy.notCalled).to.be.true; - expect(errorSpy.calledOnce).to.be.true; - sinon.assert.callOrder(shouldFetchSpy, fetchSpy, errorSpy); - done(); - }, done); - }); -}); -/* eslint-enable no-unused-expressions */ diff --git a/test/load.test.js b/test/load.test.js index 6a9aad8..4a29497 100644 --- a/test/load.test.js +++ b/test/load.test.js @@ -11,7 +11,7 @@ describe('load()', () => { }, }; - it('wrap an action, should return promise', (done) => { + it('load() should return a promise, which will be resolved to the given action', (done) => { const promise = load(requestAction); const expected = { type: LOAD_DATA_REQUEST_ACTION, @@ -22,7 +22,7 @@ describe('load()', () => { promise.then((result) => { expect(result).to.be.deep.equal(expected); done(); - }, done); + }).catch(done); }); it('pass a non-object to load(), should throw an Error', () => { diff --git a/test/middleware.test.js b/test/middleware.test.js deleted file mode 100644 index 2768a4a..0000000 --- a/test/middleware.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { createLoader, createDataLoaderMiddleware } from '../src'; - -describe('createDataLoaderMiddleware()', () => { - const loaderObj = { - success: () => {}, - error: () => {}, - fetch: () => {}, - }; - - const loader = createLoader(loaderObj); - - const extraSymbol = 'Extra'; - const nextHandler = createDataLoaderMiddleware([loader], extraSymbol); - - it('createDataLoaderMiddleware() should return a function to handle next', () => { - expect(nextHandler).to.be.a('function'); - expect(nextHandler.length).to.be.equal(1); - }); -}); diff --git a/test/utils.test.js b/test/utils.test.js index 629cc7f..904f6f3 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -31,4 +31,3 @@ describe('utils', () => { }); }); }); -/* eslint-enable no-unused-expressions */ diff --git a/test/wait-strategies.test.js b/test/wait-strategies.test.js deleted file mode 100644 index 7deeb3d..0000000 --- a/test/wait-strategies.test.js +++ /dev/null @@ -1,119 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; -import { fixedWait, incrementingWait, fibonacciWait, randomWait, exponentialWait } from '../src'; - -describe('wait-strategies', () => { - describe('fixedWait', () => { - it('fixedWait(1000), should always return 1000', () => { - const wait = fixedWait(1000); - expect(wait.next().value).to.be.equal(1000); - expect(wait.next().value).to.be.equal(1000); - expect(wait.next().value).to.be.equal(1000); - }); - - it('fixedWait(0), should always return 0', () => { - const wait = fixedWait(0); - expect(wait.next().value).to.be.equal(0); - expect(wait.next().value).to.be.equal(0); - expect(wait.next().value).to.be.equal(0); - }); - }); - - describe('incrementingWait', () => { - it('incrementingWait(1000, 2000), should return 1000, 3000, 5000, 7000, 9000', () => { - const wait = incrementingWait(1000, 2000); - expect(wait.next().value).to.be.equal(1000); - expect(wait.next().value).to.be.equal(3000); - expect(wait.next().value).to.be.equal(5000); - expect(wait.next().value).to.be.equal(7000); - expect(wait.next().value).to.be.equal(9000); - }); - - it('incrementingWait(1000, 2000, 8000), should return 1000, 3000, 5000, 7000, 7000', () => { - const wait = incrementingWait(1000, 2000, 8000); - expect(wait.next().value).to.be.equal(1000); - expect(wait.next().value).to.be.equal(3000); - expect(wait.next().value).to.be.equal(5000); - expect(wait.next().value).to.be.equal(7000); - expect(wait.next().value).to.be.equal(7000); - }); - }); - - describe('fibonacciWait', () => { - it('fibonacciWait(), should return 1, 1, 2, 3, 5, 8, 13', () => { - const wait = fibonacciWait(); - expect(wait.next().value).to.be.equal(1); - expect(wait.next().value).to.be.equal(1); - expect(wait.next().value).to.be.equal(2); - expect(wait.next().value).to.be.equal(3); - expect(wait.next().value).to.be.equal(5); - expect(wait.next().value).to.be.equal(8); - expect(wait.next().value).to.be.equal(13); - }); - - it('fibonacciWait(100), should return 100, 100, 200, 300, 500, 800, 1300', () => { - const wait = fibonacciWait(100); - expect(wait.next().value).to.be.equal(100); - expect(wait.next().value).to.be.equal(100); - expect(wait.next().value).to.be.equal(200); - expect(wait.next().value).to.be.equal(300); - expect(wait.next().value).to.be.equal(500); - expect(wait.next().value).to.be.equal(800); - expect(wait.next().value).to.be.equal(1300); - }); - - it('fibonacciWait(100, 800), should return 100, 100, 200, 300, 500, 800, 800', () => { - const wait = fibonacciWait(100, 800); - expect(wait.next().value).to.be.equal(100); - expect(wait.next().value).to.be.equal(100); - expect(wait.next().value).to.be.equal(200); - expect(wait.next().value).to.be.equal(300); - expect(wait.next().value).to.be.equal(500); - expect(wait.next().value).to.be.equal(800); - expect(wait.next().value).to.be.equal(800); - }); - }); - - describe('randomWait', () => { - it('randomWait(35, 45), should return a number between 35 and 45', () => { - const wait = randomWait(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - expect(wait.next().value).to.be.within(35, 45); - }); - }); - - describe('exponentialWait', () => { - it('exponentialWait(), should return 2, 4, 8, 16, 32, 64', () => { - const wait = exponentialWait(); - expect(wait.next().value).to.be.equal(2); - expect(wait.next().value).to.be.equal(4); - expect(wait.next().value).to.be.equal(8); - expect(wait.next().value).to.be.equal(16); - expect(wait.next().value).to.be.equal(32); - expect(wait.next().value).to.be.equal(64); - }); - - it('exponentialWait(100, 3200), should return 200, 400, 800, 1600, 3200, 3200', () => { - const wait = exponentialWait(100, 3200); - expect(wait.next().value).to.be.equal(200); - expect(wait.next().value).to.be.equal(400); - expect(wait.next().value).to.be.equal(800); - expect(wait.next().value).to.be.equal(1600); - expect(wait.next().value).to.be.equal(3200); - expect(wait.next().value).to.be.equal(3200); - }); - }); -}); diff --git a/test/waitStrategies.test.js b/test/waitStrategies.test.js new file mode 100644 index 0000000..277504e --- /dev/null +++ b/test/waitStrategies.test.js @@ -0,0 +1,119 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { fixedWait, incrementingWait, fibonacciWait, randomWait, exponentialWait } from '../src'; + +describe('waitStrategies', () => { + describe('fixedWait', () => { + it('fixedWait(1000), should always return 1000', () => { + const wait = fixedWait(1000); + expect(wait(1)).to.be.equal(1000); + expect(wait(2)).to.be.equal(1000); + expect(wait(3)).to.be.equal(1000); + }); + + it('fixedWait(0), should always return 0', () => { + const wait = fixedWait(0); + expect(wait(1)).to.be.equal(0); + expect(wait(2)).to.be.equal(0); + expect(wait(3)).to.be.equal(0); + }); + }); + + describe('incrementingWait', () => { + it('incrementingWait(1000, 2000), should return 1000, 3000, 5000, 7000, 9000', () => { + const wait = incrementingWait(1000, 2000); + expect(wait(1)).to.be.equal(1000); + expect(wait(2)).to.be.equal(3000); + expect(wait(3)).to.be.equal(5000); + expect(wait(4)).to.be.equal(7000); + expect(wait(5)).to.be.equal(9000); + }); + + it('incrementingWait(1000, 2000, 8000), should return 1000, 3000, 5000, 7000, 8000', () => { + const wait = incrementingWait(1000, 2000, 8000); + expect(wait(1)).to.be.equal(1000); + expect(wait(2)).to.be.equal(3000); + expect(wait(3)).to.be.equal(5000); + expect(wait(4)).to.be.equal(7000); + expect(wait(5)).to.be.equal(8000); + }); + }); + + describe('fibonacciWait', () => { + it('fibonacciWait(), should return 1, 1, 2, 3, 5, 8, 13', () => { + const wait = fibonacciWait(); + expect(wait(1)).to.be.equal(1); + expect(wait(2)).to.be.equal(1); + expect(wait(3)).to.be.equal(2); + expect(wait(4)).to.be.equal(3); + expect(wait(5)).to.be.equal(5); + expect(wait(6)).to.be.equal(8); + expect(wait(7)).to.be.equal(13); + }); + + it('fibonacciWait(100), should return 100, 100, 200, 300, 500, 800, 1300', () => { + const wait = fibonacciWait(100); + expect(wait(1)).to.be.equal(100); + expect(wait(2)).to.be.equal(100); + expect(wait(3)).to.be.equal(200); + expect(wait(4)).to.be.equal(300); + expect(wait(5)).to.be.equal(500); + expect(wait(6)).to.be.equal(800); + expect(wait(7)).to.be.equal(1300); + }); + + it('fibonacciWait(100, 800), should return 100, 100, 200, 300, 500, 800, 800', () => { + const wait = fibonacciWait(100, 800); + expect(wait(1)).to.be.equal(100); + expect(wait(2)).to.be.equal(100); + expect(wait(3)).to.be.equal(200); + expect(wait(4)).to.be.equal(300); + expect(wait(5)).to.be.equal(500); + expect(wait(6)).to.be.equal(800); + expect(wait(7)).to.be.equal(800); + }); + }); + + describe('randomWait', () => { + it('randomWait(35, 45), should return a number between 35 and 45', () => { + const wait = randomWait(35, 45); + expect(wait(1)).to.be.within(35, 45); + expect(wait(2)).to.be.within(35, 45); + expect(wait(3)).to.be.within(35, 45); + expect(wait(4)).to.be.within(35, 45); + expect(wait(5)).to.be.within(35, 45); + expect(wait(6)).to.be.within(35, 45); + expect(wait(7)).to.be.within(35, 45); + expect(wait(8)).to.be.within(35, 45); + expect(wait(9)).to.be.within(35, 45); + expect(wait(10)).to.be.within(35, 45); + expect(wait(11)).to.be.within(35, 45); + expect(wait(12)).to.be.within(35, 45); + expect(wait(13)).to.be.within(35, 45); + expect(wait(14)).to.be.within(35, 45); + expect(wait(15)).to.be.within(35, 45); + }); + }); + + describe('exponentialWait', () => { + it('exponentialWait(), should return 2, 4, 8, 16, 32, 64', () => { + const wait = exponentialWait(); + expect(wait(1)).to.be.equal(2); + expect(wait(2)).to.be.equal(4); + expect(wait(3)).to.be.equal(8); + expect(wait(4)).to.be.equal(16); + expect(wait(5)).to.be.equal(32); + expect(wait(6)).to.be.equal(64); + }); + + it('exponentialWait(100, 3200), should return 200, 400, 800, 1600, 3200, 3200', () => { + const wait = exponentialWait(100, 3200); + expect(wait(1)).to.be.equal(200); + expect(wait(2)).to.be.equal(400); + expect(wait(3)).to.be.equal(800); + expect(wait(4)).to.be.equal(1600); + expect(wait(5)).to.be.equal(3200); + expect(wait(6)).to.be.equal(3200); + }); + }); +}); diff --git a/webpack.config.js b/webpack.config.js index 292de11..d672e44 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,28 +1,28 @@ -var webpack = require('webpack') +const webpack = require('webpack'); module.exports = { entry: './src/index', module: { loaders: [ - { test: /\.js$/, loader: 'babel', exclude: /node_modules/ } - ] + { test: /\.js$/, loader: 'babel', exclude: /node_modules/ }, + ], }, output: { filename: 'dist/redux-dataloader.min.js', libraryTarget: 'umd', - library: 'redux-dataloader' + library: 'redux-dataloader', }, plugins: [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin({ 'process.env': { - 'NODE_ENV': JSON.stringify('production') - } + NODE_ENV: JSON.stringify('production'), + }, }), new webpack.optimize.UglifyJsPlugin({ compressor: { - warnings: false - } - }) - ] -} + warnings: false, + }, + }), + ], +}; From 86122c1873072d0e0668d635c16b9f774f2d44ab Mon Sep 17 00:00:00 2001 From: kou_hin Date: Wed, 18 Jan 2017 13:40:42 +0900 Subject: [PATCH 2/4] revert cache to runningTasks --- src/createDataLoaderMiddleware.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/createDataLoaderMiddleware.js b/src/createDataLoaderMiddleware.js index f6a3dd6..2b70386 100644 --- a/src/createDataLoaderMiddleware.js +++ b/src/createDataLoaderMiddleware.js @@ -26,7 +26,7 @@ export default function createDataLoaderMiddleware( }; const middleware = ({ dispatch, getState }) => { - middleware.cache = {}; + middleware.runningTasks = {}; const ctx = assign({}, withArgs, { dispatch, getState, @@ -42,9 +42,9 @@ export default function createDataLoaderMiddleware( next(asyncAction); const { action } = asyncAction.meta; - const taskKey = findTaskKey(middleware.cache, action); + const taskKey = findTaskKey(middleware.runningTasks, action); if (taskKey) { - return middleware.cache[taskKey].promise; + return middleware.runningTasks[taskKey].promise; } const taskDescriptor = find(flattenedLoaders, loader => loader.supports(action)); @@ -73,10 +73,10 @@ export default function createDataLoaderMiddleware( if (isInteger(options.ttl) && options.ttl > 0) { const key = uniqueId(`${action.type}__`); - middleware.cache[key] = { action, promise: runningTask }; + middleware.runningTasks[key] = { action, promise: runningTask }; if (typeof window !== 'undefined' && typeof document !== 'undefined') { setTimeout(() => { - delete middleware.cache[key]; + delete middleware.runningTasks[key]; }, options.ttl); } } From f9fdf1cad0e8d4d8ec6542542583a2730b552a8e Mon Sep 17 00:00:00 2001 From: kou_hin Date: Wed, 25 Jan 2017 17:45:14 +0900 Subject: [PATCH 3/4] fix build --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 399bc88..429536b 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ }, "homepage": "https://github.com/kouhin/redux-dataloader", "scripts": { + "prepare": "npm run clean && npm run lint && npm test && npm run build", "clean": "rimraf lib coverage", "build": "babel src --out-dir lib", - "prepublish": "npm run clean && npm run lint && npm test && npm run build", "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" From 6037ddb6fd177cf3f955b65165f645a9ac07cbc8 Mon Sep 17 00:00:00 2001 From: kou_hin Date: Wed, 15 Feb 2017 19:42:40 +0900 Subject: [PATCH 4/4] v1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 429536b..e516ef8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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",