Here would be one approach:
const URL = 'https://jsonplaceholder.typicode.com/todos/1';
const notifier = new Subject();
const pending = new BehaviorSubject(false);
const cacheEmpty = Symbol('cache empty')
const shared$ = notifier.pipe(
withLatestFrom(pending),
filter(([_, isPending]) => isPending === false),
switchMap(() => (
console.warn('[FETCHING DATA]'),
pending.next(true),
fetch(URL).then(r => r.json())
)),
tap(() => pending.next(false)),
shareReplay(1, 1000),
);
const src$ = shared$.pipe(
mergeWith(of(cacheEmpty).pipe(delay(0), takeUntil(shared$))),
tap(v => v === cacheEmpty && notifier.next()),
filter(v => v !== cacheEmpty)
)
src$.subscribe(v => console.log('[1]', v));
setTimeout(() => {
src$.subscribe(v => console.log('[2]', v));
}, 500);
setTimeout(() => {
src$.subscribe(v => console.log('[3]', v));
}, 1200);
StackBlitz.
mergeWith
is import { merge as mergeWith } from 'rxjs/operators'
(I think that as of RxJs 7, it will be accessible as mergeWith
directly).
My reasoning was that I needed to find a way to determine whether the cache of the ReplaySubject
in use is empty or not. It's known that if the cache is not empty and a new subscriber arrives, it will send the cached values synchronously.
So,
mergeWith(of(cacheEmpty).pipe(delay(0), takeUntil(shared$))),
is essentially the same as
merge(
shared$,
of(cacheEmpty).pipe(delay(0), takeUntil(shared$)) // #2
)
If there are values in the cache, shared$
will emit and #2
will be unsubscribed to.
If there are no values, #2
will emit and then complete(the fact that it completes won't affect the outer observable).
Next, we see that if cacheEmpty
has been emitted, then we know that it's time to refresh the data.
tap(v => v === cacheEmpty && notifier.next()), // `notifier.next()` -> time to refresh
filter(v => v !== cacheEmpty)
Now, let's have a look at how notifier
works
const shared$ = notifier.pipe(
// These 2 operators + `pending` make sure that if 2 subscribers register one after the other, thus synchronously
// the source won't be subscribed more than needed
withLatestFrom(pending),
filter(([_, isPending]) => isPending === false),
switchMap(() => (
console.warn('[FETCHING DATA]'),
pending.next(true), // If a new subscriber registers while the request is pending, the source won't be requested twice
fetch(URL).then(r => r.json())
)),
// The request has finished, we have the new data
tap(() => pending.next(false)),
shareReplay(1, 1000),
);