RxJS retry only last item, blocking the stream - javascript

I'm trying to use RxJS to process a stream of items, and I would like for it to retry any failures, preferentially with a delay (and exponential backoff if possible), but I need to guarantee the ordering so I effectively want to block the stream, and I don't want to reprocess any items that were already processed
So I was trying to play with retryWhen, following this example:
const { interval, timer } = Rx;
const { take, map, retryWhen, delayWhen, tap } = RxOperators;
const source = take(5)(interval(1000));
source.pipe(
map(val => {
if (val >= 3) {
throw val;
}
return val;
}),
retryWhen(errors =>
errors.pipe(
delayWhen(val => timer(1000))
)
)
);
But the stream restarts at the beginning, it doesn't just retry the last one:
Is it possible to achieve what I want? I tried other operators from docs as well, no luck. Would it be kinda against RxJS philosophy somehow?

The retryWhen should be moved to an inner Observable to handle the failed values only and keep the main Observable working.
Try something like the following:
// import { timer, interval, of } from 'rxjs';
// import { concatMap, delayWhen, map, retryWhen, take } from 'rxjs/operators';
const source = interval(1000).pipe(take(5));
source.pipe(
concatMap((value) =>
of(value).pipe(
map((val) => {
if (val >= 3) {
throw val;
}
return val;
}),
retryWhen((errors) => errors.pipe(delayWhen(() => timer(1000))))
)
)
);

Related

Angular wait for all subscriptions to complete

In Angular a page makes multiple http calls on multiple actions, let's say button clicks. But when the last "DONE" button is pressed I want to make sure that all those requests are finished before it progresses. I tried to use forkJoin with observables but it triggers requests itself which is not what I want to do, I want other actions to trigger requests and just to make sure that async requests are finished when "DONE" is clicked. With promises I would just push promises to array and then do Promise.all(allRequests).then(()=>{})
observables: Observable<any>[];
onBtn1Click(){
let o1 = this.service.doAction1();
this.observables.push(o1);
o1.subscribe(resp => {
//do action1
});
}
onBtn2Click(){
let o2 = this.service.doAction2();
this.observables.push(o2);
o2.subscribe(resp => {
//do action2
});
}
onDoneClick(){
// I would like something like this just that it wouldn't trigger the requests but make sure they are completed.
forkJoin(this.observables).subscribe(()=>{
//Proceed with other things
});
}
Unless someone comes up with an elegant approach, the following should do it.
I'm creating an object to hold hot observable for each cold observable from the HTTP request. The request would emit to it's corresponding hot observable using RxJS finalize operator. These hot observables could then be combined using forkJoin with a take(1) to wait for the source requests to complete.
private httpReqs: { [key: string]: ReplaySubject<boolean> } = Object.create(null);
onBtn1Click() {
this.httpReqs['btn1'] = new ReplaySubject<boolean>(1);
this.service.doAction1().pipe(
finalize(() => this.httpReqs['btn1'].next(true))
).subscribe(resp => {
// do action1
});
}
onBtn2Click() {
this.httpReqs['btn2'] = new ReplaySubject<boolean>(1);
this.service.doAction1().pipe(
finalize(() => this.httpReqs['btn2'].next(true))
).subscribe(resp => {
// do action2
});
}
onDoneClick(){
forkJoin(
Object.values(this.httpReqs).map(repSub =>
repSub.asObservable().pipe(
take(1)
)
)
).subscribe(() => {
// Proceed with other things
});
}
Using shareReplay
If you multicast, any subscriber who subscribes to a completed stream gets the complete notification. You can leverage that.
The various share operators have an implicit refCount that changes its default every few RxJS versions. The current version for shareReplay(n) is pretty intuitive, but you may need to set refCount:false on older versions, or even use multicast(new ReplaySubject(1)), refCount()
onBtn1Click(){
let o1 = this.service.doAction1().pipe(
shareReplay(1)
);
this.observables.push(o1);
o1.subscribe(resp => {
//do action1
});
}
This is the smallest change that should get your code working the way you'd like
Scan to count activity
You can avoid forkJoin entirely if you just count currently active operations.
count = (() => {
const cc = new BehaviorSubject<number>(0);
return {
start: () => cc.next(1),
stop: () => cc.next(-1),
value$: cc.pipe(
scan((acc, curr) => acc + curr, 0)
)
}
})();
onBtn1Click(){
this.count.start();
this.service.doAction1().pipe(
finalize(this.count.stop)
).subscribe(resp => {
//do action1
});
}
onDoneClick(){
this.count.value$.pipe(
first(v => v === 0) // Wait until nothing is currently active
).subscribe(() => {
//Proceed with other things
});
}

How to handle the canceled request inside forloop in Angular.?

I have 5+ pages in my App. I have the following method on header component.
The aim is, I need to show the status in the header if the user clicks the particular button. If I make a minimal or slow navigation between pages below code works fine. But if I navigate pages very frequently, the request getting canceled, because in some other pages I am calling the different set of API's.
async geneStatus() {
for (const x of Object.keys(this.gene)) {
const operationId = this.gene[x]['name'];
let operArr;
try {
operArr = await this.fetchEachStatus(name);
} catch (err) {
continue;
}
if (operArr[0] && operArr[0] === 'error') {
continue;
}
// Doing my logics
}
fetchEachStatus(geneId): Promise<any[]> {
return new Promise((resolve, reject) => {
this.apiDataService.get(this.geneUrl+ '/' + geneId).subscribe(
(res) => {
setTimeout(() => {
resolve(res);
}, 500);
}, err => {
setTimeout(() => {
resolve(['error']);
}, 500);
});
});
}
Here the problem is if any one of the API gets cancelled the for loop is not iterating for the next elements. I need to iterate the loop if one API is get cancelled. How can I fix this issue? I am not sure where I am making the problem.
I see multiple issues. I think the conversion to observable to promise is not only unnecessary, but counter-productive. Using the observables directly would enable you to use RxJS functions and operators. We can use the forkJoin function to make multiple simultaneous requests and catchError operator to mitigate the effects of potential errors.
Try the following
import { forkJoin } from 'rxjs';
import { catchError } from 'rxjs/operators';
geneStatus() {
forkJoin(Object.keys(this.gene).map(gene => this.fetchEachStatus(gene['name']))).subscribe(
res => {
// res[0] - `{ success: true | false, geneId: geneId }` from `this.apiDataService.get(this.geneUrl + '/' + this.gene[0]['name'])`
// res[1] - `{ success: true | false, geneId: geneId }` from `this.apiDataService.get(this.geneUrl + '/' + this.gene[1]['name'])`
...
const passedGeneIds = res.filter(item => item.success).map(item => item.geneId);
// passedGeneIds = [`geneId`, `geneId`, ...] - list of passed gene IDs
const failedGeneIds = res.filter(item => !item.success).map(item => item.geneId);
// failedGeneIds = [`geneId`, `geneId`, ...] - list of failed gene IDs
// some other logic
},
error => {
// essentially will never be hit since all the errors return a response instead
}
);
}
fetchEachStatus(geneId): Observable<any> {
return this.apiDataService.get(this.geneUrl + '/' + geneId).pipe(
map(_ => ({ sucess: true, geneId: geneId })), // <-- map to return the `geneId`
catchError(error => of({ sucess: false, geneId: geneId })) // <-- retun an observble from `catchError`
);
}
Now you need to remember that each time a button is clicked multiple simultaeneous requests are triggered. One solution to overcome this issue is to cancel all current requests before triggering a new set of request. For that you could bind the buttons to emit a central observable and trigger the requests using switchMap operator piped to that observable.

Execute function on observable cancellation

I want to have an observable that when unsubscribed it calls a function but only when it is unsubscribed without error and without getting to complete. The observable I am trying to build usually gets raced with another observable. I want when the other observable "wins" this one executes a function.
I tried finalize operator but it executes always.
playback.ts
import { timer } from "rxjs";
import { takeUntil, finalize } from "rxjs/operators";
import errorobs$ from "./errorobs";
export default function() {
return timer(10000).pipe(
takeUntil(errorobs$),
finalize(finalFunc)
);
}
function finalFunc() {
console.log("final function executed");
}
errorobs.ts
import { fromEvent } from "rxjs";
import { map } from "rxjs/operators";
export default fromEvent(document.getElementById("errorBtn"), "click").pipe(
map(() => {
throw new Error("my error");
})
);
I have made a small demo here https://codesandbox.io/s/q7pwowm4l6
click start to start "the observable".
click cancel to make the other observable win
click error to generate an error
One way to achieve this is using a custom operator, like my onCancel() below:
const {Observable} = rxjs
function onCancel(f) {
return observable => new Observable(observer => {
let completed = false
let errored = false
const subscription = observable.subscribe({
next: v => observer.next(v),
error: e => {
errored = true
observer.error(e)
},
complete: () => {
completed = true
observer.complete()
}
})
return () => {
subscription.unsubscribe()
if (!completed && !errored) f()
}
})
}
// Test:
const {interval} = rxjs
const {take} = rxjs.operators
// This one gets cancelled:
const s = interval(200).pipe(
onCancel(() => console.warn('s cancelled!'))
).subscribe(() => {})
setTimeout(() => s.unsubscribe(), 500)
// This one completes before unsubscribe():
const q = interval(200).pipe(
take(2),
onCancel(() => console.warn('q cancelled!'))
).subscribe(() => {})
setTimeout(() => q.unsubscribe(), 500)
<script src="//unpkg.com/rxjs#6/bundles/rxjs.umd.min.js"></script>
It really works as you describe it. finalize is executed when the chain is being disposed which is when all subscribers unsubscribe, when the chain errors or when it completes.
There's already an issue on RxJS Github page for this feature: https://github.com/ReactiveX/rxjs/issues/2823
In the link above you can see an example of a custom operator that adds reason to the finalize operator.
I had to deal with this use-case myself and added this operator to my own collection of RxJS operators: https://github.com/martinsik/rxjs-extra/blob/master/doc/finalizeWithReason.md

Redux observable epic takes few actions in a row but return only last

I have an epic which should proceed few actions of type VERIFY_INSURANCE_REQUEST in a row. Everything works good inside switchMap block (all items are proceeded as well) but only last one goes to map block, so I have only one successfully dispatched action instead of many.
function verifyInsuranceEpic(action$) {
return action$.pipe(
ofType(types.VERIFY_INSURANCE_REQUEST),
switchMap((action) => {
const { verifyInsuranceModel } = action;
const promise = InsuranceApi.verifyInsurance(verifyInsuranceModel).then(result => {
const returnResult = result && result.rejectReason === null;
const actionResponse = {
returnResult,
key: verifyInsuranceModel.key
}
return actionResponse;
})
return from(promise);
}),
map(result => {
return verifyInsuranceSuccess(result)
}),
catchError(error => of(verifyInsuranceFailure(error)))
);
}
Is there any way to make all responses go to map block?
As mentioned in comments, the solution is just change switchMap to concatMap.

RxJS 5.0 "do while" like mechanism

I'm trying to use RxJS for a simple short poll. It needs to make a request once every delay seconds to the location path on the server, ending once one of two conditions are reached: either the callback isComplete(data) returns true or it has tried the server more than maxTries. Here's the basic code:
newShortPoll(path, maxTries, delay, isComplete) {
return Observable.interval(delay)
.take(maxTries)
.flatMap((tryNumber) => http.get(path))
.doWhile((data) => !isComplete(data));
}
However, doWhile doesn't exist in RxJS 5.0, so the condition where it can only try the server maxTries works, thanks to the take() call, but the isComplete condition does not work. How can I make it so the observable will next() values until isComplete returns true, at which point it will next() that value and complete().
I should note that takeWhile() does not work for me here. It does not return the last value, which is actually the most important, since that's when we know it's done.
Thanks!
We can create a utility function to create a second Observable that emits every item that the inner Observable emits; however, we will call the onCompleted function once our condition is met:
function takeUntilInclusive(inner$, predicate) {
return Rx.Observable.create(observer => {
var subscription = inner$.subscribe(item => {
observer.onNext(item);
if (predicate(item)) {
observer.onCompleted();
}
}, observer.onError, observer.onCompleted);
return () => {
subscription.dispose();
}
});
}
And here's a quick snippet using our new utility method:
const inner$ = Rx.Observable.range(0, 4);
const data$ = takeUntilInclusive(inner$, (x) => x > 2);
data$.subscribe(x => console.log(x));
// >> 0
// >> 1
// >> 2
// >> 3
This answer is based off: RX Observable.TakeWhile checks condition BEFORE each element but I need to perform the check after
You can achieve this by using retry and first operators.
// helper observable that can return incomplete/complete data or fail.
var server = Rx.Observable.create(function (observer) {
var x = Math.random();
if(x < 0.1) {
observer.next(true);
} else if (x < 0.5) {
observer.error("error");
} else {
observer.next(false);
}
observer.complete();
return function () {
};
});
function isComplete(data) {
return data;
}
var delay = 1000;
Rx.Observable.interval(delay)
.switchMap(() => {
return server
.do((data) => {
console.log('Server returned ' + data);
}, () => {
console.log('Server threw');
})
.retry(3);
})
.first((data) => isComplete(data))
.subscribe(() => {
console.log('Got completed value');
}, () => {
console.log('Got error');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.1/Rx.min.js"></script>
It's an old question, but I also had to poll an endpoint and arrived at this question. Here's my own doWhile operator I ended up creating:
import { pipe, from } from 'rxjs';
import { switchMap, takeWhile, filter, map } from 'rxjs/operators';
export function doWhile<T>(shouldContinue: (a: T) => boolean) {
return pipe(
switchMap((data: T) => from([
{ data, continue: true },
{ data, continue: shouldContinue(data), exclude: true }
])),
takeWhile(message => message.continue),
filter(message => !message.exclude),
map(message => message.data)
);
}
It's a little weird, but it works for me so far. You could use it with the take like you were trying.
i was googling to find a do while behavior, i found this question. and then i found out that doWhile takes in a second param inclusive boolean. so maybe you can do?:
takeWhile((data) => !isComplete(data), true)

Categories

Resources