Skip to content

Commit

Permalink
Merge pull request #4 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 20, 2017
2 parents 33fef6a + 6eadddc commit a9b8dfd
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 43 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Memoize action creator for [redux](http://redux.js.org), and let you dispatch co

[![CircleCI](https://img.shields.io/circleci/project/github/kouhin/redux-memoize.svg)](https://circleci.com/gh/kouhin/redux-memoize/tree/develop)
[![npm](https://img.shields.io/npm/v/redux-memoize.svg)](https://www.npmjs.com/package/redux-memoize)
[![dependency status](https://david-dm.org/kouhin/react-router-hook.svg?style=flat-square)](https://david-dm.org/kouhin/react-router-hook)
[![dependency status](https://david-dm.org/kouhin/redux-memoize.svg?style=flat-square)](https://david-dm.org/kouhin/redux-memoize)
[![airbnb style](https://img.shields.io/badge/code_style-airbnb-blue.svg)](https://github.com/airbnb/javascript)

```js
Expand Down Expand Up @@ -136,7 +136,6 @@ Memoize actionCreator and returns a memoized actionCreator. When dispatch action
#### Arguments

- `opts` _Object_
- **Default**: `{ ttl: 0, enabled: true, isEqual: lodash.isEqual }`
- `ttl` _Number|Function_: The time to live for cached action creator. When `ttl` is a function, `getState` will be passed as argument, and it must returns a number.
- `enabled` _Boolean|Function_: Whether use memorized action creator or not. When `false`, cache will be ignored and the result of original action creator will be dispatched without caching. When `enabled` is a function, `getState` will be passed argument, and it must returns a boolean.
- `isEqual`: arguments of action creator will be used as the map cache key. It uses lodash.isEqual to find the existed cached action creator. You can customize this function.
Expand All @@ -153,6 +152,8 @@ Create a redux [middleware](http://redux.js.org/docs/advanced/Middleware.html).

- `opts` _Object_
- disableTTL _Boolean_: The default value is `true` on server and `false` on browser. By default, cached action creator will not be evicted by setTimeout with TTL on server in order to prevent memory leak. You can enable it for test purpose.
- globalOptions _Object_: Default opts for memorize().
- **Default**: `{ ttl: 0, enabled: true, isEqual: lodash.isEqual }`

#### Returns

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-memoize",
"version": "1.0.1",
"version": "1.1.0",
"description": "Memoize action creator for redux, and let you dispatch common/thunk/promise/async action whenever you want to, without worrying about duplication",
"main": "lib/index.js",
"directories": {
Expand Down
41 changes: 26 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import lodashIsEqual from 'lodash/isEqual';

const ACTION_TYPE = '@redux-memoize/action';

const DEFAULT_META = {
ttl: 0,
enabled: true,
isEqual: lodashIsEqual,
};

function isPromise(v) {
return v && typeof v.then === 'function';
}
Expand All @@ -20,17 +26,21 @@ function deepGet(map, args, isEqual) {
return null;
}

export default function createMemoizeMiddleware(options) {
export default function createMemoizeMiddleware(options = {}) {
const opts = {
// default disableTTL is true on server side, to prevent memory leak (use GC to remove cache)
disableTTL: !canUseDOM,
...(options || {}),
...options,
};
const cache = new Map();
const middleware = ({ dispatch, getState }) => next => (action) => {
if (typeof action === 'object' && action.type === ACTION_TYPE) {
const { fn, args } = action.payload;
const { ttl, enabled, isEqual } = action.meta;
const { ttl, enabled, isEqual } = {
...DEFAULT_META,
...(options.globalOptions || {}),
...(action.meta || {}),
};
let taskCache = cache.get(fn);
if (!taskCache) {
taskCache = new Map();
Expand Down Expand Up @@ -75,16 +85,17 @@ export const memoize = options => (fn) => {
if (typeof fn !== 'function') {
throw new Error('Not a function');
}
return (...args) => ({
type: ACTION_TYPE,
payload: {
fn,
args,
},
meta: {
ttl: (options && typeof options.ttl !== 'undefined') ? options.ttl : 0,
enabled: (options && typeof options.enabled !== 'undefined') ? options.enabled : true,
isEqual: (options && typeof options.isEqual !== 'undefined') ? options.isEqual : lodashIsEqual,
},
});
return (...args) => {
const action = {
type: ACTION_TYPE,
payload: {
fn,
args,
},
};
if (options) {
action.meta = options;
}
return action;
};
};
161 changes: 136 additions & 25 deletions test/middleware.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint import/no-extraneous-dependencies:0 */
import { createStore, compose, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import lodashIsEqual from 'lodash/isEqual';

import createMemoizeMiddleware, { memoize } from '../src/index';

Expand Down Expand Up @@ -34,25 +33,20 @@ describe('memoize', () => {
memoize()(() => {});
}).not.toThrow();
});
});

it('memoized action creator must return an action with default options', () => {
describe('options', () => {
it('when options is not specified, must return an action without meta', () => {
const args = [1, 2, '3'];
expect(memoize()(actionCreator)(...args)).toEqual({
type: '@redux-memoize/action',
payload: {
fn: actionCreator,
args,
},
meta: {
ttl: 0,
enabled: true,
isEqual: lodashIsEqual,
},
});
});
});

describe('options', () => {
it('memoized action creator returns an action with specified ttl', () => {
const args = [1, 2, '3'];
expect(memoize({
Expand All @@ -65,8 +59,6 @@ describe('memoize', () => {
},
meta: {
ttl: 100,
enabled: true,
isEqual: lodashIsEqual,
},
});

Expand All @@ -81,8 +73,6 @@ describe('memoize', () => {
},
meta: {
ttl,
enabled: true,
isEqual: lodashIsEqual,
},
});
});
Expand All @@ -99,8 +89,6 @@ describe('memoize', () => {
args,
},
meta: {
ttl: 0,
enabled: true,
isEqual,
},
});
Expand Down Expand Up @@ -145,11 +133,6 @@ describe('middleware', () => {
fn: () => ({ type: 'COMMON_ACTION' }),
args: [1, 2],
},
meta: {
ttl: 0,
enabled: true,
isEqual: lodashIsEqual,
},
};
const actionHandler = nextHandler((action) => {
expect(action).toBeUndefined();
Expand All @@ -173,11 +156,6 @@ describe('middleware', () => {
fn: () => originalAction,
args: [1, 2],
},
meta: {
ttl: 0,
enabled: true,
isEqual: lodashIsEqual,
},
};
const nextHandler1 = createMemoizeMiddleware()({
dispatch: (action) => {
Expand Down Expand Up @@ -247,6 +225,139 @@ describe('unit test', () => {
.catch(done);
});

it('use with common action, disableTTL = false (Browser)', (done) => {
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.payload;
default:
return state;
}
}
const createThunk = memoize({ ttl: 50 })(num => ({
type: 'INCREMENT',
payload: num,
}));

const memoizeMiddleware = createMemoizeMiddleware({ disableTTL: false });

const store = applyMiddleware(
memoizeMiddleware,
)(configureStore)(counter);

const result1 = store.dispatch(createThunk(2));
const result2 = store.dispatch(createThunk(3));
const result3 = store.dispatch(createThunk(2));

expect(typeof result1.then).toBe('function');
expect(typeof result2.then).toBe('function');
expect(typeof result3.then).toBe('function');
expect(result1 === result3).toBeTruthy();
expect(result1 === result2).not.toBeTruthy();
expect(memoizeMiddleware.getAll().length).toBe(2);
expect(store.getState()).toBe(5);
new Promise((resolve) => {
setTimeout(() => {
store.dispatch(createThunk(2));
resolve();
}, 100);
})
.then(() => {
expect(store.getState()).toBe(7);
return new Promise((resolve) => {
setTimeout(() => {
store.dispatch(createThunk(2));
resolve();
}, 10);
});
})
.then(() => {
expect(store.getState()).toBe(7);
return new Promise((resolve) => {
setTimeout(() => {
store.dispatch(createThunk(2));
resolve();
}, 100);
});
})
.then(() => {
expect(store.getState()).toBe(9);
done();
})
.catch((err) => {
done(`ERROR: ${err}`);
});
});

it('use with common action with globalOptions, disableTTL = false (Browser)', (done) => {
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.payload;
default:
return state;
}
}
const createThunk = memoize()(num => ({
type: 'INCREMENT',
payload: num,
}));

const memoizeMiddleware = createMemoizeMiddleware({
disableTTL: false,
globalOptions: {
ttl: 50,
},
});

const store = applyMiddleware(
memoizeMiddleware,
)(configureStore)(counter);

const result1 = store.dispatch(createThunk(2));
const result2 = store.dispatch(createThunk(3));
const result3 = store.dispatch(createThunk(2));

expect(typeof result1.then).toBe('function');
expect(typeof result2.then).toBe('function');
expect(typeof result3.then).toBe('function');
expect(result1 === result3).toBeTruthy();
expect(result1 === result2).not.toBeTruthy();
expect(memoizeMiddleware.getAll().length).toBe(2);
expect(store.getState()).toBe(5);
new Promise((resolve) => {
setTimeout(() => {
store.dispatch(createThunk(2));
resolve();
}, 100);
})
.then(() => {
expect(store.getState()).toBe(7);
return new Promise((resolve) => {
setTimeout(() => {
store.dispatch(createThunk(2));
resolve();
}, 10);
});
})
.then(() => {
expect(store.getState()).toBe(7);
return new Promise((resolve) => {
setTimeout(() => {
store.dispatch(createThunk(2));
resolve();
}, 100);
});
})
.then(() => {
expect(store.getState()).toBe(9);
done();
})
.catch((err) => {
done(`ERROR: ${err}`);
});
});

it('use with redux-thunk', (done) => {
let thunkCreatorCalled = 0;
let thunkCalled = 0;
Expand Down

0 comments on commit a9b8dfd

Please sign in to comment.