emitter-io / csharp

Client library for emitter.io compatible with .Net, .Net MicroFramework and WinRT
http://emitter.io
Eclipse Public License 1.0
29 stars 21 forks source link

Do you plan to update the library for the latest version of emitter #10

Closed postacik closed 5 years ago

postacik commented 5 years ago

Hi,

The C# library for emitter contains less functionality than the go and javascript libraries.

Do you have any plans to update it to add functions for presence and other new emitter features?

Florimond commented 5 years ago

Hello,

yes. I've been recently working on the Python V2 of the lib, but the C# is the very next thing I'm going to do. I'll be most likely working on it this week-end. Then comes the Javascript lib, which also needs some work.

postacik commented 5 years ago

Great news :))

In the current library you're using a Hashtable to keep keygen event handlers which are not disposed after the keygen response arrives.

But for the message handlers you've used a so called ReverseTrie which removes the event handler in case of unsubscribe.

Are you also going to use a ReverseTrie to keep event handlers for presence requests?

Is it possible to unsubscribe from presence notifications?

Florimond commented 5 years ago

Yes, it's possible to unsubscribe from getting notifications. In the Go and JS libs, the Presence function takes two arguments: status, and changes. You can simply send a new request with changes set to false to disable the notifications. I'll probably do it the same way in the C# lib.

Unless I'm missing something, using the ReverseTrie for presence events could work. However, I'm thinking there must be a good reason the main engineer didn't do that in the Go lib. I'll discuss it with him and see whether it's actually feasible.

Florimond commented 5 years ago

So, I've been working a bit on the C# lib on Saturday. There is a lot of work to do, and it's actually pretty difficult to design an API to be coherent with itself, and with the versions in other languages.

The C# version uses On to subscribe, but has an Unsubscribe method. I'm going to deprecate the On and call the new one Subscribe, just like in other libs.

I want to use the trie for Presence handlers. However, I'm not sure how to call the methods related to this feature. The thing is, unsubscribing from Presence is done by sending a Presence request with both status and changes flags set to false. I could check both flags in the Presence method, and add a handler to the trie if any of them is set to true. I would remove the handler from the trie if both flags were false. I think this is quite ugly, and un-intuitive.

Alternatively, I could consider the Presence method should only be used when you want to receive the status, the changes, or both. I would not make a check on the flags in the method, and always register the handler with the trie. I would then create a second method, something like Unpresence, that would not take any flags as parameters, would send a Presence request with flags set to false, and unregister with the trie.

What do you think?

Florimond commented 5 years ago

Another proposition, 3 methods:

That way, all use cases should be covered. Among others

In any case, one can unsubscribe and remove the handler from the trie with PresenceUnsubscribe.

Note: this is still not ideal because one could forget to unsubscribe after a PresenceStatus. Another mechanism could be put in place, flagging a handler for a one time use. If a handler has a "one time use" flag set to true, and a status is received, the handler would be called then removed. That might be a bit more complex to implement though...

postacik commented 5 years ago

Nice ideas. I was thinking of a similar way of doing it.

For PresenceStatus, flagging the handler for one time use seems to be only reasonable way of doing it.

For status only events, the channel name that arrives always equals to the channel name sent.

However, for subscription (changes = true), since events arrive also for sub channels, I had to do something like below:

                // Did we receive a presence response?
                if (e.Topic == "emitter/presence/")
                {
                    var response = PresenceResponse.FromBinary(e.Message);
                    if (response != null && response.Status == 200)
                    {
                        // Invoke every handler matching the channel
                        foreach (var item in PresenceHandlers)
                        {
                            if (response.Channel.StartsWith(item.Key)) item.Value(response);
                        }
                        return;
                    }
                }

And my presence function is like below:

        private readonly List<KeyValuePair<string, PresenceHandler>> PresenceHandlers = new List<KeyValuePair<string, PresenceHandler>>();

        /// <summary>
        /// Asynchronously sends a presence request to the emitter.io service.
        /// </summary>
        /// <param name="secretKey">The secret key for this request.</param>
        /// <param name="channel">The target channel for the requested key.</param>
        /// <param name="status">Whether the current state should be retrieved or not.</param>
        /// <param name="changes">Whether the future changes should be received or not.</param>
        public void Presence(string secretKey, string channel, bool status, bool changes, PresenceHandler handler)
        {
            // Prepare the request
            var request = new PresenceRequest();
            request.Key = secretKey;
            request.Channel = channel;
            request.Status = status;
            request.Changes = changes;

            // Register the handler
            PresenceHandlers.Add(new KeyValuePair<string, PresenceHandler>(request.Channel, handler));

            // Serialize and publish the request
            this.Publish("emitter/", "presence/", Encoding.UTF8.GetBytes(request.ToJson()));
        }

PresenceRequest:

namespace Emitter.Messages
{
    /// <summary>
    /// Represents a presence request.
    /// </summary>
    public class PresenceRequest
    {
        /// <summary>
        /// Gets or sets the secret key for this request.
        /// </summary>
        public string Key;

        /// <summary>
        /// Gets or sets the target channel for presence.
        /// </summary>
        public string Channel;

        /// <summary>
        /// Determines whether the current state should be retrieved or not.
        /// </summary>
        public bool Status;

        /// <summary>
        /// Determines whether the future changes should be received or not.
        /// </summary>
        public bool Changes;

        /// <summary>
        /// Converts the request to JSON format.
        /// </summary>
        /// <returns></returns>
        public string ToJson()
        {
            return JsonSerializer.SerializeObject(new Hashtable
            {
                {"key", this.Key},
                {"channel", this.Channel},
                {"status", this.Status},
                {"changes", this.Changes}
            });
        }
    }

    /// <summary>
    /// Handles the presence response.
    /// </summary>
    /// <param name="response">The presence response to handle.</param>
    public delegate void PresenceHandler(PresenceResponse response);

}

PresenceResponse:

namespace Emitter.Messages
{
    /// <summary>
    /// Represents a presence response.
    /// </summary>
    public class PresenceResponse
    {
        /// <summary>
        /// Gets or sets the presence channel.
        /// </summary>
        public string Channel;

        /// <summary>
        /// Gets or sets presence event name.
        /// </summary>
        public string Event;

        /// <summary>
        /// Gets or sets presence event time.
        /// </summary>
        public int Time;

        /// <summary>
        /// Gets or sets list of subscribers.
        /// </summary>
        public List<PresenceWho> Who;

        /// <summary>
        /// Gets or sets the status code returned by emitter.io service.
        /// </summary>
        public int Status;

        /// <summary>
        /// Deserializes the JSON presence response.
        /// </summary>
        /// <param name="json">The json string to deserialize from.</param>
        /// <returns></returns>
        public static PresenceResponse FromJson(string json)
        {
            var map = JsonSerializer.DeserializeString(json) as Hashtable;
            var response = new PresenceResponse();
            response.Status = 200;
            if (map.Contains("status"))
            {
                response.Status = Convert.ToInt32(map["status"].ToString());
            }
            if (response.Status == 200)
            {
                response.Channel = (string)map["channel"];
                response.Event = (string)map["event"];
                response.Time = Convert.ToInt32(map["time"].ToString());
                response.Who = new List<PresenceWho>();
                if (map.Contains("who"))
                {
                    if (map["who"] is ArrayList)
                    {
                        foreach (Hashtable who in (ArrayList)map["who"])
                        {
                            response.Who.Add(new PresenceWho { Id = (string)who["id"] });
                        }
                    }
                    if (map["who"] is Hashtable)
                    {
                        var who = (Hashtable)map["who"];
                        response.Who.Add(new PresenceWho { Id = (string)who["id"] });
                    }
                }
            }

            return response;
        }

        /// <summary>
        /// Deserializes the JSON presence response.
        /// </summary>
        /// <param name="message">The binary UTF-8 encoded string.</param>
        /// <returns></returns>
        public static PresenceResponse FromBinary(byte[] message)
        {
            return FromJson(new string(Encoding.UTF8.GetChars(message)));
        }

    }

    public class PresenceWho
    {
        /// <summary>
        /// Gets or sets the id of the subscriber.
        /// </summary>
        public string Id;
    }

}

This implementation is not complete, yet.

And I'm not sure if I serialized the "who" property properly.

Florimond commented 5 years ago

First, I want to let you know I'll be too busy to work on this until tomorrow evening. But I'm currently working on those features on an update branch : https://github.com/emitter-io/csharp/tree/update You can have a look at the commits I already did to see what I've already changed.

You'll find PresenceSubscribe and PresenceUnsubscribe, and they already use the trie. This means you can give a specific handler to subchannels (if it works properly, it still requires some testing), but you will still receive messages for sub-channels if you do a PresenceSubscribe for a master channel, that's the way it's intended to work.

I discussed the idea of one-time handlers with the main engineer, and he didn't think it was a good idea. After thinking about it again, I'm not overly concerned about this. It's actually unlikely that people will often only request one status on a channel and then want to unsubscribe immediately. It won't be a common use case...

Edit: On the update branch, new files are added to the Emitter project, and I only add links to these files into the project using .NET 4.5. That is the one used by the sample project. I'll add links into the other .NET versions later, it's annoying and time consuming.

PS: have a nice day.

Florimond commented 5 years ago

I found the time to finish (I think) the presence feature on the update branch. It will work best with the latest version of the emitter broker (I had to do a little modification there too).

postacik commented 5 years ago

I'll test it asap. Thanks :)