Rx: Buffer with variable back pressured buffer time - rxjs

I have played a little with Rx, but still consider myself newish to the world. I have a problem, and I am wondering if I can solve it via Rx. My initial use case is in C#, but may later want the same in JS (though if any code snippets in answers, any language of pseudo code is fine)
Currently I have a client app which simply buffers data (transactions) it creates, and sends to the server every 5 seconds. This data can contain many individual transactions, e.g. it has stored many and now come online so we want to send perhaps thousands to the server. When sending many as just described, the 8 second delay and buffering is fine (the data is already delayed anyway). However, when this client is, for example connected, and we create a transaction in real-time (i.e. just 1 or 2 single transactions), I would like to be able to send these immediately, i.e. not wait the 8 seconds.
So it looks similar to the Rx buffer, crossed with maybe the debounce? I have attempted to draw a marble diagram to help explain using an advanced graphic package (not) paint.
So, to walk through this, the 1st red marble is received and forwarded straight away. Next the yellow marble is also forwarded straight away as it has been 1 second since the last red marble.
Now the light blue marble, and a bunch of others come in less than 1 second, so we now want to buffer these, as we don't want to spam the network with possibly thousands of requests - what we will do here is buffer for say 5 seconds, and then send however many we have buffered, every 5 seconds, until this "spurt" has finished. After this, we then want to return to sending any other "individual" requests as they come.
It doesn't have t be exactly like the above, basically we want
individual transactions (to user input with a connected client app), to be sent immediately, (or some very small delay)
Detect when we start getting many transactions, and have this start to
"throttle" and then send the buffered batches at some longer time interval apart (eg 5 or 8 seconds)
Do NOT want to throw away any transactions (marbles), I want to send them all
I have found a number of other posts similar, but not the same as this.
I've come up with some clunky "manual" ways of doing this (using just standard lists, and various timers etc), but was wondering would this be possible using Rx to do some of this work, and hopefully less prone to bugs?
Thanks in advance for any help here!

I think you picked it already, buffer and debounce as the buffer trigger,
when this client is, for example connected
If you want to added a connection event, you could merge that into bufferTrigger as well.
console.clear()
const source = new Rx.Subject();
const bufferTrigger = source.debounceTime(500);
source
.buffer(bufferTrigger)
.subscribe(console.log);
setTimeout(() => source.next('red'), 0);
setTimeout(() => source.next('yellow'), 1000);
setTimeout(() => source.next('lightblue'), 3000);
setTimeout(() => source.next('green'), 3100);
setTimeout(() => source.next('blue'), 3200);
setTimeout(() => source.next('pink'), 4000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
Example with additional trigger 5 secs after last emit
const source = new Rx.Subject();
const lastEmit = new Rx.Subject();
const maxDelayAfterLastEmit = lastEmit.delay(5000);
const bufferTrigger = source.debounceTime(500)
.merge(maxDelayAfterLastEmit);
const emits = source
.buffer(bufferTrigger)
.do(x => lastEmit.next(x))
.filter(x => x.length);
var start = new Date().getTime();
emits.subscribe(x => console.log((new Date().getTime() - start)/1000 + "s " + x));
setTimeout(() => source.next('red'), 0);
setTimeout(() => source.next('yellow'), 1000);
setTimeout(() => source.next('lightblue'), 3000);
setTimeout(() => source.next('green'), 3100);
setTimeout(() => source.next('blue1'), 3200);
setTimeout(() => source.next('blue2'), 3300);
setTimeout(() => source.next('blue3'), 3600);
setTimeout(() => source.next('pink1'), 4000);
setTimeout(() => source.next('pink2'), 4400);
setTimeout(() => source.next('pink3'), 4800);
setTimeout(() => source.next('pink4'), 5200);
setTimeout(() => source.next('pink5'), 5600);
setTimeout(() => source.next('pink6'), 6000);
setTimeout(() => source.next('pink7'), 6400);
setTimeout(() => source.next('pink8'), 6800);
setTimeout(() => source.next('pink9'), 7200);
setTimeout(() => source.next('pink10'), 7700);
setTimeout(() => source.next('pink11'), 8200);
setTimeout(() => source.next('pink12'), 8600);
setTimeout(() => source.next('pink13'), 9000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>

Related

Compose event streams with a resettable delay on one of them

I want to change the size of an element while it is being scrolled (touch pan, etc). In order to do that I intend to use touch events as observable sources. The size should grow when user starts to pan the element and should shrink 5s after user stopped.
In other words:
Given the streams of 'start' and 'end' events, immediately do the action on 'start' and delay the action on 'end'. But if during that delay new event 'start' come, the delay should be cancelled.
Here is marble, the best I could do.
PS
Feel free to suggest a better title. I struggle to come up with something descriptive enough.
Assuming that start$ and end$ are the Observable streams representing the start and end events, than I would probably proceed like this
start$.pipe(
// use tap to implement the side effect of growing the size of an element
// the event is passed assuming that it contains somehow the reference to the element
// whose size has to grow
tap(event => growSize(event)),
// any time a start event is notified, we pass the control to the end$ stream
// if a new start event is notified, the subscription to end$ will be unsubscribed
// a new subscription will be establishes, which means that a new delay will start
switchMap(() =>
end$.pipe(
// delay 5 seconds
delay(5000),
// decrease the size of the element as a side effect
tap(event => decreaseSize(event)),
)
)
)
.subscribe()
You could merge your start and end triggers and use debounceTime with filter:
merge(start$, end$).pipe(
debounceTime(5000),
filter(event => event === 'end')
);
Here's a little StackBlitz to play with.

RXJS marble test

I have a test for mergemap, But it does not return the correct expect result. Can someone help me?
Thanks!
it("should maps to inner observable and flattens", () => {
const values = { a: "hello", b: "world", x: "hello world" };
const obs1 = cold( "-a---a|", values);
const obs2 = cold( "-b-b-b-|", values);
const expected = cold("--x-xxxx-x-|", values);
const result = obs1.pipe(mergeMap(x => obs2.pipe(map(y => x + " " + y))));
expect(result).toBeObservable(expected);
I will try to break down your test so you can understand what's going on and where you're doing things wrong. I'm really sorry, but I won't be using jasmine-marbles library, but a preferred way of writing tests (I'd also recommend you to avoid jasmine-marbles library).
When converted to plain, old marbles (not using jasmine-marbles library), your test looks like this:
import { mergeMap, map } from 'rxjs/operators';
import { TestScheduler } from 'rxjs/testing';
describe('mergeMap', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler(someMatcher);
});
it('should maps to inner observable and flattens', () => {
testScheduler.run(({ cold, expectObservable }) => {
const values = { a: 'hello', b: 'world', x: 'hello world' };
const obs1 = cold('-a---a| ', values);
const obs2 = cold('-b-b-b-| ', values);
const expected = ' --x-xxxx-x-|';
const result = obs1.pipe(mergeMap(x => obs2.pipe(map(y => x + ' ' + y))));
expectObservable(result).toBe(expected, values);
});
});
});
And this test is failing. Here is why: you're expecting your test to emit at frames 2, 4, 5, 6, 7, 9 and a complete at frame 11. But, the actual emissions happen at frames 2, 4, 6, 6, 8, 10 and a complete at frame 12. Now, to be able to visually understand why and how this is happening, I will write a test with couple of comments and I will align them a different way so you get a better filling of what happens:
const obs1 = cold('-a---a| ', values);
const obs2 = cold(' -b-b-b-| ', values);
// -b-b-b-|
const expected = ' --x-xxxx-x-| ';
// frames: 0123456789012
Basically, in mergeMap, you're returning an instance of obs2 when the source observable emits. In this case, the source is obs1. When it emits the first value (a), at frame 1, mergeMap internally subscribes to obs2 - this is why I aligned the start of obs2 emissions to be below a at frame 1. Emissions from obs2 are the ones that get to the consumer.
Similarly, when obs1 emits the second value, at frame 5, another subscribe to obs2 happens and, since obs2 is a cold observable, another producer is instantiated, so another stream starts to flow. This is why I added a comment to indicate when the second subscribe to obs2 happens. It starts at frame 5, right when the second a is emitted from obs1. Similarly, emissions from the second subscribe to obs2 are the ones that get to the consumer.
So, combining this, we get to conclusion where the expected frames should be:
-b-b-b-| emits at frames: 2, 4 and 6 and a complete at frame 8
-b-b-b-| emits at frames: 6, 8 and 10 and a complete at frame 12
0123456789012
Based on this, the final emissions happen at frames 2, 4, 6, 6, 8 and 10 and the complete happens at frame 12. The problem with this setup is that it is not possible to show an emission which comes early after two or more emissions that come at the same frame.
This is to say, emission at frame 8 is too close to the two emissions at frame 6. The reason being is that emissions that happen at the same frame are grouped with brackets () in marble diagrams and brackets are somewhat hiding some number of emissions. This is your case:
-b-b-b-|
-b-b-b-|
--x-x-(xx)x-| // brackets start at frame 6 and represent grouped emissions which all happen at frame 6
0123456666012 // the frames 7, 8 and 9 are "hidden"
Frames 7, 8 and 9 are hidden and can't be represented so emissions at these frames can't be shown in marble diagrams, no matter what. And, since there is an emission at frame 8 which gets lost, you can't create a proper marble diagram for this expected emissions.
In order for this test to pass, you could emit the second a from the obs1 one frame further, at frame 6. Now, your test could look like this (and it should pass now):
testScheduler.run(({ cold, expectObservable }) => {
const values = { a: 'hello', b: 'world', x: 'hello world' };
const obs1 = cold('-a----a| ', values);
const obs2 = cold(' -b-b-b-| ', values);
// -b-b-b-|
const expected = ' --x-x-xx-x-x-|';
const result = obs1.pipe(mergeMap(x => obs2.pipe(map(y => x + ' ' + y))));
expectObservable(result).toBe(expected, values);
});

Rxjs throttleTime - update the throttling in realtime?

Is there a way to update the throttleTime in Rxjs. You're scrolling near the bottom of the page, so we only need throttle every 400ms, while as you get nearer the top, we increate to 100ms as the target area is in that region
this.scrollSubscription$ = this.scrollSubject$
.asObservable()
.throttleTime(200, undefined, throttleTimeConfig)
.subscribe(event => this.throttledScrollHandler(event));
What I am going to do is have two separate subscripbtions, as I cannot find a way of doing it.
this.slowerSubscriber$ = this.scrollSubject$
.asObservable()
.throttleTime(400,.......
Thought I would ask as I can't think of another way of achieving it
You can use throttle to implement your own silencing duration:
// Make this function extract the position using scrollY, page offsets or whatever you want to use
const getScrollPositionFromEvent = event => 42;
this.scrollSubject$.asObservable()
.throttle(event => Observable.interval(
getScrollPositionFromEvent(event) <= 100 ? 100 : 400
))
.subscribe(console.log);
You can see a working example here. It simply emits by a given input, which in your case would be based on the scroll offset.

RxJS Buffer, how to group multi click events as stream

In RxJS version 2.2.26
The following code produced a stream that would emit the number of clicks (double click, triple click etc..)
var multiClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x >= 2; });
In RxJS version 4.0.6
This code no longer produces the desired result.
How can I get that same functionality in RxJS 4.0.6?
Working 2.2.26 Example
Broken 4.0.6 Example
There is actually a much better way to find double clicks via RxJs:
var mouseDowns = Rx.Observable.fromEvent(button, "mousedown");
var doubleClicks = mouseDowns.timeInterval()
.scan<number>((acc, val) => val.interval < 250 ? acc + 1 : 0, 0)
.filter(val => val == 1);
The benefit of this is that you don't need to wait for the full 250ms to recognize the double click - if the user has only 120ms between clicks there is a noticable delay between the double click and the resulting action.
Notice, that through counting up (with .scan()) and filtering to the first count we can limit our stream to just double-clicks and ignore every fast click after that - so click click click will not result in two double clicks.
Just in case someone is wondering on how to do the same thing with RxJS 5 (5.0.0-beta.10), this is how I got it working:
const single$ = Rx.Observable.fromEvent(button, 'click');
single$
.bufferWhen(() => single$.debounceTime(250))
.map(list => list.length)
.filter(length => length >= 2)
.subscribe(totalClicks => {
console.log(`multi clicks total: ${totalClicks}`);
});
I spent a lot of time trying to figure this out as I'm also learning Reactive Programming (with RxJS) so, if you see a problem with this implementation or know of a better way to do the same thing, I'll be very glad to know!
Detect single or double-clicks, but not multi-clicks
Here's a solution I came up with that creates a stream of single or double clicks (not anything more, so no triple clicks). The purpose is to detect multiple double clicks in quick succession, but also to receive notifications of single clicks.
let click$ = Observable.fromEvent(theElement, "click");
let trigger$ = click$.exhaustMap(r =>
click$.merge(Observable.timer(250)).take(1));
let multiclick$ = click$.buffer(trigger$).map(r => r.length);
The reasoning is this, we use the buffer operator to group clicks, and design a buffer trigger event as follows: Every time a click happens, we start a race between two observables:
A 250 msec timer
The original click stream
Once this race concludes (we use take(1) because we are only interested in the first event) we emit the buffer trigger event. What does this do? It makes the buffering stop either when a 2nd click arrives, or when 250 msec have elapsed.
We use the exhaustMap operator to suppress the creation of another race while one is already going on. This would otherwise occur for the 2nd click in a double-click pair.
Here's another take (produces N doubleclicks if the user quickly clicks 2*N times):
const clicks = fromEvent(document, 'click');
const doubleclicks = clicks.pipe(
exhaustMap(() => clicks.pipe(takeUntil(interval(250)))),
);
This answer is inspired by #Aviad P.
I believe the following code is the right answer. It meets all these requirements.
It differentiates single-click, double-click, and triple-click.
No unrecognized clicks will be streamed. i.e. If you click the mouse 6 times quickly, you will get 2 triple-clicks. If you click 5 times, you get one triple-click and one double-click. etc
triple-click will be fired immediately, no need to wait for the whole time-window complete.
No clicks will be lost.
It's easy to make it accept four-click, five-click...
const btn = document.querySelector('#btn');
const click$ = Rx.Observable.fromEvent(btn, "click");
const trigger$ =
click$.exhaustMap(r =>
click$
.take(2)
.last()
.race(click$
.startWith(0)
.debounceTime(500)
.take(1))
);
click$
.buffer(trigger$)
.map(l => l.length)
.map(x => ({
1: 'single',
2: 'double',
3: 'tripple'
}[x]))
.map(c => c + '-click')
.subscribe(log);
Rx.Observable.fromEvent(document.querySelector('#reset'), 'click')
.subscribe(clearLog);
function log(message) {
document.querySelector('.log-content').innerHTML += ('<div>' + message + '</div>');
}
function clearLog() {
document.querySelector('.log-content').innerHTML = null;
}
.log {
padding: 1rem;
background-color: lightgrey;
margin: 0.5rem 0;
}
.log-content {
border-top: 1px solid grey;
margin-top: 0.5rem;
min-height: 2rem;
}
.log-header {
display: flex;
justify-content: space-between;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.7/Rx.js"></script>
<button id='btn'>Click Me</button>
<button type="button" id='reset'> Reset </button>
<div class='log' id='logpanel'>
<div class='log-header'>
Log:
</div>
<div class="log-content"></div>
</div>
A more generic solution: starting from a source observable, detect and propagate when this observable quickly emits the exact same value (at least) twice quickly in a row:
var scheduler = Rx.Scheduler.default; // replace with TestScheduler for unit tests
var events = // some observable
var doubleClickedEvents = events
/* Collect the current and previous value */
.startWith(null) // startWith null is necessary if the double click happens immediately
.pairwise()
.map(pair => ({ previous: pair[0], current: pair[1] })
/* Time the interval between selections. */
.timeInterval(scheduler)
/* Only accept two events when they happen within 500ms */
.filter(timedPair => timedPair.interval < 500)
.map(timedPair => timedPair.value)
/* Only accept two events when they contain the same value */
.filter(pair => pair.previous && pair.previous === pair.current)
.map(pair => pair.current)
/* Throttle output. This way, triple and quadruple clicks are filtered out */
.throttle(500, scheduler)
The difference with the provided solutions is:
Like Wolfgang's solution, this observable will immediately emit the value at the exact moment a 'double click' happens, not after a debounce time of 500ms
Does not needlessly accumulate the amount of clicks. If you ever need to implement 'Triple-click' behavior, then Wolfgang's solution is better. But for double click behavior, this can be replaced with startWith(null).pairwise()
Uses throttle to hold back multiple double clicks.
Actually propagates the values that are emitted from the original events, not just a 'signal'.
throttle was changed to debounce starting in RxJS 3 I believe.
There was a big debate about this way back when because the original naming scheme didn't actually match with other implementations of Rx or the actual definition.
https://github.com/Reactive-Extensions/RxJS/issues/284
And it even managed to confuse people further down the road in the reimplementation of RxJS 5:
https://github.com/ReactiveX/rxjs/issues/480
Found a way fit better to distinguish between single / double click. (So I can get either Single-Click or Double-Click, but not both)
For double click, this handled fast double click 2*N times produce N events:
Observable.prototype.doubleEvent = function(timing = 150, count = 2, evt = this) { /// flexibility to handle multiple events > 2
return this.exhaustMap( () => /// take over tapping event,
evt.takeUntil( Observable.timer(timing) ) /// until time up,
.take(count-1) /// or until matching count.
);
}
Note: argument evt = this make doubleEvent works with startWith(), to take this Observable correctly.
Usage:
this.onTap
.doubleEvent()
.subscribe( (result) => console.log('double click!') );
For distinguish between single / double click:
Observable.prototype.singleEvent = function(timing = 150) {
return this.mergeMap( /// parallel all single click event
() => Observable.timer(timing) /// fire event when time up
);
}
Observable.prototype.singleOrDoubleEvent = function(timing = 150) {
return this.exhaustMap( (e) => /// take over tapping event
Observable.race( /// wait until single or double event raise
this.startWith(e).doubleEvent(timing, 2, this)
.take(1).mapTo(1),
this.startWith(e).singleEvent(timing)
.take(1).mapTo(0),
)
);
}
Usage:
this.onTap
.singleOrDoubleEvent()
.subscribe( (result) => console.log('click result: ', result===1 ? 'double click' : 'single click') );

rxjs one sequence multiple subscriptions take x

Let's say I have an rxjs observable created from an array of values:
let obs = Observable.fromArray([1,2,3,4,5,6]);
on some button click, we do this:
obs.take(2).toArray().subscribe(x => {
console.log('the value is:', x); // and the value will be [1,2]
})
The button is clicked again, and again we get [1,2].
The desired behavior is, however, to receive [3,4], and then [5,6].
How would you go about doing this in rxjs?
Thanks!
Rather than taking out a subscription when the button is clicked, I would model the clicks as a stream with something like this:
const clicks = Rx.Observable.fromEvent(document.getElementById('trigger'), 'click');
You can use bufferWithCount(2) to reduce your obs stream in the desired way. You can then combine the streams as appropriate. For example, if you'd like to match each click with a new set (1:1, without skipping any), you could use zip:
const subscription = clicks.zip(obs.bufferWithCount(2), (x, y) => y)
.subscribe(x => console.log(x));
Here's a working example: http://jsbin.com/vonegoq/1/edit?js,console,output

Resources