I'm trying to achieve something very similar to a buffer count. As values come through the pipe, bufferCount of course buffers them and sends them down in batches. I'd like something similar to this that will emit all remaining items if there are currently fewer than the buffer size in the stream.
It's a little confusing to word, so I'll provide an example with what I'm trying to achieve.
I have something adding items individually to a subject. Sometimes it'll add 1 item a minute, sometimes it'll add 1000 items in 1 second. I wish to do a long running process (2 seconds~) on batches of these items as to not overload the server.
So for example, consider the timeline where P is processing
---A-----------B----------C---D--EFGHI------------------
|_( P(A) ) |_(P(B)) |_( P(C) ) |_(P([D, E, F, G, H, I]))
This way I can process the events in small or large batches depending on how many events are coming through, but i ensure the batches remain smaller than X.
I basically need to map all the individual emits into emits that contain chunks of 5 or fewer. As I pipe the events into a concatMap, events will start to stack up. I want to pick these stacked up events off in batches. How can I achieve this?
Here's a stackblitz with what I've got so far: https://stackblitz.com/edit/rxjs-iqwcbh?file=index.ts
Note how item 4 and 5 don't process until more come in and fill in the buffer. Ideally after 1,2,3 are processed, it'll pick off 4,5 the queue. Then when 6,7,8 come in, it'll process those.
EDIT: today I learned that bufferTime has a maxBufferSize parameter, that will emit when the buffer reaches that size. Therefore, the original answer below isn't necessary, we can simply do this:
const stream$ = subject$.pipe(
bufferTime(2000, null, 3), // <-- buffer emits # 2000ms OR when 3 items collected
filter(arr => !!arr.length)
);
StackBlitz
ORIGINAL:
It sounds like you want a combination of bufferCount and bufferTime. In other words: "release the buffer when it reaches size X or after Y time has passed".
We can use the race operator, along with those other two to create an observable that emits when the buffer reaches the desired size OR after the duration has passed. We'll also need a little help from take and repeat:
const chunk$ = subject$.pipe(bufferCount(3));
const partial$ = subject$.pipe(
bufferTime(2000),
filter(arr => !!arr.length) // don't emit empty array
);
const stream$ = race([chunk$, partial$]).pipe(
take(1),
repeat()
);
Here we define stream$ to be the first to emit between chunk$ and partial$. However, race will only use the first source that emits, so we use take(1) and repeat to sort of "reset the race".
Then you can do your work with concatMap like this:
stream$.pipe(
concatMap(chunk => this.doWorkWithChunk(chunk))
);
Here's a working StackBlitz demo.
You may want to roll it into a custom operator, so you can simply do something like this:
const stream$ = subject$.pipe(
bufferCountTime(5, 2000)
);
The definition of bufferCountTime() could look like this:
function bufferCountTime<T>(count: number, time: number) {
return (source$: Observable<T>) => {
const chunk$ = source$.pipe(bufferCount(count));
const partial$ = source$.pipe(
bufferTime(time),
filter((arr: T[]) => !!arr.length)
);
return race([chunk$, partial$]).pipe(
take(1),
repeat()
);
}
}
Another StackBlitz sample.
Since I noticed the use of forkJoin in your sample code, I can see you are sending a request to the server for each emission (I was originally under the impression that you were making only 1 call per batch with combined data).
In the case of sending one request per item the solution is much simpler!
There is no need to batch the emissions, you can simply use mergeMap and specify its concurrency parameter. This will limit the number of currently executing requests:
const stream$ = subject$.pipe(
mergeMap(val => doWork(val), 3), // 3 max concurrent requests
);
Here is a visual of what the output would look like when the subject rapidly emits:
Notice the work only starts for the first 3 items initially. Emissions after that are queued up and processed as the prior in flight items complete.
Here's a StackBlitz example of this behavior.
TLDR;
A StackBlitz app with the solution can be found here.
Explanation
Here would be an approach:
const bufferLen = 3;
const count$ = subject.pipe(filter((_, idx) => (idx + 1) % bufferLen === 0));
const timeout$ = subject.pipe(
filter((_, idx) => idx === 0),
switchMapTo(timer(0))
);
subject
.pipe(
buffer(
merge(count$, timeout$).pipe(
take(1),
repeat()
)
),
concatMap(buffer => forkJoin(buffer.map(doWork)))
)
.subscribe(/* console.warn */);
/* Output:
Processing 1
Processing 2
Processing 3
Processed 1
Processed 2
Processed 3
Processing 4
Processing 5
Processed 4
Processed 5
Processing 6 <- after the `setTimeout`'s timer expires
Processing 7
Processing 8
Processed 6
Processed 7
Processed 8
*/
The idea was to still use the bufferCount's behavior when items come in synchronously, but, at the same time, detect when fewer items than the chosen bufferLen are in the buffer. I thought that this detection could be done using a timer(0), because it internally schedules a macrotask, so it is ensured that items emitted synchronously will be considered first.
However, there is no operator that exactly combines the logic delineated above. But it's important to keep in mind that we certainly want a behavior similar to the one the buffer operator provides. As in, we will for sure have something like subject.pipe(buffer(...)).
Let's see how we can achieve something similar to what bufferTime does, but without using bufferTime:
const bufferLen = 3;
const count$ = subject.pipe(filter((_, idx) => (idx + 1) % bufferLen === 0));
Given the above snippet, using buffer(count$) and bufferTime(3), we should get the same behavior.
Let's move now onto the detection part:
const timeout$ = subject.pipe(
filter((_, idx) => idx === 0),
switchMapTo(timer(0))
);
What it essentially does is to start a timer after the subject has emitted its first item. This will make more sense when we have more context:
subject
.pipe(
buffer(
merge(count$, timeout$).pipe(
take(1),
repeat()
)
),
concatMap(buffer => forkJoin(buffer.map(doWork)))
)
.subscribe(/* console.warn */);
By using merge(count$, timeout$), this is what we'd be saying: when the subject emits, start adding items to the buffer and, at the same time, start the timer. The timer is started too because it is used to determine if fewer items will be in the buffer.
Let's walk through the example provided in the StackBlitz app:
from([1, 2, 3, 4, 5])
.pipe(tap(i => subject.next(i)))
.subscribe();
// Then mimic some more items coming through a while later
setTimeout(() => {
subject.next(6);
subject.next(7);
subject.next(8);
}, 10000);
When 1 is emitted, it will be added to the buffer and the timer will start. Then 2 and 3 arrive immediately, so the accumulated values will be emitted.
Because we're also using take(1) and repeat(), the process will restart. Now, when 4 is emitted, it will be added to the buffer and the timer will start again. 5 arrives immediately, but the number of the collected items until now is less than the given buffer length, meaning that until the 3rd value arrives, the timer will have time to finish. When the timer finishes, the [4,5] chunk will be emitted. What happens with [6, 7, 8] is the same as what happened with [1, 2, 3].
I have a list of observables I'd like to fire 5 at a time. I've tried using a mergeMap, but clearly I'm using it wrong:
// obsArray defined above... is a Array of Observables... about 30 of them
of(obsArray).pipe(
mergeMap(x => x, 5)
).subscribe();
The issue is that x in the mergeMap is the entire observable list. How do I send 5 at a time to be fired (they are http calls)?
Use from to emit single items from an array. You can also replace mergeMap(x => x) with mergeAll.
from(obsArray).pipe(mergeAll(5))
I have this sample code:
interval(500).pipe(
throttleTime(1000)
).subscribe(arg => {
console.log(arg);
});
Which emits:
0
3
6
...
I understand that it emits the latest value every 1000 milliseconds. My problem is that it ignores the values that aren't the latest. Is there an operator similar to throttleTime, but one that saves these ignored values? I'd like it to emit:
[0]
[1,2,3]
[4,5,6]
...
Edit: Ideally, I'd like something that listens to clicks of a button. When the first click happens, the code starts a timer for N milliseconds. The user can keep clicking during this time. Once N milliseconds are up, the operator fires with an array holding all of the events that happened during those N milliseconds.
Super-ideally, I'd like the timer to reset everytime the user clicks the button.
You can use bufferToggle. It collects values and returns in an array, just as per your requirement:
const click$ = fromEvent(document, 'click').pipe(
// count emitions
scan(acc => acc += 1, 0)
)
const timerInterval = () => timer(5000);
// buffer emitions to an array. When first click happens 'turn on' buffer
// and when timer ends turn it off.
// throttle used to avoid turning on buffer on every click
click$.pipe(
bufferToggle(
click$.pipe(throttle(timerInterval)),
timerInterval
),
)
.subscribe(console.log)
But to note - there is no clear separation between clicking intervals. For ex., user might be clicking longer than 5 secs and in result, two emitions will happen.
But this is more as an architectural task for you to solve.
DEMO
Given a stream that pushes items into Observable A.
How to notify subscribers when there are exactly 3 items pushed in A?
To visualize it:
--o--i--o------i---------o----i------|---> A
I need to wait for all i to arrive even though there are other items present such as o.
And if there are no exactly 3 is pushed within a timeframe, then fire an error to redo the procedure.
Thanks.
I'd use filter() to filter only i characters, count() to count all items emitted until the source completes and concatMap() to decide whether the number of items is correct and eventually throw an error.
import {Observable} from 'rxjs';
Observable.from(['o', 'i', 'o', 'i', 'o', 'i'/*, 'i'*/])
.filter(val => val == 'i')
.count()
.concatMap(count => count == 3 ? Observable.of(count) : Observable.throw('Expected 3 values'))
.subscribe(
count => console.log(count),
err => console.log(err)
);
Which prints to console:
3
And if you add one more i to the source Observable the subscriber receives an error:
Expected 3 values
See live demo: http://plnkr.co/edit/VkQeGpY2gDj3gFhXD2uR?p=preview
If you want to resubscribe when an error happens use retry() or retryWhen(). This another demo fails first because it contains only 2 is but it resubscribes with retry() and then succeeds.
Here's more complicated demo: http://plnkr.co/edit/ID5WTVYGYZuhEJ7fXn1F?p=preview
RxJS 5 Angular 2 RC4 app written in Typescript 1.9:
I have two observables in a chain. I would like, if a condition is met in the 2nd, for the first to be completed immediately. My efforts seem unnecessarily complex. In the example below, I try to stop the first observable after it has emitted 3 values:
source = Observable.interval(1000)
.do(()=>this.print("*******EMITTING from Source*******"))
.switchMap(count => {
if(count<3){ //just pass along the value
return Observable.create(observer=>{
observer.next(count);observer.complete()
})
}
else{ //abort by issuing a non-productive observable
return Observable.create(observer=>
observer.complete()
)
}
})
this.source.subscribe(count=>this.print('Ouput is '+count);
Here is the output:
*******EMITTING from Source*******
Output is 0
*******EMITTING from Source*******
Output is 1
*******EMITTING from Source*******
Output is 2
*******EMITTING from Source*******
*******EMITTING from Source*******
*******EMITTING from Source*******
So, functionally I get the result I want because the wider script stops getting notifications after three outputs. However, I'm sure there is a better way. My problems are:
The upstream observable continues to emitting forever. How can I stop it?
I'm creating a new observable down the chain on every emission. Shouldn't I be able to just pass along the first 3 values but abort or complete the chain on the 4th?
You can use take operator to do it.take takes the first N events and completes the stream.
this.source = Observable.interval(1000)
.do(()=>this.print("*******EMITTING from Source*******"))
.take(3);
this.source.subscribe(count=>this.print('Ouput is '+count);
Your example's stream doesn't complete because switchMap's outer stream doesn't complete when inner streams complete. switchMap() is equal to map().switch(). In your example, the map part emits observables like:
next(0), complete()
next(1), complete()
next(2), complete()
complete()
complete()
complete()
complete()
...(continues infinitely)...
And the switch part switches those observables and keeps waiting for upcoming observables.
EDIT
Your example also could be written as:
source = Observable.interval(1000)
.do(()=>this.print("*******EMITTING from Source*******"))
.takeWhile(count => count < 3);
EDIT 2
Regarding your comment, if you want to terminate the stream if the inner stream emits true:
source = Observable.interval(1000)
.do(()=>this.print("*******EMITTING from Source*******"))
.switchMap(count => createSomeObservable(count))
.takeWhile(x => x !== true);