@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
86 lines (85 loc) • 4.11 kB
JavaScript
import { defaultArgsHashFn, memoizeFn } from '../misc/memoize';
import { isPrototype, getPropertyDescriptor } from '../misc/object';
/**
* `@memoize` decorator: caches method results or getter value.
* - Supports prototype methods & getters and static methods & getters
* - Prototype method: first call installs a per‑instance memoized function (independent caches per instance)
* - Prototype getter: first access computes & stores value directly on the instance (value cache removable by deleting own prop)
* - Static members: wrapped by shared `memoizeFn` (single cache per class member)
* - Hash fn: string | null key; `undefined` means skip caching; default only supports 0-1 primitive arg
* - Cache control: `memoize.clear(target, key)` removes cached value/function; `memoize.has(target, key, ...args)` checks presence
* - Underlying memoized functions expose: `.cache: Map`, `.clear()`, `.has(...args)`
*
* @param hashFn - optional hash function; defaults to `defaultArgsHashFn`
* @throws TypeError when applied to non-function / non-getter descriptor
* @see memoizeFn low-level function memoizer
*/
export function memoize(hashFn = defaultArgsHashFn) {
return function (target, prop, descriptor) {
if (!descriptor || typeof (descriptor.value || descriptor.get) !== 'function') {
throw new TypeError('Only get accessors or class methods can be decorated via @memoize');
}
if (isPrototype(target)) {
// Object members
(typeof descriptor.get === 'function') && (descriptor.get = memoizeGetter(descriptor.get, prop));
(typeof descriptor.value === 'function') && (descriptor.value = memoizeMethod(descriptor.value, prop, hashFn));
}
else {
// Static members
(typeof descriptor.get === 'function') && (descriptor.get = memoizeFn(descriptor.get));
(typeof descriptor.value === 'function') && (descriptor.value = memoizeFn(descriptor.value, hashFn));
}
};
}
// Lock storage to prevent cache logic for some key
const locks = new WeakMap();
const defineOwnKeySafe = (obj, prop, value) => {
locks.set(obj, prop); // IE try to get key with the prototype instance call, so we lock it
Object.defineProperty(obj, prop, { value, writable: true, configurable: true });
locks.delete(obj); // Free property key
};
/** Cache getter result as an object own property */
function memoizeGetter(originalMethod, prop) {
return function () {
if (locks.get(this) === prop)
return originalMethod;
const value = originalMethod.call(this);
defineOwnKeySafe(this, prop, value);
return value;
};
}
/** Cache method memo function in the current context on call */
function memoizeMethod(originalMethod, prop, hashFn) {
return function (...args) {
if (locks.get(this) === prop)
return originalMethod;
const memo = memoizeFn(originalMethod, hashFn);
defineOwnKeySafe(this, prop, memo);
return memo.apply(this, args);
};
}
function clearMemo(target, property) {
if (Array.isArray(property))
return property.forEach((prop) => memoize.clear(target, prop));
const desc = getPropertyDescriptor(target, property);
if (!desc)
return;
if (typeof desc.get === 'function' && typeof desc.get.clear === 'function')
return desc.get.clear();
if (typeof desc.value === 'function' && typeof desc.value.clear === 'function')
return desc.value.clear();
if (Object.hasOwnProperty.call(target, property))
delete target[property];
}
memoize.clear = clearMemo;
function hasMemo(target, property, ...params) {
const desc = getPropertyDescriptor(target, property);
if (!desc)
return false;
if (typeof desc.get === 'function' && typeof desc.get.has === 'function')
return desc.get.has(...params);
if (typeof desc.value === 'function' && typeof desc.value.has === 'function')
return desc.value.has(...params);
return Object.hasOwnProperty.call(target, property);
}
memoize.has = hasMemo;