Skip to content

Commit

Permalink
adding more testing around context
Browse files Browse the repository at this point in the history
  • Loading branch information
alexreardon committed Feb 7, 2017
1 parent 3afb7e1 commit f649a22
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 70 deletions.
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ A memoization library which only remembers the latest invokation

[![Build Status](https://travis-ci.org/alexreardon/memoize-one.svg?branch=master)](https://travis-ci.org/alexreardon/memoize-one) [![codecov](https://codecov.io/gh/alexreardon/memoize-one/branch/master/graph/badge.svg)](https://codecov.io/gh/alexreardon/memoize-one)

## DOCS: Work in progress

## Rationale

Cache invalidation is hard:
Expand All @@ -16,8 +14,7 @@ Cache invalidation is hard:
So keep things simple and just use a cache size of one.

Unlike other memoization libraries, `memoizeOne` only remembers the latest arguments. No need to worry about cache busting mechanisms such as `maxAge`, `maxSize`, `exlusions` and so on which can be prone to memory leaks. `memoizeOne` simply remembers the last arguments, and if the function is next called with the same arguments then it returns the previous result.

Unlike other memoization libraries, `memoizeOne` only remembers the latest arguments and result. No need to worry about cache busting mechanisms such as `maxAge`, `maxSize`, `exlusions` and so on which can be prone to memory leaks. `memoizeOne` simply remembers the last arguments, and if the function is next called with the same arguments then it returns the previous result.

## Usage

Expand Down Expand Up @@ -71,6 +68,13 @@ result3 === result4 // true - arguments are deep equal
```
[Play with this example](http://www.webpackbin.com/NJW-tJMdf)

#### Type signature
Here is the expected [flow](http://flowtype.org) type signature for a custom equality function:

```js
type EqualityFn = (a: any, b: any) => boolean;
```

## Installation

```bash
Expand All @@ -81,15 +85,22 @@ yarn add memoize-one
npm install memoize-one --save
```

## Other features
## Other

### Correctly supports `this` binding
### memoizeOne correctly supports `this` control

### Custom equality function
This library takes special care to maintain, and allow control over the the `this` context just like a regular function. Both the original function and the memoized function's can be controlled using all the standard `this` controlling techniques:

- new bindings (`new`)
- explicit binding (`call`, `apply`, `bind`);
- implicit binding (call site: `obj.foo()`);
- default binding (`window` or `undefined` in `strict mode`);
- fat arrow binding (binding to lexical `this`)
- ignored this (pass `null` as `this` to explicit binding)

### Code health

- Tested with [all JavaScript *types*](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md)
- 100% code coverage
- [flow types](http://flowtype.org) for safer internal execution and type checking / auto complete for editors
- [Semantically versioning (2.0)](http://semver.org/)
- Tested with all built in [JavaScript types](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md).
- 100% code coverage.
- [flow types](http://flowtype.org) for safer internal execution and external consumption. Also allows for editor autocompletion.
- Follows [Semantic versioning (2.0)](http://semver.org/) for safer versioning.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "memoize-one",
"version": "1.0.0-rc.1",
"version": "1.0.0-rc.2",
"description": "A memoization library for memoizing a function with a cache size of one",
"main": "lib/index.js",
"module": "src/index.js",
Expand Down
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ type EqualityFn = (a: any, b: any) => boolean;
const simpleIsEqual = (a: any, b: any): boolean => a === b;

export default function (resultFn: Function, isEqual?: EqualityFn = simpleIsEqual) {
let lastThis: any;
let lastArgs: Array<any> = [];
let lastResult: any;
let calledOnce: boolean = false;

return function (...newArgs: Array<any>) {
if (calledOnce && newArgs.length === lastArgs.length &&
if (calledOnce &&
newArgs.length === lastArgs.length &&
lastArgs.every((lastArg, i) => isEqual(lastArg, newArgs[i]))) {
return lastResult;
}
Expand Down
203 changes: 146 additions & 57 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,66 @@ import memoizeOne from '../src/';

type Expectation = {|
args: any[],
result: any
|};
result: any
|};

type Input = {|
name: string,
first: Expectation,
second: Expectation
|};
first: Expectation,
second: Expectation
|};

describe('memoizeOne', () => {
// [JavaScript defines seven built-in types:](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md)
// - null
// - undefined
// - boolean
// - number
// - string
// - object
// - symbol
describe('standard behaviour - baseline', () => {
let add;
let memoizedAdd;

beforeEach(() => {
add = sinon.spy((value1: number, value2: number): number => value1 + value2);
memoizedAdd = memoizeOne(add);
});

it('should return the result of a function', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
});

it('should return the same result if the arguments have not changed', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
expect(memoizedAdd(1, 2)).to.equal(3);
});

it('should not execute the memoized function if the arguments have not changed', () => {
memoizedAdd(1, 2);
memoizedAdd(1, 2);

expect(add.callCount).to.equal(1);
});

it('should invalidate a memoize cache if new arguments are provided', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
expect(memoizedAdd(2, 2)).to.equal(4);
expect(add.callCount).to.equal(2);
});

it('should resume memoization after a cache invalidation', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
expect(add.callCount).to.equal(1);
expect(memoizedAdd(2, 2)).to.equal(4);
expect(add.callCount).to.equal(2);
expect(memoizedAdd(2, 2)).to.equal(4);
expect(add.callCount).to.equal(2);
});
});

describe('standard behaviour', () => {
// [JavaScript defines seven built-in types:](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md)
// - null
// - undefined
// - boolean
// - number
// - string
// - object
// - symbol
const inputs: Input[] = [
{
name: 'null',
Expand Down Expand Up @@ -128,7 +168,7 @@ describe('memoizeOne', () => {
};

inputs.forEach(({ name, first, second }) => {
describe(`dynamic type test:[${name}]`, () => {
describe(`type test:[${name}]`, () => {

let spy;
let memoized;
Expand Down Expand Up @@ -188,6 +228,20 @@ describe('memoizeOne', () => {
return this.a;
}

it('should respect new bindings', () => {
const Foo = function (bar) {
this.bar = bar;
};
const memoized = memoizeOne(function (bar) {
return new Foo(bar);
});

const result = memoized('baz');

expect(result instanceof Foo).to.equal(true);
expect(result.bar).to.equal('baz');
});

it('should respect explicit bindings', () => {
const temp = {
a: 10,
Expand All @@ -210,8 +264,39 @@ describe('memoizeOne', () => {
expect(memoized()).to.equal(20);
});

it('should respect default bindings', () => {
const memoized = memoizeOne(getA);
it('should respect implicit bindings', () => {
const temp = {
a: 2,
getA,
};

const memoized = memoizeOne(function () {
return temp.getA();
});

expect(memoized()).to.equal(2);
});

it('should respect fat arrow bindings', () => {
const temp = {
a: 50,
};
function foo() {
// return an arrow function
return () => {
// `this` here is lexically adopted from `foo()`
return this.a;
};
}
const bound = foo.call(temp);
const memoized = memoizeOne(bound);

expect(memoized()).to.equal(50);
});

it('should respect ignored bindings', () => {
const bound = getA.bind(null);
const memoized = memoizeOne(bound);

expect(memoized).to.throw(TypeError);
});
Expand All @@ -222,6 +307,8 @@ describe('memoizeOne', () => {
return this.a;
}

it('should respect new bindings');

it('should respect implicit bindings', () => {
const getAMemoized = memoizeOne(getA);
const temp = {
Expand Down Expand Up @@ -264,6 +351,49 @@ describe('memoizeOne', () => {
expect(getAMemoized()).to.equal(40);
expect(spy.callCount).to.equal(1);
});

it('should respect implicit bindings', () => {
const getAMemoized = memoizeOne(getA);
const temp = {
a: 2,
getAMemoized,
};

expect(temp.getAMemoized()).to.equal(2);
});

it('should respect fat arrow bindings', () => {

});

it('should respect ignored bindings', () => {
const memoized = memoizeOne(getA);

const getResult = function () {
return memoized.call(null);
};

expect(getResult).to.throw(TypeError);
});

// This behaviour is debatable.
// For: sometimes you might call a function in different `this` without knowing it and this would bust your cache
// Against: if your function is reading a value from `this` then your memoized function would be returning the wrong result.
it('should memoize the previous result even if the this context changes', () => {
const memoized = memoizeOne(getA);
const temp1 = {
a: 20,
getMemoizedA: memoized,
};
const temp2 = {
a: 30,
getMemoizedA: memoized,
};

expect(temp1.getMemoizedA()).to.equal(20);
// this might be unexpected
expect(temp2.getMemoizedA()).to.equal(20);
});
});
});

Expand Down Expand Up @@ -329,46 +459,5 @@ describe('memoizeOne', () => {
expect(add.callCount).to.equal(2);
});
});

describe('standard behaviour', () => {
let add;
let memoizedAdd;

beforeEach(() => {
add = sinon.spy((value1: number, value2: number): number => value1 + value2);
memoizedAdd = memoizeOne(add);
});

it('should return the result of a function', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
});

it('should return the same result if the arguments have not changed', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
expect(memoizedAdd(1, 2)).to.equal(3);
});

it('should not execute the memoized function if the arguments have not changed', () => {
memoizedAdd(1, 2);
memoizedAdd(1, 2);

expect(add.callCount).to.equal(1);
});

it('should invalidate a memoize cache if new arguments are provided', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
expect(memoizedAdd(2, 2)).to.equal(4);
expect(add.callCount).to.equal(2);
});

it('should resume memoization after a cache invalidation', () => {
expect(memoizedAdd(1, 2)).to.equal(3);
expect(add.callCount).to.equal(1);
expect(memoizedAdd(2, 2)).to.equal(4);
expect(add.callCount).to.equal(2);
expect(memoizedAdd(2, 2)).to.equal(4);
expect(add.callCount).to.equal(2);
});
});
});

0 comments on commit f649a22

Please sign in to comment.