Bunching subscribers to in-flight async requests - RxJS - javascript

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

Related

Angular wait for all subscriptions to complete

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

How to handle the canceled request inside forloop in Angular.?

I have 5+ pages in my App. I have the following method on header component.
The aim is, I need to show the status in the header if the user clicks the particular button. If I make a minimal or slow navigation between pages below code works fine. But if I navigate pages very frequently, the request getting canceled, because in some other pages I am calling the different set of API's.
async geneStatus() {
for (const x of Object.keys(this.gene)) {
const operationId = this.gene[x]['name'];
let operArr;
try {
operArr = await this.fetchEachStatus(name);
} catch (err) {
continue;
}
if (operArr[0] && operArr[0] === 'error') {
continue;
}
// Doing my logics
}
fetchEachStatus(geneId): Promise<any[]> {
return new Promise((resolve, reject) => {
this.apiDataService.get(this.geneUrl+ '/' + geneId).subscribe(
(res) => {
setTimeout(() => {
resolve(res);
}, 500);
}, err => {
setTimeout(() => {
resolve(['error']);
}, 500);
});
});
}
Here the problem is if any one of the API gets cancelled the for loop is not iterating for the next elements. I need to iterate the loop if one API is get cancelled. How can I fix this issue? I am not sure where I am making the problem.
I see multiple issues. I think the conversion to observable to promise is not only unnecessary, but counter-productive. Using the observables directly would enable you to use RxJS functions and operators. We can use the forkJoin function to make multiple simultaneous requests and catchError operator to mitigate the effects of potential errors.
Try the following
import { forkJoin } from 'rxjs';
import { catchError } from 'rxjs/operators';
geneStatus() {
forkJoin(Object.keys(this.gene).map(gene => this.fetchEachStatus(gene['name']))).subscribe(
res => {
// res[0] - `{ success: true | false, geneId: geneId }` from `this.apiDataService.get(this.geneUrl + '/' + this.gene[0]['name'])`
// res[1] - `{ success: true | false, geneId: geneId }` from `this.apiDataService.get(this.geneUrl + '/' + this.gene[1]['name'])`
...
const passedGeneIds = res.filter(item => item.success).map(item => item.geneId);
// passedGeneIds = [`geneId`, `geneId`, ...] - list of passed gene IDs
const failedGeneIds = res.filter(item => !item.success).map(item => item.geneId);
// failedGeneIds = [`geneId`, `geneId`, ...] - list of failed gene IDs
// some other logic
},
error => {
// essentially will never be hit since all the errors return a response instead
}
);
}
fetchEachStatus(geneId): Observable<any> {
return this.apiDataService.get(this.geneUrl + '/' + geneId).pipe(
map(_ => ({ sucess: true, geneId: geneId })), // <-- map to return the `geneId`
catchError(error => of({ sucess: false, geneId: geneId })) // <-- retun an observble from `catchError`
);
}
Now you need to remember that each time a button is clicked multiple simultaeneous requests are triggered. One solution to overcome this issue is to cancel all current requests before triggering a new set of request. For that you could bind the buttons to emit a central observable and trigger the requests using switchMap operator piped to that observable.

Execute function on observable cancellation

I want to have an observable that when unsubscribed it calls a function but only when it is unsubscribed without error and without getting to complete. The observable I am trying to build usually gets raced with another observable. I want when the other observable "wins" this one executes a function.
I tried finalize operator but it executes always.
playback.ts
import { timer } from "rxjs";
import { takeUntil, finalize } from "rxjs/operators";
import errorobs$ from "./errorobs";
export default function() {
return timer(10000).pipe(
takeUntil(errorobs$),
finalize(finalFunc)
);
}
function finalFunc() {
console.log("final function executed");
}
errorobs.ts
import { fromEvent } from "rxjs";
import { map } from "rxjs/operators";
export default fromEvent(document.getElementById("errorBtn"), "click").pipe(
map(() => {
throw new Error("my error");
})
);
I have made a small demo here https://codesandbox.io/s/q7pwowm4l6
click start to start "the observable".
click cancel to make the other observable win
click error to generate an error
One way to achieve this is using a custom operator, like my onCancel() below:
const {Observable} = rxjs
function onCancel(f) {
return observable => new Observable(observer => {
let completed = false
let errored = false
const subscription = observable.subscribe({
next: v => observer.next(v),
error: e => {
errored = true
observer.error(e)
},
complete: () => {
completed = true
observer.complete()
}
})
return () => {
subscription.unsubscribe()
if (!completed && !errored) f()
}
})
}
// Test:
const {interval} = rxjs
const {take} = rxjs.operators
// This one gets cancelled:
const s = interval(200).pipe(
onCancel(() => console.warn('s cancelled!'))
).subscribe(() => {})
setTimeout(() => s.unsubscribe(), 500)
// This one completes before unsubscribe():
const q = interval(200).pipe(
take(2),
onCancel(() => console.warn('q cancelled!'))
).subscribe(() => {})
setTimeout(() => q.unsubscribe(), 500)
<script src="//unpkg.com/rxjs#6/bundles/rxjs.umd.min.js"></script>
It really works as you describe it. finalize is executed when the chain is being disposed which is when all subscribers unsubscribe, when the chain errors or when it completes.
There's already an issue on RxJS Github page for this feature: https://github.com/ReactiveX/rxjs/issues/2823
In the link above you can see an example of a custom operator that adds reason to the finalize operator.
I had to deal with this use-case myself and added this operator to my own collection of RxJS operators: https://github.com/martinsik/rxjs-extra/blob/master/doc/finalizeWithReason.md

How to attach function to the Observer while using Observable in Angular?

I know how to emit a value to the observer and subscribe to them using observable, as shown here
var observable = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
}).subscribe((success) => {
console.log(success);
})
but if I want to do the same thing with the function, ie. I have something like this, then how can I achieve it
var observable = new Observable(observer => {
observer.next(function () {
setTimeout(() => {
1
}, 1000)
})
observer.next(function () {
setTimeout(() => {
2
}, 1000)
})
observer.next(function () {
setTimeout(() => {
3
}, 1000)
})
}).subscribe((success) => {
console.log(success);
})
is it possible, all I have to do is call a series of async functions, how can I do it
UPDATE
i want to call a series of asnc fuctions in a sequence, ie. the second should be called only after the completion of the first functions operation and so on and so forth
You can do something like this. This is just the fundamental here. You can call your async instead of emitting static values.
var ParentObservable = new Observable();
ParentObservable.subscribe((res) => {
//res is your response from async calls
//Call asyncCall again from here
})
function asyncCall(){
this.http.get("your URL").map((res)=> res.json()).subscribe((res)=>{
ParentObservable.next(res);
})
}

React Jest Test Fails when setState Called in Promise

I'm trying to mock out the a service that returns promises so that I can verify it gets called with the correct parameters. The way the service is called varies based on the state and the first call to the service sets the state.
When setting the state in the promise it is not updating unless I wrap the assertion in setTimeout or completely stub out the promise. Is there a way to do this with just a plain promise and an expect?
My component:
class App extends Component {
constructor(props) {
super(props);
this.state = {results: []};
this.service = props.service;
this.load = this.load.bind(this);
}
load() {
if (this.state.results.length === 0) {
this.service.load('state is empty')
.then(result => this.setState({results: result.data}));
} else {
this.service.load('state is nonempty')
.then(result => this.setState({results: result.data}));
}
}
render() {
return (
<div className="App">
<button id="submit" onClick={this.load}/>
</div>
);
}
}
My test:
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return new Promise((resolve, reject) => {
resolve({data: [1, 2]});
});
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
//this assertion fails as the state has not updated and is still 'state is empty'
expect(mockLoad).toBeCalledWith('state is nonempty');
});
As mentioned, the following works, but I'd rather not wrap the expect if there's a way around it:
setTimeout(() => {
expect(mockLoad).toBeCalledWith('state is nonempty');
done();
}, 50);
I can also change how I mock the function to stub out the promise which will work:
const mockLoad = jest.fn((text) => {
return {
then: function (callback) {
return callback({
data : [1, 2]
})
}
}
});
But I'd like to just return a promise.
React batches setState calls for performance reasons, so at this point
expect(mockLoad).toBeCalledWith('state is nonempty');
the condition
if (this.state.results.length === 0) {
is most likely still true, because data has not yet been added to state.
Your best bets here are
Either use forceUpdate between the first and second click event.
Or split the test into two separate, while extracting common logic outside of the test. Even the it clause will become more descriptive, for instance: it('calls service correctly when state is empty') for the first test, and similar for the second one.
I'd favour the second approach.
setState() does not always immediately update the component. It may batch or defer the update until later.
Read more here.
Using Sinon with Sinon Stub Promise I was able to get this to work. The stub promise library removes the async aspects of the promise, which means that state gets updated in time for the render:
const sinon = require('sinon');
const sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return sinon.stub().returnsPromise().resolves({data: [1, 2]})();
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is nonempty');
});
See:
http://sinonjs.org/
https://github.com/substantial/sinon-stub-promise

Categories

Resources