Skip to content

Commit

Permalink
Merge pull request #28 from Mazyod/refactor/json-handling
Browse files Browse the repository at this point in the history
Refactoring Json handling
  • Loading branch information
Mazyod committed Apr 27, 2023
2 parents 8b66586 + ea5e2b8 commit 18eb202
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 277 deletions.
27 changes: 25 additions & 2 deletions Migration.md
@@ -1,7 +1,30 @@

# Migration Guide

## From pre-release
## Json Refactoring Effort (May 2023)

The library was missing the ability to expose the full JSON response from the presence payload. This limitation exposed a major weakness in the library's design, which was the lack of a unified JSON response interface.
Also previously, the library abstracted the underlying JSON object type using an opaque `object` type. This caused a lot of frustration due to the lack of type safety and the need to cast the object to the correct type.

Now, `IJsonBox` interface is introduced to abstract the underlying JSON object type. It also lead to a unified interface for interacting with any JSON response.
More nice side-effects of this change were better performance and less memory usage.

If you are implementing your own IMessageSerializer, you may need to update your implementation to support the new `IJsonBox` interface.
The new interface should be simpler, allowing you to easily migrate to the new version.

In order to migrate to the new version, you need to make sure you are using the new `IJsonBox` interface instead of the `object` type.
Also, the JsonResponse and JsonPayload extension methods are now removed in favor of the new `IJsonBox` interface.
(The type system should detect all these issues.)

```diff
-reply.JsonResponse<ChannelError>()
+reply.Response.Unbox<ChannelError>()

-message.JsonPayload<PresenceEvent>()
+message.Payload.Unbox<PresenceEvent>()
```

## From pre-release (before 2022)

The library underwent a major overhaul since the pre-release version, so it will be very difficult to document every change.

Expand All @@ -28,7 +51,7 @@ public WebsocketState State {

#### DelayedExecutor Changes

Instead of returning `uint`, `DelayedExecutor` now returns `IDelayedExecution` instance. It is a simple object that "knows" how to cancel the delayed exection.
Instead of returning `uint`, `DelayedExecutor` now returns `IDelayedExecution` instance. It is a simple object that "knows" how to cancel the delayed execution.

```diff
-public uint Execute(Action action, TimeSpan delay) {
Expand Down
46 changes: 32 additions & 14 deletions Phoenix/Channel.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using ParamsType = System.Collections.Generic.Dictionary<string, object>;
using SubscriptionTable = System.Collections.Generic.Dictionary<
string, System.Collections.Generic.List<Phoenix.ChannelSubscription>>;

Expand Down Expand Up @@ -47,7 +46,7 @@ public class Channel
public ChannelState State = ChannelState.Closed;

// TODO: possibly support lazy instantiation of payload (same as Phoenix js)
public Channel(string topic, ParamsType @params, Socket socket)
public Channel(string topic, Dictionary<string, object> @params, Socket socket)
{
Topic = topic;
Socket = socket;
Expand All @@ -56,7 +55,7 @@ public Channel(string topic, ParamsType @params, Socket socket)
_joinPush = new Push(
this,
Message.OutBoundEvent.Join.Serialized(),
() => @params,
() => socket.Opts.MessageSerializer.Box(@params),
_timeout
);

Expand Down Expand Up @@ -212,11 +211,10 @@ public ChannelSubscription On(string anyEvent, Action<Message> callback)

public ChannelSubscription On<T>(string anyEvent, Action<T> callback)
{
return On(anyEvent, message =>
{
var serializer = Socket.Opts.MessageSerializer;
callback(serializer.MapPayload<T>(message.Payload));
});
return On(
anyEvent,
message => callback(message.Payload.Unbox<T>())
);
}

public bool Off(ChannelSubscription subscription)
Expand All @@ -225,12 +223,25 @@ public bool Off(ChannelSubscription subscription)
subscriptions.Remove(subscription);
}

public bool Off(Message.InBoundEvent @event) => Off(@event.Serialized());
public bool Off(Message.OutBoundEvent @event) => Off(@event.Serialized());
public bool Off(Message.InBoundEvent @event)
{
return Off(@event.Serialized());
}

public bool Off(string anyEvent) => _bindings.Remove(anyEvent);
public bool Off(Message.OutBoundEvent @event)
{
return Off(@event.Serialized());
}

internal bool CanPush() => Socket.IsConnected() && IsJoined();
public bool Off(string anyEvent)
{
return _bindings.Remove(anyEvent);
}

internal bool CanPush()
{
return Socket.IsConnected() && IsJoined();
}

public Push Push(string @event, object payload = null, TimeSpan? timeout = null)
{
Expand All @@ -242,7 +253,14 @@ public Push Push(string @event, object payload = null, TimeSpan? timeout = null)
);
}

var pushEvent = new Push(this, @event, () => payload, timeout ?? _timeout);
var serializer = Socket.Opts.MessageSerializer;
var pushEvent = new Push(
this,
@event,
() => serializer.Box(payload),
timeout ?? _timeout
);

if (CanPush())
{
pushEvent.Send();
Expand Down Expand Up @@ -289,7 +307,7 @@ void TriggerClose()
}

// overrideable message hook
public virtual object OnMessage(Message message)
public virtual IJsonBox OnMessage(Message message)
{
return message.Payload;
}
Expand Down
6 changes: 4 additions & 2 deletions Phoenix/DelayedExecutor.cs
Expand Up @@ -60,7 +60,10 @@ public sealed class TaskExecution : IDelayedExecution
{
internal bool Cancelled;

public void Cancel() => Cancelled = true;
public void Cancel()
{
Cancelled = true;
}
}


Expand All @@ -69,7 +72,6 @@ public sealed class TaskDelayedExecutor : IDelayedExecutor
public IDelayedExecution Execute(Action action, TimeSpan delay)
{
var execution = new TaskExecution();
// NOTE: using GetAwaiter() will allow callbacks to be called on the same thread.
Task.Delay(delay).GetAwaiter().OnCompleted(() =>
{
if (!execution.Cancelled)
Expand Down
162 changes: 127 additions & 35 deletions Phoenix/JSONMessageSerializer.cs
@@ -1,69 +1,161 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;

namespace Phoenix
{
/**
* An adapter for abstracting over the JSON library used.
*/
public sealed class JsonBox : IJsonBox
{
public readonly JToken Element;

public JsonBox(JToken element)
{
Element = element;
}

public static JsonBox Serialize(object obj)
{
var token = obj == null
? JValue.CreateNull()
: JToken.FromObject(obj, JsonMessageSerializer.Serializer);

return new JsonBox(token);
}

public T Unbox<T>()
{
return Element.ToObject<T>(JsonMessageSerializer.Serializer);
}
}

public sealed class JsonMessageSerializer : IMessageSerializer
{
public string Serialize(Message message)
internal static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
Converters =
{
new StringEnumConverter(),
new JsonBoxConverter(),
new MessageConverter(),
new PresencePayloadConverter(),
new PresenceMetaConverter()
},
Formatting = Formatting.None
};

internal static readonly JsonSerializer Serializer = JsonSerializer.Create(Settings);

public string Serialize(object element)
{
return JsonConvert.SerializeObject(element, Formatting.None, Settings);
}

public T Deserialize<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json, Settings);
}

public IJsonBox Box(object element)
{
return JsonBox.Serialize(element);
}
}

internal sealed class JsonBoxConverter : JsonConverter<IJsonBox>
{
public override void WriteJson(JsonWriter writer, IJsonBox value, JsonSerializer serializer)
{
var element = value?.Unbox<JToken>();
if (serializer.NullValueHandling != NullValueHandling.Ignore)
{
element ??= JValue.CreateNull();
}

element?.WriteTo(writer);
}

public override IJsonBox ReadJson(JsonReader reader, Type objectType, IJsonBox existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
return new JsonBox(JToken.Load(reader));
}
}

internal sealed class MessageConverter : JsonConverter<Message>
{
public override void WriteJson(JsonWriter writer, Message value, JsonSerializer serializer)
{
return new JArray(
message.JoinRef,
message.Ref,
message.Topic,
message.Event,
// phoenix.js: consistent with phoenix, also backwards compatible
// e.g. if the backend has handle_in(event, {}, socket)
message.Payload == null
? new JObject()
: JObject.FromObject(message.Payload)
)
.ToString(
Formatting.None,
new StringEnumConverter()
);
// phoenix.js: consistent with phoenix, also backwards compatible
// e.g. if the backend has handle_in(event, {}, socket)
var payload = value.Payload?.Unbox<JToken>();
if (payload == null || payload.Type == JTokenType.Null || payload.Type == JTokenType.Undefined)
{
payload = new JObject();
}

writer.WriteStartArray();
writer.WriteValue(value.JoinRef);
writer.WriteValue(value.Ref);
writer.WriteValue(value.Topic);
writer.WriteValue(value.Event);
payload.WriteTo(writer);
writer.WriteEndArray();
}

public Message Deserialize(string message)
public override Message ReadJson(JsonReader reader, Type objectType, Message existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
var array = JArray.Parse(message);
var array = JArray.Load(reader);
return new Message(
joinRef: array[0].ToObject<string>(),
@ref: array[1].ToObject<string>(),
topic: array[2].ToObject<string>(),
@event: array[3].ToObject<string>(),
payload: array[4]
payload: new JsonBox(array[4])
);
}
}

public Reply? MapReply(object payload)
internal sealed class PresencePayloadConverter : JsonConverter<PresencePayload>
{
public override void WriteJson(JsonWriter writer, PresencePayload value, JsonSerializer serializer)
{
var jObject = JObject.FromObject(payload);
return new Reply(
jObject.Value<string>("status"),
jObject["response"]
);
value.Payload.Unbox<JToken>().WriteTo(writer);
}

public T MapPayload<T>(object payload)
public override PresencePayload ReadJson(JsonReader reader, Type objectType, PresencePayload existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
return payload == null
? default
: JToken.FromObject(payload).ToObject<T>();
var obj = JObject.Load(reader);
return new PresencePayload
{
Metas = obj["metas"]?.ToObject<List<PresenceMeta>>(serializer),
Payload = new JsonBox(obj)
};
}
}

public static class JsonPayloadExtensions
internal sealed class PresenceMetaConverter : JsonConverter<PresenceMeta>
{
public static T JsonResponse<T>(this Reply reply)
public override void WriteJson(JsonWriter writer, PresenceMeta value, JsonSerializer serializer)
{
return ((JToken) reply.Response).ToObject<T>();
value.Payload.Unbox<JToken>().WriteTo(writer);
}

public static T JsonPayload<T>(this Message message)
public override PresenceMeta ReadJson(JsonReader reader, Type objectType, PresenceMeta existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
return ((JToken) message.Payload).ToObject<T>();
var meta = JObject.Load(reader);
return new PresenceMeta
{
PhxRef = meta.Value<string>("phx_ref"),
Payload = new JsonBox(meta)
};
}
}
}

0 comments on commit 18eb202

Please sign in to comment.