UNPKG

fast-typescript-memoize

Version:

Fast memoization decorator and other helpers with 1st class support for Promises.

156 lines 6.37 kB
"use strict"; 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