I want to run through an IObservable<T> looking for an element that matches a predicate, and if not found, return the last element of the IObservable<T>. I don’t want to have to store the entire contents of the IObservable<T>, and I don’t want to loop through the IObservable twice, so I’ve set up an extension method
public static class ObservableExtensions
{
public static IObservable<T> FirstOrLastAsync<T>(this IObservable<T> source, Func<T, bool> pred)
{
return Observable.Create<T>(o =>
{
var hot = source.Publish();
var store = new AsyncSubject<T>();
var d1 = hot.Subscribe(store);
var d2 = hot.FirstAsync(x => pred(x)).Amb(store).Subscribe(o);
var d3 = hot.Connect();
return new CompositeDisposable(d1, d2, d3);
});
}
public static T FirstOrLast<T>(this IObservable<T> source, Func<T, bool> pred)
{
return source.FirstOrLastAsync(pred).Wait();
}
}
The Async method creates a hot observable from a potentially cold one passed in. It subscribes an AsyncSubject<T> to remember the last element, and an IObservable<T> that looks for the element. It then takes the first element from either of those IObservable<T>s, which ever returns a value first via .Amb (AsyncSubject<T> doesn’t return a value until it gets an .OnCompleted message).
My questions are the following:
- Can this be written better or more concisely using different Observable methods?
- Do all of those disposables need to be included in the CompositeDisposable?
- When the hot observable is completed without finding a matching element, is there a race condition between FirstAsync throwing an exception, and the AsyncSubject propagating its value?
- If so, do I need to change the line to:
var d2 = hot.Where(x => pred(x)).Take(1).Amb(store).Subscribe(o);
I’m pretty new to RX, and this is my first extension on IObservable.
EDIT
I ended up going with
public static class ObservableExtensions
{
public static IObservable<T> FirstOrLastAsync<T>(this IObservable<T> source, Func<T, bool> pred)
{
var hot = source.Publish().RefCount();
return hot.TakeLast(1).Amb(hot.Where(pred).Take(1).Concat(Observable.Never<T>()));
}
public static T FirstOrLast<T>(this IObservable<T> source, Func<T, bool> pred)
{
return source.FirstOrLastAsync(pred).First();
}
}
You could Amb the two cases you want together.
If your source observable is cold, you can do a
Publish|Refcount.Test: