I'm trying to implement a cache using a ReplaySubject
like follows, but I'm unable to solve the situation using Rx. See code and accompanying tests. The trouble is that the cache drops the newest entries and preserves the oldest.
public static class RxExtensions
{
/// <summary>
/// A cache that keeps distinct elements where the elements are replaced by the latest. Upon subscription the subscriber should receive the full cache contents.
/// </summary>
/// <typeparam name="T">The type of the result</typeparam>
/// <typeparam name="TKey">The type of the selector key for distinct results.</typeparam>
/// <param name="newElements">The sequence of new elements.</param>
/// <param name="seedElements">The elements when the cache is started.</param>
/// <param name="replacementSelector">The replacement to select distinct elements in the cache.</param>
/// <returns>The cache contents upon first call and changes thereafter.</returns>
public static IObservable<T> Cache<T, TKey>(this IObservable<T> newElements, IEnumerable<T> seedElements, Func<T, TKey> replacementSelector)
{
var replaySubject = new ReplaySubject<T>();
seedElements.ToObservable().Concat(newElements).Subscribe(replaySubject);
return replaySubject.Distinct(replacementSelector);
}
}
It looks like the old ones, the seed values, would be dropped if I write the function like
newElements.Subscribe(replaySubject);
return replaySubject.Concat(seedElements.ToObservable()).Distinct(replacementSelector);
but due to how I think .Concat
works, the "works" is likely just because how the test currently are, see next.
public void CacheTests()
{
var seedElements = new List<Event>(new[]
{
new Event { Id = 0, Batch = 1 },
new Event { Id = 1, Batch = 1 },
new Event { Id = 2, Batch = 1 }
});
var testScheduler = new TestScheduler();
var observer = testScheduler.CreateObserver<Event>();
var batchTicks = TimeSpan.FromSeconds(10);
var xs = testScheduler.CreateHotObservable
(
ReactiveTest.OnNext(batchTicks.Ticks, new Event { Id = 0, Batch = 2 }),
ReactiveTest.OnNext(batchTicks.Ticks, new Event { Id = 1, Batch = 2 }),
ReactiveTest.OnNext(batchTicks.Ticks, new Event { Id = 2, Batch = 2 }),
ReactiveTest.OnNext(batchTicks.Ticks, new Event { Id = 3, Batch = 2 }),
ReactiveTest.OnNext(batchTicks.Ticks, new Event { Id = 4, Batch = 2 }),
ReactiveTest.OnNext(batchTicks.Ticks + 10, new Event { Id = 0, Batch = 3 }),
ReactiveTest.OnNext(batchTicks.Ticks + 10, new Event { Id = 1, Batch = 3 })
);
var subs = xs.Cache(seedElements, i => i.Id).Subscribe(observer);
var seedElementsAndNoMore = observer.Messages.ToArray();
Assert.IsTrue(observer.Messages.Count == 3);
testScheduler.Start();
var seedAndReplacedElements = observer.Messages.ToArray();
//OK, a bad assert, we should create expected timings and want to check
//also the actual batch numbers, but to get things going...
//There should be Events with IDs { 1, 2, 3, 4, 5 } all having a batch number
//of either 2 or 3. Also, a total of 7 (not 10) events
//should've been observed.
Assert.IsTrue(observer.Messages.Count == 7);
for(int i = 0; i < seedAndReplacedElements.Length; ++i)
{
Assert.IsTrue(seedAndReplacedElements[i].Value.Value.Batch > 1)
}
}
I think what I'd like to have is
public static IObservable<T> Cache<T, TKey>(this IObservable<T> newElements, IEnumerable<T> seedElements, Func<T, TKey> replacementSelector)
{
var replaySubject = new ReplaySubject<T>();
newElements.StartWith(seedElements).Distinct(replacementSelector).Subscribe(replaySubject);
return replaySubject;
}
but the trouble is that the seed values are there first and then Rx drops the newer values, not the seed values. Then doing the other way around (maybe using .Merge
) could create a situation the seed is introduced to the observable after new values have been received, thus creating a situation where the seed values aren't actually replaced.