Given an Stream within an Observable, I want to validate/check each item. In case one is broken I want to throw an error via Observable.throw, hence break all further processing.
My clunky solution would be
import * as Rx from 'rxjs'
inputStream.mergeMap(item => (isValid(item))
? Rx.Observable.of(item)
: Rx.Observable.throw(new Error("not valid"))
)
This seems ugly, as it constructs for the positive flow a bunch of unnecessary Observables.
Is there a better way to check items in an Observable?
You can use just normal map and throw an exception inside it:
inputStream.map(item => {
if (isValid(item)) {
return item;
}
throw new Error("not valid");
})
If I understand your question well, you can use takeWhile operator to do this. For example;
yourObservable.takeWhile(item => {
//your condition
}).subscribe(i => console.log(i));
It just takes values while your expression is true. When it becomes false, it stops.
You can learn more from here. Also you can check this page out. I hope it helps!
Related
I'm writing an angular15 app with a youtube player component in it, i'm trying to work with rxjs but i think that i have one issue that i got wrong, the mergeMap. i'm really new to rxjs so sorry for any mistakes
I have 2 subscriptions, one for if youtube library as finished loading, and the other if the youtube player is ready.
first lets look just at the interval:
this.YTSubscription=interval(100).pipe(
exhaustMap((x, y)=>{
this.currentTimeSubject.next(this.player.getCurrentTime());
this.isPlayingSubject.next(this.player.getPlayerState() === YT.PlayerState.PLAYING);
this.isMutedSubject.next(this.player.isMuted());
this.volumeSubject.next(this.player.getVolume());
return of(true);
}),
).subscribe({next: (data )=>{
},
error: (err)=> {
this.YTSubscription?.unsubscribe();
}
});
this works fine, it runs in intervals on 100ms and i use exhaustMap to make sure that the next iteration will be executed only if the previous one completed in case when i'll add more calculations it may take more than 100 ms.
next i want in the interval to check if youtube is loaded, for that i have the observable isYouTubeLoaded, so i tried using mergeMap for this.. i guess this is not the right way? but it still worked:
this.YTSubscription=interval(100).pipe(
mergeMap(x => this.isYouTubeLoaded),
exhaustMap((x, y)=>{
if (!x) {
return of(false);
}
...
now x inside exahustMap contains the isYouTubeLoaded and this does the job.
now i have another observable that i want to check and only if both of them are true to run the interval, if not to wait for the next iteration, this is where i get lost because if i add another mergeMap i can't see both values in exhaustMap.
so from reading some more i assume that i'm not supposed to use mergeMap at all, maybe filter ? but i still have no clue how to do that with 2 observables.
any ideas?
I'm not entirely sure, what you want to do, but I'll try to answer this part of your question:
now i have another observable that i want to check and only if both of them are true to run the interval, if not to wait for the next iteration, this is where i get lost because if i add another mergeMap i can't see both values in exhaustMap.
combineLatest([src1, src2]).pipe( // check both
filter(([ok1, ok2]) => ok1 && ok2), // only if both are true
switchMap(() => timer(...) // run the timer
).subscribe(...);
#churill really helped, in the end i need two pipes and not 3 but the implementation is the same, still marking his answer as the correct one, just showing here the resulting code:
this.YTSubscription=combineLatest([interval(100), this.isYouTubeLoaded]).pipe(
map(([intr, loaded])=>(loaded)),
filter((loaded)=> (loaded)),
exhaustMap(()=>{
try {
if (this.player.getPlayerState() === YT.PlayerState.UNSTARTED) {
return of(false);
}
} catch (e) {
return of(false);
}
this.currentTimeSubject.next(this.player.getCurrentTime());
this.isPlayingSubject.next(this.player.getPlayerState() === YT.PlayerState.PLAYING);
this.isMutedSubject.next(this.player.isMuted());
this.volumeSubject.next(this.player.getVolume());
return of(true);
}),
).subscribe({next: (isUpdated)=>{
},
error: (err)=> {
console.error(err);
}
});
Still figuring rxjs out and its a great library (have to admit). Given that we have this source and booleans.
let source = [0,1,2,3,4,5,6,7,8,9];
let swEnabled = false;
let ewEnabled = false;
And we would like to conditionally enable startwith and endwith operators based on swEnabled and ewEnabled to the source.....
source.pipe(
iif(() => swEnabled,startWith(1000),),
iif(() => ewEnabled,endWith(1000),),
).subscribe(
(n)=>{
console.log(n);
});
but no joy..... anyone can provide an example of this? Maybe the approach is wrong. Or can you suggest some alternative approach ?
Thanks in advance
You can conditionally build an array of operators as follows:
import { MonoTypeOperatorFunction, of } from 'rxjs';
import { pipeFromArray } from 'rxjs/internal/util/pipe';
import { endWith, startWith } from 'rxjs/operators';
let source = of(0,1,2,3,4,5,6,7,8,9);
let swEnabled = false;
let ewEnabled = false;
let operators: MonoTypeOperatorFunction<number>[] = []
if (swEnabled) {
operators.push(startWith(1000))
}
if (ewEnabled) {
operators.push(endWith(1000))
}
source.pipe(pipeFromArray(operators)).subscribe(e => console.log(e))
There is currently an open issue about not being able to use the spread syntax inside the pipe. If that issue gets fixed, then you will no longer need the pipeFromArray function and the solution can be simplified to source.pipe(...operators).
Re iif: Note that the iif function is an Observable constructor function and not an operator function. That means that you cannot use iif as an operator inside pipe() as you have shown. By observing the RxJx reference page, you can see that there are two sections, amongst others, titled index and operators. Only the functions under operators can be used directly within the pipe(). The functions under index can be used to construct source Observables.
Re startWith and endWith: These two functions are operator functions (meant to be used inside pipe) and not Observables. The iif function expects an Observable as the second and third argument.
I was having the same question, but after trying different strategies I came up with this solution:
someObservable.pipe(
// anyOperator(),
condition ? conditionalOperator() : tap(() => {})
)
In this case, if condition is true, operatorA is added to pipe, if not, the tap(() => {}) is like a "noop" (anyone can mix the condition and parameters to fit any needs).
Q: can RxJs operators be used to flatten an array, transform items, then unflatten it, whilst maintaining a continuous stream (not completing)?
For the simplified example here: https://stackblitz.com/edit/rxjs-a1791p?file=index.ts
If following the approach:
mergeMap(next => next),
switchMap(next => of(***transforming logic***)),
toArray()
then the observable does not complete, and the values do not come through. A take(1) could be added but this is intended to be a continuous stream.
If using:
mergeMap(next => next),
switchMap(next => of(***transforming logic***)),
scan()
then this works great. However, then each time the source observable emits, the accumulator never resets, so the scan() which is intended to accumulate the values back into an array ends up combining multiple arrays from each pass. Can the accumulator be reset?
Obviously it can be accomplished with:
switchMap(next => of(next.map(***transforming logic***)))
But my real-world example is an awful lot more complicated than this, and is tied into NgRx.
Here would be one approach:
src$.pipe(
mergeMap(
arr => from(arr)
.pipe(
switchMap(item => /* ... */),
toArray(),
)
)
)
For each emitted array, mergeMap will create an inner observable(from(..)). There, from(array) will emit each item separately, allowing you to perform some logic in switchMap. Attaching toArray() at the end will give you an array with the results from switchMap's inner observable.
You don't need to use mergeMap or switchMap here. You would only need those if you are doing something asynchronously. Like if you were taking the input value and creating an observable (ex: to make an http call).
By using of inside of mergeMap, you are essentially starting with an Observable, taking the unpacked value (an array), then turning it back into an Observable.
From your stack blitz:
The reason your first strategy doesn't complete is because toArray() is happening on the level of the source (clicksFromToArrayButton), and that is never going to complete.
If you really wanted to, you could nest it up a level, so that toArray() happens on the level of your array (created with from(), which will complete after all values are emitted).
const transformedMaleNames = maleNames.pipe(
mergeMap(next => from(next).pipe(
map(next => {
const parts = next.name.split(' ');
return { firstName: parts[0], lastName: parts[1] };
}),
toArray()
)
),
);
But... we don't really need to use from to create an observable, just so it can complete, just so toArray() can put it back together for you. We can use the regular map operator instead of mergeMap, along with Array.map():
const transformedMaleNames = maleNames.pipe(
map(nextArray => {
return nextArray.map(next => {
const parts = next.name.split(' ');
return { firstName: parts[0], lastName: parts[1] };
})
})
);
this works, but isn't necessarily utilizing RxJS operators fully?
Well, ya gotta use the right tool for the right job! In this case, you are simply transforming array elements, so Array.map() is perfect for this.
But my real-world example is an awful lot more complicated than this
If you are concerned about the code getting messy, you can just break the transformation logic out into it's own function:
const transformedMaleNames = maleNames.pipe(
map(next => next.map(transformName))
);
function transformName(next) {
const parts = next.name.split(' ');
return { firstName: parts[0], lastName: parts[1] };
}
Here's a working StackBlitz.
I want to conditionally dispatch some actions using iif utility from RxJS. The problem is that second argument to iif is called even if test function returns false. This throws an error and app crashes immediately. I am new to to the power of RxJS so i probably don't know something. And i am using connected-react-router package if that matters.
export const roomRouteEpic: Epic = (action$, state$) =>
action$.ofType(LOCATION_CHANGE).pipe(
pluck('payload'),
mergeMap(payload =>
iif(
() => {
console.log('NOT LOGGED');
return /^\/room\/\d+$/.test(payload.location.pathname); // set as '/login'
},
merge(
tap(v => console.log('NOT LOGGED TOO')),
of(
// following state value is immediately evaluated
state$.value.rooms.list[payload.location.pathname.split('/')[1]]
? actions.rooms.initRoomEnter()
: actions.rooms.initRoomCreate(),
),
of(actions.global.setIsLoading(true)),
),
empty(),
),
),
);
A little late to the party, but I found that the role of iif is not to execute one path over the other, but to subscribe to one Observable or the other. That said, it will execute any and all code paths required to get each Observable.
From this example...
import { iif, of, pipe } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
const source$ = of('Hello');
const obsOne$ = (x) => {console.log(`${x} World`); return of('One')};
const obsTwo$ = (x) => {console.log(`${x}, Goodbye`); return of('Two')};
source$.pipe(
mergeMap(v =>
iif(
() => v === 'Hello',
obsOne$(v),
obsTwo$(v)
))
).subscribe(console.log);
you'll get the following output
Hello World
Hello, Goodbye
One
This is because, in order to get obsOne$ it needed to print Hello World. The same is true for obsTwo$ (except that path prints Hello, Goodbye).
However you'll notice that it only prints One and not Two. This is because iif evaluated to true, thus subscribing to obsOne$.
While your ternary works - I found this article explains a more RxJS driven way of achieving your desired outcome quite nicely: https://rangle.io/blog/rxjs-where-is-the-if-else-operator/
Ok, i found an answer on my own. My solution is to remove iif completely and rely on just ternary operator inside mergeMap. that way its not evaluated after every 'LOCATION_CHANGE' and just if regExp returns true. Thanks for your interest.
export const roomRouteEpic: Epic = (action$, state$) =>
action$.ofType(LOCATION_CHANGE).pipe(
pluck<any, any>('payload'),
mergeMap(payload =>
/^\/room\/\d+$/.test(payload.location.pathname)
? of(
state$.value.rooms.list[payload.location.pathname.split('/')[2]]
? actions.rooms.initRoomEnter()
: actions.rooms.initRoomCreate(),
actions.global.setIsLoading(true),
)
: EMPTY,
),
);
If you use tap operator inside observable creation(because it returns void), it will cause error as below
Error: You provided 'function tapOperatorFunction(source) {
return source.lift(new DoOperator(nextOrObserver, error, complete));
}' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
Remove the tap and put the console in the subscribe().
I have created a stackblitz demo.
Another consideration is that even though Observables and Promises can be used in the same context many times when working with RxJS, their behavior will be different when dealing with iif. As mentioned above, iif conditionally subscribes; it doesn't conditionally execute. I had something like this:
.pipe(
mergeMap((input) =>
iif(() => condition,
functionReturningAPromise(input), // A Promise!
of(null)
)
)
)
This was evaluating the Promise-returning function regardless of the condition because Promises don't need to be subscribed to to run. I fixed it by switching to an if statement (a ternary would have worked as well).
Tasks is: I have two effects (ngrx/store), let's say ef1$ and ef2$, which are Observables. In angular component I would like to detect which one is triggered. I added the following code:
Observable.race(ef1$, ef2$)
.subscribe((payload) => {
if (payload.type == ThreadActions.THREAD_READY_TO_BE_RENDERED) {
// do one thing
}
else if (payload.type == ThreadActions.THREAD_ITEM_READY_TO_BE_RENDERED) {
// do another thing
}
});
But it looks like that after first emit of any effects Observable.race is going to be dead and no reaction more on additional emits, despite of I sure that effect executes again and again.
It's hard to tell what is you desired functionality but if you want to use the race operator multiple times you'll need to complete the chain when it emits a resubscribe immediately:
Observable.defer(() => Observable.race(ef1$, ef2$))
.take(1)
.repeat()
.subscribe(...);
Observable.race() will only emit whichever the emission comes first.
If you want two Observable streams and observe them concurrently aka you do not care whoever will be emitting, use Observable.combineLatest:
Observable.combineLatest(ef1$, ef2$)
.subscribe(([ef1,ef2]) => {
//ef1.type will be ThreadActions.THREAD_READY_TO_BE_RENDERED
//ef2.type will be ThreadActions.THREAD_ITEM_READY_TO_BE_RENDERED
//check if ef1 is truthy
if(ef1){
//do one thing
}
if(ef2){
//do another thing
}
});