How to cache results in TypeScript

This is an example of a caching mechanism that I added to an Angular 2+ page.

Problem

At a certain point, during the development of a visual calculator, I noticed that the computation of results corresponding to moving sliders on the page had become much slower than before.

Solution

To mitigate the problem I introduced a caching mechanism based on:

  1. selecting slow methods, whose results we’re going to cache;
  2. wrapping those methods into a check:
  • if there is a cached result, use it
  • otherwise, compute it now, save it, then use it;
  1. deleting cached results as soon as they become stale.

A nice thing about this cache mechanism is that it’s standard.

  • We don’t need to change the code of the app.
  • A new cached result is computed only when unavailable.
  • Cached results become unavailable after certain events.

A nice thing of my implementation is that it automatically computes which cached values to invalidate, based on their dependencies lists.

  • This means that we only have to define the dependencies lists, which is very easy. Just look at the method’s definition and list all of its inputs (params, function calls, and contextual values).
escalivada(): number {
  return (this.escabeche() - this.gazpacho.value) / this.paella();
}

Dependencies: escabeche, gazpacho, paella.

Code

import * as _ from 'lodash';

export class CachedResults {

    private cache: any = {};

    private invalidations: any;

    static getPathsOfTheTransitiveClosure(edges) {
        // TODO check for dependency loops and throw an error
        const result = [];
        _.forEach(edges, edge => {
            let [from,] = edge;
            let path = [from];
            let found = edge;
            while (found) {
                let [, to] = found;
                path.push(to);
                found = edges.find(([x,]) => x === to);
            }
            result.push(path);
        });
        return result;
    }

    constructor(
        private definitions: any,
    ) {
        this.invalidations = this.getInvalidations();
    }

    use(methodName, fn) {
        if (!this.cache[methodName]) {
            this.cache[methodName] = fn(...args);
        }
        return this.cache[methodName];
    }

    invalidate(changed) {
        const invalidCache = this.invalidations[changed];
        invalidCache.forEach(x => { delete this.cache[x] });
    }

    private getInvalidations() {
        const arcs = this.getArcsFromInputToComputed();
        const paths = CachedResults.getPathsOfTheTransitiveClosure(arcs);
        return this.getPathsFromRoots(paths);
    }

    private getComputedValues() {
        return Object.keys(this.definitions);
    }

    private getArcsFromInputToComputed() {
        const result = [];
        _.forEach(this.definitions, (inputs, computed) => {
            _.forEach(inputs, input => {
                result.push([input, computed]);
            });
        });
        return result;
    }

    private getRootValues() {
        const allInputs = [];
        _.forEach(this.definitions, inputs => {
            allInputs.push(...inputs);
        });
        const computed = this.getComputedValues();
        const origins = _.difference(allInputs, computed);
        return _.uniq(origins);
    }

    private getPathsFromRoots(paths) {
        const roots = this.getRootValues();
        const result = {};
        roots.forEach(origin => {
            const path = paths.find(([x,]) => x === origin);
            const [, ...rest] = path;
            result[origin] = rest;
        });
        return result;
    }

}

Usage

import { CachedResults } from '...';
//...
cache: CachedResults;
//...
constructor() {
    this.cache = new CachedResults({
        paella: 'arrozNegre'.split(' '),
        escabeche: 'paella arrozCubana'.split(' '),
        arrozCubana: 'gachas arrozNegre gazpacho'.split(' '),
    });
}
//...
escalivada(): number {
    return this.cache.use('escalivada', () => {
        return (this.escabeche() - this.gazpacho.value) / this.paella();
    });
}
//...
<input id="arrozNegre" max="30" min="5" step="1" type="range" />

How canActivate works for multiple guards

Angular 2 lets developers forbid routes activation by means of a list of guards assigned to the canActivate property of a route definition.

{ 
  path: 'vip-lounge', 
  component: 'VipLoungeComponent', 
  canActivate: [ IfUserIsLoggedIn, 
    AndSpendsEnough,
    AndMembershipIsCurrent ]
}

Those guards are classes which implement the CanActivate interface, and the implementation can be a promise, for example when it entails a roundtrip to the server.

The way Angular 2 treats this list of guards is by applying .every(...) to the values of a merge-mapped list of observables.

  checkGuards(): Observable<boolean> {
    if (this.checks.length === 0) return of (true);
    const checks$ = from(this.checks);
    const runningChecks$ = mergeMap.call(checks$, (s: any) => {
      if (s instanceof CanActivate) {
        return andObservables(
            from([this.runCanActivateChild(s.path), this.runCanActivate(s.route)]));
      } else if (s instanceof CanDeactivate) {
        // workaround https://github.com/Microsoft/TypeScript/issues/7271
        const s2 = s as CanDeactivate;
        return this.runCanDeactivate(s2.component, s2.route);
      } else {
        throw new Error('Cannot be reached');
      }
    });
    return every.call(runningChecks$, (result: any) => result === true);
  }

The list of guards is conceptually the same as a list of promises, and in fact all the guards could be promises because, as we see above, .from(...) is used to initialise check$.

Merge-mapping is exactly what we did in How to convert promises to an observable.

var source = Rx.Observable.from(promises.map(function (promise) { 
  return Rx.Observable.from(promise); 
})).mergeAll(); 
/* 
 
true 
"Next: true" 
true 
"Next: true" 
true 
"Next: true" 
false 
"Next: false" 
true 
"Next: true" 
"Completed" 
 
*/

var source = Rx.Observable.from(promises).mergeMap(function (promise) {
  return Rx.Observable.from(promise);
});
/* 
 
true 
"Next: true" 
true 
"Next: true" 
true 
"Next: true" 
false 
"Next: false" 
true 
"Next: true" 
"Completed" 
 
*/

If we now apply .every(...) to that merge-mapped list of promises, we can see how canActivate works for multiple guards.

var source = Rx.Observable
  .from(promises)
  .mergeMap(promise => Rx.Observable.from(promise))
  .every(value => value === true);
/*

true
true
true
false
"Next: false"
"Completed"
true

*/

Notice that the subscription is notified only once, as soon as the resolved promises make it clear what the result would be: the observable emits a false soon after getting the first false, or a true after getting all true-s. So, as expected, canActivate works as a short-circuit-ed AND of each guard of the list, according to the arrival order of values.

Also notice that the values of all the promises keep arriving as promises get resolved, even after the observable has completed. This means that guards intended to be used together in a list of guards should never have side effects (to avoid race conditions). If guard G1 had a side effect of activating route R1, and G2 had another for R2, then canActivate: [G1, G2] would unpredictably activate R1 or R2, depending on which guard completes later.