diff --git a/Ooui/Client.js b/Ooui/Client.js index 9a1ed3a..3c9b433 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -2,7 +2,7 @@ // Create WebSocket connection. const socket = new WebSocket ("ws://localhost:8080" + rootElementPath, "ooui-1.0"); -console.log("WebSocket created"); +console.log("Web socket created"); const nodes = {} @@ -35,7 +35,7 @@ function msgSet (m) { return; } node[m.k] = m.v; - console.log ("Set property", node, m.k, m.v); + console.log ("Set", node, m.k, m.v); } function msgCall (m) { @@ -56,6 +56,7 @@ function msgListen (m) { console.error ("Unknown node id", m); return; } + console.log ("Listen", node, m.k); node.addEventListener(m.k, function () { const em = { m: "event", @@ -64,7 +65,7 @@ function msgListen (m) { }; const ems = JSON.stringify (em); socket.send (ems); - console.log ("EVENT", em); + console.log ("Send event", em); }); } @@ -97,23 +98,22 @@ function fixupValue (v) { return v; } else if (typeof v === 'string' || v instanceof String) { - if ((v.length >= 2) && (v[0] === "\u2999") && (v[1] === "n")) { + if ((v.length > 1) && (v[0] === "\u2999")) { // console.log("V", v); - const id = v.substr(1); - // console.log("ID", id); - return getNode (id); + return getNode (v); } } return v; } socket.addEventListener('open', function (event) { - console.log("WebSocket opened"); + console.log("Web socket opened"); }); socket.addEventListener('message', function (event) { const message = JSON.parse (event.data); + // console.log('Raw value from server', message.v); message.v = fixupValue (message.v); - console.log('Message from server', message); + // console.log('Message from server', message); processMessage (message); }); diff --git a/Ooui/Element.cs b/Ooui/Element.cs index cdbb38a..6fe66f3 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -4,11 +4,6 @@ namespace Ooui { public abstract class Element : Node { - protected Element (string tagName) - : base (tagName) - { - } - string className = ""; public string ClassName { get => className; @@ -26,5 +21,10 @@ namespace Ooui get => hidden; set => SetProperty (ref hidden, value, "hidden"); } + + protected Element (string tagName) + : base (tagName) + { + } } } diff --git a/Ooui/EventTarget.cs b/Ooui/EventTarget.cs index afadfe7..44b2b33 100644 --- a/Ooui/EventTarget.cs +++ b/Ooui/EventTarget.cs @@ -4,6 +4,7 @@ using System.Linq; namespace Ooui { + [Newtonsoft.Json.JsonConverter (typeof (EventTargetJsonConverter))] public abstract class EventTarget { readonly List stateMessages = new List (); @@ -30,6 +31,12 @@ namespace Ooui }); } + public virtual EventTarget GetElementById (string id) + { + if (id == Id) return this; + return null; + } + public void AddEventListener (string eventType, EventHandler handler) { if (eventType == null) return; @@ -74,11 +81,13 @@ namespace Ooui return false; } + public const char IdPrefix = '\u2999'; + static long idCounter = 0; static string GenerateId () { var id = System.Threading.Interlocked.Increment (ref idCounter); - return "n" + id; + return $"{IdPrefix}{id}"; } public virtual void Send (Message message) @@ -161,4 +170,24 @@ namespace Ooui } } } + + class EventTargetJsonConverter : Newtonsoft.Json.JsonConverter + { + public override bool CanRead => false; + + public override void WriteJson (Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + writer.WriteValue (((EventTarget)value).Id); + } + + public override object ReadJson (Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + throw new NotImplementedException (); + } + + public override bool CanConvert (Type objectType) + { + return typeof (EventTarget).IsAssignableFrom (objectType); + } + } } diff --git a/Ooui/Message.cs b/Ooui/Message.cs index f946a04..6fe19b9 100644 --- a/Ooui/Message.cs +++ b/Ooui/Message.cs @@ -19,12 +19,8 @@ namespace Ooui [JsonProperty("k")] public string Key = ""; - object v = null; [JsonProperty("v")] - public object Value { - get => v; - set => v = FixupValue (value); - } + public object Value = null; public static Message Event (string targetId, string eventType) => new Message { MessageType = MessageType.Event, @@ -32,21 +28,6 @@ namespace Ooui Key = eventType, }; - static object FixupValue (object v) - { - if (v is Array a) { - var na = new object[a.Length]; - for (var i = 0; i < a.Length; i++) { - na[i] = FixupValue (a.GetValue (i)); - } - return na; - } - else if (v is EventTarget t) { - return "\u2999" + t.Id; - } - return v; - } - static long idCounter = 0; static long GenerateId () { diff --git a/Ooui/Node.cs b/Ooui/Node.cs index 23b2bb4..3f1f703 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -8,10 +8,7 @@ namespace Ooui { readonly List children = new List (); - public IEnumerable AllStateMessages => - StateMessages - .Concat (from c in children from m in c.AllStateMessages select m) - .OrderBy (x => x.Id); + public IEnumerable Children => children; public virtual string Text { get { return String.Join ("", from c in children select c.Text); } @@ -25,6 +22,19 @@ namespace Ooui { } + public override EventTarget GetElementById (string id) + { + if (id == Id) return this; + foreach (var c in Children) { + if (c is Element e) { + var r = e.GetElementById (id); + if (r != null) + return r; + } + } + return null; + } + public Node AppendChild (Node newChild) { return InsertBefore (newChild, null); @@ -66,5 +76,19 @@ namespace Ooui RemoveChild (c); InsertBefore (newNode, null); } + + protected override void SaveStateMessageIfNeeded (Message message) + { + switch (message.MessageType) { + case MessageType.Call when + message.Key == "insertBefore" || + message.Key == "removeChild": + SaveStateMessage (message); + break; + default: + base.SaveStateMessageIfNeeded (message); + break; + } + } } } diff --git a/Ooui/UI.cs b/Ooui/UI.cs index ba0ee1e..604a706 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -218,6 +218,14 @@ namespace Ooui return; } + // + // Keep a list of all the elements for which we've transmitted the initial state + // + var createdIds = new HashSet { + "window", + "document", + "document.body", + }; // // Preparse handlers for the element @@ -225,7 +233,7 @@ namespace Ooui Action onElementMessage = async m => { if (webSocket == null) return; try { - await SendMessageAsync (webSocket, m, token); + await SendMessageAsync (webSocket, m, element, createdIds, token); } catch (Exception ex) { Error ("Failed to handled element message", ex); @@ -237,20 +245,19 @@ namespace Ooui // try { // - // Send message history, start sending updates, and add it to the body + // Start watching for changes in the element // - foreach (var m in element.AllStateMessages) { - if (webSocket.State == WebSocketState.Open) { - await SendMessageAsync (webSocket, m, token); - } - } element.MessageSent += onElementMessage; + + // + // Add it to the document body + // await SendMessageAsync (webSocket, new Message { TargetId = "document.body", MessageType = MessageType.Call, Key = "appendChild", Value = new[] { element }, - }, token); + }, element, createdIds, token); // // Listen for events @@ -279,16 +286,13 @@ namespace Ooui var receivedString = Encoding.UTF8.GetString (receiveBuffer, 0, size); try { - Console.WriteLine ("RECEIVED: {0}", receivedString); + // Console.WriteLine ("RECEIVED: {0}", receivedString); var message = Newtonsoft.Json.JsonConvert.DeserializeObject (receivedString); element.Receive (message); } catch (Exception ex) { Error ("Failed to process received message", ex); } - - // var outputBuffer = new ArraySegment (Encoding.UTF8.GetBytes ($"You said: {receivedString}")); - // await webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false); } } } @@ -296,7 +300,7 @@ namespace Ooui // The remote party closed the WebSocket connection without completing the close handshake. } catch (Exception ex) { - Error ("Failed to process web socket", ex); + Error ("Web socket failed", ex); } finally { element.MessageSent -= onElementMessage; @@ -304,11 +308,48 @@ namespace Ooui } } - static Task SendMessageAsync (WebSocket webSocket, Message message, CancellationToken token) + static async Task SendMessageAsync (WebSocket webSocket, Message message, EventTarget target, HashSet createdIds, CancellationToken token) { + // + // Make sure all the referenced objects have been created + // + if (message.MessageType == MessageType.Create) { + createdIds.Add (message.TargetId); + } + else { + if (!createdIds.Contains (message.TargetId)) { + createdIds.Add (message.TargetId); + await SendStateMessagesAsync (webSocket, target.GetElementById (message.TargetId), createdIds, token); + } + if (message.Value is Array a) { + for (var i = 0; i < a.Length; i++) { + // Console.WriteLine ($"A{i} = {a.GetValue(i)}"); + if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) { + createdIds.Add (e.Id); + await SendStateMessagesAsync (webSocket, e, createdIds, token); + } + } + } + } + + // + // Now actually send this message + // + if (token.IsCancellationRequested) + return; var json = Newtonsoft.Json.JsonConvert.SerializeObject (message); var outputBuffer = new ArraySegment (Encoding.UTF8.GetBytes (json)); - return webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token); + await webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token); + } + + static async Task SendStateMessagesAsync (WebSocket webSocket, EventTarget target, HashSet createdIds, CancellationToken token) + { + if (target == null) return; + + foreach (var m in target.StateMessages) { + if (token.IsCancellationRequested) return; + await SendMessageAsync (webSocket, m, target, createdIds, token); + } } static void Error (string message, Exception ex)