How to throttle event stream using RX?

前端 未结 8 722
终归单人心
终归单人心 2020-11-27 04:04

I want to effectively throttle an event stream, so that my delegate is called when the first event is received but then not for 1 second if subsequent events are received. A

相关标签:
8条回答
  • 2020-11-27 04:36

    I was struggling with this same problem last night, and believe I've found a more elegant (or at least shorter) solution:

    var delay = Observable.Empty<T>().Delay(TimeSpan.FromSeconds(1));
    var throttledSource = source.Take(1).Concat(delay).Repeat();
    
    0 讨论(0)
  • 2020-11-27 04:42

    Here's is what I got with some help from the RX Forum:

    The idea is to issue a series of "tickets" for the original sequence to fire. These "tickets" are delayed for the timeout, excluding the very first one, which is immediately pre-pended to the ticket sequence. When an event comes in and there is a ticket waiting, the event fires immediately, otherwise it waits till the ticket and then fires. When it fires, the next ticket is issued, and so on...

    To combine the tickets and original events, we need a combinator. Unfortunately, the "standard" .CombineLatest cannot be used here because it would fire on tickets and events that were used previousely. So I had to create my own combinator, which is basically a filtered .CombineLatest, that fires only when both elements in the combination are "fresh" - were never returned before. I call it .CombineVeryLatest aka .BrokenZip ;)

    Using .CombineVeryLatest, the above idea can be implemented as such:

        public static IObservable<T> SampleResponsive<T>(
            this IObservable<T> source, TimeSpan delay)
        {
            return source.Publish(src =>
            {
                var fire = new Subject<T>();
    
                var whenCanFire = fire
                    .Select(u => new Unit())
                    .Delay(delay)
                    .StartWith(new Unit());
    
                var subscription = src
                    .CombineVeryLatest(whenCanFire, (x, flag) => x)
                    .Subscribe(fire);
    
                return fire.Finally(subscription.Dispose);
            });
        }
    
        public static IObservable<TResult> CombineVeryLatest
            <TLeft, TRight, TResult>(this IObservable<TLeft> leftSource,
            IObservable<TRight> rightSource, Func<TLeft, TRight, TResult> selector)
        {
            var ls = leftSource.Select(x => new Used<TLeft>(x));
            var rs = rightSource.Select(x => new Used<TRight>(x));
            var cmb = ls.CombineLatest(rs, (x, y) => new { x, y });
            var fltCmb = cmb
                .Where(a => !(a.x.IsUsed || a.y.IsUsed))
                .Do(a => { a.x.IsUsed = true; a.y.IsUsed = true; });
            return fltCmb.Select(a => selector(a.x.Value, a.y.Value));
        }
    
        private class Used<T>
        {
            internal T Value { get; private set; }
            internal bool IsUsed { get; set; }
    
            internal Used(T value)
            {
                Value = value;
            }
        }
    

    Edit: here's another more compact variation of CombineVeryLatest proposed by Andreas Köpf on the forum:

    public static IObservable<TResult> CombineVeryLatest
      <TLeft, TRight, TResult>(this IObservable<TLeft> leftSource,
      IObservable<TRight> rightSource, Func<TLeft, TRight, TResult> selector)
    {
        return Observable.Defer(() =>
        {
            int l = -1, r = -1;
            return Observable.CombineLatest(
                leftSource.Select(Tuple.Create<TLeft, int>),
                rightSource.Select(Tuple.Create<TRight, int>),
                    (x, y) => new { x, y })
                .Where(t => t.x.Item2 != l && t.y.Item2 != r)
                .Do(t => { l = t.x.Item2; r = t.y.Item2; })
                .Select(t => selector(t.x.Item1, t.y.Item1));
        });
    }
    
    0 讨论(0)
提交回复
热议问题