My solution is to create a custom pipable operator that achieves the aim.
What do we need? Of course, we need a debounceTime
operator.
.pipe(
debounceTime(300),
)
Then we should calculate the number of values has been emitted. There is a scan
operator that pretty much looks like a well-known reduce
function. We'll give it an initial value and will increase a counter on every received value from the source stream. We have to place it before the debounceTime
operator. It looks now like a stream of indices.
.pipe(
scan(acc => acc + 1, 0),
debounceTime(300),
)
When debounceTime
notifies us of the latest index, how can we know the number of muted values? We have to compare it with the previous index that has been emitted. The previous value can be received by using a pairwise
operator. And then get a difference between them using the map
operator.
.pipe(
scan(acc => acc + 1, 0),
debounceTime(300),
pairwise(),
map(([previous, current]) => current - previous),
)
If you try this in the current state you notice that something is wrong, that it doesn't work for the first time. The problem lies in the pairwise
operator. It emits pairs of values (previous and current), so it waits until it has at least two values before starting the emission of pairs. Is it fair? Yes, it is? That's why we need to cheat it a little and provide a first value (that is 0), with the use of the startWith
operator.
The final implementation
/**
* Emits a notification from the source Observable only after a particular time span has passed without another source emission,
* with an exact number of values were emitted during that time.
*
* @param dueTime the timeout duration in milliseconds for the window of time required to wait for emission silence before emitting the most recent source value.
* @returns MonoTypeOperatorFunction
*/
export const debounceCounter =
(dueTime: number): MonoTypeOperatorFunction<number> =>
(source: Observable<unknown>): Observable<number> =>
new Observable(observer =>
source
.pipe(
scan(acc => acc + 1, 0),
debounceTime(dueTime),
startWith(0),
pairwise(),
map(([previous, current]) => current - previous),
)
.subscribe({
next: x => {
observer.next(x);
},
error: err => {
observer.error(err);
},
complete: () => {
observer.complete();
},
}),
);
Usage example
public readonly zoomIn$ = createEffect(
() =>
this.actions$.pipe(
ofType(zoomIn),
debounceCounter(300),
tap(times => {
// scale n-times
}),
),
{ dispatch: false },
);