RxJS: Debounce with regular sampling - javascript

I have an Observable that emits a stream of values from user input (offset values of a slider).
I want to debounce that stream, so while the user is busy sliding, I only emit a value if nothing has come through for, say 100ms, to avoid being flooded with values. But then I also want to emit a value every 1 second if it is just endlessly debouncing (user is sliding back and forth continuously). Once the user stops sliding though, I just want the final value from the debounced stream.
So I want to combine the debounce with a regular "sampling" of the stream. Right now my setup is something like this:
const debounce$ = slider$.debounceTime(100),
sampler$ = slider$.auditTime(1000);
debounce$
.merge(sampler$)
.subscribe((value) => console.log(value));
Assuming the user moves the slider for 2.4 seconds, this emits values as follows:
start end
(x)---------|---------|---(x)|----|
| | | |
1.0 2.0 2.5 3.0 <-- unwanted value at the end
^ ^ ^
sample sample debounce <-- these are all good
I don't want that extra value emitted at 3 seconds (from the sampler$ stream).
Obviously merge is the wrong way to combine these two streams, but I can't figure out what combination of switch, race, window or whatever to use here.

You can solve the problem by composing an observable that serves as a signal, indicating whether or not the user is currently sliding. This should do it:
const sliding$ = slider$.mapTo(true).merge(debounce$.mapTo(false));
And you can use that to control whether or not the sampler$ emits a value.
A working example:
const since = Date.now();
const slider$ = new Rx.Subject();
const debounce$ = slider$.debounceTime(100);
const sliding$ = slider$.mapTo(true).merge(debounce$.mapTo(false));
const sampler$ = slider$
.auditTime(1000)
.withLatestFrom(sliding$)
.filter(([value, sliding]) => sliding)
.map(([value]) => value);
debounce$
.merge(sampler$)
.subscribe(value => console.log(`${time()}: ${value}`));
// Simulate sliding:
let value = 0;
for (let i = 0; i <= 2400; i += 10) {
value += Math.random() > 0.5 ? 1 : -1;
slide(value, i);
}
function slide(value, at) {
setTimeout(() => slider$.next(value), at);
}
function time() {
return `T+${((Date.now() - since) / 1000).toFixed(3)}`;
}
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>

For those who are interested, this is the approach I took, inspired by #cartant's answer.
const slider$ = new Rx.Subject();
const nothing$ = Rx.Observable.never();
const debounce$ = slider$.debounceTime(100);
const sliding$ = slider$.mapTo(true)
.merge(debounce$.mapTo(false))
.distinctUntilChanged();
const sampler$ = sliding$
.switchMap((active) => active ? slider$.auditTime(1000) : nothing$);
debounce$
.merge(sampler$)
.subscribe(value => console.log(`${time()}: ${value}`));
The difference is adding distinctUntilChanged on the sliding$ stream to only get the on/off changes, and then doing a switchMap on that to either have the sampler return values or not.

Related

How can I determine the number of values has been emitted during the debounce time?

Given:
An NgRx effect that handles a request to "zoom in" a display. It gets a notification every time users click on an appropriate button.
public readonly zoomIn$ = createEffect(
() =>
this.actions$.pipe(
ofType(zoomIn),
tap(() => {
scale();
}),
),
{ dispatch: false },
);
Note: The zoomIn action couldn't and doesn't contain any payload. Consider it only as a trigger
Problem:
The redrawing costs resources and in some cases occupy a few seconds to get a new scale. So if you want to scale up several times in a row you'll be compelled to wait.
Solution:
By using the debounceTime operator postpone the call of the scale() function and wait until users make several clicks. Sounds good. The only problem is debounceTime notifying us of the latest value. And what we need is a number of values (user's clicks) silenced by the debounceTime operator.
In a more general view, the task sounds like: how to calculate the count of values emitted by the source stream and silenced by the debounceTime operator?
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 },
);

Conditional observable interaction RxJS

I have the following observables:
this.searchValue$ = this.searchValue.valueChanges.pipe(
startWith(''),
debounceTime(500)
);
this.cardAmount$ = fromEvent(this.button.nativeElement, 'click').pipe(
startWith(0),
scan(count => count + 20, 0)
);
To add a bit more context to the code: searchValue$ is related to input field changes and emits the value that is changed. cardAmount$ is related to button presses. Each time you press a button it emits a new value 20, 40, 60 and so on.
I would like "set" the value of cardAmount$ back to 0 once searchValue$ is emited. What is a correct RxJS way of doing this?
Sounds like the perfect case for the switchMap operator :
this.cardAmount$ = this.searchValue$
.pipe(switchMap( search =>
fromEvent(this.button.nativeElement, 'click')
.pipe(
startWith(0),
scan(count => count + 20, 0)
)));
A new Observable starting from 0 will be generated on each searchValue emission.
You can't with this code (as far as I know).
For that, you will need a proxy that acts both as an observable and an observer. Otherwise, you can't emit a value in your stream.
Try with BehaviorSubject :
this.searchValue$ = this.searchValue.valueChanges.pipe(
startWith(''),
debounceTime(500),
tap(() => this.cardAmount.next(0)),
);
this.cardAmount$ = new BehaviorSubject(0);
fromEvent(this.button.nativeElement, 'click').pipe(
startWith(0),
switchMap(() => this.cardAmount$),
).subscribe(curr => this.cardAmount$.next(curr + 20));
I slightly changed the last observer because if you don't and keep your previous one, the count value won't care about the reset of the value changes. To be sure it does care, you'll have to use the current value of the observer.

Not truly async?

I have a array of about 18 000 elements. I'm creating a map application where I want to add the elements when the user zooms in to a certain level.
So when the user zooms in under 9 I loop tru the array looking for elements that is in the view.
However, it does take some time looping thru the elements, causing the map application lag each time the user zooms out and in of "level 9". Even if there are no elements to add or not, so the bottleneck is the looping I guess.
I've tried to solve it by asyncing it like:
function SearchElements(elementArr) {
var ret = new Promise(resolve => {
var arr = [];
for (var i in elementArr) {
var distanceFromCenter = getDistanceFromLatLonInKm(view.center.latitude, view.center.longitude, dynamicsEntities[i].pss_latitude, dynamicsEntities[i].pss_longitude);
var viewWidthInKm = getSceneWidthInKm(view, true);
if (distanceFromCenter > viewWidthInKm) continue;
arr.push(elementArr[i]);
}
resolve(arr);
});
return ret;
}
SearchElements(myElementsArray).Then(arr => {
// ...
});
But its still not async, this method hangs while the for loop runs.
Because you still have a tight loop that loops through all the elements in one loop, you'll always have the responsiveness issues
One way to tackle the issue is to works on chunks of the data
Note: I'm assuming elementArr is a javascript Array
function SearchElements(elementArr) {
var sliceLength = 100; // how many elements to work on at a time
var totalLength = elementArr.length;
var slices = ((totalLength + sliceLength - 1) / sliceLength) | 0; // integer
return Array.from({length:slices})
.reduce((promise, unused, outerIndex) =>
promise.then(results =>
Promise.resolve(elementArr.slice(outerIndex * sliceLength, sliceLength).map((item, innerIndex) => {
const i = outerIndex * sliceLength + innerIndex;
const distanceFromCenter = getDistanceFromLatLonInKm(view.center.latitude, view.center.longitude, dynamicsEntities[i].pss_latitude, dynamicsEntities[i].pss_longitude);
const viewWidthInKm = getSceneWidthInKm(view, true);
if (distanceFromCenter <= viewWidthInKm) {
return item; // this is like your `push`
}
// if distanceFromCenter > viewWidthInKm, return value will be `undefined`, filtered out later - this is like your `continue`
})).then(batch => results.concat(batch)) // concatenate to results
), Promise.resolve([]))
.then(results => results.filter(v => v !== undefined)); // filter out the "undefined"
}
use:
SearchElements(yourDataArray).then(results => {
// all results available here
});
My other suggestion in the comment was Web Workers (I originally called it worker threads, not sure where I got that term from) - I'm not familiar enough with Web Workers to offer a solution, however https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers and https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API should get you going
To be honest, I think this sort of heavy task would be better suited to Web Workers

Clear an Rx.Observable bufferCount with an event driven Timeout?

I'm using rxjs 5.0:
How can I set a timeout, on this buffer. So that it will clear the bufferCount (11) when no keyup events happen for 5 seconds?
var keys = Rx.Observable.fromEvent(document, 'keyup');
var buffered = keys.bufferCount(11,1);
buffered.subscribe(x => console.log(x));
You can append a timeoutWith, which could return a fresh buffered after a certain timeout (5seconds in your case).
const keys$ = Rx.Observable.fromEvent(document, "keyup")
.map(ev => ev.keyCode|| ev.which); // this is just to have a readable output here in the SO-console
const buffered$ = keys$
.bufferCount(3,1) // replaced your 11 with 3 for easy demonstration
.timeoutWith(2000, Rx.Observable.defer(() => { // replaced 5 with 2 seconds (easier to test here)
console.log("New Buffer!");
return buffered$;
}));
buffered$.subscribe(console.log);
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
As an improvement, this could be even enhanced to only start the stream on the first stroke, otherwise we would have a constant timeout running (not critical, but could still be prevented).
const keys$ = Rx.Observable.fromEvent(document, "keyup")
.map(ev => ev.keyCode|| ev.which); // this is just to have a readable output here in the SO-console
const buffered$ = keys$
.take(1)
.switchMap(firstKey => {
console.log("New Buffer!");
return keys$
.startWith(firstKey)
.bufferCount(3,1) // replaced your 11 with 3 for easy demonstration
.timeoutWith(2000, Rx.Observable.defer(() => buffered$)); // replaced 5 with 2 seconds (easier to test here)
});
buffered$.subscribe(console.log);
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
I have another (and probably easier to understand) solution using window and switchMap():
var keys = Rx.Observable.fromEvent(document.getElementById('myinput'), 'keyup')
.map(event => event.keyCode)
.share();
var buffered = keys
.window(keys.debounceTime(5000))
.switchMap(observable => observable.bufferCount(5, 1))
.filter(buffer => buffer.length === 5);
buffered.subscribe(x => console.log(x));
See demo: https://jsbin.com/cakoru/17/edit?js,console,output
When you don't type for at least 5s the window() operator creates a new Observable that is subscribed internally in switchMap() and chained with a new .bufferCount() operator.
Here's how I'd do it:
const keys$ = Rx.Observable.fromEvent(document, 'keyup').map(ev => ev.keyCode|| ev.which);
keys$
.debounceTime(5000)
.startWith({})
.switchMap(x => keys$.bufferCount(11, 1))
.subscribe(x => console.log(x));
Here we've got a stream that yields a value each time typing stops for five seconds (kicked off with a dummy value) that switchMaps into a bufferCount.

Cyclejs and trigger events with values

I m trying to learn cyclejs and reactive programming, and I can't get how to manage events with values.
For example,I need to create four functions that makes some maths operations such as :
addition
substraction
divison
multiplication
Here's the code that I have :
function main ({DOM}) {
const secondNumber$ = DOM
.select('.second-number')
.events('change')
.map(ev => ev.target.value)
.startWith(0)
const firstNumber$ = DOM
.select('.first-number')
.events('change')
.map(ev => ev.target.value)
.startWith(0)
const addClick$ = DOM
.select('.add')
.events('click')
.merge(firstNumber$)
.merge(secondNumber$)
.filter(value => console.log(value))
.scan((nb1, nb2) => nb1 + nb2)
return {
DOM: addClick$.map(result =>
div([
input('.first-number',{type:'text'}),
input('.second-number',{type:'text'}),
button('.add', 'Add'),
h2('Result is : ' + result)
])
)
};
}
It doesn't work at all and I can't figure out in my mind what I m doing wrong out there.
I m looking for a simple explanation how can I make this working ? I feel just like the merging streams of secondNumber$ and firstNumber$ are not correct and I can't find why..
Any idea ?
EDIT : I got that I shouldn't use the operator I was using, but use withLatestFrom.
The fact is that I m using xstream and so I have to map / flatten :
import {
div,
h1,
input,
button
} from '#cycle/dom';
/**
* Counter
* #param {Object} sources Contains the inputs
* #return {Object} The output sinks
*/
function counter(sources) {
const input1$ = sources.DOM
.select('.input1')
.events('input')
.map(ev => ev.target.value)
.startWith(0);
const input2$ = sources.DOM
.select('.input2')
.events('input')
.map(ev => ev.target.value)
.startWith(0);
const add$ = sources.DOM
.select('.add')
.events('click');
const resultAddition$ = add$
.map(ev => input1$
.map(value => input2$
.map(value2 => Number(value) + Number(value2)))
.flatten())
.flatten()
.startWith(0);
return {
DOM: resultAddition$.map(item => {
console.log(item); // triggered each time an input is modified
return div([
h1(`Super new value : ${item}`),
input('.input1', {
attrs: {
type: 'text'
}
}),
input('.input2', {
attrs: {
type: 'text'
}
}),
button('.add', 'Ajouter')
]);
})
};
}
export default counter;
From now, I have got in mind what the code should do, mapping on each click the operation and flatten the two input$ to get my result only when clicking the button
The fact is that the result value is changing on input and not and click. And more important, it changes on input only after the first click on the add button that is not what I want to.
What am I doing wrong this time ?
Thanks for your replies
It seems like you want combineLatest, not merge.
Both combineLatest and merge are "combination operators". They bring multiple Observables together and output one Observable. However, combineLatest is for "AND" combinations, while merge is for "OR" combinations.
You probably need "AND", because you want the value from first-number AND the value from second-number. That said, you want those values only when an add click happens. In that case, there is a variant of combineLatest called withLatestFrom. It allows you to sample the values from first-number AND second-number, but only when the add click happens.
const addClick$ = DOM
.select('.add')
.events('click')
const added$ = addClick$
.withLatestFrom(firstNumber$, secondNumber$,
(click, first, second) => first + second
)
As a side note, you should never do something like .filter(value => console.log(value)). The function for filter is a predicate. It's supposed to be a "condition" function that returns a boolean. If you want to debug, use .do(value => console.log(value)).
PS: I'm assuming you were using RxJS v4.

Categories

Resources