I want to keep retrying a call to the server (to see if it returns true).
In my service I created a http call which returns true if it was able to retrieve a value, and returns false when it was unable to retrieve a value (and thus get an error).
public retryServerCheck(): Observable<boolean> {
return this._httpClient.get<boolean>(this.baseUrl + 'ServerCheck')
.pipe(
map(() => true),
catchError(() => of(false)
));
}
In my component, I want to retry this until I get back true, and here lies my problem.
this._serverService.retryServerCheck()
.subscribe(
(isOnline) => {
if (isOnline) {
this._helperServer.navigateToComponent('Login', undefined, 0);
console.log('online');
} else {
this.lastRetry = 'A little later...';
console.log('offline');
}
}
);
I tried adding a pipe infront of the subsribe but no luck
this._serverService.retryServerCheck()
.pipe(
retry(10),
delay(100)
).subscribe(
(isOnline) => {
if (isOnline) {
this._helperServer.navigateToComponent('Login', undefined, 0);
console.log('online');
} else {
this.lastRetry = 'A little later...';
console.log('offline');
}
}
);
I was able to do the retry in my service, but then I'm not able to react to it in my component
public retryServerCheck(): Observable<boolean | HttpError> {
return this._httpClient.get<boolean>(this.baseUrl + 'ServerCheck')
.pipe(
retryWhen(errors => errors.pipe(delay(500))),
catchError(err => {
return this._helperService.handelHttpError(err);
})
);
}
Both catchError and retryWhen will suppress errors on the stream. So in the component errors are already handled.
Try either making retryWhen responsible for handling the number of retries
// service
public retryServerCheck() {
return this._httpClient.get(this.baseUrl + 'ServerCheck').pipe(
retryWhen(error$ => error$.pipe(
take(10), // <-- number of retries
delay(500),
concat(
/* either throw your own error */
throwError('no more retries left')
/* or to pass original error -- use `NEVER` */
// NEVER
)
))
)
}
// component
this._serverService.retryServerCheck()
.subscribe({
next: (result) => {
// succeeded
},
error: (error)=> {
// failed
}
});
Run this example
OR adding an expand to the component -- to retry when you have a false on the stream.
Read more about rxjs error handling and specifics of retryWhen
Related
I need to call an API that can return errors, warnings or success.
If it returns warnings the user must able to accept the warning and I should send the same payload + acceptWarning: true.
I need to display an ionic modal and wait for the user's response to see if he accepts or cancel the warning.
What should be the best way to achieve that?
Right now I have something like this:
#Effect()
public Assign$ = this.actions$.pipe(
ofType(myActions.Assign),
map(action => action.payload),
exhaustMap(assignment =>
this.assignService.assign(assignment).pipe(
switchMap(() => {
this.errorService.showPositiveToast(' Assigned Successfully');
return [
new LoadAssignments(),
new LoadOtherData()
];
}),
catchError(error =>
from(this.openErrorModal(error)).pipe(
switchMap(({ data = '' }) => {
if (data === 'Accept') {
return of(new Assign({ ...assignment, acceptWarning: true }));
}
return of(new AssignShipmentFailure());
})
)
)
)
)
);
async openErrorModal(response: any) {
const errorModal = await this.modalCtrl.create({
component: ErrorValidationPopup,
componentProps: {
response: response,
},
});
await errorModal.present();
return errorModal.onDidDismiss();
}
But it is not triggering the Assign action again. Thanks for your help
If any error occurred in the effect's observable (or any Observable), then its stream emitted no value and it immediately errored out. After the error, no completion occurred, and the Effect will stop working.
To keep the Effect working if any error occurred, you have to swichMap instead of exhaustMap, and handle the errors within the inner observable of the switchMap, so the main Observable won't be affected by that.
Why use switchMap?
The main difference between switchMap and other flattening operators is the cancelling effect. On each emission the previous inner observable (the result of the function you supplied) is cancelled and the new observable is subscribed. You can remember this by the phrase switch to a new observable
You can try something like the following:
#Effect()
public Assign$ = this.actions$.pipe(
ofType(myActions.Assign),
map(action => action.payload),
switchMap(assignment =>
this.assignService.assign(assignment).pipe(
switchMap(() => {
this.errorService.showPositiveToast('Assigned Successfully');
return [
new LoadAssignments(),
new LoadOtherData()
];
}),
catchError(error =>
from(this.openErrorModal(error)).pipe(
map(({ data = '' }) => {
if (data === 'Accept') {
return new Assign({ ...assignment, acceptWarning: true });
}
return new AssignShipmentFailure();
})
)
)
)
)
);
here is my complete code function example:
public scan(formData: Object): Observable<any> {
let url = this.remoteUrl;
let result;
this.onlineService.isOnline$.subscribe( (isOnline) => {
if (isOnline) {
console.log('services is online connected');
result = this
._http
.post(url, formData, { headers: headers })
.pipe(map((res: any) => {
// console.log(res);
let response = res;
return response;
}),
catchError(error => {
if (error.status === 401 || error.status === 403) {
// handle error
}
return throwError(error);
}));
}else{
console.log('services are offline');
result = this.dbService.getByIndex('safety', 'code', formData['trafoStation']).subscribe( (location) => {
return location;
});
}
});
console.log(result);
return result;
};
actually, I need to run two different services based on an internet connection if the connection is available then call server API otherwise store on offline ngx-indexed-db.
i have stored data both online and offline.
getting undefined in result.
Result is undefined because it's an async operation: this.onlineService.isOnline$ has not emmited yet, but you already have return result, thus the undefined.
Also, the way you combine your observables is not right. You should NOT create new observables (and subscribe to them) in a subscribe method. That lead to weird side effects and memory leaks down the line.
Here's my proposal to get your code to work. I used the switchMap operator to return either your apiCall or your store operation based on isOnline$ value. SwitchMap is used to combine a higher observable with an inner observable and flatten the stream. It will also interupt the current subscription each time isOnline$ emits:
private _handleServices(formData, isOnline: boolean): Observable<any> {
console.log(`services are ${isOnline ? 'online': 'offline'}`);
const url = this.remoteUrl;
const apiCall$ = this._http.post(url, formData, { headers: headers })
.pipe(
catchError(error => {
if (error.status === 401 || error.status === 403) {
// handle error
}
return throwError(error);
})
);
const store$ = this.dbService.getByIndex('safety', 'code', formData['trafoStation']);
return (isOnline) ? apiCall$ : store$;
}
public scan(formData: Object): Observable<any> {
return this.onlineService.isOnline$.pipe(
switchMap((isOnline) => this._handleServices(formData, isOnline)),
tap(res => console.log(res))
);
};
Then, when you call your function in your component, you will call it like this:
this.scan(formData).subscribe(res => /* handle scan response */);
I have an Angular application using AngularFire, NgRx and Cloud Firestore as db, where I enabled offline persistence.
My problem is that when I change a document while offline the effect function does not trigger the success action, as Firestore promises are not resolved while offline, but only after the request reaches the server.
At the moment I am stuck in trying to find a good way to update the store with the local data when offline.
One idea could be to check the fromCache flag before loading the data, so that if fromCache is true (that is, we are offline) I can load the data from the local db instead of the store, but it looks to me like a dirty workaround.
Effect
//--- Effect triggered to load document in the home page ----
firstSet$ = createEffect(() => {
return this.actions$.pipe(
ofType(PlacesActions.loadFirstPlaces),
switchMap(() => {
return this.placeService.getFirstSet().pipe(
map((places) => {
return PlacesActions.loadFirstPlacesSuccess({ places });
}),
catchError((error) => of(PlacesActions.loadFirstPlacesFailure({ error }))),
takeUntil(this.subService.unsubscribe$)
);
})
);
});
//--- Effect triggered when a document is updated ----
updatePlace$ = createEffect(() => {
return this.actions$.pipe(
ofType(PlacesActions.updatePlace),
concatMap((action) => {
// ----- Below the NEW code, without promise ----
try {
this.placeService.savePlace(action.place);
this.router.navigate(['/home']);
return of(PlacesActions.updatePlaceSuccess({ place: action.place }));
}
catch(error) {
return of(PlacesActions.updatePlaceFailure({ error }))
}
/*
// ----- Below the old code ----
return from(this.placeService.savePlace(action.place))
.pipe(
map((place: Place) => {
this.router.navigate(['/home']);
return PlacesActions.updatePlaceSuccess({ place });
}),
catchError((error) => of(PlacesActions.updatePlaceFailure({ error })))
);
*/
})
);
});
DB service
savePlace(place): void {
this.firestoreRef.doc<Place>(`places/${place.id}`).update(place);
}
/* Old version of savePlace using Promise for the Update
async savePlace(place): Promise<void> {
return await this.firestoreRef.doc<Place>(`places/${place.id}`).update(place);
}
*/
loadFirstPlaces(limit: number = 9,
orderBy: OrderModel = { propName: 'modifiedOn', order: 'desc' }){
const query = (ref: CollectionReference) =>
ref.orderBy(orderBy.propName, orderBy.order)
.limit(limit);
return this.firestoreRef.collection<Place>('Places', query)
.valueChanges()
.pipe(shareReplay(1));
}
Home component
ngOnInit(): void {
// Set the data from the store, if available there
this.places$ = this.store.select(getAllPlaces).pipe(
tap((data) => {
this.places = data;
})
);
/*Dispatch load action to load data from the server or local cache.
If I use the PROMISE approach for the update method,
the data coming from the local cache has the old data.
The edits done while offline are not provided.*/
this.store.dispatch(loadFirstPlaces());
}
The local cache has been updated when the update() call completes. So that's the right moment to update the state of your application too:
async savePlace(place): Promise<void> {
const result = this.firestoreRef.doc<T>(`places/${place.id}`).update(place)
// TODO: update the state here
return await result;
}
I'm not even sure if you should be returning a promise here. If savePlace is meant to return whether the local operation was successful, it should simply be:
savePlace(place): void {
this.firestoreRef.doc<T>(`places/${place.id}`).update(place)
}
If the local write operation fails, update will throw an exception and that will escape from savePlace to signal failure to the caller.
I am new to angular and rxjs, and I have the following scenario, in which I need that after a call to an api is successfully resolved to make a new call, in the context of angular / rxjs I don't know how to do it
handler(): void {
this.serviceNAme
.createDirectory(this.path)
.pipe(
finalize(() => {
this.someProperty = false;
})
)
.subscribe(
(data) => console.log(data),
(error) => console.error(error.message)
);
}
What is the correct way to make a new call to an api when a previous one was successful?
I understand you have a serviceOne and a serviceTwo. And you want to call serviceTwo using the retrieved data from serviceOne.
Using rxjs switchMap you can pipe one observable into another.
handler(): void {
this.serviceOne
.createDirectory(this.path)
.pipe(
switchMap(serviceOneResult => {
// transform data as you wish
return this.serviceTwo.methodCall(serviceOneResult);
})
)
.subscribe({
next: serviceTwoResult => {
// here we have the data returned by serviceTwo
},
error: err => {},
});
}
If you don't need to pass the data from serviceOne to serviceTwo but you need them to be both completed together, you could use rxjs forkJoin.
handler(): void {
forkJoin([
this.serviceOne.createDirectory(this.path),
this.serviceTwo.methodCall()
])
.subscribe({
next: ([serviceOneResult, serviceTwoResult]) => {
// here we have data returned by both services
},
error: err => {},
});
}
Using aysnc and await you can do:
async handler(): void {
await this.serviceNAme
.createDirectory(this.path)
.pipe(
finalize(() => {
this.someProperty = false;
})
)
.subscribe(
(data) => console.log(data),
(error) => console.error(error.message)
);
// Do second api call
}
There are a few says to do this:
Scenario # 1
Your two service api calls are independent, you just want one to go and then the next
const serviceCall1 = this.serviceName.createDirectory(this.path);
const serviceCall2 = this.serviceName.createDirectory(this.otherPath);
concat(serviceCall1 , serviceCall2).subscribe({
next: console.log,
error: err => console.error(err.message),
complete: () => console.log("service call 1&2 complete")
});
Scenario # 2
Your two calls dependant on one another, so you need the result of the first before you can start the second
this.serviceName.getDirectoryRoot().pipe(
switchMap(root => this.serviceName.createDirectoryInRoot(root, this.path))
).subscribe({
next: console.log,
error: err => console.error(err.message),
complete: () => console.log("service call 1 used to create service call 2, which is complete")
});
You'll want scenario # 2, because done this way, an error in the first call will mean no result is sent to the switchMap, and second call is never made.
I am trying to make 2 HTTP requests and in the first call I try to create a record and then according to its results (response from the API method) I want to execute or omit the second call that updates another data. However, although I can catch the error in catchError block, I cannot get the response in the switchMap method of the first call. So, what is wrong with this implementation according to teh given scenario? And how can I get the response of the first result and continue or not to the second call according to this first response?
let result;
let statusCode;
this.demoService.create(...).pipe(
catchError((err: any) => { ... }),
switchMap(response => {
// I am trying to get the response of first request at here
statusCode = response.statusCode;
if(...){
return this.demoService.update(...).pipe(
catchError((err: any) => { ... }),
map(response => {
return {
result: response
}
}
)
)}
}
))
.subscribe(result => console.log(result));
The question is still vague to me. I'll post a more generic answer to make few things clear
There are multiple things to note
When an observable emits an error notification, the observable is considered closed (unless triggered again) and none of the following operators that depend on next notifications will be triggered. If you wish to catch the error notifications inside the switchMap, you could return a next notification from the catchError. Something like catchError(error => of(error)) using RxJS of function. The notification would then be caught by the following switchMap.
You must return an observable from switchMap regardless of your condition. In this case if you do not wish to return anything when the condition fails, you could return RxJS NEVER. If you however wish to emit a message that could be caught by the subscriptions next callback, you could use RxJS of function. Replace return NEVER with return of('Some message that will be emitted to subscription's next callback');
import { of, NEVER } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';
this.demoService.create(...).pipe(
catchError((err: any) => { ... }),
switchMap(response => {
statusCode = response.statusCode;
if (someCondition) {
return this.demoService.update(...).pipe( // emit `update()` when `someCondition` passes
catchError((err: any) => { ... }),
map(response => ({ result: response }))
);
}
// Show error message
return NEVER; // never emit when `someCondition` fails
}
)).subscribe({
next: result => console.log(result),
error: error => console.log(error)
});
You can implement with iif
this.demoService
.create(...)
.pipe(
// tap first to be sure there's actually a response to process through
tap(console.log),
// You can use any condition in your iif, "response.user.exists" is just a sample
// If the condition is true, it will run the run the update$ observable
// If not, it will run the default$
// NOTE: All of them must be an observable since you are inside the switchMap
switchMap(response =>
iif(() =>
response.user.exists,
this.demoService.update(response.id), // Pass ID
of('Default Random Message')
)
),
catchError((err: any) => { ... })
);