Retry failed API call to another origin in Angular - javascript

I want to implement a logic of retrying an API call to another origin if the call to current origin failed.
I want it to be handled in the service layer, for not to implement this logic in each component.
For example, I have such function in endpoint service
getAll(): Observable<IssueListItem[]> {
let endpointUrl = `${this.apiConnectionService.getApiUrl()}api/Issues`;
return this.http.get<IssueListItem[]>(endpointUrl, { headers: this.dataService.requestHeaders })
.pipe(retryOtherApi(this.apiConnectionService),
catchError(error => this.handleError(error)));
}
The consumer of this function looks like:
ngOnInit() {
this.issuesEndpointService.getAll()
.subscribe(_ => this.issues = _);
}
And I whant it know nothing about retry logic.
So, I tried to create an operator 'retryOtherApi' where I switch an origin to another one.
export function retryOtherApi(apiConnectionService: ApiConnectionService) {
const maxRetry = apiConnectionService.apiOriginsCount;
return (src: Observable<any>) => src.pipe(
retryWhen(_ => {
return interval().pipe(
flatMap(count => {
console.log('switch to: ' + apiConnectionService.getApiUrl())
apiConnectionService.switchToOther();
return count === maxRetry ? throwError("Giving up") : of(count);
})
);
})
);
}
Switching works, but unfortunately, the whole getAll function is not called and it retries n times with the same old URL.
So the question is how to implement common retry to other API logic if the current API become unavailable.
If to rephrase the question to the more common case, it becomes like how to recall HTTP endpoint with other params if the current one failed.

public getAll(): Observable<IssueListItem[]> {
let call = () => {
let endpointUrl = `${this.apiConnectionService.getApiUrl()}api/Issues`;
return this.http.get<IssueListItem[]>(endpointUrl, { headers: this.dataService.requestHeaders })
};
return <Observable<IssueListItem[]>>call()
.pipe(
retryOtherApi(this.apiConnectionService, () => call()),
catchError(error => this.handleError(error))
);
}
And here is a retry operator:
export function retryOtherApi(apiConnectionService: ApiConnectionService, recall: () => Observable<any>) {
const maxRetry = apiConnectionService.apiOriginsCount;
let count = 0;
let retryLogic = (src: Observable<any>) => src.pipe(
catchError(e => {
if (e.status === 0) {
apiConnectionService.switchToOther();
console.log('switch to: ' + apiConnectionService.getApiUrl())
return count++ === maxRetry ? throwError(e) : retryLogic(recall());
} else {
return throwError(e);
}
}));
return retryLogic;
}
It works, but:
Now I need to implement logic for other threads not to switch api
simultaneously.
Have some problems with TS typecasting, that is
why I hardcoded type before return.

Related

How to wait for API response in the nested subscription before executing next line of code

I have a method that should return me a boolean value, the problem is that this is asynchronus and I want to avoid race condition..
So I have some method for example:
someMethod(data: any): boolean{
let isOccupied = false;
firstValueFrom(this.checkIfOccupied('102')).then(toilet=> isOccupied = toilet.name === data.name);
this.someObject.isOccupied = isOccupied;
return isOccupied;
}
So before I proceed to the line with this.someObject... I want to wait for things that are happening inside of then( )
And the checkIfOccupied looks like this:
checkIfOccupied(toiletName: string): Observable<Toilet> {
return this.store$
.select(selectAlarmsForToilet(toiletName))
.pipe(
filter(res => !!res),
take(1),
switchMap((alarms: AlarmsObject[]) => {
alarms.forEach(alarm => {
if (Object.keys(alarm)[0].includes('occupied')) {
const toiletId = this.getToiletIdFromAlarm(toiletName, alarm); <= this method only finds needed ID
if (toiletId ) {
return this.toiletService.getToiletForId(toiletId ); <= this is API call
}
}
});
return of({} as SomeObject);
}));
}
I have tried to make it async and then use await in the someMethod but it doesn't work. Probably I made some mistake in the code (I dont want to make someMethod async - is it even possible?)
You are very close in your thinking.
Consider the words you stated, which I will paraphase:
"Before I proceed to the next line, I want to wait for something"
So change your code like so:
async someMethod(data: any): Promise<boolean>{
let isOccupied = false;
data = await firstValueFrom(this.checkIfOccupied('102'));
this.someObject.isOccupied = toilet.name === data.name;
return toilet.name === data.name;
}
You are using the Arrays.prototype.forEach inside your switchMap, which does not know anything about asynchronousity.
To keep up with race conditions, you can use the forkJoin Operator, which waits for Observables in arrays to complete such as:
someMethod(data: any): boolean {
let isOccupied = false;
firstValueFrom(this.checkIfOccupied('102')).then(toilet => isOccupied =
toilet.name === data.name);
this.someObject.isOccupied = isOccupied;
return isOccupied;
}
checkIfOccupied(toiletName: string): Observable<any> {
return this.store$
.select(selectAlarmsForToilet(toiletName))
.pipe(
filter(res => !!res),
take(1),
switchMap((alarms: AlarmsObject[]) => {
return alarms.length ? forkJoin(alarms
.filter(alarm => Object.keys(alarm)[0].includes('occupied'))
.map((alarm: any) => this.getToiletIdFromAlarm(toiletName, alarm))
.filter(toiletId => !!toiledId)
.map((toiletId: any) => {
return this.toiletService.getToiletForId(toiletId); // api call
})) : of([]);
}));
}

Function returns value before inner async results are finished

Im trying to add a helper method to my service that will call another method in the same service which returns an Observable. The helper method needs to return a Boolean. The problem is the logic required to determine whether the return value is true or false is done inside the subscribe block which is obviously async. So even through the value is true I still always get a false returned. Maybe trying to mix synchronous and asynchronous programming like this is not a good approach.
In my component I am trying to make the following call to the service:
this.isAdmin = this.teamsService.isAdmin(this.teamId, this.user['userId'])
Service:
getAssociations(id, postfix) {
return this.httpService.get(this.base, id, postfix)
.map((response) => {
return this.responseHandler.handleData(response);
})
.catch((error) => {
return this.responseHandler.handleError(error);
}
);
}
isAdmin(teamId, userId) {
let isAdmin = false;
this.getAssociations(teamId, '/teamMembers')
.subscribe(
_data => {
if (_data['teamMembers']) {
for (let i = 0; i < _data['teamMembers'].length; i++) {
if (_data['teamMembers'][i]['id'] === userId) {
isAdmin = true;
break;
}
}
}
}
);
return isAdmin;
}
So the isAdmin method always return false for obvious reasons. I initially was dealing with this logic inside the component by doing the following:
private checkIsAdmin() {
this.teamsService.getAssociations(this.teamId, '/teamMembers')
.subscribe(
_data => {
if (_data['teamMembers'] && _data['teamMembers'].length) {
_data['teamMembers'].map(member => {
if (member['id'] === this.user['userId']) {
this.isAdmin = true;
}
});
}
this.spinner.hide();
this.loading = false;
},
_error => {
this.spinner.hide();
}
);
}
This worked fine until I had to repeat this same logic on other components. So I am trying to come up with a way to abstract that logic into the service itself returning a boolean since that is really all I need from the function.
I have tried a few ideas but none of them worked. Any help would be great. Thanks.
You can also move all control logic up into the Rx stream like so:
isAdmin(teamId, userId) {
return this.getAssociations(teamId, '/teamMembers')
.mergeMap(data => data['teamMembers'] != null ? Rx.Observable.empty() : Rx.Observable.from(data['teamMembers']))
.filter(user => user['id'] === userId)
.mapTo(true) // found admin user
.take(1)
.concat(Rx.Observable.of(false));//default state; no admin user
}
Depending on the usage of getAssociations() you can also change its return type signature from Observable<ResponseTeamWrapperSomething> towards Observable<TeamMember> so you can lose the mergeMap operator in this function.
By moving this logic and replacing your custom control flow with platform supplied function you reduce the cognitive overload of what your code is doing. This also reduces the testing burden on you because you do not have to test framework supplied logic.
I changed the call in the component to:
this.teamsService.isAdmin(this.teamId, this.user['userId'])
.subscribe(response => {
this.isAdmin = response;
}
);
And updated the method inside the service to:
isAdmin(teamId, userId) {
return this.getAssociations(teamId, '/teamMembers')
.map(_data => {
let isAdmin = false;
if (_data['teamMembers']) {
for (let i = 0; i < _data['teamMembers'].length; i++) {
if (_data['teamMembers'][i]['id'] === userId) {
isAdmin = true;
break;
}
}
}
return isAdmin;
});
}

Property 'do' does not exist on type 'Subscription'

What am I misunderstanding about how to use the .subscribe function in combination with the .do function?
Here is my observable sequence:
lookupSubscriber = (text$: Observable<string>) =>
text$.debounceTime(300)
.distinctUntilChanged()
.do(() => this.searching = true)
.switchMap(term => {
var data = this._callApi(this.lookupSubscriberAPI, term)
.do(() => {
this.searchFailed = false;
this.searching = false;
})
.catch(() => {
this.searchFailed = true;
this.searching = false;
return Observable.of([]);
})
return data;
})
.do(() => this.searching = false);
If my _callApi function is as follows, it works:
_callApi(url: string, term: string) {
if (term === '') {
return of.call([]);
}
return map.call(this.dataService.get(url + term), response => {
var data = this._transformSubscriberInfo(response);
return data;
})
}
However, when I try to rewrite it with a subscribe function like this:
_callApi = (url: string, term: string) => {
return this.dataService.get(url + term)
.subscribe(
response => { this._transformSubscriberInfo(response) },
error => error.text(),
() => {
if (Logging.isEnabled.light) {
console.log('%c API Call Complete', Logging.normal.orange);
}
))
}
... then the data call succeeds, but I receive error: Property 'do' does not exist on type 'Subscription'.
Basically I am trying to catch errors and run an "always" function after the api call, as shown in the second version of _callApi.
The first version of _callApi seems to return an Observable while the second one returns a Subscription object. And Subscription does not expose do, exactly as the error message states.
What you may want to try is to use a version of do that accepts error and complete callbacks in addition to the next callback:
return this.dataService.get(url + term)
.map(response => this._transformSubscriberInfo(response))
.do(
response => { /* log response */ },
error => { /* log error */ },
() => { /* log completion */ }
);
It worth mentioning that do is not capable of transforming the source stream, the observable it returns contains the same values as the observable it is called on. That's why we need the line .map(response => this._transformSubscriberInfo(response)).
Also complete callback should not be confused for an "always" function: it gets called only when the source observable completes and it does not get called when the observable produces an error or gets unsubscribed.

Angular2 apply filter inside service

I am building an app with Angular 2. I have a service, and I am trying to filter the data before getting it from the service. I want a function where I can just ask one project element instead of the whole array.
This is the code I tried, but this approach doesn't work:
getProject(id: number): Observable<Project> {
return this.http.get(this.url).map(this.extractData).filter(project => (<Project>project).id == id).catch(this.handleError2);
// return this.getProjects().filter(project => project.id === id);
//return this.http.get(this.url).toPromise().then(x => x.json().data.filter(project => project.id === id)[0]).catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError2 (error: any) {
// In a real world app, we might use a remote logging infrastructure
// We'd also dig deeper into the error to get a better message
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
Its better to filter data in component from where you have called the service.
So your service is like :
getProject(id: number): Observable<Project> {
return this.http.get(this.url).map(this.extractData);
}
private extractData(res: Response) {
let body = res.json();
return body.data || []; // return value must be array to use filter function in component
}
in your component you can do as below :
service.getProject(projectId).subscribe((res) => {
let filtered = [];
if(res.length){
filtered = res.filter((item) => {
return item.id === projectId;
});
}
});
To me, it looks like you're mixing sync and async code, and return doesn't work like that. You can return an object (function) who's properties change later (aka a Promise)
I doubt that http.get() provides an array for you to map
.toPromise() seems like a hack, but you should return a Promise chain
return this.http.get(this.url).then(data => data.map(this.extractData).filter(project => (<Project>project).id == id).catch(this.handleError2));
If this.http.get() does not return a Promise, but takes a callback, you can construct one:
return new Promise (resolve => this.http.get(this.url, resolve)).then(data => data.map(this.extractData).filter(project => (<Project>project).id == id).catch(this.handleError2));
And whatever is calling getProject() can chain with getProject().then()
If this part of code works :
getProjects() {
return this.http.get(this.url).map(this.extractData).catch(this.handleError2);
}
Then you can call a single project with :
getProject(id: number) {
return this.getProjects()
.then(projects => projects.filter(project => (<Project>project).id == id);
}

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