I'm using a concatMap to handle multiple requests to an API, where I want each request batch to be completed before the next batch is processed. The concatMap works as expected when triggering the flow with callSubject.next(requestData)
The problem: for certain types of requestData I want to cancel any in-flight http calls, and reset the concatMap. Cancelling the httpClient calls that are occurring within the getAll function is handy enough (I have a takeUntil that does that - not shown), but the concatMap may still have a number of queued up requests that will then be processed.
Is there a way to reset the concatMap without completing the callSubject Subject?
Note: if I trigger unsubscribeCallSubject$.next() this clears the concatmap, but also completes the callSubject, which means it can no longer be used with callSubject.next(reqData)
// callSubject is a Subject which can be triggered multiple times
callSubject
.pipe(
concatMap((req) => {
// getAll makes multiple httpClient calls in sequence
return getAll(req).pipe(
catchError((err) => {
// prevent callSubject completing on http error
return of(err);
})
);
}),
takeUntil(unsubscribeCallSubject$)
)
.subscribe(
(v) => log("callSubject: next handler", v),
(e) => log("callSubject: error", e),
() => log("callSubject: complete")
);
If I understand the problem right, you could try an approach which uses switchMap any time unsubscribeCallSubject$ emits.
The code would look something like this
unsubscribeCallSubject$.pipe(
// use startWith to start the stream with something
startWith('anything'),
// switchMap any time unsubscribeCallSubject$ emits, which will unsubscribe
// any Observable within the following concatMap
switchMap(() => callSubject$),
// concatMap as in your example
concatMap((req) => {
// getAll makes multiple httpClient calls in sequence
return getAll(req).pipe(
catchError((err) => {
// prevent callSubject completing on http error
return of(err);
})
);
}),
)
.subscribe(
(v) => log("callSubject: next handler", v),
(e) => log("callSubject: error", e),
() => log("callSubject: complete")
);
To be honest I have not tested this approach and so I am not sure whether it solves your problem, but if I have understood your problem right, this could work.
Related
Use case: call an endpoint every 3-minutes that would update a status of a certain service over the application.
my current code:
interval(180000)
.subscribe(() => this.doRequest
.pipe(catchError(() => {
this.applicationFlag = false;
return EMPTY;
}))
.subscribe(result => this.applicationFlag = result));
My current problem is that sometimes the previous interval request has not been completed yet but the next interval request was also doing a request.
Is there a way to flag to wait for previous or don't execute the interval when the previous request is not completed yet?
When you have one subscribe inside another subscribe there's no way the outer chain can be notified that the inner chain has completed. You have to restructure your chain and use operators such as concatMap, mergeMap or switchMap. For example like the following:
interval(180000)
.pipe(
concatMap(() => this.doRequest.pipe(
catchError(() => {
this.applicationFlag = false;
return EMPTY;
}),
),
)
.subscribe();
What's the best way to write this short pooling routine using rx.js
1. call the function this.dataService.getRowsByAccountId(id) to return Observable<Row[]> from back-end
2. send the received data to this function this.refreshGrid(data);
3. if one of items in the data meet this criteria r.stillProcessing==true
4. then wait 2 seconds and start again from step-1
5. if another call was made to this routine and there is a pending timer scheduled. Don't schedule another one because i don't want multiple timers running.
I think the best solution would be using retryWhen
I don't know if this will work out of the box with your code, but according to your comment try to tweak this.
this.dataService.getRowsByAccountId(id)
.pipe(
tap((data: Row[]) => this.refreshGrid(data)),
map((data: Row[]) => {
if (data.some((r: Row) => r.stillProcessing === true) {
//error will be picked up by retryWhen
throw r.Name; //(or something else)
}
return r;
}),
retryWhen(errors =>
errors.pipe(
//log error message
tap((r: Row) => console.log(`Row ${r} is still processing!`)),
//restart in 2 seconds
delayWhen(() => timer(2000))
)
).subscribe();
);
I have few Observables like this one in my code.
this.server.doRequest().subscribe(response => console.log(response)
error => console.log(error),
() => {
console.log('completed');
});
There could be any number of these Observables,
so I need to write a function that checks if each Observable is done otherwise waits till each is finished.
I'm assuming I can create an array push every new Observable there and when it's completed remove it by index. But is it good solution?
Where I want to use it. For example I have a page where user upload photos any amount asynchronously and then he press Finish button. Once he pressed Finish button I need to wait till ALL dynamically created Observables are completed.
you should use higher order observables for this, your exact use case will dictate the exact operator, but forkJoin seems a good candidate:
forkJoin(
this.server.doRequest1(),
this.server.doRequest2(),
this.server.doRequest3(),
this.server.doRequest4()
).subscribe(vals => console.log('all values', vals));
forkJoin won't emit till all innter observables have completed. making it the operator of choice for waiting for multiple observables to complete. You can also feed it an array of observables. There are multiple other operators that may fulfill your case too, such as concat, merge, combineLatest or a few others.
edit based on more details:
in the use case described in your update, you'll still want to use a higher order observable, but forkjoin is not what you want. you'll want to use a local subject to accomplish the goal as wanting to kick off each observable as it is selected and waiting for them all to be done complicates things a little (but not too much):
suppose you had a template like:
<button (click)="addPhoto()">Add Photo</button>
<button (click)="finish()">Finish</button>
where the add photo button gets the users photo and all that, and finish is your completion, you could have a component like this:
private addPhoto$ = new Subject();
constructor() {
this.addPhoto$.pipe(
mergeMap(() => this.uploadPhoto()),
).subscribe(
(resp) => console.log('resp', resp),
(err) => console.log('err', err),
() => console.log('complete')
);
}
private uploadPhoto() {
// stub to simulate upload
return timer(3000);
}
addPhoto() {
this.addPhoto$.next();
}
finish() {
this.addPhoto$.complete();
}
if you run this code, you'll see that the photo adds will emit in the subscribe handler as they complete, but complete will only fire once all the photo uploads have completed and the user has clicked finish.
here is a stackblitz demonstrating the functionality:
https://stackblitz.com/edit/angular-bsn6pz
I'd create a dictionary (in javascript that would be a JSON with observable names as boolean properties) where you push each observable on "create" and a method which should execute on completion of each observable, which will iterate through that dictionary and if all completed do something.
That will ensure parallelism and final execution after all completed.
var requests = {
doRequest1: false,
doRequest2: false,
doRequest3: false
};
var checkIfCAllCompleted = name => {
requests[name] = true;
for (var property in requests) {
if (object.hasOwnProperty(property)) {
if (!property) {
return;
}
}
}
// all properties are true - do something here
console.log("here");
}
this.server.doRequest1().then(() => checkIfCAllCompleted("doRequest1"));
this.server.doRequest2().then(() => checkIfCAllCompleted("doRequest2"));
this.server.doRequest3().then(() => checkIfCAllCompleted("doRequest3"));
I have an array of objects. For each object I need to trigger an asynchronous request (http call). But I only want to have a certain maximum of requests running at the same time. Also, it would be nice (but not neccessary) if I could have one single synchronization point after all requests finished to execute some code.
I've tried suggestions from:
Limit number of requests at a time with RxJS
How to limit the concurrency of flatMap?
Fire async request in parallel but get result in order using rxjs
and many more... I even tried making my own operators.
Either the answers on those pages are too old to work with my code or I can't figure out how to put everything together so all types fit nicely.
This is what I have so far:
for (const obj of objects) {
this.myService.updateObject(obj).subscribe(value => {
this.anotherService.set(obj);
});
}
EDIT 1:
Ok, I think we're getting there! With the answers of Julius and pschild (both seem to work equally) I managed to limit the number of requests. But now it will only fire the first batch of 4 and never fire the rest. So now I have:
const concurrentRequests = 4;
from(objects)
.pipe(
mergeMap(obj => this.myService.updateObject(obj), concurrentRequests),
tap(result => this.anotherService.set(result))
).subscribe();
Am I doing something wrong with the subscribe()?
Btw: The mergeMap with resultSelector parameter is deprecated, so I used mergeMap without it.
Also, the obj of the mergeMap is not visible in the tap, so I had to use tap's parameter
EDIT 2:
Make sure your observers complete! (It cost me a whole day)
You can use the third parameter of mergeMap to limit the number of concurrent inner subscriptions. Use finalize to execute something after all requests finished:
const concurrentRequests = 5;
from(objects)
.pipe(
mergeMap(obj => this.myService.updateObject(obj), concurrentRequests),
tap(res => this.anotherService.set(res))),
finalize(() => console.log('Sequence complete'))
);
See the example on Stackblitz.
from(objects).pipe(
bufferCount(10),
concatMap(objs => forkJoin(objs.map(obj =>
this.myService.updateObject(obj).pipe(
tap(value => this.anotherService.set(obj))
)))),
finalize(() => console.log('all requests are done'))
)
Code is not tested, but you get the idea. Let me know if any error or explanation is needed
I had the same issue once. When I tried to load multiple images from server. I had to send http requests one after another. I achieved desired outcome using awaited promise. Here is the sample code:
async ngOnInit() {
for (const number of this.numbers) {
await new Promise(resolve => {
this.http.get(`https://jsonplaceholder.typicode.com/todos/${number}`).subscribe(
data => {
this.responses.push(data);
console.log(data);
resolve();
}
);
});
}
}
Main idea is here to resolve the promise once you get the response.
With this technique you can come up with custom logic to execute one method once all the requests finished.
Here is the stackblitz. Open up the console to see it in action. :)
I have an observable that submits the form submit$. This observable may end up with error with status code 403 what means that a user is not authorised and has to log in first.
Is there a way where I could on specific error code invoke another observable which performs an authorisation process. When authorisation is succeeded I want to repeat the submit$ without having user to invoke that observable once again.
To illustrate steps I want to have:
A user tries to submit and submit$ is being subscribed
This ends up with error with status code 403
Observable calls another authorise$ observable which has own workflow
When authorise$ succeeds the submit$ is invoked again
The process completes with success or error
If There is an error during authorise$ abort the submit$ process
I tried an approach here where I am separating into two observables, submit$ and authenticationSubmit$, and then I merge them again. I haven't tested it, and I am writing http.post(...) twice, so it is not exactly as you described it.
import { merge } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
...
const submit$ = http.post(...);
const authenticationAndSubmit$ = submit$.pipe(
filter(httpResponse => httpResponse.status === 403),
switchMap(() => authService.login()),
filter(authResult => authResult === 'success'),
switchMap(() => http.post(...))
);
merge(submit$, authenticationAndSubmit$)
.pipe(
filter(httpResponse => httpResponse.status === 200),
)
.subscribe(httpResponse => {
// Do something
});