6

I'm trying to achieve what is described here: https://www.prestonlamb.com/blog/rxjs-cache-and-refresh-in-angular

In other words, I want to cache an observable, during a given time (let's say 1minute). When a subscription is made after that given time, data should be retrieved again and cache again for 1 minute.

Example of the expected result:

T 00:00: Request (1) => RETRIEVE data
T 00:10: Request (2) => data from cache
T 00:35: Request (3) => data from cache
T 00:50: Request (4) => data from cache
T 01:10: Request (5) => RETRIEVE data
T 01:15: Request (6) => data from cache
T 01:30: Request (7) => data from cache
T 02:30: Request (8) => RETRIEVE data

The shareReplay operator is working fine to cache data for a given time, but I'm not able to re-launch it when that given time is elapsed.

Example using shareRelay(1, 1000) operator:

T 00:00: Request (1) => RETRIEVE data
T 00:10: Request (2) => data from cache
T 00:35: Request (3) => data from cache
T 00:50: Request (4) => data from cache
T 01:10: Request (5) => no response
T 01:15: Request (6) => no response
T 01:30: Request (7) => no response
T 02:30: Request (8) => no response

The link above try to change that behavior using first operator catching null results. Unfortunately, it's not working fine as data is not cached after the first time.

Here's what I've got using the article of the above link (following picture is describing the code used)

code details

Result I've got:

T 00:00: Request (1) => RETRIEVE data
T 00:10: Request (2) => data from cache
T 00:35: Request (3) => data from cache
T 00:50: Request (4) => data from cache
T 01:10: Request (5) => RETRIEVE data
T 01:15: Request (6) => RETRIEVE data
T 01:30: Request (7) => RETRIEVE data
T 02:30: Request (8) => RETRIEVE data

I've also seen some example with the timer operator, but in that cases, data is retrieved every minute, even if there is no subscribtion on it. I do not want to refresh data every minute, I want to expire cache every minute. Unfortunately, I've lost the code with the timer operator, but the result was something like that:

Result with timer operator:

T 00:00: Request (1) => RETRIEVE data
T 00:10: Request (2) => data from cache
T 00:35: Request (3) => data from cache
T 00:50: Request (4) => data from cache
T 01:00: NO REQUEST => RETRIEVE data
T 01:10: Request (5) => data from cache
T 01:15: Request (6) => data from cache
T 01:30: Request (7) => data from cache
T 02:00: NO REQUEST => RETRIEVE data
T 02:30: Request (8) => data from cache

Anyone with a "pure" RxJS solution to do what I want?

Sébastien BATEZAT
  • 2,353
  • 26
  • 42
  • You said you do not want to refresh data every minute but expire cache every minute.. what does it mean. Can you make it more clear ? – YogendraR Jun 03 '20 at 07:29
  • The last example show exactly what I mean. If there is no request after 1minute, cache is clear but data is not retrieved automatically (i.e : no request at 01:00). The idea is to have the first scenario working, and not the last one. – Sébastien BATEZAT Jun 03 '20 at 07:41

3 Answers3

2

I think that solution you provided a link for has a small bug, as I tried to highlight in this StackBlitz. (or I might have misunderstood the idea)

You could try this:

const refetchSbj = new Subject();
const refetchData$ = refetchSbj.pipe(
    switchMap(() => service.fetchData())
  ).pipe(share());

merge(
  src$,
  refetchData$
).pipe(
  shareReplay(1, 1000),
  buffer(concat(timer(0), refetchData$)),
  tap(values => !values.length && refetchSbj.next()),
  filter(values => values.length !== 0),
  // In case there is only one value,
  map(([v]) => v),
  // Might want to add this, because each subscriber will receive the value emitted by the `shareReplay`
  take(1)
)

shareReplay internally uses a ReplaySubject, which emits all the cached values synchronously to a new subscriber. timer(0) is similar to setTimeout(fn, 0), but the important aspect here is that it's asynchronous, which allows buffer to collect the values emitted by the ReplaySubject.

buffer(concat(timer(0), refetchData$)), - we want to make sure that the inner observable provided to buffer does not complete, otherwise the entire stream would complete. refetchData$ will emit the newly fetched data when it's the case(we'll see when a bit later).

tap(values => !values.length && refetchSbj.next()) - if no values were emitted, it means that the ReplaySubject in use does not have any values, which means that time has passed. If that's the case, with the help of refetchSbj, we can repopulate the cache.


So this is how we could visualize the flow:

T 00:00: Request (1) => RETRIEVE data
1) `refetchSbj.next()`
2) shareReplay will send the value resulted from `service.fetchData()` to the subscriber
3) the newly fetched value will be added to the `buffer`, and then the `refetchData$` from `concat(timer(0), refetchData$)` will emit(this is why we've used `share()`), meaning that `values` will not be an empty array
4) take(1) is reached, the value will be sent to the subscriber and then it will complete, so the `ReplaySubject` from `shareReplay()` will have no subscribers.

T 00:10: Request (2) => data from cache
`values` will not be empty, so `refetchSbj` won't emit and `take(1)` will be reached

T 00:35: Request (3) => data from cache
T 00:50: Request (4) => data from cache
T 01:10: Request (5) => RETRIEVE data
Same as `Request (1)`
T 01:15: Request (6) => data from cache
T 01:30: Request (7) => data from cache
T 02:30: Request (8) => RETRIEVE data
Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31
2

The Observable const shared$ = data$.pipe(shareReplay(1, 1000)) will cache a value for 1000 ms. After that time it will only send complete notifications to future subscribers (if data$ completed). By checking if shared$ completed you know that the cache expired and you have to create a new shared$ Observable for the current and future subscribers to use.

You can use a higher order Observable to supply those shared$ Observables to your subscribers.

const createShared = () => data$.pipe(shareReplay(1, 1000))
const sharedSupplier = new BehaviorSubject(createShared())

const cache = sharedSupplier.pipe(
  concatMap(shared$ => shared$.pipe(
    tap({ complete: () => sharedSupplier.next(createShared()) }),
  )),
  take(1) // emit once and complete so subscribers don't receive values from newly created shared Observables
)

https://stackblitz.com/edit/ggasua-2ntq7n


Using the operators from the image you posted, you could also do:

const cache = sharedSupplier.pipe(
  concatMap(shared$ => shared$.pipe(
    first(null, defer(() => (sharedSupplier.next(createShared()), EMPTY))),
    mergeMap(d => isObservable(d) ? d : of(d))
  )),
  take(1)
)

But this is more code and the outcome is the same.

frido
  • 13,065
  • 5
  • 42
  • 56
0

I would consider the following strategy.

First of all you create a function createCachedSource which returns an Observable with its cache implemented via shareReplay(1).

Then I would use this function to set a variable source$ which is the one that clients have to use to subscribe to get data.

Now the trick is to reset source$ at the desired interval, using again createCachedSource.

All these concepts in code look like this

// this function created an Observable cached via shareReplay(1)
const createCachedSource = () =>
  of(1).pipe(
    tap((d) => console.log(`Go out and fetch ${d}`)),
    shareReplay(1),
    tap((d) => console.log(`Return cached data ${d}`))
  );

// source$ is the Observable the clients must subscribe to
let source$ = createCachedSource();

// every 1 sec reset the cache by creating a new Observable and setting it as value of source$
interval(1000)
  .pipe(
    take(10),
    tap(() => (source$ = createCachedSource()))
  )
  .subscribe();

// here we simulate 30 subscriptions to source$, one every 300 mS
interval(300)
  .pipe(
    take(30),
    switchMap(() => source$)
  )
  .subscribe();
Picci
  • 16,775
  • 13
  • 70
  • 113
  • Thanks for the answer. I don't like the idea to subscribe to another observer. That's why I'll not accept this answer and be focused on the others. Thanks anyway for the try! – Sébastien BATEZAT Jun 10 '20 at 07:52