How may I block a constructor, to wait for an Http call to return data?
constructor() {
this.initRestData().subscribe();
// wait for data to be read
this.nextStep();
}
The data retreived by the initRestData() call is needed by other services/components in the application. I only have to do this at startup. If there is a better way to handle this then Observable that would be ok too.
You could chain the calls either inside the subscribe or in a do-operator:
constructor() {
this.initRestData()
.do(receivedData => this.nextStep(receivedData)})
.subscribe();
}
For the case that other services rely on this.nextStep() as well, you should implement this as a stream as well:
private initialData$ = new BehaviorSubject<any>(null);
constructor() {
this.initRestData()
.do(data => this.initialData$.next(data))
.switchMap(() => this.nextStep())
.subscribe();
}
nextStep(): Observable<any> {
return this.initialData$
.filter(data => data != null)
.take(1)
.map(data => {
// do the logic of "nextStep"
// and return the result
});
}
Related
I have a list of servers urls and making sequential http requests to them in a loop. When the success response arrives from the current request I want to break the loop and not to call all other servers. Could someone advice me how this could be handled in Angular/RxJS? Something like:
getClientData() {
for(let server of this.httpsServersList) {
var myObservable = this.queryData(server)
.pipe(
map((response: any) => {
const data = (response || '').trim();
if(data && this.dataIsCorrect(data)) {
return data; // **here I want to break from the loop!**
}
})
);
return myObservable;
}
}
private queryData(url: string) {
return this.http.get(url, { responseType: 'text' });
}
IMO it's better to avoid using a for loop for subscribing to multiple observables. It might lead to multiple open subscriptions. Common function used for this case is RxJS forkJoin. But given your specific condition, I'd suggest using RxJS from function with concatMap operator to iterator each element in order and takeWhile operator with it's inclusive argument set to true (thanks #Chris) to stop based on a condition and to return the last value.
import { from } from 'rxjs';
import { concatMap, filter, map, takeWhile } from 'rxjs/operators';
getClientData(): Observable<any> {
return from(this.httpsServersList).pipe(
concatMap((server: string) => this.queryData(server)),
map((response: any) => (response || '').trim()),
filter((data: string) => !!data && this.dataIsCorrect(data)) // <-- ignore empty or undefined and invalid data
takeWhile(((data: string) => // <-- close stream when data is valid and condition is true
!data || !this.dataIsCorrect(data)
), true)
);
}
Note: Try to tweak the condition inside the takeWhile predicate to match your requirement.
Edit 1: add inclusive argument in takeWhile opeartor
Edit 2: add additional condition in the filter operator
In angular we rely on RxJS operators for such complex calls
If you want to to call all of them in parallel then once one of them is fulfilled or rejected to cancel the other calls you should use
RxJS race learnrxjs.io/learn-rxjs/operators/combination/race
Or without RxJS you could use Promise.race
However if you want to call them in parallel and wait until first fulfilled "not rejected" or all of them rejected this is the case for Promise.any
Unfortunately no RxJS operator for it but on the follwoing article you could see how to implement this custom operator for Promise.any and an example for that operator
https://tmair.dev/blog/2020/08/promise-any-for-observables/
create a subject like this
responseArrived=new Subject();
and after pipe add takeuntil like this
var myObservable = this.queryData(server).pipe(takeUntil(responseArrived),map...
and in the line of code return data just call
responseArrived.next()
You can't use race because it will call all URLs in parallel, but you can use switchMap with recursive implementation
import { of, Observable, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators'
function getClientData(urls: string[]) {
// check if remaining urls
if (!urls.length) throw throwError(new Error('all urls have a error')); ;
return queryData(urls[0]).pipe(
switchMap((response) => {
const data = (response || '').trim();
if(data && this.dataIsCorrect(data))
// if response is correct, return an observable with the data
// for that we use of() observable
return of(data)
// if response is not correct, we call one more time the function with the next url
return getClientData(urls.slice(1))
}),
catchError(() => getClientData(urls.slice(1)))
);
}
function queryData(url: string): Observable<unknown> {
return this.http.get(url, { responseType: 'text' });
}
If your only condition is that you cancel requests once at least one response is received, can't just simply unsubscribe from the observable returned from the HttpClient call?
getData() {
const subscriptions = [];
[
'https://reqres.in/api/products/1',
'https://reqres.in/api/products/2',
'https://reqres.in/api/products/3',
].forEach((url, i) => {
subscriptions[i] = this.getClientData(url).subscribe(() => {
// Unsubscribe
subscriptions.forEach((v, j) => {
if (j !== i) {
console.log('Unsubscribe from ', j);
v.unsubscribe();
}
});
});
});
}
private getClientData(url: string) {
return this.httpClient.get(url, { responseType: 'text' }).pipe(
map((response: any) => {
const data = (response || '').trim();
if (data && true) return data;
return null;
})
);
}
I have a service layer responsible for handling the data and sending it to components that use the same value.
I need to make requests to the api, as long as the value is isProcessed == false it will do 10 attempts every half a second.
If it fails during the attempts, it will only make 3 more requests.
If isProcessed == true, stopped the requisitions.
All the logic is built in, but I can't output the last observable value. All responses are sent.
All requests are sent by observables, both false and true, but I only need the last one.
Here all requests responses are arriving in the component, not just the last
Service responsible for accessing API:
public getPositionConsolidate(): Observable<PosicaoConsolidada> {
return this.http.get<PosicaoConsolidada>(`${environment.api.basePosicaoConsolidada}/consolidado`)
.pipe(
map(res => res),
retryWhen(genericRetryStrategy()),
shareReplay(1),
catchError(err => {
console.log('Error in Position Consolidate', err);
return throwError(err);
})
)
}
Service responsible for handling the data and sending it to the component:
public positionConsolidate() {
let subject = new BehaviorSubject<any>([]);
this.api.getPositionConsolidate().subscribe(response => {
if(response.hasProcessado == false) {
for (let numberRequest = 0; numberRequest < 10; numberRequest++) {
setTimeout(() => {
//subject.next(this.api.getPosicaoConsolidada().subscribe());
this.api.getPositionConsolidate().subscribe(res => {
subject.next(res)
})
}, numberRequest * 500, numberRequest);
}
} else {
retryWhen(genericRetryStrategy()),
finalize(() => this.loadingService.loadingOff())
}
})
return subject.asObservable()
}
In component:
public ngOnInit() {
this.coreState.positionConsolidate().subscribe(res => console.log(res))
}
The easiest part to answer of your question, is that if you just want the last emission from an observable, then just use the last operator. However, the way you've written things, makes it difficult to incorperate. The following refactors your code as a single stream without any non-rxjs control structures.
public positionConsolidate() {
return this.api.getPositionConsolidate().pipe(
concatMap(res => iif(() => res.hasProcessado,
of(res),
interval(500).pipe(
take(10),
concatMap(() => this.api.getPositionConsolidate())
)
)),
retryWhen(genericRetryStrategy()),
finalize(() => this.loadingService.loadingOff()),
last()
);
}
What's happening
First this executes the initial api call.
Then based on the result, either...
Returns the initial result
Calls the api 10 times every 500ms.
Implements what you have for retryWhen and finalize.
Returns the last emitted result.
Also don't subscribe to observables inside of observables - that's what higher order observables such as concatMap are for.
I have two components that share data using a service:
export class ShareDataService {
msg: BehaviorSubject<string>;
constructor(
) {
this.msg = new BehaviorSubject("Default Message")
}
changeMessage(message: string) {
this.msg.next(message)
}
}
First component works as a router, when i click a name it redirects to the profile of that name using its id. I import the share-data service i created above, when clicking a name it triggers de sendData() function which gets the id of the clicked element and passes it as a parameter on the service changeMessagge() function:
constructor(
private _data: ShareDataService
) { }
message: string = "";
ngOnInit(): void {
this._data.msg.subscribe(new_msg => this.message = new_msg)
}
sendData() {
var self = this
$("ul.users-list").on("click", ".user", function(event) {
self._data.changeMessage(event.target.id)
});
}
}
After that, when it redirects to the new component, this new component also imports data from the service:
message: string = "";
constructor(
private _http: HttpClient,
private _data: ShareDataService,
private _router: Router
) { }
async ngOnInit() {
this._data.msg.subscribe(new_msg => this.message = new_msg)
await new Promise(r => setTimeout(r, 1)); // Sleeps 1ms so it can load message
if (this.message === "Default Message") { // Returns to landing if no user loaded
this._router.navigateByUrl("/landing");
return
}
this._http.get<UserInterface>(`https://jsonplaceholder.typicode.com/users/${this.message}`)
.subscribe((data: UserInterface) => this.user = data)
}
As you can see, it subscribes to the service and gets the value of messagge, which should have been changed when i clicked a name on the other component. The issue is, it fristly doesn't detect the change, and i have to use the new Promise() function to make it sleep for 1ms so it can load the new value of messagge from the service and then continue. Is there any way i can make it wait for the subscribe to finish loading the new messagge without using the Promise for 1ms? already tried putting the code inside the subscribe after it assigns messagge.
I see multiple issues
The variable this.message is assigned asynchronously. By the time the HTTP request is sent, it isn't assigned a value yet. You need to use higher order mapping operator like switchMap to map from one observable to another.
import { switchMap } from 'rxjs/operators';
this._data.msg.pipe(
switchMap(message =>
this._http.get<UserInterface>(`https://jsonplaceholder.typicode.com/users/${message}`)
)
).subscribe((data: UserInterface) => this.user = data)
You could see my post here for brief info on handling multiple co-dependent observables.
await new Promise(r => setTimeout(r, 1)); looks inelegant however small the timeout be. With the above mapping operator it shouldn't be needed anymore.
Avoid using jQuery in Angular. Almost everything from jQuery could be accomplished directly in Angular. Mixing them might lead to maintenance later.
Update: returning observable conditionally
Based on your condition, you could use RxJS iif function with new Observable() construct to either return the HTTP request or do something else and close the observable based on a condition.
Try the following
import { iif, EMPTY, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
this._data.msg.pipe(
switchMap(message =>
iif(
() => message === "Default Message",
new Observable(subscriber => {
this._router.navigateByUrl("/landing");
subscriber.complete();
}),
this._http.get<UserInterface>(`https://jsonplaceholder.typicode.com/users/${message}`)
)
)
).subscribe((data: UserInterface) => this.user = data)
I'm using Observable from rxJS in my Angular2 with TypeScript application. I would like to take a copy of the http get response data.
Service:
getSizes(sku: number): Observable<SizeList[]> {
let api = this.host + this.routes.sizes + sku;
return this._http.get(api)
.map((response: Response) => <SizeList[]>response.json())
.catch(this.handleError);
}
Component:
getSizes() {
this._productService.getSizes(this.productColour)
.subscribe(
sizes => this.sizes = sizes,
error => this.errorMessage = <any>error);
}
How can I take a copy of this.sizes? If I try to take a copy at the end of my components getSizes(), it's undefined.
I think that your problem is related to the asynchronous aspect of observables. At the end of the getSizes method, the data are there yet. They will be available within the subscribe callback:
getSizes() {
this._productService.getSizes(this.productColour)
.subscribe(
sizes => {
this.sizes = sizes;
console.log(this.sizes); // <------
},
error => this.errorMessage = <any>error);
}
If you want to return the value from the getSizes method, you need to return an observable and let the method caller subscribe on it:
getSizes() {
return this._productService.getSizes(this.productColour)
.catch(error => this.errorMessage = <any>error);
}
someOtherMethod() {
this.getSizes().subscribe(sizes => this.sizes = sizes);
}
This is because HTTP requests are made asynchronously in JS/Angular 2, so logic at the end of your getSizes() method is probably running before the method this._productService.getSizes(...) has finished loading your content.
You should place your logic therefore in the subscribe() method like this:
getSizes() {
this._productService.getSizes(this.productColour)
.subscribe(
sizes => {
this.sizes = sizes
// more logic here
},
error => this.errorMessage = <any>error);
// code here gets executed before the subscribe() method gets called
}
I can't get my head around observable and observer (rxjs), i understand that observable can dispatch the messages to observer, and observer subscribe to observable, but I dont know how to setup this?
Lets say I would like to request URL, first time the user call "loadData", the data is loaded from http request, and saved locally inside the class, next time the user call "loadData", we dont want to load from http, but get the data locally, but I would like to use the same code "loadData", and it should return Observer, so the developed don't know where and how the data is loaded!
let data = [];
function loadData():Observer {
var observer = new Observer();
if (data.length > 0) {
var observable = new Observable.from(data);
observable.add(observer);
observable.notify();
} else {
var observable = this.http.get("data.json");
observable.add(observer);
observable.readyData( (data) => {
this.data = data;
observable.notify();
};
}
}
var observer = loadData();
observer.dataComing((data) => console.log(data));
Any explanation or link to any page would be great, I understand map filter reduce in Array etc, and also the observer pattern which is easy, but not RXJS way, it's very confusing!
Thank you very much!
Here is sample of observer / observable:
var obs = Observable.create((observer) => {
setTimeout(() => {
observer.next('some event');
}, 1000);
});
obs.subscribe((event) => {
// The event is received here
});
The observer is used to trigger an event and the observable to receive it in short. Most of time, the observer is intervalle used. For example by the HTTP support of Angular2
Here are some links regarding reactive programming:
http://slides.com/robwormald/everything-is-a-stream
https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
For your particularly use case, you could use this:
loadData(url:string):Observable {
if (this.cachedData) {
return Observable.of(this.cachedData);
} else {
return this.get(...).map(res => res.map()).do((data) => {
this.cachedData = data;
});
}
}
Edit
I would refactor your code this way:
#Injectable()
export class LoungesService {
constructor(private http:Http) {
this.loungesByCity = {};
}
getLoungesByCity(city:City):Observable<any> {
if (this.loungesByCity && this.loungesByCity[city.id]) {
return Observable.of(this. loungesByCity[city.id]);
} else {
return this.http.get("lounges.json")
.map(res => <Lounge[]> res.json().data)
.map((lounges) => {
return lounges.filter((lounge) => lounge.city_id === city.id);
})
.do(data => this.loungesByCity[city.id] = data);
}
}
Note that LoungesService must be defined as shared service when bootstrapping your application:
bootstrap(AppComponent, [ LoungesService ]);