Angular RXJS retry a task based on a value - javascript

Ok, so I have a service that checks to see if a particular 3rd party JS plugin has loaded. I want to listen in for when it has entered the DOM, meanwhile it is in an undefined state. How do I do that? So far I have tried using a subject and retrying periodically but I can't get it to work:
$apiReady: Subject<boolean> = new Subject();
RegisterOnNewDocumentLoadedOnDocuViewareAPIReady(reControl: any): any {
this.$apiReady.asObservable().subscribe((isReady: boolean) => {
if (isReady) {
//Do something
return of(isReady);
}
})
let IsreControlInitialized = ThirdPartyAPI.IsInitialized(reControl);
if (IsreControlInitialized) {
this.$apiReady.next(true);
}
return throwError(false);
}
Then in the component:
this._apiService.RegisterOnAPIReady(this.elementID).pipe(
retryWhen(error => {
return error.pipe(delay(2000)); //<---- Doesn't work
})).subscribe((response: boolean) => {
if (response) {
//Do some stuff
}
});
My intentions were to check if the API element had been loaded, if not retry in 2 seconds but this doesn't work, can anyone help?

Throwing and catching an error until some condition is met is a little counter-intuitive to me.
My approach consists of using the interval operator along with takeUntil operator.
apiReady = new Subject();
interval(2000) // Check every 2s
.pipe(
map(() => this.isApiReady())
takeUntil(this.apiReady)
)
.subscribe(isReady => {
if (isReady) {
this.apiReady.next();
this.apiReady.complete();
}
})

Related

How do I make sure one Subcription finishes before another?

globleVariable: any;
ngOnInit() {
// This doesn't work. methodTwo throws error saying "cannot read someField from null. "
this.methodOne();
this.methodTwo();
}
methodOne() {
this.firstService.subscribe((res) => { this.globleVariable = res });
}
methodTwo() {
this.secondService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
As shown above, methodOne set the value of globleVariable and methodTwo uses it, therefore the former must finish running before the latter.
I am wondering how to achieve that.
Instead of subscribing in the methods, combine them into one stream and subscribe to that in ngInit(). You can use tap to perform the side effect of updating globaleVariable that you were previously performing in subscribe().
In the example below the "methods" are converted into fields since there is no reason for them to be methods anymore (you can keep them as methods if you want). Then the concat operator is used to create a single stream, where methodOne$ will execute and then when it's complete, methodTwo$ will execute.
Because concat executes in order, you are guaranteed that globaleVariable will be set by methodOne$ before methodTwo$ begins.
globleVariable: any;
methodOne$ = this.someService.pipe(tap((res) => this.globleVariable = res));
methodTwo$ = this.someService.pipe(tap((res) => console.log(this.globleVariable.someField));
ngOnInit() {
concat(this.methodOne$, this.methodTwo$).subscribe();
}
You can create a subject for which observable 2 will wait to subscribe like below :-
globalVariable: any;
subject: Subject = new Subject();
methodOne() {
this.someService.subscribe((res) => { this.globleVariable = res; this.subject.next(); });
}
methodTwo() {
this.subject.pipe(take(1), mergeMap(() => this.someService)).subscribe((res) => {
console.log(this.globleVariable.someField) });
}
The only way to guarantee a method call after a subscription yields is to use the subscription callbacks.
Subscriptions have two main callbacks a success and a failure.
So the way to implement a method call after the subscription yeilds is to chain it like this:
globleVariable: any;
ngOnInit() {
this.methodOne();
}
methodOne() {
this.someService.subscribe((res) => {
this.globleVariable = res
this.methodTwo(); // <-- here, in the callback
});
}
methodTwo() {
this.someService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
You might want to chain the calls with some other rxjs operators for a more standard usage.
ngOnInit() {
this.someService.method1.pipe(
take(1),
tap(res1 => this.globleVariable = res1)
switchmap(res1 => this.someService.method2), // <-- when first service call yelds success
catchError(err => { // <-- failure callback
console.log(err);
return throwError(err)
}),
).subscribe(res2 => { // <-- when second service call yelds success
console.log(this.globleVariable.someField) });
});
}
Please remember to complete any subscriptions when the component is destroyed to avoid the common memory leak.
my take,
so it's a bit confusing when you use same service that throws different results, so instead of someService I used firstService and secondService here.
this.firstService.pipe(
switchMap(globalVariable) =>
this.secondService.pipe(
map(fields => Object.assign({}, globalVariable, { someField: fields }))
)
)
).subscribe(result => {
this.globalVariable = result;
})
What I like about this approach is that you have the flexibility on how you want to use the final result as it is decoupled with any of the property in your class.

Angular Rxjs operator that help us start emitting when some variable changes

I just want to know if is there some rxjs operator that can delay my observable to start emitting when my variable is set to true.
Something like this on Angular:
loading: boolean;
constructor () {
this.subscriptions.push(
this._messageService.loadMyEvent$.pipe(
// here the operator that emitts data when loading is set to true
).subscribe(response => {
// load data
this.loadData();
this.loading = false;
})
);
}
ngAfterViewInit(): void {
this.loading = true;
}
So with all of this I want to loadData() when loading is set to true, this means all my content is rendered (AfterViewInit). Thank you.
You could use a multicast observable like ReplaySubject instead of a boolean primitive to emit the values. It could then be combined with the source observable using RxJS combineLatest function. It'll emit when any of it's source emits. But note all sources must've emitted at least once for the combineLatest to start emitting. You could also use filter operator to go forward only when loading is true.
Try the following
Controller
loading: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
constructor () {
this.subscriptions.push(
combineLatest(
this._messageService.loadMyEvent$,
this.loading
).pipe(
filter(([response, loading]) => loading) // <-- emit only when `loading` is true
).subscribe({
next: ([response, loading]) => {
// load data
this.loadData();
this.loading = false;
},
error: error => {
// handle error
}
})
);
}
ngAfterViewInit(): void {
this.loading.next(true); // <-- push `true`
}
It really depends on what exactly you want to do but you can merge EMPTY:
import { EMPTY } from 'rxjs';
...
this._messageService.loadMyEvent$.pipe(
switchMap(loading => loading ? someObservable : EMPTY),
).subscribe(response => {
// load data
this.loadData();
this.loading = false;
});

Assign observable within callback in Angular 9

I am completely a newbie to Angular. Some how struggled to get the initial code working now I am stuck here.
Use case : Show loading bar. Fetch merchant list to an observable. Stop loading bar.
Problem:
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm);
--> this.isLoading=false;
}
}
this.isLoading becomes false even before getting the response from getMerchantsBySearch call. I know I will need to perform this step in callback but I am not sure how to.
I want to do something like this but I a missing something. What is the correct way to do it.
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm).subscribe(
res={
---> this.people$ =res;
---> this.isLoading=false;
}
);
}
}
dashboard.component.ts
people$: Observable<IMerchant[]>;
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm);
this.isLoading=false;
}
}
user.service.ts
getMerchantProfile(id){
var options = this.getOptions();
return this.http.get<IMerchant[]>(environment.APIBaseURL+"/getMerchantProfile/"+id,options);
}
merchant.model.ts
export interface IMerchant{
email:String,
mobile : String,
password: String,
businessName : String,
otp:Number,
info:string,
url:String
}
i prefer using the teardown method .add(). it doesn't pollute your pipe chain of data manipulation and is also executed after an error is handled:
this.userService.getMerchantsBySearch(searchTerm)
.pipe(
// manipulate data here
)
.subscribe(
res => {
// handle final data
},
error => {
// handle error
}
).add(() => {
// teardown here (e.g. this.isLoading=false;)
});
By the way: if this.people$ is an observable, you can not assign it inside the subscribe callback. you'll need to assign it to the response of the userService method you are using. Be careful not to call subscribe on the stream since it will return you a subscription instead of an observable:
this.people$ = this.userService.getMerchantsBySearch(searchTerm).pipe(...)
You can actually pipe it and add a finalize
this.userService.getMerchantsBySearch(searchTerm)
.pipe(finalize(() => this.isLoading=false))
.subscribe(
res => {
this.people$ =res;
});

(Closed) Integrate delete subject for an existing search stream

I've been working on a personal app. My goal is to have a search and delete users using Observables. For search functionality I have used this idea, this is how I implemented:
export class UserComponent implements OnInit {
users: Observable<User[]>;
private searchTerms = new Subject<string>();
ngOnInit(): void {
this.setUsers();
}
private setUsers(): void {
this.users = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.userService.search(term)
// or the observable of all users if there was no search term
: this.userService.search(''))
.catch(() => {
this.response = -1;
return Observable.of<User[]>([]);
});
}
}
For delete functionality I use this:
private deleteSubject = new Subject();
delete(user: User): void {
this.userService
.delete(user)
.then(() => {
this.deleteSubject.next('');
})
.catch(() => {
console.log('error');
});
}
private setUsers(): void {
... // here comes the search functionality shown above
this.subscription = this.deleteSubject.subscribe(
(term: string) => this.users = this.userService.search(''),
(err) => console.log(err)
);
}
The problem that I faced with this implementation is that after I delete an user my search functionality is broken because I lost the subscriber for searchTerms because I reassigned this.users. So I've been searching and found that I can use merge operator to append another subject, follow this idea. Sadly I'm facing a problem with the implementation. I tried this:
delete(user: User): void {
...
// change
this.deleteSubject.next('');
// with
this.deleteSubject.next({op: 'delete', id: user.id});
}
private setUsers(): void {
...
this.users.merge(this.deleteSubject)
.startWith([])
.scan((acc: any, val: any) => {
if (val.op && val.op === 'delete') {
let index = acc.findIndex((elt: any) => elt.id === val.id);
acc.splice(index, 1);
return acc;
} else {
return acc.concat(val);
}
});
But my scan operator is never execute because this.deleteSubject doesn't have a subscriber for it. I know how to create and associate a subscriber to my this.deleteSubject but don't know how to mix it with my merge operator. Also see that it's getting a bit complex, I guess it should be another way to get this done. Comments are appreciated.
Please take a look at my plunker, hope gets a bit clear. I've simulated the calls to my services using a memory array userList.
I fixed my problem, I had to do two changes:
Assign this.users.merge(this.deleteSubject) to this.users
In the else block for scan operator change return acc.concat(val); to return val;
I updated my plunker if you want to take a look.
Cheers.

Observables "retryWhen" delay

How can I set a delay in retryWhen?
import 'rxjs/add/operator/retry';
import 'rxjs/add/operator/retrywhen';
...
constructor(http: Http) {
var headers = new Headers();
headers.append('Content-Type', 'text/plain');
http.post('https://mywebsite.azurewebsites.net/account/getip', "", { headers: headers })
.retryWhen(errors => {
return errors.delay(1000); // errors is not a function
})
(event) => {
// handle events
this.ip = event.json();
},
(error) => {
console.error(error);
toastr.error('No connection to server. Please reload the page!')
}
);
}
I am getting the error: errors is not a function.
import {Http, Headers, Response} from '#angular/http';
http.get('http://jsonplaceholder.typicode.com/posts/1/commentsss')
.retryWhen(e => e.scan<number>((errorCount, err) => {
if (errorCount >= 10) {
throw err;
}
return errorCount + 1;
}, 0).delay(1000))
.subscribe(r => console.log(`Sub: ${r}`))
This retries for 10 times with 1 second delay.
plnkr
A little late, but this is how to do with a new version of rxjs ( >6)
I guess you are trying to automatically retry network incase of a failure.
This could be achieved via different ways, but here is a very small implementation
RetryWhen() - THis operator catches if any errors are thrown by an observable and creates errorObservable from that.
Now using the errorObservable we could retry for a specified number of attempts for a specific set of failure
For example, retries incase of a 404 failure, is really unnecessary, but incase of 500X exceptions it is really mandatory.
DelayWhen - we could use this operator to specify a timer observable that delays the next retry attempt until the time elapses. This also gives the additional advantage of lineraly increasing the delay between each retry attempts
iif - Use of this conditional operator really enables us to filter and execute the necessary observable based on a given condition. You could examples in stackoverflow as well. But I am going give a simple if else illustration
contactMap - This is higher-order observable operator, meaning it produces /maps an input/observable to an output observable. The reason for using this is, we need to redo the retry operation incase of the same failure for specified number of times and the best way to restart the operation is via an error Observable.
Note that, we could use other higher-order observable operators like mergeMap/switchMap- each have their own advantages and disadvantages, but I prefer using this. Again contactMap is different from concat operator so care should be taken here
I find the best place to implement such a retry in Angular, is inside the httpinterceptors, but this could also be done inside the service
Here is a sample implementation:
// Class to intercept all network calls and retry for X number of times
export class ErrorInterceptor implements HttpInterceptor {
// private authService: UserAuthenticationService;
private ngxLogger: NGXLogger;
private errorHandlerService: ErrorHandlerService;
constructor(injector: Injector) {
this.ngxLogger = injector.get(NGXLogger);
this.errorHandlerService = injector.get(ErrorHandlerService);
}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const interceptObs$ = next.handle(req);
// you could filter for the URLS that this should be applied like this
if (req.url.match(API_FETCH_TOKEN)) {
let retryAttempts = 0;
const httpInterceptor$ = interceptObs$.pipe(
retryWhen(errorObs => {
return errorObs.pipe(
tap(errval => console.log(`Retrying because of err:${errval}`)),
concatMap(errObsValue => {
if (
errObsValue instanceof HttpErrorResponse &&
errObsValue.status != 404
) {
console.log('inside concatMap', errObsValue);
if (retryAttempts > APP_NET_RETRIES) {
return throwError(this.getErrorModel(errObsValue, req));
} else {
retryAttempts++;
// this linearly increases the delay of attempts
delayWhen(() =>
timer(retryAttempts * APP_NET_RETRIES_DELAY * 1000)
);
return httpInterceptor$;
}
}
})
);
})
);
return httpInterceptor$;
} else {
return interceptObs$;
}
// above is the notifier observable, with errors captured as input observable
// so we could operate on this observable for retires
// also make sure to return the error observable - i.e the notifier observable
// reason being all errors should again be returned so as to capture them and
// only when they are returned they can be retried
// Also make sure after take() - no.of retries we just throw a last occuring error obs
}
// I like to create a custom error model and handle that in Custom ErrorHandler
// Below is the sample implementation I use. but again this is your preference
// you can just rethrow the error alone
async getErrorModel(errValue, req): Promise<ErrorModel> {
const errModel = new ErrorModel();
errModel.Error = errValue;
errModel.ErrorMessage = 'Error in retrieving the token:' + errValue.message;
errModel.ErrorStatus = 421;
errModel.ErrorUrl = req.url;
errModel.Timestamp = momentTimeZone
.tz(DEFAULT_TIME_ZONE)
.format(DEFAULT_TIME_ZONE);
await this.errorHandlerService.handleError(errModel);
return Promise.resolve(errModel);
}
}
Hopefully, it helps..
EDIT: There is really a nice article about backoff-rxjs that really shortens eveything that we did above. Please refer this link
I found this page, and I made some improvements:
retryWhen(errors => errors.pipe(
takeWhile((err, index) => {
if (index === 3) throw new Error(err.toString());
return true
}),
delay(1000),
))

Categories

Resources