3

I am looking for the best way of implementing an optimization for very expensive method that takes multiple parameters and returns an Observable. Is there an elegant way of doing it?

What I am looking for is prettier version of this:

class Example {

constructor(
       private databaseService: DatabaseService, 
       private someService: SomeService) 

expensive(param1: string, param2: string) : Observable<string> {
    if (isMemoraized(param1,param2) { 
       return Observable.create(observer=>
         observer.next(memorizedValue(param1, param2));
         observer.complete();
    } else {
        return Observable.create(observer=>{
           Observable.forkJoin([
           this.databaseService.getValue(param1, param2),
           this.someService.fetchDataFromServer(param2)].subscribe( 
          results => {
        let result = results[0] + ' ' + results[1];
        memorizeValue([param1,param2], result);
        observer.next(result);
        observer.complete();
        });
        });
    }
}
}

Any help appreciated!

Vamshi
  • 9,194
  • 4
  • 38
  • 54
karruma
  • 768
  • 1
  • 12
  • 32
  • The service should likely implement cache storage instead of basic memoization. Should the results never expire? How many requests are supposed to be stored? What will happen if error response will be momoized? – Estus Flask Aug 20 '17 at 01:06
  • Yes - I prefer cache storage over basic memoization and preferably a package that will have everything including expiration times, methods to clear the cache and so on... In terms of number, possibly hundreds. – karruma Aug 20 '17 at 07:07
  • I would suggest something like https://github.com/jmdobry/CacheFactory then. As for cache key, it could be something like `JSON.stringify([param1, param2])`. – Estus Flask Aug 20 '17 at 10:46
  • Thanks - this simplifies it - would you know any caching library that would support Observables? – karruma Aug 20 '17 at 13:47
  • You don't really need them to be supported. It's just a storage, you pick objects from it and return them with Observable.of. – Estus Flask Aug 20 '17 at 14:02

4 Answers4

4

You can create a decorator to memoize the results in run-time for every decorated function:

function Memoized() {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const method = descriptor.value; // references the method being decorated
    let cacheMember = propertyKey + "CacheMember";

    // the Observable function
    if (!descriptor.value) {
      throw new Error("use MemoizeDecorator only on services methods");
    }

    descriptor.value = function(...args) {
      if (!target[cacheMember]) {
        let returnedObservable = method.apply(this, args);
        if (!(returnedObservable instanceof Observable)) {
          throw new Error(
            `method decorated with Memoized Decorator must return Observable`
          );
        }

        target[cacheMember] = returnedObservable.pipe(
          publishReplay(),
          refCount()
        );
      }

      return target[cacheMember];
    };
  };
}

Usage:

@Memoized()
expensive(param1: string, param2: string) : Observable<string> {
// ...the expensive task
}

Warning! Decorators are stage 2 proposal of js! Don't use decorators without transpiling your code (Typescript supports it completely)

Eylon Sultan
  • 936
  • 9
  • 16
1

There are a number of memoization packages available on NPM. For TypeScript, I'd recommend typescript-memoize, which will provide you with a decorator you can use to memoize your methods.

For example:

import {Memoize} from 'typescript-memoize';

class Example {

    @Memoize((param1: string, param2: string) => {
        return param1 + ';' + param2;
    })
    expensive(param1: string, param2: string) : Observable<string> {
        // ...
    }
}
fny
  • 31,255
  • 16
  • 96
  • 127
  • 1
    This doesn't seem to be working when a returned type is Observable. I have written some tests around it and it doesn't work. – karruma Aug 20 '17 at 08:44
  • Maybe it would be better to separate the expensive operations (i.e. db retrieval) into helper functions that you can memoize? – fny Aug 20 '17 at 14:38
1

If you are not willing to use any library and write your own code. I can refactor your code to this :

expensive(param1: string, param2: string) : Observable<string> {
    return isMemoraized(param1, param2)
        ? Observable.of(memorizedValue(param1, param2))
        : Observable.forkJoin([
            this.databaseService.getValue(param1, param2),
            this.someService.fetchDataFromServer(param2)
          ])
          .map(results => results[0] +results[1])
          .do( memorizeValue(result);

}
Vamshi
  • 9,194
  • 4
  • 38
  • 54
1

You could use localStorage to keep the results of your expensive operation, indexed by a hash of the two parameters. My solution below also implements expiration to avoid using stale results.

/**
 * Gets the key to be used to store result. Each key should be unique 
 * to the parameters supplied,
 * and the same parameters should always yield the same key
 * @return {string}
 */
getKey(param1, param2){
  return `${param1}__${param2}`;
}

/**
 * Stores results in localStorage and sets expiration date into the future
 */
store(param1, param2, result, secsToExpire){
  let toStore = {
    data: result,
    expires: Math.floor(Date.now() / 1000) + secsToExpire
  };
  localStorage.setItem(this.getKey(param1,param2), JSON.stringify(toStore));
}

/**
 * Gets result from storage. If result is stale (expired) or unavailable,
 * returns NULL
 * @return {string|null}
 */
retrieve(param1, param2){
  let result = localStorage.getItem(getKey(param1,param2));
  if(!result==null) result = JSON.parse(result);
  if(result==null || result.expires < Math.floor(Date.now() / 1000)){
    return null;
  }
  return result.data;
}

/**
 * Gets result from localStorage if available. Else, fetch from server
 * and store before returning an Observable that will emit the result
 * @return {Observable<string>}
 */
expensive(param1, param2):Observable<string>{
  let result = this.retrieve(param1,param2);
  if(result) return Observable.of(result);

  // zip will match up outputs into an array
  return Observable.zip(
    this.databaseService.getValue(param1, param2),
    this.someService.fetchDataFromServer(param2)
  ) // take ensures completion after 1 result.
    .take(1).map(e => e[0] + ' ' + e[1])
    // store in localStorage
    .do(res => this.store(param1,param2, res))
}
BeetleJuice
  • 39,516
  • 19
  • 105
  • 165