@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
81 lines (80 loc) • 3.56 kB
JavaScript
import { _assert, _assertTypeOf } from '../error/assert.js';
import { _objectAssign } from '../types.js';
import { _getTargetMethodSignature } from './decorator.util.js';
import { jsonMemoSerializer, MapMemoCache } from './memo.util.js';
/**
* Memoizes the method of the class, so it caches the output and returns the cached version if the "key"
* of the cache is the same. Key, by defaul, is calculated as `JSON.stringify(...args)`.
* Cache is stored indefinitely in the internal Map.
*
* If origin function throws an Error - it is NOT cached.
* So, error-throwing functions will be called multiple times.
* Therefor, if the origin function can possibly throw - it should try to be idempotent.
*
* Cache is stored **per instance** - separate cache for separate instances of the class.
* If you don't want it that way - you can use a static method, then there will be only one "instance".
*
* Supports dropping it's cache by calling .clear() method of decorated function (useful in unit testing).
*
* Based on:
* https://github.com/mgechev/memo-decorator/blob/master/index.ts
* http://decodize.com/blog/2012/08/27/javascript-memoization-caching-results-for-better-performance/
* http://inlehmansterms.net/2015/03/01/javascript-memoization/
* https://community.risingstack.com/the-worlds-fastest-javascript-memoization-library/
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const _Memo = (opt = {}) => (target, key, descriptor) => {
_assertTypeOf(descriptor.value, 'function', 'Memoization can be applied only to methods');
const originalFn = descriptor.value;
// Map<ctx => MemoCache<cacheKey, result>>
//
// Internal map is from cacheKey to result
// External map (instanceCache) is from ctx (instance of class) to Internal map
// External map is Weak to not cause memory leaks, to allow ctx objects to be garbage collected
// UPD: tests show that normal Map also doesn't leak (to be tested further)
// Normal Map is needed to allow .clear()
const instanceCache = new Map();
const { logger = console, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, } = opt;
const keyStr = String(key);
const methodSignature = _getTargetMethodSignature(target, keyStr);
descriptor.value = function (...args) {
const ctx = this;
const cacheKey = cacheKeyFn(args);
let cache = instanceCache.get(ctx);
if (!cache) {
cache = cacheFactory();
instanceCache.set(ctx, cache);
}
if (cache.has(cacheKey)) {
// Hit
return cache.get(cacheKey);
}
// Miss
const value = originalFn.apply(ctx, args);
try {
cache.set(cacheKey, value);
}
catch (err) {
logger.error(err);
}
return value;
};
_objectAssign(descriptor.value, {
clear: () => {
logger.log(`${methodSignature} @_Memo.clear()`);
instanceCache.forEach(memoCache => memoCache.clear());
instanceCache.clear();
},
getInstanceCache: () => instanceCache,
getCache: instance => instanceCache.get(instance),
});
return descriptor;
};
/**
Call it on a method that is decorated with `@_Memo` to get access to additional functions,
e.g `clear` to clear the cache, or get its underlying data.
*/
export function _getMemo(method) {
_assert(typeof method?.getInstanceCache === 'function', 'method is not a Memo instance');
return method;
}