2

In my Angular 2 app, I have implemented a simple service cache using promises, and it works fine. However, I am not yet satisfied with my implementation:

export class Cache<T extends Cacheable<T, I>, I> {

    items: T[];

    init(items: T[]): void {
        this.items = items;
    }

    isInitialised(): boolean {
        return this.items != null;
    }

    all(): Promise<T[]> {
        return Promise.resolve(this.items);
    }

    get(id: I): Promise<T> {
        return Promise.resolve(this.items.find(item => item.id == id));
    }

    put(item: T): void {
        this.items.push(item);
    }

    update(item: T): void {
        this.get(item.id)
            .then(cached => cached.copy(item));
    }

    remove(id: I): void {
        let index = this.items.findIndex(item => item.id == id);
        if (index > -1) {
            this.items.splice(index, 1);
        }
    }
}

I would like the cache to initialise no matter which method is called, whether it is .all, .get etc. But, the key is that I want the first call to this.items to force the cache to initialise. Any subsequent call should somehow wait until the cache is initialised before manipulating ´this.items´.

To do so, I thought of using a proxy method .itemsProxy() which would return a promise, and replace any call to .items with that method and adapt code consequently. This would require having a cache state which is either NOT_INITIALSED, INITIALSING or INITIALSED.

The proxy method would look like this:

export class Cache<T extends Cacheable<T, I>, I> {

  constructor(private service: AbstractService) {}

  initPromise: Promise<T[]>;
  state: NOT_INITIALISED;

  itemsProxy(): Promise<T> {
    if (this.state == INITIALISED)
      return Promise.resolve(this.items);

    if (this.state == NOT_INITIALISED) {
      this.initPromise = this.service.all().then(items => {
        // Avoid override when multiple subscribers
        if (this.state != INITIALISED) {
          this.items = items;
          this.state = INITIALISED;
        }

        return this.items;
      });
      this.state = INITIALISING;
    }

    return this.initPromise;
  }

  ...

}

Notice how initialisation is done via the this.service.all(), which looks like this:

all(): Promise<T[]> {
    if (this.cache.isInitialised()) {
        return this.cache.all();
    }

    return this.http
        .get(...)
        .toPromise()
        .then(response => {
            this.cache.init(
              response.json().data.map(item => new T(item))
            );
            return this.cache.all();
        }); // No catch for demo
}

Note that new T(...) is not valid, but I simplified the code for demo purposes.

The above solution does not seem to work like I'd expect it to, and asynchronous calls are hard to debug.

I thought of making this.items an Observable, where each call would subscribe to it. However, my understanding of observables is quite limited: I don't know if a subscriber can modify the data of the observable it is subscribed to (i.e. are observables mutable like a T[] ?). In addition, it seems to me that observables "produce" results, so after the cache is initialised, would any new subscriber be able to access the data produced by the observable ?

How would you implement this kind of "synchronization" mechanism ?

J. Doe
  • 201
  • 1
  • 3
  • 12
  • is your cache "clearable" ? – n00dl3 Jan 23 '17 at 08:46
  • Not yet, I guess I would implement that in a later version, but I don't need it at this time. – J. Doe Jan 23 '17 at 09:01
  • If you don't clean you only need to `if(!this.obs) this.obs= http.get().publishReplay(1).refCount(); return this.obs;`. No need for external service. – n00dl3 Jan 23 '17 at 09:21
  • It isn't clear why promises are used in the first place. If you're trying to store a promise as an item, it may be not a good idea. – Estus Flask Jan 23 '17 at 09:21
  • @n00d3 Caching support in Rxjs has been [dropped](https://github.com/ReactiveX/rxjs/pull/2012) in versions 5+. Plus, assume I used this mechanism. In my understanding of `.publishRepla(1).refCount()`, calls to `.all` would be cached, but calls to `.get` will not be using values from the cache used by `.all`. So, If you call `.all()`, then call `.get()`, the call to `.get` will fetch data from the server, because it's not aware that `.all()` already cached that data. – J. Doe Jan 23 '17 at 09:56
  • @estus The promise is not the item. Promises are used to "proxy" calls to the actual items array, in order to introduce asynchronicity – J. Doe Jan 23 '17 at 10:07
  • Why asynchronicity should be introduced? If the service is synchronous (and it looks like it is), promises there just make everything more complicated. This may be XY problem, and the final goal is not clear. – Estus Flask Jan 23 '17 at 10:13
  • I may be wrong, be the service uses Angular http service, which I think is not synchronous since it uses Observables ? Or are Observables synchronous ? Other than that, the goal is to have a mutable cache (I can get, put, update, delete from the items array), but make sure that before any method uses the array of items, it is filled with all the data from the backend. – J. Doe Jan 23 '17 at 10:35
  • I don't talk about the `cache` operator, and I use the same observable for every `all ()` call... – n00dl3 Jan 23 '17 at 11:12
  • Can `.all` and `.get` share the same cache ? Could you post an answer with an example ? PS: I know you're not talking about it, but the `.cache` was introduced as a shortcut of `.publishReplay().refCount()`, so I guess that since they have dropped `.cache`, they're assuming that the lib does not have sufficient support for caching, hence why I would tend to think that `.publishReplay` might not be the best approach. I would love a plunker to see this behave. – J. Doe Jan 23 '17 at 11:21
  • 1
    See also http://stackoverflow.com/questions/36271899/what-is-the-correct-way-to-share-the-result-of-an-angular-2-http-network-call-in – Günter Zöchbauer Jan 23 '17 at 11:27
  • @Gunter Zochbauer seems like we've got a good solution. Not too far from what I had tried :) I will try to implement it in my use case and will report when done, so you can post a complete answer and I can accept it as solution. – J. Doe Jan 23 '17 at 12:22

0 Answers0