fast-typescript-memoize
Advanced tools
Comparing version
@@ -10,8 +10,8 @@ "use strict"; | ||
const [hasher, options] = typeof a1 === "function" ? [a1, a2] : [undefined, a1]; | ||
return (_target, _propertyKey, descriptor) => { | ||
return (_target, propName, descriptor) => { | ||
if (typeof descriptor.value === "function") { | ||
descriptor.value = buildNewMethod(descriptor.value, hasher, options); | ||
descriptor.value = buildNewMethod(descriptor.value, propName, hasher, options); | ||
} | ||
else if (descriptor.get) { | ||
descriptor.get = buildNewMethod(descriptor.get, hasher, options); | ||
descriptor.get = buildNewMethod(descriptor.get, propName, hasher, options); | ||
} | ||
@@ -29,38 +29,80 @@ else { | ||
*/ | ||
function buildNewMethod(origMethod, hasher, { clearOnReject, clearOnResolve } = { | ||
function buildNewMethod(origMethod, propName, hasher, { clearOnReject, clearOnResolve } = { | ||
clearOnReject: true, | ||
clearOnResolve: false, | ||
}) { | ||
const identifier = ++counter; | ||
// 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 propMapName = `__memoized_map_${identifier}`; | ||
// Get or create map | ||
if (!this.hasOwnProperty(propMapName)) { | ||
Object.defineProperty(this, propMapName, { | ||
configurable: false, | ||
enumerable: false, | ||
writable: false, | ||
value: new Map(), | ||
}); | ||
} | ||
const map = this[propMapName]; | ||
const hashKey = hasher ? hasher.apply(this, args) : args[0]; | ||
if (map.has(hashKey)) { | ||
value = map.get(hashKey); | ||
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 { | ||
value = origMethod.apply(this, args); | ||
if (clearOnReject && value instanceof Promise) { | ||
value = value.catch(deleteMapKeyAndRethrow.bind(undefined, map, hashKey)); | ||
// 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(), | ||
}); | ||
} | ||
if (clearOnResolve && value instanceof Promise) { | ||
value = value.then(deleteMapKeyAndReturn.bind(undefined, map, hashKey)); | ||
const map = this[propMapName]; | ||
if (map.has(hashKey)) { | ||
value = map.get(hashKey); | ||
} | ||
map.set(hashKey, value); | ||
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 { | ||
const propValName = `__memoized_value_${identifier}`; | ||
// No arg: plain object property. | ||
if (this.hasOwnProperty(propValName)) { | ||
@@ -88,6 +130,10 @@ value = this[propValName]; | ||
} | ||
/** | ||
* A helper function to just not use "=>" closures and thus control, which | ||
* variables will be retained from garbage collection. | ||
*/ | ||
// | ||
// 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) { | ||
@@ -97,6 +143,2 @@ map.delete(key); | ||
} | ||
/** | ||
* A helper function to just not use "=>" closures and thus control, which | ||
* variables will be retained from garbage collection. | ||
*/ | ||
function deleteObjPropAndRethrow(obj, key, e) { | ||
@@ -106,6 +148,6 @@ delete obj[key]; | ||
} | ||
/** | ||
* A helper function to just not use "=>" closures and thus control, which | ||
* variables will be retained from garbage collection. | ||
*/ | ||
function deleteWeakKeyAndReturn(weak, key, value) { | ||
weak.delete(key); | ||
return value; | ||
} | ||
function deleteMapKeyAndReturn(map, key, value) { | ||
@@ -115,6 +157,2 @@ map.delete(key); | ||
} | ||
/** | ||
* A helper function to just not use "=>" closures and thus control, which | ||
* variables will be retained from garbage collection. | ||
*/ | ||
function deleteObjPropAndReturn(obj, key, value) { | ||
@@ -121,0 +159,0 @@ delete obj[key]; |
{ | ||
"name": "fast-typescript-memoize", | ||
"version": "1.1.1", | ||
"version": "1.2.1", | ||
"description": "Fast memoization decorator and other helpers with 1st class support for Promises.", | ||
@@ -5,0 +5,0 @@ "homepage": "https://github.com/dimikot/fast-typescript-memoize#readme", |
@@ -30,2 +30,5 @@ # fast-typescript-memoize: fast memoization decorator and other helpers with 1st class support for Promises | ||
4. Does not support any notion of expiration. | ||
5. When the 1st argument of the method is an object (or when hasher handler | ||
returns an object), it is not retained from GC, so you can memoize on object | ||
args safely, without thinking about memory leaks. | ||
@@ -49,2 +52,7 @@ ```ts | ||
@Memoize() | ||
method1obj(arg: object) { | ||
return count++; | ||
} | ||
@Memoize((arg1, arg2) => `${arg1}#${arg2}`) | ||
@@ -56,3 +64,3 @@ method2(arg1: string, arg2: number) { | ||
@Memoize(function (arg1, arg2) { return `${this.some}:${arg1}#${arg2}`; }) | ||
method3(arg1: string, arg2: number) { | ||
method2this(arg1: string, arg2: number) { | ||
return count++; | ||
@@ -77,2 +85,3 @@ } | ||
const obj = new Class(); | ||
const arg = { my: 42 }; | ||
@@ -86,7 +95,10 @@ obj.method0(); // count is incremented | ||
obj.method1obj(arg); // count is incremented, arg is not retained | ||
obj.method1obj(arg); // count is NOT incremented | ||
obj.method2("abc", 42); // count is incremented | ||
obj.method2("abc", 42); // count is NOT incremented | ||
obj.method3("abc", 42); // count is incremented (strongly typed `this`) | ||
obj.method3("abc", 42); // count is NOT incremented | ||
obj.method2this("abc", 42); // count is incremented (strongly typed `this`) | ||
obj.method2this("abc", 42); // count is NOT incremented | ||
@@ -93,0 +105,0 @@ await asyncMethod("ok"); // count is incremented |
Sorry, the diff of this file is not supported yet
32812
14.55%337
12.71%172
7.5%