All files / app/util memo.ts

96.87% Statements 31/32
94.44% Branches 17/18
100% Functions 7/7
96.77% Lines 30/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102169x                                                                                   19080x   17838x 1242x   1242x       1242x       17838x 17838x   17838x 1112x 1112x 639x   1112x         1242x 160x 160x   160x 101x   160x       169x 169x     1277x 123x   1277x       87x 52x 1x 51x 1x   50x          
/**
 * Memoization Decorator (@memo)
 *
 * This file implements the @memo decorator, used for memoizing expensive computations in
 * class getters and methods that accept a single string argument. It is particularly effective
 * in Angular applications where getters are used in templates for readability but can lead to
 * performance issues due to repeated evaluations during Angular's change detection cycles.
 *
 * Primary Use:
 * - Ideal for getters in Angular components that are referenced in templates and perform calculations.
 * - Using @memo on such getters caches the result, reducing the performance overhead
 *   of Angular's change detection.
 * - Best suited for getters with computational logic, rather than simple property access.
 *
 * Quick Start:
 * - Apply @memo to a class getter or a method that takes a single string argument.
 * - For getters, @memo caches the result of the first call and returns this cached value on subsequent calls.
 * - For methods, @memo caches results based on the string argument value.
 *
 * Example Usage:
 * class MyClass {
 *   @memo
 *   get expensiveComputation() {
 *     // Expensive calculation or logic here
 *   }
 *
 *   @memo
 *   computeSomething(arg: string) {
 *     // Function logic here
 *   }
 * }
 *
 * Caveats:
 * - @memo is only suitable for getters and methods with a single string argument.
 * - It's not designed for async functions or methods with multiple or non-string arguments.
 * - Cached values persist for the lifetime of the class instance, unless explicitly cleared.
 * - Use MemoCache.clear(instance) to clear cached values for a specific class instance.
 *
 * This implementation aims to provide a simple and effective way to optimize performance
 * by avoiding unnecessary recalculations, especially in data-intensive or CPU-heavy applications.
 */
export function memo(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  if (descriptor.get) {
    // Handle as a getter
    return memoizeGetter(descriptor, propertyKey);
  } else if (descriptor.value && typeof descriptor.value === 'function') {
    // Handle as a function with a single string argument
    descriptor.value = memoizeFunction(descriptor.value, propertyKey);
  } else E{
    console.warn(`@Memo is applied to '${propertyKey}', which is neither a getter nor a suitable function.`);
  }
  return descriptor;
}
 
function memoizeGetter(descriptor: PropertyDescriptor, propertyKey: string) {
  const originalMethod = descriptor.get!;
  const cacheKey = Symbol.for(`__cache__${propertyKey}`);
 
  descriptor.get = function () {
    const classCache = MemoCache.getCache(this);
    if (!(cacheKey in classCache)) {
      classCache[cacheKey] = originalMethod.apply(this);
    }
    return classCache[cacheKey];
  };
}
 
function memoizeFunction<T extends Object>(originalFunction: (this: T, arg: string) => any, propertyKey: string) {
  return function(this: T, arg: string) {
    const cacheKey = Symbol.for(`__cache__${propertyKey}_${arg}`);
    const classCache = MemoCache.getCache(this);
 
    if (!(cacheKey in classCache)) {
      classCache[cacheKey] = originalFunction.call(this, arg);
    }
    return classCache[cacheKey];
  }
}
 
export class MemoCache {
  private static cacheMap = new WeakMap<Object, any>();
 
  public static getCache(instance: Object) {
    if (!this.cacheMap.has(instance)) {
      this.cacheMap.set(instance, {});
    }
    return this.cacheMap.get(instance);
  }
 
  public static clear(instance: Object, propertyKey?: string, arg?: string) {
    if (this.cacheMap.has(instance)) {
      if (arg) {
        delete this.cacheMap.get(instance)[Symbol.for(`__cache__${propertyKey}_${arg}`)];
      } else if (propertyKey) {
        delete this.cacheMap.get(instance)[Symbol.for(`__cache__${propertyKey}`)];
      } else {
        this.cacheMap.set(instance, {});
      }
    }
  }
}