diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9c44b4b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: node_js - -node_js: - - "5" - - "6" - -script: - - npm test - - npm run build diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..bced51f --- /dev/null +++ b/circle.yml @@ -0,0 +1,19 @@ +machine: + node: + version: 7 +dependencies: + override: + - sudo apt-get install jq + - npm install + - npm run build +test: + override: + - npm test +deployment: + production: + branch: master + commands: + - git tag v`jq -r '.version' package.json` + - git push origin --tags + - echo -e "$NPM_USERNAME\n$NPM_PASSWORD\n$NPM_EMAIL" | npm login + - npm publish diff --git a/package.json b/package.json index 7ba835c..d5c31e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-dataloader", - "version": "1.0.0-beta.6", + "version": "1.0.0-rc.1", "description": "Loads async data for Redux apps focusing on preventing duplicated requests and dealing with async dependencies.", "main": "lib/index.js", "repository": { @@ -41,33 +41,26 @@ "author": "Bin Hou", "license": "MIT", "devDependencies": { - "babel-cli": "^6.10.1", - "babel-core": "^6.10.4", - "babel-eslint": "^6.1.2", - "babel-loader": "^6.2.4", - "babel-plugin-transform-runtime": "^6.9.0", - "babel-preset-es2015": "^6.9.0", - "babel-preset-react": "^6.11.1", - "babel-preset-stage-0": "^6.5.0", + "babel-cli": "^6.18.0", + "babel-core": "^6.21.0", + "babel-eslint": "^7.1.1", + "babel-loader": "^6.2.10", + "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", - "chai-as-promised": "^5.3.0", - "eslint": "^3.0.1", - "eslint-config-airbnb": "^9.0.1", - "eslint-plugin-import": "^1.10.2", - "eslint-plugin-jsx-a11y": "^1.5.5", - "eslint-plugin-react": "^5.2.2", + "eslint-config-airbnb-deps": "^13.0.0", "isparta": "^4.0.0", - "istanbul": "^0.4.4", - "mocha": "^2.5.3", - "rimraf": "^2.5.3", - "sinon": "^1.17.4", - "sinon-chai": "^2.8.0", - "webpack": "^1.13.1" + "istanbul": "^0.4.5", + "mocha": "^3.2.0", + "rimraf": "^2.5.4", + "sinon": "^1.17.6", + "webpack": "^1.14.0" }, "dependencies": { - "babel-polyfill": "^6.9.1", - "debug": "^2.2.0", - "lodash": "^4.13.1" + "babel-runtime": "^6.20.0", + "lodash": "^4.17.2" }, "eslintConfig": { "parser": "babel-eslint", @@ -80,7 +73,15 @@ "plugins": [ "react", "import" - ] + ], + "rules": { + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": true + } + ] + } }, "babel": { "presets": [ diff --git a/src/data-loader.js b/src/data-loader.js index 9dbc65d..c1ab059 100644 --- a/src/data-loader.js +++ b/src/data-loader.js @@ -1,12 +1,9 @@ import isEqual from 'lodash/isEqual'; -import Debug from 'debug'; import { loadFailure, loadSuccess } from './action'; import { isAction } from './utils'; import { fixedWait } from './wait-strategies'; -const debug = new Debug('redux-dataloader:data-loader'); - const DEFAULT_OPTIONS = { ttl: 10000, // Default TTL: 10s retryTimes: 1, @@ -56,28 +53,15 @@ class DataLoaderTask { const disableInternalAction = !!options.disableInternalAction; - if (debug.enabled) { - debug('Excute with options', opts); - } - if (!this.params.shouldFetch(this.context)) { - if (debug.enabled) { - debug('shouldFetch() returns false'); - } if (!disableInternalAction) { const successAction = loadSuccess(this.context.action); this.context.dispatch(successAction); // load nothing - if (debug.enabled) { - debug('A success action is dispatched for shouldFetch() = false', successAction); - } } return null; } const loadingAction = this.params.loading(this.context); this.context.dispatch(loadingAction); - if (debug.enabled) { - debug('A loading action is dispatched', loadingAction); - } let currentRetry = 0; let result; @@ -85,29 +69,20 @@ class DataLoaderTask { for (;;) { try { - if (debug.enabled) { - debug('Start fetching, try = ', (currentRetry + 1)); - } result = await this.params.fetch(this.context); - if (debug.enabled) { - debug('Fetching success, result = ', result); - } break; } catch (ex) { - debug('Fetching failed, ex = ', ex); - currentRetry++; + currentRetry += 1; if (options.retryTimes && currentRetry < opts.retryTimes) { const sleepTime = opts.retryWait.next().value; - if (debug.enabled) { - debug(`Sleeping for ${sleepTime} ms..., and retry`); - } await sleep(sleepTime); - continue; + } else { + error = ex; + break; } - error = ex; - break; } } + if (!error) { const successAction = this.params.success(this.context, result); @@ -115,7 +90,7 @@ class DataLoaderTask { 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) + new Error('Result action type equals origial action type', this.context.action), ); this.context.dispatch(errorAction); if (!disableInternalAction) { @@ -124,7 +99,6 @@ class DataLoaderTask { return errorAction; } - debug('Dispatch a success action', successAction); this.context.dispatch(successAction); if (!disableInternalAction) { this.context.dispatch(loadSuccess(this.context.action, result)); @@ -142,7 +116,6 @@ class DataLoaderTask { } return errorAction; } - debug('Dispatch an error action', errorAction); this.context.dispatch(errorAction); if (!disableInternalAction) { this.context.dispatch(loadFailure(this.context.action, error)); @@ -192,9 +165,6 @@ class DataLoaderTaskDescriptor { */ function createLoader(pattern, params, options) { const dataLoaderDescriptor = new DataLoaderTaskDescriptor(pattern, params, options); - if (debug.enabled) { - debug('Create a new data loader descriptor', dataLoaderDescriptor); - } return dataLoaderDescriptor; } diff --git a/src/load.js b/src/load.js index d3bc8cd..e530feb 100644 --- a/src/load.js +++ b/src/load.js @@ -1,16 +1,14 @@ -import Debug from 'debug'; import { loadRequest } from './action'; import { isAction } from './utils'; -const debug = new Debug('redux-dataloader:load'); +export const DATALOADER_ACTION_ID = () => {}; export default function load(action) { if (!isAction(action)) { throw new Error('action must be object', action); } - if (debug.enabled) { - debug('load() an action = ', action); - debug('A Promise with a wrapped action is returned', loadRequest(action)); - } - return Promise.resolve(loadRequest(action)); + const asyncAction = Promise.resolve(loadRequest(action)); + // eslint-disable-next-line no-underscore-dangle + asyncAction._id = DATALOADER_ACTION_ID; + return asyncAction; } diff --git a/src/middleware.js b/src/middleware.js index 34c105b..b7632c4 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -2,22 +2,19 @@ import findKey from 'lodash/findKey'; import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; import isInteger from 'lodash/isInteger'; -import Debug from 'debug'; -import { isPromise } from './utils'; -import { LOAD_DATA_REQUEST_ACTION } from './action'; - -const debug = new Debug('redux-dataloader:middleware'); +import { DATALOADER_ACTION_ID } from './load'; function findRunningTaskKey(runningTasksMap, action) { - return findKey(runningTasksMap, (o) => isEqual(o.action, action)); + return findKey(runningTasksMap, o => isEqual(o.action, action)); } export default function createDataLoaderMiddleware(loaders, args, opts) { const runningTasks = {}; let currentId = 1; - const uniqueId = (prefix) => `${prefix}${currentId++}`; + currentId += 1; + const uniqueId = prefix => `${prefix}${currentId}`; const middleware = ({ dispatch, getState }) => { const ctx = { @@ -26,30 +23,21 @@ export default function createDataLoaderMiddleware(loaders, args, opts) { getState, }; - return (next) => (receivedAction) => { - if (!isPromise(receivedAction)) { + return next => (receivedAction) => { + // eslint-disable-next-line no-underscore-dangle + if (!receivedAction._id || receivedAction._id !== DATALOADER_ACTION_ID) { return next(receivedAction); } - debug('Received a promise action', receivedAction); return receivedAction.then((asyncAction) => { - if (asyncAction.type !== LOAD_DATA_REQUEST_ACTION) { - debug(`Received promise action is not ${LOAD_DATA_REQUEST_ACTION}, pass it to the next middleware`); // eslint-disable-line max-len - return next(receivedAction); - } - debug(`Received promise action is ${LOAD_DATA_REQUEST_ACTION}, pass wrapped action to the next middleware`); // eslint-disable-line max-len next(asyncAction); // dispatch data loader request action const { action } = asyncAction.meta; - debug('Original action is', action); const runningTaskKey = findRunningTaskKey(runningTasks, action); - debug('Find task from task cache'); if (runningTaskKey) { - debug('Cache hit!, Key = ', runningTaskKey); return runningTasks[runningTaskKey].promise; } - const taskDescriptor = find(loaders, (loader) => loader.supports(action)); - debug('Cache does not hit, finding task descriptor', taskDescriptor); + const taskDescriptor = find(loaders, loader => loader.supports(action)); if (!taskDescriptor) { throw new Error('No loader for action', action); @@ -60,15 +48,8 @@ export default function createDataLoaderMiddleware(loaders, args, opts) { ...taskDescriptor.options, ...(asyncAction.meta.options || {}), }; - debug( - 'Merge options from taskDescriptor and dispatched action', - taskDescriptor.options, - asyncAction.meta.options, options - ); const key = uniqueId(`${action.type}__`); - debug('Generate new cache key', key); - debug('Start executing'); const runningTask = taskDescriptor.newTask(ctx, action).execute(options); if (isInteger(options.ttl) && options.ttl > 0) { @@ -77,9 +58,7 @@ export default function createDataLoaderMiddleware(loaders, args, opts) { promise: runningTask, }; if (typeof window !== 'undefined' && typeof document !== 'undefined') { - debug(`Set cache ttl for task[${key}], ttl = ${options.ttl}`); setTimeout(() => { - debug(`Task[${key}] is removed from cache, for ttl = ${options.ttl} ms`); delete runningTasks[key]; }, options.ttl); } diff --git a/src/utils.js b/src/utils.js index 0d06e99..a580900 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,7 +1,3 @@ -export function isPromise(val) { - return val && typeof val.then === 'function'; -} - export function isAction(action) { const result = action && (typeof action) === 'object' && action.type && (typeof action.type) === 'string'; @@ -11,7 +7,7 @@ export function isAction(action) { export function formatError(err) { const error = (err instanceof Error) ? err : new Error(err); const result = {}; - Object.getOwnPropertyNames(error).forEach(key => { + Object.getOwnPropertyNames(error).forEach((key) => { result[key] = error[key]; }); return result; diff --git a/src/wait-strategies.js b/src/wait-strategies.js index 529dbc1..a09c23b 100644 --- a/src/wait-strategies.js +++ b/src/wait-strategies.js @@ -7,7 +7,7 @@ /** * Returns a wait strategy that sleeps a fixed amount of time before retrying (in millisecond). */ -export function * fixedWait(sleepTime) { +export function* fixedWait(sleepTime) { for (;;) { yield sleepTime; } @@ -17,16 +17,16 @@ export function * fixedWait(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) { +export function* exponentialWait(multiplier = 1, max = Number.MAX_VALUE) { let current = 2 * multiplier; for (;;) { const next = 2 * current; if (next > max) { yield current; - continue; + } else { + yield current; + current = next; } - yield current; - current = next; } } @@ -34,18 +34,18 @@ export function * exponentialWait(multiplier = 1, max = Number.MAX_VALUE) { * 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) { +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; - continue; + } else { + fn2 = fn1; + fn1 += current; + yield current; } - fn2 = fn1; - fn1 = fn1 + current; - yield current; } } @@ -53,7 +53,7 @@ export function * fibonacciWait(multiplier = 1, max = Number.MAX_VALUE) { * 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) { +export function* incrementingWait(initialSleepTime = 0, increment = 1000, max = Number.MAX_VALUE) { let current = initialSleepTime; yield current; for (;;) { @@ -68,8 +68,8 @@ export function * incrementingWait(initialSleepTime = 0, increment = 1000, max = /** * Returns a strategy that sleeps a random amount of time before retrying. */ -export function * randomWait(min, max) { +export function* randomWait(min, max) { for (;;) { - yield parseInt(min + (max - min) * Math.random(), 10); + yield parseInt(min + ((max - min) * Math.random()), 10); } } diff --git a/test/data-loader.test.js b/test/data-loader.test.js index 022068c..7adc92a 100644 --- a/test/data-loader.test.js +++ b/test/data-loader.test.js @@ -1,15 +1,10 @@ /* eslint-disable no-unused-expressions */ -import chai, { expect } from 'chai'; -import sinonChai from 'sinon-chai'; -import chaiAsPromised from 'chai-as-promised'; +import { expect } from 'chai'; import { describe, it } from 'mocha'; import sinon from 'sinon'; import { createLoader } from '../src'; -chai.use(sinonChai); -chai.use(chaiAsPromised); - describe('test createLoader: action matcher', () => { const loader = { success: () => null, @@ -47,7 +42,7 @@ describe('test createLoader: action matcher', () => { }); it('create a data loader that uses function to match', () => { - const descriptor = createLoader((action) => + 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); @@ -117,14 +112,16 @@ describe('test createLoader: DataLoderTask', () => { }, }).execute(); - return expect(promise.then(() => { - 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; + 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); - })).to.be.fulfilled.notify(done); + done(); + }, done); }); it('loading -> shouldFetch(return false) -> noop', (done) => { @@ -154,13 +151,14 @@ describe('test createLoader: DataLoderTask', () => { }, }).execute(); - return expect(promise.then(() => { - 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; - })).to.be.fulfilled.notify(done); + 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) => { @@ -190,14 +188,15 @@ describe('test createLoader: DataLoderTask', () => { }, }).execute(); - return expect(promise.then(() => { - 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; + 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); - })).to.be.fulfilled.notify(done); + done(); + }, done); }); }); /* eslint-enable no-unused-expressions */ diff --git a/test/load.test.js b/test/load.test.js index e062f7f..6a9aad8 100644 --- a/test/load.test.js +++ b/test/load.test.js @@ -11,7 +11,7 @@ describe('load()', () => { }, }; - it('wrap an action, should return promise', () => { + it('wrap an action, should return promise', (done) => { const promise = load(requestAction); const expected = { type: LOAD_DATA_REQUEST_ACTION, @@ -19,7 +19,10 @@ describe('load()', () => { action: requestAction, }, }; - return expect(promise).to.eventually.deep.equal(expected); + promise.then((result) => { + expect(result).to.be.deep.equal(expected); + done(); + }, done); }); it('pass a non-object to load(), should throw an Error', () => { diff --git a/test/middleware.test.js b/test/middleware.test.js index cfc67ec..2768a4a 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -1,13 +1,8 @@ -import chai, { expect } from 'chai'; -import sinonChai from 'sinon-chai'; -import chaiAsPromised from 'chai-as-promised'; +import { expect } from 'chai'; import { describe, it } from 'mocha'; import { createLoader, createDataLoaderMiddleware } from '../src'; -chai.use(sinonChai); -chai.use(chaiAsPromised); - describe('createDataLoaderMiddleware()', () => { const loaderObj = { success: () => {}, diff --git a/test/utils.test.js b/test/utils.test.js index 0d2fd17..629cc7f 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -2,25 +2,9 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { isPromise, isAction } from '../src/utils'; +import { isAction } from '../src/utils'; describe('utils', () => { - describe('isPromise()', () => { - it('Promise.resolve() is a Promise', () => { - expect(isPromise(Promise.resolve())).to.be.true; - }); - - it('new Promise() is a Promise', () => { - expect(isPromise(new Promise((resolve) => resolve(false)))).to.be.true; - }); - it('Function is not a Promise', () => { - expect(isPromise(() => true)).to.not.be.true; - }); - it('Object is not a Promise', () => { - expect(isPromise({})).to.not.be.true; - }); - }); - describe('isAction()', () => { it('Object is an action', () => { expect(isAction({