I'm trying to wrap a 3rd party websocket client library with some Reactive fairy dust.
The behaviour I would like to implement is as follows:
- There must be a single connection to the server, so the client must be shared between subscribers.
- A connection needs to be established when the first subscription is made on the client.
- When the last subscription is disposed, the connection to the server should be disconnected.
- The connection/query/disconnection flow can happen multiple times.
- Subscribers can come and go ad-hoc (based on user actions).
Here's a mock of the client library:
public class Client
{
readonly Random _random = new Random();
public async Task Connect(CancellationToken ct)
{
Console.WriteLine($"Client {GetHashCode()} connecting");
await Task.Delay(100, ct);
}
public async Task Disconnect()
{
Console.WriteLine($"Client {GetHashCode()} disconnecting");
await Task.Delay(100);
}
public async IAsyncEnumerable<string> SubscribeFoo(string name,
[EnumeratorCancellation] CancellationToken ct = default)
{
Console.WriteLine($"Client {GetHashCode()} subscribing foo {name}");
while (!ct.IsCancellationRequested)
{
await Task.Delay(_random.Next(1500, 2000), ct);
yield return $"foo {name} {_random.Next(1, 100)}";
}
}
public async IAsyncEnumerable<string> SubscribeBar(string name,
[EnumeratorCancellation] CancellationToken ct = default)
{
Console.WriteLine($"Client {GetHashCode()} subscribing bar {name}");
while (!ct.IsCancellationRequested)
{
await Task.Delay(_random.Next(1500, 2000), ct);
yield return $"bar {name} {_random.Next(1, 100)}";
}
}
}
The observable wrapper query service I've built so far looks like this:
public class QueryService
{
public QueryService(Client client)
{
ClientObservable = Observable.Create<Client>(async (o, ct) =>
{
await client.Connect(ct);
o.OnNext(client);
async void Dispose()
{
await client.Disconnect();
}
return Disposable.Create(Dispose);
})
.Publish() // <- doesn't allow late subscribers
//.Replay(1) // <- means 2nd group of subscribers get the disconnected client first
.RefCount();
}
private IObservable<Client> ClientObservable { get; }
public IObservable<string> Foo(string name)
{
return ClientObservable
.SelectMany(client => client.SubscribeFoo(name)
.ToObservable());
}
public IObservable<string> Bar(string name)
{
return ClientObservable
.SelectMany(client => client.SubscribeBar(name)
.ToObservable());
}
}
And finally some test harness code:
internal class Program
{
public static async Task Main()
{
var c = new Client();
var queryService = new QueryService(c);
var red = queryService.Foo("red")
.SubscribeConsole();
await Task.Delay(3000);
var blue = queryService.Bar("blue")
.SubscribeConsole();
await Task.Delay(3000);
red.Dispose();
blue.Dispose();
await Task.Delay(3000);
Console.WriteLine("reconnecting blue");
blue = queryService.Foo("blue")
.SubscribeConsole();
await Task.Delay(3000);
}
}
The problems I'm having are as follows:
- With the
.Publish().RefCount()
operators, late subscribers don't get a OnNext notification from theClient
observable. - With the
.Replay(1).RefCount()
operators, subscribers after the initial disconnection get a disconnected client replayed to them. - If the client itself errors, the whole
Client
observable is completely dead. No events after OnError/OnCompleted.
I feel like this is a pretty common pattern, but even after reading a lot of SO questions and poring over Rx.NET in Action, a proper solution has eluded me for a couple of days now :(
Does anyone have some advice on how to better implement the requirements above?