Leave rxjs-for-await loop programatically - rxjs

I have app that opens stream where server pumps SSEs.
I want the loop to run until desired value arrives, then leave the loop and continue.
I've looked at takeWhile operator, but couldn't find a way to implement it. I also don't know how could I unsubscribe and since the stream never completes...
const stream = this.sseService.returnAsObservable();
for await (const data of eachValueFrom(stream)) {
console.log(data);
if (data.jobid === "JOB05879") {
this.sseService.stopConnection();
// how to get out now?
}
}
console.log('we are out');

Generally, mixing promises and observables is an anti-pattern. If you don't mind that, then here's a way to get only the first data where data.jobid === "JOB05879". You don't need for-await as you're just expecting 1 value from this stream.
const stream = this.sseService.returnAsObservable().pipe(
filter(data => data.jobid === "JOB05879"),
first()
);
data = await stream.toPromise();
console.log(data);
this.sseService.stopConnection();
console.log('we are done');
Without promises:
const stream = this.sseService.returnAsObservable().pipe(
filter(data => data.jobid === "JOB05879"),
first()
).subscribe({
next: data => {
console.log(data);
this.sseService.stopConnection();
},
error: _ console.log("Data with jobid JOB05879 not found"),
complete: () => console.log("We are done");
});
Update #1: Using forkJoin
You wrote the following which first grabs an array of finito and then uses that array as input for your notes.
const finito = await forkJoin([
this.jobsService.submitTestJob('blabla1').pipe(first()),
this.jobsService.submitTestJob('blabla2').pipe(first())
]).toPromise();
const notes = await forkJoin([
this.sseService.returnAsObservable().pipe(
filter((d: any) => d.jobid === finito[0].jobid),
first()
),
this.sseService.returnAsObservable().pipe(
filter((d: any) => d.jobid === finito[1].jobid),
first()
)
]).toPromise();
That should work, though there's a bunch of error checking you should do.
This can actually be simplified using RxJS operators so that you're not waiting on each finito
const notes = await merge(
this.jobsService.submitTestJob('blabla1').pipe(first()),
this.jobsService.submitTestJob('blabla2').pipe(first()),
this.jobsService.submitTestJob('blabla3').pipe(first()),
this.jobsService.submitTestJob('blabla4').pipe(first()),
this.jobsService.submitTestJob('blabla5').pipe(first()),
).pipe(
mergeMap(finito => this.sseService.returnAsObservable().pipe(
filter((d: any) => d.jobid === finito.jobid),
first()
)),
toArray()
).toPromise();
And another step to make it even more concise, if you'd like:
const notes = await from([
"blabla1",
"blabla2",
"blabla3",
"blabla4",
"blabla5"
]).pipe(
mergeMap(blabla =>
this.jobsService.submitTestJob(blabla).pipe(first())
),
mergeMap(finito => this.sseService.returnAsObservable().pipe(
filter((d: any) => d.jobid === finito.jobid),
first()
)),
toArray()
).toPromise();
Even more concise:
const notes = await from([1,2,3,4,5]).pipe(
map(num => `blabla${num}`),
mergeMap(blabla =>
this.jobsService.submitTestJob(blabla).pipe(first())
),
mergeMap(finito => this.sseService.returnAsObservable().pipe(
filter((d: any) => d.jobid === finito.jobid),
first()
)),
toArray()
).toPromise();
Making only 1 call to sseService
From the look of your example code, it seems it should be possible for you to make just one call to this.sseService.returnAsObservable(). It can filter through any of the allowable jobs.
That might look like this:
const params = [1,2,3,4,5];
const notes = await from(params).pipe(
map(num => `blabla${num}`),
mergeMap(blabla =>
this.jobsService.submitTestJob(blabla).pipe(first())
),
toArray(),
mergeMap(finitoArray => this.sseService.returnAsObservable().pipe(
filter(d => finitoArray.map(f => f.jobid).includes(d.jobid))
)),
take(params.length),
toArray()
).toPromise();
and to take this example back to the two-stage code you wrote, that would look like this:
const params = [1,2,3,4,5];
const streams = params
.map(num => `blabla${num}`)
.map(blabla => this.jobsService.submitTestJob(blabla).pipe(first())
const finito = await forkJoin(streams).toPromise();
const notes = await this.sseService.returnAsObservable().pipe(
filter(d => finitoArray.map(f => f.jobid).includes(d.jobid)),
take(params.length),
toArray()
).toPromise();

Related

Synchronous RxJs unsubscription not working

Demo: https://stackblitz.com/edit/rxjs-unsubscribe-issue?file=index.ts
Below code is not working
Error: Cannot read property 'unsubscribe' of undefined
const a = (): Observable<any> =>
new Observable(sub => {
sub.next(1);
return () => {
console.log('unsubscribe');
};
});
const observer = a().subscribe(
value => {
console.log('Subscription');
observer.unsubscribe();
},
e => console.log(e),
() => console.log('complete')
);
But the following code is working
const b = (): Observable<any> =>
new Observable(sub => {
setTimeout(()=>sub.next(1),0);
return () => {
console.log('unsubscribe');
};
});
const observer2 = b().subscribe(
value => {
console.log('Subscription b');
observer2.unsubscribe();
},
e => console.log(e),
() => console.log('complete')
);
Help me understand the reason behind it
as you mentioned in the title of your question, the first example is synchronous, so you get the first value while still inside of the .subscribe() method. Naturally, observer, which is supposed to have a Subscription object hasn't been initialized yet.
If you want to unsubscribe after receiving a single value I would suggest to use .take(1)

RxJs: Can you spread operators as arguments into pipe operator

I have two observable streams which do very separate mapping logic, but then ultimately end with following 3 operators:
this.selection
.pipe(
..Custom mapping operators
tap(_ => this.devicesLoading = true),
switchMap(d => this.mapService.findLocationForDevices(d)),
map(loc => marker([loc.latitude, loc.longitude])
)
.subscribe(markers => this.plotMarkers(markers));
I want to move the last tap, switchMap, map operators to a common function so I can just apply these within both of my observable streams.
I thought of doing:
private resolveLocationsAndConvertToMarkers = (devices: String[]) => [
tap(_ => this.devicesLoading = true),
switchMap((devices: string[]) => this.mapService.findLocationForDevices(devices)),
map(loc => marker([loc.latitude, loc.longitude])
];
But I wasn't sure how to spread these operators into the pipe arguments, like:#
this.selection
.pipe(
// Custom mapping operators
... this.resolveLocationsAndConvertToMarkers
)
.subscribe(markers => this.plotMarkers(markers));
this errors that there are no overloads that expect 3 or 5 arguments..
You can try use native .apply()
this.selection
.pipe.apply(null,this.resolveLocationsAndConvertToMarkers)
or wrap the list of operator in pipe()
private resolveLocationsAndConvertToMarkers = (devices: String[]) => pipe(
tap(_ => this.devicesLoading = true),
switchMap((devices: string[]) => this.mapService.findLocationForDevices(devices)),
map(loc => marker([loc.latitude, loc.longitude])
);
or return higher order function
private resolveLocationsAndConvertToMarkers = (devices: String[]) => source=>source.pipe(
tap(_ => this.devicesLoading = true),
switchMap((devices: string[]) => this.mapService.findLocationForDevices(devices)),
map(loc => marker([loc.latitude, loc.longitude])
);
You could try a reactive approach (with no side effects unless really isolated):
const preSelection$ = this.selection
.pipe
//..Custom mapping operators
();
const selection$: Observable<Marker> = preSelection$.pipe(
switchMap(preSelection =>
concat(
of(null),
of(preSelection).pipe(
switchMap(d => this.mapService.findLocationForDevices(d)),
map(loc => marker([loc.latitude, loc.longitude]))
)
)
),
shareReplay({ bufferSize: 1, refCount: true })
);
const isLoading$: Observable<boolean> = selection$.pipe(map(x => !!x));
const sideEffectUpdatePlotMarkers$ = selection$.pipe(
tap(markers => this.plotMarkers(markers))
);
// isolate `subscribe` calls and side effects as much as possible
sideEffectUpdatePlotMarkers$.subscribe();
I'm hoping this answer will help anyone else who stumbles across this question. The accepted answer did not exactly work for me, with the primary reason being null was passed as the first parameter for .apply() instead of my observable function again. Here is an example similar to what I successfully implemented in my project.
private pipeActions = [
filter(...),
map(...),
];
private myObservable = combineLatest(...);
doThing(): Observable<any> {
return this.myObservable
.pipe.apply(this.myObservable, [...this.pipeActions]);
}
doOtherThing(): Observable<any> {
return this.myObservable
.pipe.apply(
this.myObservable,
[...this.pipeActions, map(...)], // Do something additionally after my list of pipe actions
);
}

How to dispatch multiple actions from an effect in ngrx conditionally

I am a back-end developer starting with front-end development for a project I am working on. The front-end uses Angular7 and NgRx. I have studied a lot in the last 4 days, but here is something I am stuck with and would appreciate your help.
I learnt that we can dispatch multiple actions from an effect in NgRx by returning an Observable array having multiple actions. I want to dispatch one of the action in the array based on a condition.
My code looks something like this
#Effect()
something$: Observable<Action> = this.actions$.pipe(
ofType(ActionType),
switchMap.(action: any) => {
return service.call(action.payload)
.pipe(
switchMap((data: ReturnType) => [
new Action1(),
new Action2(),
]),
catchError(error handling)
);
}),
);
and I want to achieve something like this
#Effect()
something$: Observable<Action> = this.actions$.pipe(
ofType(ActionType),
switchMap.(action: any) => {
return service.call(action.payload)
.pipe(
switchMap((data: ReturnType) => [
if(condition)
new Action1()
else
new Action1.1() ,
new Action2(),
]),
catchError(error handling)
);
}),
);
I think its my lack of knowledge of RxJs, which is preventing me to implement the condition.
You can dispatch multiple actions or specific actions by letting conditional ifs determine what iterable to return
I recommend you read: https://www.learnrxjs.io/operators/transformation/switchmap.html
#Effect()
something$: Observable<Action> = this.actions$.pipe(
ofType(ActionType),
switchMap(action: any) => {
return service.call(action.payload)
.pipe(
switchMap((data: ReturnType) => {
let actionsToDispatch = [];
if(condition) {
actionsToDispatch.push(new SomeAction())
} else {
actionsToDispatch.push(new SomeOtherAction())
}
return actionsToDispatch
}),
catchError(error handling)
);
}),
);
To dispatch multiple actions you can pass the action array as shown below:
#Effect()
getTodos$ = this.actions$.ofType(todoActions.LOAD_TODOS).pipe(
switchMap(() => {
return this.todoService
.getTodos()
.pipe(
switchMap(todos => [
new todoActions.LoadTodosSuccess(todos),
new todoActions.ShowAnimation()
]),
catchError(error => of(new todoActions.LoadTodosFail(error)))
);
})
);
To dispatch actions conditionally you can wrap the actions in if/else as shown below:
#Effect()
getTodos$ = this.actions$.ofType(todoActions.LOAD_TODOS).pipe(
switchMap(() => {
return this.todoService
.getTodos()
.pipe(
switchMap(todos => {
if(true) {
return new todoActions.LoadTodosSuccess(todos),
} else {
return new todoActions.ShowAnimation()
}),
catchError(error => of(new todoActions.LoadTodosFail(error)))
);
})
);
Hope that helps!

RxJS: Make sure "tap" is fired even after unsubscribe

How can I make sure the tap operator is called even if a subscription is unsubscribed? Imagine the following:
function doUpdate() {
return this.http.post('somewhere', {})
.pipe(
tap(res => console.log(res))
)
}
const sub = doUpdate().subscribe(res => /* do something with res */);
setTimeout(() => sub.unsubscribe(), 1000)
In this case, I just want to prevent the subscribe action from being executed, yet I want to make sure the console log is fired, even if the request took longer than 1000 milliseconds to execute (and in this particular case, I don't even want the POST to be cancelled either).
use finalize() operator, although that will also get called when observable is completed
function doUpdate() {
return this.http.post('somewhere', {})
.pipe(
finalize(() => console.log(res))
)
}
some code to demonstrate the idea:
https://stackblitz.com/edit/rxjs-playground-test-m9ujv9
In Rxjs 7.4 tap now has three more subscribe handlers, so you can use it to get notified on subscribe, unsubscribe and finalize:
https://github.com/ReactiveX/rxjs/commit/eb26cbc4488c9953cdde565b598b1dbdeeeee9ea#diff-93cd3ac7329d72ed4ded62c6cbae17b6bdceb643fa7c1faa6f389729773364cc
So you can do:
const subscription = subject
.pipe(
tap({
subscribe: () => results.push('subscribe'),
next: (value) => results.push(`next ${value}`),
error: (err) => results.push(`error: ${err.message}`),
complete: () => results.push('complete'),
unsubscribe: () => results.push('unsubscribe'),
finalize: () => results.push('finalize'),
})
)
.subscribe();
Unsubscribing from a http request will definitely cancel the request, you could try shareReplay
function doUpdate() {
return this.http.post('somewhere', {})
.pipe(
tap(res => console.log(res)),
shareReplay()
)
}
and if that doesn't work the passing the result into a subject
function doUpdate() {
const subject = new Subject();
this.http.post('somewhere', {})
.pipe(
tap(res => console.log(res))
).subscribe(subject);
return subject;
}

How do you turn an epic into an async function?

The following code works without errors:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() => concat(
// fires an actionCreator and triggers another epic
of(actionOne()),
// fires an actionCreator
of(actionTwo())
))
);
The problem is that I need the data from actionOne to be available before actionTwo gets fired, and it doesn't seem to be happening. So I want to make this an async function like:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(async () => concat(
of(await actionOne()),
of(actionTwo())
))
);
This throws an error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
EDIT
Other relevant code:
// main component that loads
constructor(props) {
props.dispatch(init());
}
componentDidUpdate(prevProps) {
if (prevProps.actionTwoFlag !== this.props.actionTwoFlag) {
// do stuff with result from actionOne
// error is thrown here because there's no data
}
}
// actions
export const init = () => ({ type: "TEST" });
export const actionOne = () => ({ type: "ACTION_ONE" });
export const actionOneDone = (result) => ({ type: "ACTION_ONE_DONE", payload: result });
export const actionTwo = () => ({ type: "ACTION_TWO", payload: true });
// epics
export const actionOneEpic = (action$: any) => action$.pipe(
ofType("ACTION_ONE"),
mergeMap(() =>
ajax(..).pipe(
mergeMap(result => concat(
of(actionOneDone(result)),
...
))
)
)
);
);
There are various ways to solve this.
1- One way is just using defer() operator on the actionTwo. What defer() operator would do, is execute your code on subscription, since they are concatenated, the subscription to of(actionTwo()) would be done after of(actionOne()) is completed:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() => concat(
of(actionOne()),
defer(() => of(actionTwo()))
))
);
2- Another option is just do a switchMap(), this would ensure too that when you create the of(actionTwo()) observable, the of(actionOne()) observable has already been emitted and finished. switchMap() also ensures sequential order, so you can safely remove the concat() operator:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() =>
of(actionOne()).pipe(switchMap(() => of(actionTwo())))
)
);
EDIT:
Now I think I got it, although, I am not pretty familiar with redux observable epics. I have seen a solution here: Composing and sequencing multiple epics in redux-observable
that may solve your issue two. Based on that, I will give 2 proposals.
1st proposal:
This proposal just builds an epic that push action one at first, and waits for action one done in order to push the action two.
export const myEpic = (action$: any) => action$.pipe(
ofType('TEST'),
map(() => actionOne()),
mergeMap(() => {
return action$.pipe(
ofType('ACTION_ONE_DONE'),
take(1),
map(() => actionTwo()),
);
})
);
2nd proposal:
Do it all in one epic. Since both action one and action two are related (one depend on each other) it could make sense to merge both into only one epic, it would be something like this:
export const myEpic = (action$: any) => action$.pipe(
ofType('TEST'),
map(() => actionOne()),
mergeMap(() => {
return ajax(..).pipe(
mergeMap((data) => {
return concat(
actionOneDone(action),
of(actionTwo()).mergeMap(() => /* Do action two */ actionTwoDone())
)
}),
)
})
);
Hope this helps!

Resources