rxjs one sequence multiple subscriptions take x - rxjs

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

Related

concatMap() behavior in a pipe()

I am trying to understand behavior of concatMap() operator. I wrote some example code
import './style.css';
import { concatMap, fromEvent, tap } from 'rxjs';
import { first } from 'rxjs/operators';
const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
const button3 = document.getElementById('button3');
const obs1 = fromEvent(button1, 'click');
const obs2 = fromEvent(button2, 'click');
const obs3 = fromEvent(button3, 'click');
obs1
.pipe(
concatMap((obs1value) => {
console.log('obs1:', obs1value);
return obs2; // wait for this to complete before merging next value
}),
concatMap((obs2value) => {
console.log('obs2', obs2value);
return obs3; // wait for this to complete before merging next value
})
)
.subscribe((obs3value) => {
console.log('obs3:', obs3value);
});
// button2.click() // nothing
// button3.click() // nothing
button1.click(); // obs1
button2.click(); // obs2
button3.click(); // obs3
button1.click(); // nothing -???
button2.click(); // nothing -???
button3.click(); // obs3
button3.click(); // obs3
button3.click(); // obs3
Initially after button 1, button 2 and button 3 are clicked, I can see the print statements. However after that for button1 and button2, there are no print statements, it is observed only in case of button3. I see RxJS documentation says concatMap() would be "...waiting for each one to complete before merging the next". But since obs1, obs2 and obs3 have not completed yet, why do I see the print statement even once?
If I change the code slightly https://stackblitz.com/edit/rxjs-zggqxc, to complete observable as below
obs3.pipe(first());
Now licking button1, why is nothing is printed?
button1.click(); // obs1
button2.click(); // obs2
button3.click(); // obs3
button1.click(); // nothing -???
button2.click(); // obs2
button3.click(); // obs3
button3.click(); // nothing
button3.click(); // nothing
button2.click(); // obs2
When you call button1.click(); second time the next notification is emitted but obs2 hasn't completed yet so the project function in concatMap() isn't invoked. Only if obs2 completed then concatMap() would pop the oldest notification stacked and passed it to its project function.
If you log next notifications comming from obs1 you'll see the event being emitted.
...
obs1
.pipe(
tap(console.log),
concatMap((obs1value) => {
console.log('obs1:', obs1value);
return obs2; // wait for this to complete before merging next value
}),
...
Notice you are using concatMap, with the emphasis on map.
Mapping will not just concatenate all observables but replace (map) items from the previous observable with a sequence of other items.
For each item from your "outer" observable which is actually mapped to another "inner" observable, your map callback (parameter of concatMap(...)) is executed once when the mapping actually occurs.
When are those mappings performed? - concatMap will take one item from the outer observable and then defers handling all other items from the outer observable until the inner observable (returned by the map callback) completes. The outer observable's items are indeed buffered until the inner observable completes.
Side note: In a synchronous scenario this is roughly comparable to SelectMany from C# or flatMap from Java Streams API.
Let me describe your first use scenario:
Each item in obs1 will be replaced by the sequence of items from obs2.
When this replacement is evaluated by pressing button1 (you might have implemented it in a way which depends on the value you received from obs1!), the first log output becomes visible.
Further clicks are on button1 will be ignored until obs2 has completed.
Each item of obs2 will be replaced by the sequence of items from obs3.
Again, when the replacement gets evaluated by pressing button2, the second log output becomes visible.
Further clicks on button2 will be ignored until obs3 has completed.
Therefore, when pressing button1, button2 and button3 you are essentially just subscribed to obs3.
As obs3 never finishes, you will indeed never see any other output.
In your second scenario the observable you map to obs2 is obs3.pipe(take(1)) which completes after one click on button3.
As soon as that happens, the pipeline will will continue to map items from obs2. The next mapping happens as soon as one item in obs2 becomes available (which is immediately, if button2 has been clicked intermediately or later as soon as it gets clicked) and then one click to button3 is awaited again.

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.

Returning a response from the most recent event, rather than the last async response

I have 3 buttons on a page, each that make separate async requests. The content of this page should show the text returned from the request generated by the last button click.
If I merge these streams, the content of the page is always the text of the last completed async request, not the last click.
Here's an example. (S*) are the streams, (C) their clicks, and (R) their responses. My code will result in the S3 click being rendered on the page. However, I would like the content of S2 to be rendered, as it's the final click.
S1 --c-r-------------
S2 --------c---r-----
S3 ------c----------r
Use switchAll. Something like:
const s1 = clickStream1.map(() => someAsync1())
const s2 = clickStream2.map(() => someAsync2())
const s3 = clickStream3.map(() => someAsync3())
const stream = merge(s1, s3, s3).pipe(
switchAll()
)
Each time a new click occurs (and a new async action starts), the stream will "forget" any previous in-flight action and start listening to the new action.

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') );

Resources