Capture last value of an observable with multiple requests - javascript

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.

Related

RxJs concatMap is emitting only one http stream out of many

I have been trying to get stream of objects in a sequencial order, however concatMap is not working for me. With mergeMap I get the stream but no in a sequencial order.
Below is my code I am trying:
this.result
.pipe(
map((result: Result) => {
return result.contents
.map((content: Content) => this.databaseService
.getResource<Resource>('Type', content.key) // http call in service
)
}),
mergeMap((requests: Observable<Resource>[]) => {
return from(requests)
.pipe(
concatMap((resource: Observable<Resource>) => resource), // ----> Trigger only once.
filter((resource:Resource) => resource.status === 'active'),
skip(this.pageIndex * this.PAGE_SIZE),
take(this.PAGE_SIZE),
)
}),
)
.subscribe({
next: (resource: Resource) => {
console.log(resource) // resource stream
},
complete: () => console.log('completed')
});
concatMap will only process one observable source at a time. It will not process subsequent sources until the current source completes.
In your case the observable from this.databaseService.getResource() is not completing. To force it to complete after the first emission, you could append a take(1) like this:
concatMap((resource: Observable<Resource>) => resource.pipe(take(1))
Alternatively, if the call to databaseService.getResource() is only meant to emit a single value, you could modify your service to emit only a single value.
// http call in service
Note: If you are using the Angular HttpClient, a typical get request will complete when the response is received. So you can probably have your getResource() method return the oservable from the http.get() call.

Angular: how to return the first successful response from the list of http requests

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;
})
);
}

NGRX state not updated when Firestore changes occur while offline

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.

Cannot get result of first HTTP request using RxJS' switchMap

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) => { ... })
);

React Redux app - complex init action composition executes final promise before the others are done

I'm working on an app which has to manage a large amount of data.
In the init process several api calls must be done while the user sees a loading bar.
Here is my init action:
export function init(key) {
return (dispatch, getState) => {
// start init
dispatch(initStart());
setTimeout(() => {
dispatch(initProcess(10));
}, 0)
return Promise.all([
// load users
dispatch(loadUsers(key)).then(() => {
dispatch(initProcess(30));
}),
// load other stuff
// ...
// load articles
dispatch(loadArticles(key)).then(() => {
dispatch(initProcess(60));
}),
// should be executed after all actions/reducers are done
]).then(() => {
setTimeout(() => {
dispatch(initFinish());
}, 700);
});
}
}
So far it works perfectly, but there will be 20k to 50k articles. The backend has to perform some joins to get the data together, so I'm sure I'll get a server timeout if I try to get them in one piece.
The idea is to fetch the total number first and then get the articles in 1k pieces in a loop. But it wont work the way I need it. I'm getting initFinish dispatched after the articles are counted but not after they are fetched.
Here is the loadArticles action:
export function loadArticles(key) {
return (dispatch, getState) => {
// check local db first
// get total number
return dispatch(countArticles(key))
.then(result => {
Promise.all([
// No idea how to put the loop in here :(
dispatch(fetchArticles(key, 1000)),
])
});
}
}
I have no loop yet but thats not the point. The logic remains the same. I return the dispatch(countArticles(key)) before fetchArticles is done.
Has anyone a hint? That would be awesome.
EDIT
coutArticles and fetchArticles
function countArticles(key) {
return {
[CALL_API]: {
types: [ COUNT_ARTICLES_REQUEST, COUNT_ARTICLES_SUCCESS, COUNT_ARTICLES_FAILURE ],
endpoint: `articles`,
schema: Schemas.ARTICLE_COUNTER
}
}
}
function fetchArticles(key, take, skip) {
return {
[CALL_API]: {
types: [ FETCH_ARTICLES_REQUEST, FETCH_ARTICLES_SUCCESS, FETCH_ARTICLES_FAILURE ],
endpoint: `articles/${take}/${skip}`,
schema: Schemas.ARTICLE_ARRAY
}
}
}
The middleware is the same es here
2. EDIT
if i change
// get total number
return dispatch(countArticles(key))
.then(result => {
Promise.all([
// No idea how to put the loop in here :(
dispatch(fetchArticles(key, 1000)),
])
});
to
// get total number
dispatch(countArticles(key))
.then(result => {
return Promise.all([
// No idea how to put the loop in here :(
dispatch(fetchArticles(key, 1000)),
])
});
I get Uncaught TypeError: Cannot read property 'then' of undefined on dispatch(loadArticles(key)).
3. EDIT
Some days later I'm still fighting ^^
Here is the simplified init function, which should (only) get the count result and then fetch the articles:
But for now im failing already here:
export function init(key) {
return (dispatch, getState) => {
countArticles(key).then(result => {
console.log(result);
});
}
}
Output:
Uncaught TypeError: countArticles(...).then is not a function
I had problems with chaining dispatch as well; it should return a Promise but I could not get it to work.
So I would change the logic this way
countArticles(key).then(result => {
dispatch( {
type: RECEIVED_NUM_ARTICLES,
value: result
})
Promise.all([...Array(Math.floor(result/1000))].map(_ =>
fetchArticles(key,1000).then(res => dispatch( {
type: RECEIVED_1000_ARTICLES,
value: res
})
)).then( _ => dispatch({
type: RECEIVED_EVERYTHING
value: 'whatever'
})
)
)
(I did not test this code)
But basically: fetch then dispatch the result, then chain another fetch/dispatch sequence, etc...
The loop needs rework to fetch the modulo
The advantage with this approach is that you can update the UI with the number of articles when you have them, then provide updates on every 1000 articles fetched and finally notify when done

Categories