13

A use case which I have encountered, and I suspect I can't be the only one, is for a method like:

IObservable<T> Observable.RepeatLastValueDuringSilence(this IObservable<T> inner, TimeSpan maxQuietPeriod);

which would return all the future items from the inner observable, but also, if the inner observable doesn't call OnNext for a certain period of time (maxQuietPeriod), it just repeats the last value (until of course inner calls OnCompleted or OnError).

A justification would be for a service to periodically ping out a periodic status update. For example:

var myStatus = Observable.FromEvent(
    h=>this.StatusUpdate+=h,
    h=>this.StatusUpdate-=h);

var messageBusStatusPinger = myStatus
    .RepeatLastValueDuringSilence(TimeSpan.FromSeconds(1))
    .Subscribe(update => _messageBus.Send(update));

Does something like this exist? Or am I over-estimating it's usefulness?

Thanks, Alex

PS: I apologise for any incorrect terminology/syntax, as I'm only just exploring Rx for the first time.

AlexC
  • 1,646
  • 1
  • 14
  • 26
  • At what frequency should it keep repeating? – Asti Jul 12 '12 at 16:31
  • @Asti: the intention was that this should be specified by maxQuietPeriod. If the inner sequence has not produced a value for maxQuietPeriod, then a repeated value should be generated. – AlexC Jul 13 '12 at 08:28

4 Answers4

10

Similar solution to Matthew's, but here the timer starts after each element is received in the source, which I think is more correct (however the differences are unlikely to matter):

public static IObservable<T> RepeatLastValueDuringSilence<T>(this IObservable<T> inner, TimeSpan maxQuietPeriod)
{    
    return inner.Select(x => 
        Observable.Interval(maxQuietPeriod)
                  .Select(_ => x)
                  .StartWith(x)
    ).Switch();
}

And the test:

var source = Observable.Interval(TimeSpan.FromMilliseconds(100)).Take(5).Select(_ => "1")
                       .Concat(Observable.Interval(TimeSpan.FromSeconds(1)).Take(5).Select(_ => "2"))
                       .Concat(Observable.Interval(TimeSpan.FromMilliseconds(100)).Take(5).Select(_ => "3"));

source.RepeatLastValueDuringSilence(TimeSpan.FromMilliseconds(200)).Subscribe(Console.WriteLine);

You should see 1 printed 10 times (5 from source, 5 repeated during silence), then lots of 2 as you get the one from source and 4 more from silence between each, followed by infinite 3.

yamen
  • 15,390
  • 3
  • 42
  • 52
9

This fairly simple query does the job:

var query =
    source
        .Select(s =>
            Observable
                .Interval(TimeSpan.FromSeconds(1.0))
                .StartWith(s)
                .Select(x => s))
        .Switch();

Never underestimate the power of .Switch().

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
1

I think this does what you want, if your observable isn't hot you'll need to Publish and Refcount it:

public static IObservable<T> RepeatLastValueDuringSilence<T>(this IObservable<T> inner, TimeSpan maxQuietPeriod)
{
    var throttled = inner.Throttle(maxQuietPeriod);
    var repeating = throttled.SelectMany(i => 
        Observable
            .Interval(maxQuietPeriod)
            .Select(_ => i)
            .TakeUntil(inner));
    return Observable.Merge(inner, throttled, repeating);
}
Matthew Finlay
  • 3,354
  • 2
  • 27
  • 32
  • Should note that for this to work the observable must be hot - perhaps add this at the front: `var published = inner.Publish().RefCount();` – yamen Jul 13 '12 at 05:14
-1

There is not a method in the Rx libraries, but I also had need of such a method. In my use case, I needed to output values even if the source does not put out any values. If you do not want to put out any values until the first source value comes through, you can remove the defaultValue parameter and the call to createTimer() before the subscribe call.

The scheduler is needed to run the timer. An obvious overload would be one that doesn't take a scheduler and picks a default scheduler (I used the ThreadPool scheduler).

Imports System.Reactive
Imports System.Reactive.Concurrency
Imports System.Reactive.Disposables
Imports System.Reactive.Linq

<Extension()>
Public Function AtLeastEvery(Of T)(source As IObservable(Of T), 
                                   timeout As TimeSpan, 
                                   defaultValue As T, 
                                   scheduler As IScheduler
                                  ) As IObservable(Of T)
    If source Is Nothing Then Throw New ArgumentNullException("source")
    If scheduler Is Nothing Then Throw New ArgumentNullException("scheduler")
    Return Observable.Create(
        Function(observer As IObserver(Of T))
            Dim id As ULong
            Dim gate As New Object()
            Dim timer As New SerialDisposable()
            Dim lastValue As T = defaultValue

            Dim createTimer As Action =
                Sub()
                    Dim startId As ULong = id
                    timer.Disposable = scheduler.Schedule(timeout,
                                           Sub(self As Action(Of TimeSpan))
                                               Dim noChange As Boolean
                                               SyncLock gate
                                                   noChange = (id = startId)
                                                   If noChange Then
                                                       observer.OnNext(lastValue)
                                                   End If
                                               End SyncLock
                                               'only restart if no change, otherwise
                                               'the change restarted the timeout
                                               If noChange Then self(timeout)
                                           End Sub)
                End Sub
            'start the first timeout
            createTimer()
            'subscribe to the source observable
            Dim subscription = source.Subscribe(
                Sub(v)
                    SyncLock gate
                        id += 1UL
                        lastValue = v
                    End SyncLock
                    observer.OnNext(v)
                    createTimer() 'reset the timeout
                End Sub,
                Sub(ex)
                    SyncLock gate
                        id += 1UL
                    End SyncLock
                    observer.OnError(ex)
                    'do not reset the timeout, because the sequence has ended
                End Sub,
                Sub()
                    SyncLock gate
                        id += 1UL
                    End SyncLock
                    observer.OnCompleted()
                    'do not reset the timeout, because the sequence has ended
                End Sub)

            Return New CompositeDisposable(timer, subscription)
        End Function)
End Function
Gideon Engelberth
  • 6,095
  • 1
  • 21
  • 22