0

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:

  1. With the .Publish().RefCount() operators, late subscribers don't get a OnNext notification from the Client observable.
  2. With the .Replay(1).RefCount() operators, subscribers after the initial disconnection get a disconnected client replayed to them.
  3. 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?

MarkNS
  • 3,811
  • 2
  • 43
  • 60

0 Answers0