auto-memoize

Memoize function with predifined caching strategies

Usage no npm install needed!

<script type="module">
  import autoMemoize from 'https://cdn.skypack.dev/auto-memoize';
</script>

README

Fast javascript memoize function, with optimized memoize strategies provided. Class and methods memoize decorators.

Build Status Coverage Status npm version

npm i -S auto-memoize
import { memoize, CreateOnce, CallOnce } from 'auto-memoize'
import { fibo } from './fibonaci'

// memoize function
let memoizeFibonaci = memoize(fibo)
memoizeFibonaci(5)
memoizeFibonaci(5) //cache hit

// Create one instance with same parameters
@CreateOnce
class Person {
  firstName
  lastName
  constructor(firstName, lastName) {
    console.log(`Creating ${firstName} ${lastName}`)
    this.firstName = firstName,
    this.lastName = lastName
  }
  // call method once with same parameters
  @CallOnce
  getGreeting(greet: string) {
    console.log(greet)
    const name = `${this.firstName} ${this.lastName}`
    return { name, greeting }
  }
}
const person1 = new Person('Boris', 'Johnson') 
// logged 'Creating Boris Johnson'
const person2 = new Person('Boris', 'Johnson')
person1 == person2 // true

const g1 = person1.getGreeting('hi') // logged 'hi'
const g2 = person1.getGreeting('hi')
g1 == g2 // true

memoize

Default implementation

Caches by all parameters as a key, comparing them with Object.is algorithm

const calc = require('expensive-calculation')
const { memoize } = require('auto-memoize')

const memoCalc = memoize(calc)

const param = {a: 'one'}
memoCalc(param, 1)
memoCalc(param, 2)
memoCalc(param, 1) // cache hit
memoCalc({a: 'one'}, 1) // no cache hit, because reference is different

WeakMap implementation

Caches by first parameter as a key and using ES6 WeakMap as a cache. Garbage collector automatically removes entries from cache, when no references for keys are present.

const calc = require('expensive-calculation')
const { memoize } = require('auto-memoize')

const memoCalc = memoize(calc, 'weak')

const param = {a: 'one'}
memoCalc(param, 1)
memoCalc(param, 2) // cache hit
memoCalc({a: 'one'}, 1) // no cache hit, because reference is different

Deep comparison implementation

Caches by all parameters as a key and compares them by content, if references are different. fast-deep-equal npm package does the comparison. It is performing better on big objects, than JSON.stringify.

const calc = require('expensive-calculation')
const { memoize } = require('auto-memoize')

const memoCalc = memoize(calc, 'deep')

const param = {a: 'one'}
memoCalc(param)
memoCalc(param) // cache hit
memoCalc({a: 'one'}) // cache hit

String key implementation

Caches all parameters as a string key. For cache is used ES6 Map. JSON.stringify is used to turn every parameter to string. It is useful for function with several parameters, but not big objects, that takes time to turn into string.

const calc = require('expensive-calculation')
const { memoize } = require('auto-memoize')

const memoCalc = memoize(calc, 'string')

const param = {a: 'one'}
memoCalc(param, 1)
memoCalc(param, 1) // cache hit
memoCalc({a: 'one'}, 2) 
memoCalc({a: 'one'}, 1) // cache hit

Custom key implementation

Caches by key from function, that returns string. For cache is used ES6 Map

const calc = require('expensive-calculation')
const { memoize } = require('memoize.js')

const memoCalc = memoize(calc, (p) => p.a)

const param = {a: 'one'}
memoCalc(param)
memoCalc(param) // cache hit
memoCalc({a: 'one'}) // cache hit

Benchmarking strategies

Small object parameter

const simple = {
  data: {
    p1: {
      name: 'auto-memoize'
      }
    }
  }
memoized(simple, 1, 1)
Strategy ops/sec
1 callback 4,765,946
2 weak 3,751,169
3 deep 1,211,864
4 default 1,004,562
5 string 341,206

Object parameter

const data = { ...3 package.json }
memoized(data, 1, 1)
Strategy ops/sec
1 callback 5,131,611
2 weak 2,663,508
3 default 992,947
4 deep 832,758
5 string 33,763

Different reference object parameter

const data = { ...3 package.json }
memoized(Object.assign({}, data), 1, 1)
Strategy ops/sec
1 callback 2,195,159
2 deep 708,301
3 string 33,098
weak 833,162 n/a
default 1,484 n/a

Primitive parameters

memoized("argument", 1, true)
Strategy ops/sec
1 callback 3,340,699
2 deep 1,320,397
3 default 1,330,912
4 string 548,254
weak n/a

Typings includes types for:

  • memoized function parameters
  • memoized function return type

getCache

Utility function to get cache instance from memoized function, class or method. It will return instance of CacheMap. It enables you to retrieve cache records and invalidate cache.

CacheMap

For different cache strategies different keys are applicable. Key types:

  • default - any[]
  • weak - object
  • string - string (parameters joined by -)
  • deep - any[]
  • custom key - string
export interface CacheMap<K = any, V = any> {
    get(key: K): V | undefined
    has(key: K): boolean
    set(key: K, result: V): this
    clear(): void
}

Examples

import { getCache, CreateOnce, CallOnce, memoize } from 'auto-memoize'
import { fibo } from './fibonaci'

const memo = memoize(fibo)
memo(5)

// cache from meoized function
getCache(memo).has([5]) // true
getCache(memo).clear()
getCache(memo).has([5]) // false

@CreateOnce
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
  @CallOnce
  greet(greeting) {
    const { firstName } = this
    return { greeting, firstName }
  }
}
const person = new Person('A', 'B')
person.greet('Hello')

// cache from  decorated 'CreateOnce' or 'CreateOnceBy' class
getCache(Person)

// cache from  decorated 'CallOnce' or 'CallOnceBy' class
getCache(person.greet)

Decorators

Docs

@CreateOnce

Class Decorator used to create same instance of class when same parameters are passed. It follows functional design pattern memoize, also Flyweight design pattern, when one instance of Class is created once with exact state. It defines how objects with state can be shared. If class constructor has no parameters, class decorated with CreateOnce becomes singleton. If this class is extended, effect of decorator will be applied on child classes. Parent constructor will always be constructor that creates class instance. Even if child constructor accepts different parameters. If child class is decorated, child class will have cache effect. It will cache instances by default 'auto-memoize' strategy. Decorator @CreateOnceBy can be configured with other cache hit strategy

@CreateOnceBy

Class Decorator used to created same instance of class when same parameters are passed. It follows functional design pattern memoize, also Flyweight design pattern, when one instance of Class is created once with exact state. It defines how objects with state can be shared. It will cache instances by strategy from decorator parameter. Decorator @CreateOnce is preconfigured with default cache hit strategy. If this class is extended, effect of decorator will be applied on child classes. Parent constructor will always be constructor that creates class instance. Even if child constructor accepts different parameters. If child class is decorated, child class will have cache effect.

@CallOnce

Class method decorator used to create memoized class method. Method that will return same output with same parameters passed. It follows memoize functional design pattern. It will cache by default auto-memoize strategy. Decorator @CallOnceBy can be configured with other cache hit strategy.

@CallOnceBy

Class method decorator used to create 'memoized' class method. Method that will return same output with same parameters passed. It follows memoize functional design pattern. It must be configured with cache hit strategy. Decorator @CallOnce is already preconfgured with default strategy.

Examples

Caching with default strategy

import { CreateOnce, CallOnce } from 'auto-memoize';

// Creates one class instance per constructor parameter 
@CreateOnce
class Greeter {
  greeting
    constructor(greeting) {
      this.greeting = greeting
    }

    // Calls method once per parameter unique
    // method with cache
    @CallOnce
    greet(name) {
      return { greet: `${this.greeting}, ${name}!` }
    }
}

const greeter1 = new Greeter('Hello')
const greeter2 = new Greeter('Hello')
greeter1 == greeter2 // true

const greet1 = greeter1.greet('Boris')
const greet2 = greeter1.greet('Boris')
const greet3 = greeter2.greet('Boris')
greet1 == greet2 // true
greet1 == greet3 // true

Caching with provided strategy

import { CreateOnceBy, CallOnceBy } from 'auto-memoize';
 
// Creates one class instance per constructor parameter 
@CreateOnceBy('string')
class CustomGreeter {
  greeting
    constructor(greeting) {
      this.greeting = greeting
    }

    // Calls method once per parameter unique
    // method with cache
    @CallOnceBy(person => person.name)
    greet(person) {
      return { greet: `${this.greeting}, ${person.name}!` }
    }
}
  const greeter1 = new Greeter('Hello')
  const greeter2 = new Greeter('Hello')
  greeter1 == greeter2 // true
  
  const greet1 = greeter1.greet({name: 'Boris'})
  const greet2 = greeter1.greet({name: 'Boris'})
  const greet3 = greeter2.greet({name: 'Boris'})
  greet1 == greet2 // true
  greet1 == greet3 // true

Class Singleton

import { CreateOnce } from 'auto-memoize';

// Always will return same instance of class
@CreateOnce
class Greeter {
  constructor() {
    }
    greet(person) {
      return { greet: `Hello, ${person.name}!` }
    }
}
const greeter1 = new Greeter()
const greeter2 = new Greeter()
greeter1 == greeter2 // true