├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── package.json └── src ├── __tests__ └── memoize-test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Andrey Popp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = ./node_modules/.bin 2 | SRC = $(shell find src -name '*.js') 3 | LIB = $(SRC:src/%=lib/%) 4 | TESTS = $(wildcard ./src/__tests__/*.js) 5 | 6 | BABEL_OPTS = \ 7 | --stage 0 8 | 9 | build: $(LIB) 10 | 11 | test:: 12 | @$(BIN)/mochify \ 13 | --transform [ babelify $(BABEL_OPTS) ] \ 14 | $(TESTS) 15 | 16 | ci:: 17 | @$(BIN)/mochify \ 18 | --watch \ 19 | --transform [ babelify $(BABEL_OPTS) ] \ 20 | $(TESTS) 21 | 22 | lint:: 23 | 24 | release-patch: build test lint 25 | @$(call release,patch) 26 | 27 | release-minor: build test lint 28 | @$(call release,minor) 29 | 30 | release-major: build test lint 31 | @$(call release,major) 32 | 33 | publish: 34 | git push --tags origin HEAD:master 35 | npm publish 36 | 37 | lib/%.js: src/%.js 38 | @echo "building $@" 39 | @mkdir -p $(@D) 40 | @$(BIN)/babel $(BABEL_OPTS) --source-maps-inline -o $@ $< 41 | 42 | clean: 43 | @rm -f $(LIB) 44 | 45 | define release 46 | npm version $(1) 47 | endef 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memoize decorator 2 | 3 | This is a method/getter decorator which is when applied to a method or a getter 4 | memoizes the result of the first call and returns it on subsequent calls. 5 | 6 | As decorators are a part of future ES7 standard they can only be used with 7 | transpilers such as [Babel](http://babeljs.io). 8 | 9 | Installation: 10 | 11 | % npm install memoize-decorator 12 | 13 | Example: 14 | 15 | ```js 16 | import memoize from 'memoize-decorator' 17 | 18 | class Component { 19 | 20 | @memoize 21 | get expensiveValue() { 22 | console.log('heavy computations') 23 | return 42 24 | } 25 | } 26 | 27 | let component = new Component() 28 | component.expensiveValue // prints 'heavy computations', returns 42 29 | component.expensiveValue // just returns 42 30 | ``` 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memoize-decorator", 3 | "version": "1.0.2", 4 | "description": "Memoize getters and methods to compute only once", 5 | "main": "lib/index.js", 6 | "author": "Andrey Popp <8mayday@gmail.com>", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/andreypopp/memoize-decorator.git" 11 | }, 12 | "devDependencies": { 13 | "babel": "^5.0.12", 14 | "babelify": "^6.0.2", 15 | "mocha": "^2.2.4", 16 | "mochify": "^2.7.1", 17 | "phantomjs": "^1.9.16" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/__tests__/memoize-test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import memoize from '../'; 3 | 4 | describe('memoize getter/method decorator', function() { 5 | 6 | class A { 7 | constructor(value = 42) { 8 | this.computedCount = 0; 9 | this.value = value; 10 | } 11 | 12 | @memoize 13 | get expensiveValue() { 14 | this.computedCount += 1; 15 | return this.value; 16 | } 17 | 18 | @memoize 19 | expensiveMethod() { 20 | this.computedCount += 1; 21 | return this.value + 1; 22 | } 23 | } 24 | 25 | it('computes class getter only once and memoizes result for future access', function() { 26 | let a = new A(); 27 | assert(a.computedCount === 0); 28 | assert(a.expensiveValue === 42); 29 | assert(a.computedCount === 1); 30 | assert(a.expensiveValue === 42); 31 | assert(a.computedCount === 1); 32 | }); 33 | 34 | it('computes class method only once and memoizes result for future access', function() { 35 | let a = new A(); 36 | assert(a.computedCount === 0); 37 | assert(a.expensiveMethod() === 43); 38 | assert(a.computedCount === 1); 39 | assert(a.expensiveMethod() === 43); 40 | assert(a.computedCount === 1); 41 | }); 42 | 43 | it('does not override memoized values from different methods', function() { 44 | let a = new A(41); 45 | assert(a.expensiveValue, 41); 46 | let b = new A(42); 47 | assert(b.expensiveValue, 42); 48 | assert(a.expensiveValue, 41); 49 | assert(b.expensiveValue, 42); 50 | }); 51 | 52 | it('throws if applied on a method of more than zero arguments', function() { 53 | assert.throws(() => { 54 | class A { 55 | @memoize 56 | method(a) { 57 | 58 | } 59 | } 60 | }, /@memoize decorator can only be applied to methods of zero arguments/); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015, Andrey Popp <8mayday@gmail.com> 3 | */ 4 | 5 | const SENTINEL = {}; 6 | 7 | export default function memoize(target, name, descriptor) { 8 | if (typeof descriptor.value === 'function') { 9 | return _memoizeMethod(target, name, descriptor); 10 | } else if (typeof descriptor.get === 'function') { 11 | return _memoizeGetter(target, name, descriptor); 12 | } else { 13 | throw new Error('@memoize decorator can be applied to methods or getters, got ' + String(descriptor.value) + ' instead'); 14 | } 15 | } 16 | 17 | function _memoizeGetter(target, name, descriptor) { 18 | let memoizedName = `_memoized_${name}`; 19 | let get = descriptor.get; 20 | target[memoizedName] = SENTINEL; 21 | return { 22 | ...descriptor, 23 | get() { 24 | if (this[memoizedName] === SENTINEL) { 25 | this[memoizedName] = get.call(this); 26 | } 27 | return this[memoizedName]; 28 | } 29 | }; 30 | } 31 | 32 | function _memoizeMethod(target, name, descriptor) { 33 | if (descriptor.value.length > 0) { 34 | throw new Error('@memoize decorator can only be applied to methods of zero arguments'); 35 | } 36 | let memoizedName = `_memoized_${name}`; 37 | let value = descriptor.value; 38 | target[memoizedName] = SENTINEL; 39 | return { 40 | ...descriptor, 41 | value() { 42 | if (this[memoizedName] === SENTINEL) { 43 | this[memoizedName] = value.call(this); 44 | } 45 | return this[memoizedName]; 46 | } 47 | }; 48 | } 49 | --------------------------------------------------------------------------------