RxJS: How to combine multiple nested observables with buffer - javascript

Warning: RxJS newb here.
Here is my challenge:
When an onUnlink$ observable emits...
Immediately start capturing values from an onAdd$ observable, for a maximum of 1 second (I'll call this partition onAddBuffer$).
Query a database (creating a doc$ observable) to fetch a model we'll use to match against one of the onAdd$ values
If one of the values from the onAddBuffer$ observable matches the doc$ value, do not emit
If none of the values from the onAddBuffer$ observable matches the doc$ value, or if the onAddBuffer$ observable never emits, emit the doc$ value
This was my best guess:
// for starters, concatMap doesn't seem right -- I want a whole new stream
const docsToRemove$ = onUnlink$.concatMap( unlinkValue => {
const doc$ = Rx.Observable.fromPromise( db.File.findOne({ unlinkValue }) )
const onAddBuffer$ = onAdd$
.buffer( doc$ ) // capture events while fetching from db -- not sure about this
.takeUntil( Rx.Observable.timer(1000) );
// if there is a match, emit nothing. otherwise wait 1 second and emit doc
return doc$.switchMap( doc =>
Rx.Observable.race(
onAddBuffer$.single( added => doc.attr === added.attr ).mapTo( Rx.Observable.empty() ),
Rx.Observable.timer( 1000 ).mapTo( doc )
)
);
});
docsToRemove$.subscribe( doc => {
// should only ever be invoked (with doc -- the doc$ value) 1 second
// after `onUnlink$` emits, when there are no matching `onAdd$`
// values within that 1 second window.
})
This always emits EmptyObservable. Maybe it's because single appears to emit undefined when there is no match, and I'm expecting it not to emit at all when there is no match? The same thing happens with find.
If I change single to filter, nothing ever emits.
FYI: This is a rename scenario with file system events -- if an add event follows within 1 second of an unlink event and the emitted file hashes match, do nothing because it's a rename. Otherwise it's a true unlink and it should emit the database doc to be removed.

This is my guess how you could do this:
onUnlink$.concatMap(unlinkValue => {
const doc$ = Rx.Observable.fromPromise(db.File.findOne({ unlinkValue })).share();
const bufferDuration$ = Rx.Observable.race(Rx.Observable.timer(1000), doc$);
const onAddBuffer$ = onAdd$.buffer(bufferDuration$);
return Observable.forkJoin(onAddBuffer$, doc$)
.map(([buffer, docResponse]) => { /* whatever logic you need here */ });
});
The single() operator is a little tricky because it emits the item that matches the predicate function only after the source Observable completes (or emits an error when there're two items or no matching items).
The race() is tricky as well. If one of the source Observables completes and doesn't emit any value race() will just complete and not emit anything. I reported this some time ago and this is the correct behavior, see https://github.com/ReactiveX/rxjs/issues/2641.
I guess this is what went wrong in your code.
Also note that .mapTo(Rx.Observable.empty()) will map each value into an instance of Observable. If you wanted to ignore all values you can use filter(() => false) or the ignoreElements() operator.

Related

Can this rxjs merge logic be simplified?

I have an observable (onAuthStateChanged) from the Firebase client that:
emits null immediately if the user is not signed in, and
emits null and then a user object a few moments later if the user is signed in.
const observable = new Observable((obs) => {
return app.auth().onAuthStateChanged(
obs.next,
obs.error,
obs.complete
)
})
What I want is to:
ignore any emitted null values for the first 1000ms of the app lifecycle (null coming after 1000ms is accepted)
always emit user object regardless of what time it comes
if no user object comes in the first 1000ms, then emit null at the 1000ms mark
Here is what I've done (and it seems to work). However, I'm reluctant to use this code as it doesn't seem that concise:
const o1 = observable.pipe(skipUntil(timer(1000)))
const o2 = observable.pipe(
takeUntil(o1),
filter((user) => user !== null)
)
const o3 = timer(1000).pipe(takeUntil(o2), mapTo(null))
merge(o1, o2, o3).subscribe({
next: setUser,
error: console.log,
complete: () => console.log("error: obs completed, this shouldn't happen"),
})
Is there a way to do this without merge? I tried going through the docs but I'm quite lost.
Thanks for your help!
You could use concat instead of merge. Think of it as using the first source until it completes, then use the second source.
const nonNullUser = firebaseUser.pipe(
filter(user => user !== null),
takeUntil(timer(1000))
);
const user = concat(nonNullUser, firebaseUser);
user.subscribe(...);
I just realized that this solution will not explicitly perform step #3 "emit null at the 1000ms mark". I was thinking subscribing to firebaseUser would emit the latest value. But, I'm not sure if that's true for your scenario.
If not, we could easily achieve this by adding shareReplay like this:
const firebaseUser = observable.pipe(shareReplay(1));
While I liked the answer from #BizzyBob I was genuinely intrigued by these requirements that I wanted to see what other options were available. Here's what I produced:
const auth$ = observable.pipe(
startWith(null)
)
const null$ = timer(1000).pipe(
switchMap(_=>auth$)
)
const valid$ = auth$.pipe(
filter(user=>!!user)
)
const user$ = race(null$, valid$);
We have our source auth$ observable which gets your Firebase data. However, startWith() will immediately emit null before any values coming from Firebase.
I declared two observables for null and non-null cases, null$ and valid$.
The null$ observable will subscribe to auth$ after 1000ms. When this happens it immediately emits null thanks to the startWith() operator.
The valid$ observable subscribes to auth$ immediately but only emits valid user data thanks to filter(). It won't emit startWith(null) because it is caught by the filter.
Last, we declare user$ by using the race() operator. This operator accepts a list of observables as its parameters. The first observable to emit a value wins and is the resulting subscription.
So in our race, valid$ has 1000ms to emit a valid user. If it doesn't, race() will subscribe to null$ resulting in the immediate null, and all future values coming from Firebase.

Filtering RxJS stream after emission of other observable until timer runs out

I want to achieve the following behavior in RxJS but could not find a way using the available operators:
Stream A: Generated by a continuous stream of events (e.g. browser scroll)
Stream B: Generated by another arbitrary event (e.g. some kind of user input)
When B emits a value, I want to pause the processing of A, until a specified amount of time has passed. All values emitted by A in this timeframe are thrown away.
When B emits another value during this interval, the interval is reset.
After the interval has passed, the emitted values of A are no longer filtered.
// Example usage.
streamA$
.pipe(
unknownOperator(streamB$, 800),
tap(val => doSomething(val))
)
// Output: E.g. [event1, event2, <skips processing because streamB$ emitted>, event10, ...]
// Operator API.
const unknownOperator = (pauseProcessingWhenEmits: Observable<any>, pauseIntervalInMs: number) => ...
I thought that throttle could be used for this use case, however it will not let any emission through, until B has emitted for the first time (which might be never!).
streamA$
.pipe(
// If B does not emit, this never lets any emission of A pass through!
throttle(() => streamB$.pipe(delay(800)), {leading: false}),
tap(val => doSomething(val))
)
An easy hack would be to e.g. subscribe manually to B, store the timestamp when a value was emitted in the Angular component and then filter until the specified time has passed:
(obviously goes against the side-effect avoidance of a reactive framework)
streamB$
.pipe(
tap(() => this.timestamp = Date.now())
).subscribe()
streamA$
.pipe(
filter(() => Date.now() - this.timestamp > 800),
tap(val => doSomething(val))
)
I wanted to check with the experts here if somebody knows an operator (combination) that does this without introducing side-effects, before I build my own custom operator :)
I think this would be an approach:
bModified$ = b$.pipe(
switchMap(
() => of(null).pipe(
delay(ms),
switchMapTo(subject),
ignoreElements(),
startWith(null).
)
)
)
a$.pipe(
multicast(
new Subject(),
subject => merge(
subject.pipe(
takeUntil(bModified$)
),
NEVER,
)
),
refCount(),
)
It may seem that this is not a problem whose solution would necessarily involve multicasting, but in the above approach I used a sort of local multicasting.
It's not that expected multicasting behavior because if you subscribe to a$ multiple times(let's say N times), the source will be reached N times, so the multicasting does not occur at that level.
So, let's examine each relevant part:
multicast(
new Subject(),
subject => merge(
subject.pipe(
takeUntil(bModified$)
),
NEVER,
)
),
The first argument will indicate the type of Subject to be used in order to achieve that local multicasting. The second argument is a function, more accurately called a selector. Its single argument is the argument specified before(the Subject instance). This selector function will be called every time a$ is being subscribed to.
As we can see from the source code:
selector(subject).subscribe(subscriber).add(source.subscribe(subject));
the source is subscribed, with source.subscribe(subject). What's achieved through selector(subject).subscribe(subscriber) is a new subscriber that will be part of the Subject's observers list(it's always the same Subject instance), because merge internally subscribes to the provided observables.
We used merge(..., NEVER) because, if the subscriber that subscribed to the selector completes, then, next time the a$ stream becomes active again, the source would have to be resubscribed. By appending NEVER, the observable resulted form calling select(subject) will never complete, because, in order for merge to complete, all of its observables have to complete.
subscribe(subscriber).add(source.subscribe(subject)) creates a connection between subscribed and the Subject, such that when subscriber completes, the Subject instance will have its unsubscribe method called.
So, let's assume we have subscribed to a$: a$.pipe(...).subscribe(mySubscriber). The Subject instance in use will have one subscriber and if a$ emits something, mySubscriber will receive it(through the subject).
Now let's cover the case when bModified$ emits
bModified$ = b$.pipe(
switchMap(
() => of(null).pipe(
delay(ms),
switchMapTo(subject),
ignoreElements(),
startWith(null).
)
)
)
First of all, we're using switchMap because one requirement is that when b$ emits, the timer should reset. But, the way I see this problem, 2 things have to happen when b$ emits:
start a timer (1)
pause a$'s emissions (2)
(1) is achieved by using takeUntil in the Subject's subscribers. By using startWith, b$ will emit right away, so that a$'s emissions are ignored. In the switchMap's inner observable we're using delay(ms) to specify how long the timer should take. After it elapses, with the help of switchMapTo(subject), the Subject will now get a new subscriber, meaning that a$'s emissions will be received by mySubscriber(without having to resubscribe to the source). Lastly, ignoreElements is used because otherwise when a$ emits, it would mean that b$ also emit, which will cause a$ to be stopped again. What comes after switchMapTo(subject) are a$'s notifications.
Basically, we're able to achieve the pausable behavior this way: when the Subject instance as one subscriber(it will have at most one in this solution), it is not paused. When it has none, it means it is paused.
EDIT: alternatively, you could have a look at the pause operator from rxjs-etc.

=> Angularjs. Am I understanding this correctly for values passed in?

I am working through a Udemy course on RxJs 6 and need to ask this as it was not crystal clear to me.
Note: This is a type ahead tutorial I am currently in at the moment. So on the keyup event this method is firing off.
ngAfterViewInit() {
const searchLessons$ = fromEvent<any>(this.input.nativeElement, 'keyup')
.pipe(
map(event => event.target.value),
debounceTime(400),
distinctUntilChanged(),
// switchMap cancels prior calls.
switchMap(search => this.loadLessons(search))
);
const initialLessons$ = this.loadLessons();
this.lessons$ = concat(initialLessons$, searchLessons$);
}
Does the code mean,
for all events that fire the code will collect responses from completed calls to the loadLessons
the value of the event is referenced as search
then the => will trigger a call to the loadLessons(search)
Continue of 3: If the value of the event were lets just say an array of values, would that mean that for the => call, a separate call to the loadLessons(search) would be made passing for each individual array value
Continue of 3: or would it just pass in the entire array?
Here is line per line explanation:
ngAfterViewInit() {
const searchLessons$ = fromEvent<any>(this.input.nativeElement, 'keyup') // whenever keyup is triggered on this.input
.pipe(
map(event => event.target.value), // we extract input value from event target
debounceTime(400), // we wait for last event in 400ms span
distinctUntilChanged(), // we check that the input value did change
switchMap(search => this.loadLessons(search)) // and with that input value changed we call this.LoadLessons and then wait for its return
);
const initialLessons$ = this.loadLessons(); // this will call initial loadLeason
this.lessons$ = concat(initialLessons$, searchLessons$); // this will connect return of initial call and changes triggered by key up this is not secure for race conditions
}
Ad1. all key up events on input
Ad2. the value of input is referenced as search
Ad3. yes it would just push array as argument
Without seeing the loadLessons func, i can only assume. i will also assume you are using the concat Rxjs method.
So basically what the code does, get the "initial load of lessons" , and subscribe to it on the concatMethod, after that call completes, it goes to subscribe to the second observable searchLessons.
The searchLessons will be called again every input search, and add the new values to the lessons subscription, on the search observable .
If the params given to the loadSeassion is an array, it will depend how that method (loadSessions) works. not with rxjs although it can be done in this case i cant really tell you :)

RxJS Subscribe with two arguments

I have two observables which I want to combine and in subscribe use either both arguments or only one. I tried .ForkJoin, .merge, .concat but could not achieve the behaviour I'm looking for.
Example:
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return obs1.concat(obs2);
}
Then when using this function:
service.save().subscribe((first, second) => {
console.log(first); // int e.g. 1000
console.log(second); // Boolean, e.g. true
});
or
service.save().subscribe((first) => {
console.log(first); // int e.g. 1000
});
Is there a possibility to get exactly that behaviour?
Hope someone can help!
EDIT:
In my specific use case obs1<int> and obs2<bool> are two different post requests: obs1<int> is the actual save function and obs2<bool> checks if an other service is running.
The value of obs1<int> is needed to reload the page once the request is completed and the value of obs2<bool> is needed to display a message if the service is running - independant of obs1<int>.
So if obs2<bool> emits before obs1<int>, that's not a problem, the message gets display before reload. But if obs1<int> emits before obs2<bool>, the page gets reloaded and the message may not be displayed anymore.
I'm telling this because with the given answers there are different behaviours whether the values get emitted before or after onComplete of the other observable and this can impact the use case.
There are several operators that accomplish this:
CombineLatest
This operator will combine the latest values emitted by both observables, as shown in the marble diagram:
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return combineLatest(obs1, obs2);
}
save().subscribe((val1, val2) => {
// logic
});
Zip
The Zip operator will wait for both observables to emit values before emitting one.
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return zip(obs1, obs2);
}
save().subscribe((vals) => {
// Note Vals = [val1, val2]
// Logic
});
Or if you want to use destructuring with the array
save().subscribe(([val1, val2]) => {
// Logic
});
WithLatestFrom
The WithLatestFrom emits the combination of the last values emitted by the observables, note this operator skips any values that do not have a corresponding value from the other observable.
save: obs1.pipe(withLatestFrom(secondSource))
save().subscribe(([val1, val2]) => {
// Logic
});
You can use forkJoin for this purpose. Call them parallely and then if either of them is present then do something.
let numberSource = Rx.Observable.of(100);
let booleanSource = Rx.Observable.of(true);
Rx.Observable.forkJoin(
numberSource,
booleanSource
).subscribe( ([numberResp, booleanResp]) => {
if (numberResp) {
console.log(numberResp);
// do something
} else if (booleanResp) {
console.log(booleanResp);
// do something
}
});
You may use the zip static method instead of concat operator.
save(): Observable<any> {
return zip(obs1, obs2);
}
Then you should be able to do like the following:
service.save().subscribe((x) => {
console.log(x[0]); // int e.g. 1000
console.log(x[1]); // Boolean, e.g. true
});
The exact operator to use depends on the specific details of what you are trying to solve.
A valid option is to use combineLatest - Docs:
obs1$: Observable<int>;
obs2$: Observable<Boolean>;
combined$ = combineLatest(obs1$, obs2$);
combined$.subscribe(([obs1, obs2]) => {
console.log(obs1);
console.log(obs2);
})
Concat emits two events through the stream, one after the other has completed, this is not what you're after.
Merge will emit both events in the same manner, but in the order that they actually end up completing, also not what you're after.
What you want is the value of both items in the same stream event. forkJoin and zip and combineLatest will do this, where you're getting tripped up is that they all emit an array of the values that you're not accessing properly in subscribe.
zip emits every time all items zipped together emit, in sequence, so if observable 1 emits 1,2,3, and observable two emits 4,5; the emissions from zip will be [1,4], [2,5].
combineLatest will emit everytime either emits so you'll get soemthing like [1,4],[2,4],[2,5],[3,5] (depending on the exact emission order).
finally forkJoin only emits one time, once every item inside it has actually completed,a and then completes itself. This is likely what you want more than anything since you seem to be "saving". if either of those example streams don't complete, forkJoin will never emit, but if they both complete after their final value, forkjoin will only give one emission: [2,5]. I prefer this as it is the "safest" operation in that it guarantees all streams are completing properly and not creating memory leaks. And usually when "saving", you only expect one emission, so it is more explicit as well. When ever you see forkJoin, you know you're dealing with a single emission stream.
I would do it like this, personally:
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return forkJoin(obs1, obs2);
}
service.save().subscribe(([first, second]) => {
console.log(first); // int e.g. 1000
console.log(second); // Boolean, e.g. true
});
Typescript provides syntax like this to access the items in an array of a known length, but there is no way to truly create multiple arguments in a subscribe success function, as it's interface only accepts a single argument.

How to switch to another Observable when a source Observable emits but only use latest value

I have two observables
load$ is a stream of load events, emitted on startup or when a reload button is clicked
selection$ is a stream of selected list items, emitted when a list item is selected
I'm wondering if there is an operator that lets me (A) get the latest value emitted by selection$ whenever load$ emits, while (B) ignoring the value emitted by load$.
I was able to accomplish (A) by using withLatestFrom but that doesn't satisfy (B) because withLatestFrom does not ignore the source observable.
load$.withLatestFrom(selection$).subscribe(([_, latestSelection) => {
// _ is not needed and I don't like to write code like this
// I only need the latest selection
});
I've looked at switchMapTo which satisfies (B) but not (A) without also using first() or take(1).
load$.switchMapTo(selection$.take(1)).subscribe(latestSelection => {
// I would like to avoid having to use take(1)
});
I'm looking for a hypothetical switchMapToLatestFrom operator.
load$.switchMapToLatestFrom(selection$).subscribe(latestSelection => {
// switchMapToLatestFrom doesn't exist :(
});
Unfortunately it doesn't exist and I don't like the two other ways that I came up with because it's not immediately obvious why the code contains take(1) or unused arguments.
Are there any other ways to combine two observables so that the new observable emits only the latest value from the second observable whenever the first observable emits?
withLatestFrom takes an optional project function that's passed the values.
You can use it to ignore the value from load$ and emit only the latest selection:
load$
.withLatestFrom(selection$, (load, selection) => selection)
.subscribe(selection => {
// ...
});
use "sample" operator
In your case:
selection$.pipe(sample(load$)).subscribe(
// the code you want to run
)

Categories

Resources