In Angular a page makes multiple http calls on multiple actions, let's say button clicks. But when the last "DONE" button is pressed I want to make sure that all those requests are finished before it progresses. I tried to use forkJoin with observables but it triggers requests itself which is not what I want to do, I want other actions to trigger requests and just to make sure that async requests are finished when "DONE" is clicked. With promises I would just push promises to array and then do Promise.all(allRequests).then(()=>{})
observables: Observable<any>[];
onBtn1Click(){
let o1 = this.service.doAction1();
this.observables.push(o1);
o1.subscribe(resp => {
//do action1
});
}
onBtn2Click(){
let o2 = this.service.doAction2();
this.observables.push(o2);
o2.subscribe(resp => {
//do action2
});
}
onDoneClick(){
// I would like something like this just that it wouldn't trigger the requests but make sure they are completed.
forkJoin(this.observables).subscribe(()=>{
//Proceed with other things
});
}
Unless someone comes up with an elegant approach, the following should do it.
I'm creating an object to hold hot observable for each cold observable from the HTTP request. The request would emit to it's corresponding hot observable using RxJS finalize operator. These hot observables could then be combined using forkJoin with a take(1) to wait for the source requests to complete.
private httpReqs: { [key: string]: ReplaySubject<boolean> } = Object.create(null);
onBtn1Click() {
this.httpReqs['btn1'] = new ReplaySubject<boolean>(1);
this.service.doAction1().pipe(
finalize(() => this.httpReqs['btn1'].next(true))
).subscribe(resp => {
// do action1
});
}
onBtn2Click() {
this.httpReqs['btn2'] = new ReplaySubject<boolean>(1);
this.service.doAction1().pipe(
finalize(() => this.httpReqs['btn2'].next(true))
).subscribe(resp => {
// do action2
});
}
onDoneClick(){
forkJoin(
Object.values(this.httpReqs).map(repSub =>
repSub.asObservable().pipe(
take(1)
)
)
).subscribe(() => {
// Proceed with other things
});
}
Using shareReplay
If you multicast, any subscriber who subscribes to a completed stream gets the complete notification. You can leverage that.
The various share operators have an implicit refCount that changes its default every few RxJS versions. The current version for shareReplay(n) is pretty intuitive, but you may need to set refCount:false on older versions, or even use multicast(new ReplaySubject(1)), refCount()
onBtn1Click(){
let o1 = this.service.doAction1().pipe(
shareReplay(1)
);
this.observables.push(o1);
o1.subscribe(resp => {
//do action1
});
}
This is the smallest change that should get your code working the way you'd like
Scan to count activity
You can avoid forkJoin entirely if you just count currently active operations.
count = (() => {
const cc = new BehaviorSubject<number>(0);
return {
start: () => cc.next(1),
stop: () => cc.next(-1),
value$: cc.pipe(
scan((acc, curr) => acc + curr, 0)
)
}
})();
onBtn1Click(){
this.count.start();
this.service.doAction1().pipe(
finalize(this.count.stop)
).subscribe(resp => {
//do action1
});
}
onDoneClick(){
this.count.value$.pipe(
first(v => v === 0) // Wait until nothing is currently active
).subscribe(() => {
//Proceed with other things
});
}
Related
I'm trying to use RxJS to process a stream of items, and I would like for it to retry any failures, preferentially with a delay (and exponential backoff if possible), but I need to guarantee the ordering so I effectively want to block the stream, and I don't want to reprocess any items that were already processed
So I was trying to play with retryWhen, following this example:
const { interval, timer } = Rx;
const { take, map, retryWhen, delayWhen, tap } = RxOperators;
const source = take(5)(interval(1000));
source.pipe(
map(val => {
if (val >= 3) {
throw val;
}
return val;
}),
retryWhen(errors =>
errors.pipe(
delayWhen(val => timer(1000))
)
)
);
But the stream restarts at the beginning, it doesn't just retry the last one:
Is it possible to achieve what I want? I tried other operators from docs as well, no luck. Would it be kinda against RxJS philosophy somehow?
The retryWhen should be moved to an inner Observable to handle the failed values only and keep the main Observable working.
Try something like the following:
// import { timer, interval, of } from 'rxjs';
// import { concatMap, delayWhen, map, retryWhen, take } from 'rxjs/operators';
const source = interval(1000).pipe(take(5));
source.pipe(
concatMap((value) =>
of(value).pipe(
map((val) => {
if (val >= 3) {
throw val;
}
return val;
}),
retryWhen((errors) => errors.pipe(delayWhen(() => timer(1000))))
)
)
);
Tried with below code not wait for post call success jumping to next iteration before response comes.
Requirement:Need to have next iteration after the success of two api(POST/PATCH) calls
for (item of data) {
A(item)
}
A(value) {
const resp = this.post(url, {
'rationale': value['rationale']
})
.mergeMap(tempObj => {
value['detail'] = tempObj['id']
return this.patch(url, value['extid'], value)
})
.subscribe()
}
Recently I have used the toPromise function with angular http to turn an observable into a promise. If you have the outer loop inside an async function, this may work:
// This must be executed inside an async function
for (item of data) {
await A(item)
}
async A(value) {
const resp = await this.post(url, {
'rationale': value['rationale']
})
.mergeMap(tempObj => {
value['detail'] = tempObj['id']
return this.patch(url, value['extid'], value)
}).toPromise();
}
Use from to emit the items of an array. Use concatMap to map to an Observable and only map to the next one when the previous completed.
const resp$ = from(data).pipe(
concatMap(value => this.post(url, { 'rationale': value['rationale'] }).pipe(
switchMap(tempObj => {
value['detail'] = tempObj['id']
return this.patch(url, value['extid'], value)
})
))
)
I used switchMap instead of mergeMap to indicate that the feature of mergeMap to run multiple Observables simultaneously isn't used.
You could use:
<form (ngSubmit)="submit$.next(form.value)" ...
In your component:
submit$= new Subject();
ngOnInit {
submit$.pipe(
exhaustMap(value =>
this.post(url, {'rationale': value.rationale}))
.pipe(concatMap( response => {
value.detail = response.id;
return this.patch(url, value.extid, value);
}))).subscribe(); // rember to handle unsubcribe
}
The reason I use exhaustMap generally post and path are mutating calls, so that operator ensures first submit is process ignore the rest while processing AKA avoid double submit
An even better way is using ngrx effects, which if you dont know it yet I recomend to learn it
submit$ = createEffect(
() => this.actions$.pipe(
ofType(FeatureActions.submit),
exhaustMap( ({value}) => // if action has value property
this.post(url, { rationale : value.rationale}))
.pipe(concatMap( response => {
value.detail = response.id;
return this.patch(url, value.extid, value);
})),
map(result => FeatureActions.submitSuccess(result))
)
);
I have some functions that call other async functions and return the results as Observables. These functions can be subscribed to many times throughout different parts of my application at the same time.
I would like to prevent running the async function again if it's still "in-flight", however still emit the value to all subscribers once it completes. If not in-flight it should call the async function again.
Is there a better pattern for this; am I approaching this the wrong way?
What I have done is create a subject for storing the result, and a flag for keeping track of the in-flight request.
inFlight = false;
subject$ = new Subject<any>();
requestsLog = [];
getThing() {
console.log("(getThing) running?", this.inFlight);
return iif(() => this.inFlight, this.subject$, this.fakeAsyncRequest$())
.pipe(
take(1),
tap(date => console.log("(getThing) get value", date)),
tap(date => this.requestsLog.push(date))
);
}
fakeAsyncRequest$ = () => {
return of(new Date().toUTCString()).pipe(
tap(_ => {
console.log("(fakeAsyncRequest) request");
this.requestsLog = []; // Reset things
this.inFlight = true; // Set in flight flag
}),
delay(1500), // Simulate async delay
tap(date => this.subject$.next(date)),
finalize(() => {
console.log("(fakeAsyncRequest) Done");
this.inFlight = false;
})
);
};
smilutateMultiple() {
// Simulate a few calls to this function
this.getThing().subscribe();
this.getThing().subscribe();
this.getThing().subscribe();
setTimeout(() => {
this.getThing().subscribe();
}, 500);
}
I also tried to use a BehaviourSubject in combination with an ExhaustMap but the inner observable still gets called for each subscription to the observable.
private subject$ = new BehaviorSubject<any>(false);
public subjectObs$ = this.subject$
.asObservable()
.pipe(exhaustMap(() => this.fakeAsyncRequest()));
fakeAsyncRequest = () => {
console.log("call fake request", new Date().toUTCString());
return this.http
.get("https://www.reddit.com/hot.json")
.pipe(delay(1000));
};
smilutateMultiple() {
// Simulate a few subscriptions to this observable
this.subjectObs$.subscribe(thing => console.log("Thing", thing));
this.subjectObs$.subscribe(thing => console.log("Thing", thing));
// Should be same request.
setTimeout(() => {
this.subjectObs$.subscribe(thing => console.log("Thing", thing));
}, 500);
// Should be new request.
setTimeout(() => {
this.subjectObs$.subscribe(thing => console.log("Thing", thing));
}, 3000);
}
Reactive JS module - RxJS is certainly the best way to avoid this. In RxJS, you can have a state which looks like below
export interface ProjectState extends EntityState<Project> {
selectedProjectId: string;
creating: boolean;
created: boolean;
loading: boolean;
loaded: boolean;
error: string;
}
In such a state, when you fire an action - LOAD_PROJECTS, the reducer will mark the flag "loading" to true. Now, in other parts of your application, you can subscribe to "projects' entities and "loading" flag. If loading flag is false, dispatch the LOAD_PROJECTS else do not dispatch.
The subscription to "projects" entities will ensure that the subscriber inside every component is called when your data is updated. Thus preventing any additional calls.
RxJS might seem complicated and complex in the beginning. However, it is one of the best structured library for sharing state across application.
I was looking for the share operator. This "shares" the source observable with multiple subscribers.
This relates to the concepts of "hot vs cold" observables and "multicasting".
Demo: https://stackblitz.com/edit/angular-akfcy5
public observable$ = this.fakeAsyncRequest$().pipe(share());
private fakeAsyncRequest$() {
return this.http.get("https://www.reddit.com/hot.json").pipe(
tap(() => console.log("Call fake request at:", new Date().toUTCString())),
delay(500)
);
}
smilutateMultiple() {
// Simulate a few subscriptions to this observable
this.observable$.subscribe(thing => console.log("Thing 1", thing));
this.observable$.subscribe(thing => console.log("Thing 2", thing));
// Should be same request.
setTimeout(() => {
this.observable$.subscribe(thing => console.log("Thing 3", thing));
}, 500);
// Should be new request.
setTimeout(() => {
this.observable$.subscribe(thing => console.log("Thing 4", thing));
}, 3000);
}
You could create a facade, use it to retrieve the data throughout different parts of your application and use the exhaustMap operator.
More information to the exhaustMap operator: https://rxjs-dev.firebaseapp.com/api/operators/exhaustMap
We have the following stream.
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
const recorders = names
.map(entry => entry.name)
.filter(entry => !_.contains(
['recorders.starts',
'recorders.sources',
'system.indexes',
'system.users'],
entry));
console.log(recorders);
return Rx.Observable.fromArray(recorders);
});
recorders.isEmpty()
.subscribe(
empty => {
if(empty) {
logger.warn('No recorders found.');
}
},
() => {}
);
recorders.flatMapConcat(createRecorderIntervals)
.finally(() => process.exit(0))
.subscribe(
() => {},
e => logger.error('Error while updating: %s', e, {}),
() => logger.info('Finished syncing all recorders')
);
If the stream is empty then we don't want to createRecorderIntervals. The above piece of code is working. However, checking if the stream is empty, is causing the console.log to be executed twice. Why is this happening? Can I fix it somehow?
EDIT: So, I went the following way, after rethinking it thanks to #Martin's answer
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
const recorders = names
.map(entry => entry.name)
.filter(entry => !_.contains(
['recorders.starts',
'recorders.sources',
'system.indexes',
'system.users'],
entry));
if(!recorders.length) {
logger.warn('No recorders found.');
return Rx.Observable.empty();
}
return Rx.Observable.fromArray(recorders);
})
.flatMapConcat(createRecorderIntervals)
.finally(() => scheduleNextRun())
.subscribe(
() => {},
e => logger.error('Error while updating: %s', e, {}),
() => logger.info('Finished syncing all recorders')
);
When you call subscribe() method on an Observable it causes the entire chain of operators to be created which it turn calls imongo.listCollections('recorders') twice in your case.
You can insert an operator before calling flatMapConcat(createRecorderIntervals) that checks whether the result is empty. I have one of them in mind particularly but there might be other that suit your needs even better:
takeWhile() - takes predicate as an argument and emits onComplete when it return false.
Then your code would be like the following:
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
...
return Rx.Observable.fromArray(recorders);
})
.takeWhile(function(result) {
// condition
})
.flatMapConcat(createRecorderIntervals)
.finally(() => process.exit(0))
.subscribe(...);
I don't know what exactly your code does but I hope you get the idea.
Edit: If you want to be notified when the entire Observable is empty than there're a multiple of ways:
do() operator and a custom Observer object. You'll write a custom Observer and put it using do() operator before .flatMapConcat(createRecorderIntervals) . This object will count how many times its next callback was called and when the preceding Observable completes you can tell whether there was at least one or there were no results at all.
create a ConnectableObservable. This one is maybe the most similar to what you we're doing at the beginning. You'll turn your recorders into ConnectableObservable using publish() operator. Then you can subscribe multiple Observers without triggering the operator chain. When you have all your Observers subscribed you call connect() and it'll sequentially emit values to all Observers:
var published = recorders.publish();
published.subscribe(createObserver('SourceA'));
published.subscribe(createObserver('SourceB'));
// Connect the source
var connection = published.connect();
In your case, you'd create two Subjects (because they act as Observable and Observer at the same time) and chain one of them with isEmpty() and the second one with flatMapConcat(). See the doc for more info: http://reactivex.io/documentation/operators/connect.html
I think the first option is actually easier for you.
I'm new to Angular 2 so maybe this is trivial - I'm trying to figure out how to cancel an Rx.Observable interval call that happens every 2 seconds. I subscribe to the Rx.Observable with this:
getTrainingStatusUpdates() {
this.trainingService.getLatestTrainingStatus('train/status')
.subscribe(res => {
this.trainingStatus = res;
if (this.trainingStatus == "COMPLETED") {
//Training is complete
//Stop getting updates
}
});
}
This is how my Rx.Observable interval call is handled in my service.ts file (this is called by the function above from a different file):
getLatestTrainingStatus(url: string) {
return Rx.Observable
.interval(2000)
.flatMap(() => this._http.get(url))
.map(<Response>(response) => {
return response.text()
});
}
As you can see in the subscription method, I simply want to stop the interval calls (every 2 seconds) when 'trainingStatus' is 'COMPLETED'.
That's possible with the unsubscribe option.
Every .subscribe() returns a subscription object. This object allows you to unsubscribe to the underlying Observable.
getTrainingStatusUpdates() {
// here we are assigning the subsribe to a local variable
let subscription = this.trainingService.getLatestTrainingStatus('train/status')
.subscribe(res => {
this.trainingStatus = res;
if (this.trainingStatus == "COMPLETED") {
//this will cancel the subscription to the observable
subscription.unsubscribe()
}
});
}
getTrainingStatusUpdates() {
this.trainingService.getLatestTrainingStatus('train/status')
.do(res => this.trainingStatus = res)
.first(res => res == "COMPLETED")
.subscribe(/* Do stuff, if any */);
}
You don't have to unsubscribe / stop the interval. It will be stopped when res == "COMPLETED"