fast-typescript-memoize
Version:
Fast memoization decorator and other helpers with 1st class support for Promises.
156 lines • 6.37 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Memoize = void 0;
/**
* A @Memoize() decorator implementation, inspired by:
* https://www.npmjs.com/package/typescript-memoize.
*/
function Memoize(a1, a2) {
const [hasher, options] = typeof a1 === "function" ? [a1, a2] : [undefined, a1];
return (_target, propName, descriptor) => {
if (typeof descriptor.value === "function") {
descriptor.value = buildNewMethod(descriptor.value, propName, hasher, options);
}
else if (descriptor.get) {
descriptor.get = buildNewMethod(descriptor.get, propName, hasher, options);
}
else {
throw "Only put @Memoize() decorator on a method or get accessor.";
}
};
}
exports.Memoize = Memoize;
let counter = 0;
/**
* Builds a new function which will be returned instead of the original
* decorated method.
*/
function buildNewMethod(origMethod, propName, hasher, { clearOnReject, clearOnResolve } = {
clearOnReject: true,
clearOnResolve: false,
}) {
// Depending on the arguments of the method we're memoizing, we use one of 3
// storages (with some code boilerplate for performance):
// - If the arguments hash (defaults to the 1st argument) is a JS OBJECT, we
// store the memoized value in a WeakMap keyed by that object. It allows JS
// GC to garbage collect that object since it's not retained in the internal
// memoized WeakMap. Motivation: if we lose the object, we obviously can't
// pass it to any method with `Memoize()`, and thus, we anyways won't be
// able to access the memoized value, so WeakMap is a perfect hit here.
// - If the arguments hash is of a PRIMITIVE TYPE, we store the memoized value
// in a regular Map. Primitive types (like strings, numbers etc.) can't be
// used as WeakMap keys for obvious reasons.
// - And lastly, if it's a NO-ARGUMENTS METHOD, we store the value in a hidden
// object property directly. This is the most frequent use case.
const propWeakName = `__memoized_weak_${propName.toString()}_${counter}`;
const propMapName = `__memoized_map_${propName.toString()}_${counter}`;
const propValName = `__memoized_val_${propName.toString()}_${counter}`;
counter++;
return function (...args) {
let value;
if (hasher || args.length > 0) {
const hashKey = hasher ? hasher.apply(this, args) : args[0];
if (hashKey !== null && typeof hashKey === "object") {
// Arg (or hash) is an object: WeakMap.
if (!this.hasOwnProperty(propWeakName)) {
Object.defineProperty(this, propWeakName, {
configurable: false,
enumerable: false,
writable: false,
value: new WeakMap(),
});
}
const weak = this[propWeakName];
if (weak.has(hashKey)) {
value = weak.get(hashKey);
}
else {
value = origMethod.apply(this, args);
if (clearOnReject && value instanceof Promise) {
value = value.catch(deleteWeakKeyAndRethrow.bind(undefined, weak, hashKey));
}
if (clearOnResolve && value instanceof Promise) {
value = value.then(deleteWeakKeyAndReturn.bind(undefined, weak, hashKey));
}
weak.set(hashKey, value);
}
}
else {
// Arg (or hash) is a primitive type: Map.
if (!this.hasOwnProperty(propMapName)) {
Object.defineProperty(this, propMapName, {
configurable: false,
enumerable: false,
writable: false,
value: new Map(),
});
}
const map = this[propMapName];
if (map.has(hashKey)) {
value = map.get(hashKey);
}
else {
value = origMethod.apply(this, args);
if (clearOnReject && value instanceof Promise) {
value = value.catch(deleteMapKeyAndRethrow.bind(undefined, map, hashKey));
}
if (clearOnResolve && value instanceof Promise) {
value = value.then(deleteMapKeyAndReturn.bind(undefined, map, hashKey));
}
map.set(hashKey, value);
}
}
}
else {
// No arg: plain object property.
if (this.hasOwnProperty(propValName)) {
value = this[propValName];
}
else {
value = origMethod.apply(this, args);
if (clearOnReject && value instanceof Promise) {
value = value.catch(deleteObjPropAndRethrow.bind(undefined, this, propValName));
}
if (clearOnResolve && value instanceof Promise) {
value = value.then(deleteObjPropAndReturn.bind(undefined, this, propValName));
}
Object.defineProperty(this, propValName, {
configurable: true, // to be able to remove it
enumerable: false,
writable: false,
value,
});
}
}
return value;
};
}
//
// Below are helper functions to just not use "=>" closures and thus control,
// which variables will be retained from garbage collection.
//
function deleteWeakKeyAndRethrow(weak, key, e) {
weak.delete(key);
throw e;
}
function deleteMapKeyAndRethrow(map, key, e) {
map.delete(key);
throw e;
}
function deleteObjPropAndRethrow(obj, key, e) {
delete obj[key];
throw e;
}
function deleteWeakKeyAndReturn(weak, key, value) {
weak.delete(key);
return value;
}
function deleteMapKeyAndReturn(map, key, value) {
map.delete(key);
return value;
}
function deleteObjPropAndReturn(obj, key, value) {
delete obj[key];
return value;
}
//# sourceMappingURL=Memoize.js.map