RxJS: Caching Observables with a Decorator
Tags: angular, rxjs, typescript, decorator
Published: 2019-09-03 12:51:55


> Edit: The decorator hereby discussed is now available from npm. Install with `npm i @microphi/cache` or `yarn add @microphi/cache` 


I have been chasing this one from few months now.
The following should be pretty familiar to you:

```typescript
@Injectable()
export class UserService {

  constructor(private _client: HttpClient) {}



  public findAll(id: number) {
    return this._client.get(`https://reqres.in/api/users?page=${id}`);
  }
}
```

For some reason you want to cache the response of this request.
If you look online you may find some tips on how to do it and you may end yourself doing the same thing for all the requests that you want to cache.

It happens though that I was a java developer and remember the old good days when a `@Cache` annotation would leverage me from a lot of repeated code.

Well in Typescript we have decorator so there must be a way to do some caching with a simple `@Cache`, right?

My gut feeling was: of course!

But after several attempts with no success I gave up.

Until some days ago when I found this article about [caching and refreshing observable in angular](https://www.prestonlamb.com/blog/rxjs-cache-and-refresh-in-angular) by Preston Lamb which re-ignited my curiosity.

Starting from his stackbliz example I did some experiments
but again without any luck.

Until I've got an intuition: let's make a race.


```typescript
@Injectable()
export class UserService {

  private cached$: ReplaySubject<any> = new ReplaySubject(1, 2500);

  constructor(private _client: HttpClient) {}

  public findAll(id): Observable<any> {

    const req = this._client.get(`https://reqres.in/api/users?page=${id}`).pipe(
      tap((data) => {
        this.cached$.next(data);
      })
    );

    return race(this.cached$, req);
  }

}
```

Et voila'. It worked! Just how I like it: simple and neat. So simple that I don't even need to explain it, right?

Now the thing is that if in your service you've got many of those methods that you need to cache then you'll need to repeat a lot of code. REM: decorator!

### Let's move everything into a decorator

```typescript
export interface CacheOptions {
  ttl: number;
}

export function Cache(options: CacheOptions) {

  return (target: any, propertyKey: string, descriptor) => {

    const originalFunction = descriptor.value;

    target[`${propertyKey}_cached`] = new ReplaySubject(1, options.ttl);

    descriptor.value = function(...args) {

      const req = originalFunction.apply(this, args).pipe(
        tap((response) => {
          this[`${propertyKey}_cached`].next(response);
        })
      );

      return race(this[`${propertyKey}_cached`], req);

    };

    return descriptor;
  };
}
```

What I do here is to initialize a variable named, for example, `findAll_cached` with a replay subject then replace the original function with a new function that will call the original function applying the same logic we saw before.

Then the service will look like the following:

```typescript
@Injectable()
export class UserService {

  constructor(private _client: HttpClient) {}

  @Cache({
    ttl: 2500
  })
  public findAll(id): Observable<any> {
    return this._client.get(`https://reqres.in/api/users?page=${id}`)
  }

}
```

So beautiful!

### Extra points

Now here it comes my friend that says: yo Davide that's cool but if you call that function with a different argument for sure you need to do the http call again. i.e.: different input different output. Right?

Oh right, that's easy:

```typescript
export function Cache(options: CacheOptions) {

  let lastCallArguments: any[] = [];

  return (target: any, propertyKey: string, descriptor) => {

    const originalFunction = descriptor.value;

    target[`${propertyKey}_cached`] = new ReplaySubject(1, options.ttl);

    descriptor.value = function(...args) {

      let argsNotChanged = true;

      for (let i = 0; i < lastCallArguments.length; i++) {
        argsNotChanged = argsNotChanged && lastCallArguments[i] == args[i];
      }

      if (!argsNotChanged) { // args change
        this[`${propertyKey}_cached`] = new ReplaySubject(1, options.ttl);
      }

      lastCallArguments = args;

      const req: Observable<any> = originalFunction.apply(this, args).pipe(
        tap((response) => {
          this[`${propertyKey}_cached`].next(response);
        })
      );

      // despite what the documentation says i can't find that the complete is ever called
      return race(this[`${propertyKey}_cached`], req);

    };

    return descriptor;
  };
}
```


You can play with this code in the following [stackbliz](https://stackblitz.com/edit/rxjs-caching-decorator) and find the complete source code on my [github](https://github.com/microph1/microphi/blob/master/projects/cache/src/lib/cache.decorator.ts).
Please note that the code on github will probably move to another package in the future.


### Caveats
- If the method that we need to cache makes use of the typescript defaulting mechanism. i.e.:

    ```typescript

        public findAll(id: number = 1) {
            ...
        }
    ```

    and then it's called like `service.findAll()` it happens that the `args` variable will be `[]` an empty array as the defaulting takes place only when we call `.apply` so that in the following example no change of arguments it's detected
    ```typescript
    service.findAll()

    service.findAll(2)
    ```

- Let's look at the example in home.component of the forementioned stackbliz example

    ```typescript

        setTimeout(() => {
          this.$log.d('starting subscriber');
          this.userService.findAll(1).subscribe((data) => {
            this.$log.d('starting subscribed');
            this.$log.d(data);
            this.users = data;

          })
        }, 0);

        setTimeout(() => {
          this.$log.d('first subscriber 1 sec later');
          this.userService.findAll(1).subscribe((data) => {
            this.$log.d('first subscribed');
            this.$log.d(data);

          })
        }, 1000);

        setTimeout(() => {
          this.$log.d('second subscriber 2 sec later');
          this.userService.findAll(1).subscribe((data) => {
            this.$log.d('second subscribed');
            this.$log.d(data);

          })
        }, 2000);

        setTimeout(() => {
          this.$log.d('third subscriber 3 sec later, ttl expired. shoult hit the endpoint');
          this.userService.findAll(1).subscribe((data) => {
            this.$log.d('third subscribed');

            this.$log.d(data);

          })
        }, 3000);

        setTimeout(() => {
          this.$log.d('fourth subscriber 4 sec later, argument changed. should hit the endpoint');
          this.userService.findAll(2).subscribe((data) => {

            this.$log.d(' fourth subscribed');

            this.$log.d(data);
          })
        }, 4000);

        setTimeout(() => {
          this.$log.d('fifth subscriber 5 sec later, argument changed. should hit the endpoint');
          this.userService.findAll(1).subscribe((data) => {

            this.$log.d(' fifth subscribed');

            this.$log.d(data);
          })
        }, 5000);
    ```

    which outputs something like the following on console

    ```

    [...]
    third subscriber 3 sec later, ttl expired. shoult hit the endpoint
    arguments are
    [1]
    argsNotChanged
    true
    this actually hit the endpoint
    starting subscribed
    {page: 1, per_page: 6, total: 12, total_pages: 2…}
    first subscribed
    {page: 1, per_page: 6, total: 12, total_pages: 2…}
    second subscribed
    {page: 1, per_page: 6, total: 12, total_pages: 2…}
    third subscribed
    {page: 1, per_page: 6, total: 12, total_pages: 2…}
    [...]
    ```
    As you can see when we subscribe again after the cache is expired all previous subscriptions are notified again.


Thanks for reading so far I hope you enjoyed and remember: if you like this article share it with your friends, if you don't keep it for yourself ;)

(2023/05/03) Edit: Update github repo link