RxJS Observables and a wheel event - rxjs

I was wondering if anyone could help me theorize a solution. I create an Observable from a wheel event, prevent the default action, throttle it by 200ms, map deltaY (which I can use to determine direction), and then I share it.
My problem is that it emits more values than I need creating a situation where my subscribers continue to fire even after the desired action has occurred. I'm new to RxJS so bear with me but... Is there a way for me to take the "first" value emitted in a series of values within say X amount of time passed and not have the observable complete?
Below is the code.
import { fromEvent } from 'rxjs';
const wheel$ = fromEvent(document, 'wheel')
.pipe(
tap((event) => event.preventDefault()),
// throttleTime(200), /* I have tried throttling and debouncing but that doesn't work - values will continue to be emitted */
map((event) => event.deltaY),
share()
)
// handles scrolling down //
wheel$.pipe(filter((val) => val > 0))
.subscribe((event) => {
if (this.props.isScrolling) return
this.scrollDown();
})

One solution would be "bufferCount()"
of(1,2,3,4,5,6,7,8,9).pipe(
bufferCount(3)
).subscribe(data => console.log(data) )
would create packages of 3 signals. So the Events would be
[1,2,3]
[4,5,6]
[7,8,9]
Or "throttleTime(xy)", than it will put the first signal through, will ignore for "xy" milliseconds every other signal, and then give the next signal a chance.
interval(500).pipe(
throttleTime(2000)
).subscribe(data => console.log(data) )
will result in something like
1 // ignore everything the next 2 seconds
5 // ignore everything the next 2 seconds
9 // ignore everything the next 2 seconds
...
warm regards

Found this question while looking for a solution.
I went with a switchMap
fromEvent(el, 'wheel').pipe(
switchMap(e =>
concat(
//wrap the original event, which fires immediately
of({kind: 'ON_WHEEL', e}),
// fire a onWheelEnd after a 200ms delay
// this event is unsubscribed by the switchMap if a new event comes in
// before the delay
of({kind: 'ON_WHEEL_END}).pipe(delay(200))
)
)
)

Related

Rxjs, Combine last query result for multiple group (combine switchMap with groupBy)

I have an issue using the operator. Basically I have multiple payslips and I want to keep a debounce for each payslip and trigger a query. I want to subscribe to only the last query that succeed for a specific payslip and if a new request is triggered for this payslip before the previous one finished, I want to cancel the previous one.
Here's a sample marble diagram for what i'm looking for:
-----1----1-----1----3----3----3----3-----1---3---1---3---1------>
(magic operators for which I'm unclear)
-------------------1-------------------3-----1---3---1---3---1--->
I have debounce and query, which I like, but it does this:
-----1----1-----1----3----3----3----3-----1---3---1---3---1------>
debounce
-------------------1-------------------3--------------------1---->
.pipe(
groupBy(payslip => payslip._id),
map(group =>
group.pipe(
debounceTime(200),
switchMap(payslip => httpQuery)
)
),
mergeAll()
)
With the current solution, the merge all is grouping the switch map thus canceling even for other group. Is there a way to do what I want ?
Thanks !
I am struggling to find anything wrong with your code or it's outcomes.
I have put the following test code in the ThinkRx playground
const { rxObserver } = require('api/v0.3');
const { zip, timer, from, of } = require('rxjs');
const { take, map, groupBy, mergeAll, debounceTime, delay, switchMap } =
require('rxjs/operators');
zip(
timer(0, 50),
from([1,1,1,3,3,3,3,1,3,1,3,1]),
).pipe(
map(([_,i])=>i),
groupBy(i=>i),
map(group => group.pipe(
debounceTime(0),
switchMap(i=>of(i).pipe(delay(0))), // simulates http request with know duration
)),
mergeAll(),
).subscribe(rxObserver());
With debounce and time-for-http-request both 0 the result is:
With time-for-http-request = 60 (note the time scale is longer):
And then with debounce = 10:
Is it possible in your code that the http request simply takes longer than the time between the requests, so the switchMap is correctly cancelling the earlier one?

How do I create an observable which emits a value from an ngrx selector, then another value after a delay?

Given an ngrx selector:
store.select('count')
I want to create an observable that will emit values emitted by the selector, then emit another specific value after a delay.
Using concat doesn't work as (I assume) the selector doesn't complete, so the 0 is never emitted:
this.count$ = concat(store.select('count'), of(0).pipe(delay(2000)));
Stackblitz: https://stackblitz.com/edit/so-selector-delay?file=src/app/my-counter/my-counter.component.ts
- click 'Increment' button - Current Count should change to 1 then back to 0 after 2 seconds.
If you want to emit the store.select('count') value, then essentially reset it to 0 after not receiving an emission for 2 seconds, you can use a switchMap to create a source that emits two values:
The emitted count
The "default" value of 0 after 2000ms
The trick here is that the second value (0) will NOT be emitted if another store.select('count') value is received, because switchMap will create a new source and "switch" to that:
this.count$ = store.select('count').pipe(
switchMap(count => concat(
of(count),
of(0).pipe(delay(2000))
))
);
Here's a working StackBlitz demo.
It might even be worth creating a custom operator:
this.count$ = this.store.select('count').pipe(
resetAfterDelay(0, 2000)
);
export function resetAfterDelay<T>(defaultValue: T, delayMs: number) {
return switchMap((value: T) => concat(
of(value),
of(defaultValue).pipe(delay(delayMs))
));
}
StackBlitz
Below is an approach using combineLatest and BehaviorSubject
We are hold a value in a subject and create a timer that emits value 0 after 2s. So we have two Observables, one emits immediately and the other after 2s. We combine this two and the effect is a single observable as desired
valueHolderSubject$ = new BehaviorSubject(0);
...
this.count$ = combineLatest([
store.select("count").pipe(
tap(x => this.valueHolderSubject$.next(x)),
tap(() =>
timer(2000).subscribe({
next: () => {
this.valueHolderSubject$.next(0);
}
})
),
map(() => this.valueHolderSubject$.value),
distinctUntilChanged()
),
this.valueHolderSubject$
]).pipe(map(([, x]) => x));
Demo Here
A per my comments on the answer from BizzyBob, I was getting unreliable results. But I refined it to work using:
this.count$ = merge(
store.select('count'),
store.select('count').pipe(delay(2000), map(_ => 0))
);
See stackblitz: https://stackblitz.com/edit/so-selector-delay-merge-working?file=src/app/my-counter/my-counter.component.ts

RxJS - reusing an observable which has completed

Link to code in stackblitz
Is there a way to repeat a completed observable multiple times?
Say I have a button that on click creates an interval observable that emits 10 values, then completes:
fromEvent(button, 'click').pipe(
switchMapTo(interval(500)),
takeWhile(i => i < 10)
)
In the subscription I want to handle both the next and the complete methods:
.subscribe(
i => console.log(i),
() => {},
() => console.log('completed')
);
In this way - the first click will emit one sequence and once it completes, subsequent clicks will not emit again. Is there any way to write this so that all clicks emit the sequence?
I think you should complete the inner observable and not the whole sequence.
(takeWhile should be piped to interval);
You should use switchMap only if you are happy to dismiss the old sequence once a new click event comes in. mergeMap or concatMap otherwise.
const sequence = () => interval(500).pipe(take(10));
fromEvent(button, 'click').pipe(
switchMap(sequence),
)

How to combine a parent and a dependent child observable

There is a continuous stream of event objects which doesn't complete. Each event has bands. By subscribing to events you get an event with several properties, among these a property "bands" which stores an array of bandIds. With these ids you can get each band. (The stream of bands is continuous as well.)
Problem: In the end you'd not only like to have bands, but a complete event object with bandIds and the complete band objects.
// This is what I could come up with myself, but it seems pretty ugly.
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return combineLatest(of(event), ...band$Array);
})),
map(combined => {
const newEvent = combined[0];
combined.forEach((x, i) => {
if (i === 0) return;
newEvent.bands = {...newEvent.bands, ...x};
})
})
)
Question: Please help me find a cleaner way to do this (and I'm not even sure if my attempt produces the intended result).
ACCEPTED ANSWER
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return combineLatest(band$Array).pipe(
map(bandArray => ({bandArray, event}))
);
})
)
ORIGINAL ANSWER
You may want to try something along these lines
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return forkJoin(band$Array).pipe(
map(bandArray => ({bandArray, event}))
);
})
)
The Observable returned by this transformation emits an object with 2 properties: bandArray holding the array of bands retrieved with the getBand service and event which is the object emitted by the Observable returned by getEvents.
Consider also that you are using switchMap, which means that as soon as the Observable returned by getEvents emits you are going to switch to the last emission and complete anything which may be on fly at the moment. In other words you can loose some events if the time required to exectue the forkJoin is longer than the time from one emission and the other of getEvents.
If you do not want to loose anything, than you better use mergeMap rather than switchMap.
UPDATED ANSWER - The Band Observable does not complete
In this case I understand that getBand(bandId) returns an Observable which emits first when the back end is queried the first time and then when the band data in the back end changes.
If this is true, then you can consider something like this
getEvents().pipe(
switchMap(event => {
return from(event.bands).pipe(
switchMap(bandId => getBand(bandId)).pipe(
map(bandData => ({event, bandData}))
)
);
})
)
This transformation produces an Observable which emits either any time a new event occurs or any time the data of a band changes.

why concatMap is not subscribing to all groupBy Observables rxjs?

I have a question why is this not writing to console the numbers 2,4,6? what is the explanation?
Observable.range(1, 6)
.groupBy(n => n % 2 === 0)
.concatMap(obs => obs)
.subscribe((n) => console.log(n), null, () => console.log('complete concatMap'))
// this is the output
1 -
3 -
5 -
complete concatMap
The basic problem is that you're using concatMap that subscribes to the next Observable only when the previous one completed. groupBy emits two GroupedObservables so it subscribes to the first one and I think before it can subscribe to the second one the chain completes. This means the observer receives the complete notification from the first GroupedObservable and therefore you never see values from the second GroupedObservable (to be honest I'm not 100% sure it really happens like this but that makes sense to be without further investigating you example).
So if you want only the second group you could do:
import { Observable } from 'rxjs';
Observable.range(1, 6)
.groupBy(n => n % 2 === 0)
.filter(o => o.key === true)
.concatMap(obs => obs)
.subscribe((n) => console.log(n), null, () => console.log('complete concatMap'))
See live demo (open console): https://stackblitz.com/edit/rxjs5-sfused
I checked the source code and groupBy completes all groups after receiving the complete notification (which it does after receiving all values from range) and therefore there's never space for concatMap to subscribe to the second Observable.
See this: https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/groupBy.ts#L200-L210
Problem is that groupBy operator emits Subjects for each key.
Concat map subscribes to subsequent Subject only after the first one is completed, i.e. it misses a chance to catch items from subsequent Subject because all of the sub-streams emit values in the same time.
Kudos to: https://blog.angularindepth.com/those-hidden-gotchas-within-rxjs-7d5c57406041
TL;DR:
GroupBy receives subjectSelector as a 4th argument. You can use it to force using ReplaySubject instead of Subject (default).
Observable.range(1, 6)
.groupBy(
n => n % 2 === 0,
null,
null,
() => new ReplaySubject() // <-- Here we go
)
.concatMap(obs => obs)
.subscribe((n) => console.log(n))
Demo on RxViz

Resources