Rxjs shareReplay with invalidation (timeout) - rxjs

In Rxjs imagine a stream that fetches some data and then uses shareReplay to hold the latest value for optimization purposes.
const value = fetchData().pipe(
shareReplay(1)
)
But what if this value can expire in which case (and only in that case) it should be re-fetched.
const value = fetchData().pipe(
shareReplay(1),
switchMap((value) => isExpired(value) ? fetchData() : of(value))
)
This isn't quite right for after the first re-fetch the value is no longer shared and would be re-fetched every time.
How could this functionality of "hold latest value" and "invalidation" be expressed in Rxjs?

You could do the following:
const value$ = fetchData().pipe(
expand(res => timer(res.validForMs).pipe(switchMap(() => fetchData()))),
shareReplay(1)
);
This way, the cache invalidation happens before the shareReplay and every subscriber will also get latest value.
I've made a simple example to demonstrate:
// just mocks
// ---------------------------
const fetchData = () =>
of({
token: uuid(),
validForMs: 2000
});
// ---------------------------
const value$ = fetchData().pipe(
expand(res => timer(res.validForMs).pipe(switchMap(() => fetchData()))),
shareReplay(1)
);
merge(
value$.pipe(
tap(res => console.log(`Subscriber 1 received token ${res.token}`))
),
value$.pipe(
tap(res => console.log(`Subscriber 2 received token ${res.token}`))
),
value$.pipe(
tap(res => console.log(`Subscriber 3 received token ${res.token}`))
)
).subscribe();
And the output is:
Subscriber 1 received token f7facb6f-2c16-4b0f-abc7-53e5c7461778
Subscriber 2 received token f7facb6f-2c16-4b0f-abc7-53e5c7461778
Subscriber 3 received token f7facb6f-2c16-4b0f-abc7-53e5c7461778
Subscriber 1 received token 67419f6a-65ff-482d-9ede-c38b1405e31e
Subscriber 2 received token 67419f6a-65ff-482d-9ede-c38b1405e31e
Subscriber 3 received token 67419f6a-65ff-482d-9ede-c38b1405e31e
Subscriber 1 received token b978b439-3ae5-440e-9d96-9f320db6e051
Subscriber 2 received token b978b439-3ae5-440e-9d96-9f320db6e051
Subscriber 3 received token b978b439-3ae5-440e-9d96-9f320db6e051
Demo here https://stackblitz.com/edit/rxjs-kqybmd

Well, just keep a variable in place: const val = fetchData().pipe(shareReplay(1));
Then:
val.pipe(switchMap((value) => isExpired(value) ? val : of(value))).

Related

Why does using share block an observable using withLatestFrom?

Problem
I have the following operators:
const prepare = (value$: Observable<string>) =>
value$.pipe(
tap((x) => console.log("prepare: ", x)),
share()
);
const performTaskA = (removed$: Observable<string>) =>
removed$.pipe(tap((x) => console.log("taskA: ", x)));
const performTaskB = (removed$: Observable<string>) =>
removed$.pipe(
tap((x) => console.log("taskB 1: ", x)),
withLatestFrom(otherValue$),
tap((x) => console.log("taskB 2: ", x))
);
and I call them like this:
const prepared$ = value$.pipe(prepare);
const taskADone$ = prepared$.pipe(performTaskA);
const taskBDone$ = prepared$.pipe(performTaskB);
merge(taskADone$, taskBDone$).subscribe();
resulting in the following output:
prepare: TEST
taskA: TEST
taskB 1: TEST
Note that taskB 2 has not being logged - it appears the taskBDone observable has stalled at the withLatestFrom(otherValue$) in performTaskB.
If the share in prepare is removed, the observable does not stall, but it (unsurprisingly) results in prepare executing twice, which I do not want.
Questions
How can I execute both performTaskA and performTaskB but prepare only once?
Given the debug explanation below, why does share cause the change in emit sequence?
Demo
With share (as above): https://codesandbox.io/s/so-share-with-latest-from-with-share-rtyex?file=/src/index.ts:663-853
Without share: https://codesandbox.io/s/so-share-with-latest-from-no-share-p702e
Go to Tests tab on right, ensure the Console is visible and click the Play button.
Partial Explanation
Debugging withLatestFrom it is evident that when the source(removed$) emits, ready is false here which prevents emission.
This occurs because when share is present, the input(otherValue$) subscription emits after the source, so ready has not yet being set. (Or is it that share has caused the source to emit earlier?)
But when share is removed, the input subscription emits before the source has, meaning ready is set true via here and here, and therefore withLatestFrom emits as expected.
I tried running your code and I couldn't get it to hang the way yours does. I suspect it's something about how your testing framework manages observables. I can't replicate it.
I did notice that there are some interleaving/ordering issues inherent with what you've written. Sharing synchronous observables comes with issues such as the first observer synchronously observing all the source values and its completion before the second observer can subscribe. This happens even though they should be subscribed "at the same time".
Ordering with the Event Loop
What I've written here works for me:
function init(
value$: Observable<string>,
otherValue$: Observable<string>
) {
const prepare = pipe(
tap(x => console.log("prepare: ", x)),
delay(0),
share()
);
const performTaskA = pipe(
tap(x => console.log("taskA: ", x))
);
const performTaskB = pipe(
tap(x => console.log("taskB 1: ", x)),
withLatestFrom(otherValue$),
tap(x => console.log("taskB 2: ", x))
);
const prepared$ = value$.pipe(prepare);
merge(
prepared$.pipe(performTaskA),
prepared$.pipe(performTaskB)
).subscribe();
}
init(of("a"), of("b"));
For me, this prints this to the console:
prepare: a
taskA: a
taskB 1: a
taskB 2: ["a","b"]
You'll notice a call to delay(0) within prepare. Without that, prepare is called twice since the shared observable has synchronously completed. Delay(0) just puts the next call on the event queue as soon as possible.
That's not the best solution. It's a hack. The best solution depends on how this is used. Most of the time, shareReplay(1) does the job. If you use that here, that will work.
Ordering with Publish/Connect
Otherwise, you can publish and connect to insure the order is what you'd expect.
Publish/Connect lets you set up all your subscriptions to the source before the source is officially started/connected/subscribed to. This ensures that under the hood, all the callbacks and are in place before anything happens. That's the only way to ensure that a fully synchronous observable can share its values.
function init(
value$: Observable<string>,
otherValue$: Observable<string>
) {
const prepare = pipe(
tap(x => console.log("prepare: ", x)),
share()
);
const performTaskA = pipe(
tap(x => console.log("taskA: ", x))
);
const performTaskB = pipe(
tap(x => console.log("taskB 1: ", x)),
withLatestFrom(otherValue$),
tap(x => console.log("taskB 2: ", x))
);
const prepared$ = publish()(value$.pipe(prepare));
merge(
prepared$.pipe(performTaskA),
prepared$.pipe(performTaskB)
).subscribe();
prepared$.connect();
}

Repeat the previous observable

I have a function that returns a promise. I want to catch error on this and then exponentially wait longer and retry. But for now I am just waiting a constant of 5s. Here is my code:
from(connectSocket()).pipe(
catchError(e => {
console.log('got error!', e.message);
return timer(5000).pipe(
tap(() => console.log('will repeat connect now')),
repeat(1)
);
}),
tap(() => setIsConnected.next(true))
);
However it does not repeat.
My full code is in this sandbox - https://stackblitz.com/edit/rxjs-g7msgv?file=index.ts
To make an exponential retry you'll need to use retryWhen. Heres an example:
// will retry with
// 0, 10, 40, 90, 160 ms delays
retryWhen(error$ =>
error$.pipe(
delayWhen((_, i) => timer(i * i * 10))
)
)
Try this code in a playground
In your example you're repeating timer, instead of retrying from(connectSocket()). So substitute catchError with a retryWhen to get what you need.
Hope this helps
--
Also, there are 3rd party tools to add exponential backoff, e.g.
https://github.com/alex-okrushko/backoff-rxjs
See my "Error handling in RxJS" article to get better understanding of errors and retries in RxJS.

Throttle service with multiple, independant callers

I want to ensure that a given API call is throttled so that for a given time interval, only a single request is fired, and that the other, throttled requests wait and receive the results of the request that was actively fired
Example
const generateReport = (args) => client.get(...)
const generateReportA = (argsForA) =>
generateReport(argsForA).then(saveReportSomewhere)
const generateReportB = (argsForB) =>
generateReport(argsForB).then(saveReportSomewhere)
const generateReportC = (argsForC) =>
generateReport(argsForC).then(saveReportSomewhere)
If we then run the statements below
generateReportA(...).then(console.log) // should log result of C
generateReportB(...).then(console.log) // should log result of C
generateReportC(...).then(console.log) // should log result
right after each other, I only want to fire the request associated with generateReportC and I'd like both generateReportA and generateReportB to receive and handle the result of generateReportC.
In the end generateReport should have been called once and saveReportSomewhere should have been called 3 times, each with the result from generateReportC
Is this possible?
This will fire C request get result and save it, then trigger A and B at the same time with result from C and save results immediately after every emit.
generateReportC.pipe(
tap(cResult => saveReportSomewhere(cResult)),
mergeMap(cResult => merge(generateReportA(cResult), generateReportB(cResult))),
tap(result => saveReportSomewhere(result))
).subscribe();

Do side effect if observable has not emitted a value within X amount of time

I'm working on a use case that requires that if an observable has not emitted a value within a certain amount of time then we should do some side effect.
To give a practical use case:
open web socket connection
if no message has been sent/received within X time then close web socket connection and notify user
This requires for a timer to be initiated on every emitted value and upon initial subscription of observable which will then run some function after the allotted time or until a value is emitted in which the timer resets. I'm struggling to do this the Rx way. Any help would be appreciated :)
debounceTime is the operator you're looking for: it only emits a value if no others follow within a specific timeout. Listening for the first message of the debounced stream will let you time out and clean up your websocket connection. If you need to time out starting from the opening of the stream, you can simply startWith. Concretely:
messages$.startWith(null)
.debounceTime(timeout)
.take(1)
.subscribe(() => { /* side effects */ });
Edit: if instead you're looking to end the a message stream entirely when it times out (e.g. you clean up in the onComplete handler), just cram debounceTime into a takeUntil:
messages$.takeUntil(
messages$.startWith(null)
.debounceTime(timeout)
).subscribe(timeout_observer);
With a timeout_observable: Observer<TMessage> that contains your cleanup onComplete.
You can do this with race:
timer(5000).race(someSource$)
.subscribe(notifyUser);
If someSource$ notifies faster than timer(5000) (5 seconds), then someSource$ "wins" and lives on.
If you only want one value from someSource$, you can obviously have a take(1) or first() on someSource$ and that will solve that issue.
I hope that helps.
Might not be the perfect answer but it does what you asked, it depends on how you want to disconnect, there might be some variation to be done
const source = new Rx.Subject();
const duration = 2000;
source.switchMap(value=>{
return Rx.Observable.of(value).combineLatest(Rx.Observable.timer(2000).mapTo('disconnect').startWith('connected'))
}).flatMap(([emit,timer])=>{
if(timer=='disconnect'){
console.log('go disconnect')
return Rx.Observable.throw('disconnected')
}
return Rx.Observable.of(emit)
})
//.catch(e=>Rx.Observable.of('disconnect catch'))
.subscribe(value=>console.log('subscribed->',value),console.log)
setTimeout(() => source.next('normal'), 300);
setTimeout(() => source.next('normal'), 300);
setTimeout(() => source.next('last'), 1800);
setTimeout(() => source.next('ignored'), 4000);
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>
A timer is initiated on each element and if it takes 4 seconds to be shown, then it will timeout and you can execute your function in the catchError
Here an example, it displays aa at T0s, then bb at t3s, then timeout after 4 second because the last one cc takes 10s to be displayed
import './style.css';
screenLog.init()
import { from } from 'rxjs/observable/from';
import { of } from 'rxjs/observable/of';
import { race } from 'rxjs/observable/race';
import { timer } from 'rxjs/observable/timer';
import { groupBy, mergeMap, toArray, map, reduce, concatMap, delay, concat, timeout, catchError, take } from 'rxjs/operators';
// simulate a element that appear at t0, then at t30s, then at t10s
const obs1$ = of('aa ');
const obs2$ = of('bb ').pipe(delay(3000));
const obs3$ = of('cc ').pipe(delay(10000));
const example2 = obs1$.pipe(concat(obs2$.pipe(concat(obs3$))), timeout(4000), catchError(a => of('timeout'))); // here in the catchError, execute your function
const subscribe = example2.subscribe(val => console.log(val + ' ' + new Date().toLocaleTimeString()));

How to read Bluebirds' promise structure?

I started using Bluebird and I see that it changes the structure of the promise.
Everything is now with an underscore so it's private (?), so what indicates if the promise was fulfilled or failed or pending?
In contrast with the original structure:
Lets have 3 promises - 2 resolved and 1 in rejected state and mix them with one another promise - timeout that will be rejected after 1 second.
Promise.race returns promise as soon one of the promises in the given array resolves or rejects.
const Promise = require("bluebird");
let p1 = Promise.resolve('first')
let p2 = new Promise((resolve) => {
setTimeout(resolve, 1e8)
})
let p3 = Promise.resolve('third')
Promise.race([
Promise.all([p1, p2, p3]).then(() => console.log('ok')),
new Promise((resolve, reject) => setTimeout(reject, 1e3)) // rejected after 1000 ms
])
.catch(() => console.log(`Promise p2 is in pending state: ${p2.isPending()}`))
.catch() will logs Promise p2 is in pending state: true

Resources