From a993e8afaa1d63783bc05edd7b56fa6c1d078e4e Mon Sep 17 00:00:00 2001 From: Kevin Destrem Date: Fri, 3 Jun 2016 12:26:44 +0900 Subject: [PATCH 1/7] Cleanup of the previous structure --- Async/Async.cs | 80 --- EventEmitter/EventEmitter.cs | 64 --- HTTPRequest/HTTPRequest.DotNet.cs | 124 ----- HTTPRequest/HTTPRequest.Unity.cs | 126 ----- HTTPRequest/HTTPRequestException.cs | 9 - HTTPRequest/HTTPRequestManager.cs | 27 - JSONRPC/JSONRPC.cs | 119 ----- JSONRPC/JSONRPCBatch.cs | 28 -- Mage/Archivist/Archivist.cs | 473 ------------------ Mage/Archivist/VaultValue.cs | 93 ---- Mage/CommandCenter/CommandBatch.cs | 18 - Mage/CommandCenter/CommandBatchItem.cs | 16 - Mage/CommandCenter/CommandCenter.cs | 146 ------ .../TransportClient/CommandHTTPClient.cs | 127 ----- .../TransportClient/CommandJSONRPCClient.cs | 77 --- .../TransportClient/CommandTransportClient.cs | 14 - Mage/EventManager.cs | 36 -- Mage/Logger/LogEntry.cs | 59 --- Mage/Logger/Logger.cs | 95 ---- Mage/Logger/Writers/ConsoleWriter.cs | 157 ------ Mage/Logger/Writers/LogWriter.cs | 3 - Mage/Logger/Writers/ServerWriter.cs | 13 - Mage/Mage.cs | 213 -------- Mage/MessageStream/MessageStream.cs | 223 --------- .../TransportClient/LongPolling.cs | 133 ----- .../TransportClient/ShortPolling.cs | 102 ---- .../TransportClient/TransportClient.cs | 11 - Mage/Module.cs | 109 ---- Mage/Session.cs | 32 -- Singleton/MonoSingleton.cs | 38 -- Singleton/SceneInstance.cs | 17 - Singleton/Singleton.cs | 20 - UnityEditorPlayMode/UnityEditorPlayMode.cs | 64 --- 33 files changed, 2866 deletions(-) delete mode 100644 Async/Async.cs delete mode 100644 EventEmitter/EventEmitter.cs delete mode 100644 HTTPRequest/HTTPRequest.DotNet.cs delete mode 100644 HTTPRequest/HTTPRequest.Unity.cs delete mode 100644 HTTPRequest/HTTPRequestException.cs delete mode 100644 HTTPRequest/HTTPRequestManager.cs delete mode 100644 JSONRPC/JSONRPC.cs delete mode 100644 JSONRPC/JSONRPCBatch.cs delete mode 100644 Mage/Archivist/Archivist.cs delete mode 100644 Mage/Archivist/VaultValue.cs delete mode 100644 Mage/CommandCenter/CommandBatch.cs delete mode 100644 Mage/CommandCenter/CommandBatchItem.cs delete mode 100644 Mage/CommandCenter/CommandCenter.cs delete mode 100644 Mage/CommandCenter/TransportClient/CommandHTTPClient.cs delete mode 100644 Mage/CommandCenter/TransportClient/CommandJSONRPCClient.cs delete mode 100644 Mage/CommandCenter/TransportClient/CommandTransportClient.cs delete mode 100644 Mage/EventManager.cs delete mode 100644 Mage/Logger/LogEntry.cs delete mode 100644 Mage/Logger/Logger.cs delete mode 100644 Mage/Logger/Writers/ConsoleWriter.cs delete mode 100644 Mage/Logger/Writers/LogWriter.cs delete mode 100644 Mage/Logger/Writers/ServerWriter.cs delete mode 100644 Mage/Mage.cs delete mode 100644 Mage/MessageStream/MessageStream.cs delete mode 100644 Mage/MessageStream/TransportClient/LongPolling.cs delete mode 100644 Mage/MessageStream/TransportClient/ShortPolling.cs delete mode 100644 Mage/MessageStream/TransportClient/TransportClient.cs delete mode 100644 Mage/Module.cs delete mode 100644 Mage/Session.cs delete mode 100644 Singleton/MonoSingleton.cs delete mode 100644 Singleton/SceneInstance.cs delete mode 100644 Singleton/Singleton.cs delete mode 100644 UnityEditorPlayMode/UnityEditorPlayMode.cs diff --git a/Async/Async.cs b/Async/Async.cs deleted file mode 100644 index df2676d..0000000 --- a/Async/Async.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; - - -/// -/// Async is a utility class which provides straight-forward, powerful functions for working with asynchronous C#. -/// -/// Async provides around 20 functions that include the usual 'functional' suspects (map, reduce, filter, each…) as well as -/// some common patterns for asynchronous control flow (parallel, series, waterfall…). All these functions assume you follow -/// the convention of providing a single callback as the last argument of your async function. -/// -public class Async { - public static void each (List items, Action> fn, Action cb) { - if (items == null || items.Count == 0) { - cb(null); - return; - } - - int currentItemI = 0; - Action iterate = null; - iterate = () => { - if (currentItemI >= items.Count) { - cb(null); - return; - } - - // Execute the given function on this item - fn(items[currentItemI], (Exception error) => { - // Stop iteration if there was an error - if (error != null) { - cb(error); - return; - } - - // Continue to next item - currentItemI++; - iterate(); - }); - }; - - // Begin the iteration - iterate (); - } - - public static void series (List>> actionItems, Action cb) { - bool isEmpty = actionItems == null || actionItems.Count == 0; - if (isEmpty) { - cb(null); - return; - } - - int currentItemI = 0; - Action iterate = null; - iterate = () => { - if (currentItemI >= actionItems.Count) { - cb(null); - return; - } - - // Shift an element from the list - Action> actionItem = actionItems[currentItemI]; - - // Execute the given function on this item - actionItem((Exception error) => { - // Stop iteration if there was an error - if (error != null) { - cb(error); - return; - } - - // Continue to next item - currentItemI++; - iterate(); - }); - }; - - // Begin the iteration - iterate (); - } -} diff --git a/EventEmitter/EventEmitter.cs b/EventEmitter/EventEmitter.cs deleted file mode 100644 index 45b36fb..0000000 --- a/EventEmitter/EventEmitter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; - -public class EventEmitter { - // - private Dictionary eventTags = new Dictionary(); - private EventHandlerList eventsList = new EventHandlerList(); - - // - public void on(string eventTag, Action handler) - { - if (!eventTags.ContainsKey(eventTag)) { - eventTags.Add(eventTag, new object()); - } - - eventsList.AddHandler(eventTags[eventTag], handler); - } - - // - public void once(string eventTag, Action handler) - { - Action handlerWrapper = null; - handlerWrapper = (object obj, T arguments) => { - eventsList.RemoveHandler(eventTags[eventTag], handlerWrapper); - handler(obj, arguments); - }; - - on(eventTag, handlerWrapper); - } - - // - public void emit(string eventTag, object sender, T arguments) - { - if (!eventTags.ContainsKey(eventTag)) { - return; - } - - Action execEventList = (Action)eventsList[eventTags[eventTag]]; - execEventList(sender, arguments); - } - - public void emit(string eventTag, T arguments) - { - emit(eventTag, null, arguments); - } - - // - public void off(string eventTag, Action handler) - { - eventsList.RemoveHandler(eventTags[eventTag], handler); - } - - // - public void removeAllListeners() - { - // Destroy all event handlers - eventsList.Dispose(); - eventsList = new EventHandlerList(); - - // Destroy all event tags - eventTags = new Dictionary(); - } -} \ No newline at end of file diff --git a/HTTPRequest/HTTPRequest.DotNet.cs b/HTTPRequest/HTTPRequest.DotNet.cs deleted file mode 100644 index 089af23..0000000 --- a/HTTPRequest/HTTPRequest.DotNet.cs +++ /dev/null @@ -1,124 +0,0 @@ -// THIS SHOULD BE REFACTORED AND USED FOR PURE C# .NET CLIENTS -#if FALSE - -using System; -using System.Collections.Generic; -using System.Net; -using System.IO; -using System.Text; -using System.Threading; - - -public class HTTPRequest { - public static HttpWebRequest Get(string url, Dictionary headers, CookieContainer cookies, Action cb) { - // Initialize request instance - HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url); - httpRequest.Method = WebRequestMethods.Http.Get; - - if (cookies != null) { - httpRequest.CookieContainer = cookies; - } - - // Set request headers - if (headers != null) { - foreach (KeyValuePair entry in headers) { - httpRequest.Headers.Add(entry.Key, entry.Value); - } - } - - // Process the response - ReadResponseData(httpRequest, cb); - - return httpRequest; - } - - public static HttpWebRequest Post(string url, string contentType, string postData, Dictionary headers, CookieContainer cookies, Action cb) { - byte[] binaryPostData = Encoding.UTF8.GetBytes(postData); - return Post(url, contentType, binaryPostData, headers, cookies, cb); - } - - public static HttpWebRequest Post(string url, string contentType, byte[] postData, Dictionary headers, CookieContainer cookies, Action cb) { - // Initialize request instance - HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url); - httpRequest.Method = WebRequestMethods.Http.Post; - - // Set content type if provided - if (contentType != null) { - httpRequest.ContentType = contentType; - } - - if (cookies != null) { - httpRequest.CookieContainer = cookies; - } - - // Set request headers - if (headers != null) { - foreach (KeyValuePair entry in headers) { - httpRequest.Headers.Add(entry.Key, entry.Value); - } - } - - // Make a connection and send the request - WritePostData(httpRequest, postData, (Exception requestError) => { - if (requestError != null) { - cb(requestError, null); - return; - } - - // Process the response - ReadResponseData(httpRequest, cb); - }); - - return httpRequest; - } - - private static void WritePostData (HttpWebRequest httpRequest, byte[] postData, Action cb) { - httpRequest.BeginGetRequestStream ((IAsyncResult callbackResult) => { - try { - Stream postStream = httpRequest.EndGetRequestStream(callbackResult); - postStream.Write(postData, 0, postData.Length); - postStream.Close(); - } catch (Exception error) { - cb(error); - return; - } - cb(null); - }, null); - } - - private static void ReadResponseData(HttpWebRequest httpRequest, Action cb) { - /** - * NOTE: we need to implement the timeout ourselves. HttpWebRequest does not implement - * timeout on asynchronous methods. Check below link for more information: - * https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.timeout(v=vs.110).aspx - **/ - Timer timeoutTimer = new Timer((object state) => { - httpRequest.Abort(); - }, null, httpRequest.Timeout, Timeout.Infinite); - - // Begin waiting for a response - httpRequest.BeginGetResponse(new AsyncCallback((IAsyncResult callbackResult) => { - // Cleanup timeout - timeoutTimer.Dispose(); - timeoutTimer = null; - - // Process response - string responseString = null; - try { - HttpWebResponse response = (HttpWebResponse)httpRequest.EndGetResponse(callbackResult); - - using (StreamReader httpWebStreamReader = new StreamReader(response.GetResponseStream())) { - responseString = httpWebStreamReader.ReadToEnd(); - } - - response.Close(); - } catch (Exception error) { - cb(error, null); - return; - } - - cb(null, responseString); - }), null); - } -} -#endif \ No newline at end of file diff --git a/HTTPRequest/HTTPRequest.Unity.cs b/HTTPRequest/HTTPRequest.Unity.cs deleted file mode 100644 index dd881fa..0000000 --- a/HTTPRequest/HTTPRequest.Unity.cs +++ /dev/null @@ -1,126 +0,0 @@ -// IF WE ARE USING UNITY, USE THIS VERSION OF THE CLASS -#if UNITY_5 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Text; - -using UnityEngine; - - -public class HTTPRequest { - private WWW request; - private Action cb; - private Stopwatch timeoutTimer = new Stopwatch(); - - // - public double timeout = 100 * 1000; - - - // - public HTTPRequest(string url, byte[] postData, Dictionary headers, Action cb) { - // Start timeout timer - timeoutTimer.Start(); - - // Queue constructor for main thread execution - HTTPRequestManager.Queue(Constructor(url, postData, headers, cb)); - } - - - // - private IEnumerator Constructor(string url, byte[] postData, Dictionary headers, Action cb) { - this.cb = cb; - this.request = new WWW(url, postData, headers); - - HTTPRequestManager.Queue(WaitLoop()); - yield break; - } - - - // - private IEnumerator WaitLoop() { - while (!request.isDone) { - if (timeoutTimer.ElapsedMilliseconds >= timeout) { - // Timed out abort the request with timeout error - cb(new Exception("Request timed out"), null); - this.Abort(); - yield break; - } else if (request == null) { - // Check if we destroyed the request due to an abort - yield break; - } - - // Otherwise continue to wait - yield return null; - } - - // Stop the timeout timer - timeoutTimer.Stop(); - - // Check if there is a callback - if (cb == null) { - yield break; - } - - // Check if there was an error with the request - if (request.error != null) { - int statusCode = 0; - if (request.responseHeaders.ContainsKey("STATUS")) { - statusCode = int.Parse(request.responseHeaders["STATUS"].Split(' ')[1]); - } - - cb(new HTTPRequestException(request.error, statusCode), null); - yield break; - } - - // Otherwise return the response - cb(null, request.text); - } - - - // Abort request - public void Abort() { - WWW _request = request; - request = null; - - _request.Dispose(); - timeoutTimer.Stop(); - } - - - - // Create GET request and return it - public static HTTPRequest Get(string url, Dictionary headers, CookieContainer cookies, Action cb) { - // TODO: COOKIE SUPPORT - - // Create request and return it - // The callback will be called when the request is complete - return new HTTPRequest(url, null, headers, cb); - } - - // Create POST request and return it - public static HTTPRequest Post(string url, string contentType, string postData, Dictionary headers, CookieContainer cookies, Action cb) { - byte[] binaryPostData = Encoding.UTF8.GetBytes(postData); - return Post(url, contentType, binaryPostData, headers, cookies, cb); - } - - // Create POST request and return it - public static HTTPRequest Post(string url, string contentType, byte[] postData, Dictionary headers, CookieContainer cookies, Action cb) { - Dictionary headersCopy = new Dictionary(headers); - - // TODO: COOKIE SUPPORT - - // Set content type if provided - if (contentType != null) { - headersCopy.Add("ContentType", contentType); - } - - // Create request and return it - // The callback will be called when the request is complete - return new HTTPRequest(url, postData, headersCopy, cb); - } -} -#endif \ No newline at end of file diff --git a/HTTPRequest/HTTPRequestException.cs b/HTTPRequest/HTTPRequestException.cs deleted file mode 100644 index 6864e74..0000000 --- a/HTTPRequest/HTTPRequestException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -public class HTTPRequestException : Exception { - public int Status = 0; - - public HTTPRequestException(string Message, int Status) : base(Message) { - this.Status = Status; - } -} \ No newline at end of file diff --git a/HTTPRequest/HTTPRequestManager.cs b/HTTPRequest/HTTPRequestManager.cs deleted file mode 100644 index eee9c65..0000000 --- a/HTTPRequest/HTTPRequestManager.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -using UnityEngine; - -public class HTTPRequestManager : MonoSingleton { - // - private static List queued = new List(); - - // - public static void Queue(IEnumerator coroutine) { - lock ((object)queued) { - queued.Add(coroutine); - } - } - - // - void Update () { - lock ((object)queued) { - for (int i = 0; i < queued.Count; i += 1) { - StartCoroutine(queued[i]); - } - - queued.Clear(); - } - } -} \ No newline at end of file diff --git a/JSONRPC/JSONRPC.cs b/JSONRPC/JSONRPC.cs deleted file mode 100644 index 651ed70..0000000 --- a/JSONRPC/JSONRPC.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.IO; -using System.Text; - -using Newtonsoft.Json.Linq; - -public class JSONRPC { - // Endpoint and Basic Authentication - private string _endpoint; - private string _username = null; - private string _password = null; - - public void SetEndpoint(string endpoint, string username = null, string password = null) { - _endpoint = endpoint; - _username = username; - _password = password; - } - - public void Call(string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) { - Call(JValue.CreateNull(), methodName, parameters, headers, cookies, cb); - } - - public void Call(string id, string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) { - Call(new JValue(id), methodName, parameters, headers, cookies, cb); - } - - public void Call(int id, string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) { - Call(new JValue(id), methodName, parameters, headers, cookies, cb); - } - - public void Call(JValue id, string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) { - // Setup JSON request object - JObject requestObject = new JObject (); - requestObject.Add("jsonrpc", new JValue("2.0")); - - requestObject.Add("id", id); - requestObject.Add("method", new JValue(methodName)); - requestObject.Add("params", parameters); - - // Serialize JSON request object into string - string postData; - try { - postData = requestObject.ToString (); - } catch (Exception serializeError) { - cb(serializeError, null); - return; - } - - // Send request - SendRequest(postData, headers, cookies, (Exception requestError, string responseString) => { - if (requestError != null) { - cb(requestError, null); - return; - } - - // Deserialize the JSON response - JObject responseObject; - try { - responseObject = JObject.Parse(responseString); - } catch (Exception parseError) { - cb(parseError, null); - return; - } - - cb(null, responseObject); - }); - } - - public void CallBatch(JSONRPCBatch rpcBatch, Dictionary headers, CookieContainer cookies, Action cb) { - // Serialize JSON request object into string - string postData; - try { - postData = rpcBatch.batch.ToString(); - } catch (Exception serializeError) { - cb(serializeError, null); - return; - } - - // Send request - SendRequest(postData, headers, cookies, (Exception requestError, string responseString) => { - if (requestError != null) { - cb(requestError, null); - return; - } - - // Deserialize the JSON response - JArray responseArray; - try { - responseArray = JArray.Parse(responseString); - } catch (Exception parseError) { - cb(parseError, null); - return; - } - - cb(null, responseArray); - }); - } - - private void SendRequest(string postData, Dictionary headers, CookieContainer cookies, Action cb) { - // Make sure the endpoint is set - if (string.IsNullOrEmpty(_endpoint)) { - cb(new Exception("Endpoint has not been set"), null); - return; - } - - // Make a copy of the provided headers and add additional required headers - Dictionary _headers = new Dictionary(headers); - if (_username != null && _password != null) { - string authInfo = _username + ":" + _password; - string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); - _headers.Add("Authorization", "Basic " + encodedAuth); - } - - // Send HTTP post to JSON rpc endpoint - HTTPRequest.Post(_endpoint, "application/json", postData, _headers, cookies, cb); - } -} diff --git a/JSONRPC/JSONRPCBatch.cs b/JSONRPC/JSONRPCBatch.cs deleted file mode 100644 index 25d8007..0000000 --- a/JSONRPC/JSONRPCBatch.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Newtonsoft.Json.Linq; - -public class JSONRPCBatch { - public JArray batch = new JArray(); - - public void Add(string methodName, JObject parameters) { - Add(JValue.CreateNull(), methodName, parameters); - } - - public void Add(string id, string methodName, JObject parameters) { - Add(new JValue(id), methodName, parameters); - } - - public void Add(int id, string methodName, JObject parameters) { - Add(new JValue(id), methodName, parameters); - } - - public void Add(JValue id, string methodName, JObject parameters) { - JObject requestObject = new JObject(); - requestObject.Add("jsonrpc", new JValue("2.0")); - - requestObject.Add("id", id); - requestObject.Add("method", new JValue(methodName)); - requestObject.Add("params", parameters); - - batch.Add(requestObject); - } -} \ No newline at end of file diff --git a/Mage/Archivist/Archivist.cs b/Mage/Archivist/Archivist.cs deleted file mode 100644 index 526c067..0000000 --- a/Mage/Archivist/Archivist.cs +++ /dev/null @@ -1,473 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -using Newtonsoft.Json.Linq; - -public class Archivist : EventEmitter { - private Mage mage { get { return Mage.Instance; } } - private Logger logger { get { return mage.logger("archivist"); } } - - // Local cache of all retrieved vault values - private Dictionary _cache = new Dictionary(); - - - // Constructor - public Archivist () { - // Set data to vault value when set event received - mage.eventManager.on("archivist:set", (object sender, JToken info) => { - string topic = (string)info["key"]["topic"]; - JObject index = (JObject)info["key"]["index"]; - JToken data = info["value"]["data"]; - string mediaType = (string)info["value"]["mediaType"]; - int? expirationTime = (int?)info["expirationTime"]; - ValueSet(topic, index, data, mediaType, expirationTime); - }); - - // Del data inside vault value when del event received - mage.eventManager.on("archivist:del", (object sender, JToken info) => { - string topic = (string)info["key"]["topic"]; - JObject index = (JObject)info["key"]["index"]; - ValueDel(topic, index); - }); - - // Touch vault value expiry when touch event received - mage.eventManager.on("archivist:touch", (object sender, JToken info) => { - string topic = (string)info["key"]["topic"]; - JObject index = (JObject)info["key"]["index"]; - int? expirationTime = (int?)info["expirationTime"]; - ValueTouch(topic, index, expirationTime); - }); - - // Apply changes to vault value when applyDiff event is received - mage.eventManager.on("archivist:applyDiff", (object sender, JToken info) => { - string topic = (string)info["key"]["topic"]; - JObject index = (JObject)info["key"]["index"]; - JArray diff = (JArray)info["diff"]; - int? expirationTime = (int?)info["expirationTime"]; - ValueApplyDiff(topic, index, diff, expirationTime); - }); - } - - - //////////////////////////////////////////// - // Cache Manipulation // - //////////////////////////////////////////// - - // Returns string id of a vault value for given topic and index - private static string CreateCacheKey (string topic, JObject index) { - // Sort the keys so order of index is always the same - List indexKeys = new List (); - foreach (var property in index) { - indexKeys.Add(property.Key); - } - indexKeys.Sort (); - - // Construct cache key list with correct ordering - List cacheKeys = new List (); - cacheKeys.Add (topic); - - foreach (string indexKey in indexKeys) { - cacheKeys.Add (indexKey + "=" + index[indexKey].ToString()); - } - - // Join the cache key list into final key string - return string.Join(":", cacheKeys.ToArray()); - } - - - // Returns cache value if it exists and has not passed max allowed age - private VaultValue GetCacheValue(string cacheKeyName, int? maxAge = null) { - lock ((object)_cache) { - if (!_cache.ContainsKey(cacheKeyName)) { - return null; - } - - VaultValue value = _cache[cacheKeyName]; - double timespan = (DateTime.UtcNow - value.writtenAt).TotalMilliseconds; - if (maxAge != null && timespan > maxAge * 1000) { - return null; - } - - return value; - } - } - - - // Return cache dictionary - public Dictionary GetCache() { - return _cache; - } - - - // Clear out the cache entirely - public void ClearCache() { - lock ((object)_cache) { - _cache.Clear(); - } - } - - - // Remove a vault value from the cache by it's topic and index - public void DeleteCacheItem(string topic, JObject index) { - DeleteCacheItem(CreateCacheKey(topic, index)); - } - - - // Remove a vault value from the cache by it's cache key name - public void DeleteCacheItem(string cacheKeyName) { - lock ((object)_cache) { - logger.debug("Deleting cache item: " + cacheKeyName); - if (!_cache.ContainsKey(cacheKeyName)) { - return; - } - - _cache.Remove(cacheKeyName); - } - } - - - //////////////////////////////////////////// - // Vault Value Manipulation // - //////////////////////////////////////////// - private void ValueSetOrDelete(JObject info) { - string topic = (string)info["key"]["topic"]; - JObject index = (JObject)info["key"]["index"]; - JObject rawValue = (JObject)info["value"]; - - if (rawValue != null) { - ValueSet(topic, index, rawValue["data"], (string)rawValue["mediaType"], (int?)info["expirationTime"]); - } else { - ValueDel(topic, index); - } - } - - private void ValueSet(string topic, JObject index, JToken data, string mediaType, int? expirationTime) { - string cacheKeyName = CreateCacheKey(topic, index); - VaultValue cacheValue = null; - - // NOTE: even though some of these operations lock already, we put them inside this - // lock to ensure there is no time inconsistencies if things happen too fast. - lock ((object)_cache) { - cacheValue = GetCacheValue(cacheKeyName); - if (cacheValue == null) { - // If it doesn't exist, create a new vault value - cacheValue = new VaultValue(topic, index); - _cache.Add(cacheKeyName, cacheValue); - } else { - // If it exists delete existing value in preparation for set - cacheValue.Del(); - } - - // Set data to vault value - cacheValue.SetData(mediaType, data); - cacheValue.Touch(expirationTime); - } - - // Emit set event - this.emit(topic + ":set", cacheValue); - } - - private void ValueAdd(string topic, JObject index, JToken data, string mediaType, int? expirationTime) { - string cacheKeyName = CreateCacheKey(topic, index); - VaultValue cacheValue = null; - - // NOTE: even though some of these operations lock already, we put them inside this - // lock to ensure there is no time inconsistencies if things happen too fast. - lock ((object)_cache) { - // Check if value already exists - cacheValue = GetCacheValue(cacheKeyName); - if (cacheValue != null) { - logger.error("Could not add value (already exists): " + cacheKeyName); - return; - } - - // Create new vault value - cacheValue = new VaultValue(topic, index); - _cache.Add(cacheKeyName, cacheValue); - - // Set data to vault value - cacheValue.SetData(mediaType, data); - cacheValue.Touch(expirationTime); - } - - // Emit add event - this.emit(topic + ":add", cacheValue); - } - - private void ValueDel(string topic, JObject index) { - // Check if value already exists - string cacheKeyName = CreateCacheKey(topic, index); - VaultValue cacheValue = GetCacheValue(cacheKeyName); - if (cacheValue == null) { - logger.warning("Could not delete value (doesn't exist): " + cacheKeyName); - return; - } - - // Do delete - cacheValue.Del(); - - // Emit touch event - this.emit(topic + ":del", cacheValue); - } - - private void ValueTouch(string topic, JObject index, int? expirationTime) { - // Check if value already exists - string cacheKeyName = CreateCacheKey(topic, index); - VaultValue cacheValue = GetCacheValue(cacheKeyName); - if (cacheValue == null) { - logger.warning("Could not touch value (doesn't exist): " + cacheKeyName); - return; - } - - // Do touch - cacheValue.Touch(expirationTime); - - // Emit touch event - this.emit(topic + ":touch", cacheValue); - } - - private void ValueApplyDiff(string topic, JObject index, JArray diff, int? expirationTime) { - // Make sure value exists - string cacheKeyName = CreateCacheKey(topic, index); - VaultValue cacheValue = GetCacheValue(cacheKeyName); - if (cacheValue == null) { - logger.warning("Got a diff for a non-existent value:" + cacheKeyName); - return; - } - - // Apply diff - cacheValue.ApplyDiff(diff); - cacheValue.Touch(expirationTime); - - // Emit applyDiff event - this.emit(topic + ":applyDiff", cacheValue); - } - - - //////////////////////////////////////////// - // Raw Communication // - //////////////////////////////////////////// - private void rawGet(string topic, JObject index, Action cb) { - JObject parameters = new JObject(); - parameters.Add("topic", topic); - parameters.Add("index", index); - - mage.commandCenter.SendCommand("archivist.rawGet", parameters, cb); - } - - private void rawMGet(JToken queries, JObject options, Action cb) { - JObject parameters = new JObject(); - parameters.Add(new JProperty ("queries", queries)); - parameters.Add("options", options); - - mage.commandCenter.SendCommand("archivist.rawMGet", parameters, cb); - } - - private void rawList(string topic, JObject partialIndex, JObject options, Action cb) { - JObject parameters = new JObject(); - parameters.Add("topic", new JValue(topic)); - parameters.Add("partialIndex", partialIndex); - parameters.Add("options", options); - - mage.commandCenter.SendCommand("archivist.rawList", parameters, cb); - } - - private void rawSet(string topic, JObject index, JToken data, string mediaType, string encoding, string expirationTime, Action cb) { - JObject parameters = new JObject(); - parameters.Add ("topic", new JValue(topic)); - parameters.Add ("index", index); - parameters.Add(new JProperty ("data", data)); - parameters.Add ("mediaType", new JValue(mediaType)); - parameters.Add ("encoding", new JValue(encoding)); - parameters.Add ("expirationTime", new JValue(expirationTime)); - - mage.commandCenter.SendCommand("archivist.rawSet", parameters, cb); - } - - private void rawDel(string topic, JObject index, Action cb) { - JObject parameters = new JObject(); - parameters.Add ("topic", new JValue(topic)); - parameters.Add ("index", index); - - mage.commandCenter.SendCommand("archivist.rawDel", parameters, cb); - } - - - //////////////////////////////////////////// - // Exposed Operations // - //////////////////////////////////////////// - public void get(string topic, JObject index, JObject options, Action cb) { - // Default options - options = (options != null) ? options : new JObject(); - if (options["optional"] == null) { - options.Add("optional", new JValue(false)); - } - - - // Check cache - string cacheKeyName = CreateCacheKey(topic, index); - VaultValue cacheValue = GetCacheValue(cacheKeyName, (int?)options["maxAge"]); - if (cacheValue != null) { - cb(null, cacheValue.data); - return; - } - - - // Get data from server - rawGet (topic, index, (Exception error, JToken result) => { - if (error != null) { - cb (error, null); - return; - } - - // Parse value - try { - ValueSetOrDelete((JObject)result); - } catch (Exception cacheError) { - cb(cacheError, null); - return; - } - - // Return result - cb (null, GetCacheValue(cacheKeyName).data); - }); - } - - public void mget(JArray queries, JObject options, Action cb) { - // Default options - options = (options != null) ? options : new JObject(); - if (options["optional"] == null) { - options.Add("optional", new JValue(false)); - } - - - // Keep track of actual data we need from server - JArray realQueries = new JArray(); - Dictionary realQueryKeys = new Dictionary(); - JArray responseArray = new JArray(); - - - // Check cache - foreach (JObject query in queries) { - string topic = (string)query["topic"]; - JObject index = (JObject)query["index"]; - string cacheKeyName = CreateCacheKey(topic, index); - VaultValue cacheValue = GetCacheValue(cacheKeyName, (int?)options["maxAge"]); - if (cacheValue != null) { - responseArray.Add(cacheValue.data); - } else { - realQueryKeys.Add(cacheKeyName, responseArray.Count); - responseArray.Add(null); - realQueries.Add(query); - } - } - - - // Check if any real queries exist - if (realQueries.Count == 0) { - cb (null, responseArray); - return; - } - - - // Get data from server - rawMGet (realQueries, options, (Exception error, JToken results) => { - if (error != null) { - cb (error, null); - return; - } - - try { - foreach (JObject topicValue in results as JArray) { - // Determine value cacheKeyName - string topic = (string)topicValue["key"]["topic"]; - JObject index = (JObject)topicValue["key"]["index"]; - string cacheKeyName = CreateCacheKey(topic, index); - - // Set value to cache - ValueSetOrDelete(topicValue); - - // Add value to response - int responseKey = realQueryKeys[cacheKeyName]; - responseArray[responseKey].Replace(GetCacheValue(cacheKeyName).data); - } - } catch (Exception cacheError) { - cb(cacheError, null); - return; - } - - // Return result - cb (null, responseArray); - }); - } - - public void mget(JObject queries, JObject options, Action cb) { - // Default options - options = (options != null) ? options : new JObject(); - if (options["optional"] == null) { - options.Add("optional", new JValue(false)); - } - - - // Keep track of actual data we need from server - JObject realQueries = new JObject(); - Dictionary realQueryKeys = new Dictionary(); - JObject responseObject = new JObject(); - - - // Check cache - foreach (var query in queries) { - string cacheKeyName = CreateCacheKey(query.Value["topic"].ToString(), query.Value["index"] as JObject); - VaultValue cacheValue = GetCacheValue(cacheKeyName, (int?)options["maxAge"]); - if (cacheValue != null) { - responseObject.Add(query.Key, cacheValue.data); - } else { - realQueryKeys.Add(cacheKeyName, query.Key); - realQueries.Add(query.Key, query.Value); - } - } - - - // Check if any real queries exist - if (realQueries.Count == 0) { - cb (null, responseObject); - return; - } - - - // Get data from server - rawMGet (realQueries, options, (Exception error, JToken results) => { - if (error != null) { - cb (error, null); - return; - } - - try { - foreach (JObject topicValue in results as JArray) { - // Determine value cacheKeyName - string valueTopic = topicValue["key"]["topic"].ToString(); - JObject valueIndex = (JObject)topicValue["key"]["index"]; - string cacheKeyName = CreateCacheKey(valueTopic, valueIndex); - - // Set value to cache - ValueSetOrDelete(topicValue); - - // Add value to response - string responseKey = realQueryKeys[cacheKeyName]; - responseObject.Add(responseKey, GetCacheValue(cacheKeyName).data); - } - } catch (Exception cacheError) { - cb(cacheError, null); - return; - } - - // Return result - cb (null, responseObject); - }); - } - - public void list(string topic, JObject partialIndex, JObject options, Action cb) { - rawList (topic, partialIndex, options, cb); - } -} diff --git a/Mage/Archivist/VaultValue.cs b/Mage/Archivist/VaultValue.cs deleted file mode 100644 index 2214459..0000000 --- a/Mage/Archivist/VaultValue.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; - -using Newtonsoft.Json.Linq; - -public class VaultValue { - // - private string _topic; - public string topic { get { return _topic; } } - - private JObject _index; - public JObject index { get { return _index; } } - - private JToken _data; - public JToken data { get { return _data; } } - - private string _mediaType; - public string mediaType { get { return _mediaType; } } - - private int? _expirationTime; - public int? expirationTime { get { return _expirationTime; } } - - // - private DateTime _writtenAt; - public DateTime writtenAt { get { return _writtenAt; }} - - - // - public VaultValue(string topic, JObject index) { - _topic = topic; - _index = index; - } - - - // TODO: implement multiple media-types and encoding - public void SetData(string mediaType, JToken data) { - lock ((object)this) { - // Detect media type - _mediaType = mediaType; - - // Set data based on media type - _data = Tome.Conjure(JToken.Parse ((string)data)); - - // Bump the last written time - _writtenAt = DateTime.UtcNow; - } - } - - - // - public void Del() { - lock ((object)this) { - // Bump the last written time and check if we have data to destroy - _writtenAt = DateTime.UtcNow; - if (_data == null) { - return; - } - - // Cleanup data - Tome.Destroy(_data); - _data = null; - _mediaType = null; - - // Clear expiration time - Touch (null); - } - } - - - // TODO: the actual implementation of this requires the MAGE time module, - // also we have a timer to clear the value once expired. - public void Touch(int? expirationTime) { - lock ((object)this) { - _expirationTime = expirationTime; - } - } - - - // - public void ApplyDiff(JArray diff) { - lock ((object)this) { - if (diff == null || _data == null) { - return; - } - - // Apply diff to data - Tome.ApplyDiff(_data, diff); - - // Bump the last written time - _writtenAt = DateTime.UtcNow; - } - } -} - \ No newline at end of file diff --git a/Mage/CommandCenter/CommandBatch.cs b/Mage/CommandCenter/CommandBatch.cs deleted file mode 100644 index 118ddaa..0000000 --- a/Mage/CommandCenter/CommandBatch.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; - -using Newtonsoft.Json.Linq; - - -public class CommandBatch { - public int queryId; - public List batchItems = new List(); - - public CommandBatch(int queryId) { - this.queryId = queryId; - } - - public void Queue(string commandName, JObject parameters, Action cb) { - batchItems.Add(new CommandBatchItem(commandName, parameters, cb)); - } -} \ No newline at end of file diff --git a/Mage/CommandCenter/CommandBatchItem.cs b/Mage/CommandCenter/CommandBatchItem.cs deleted file mode 100644 index 2e12c17..0000000 --- a/Mage/CommandCenter/CommandBatchItem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -using Newtonsoft.Json.Linq; - - -public class CommandBatchItem { - public string commandName; - public JObject parameters; - public Action cb; - - public CommandBatchItem(string commandName, JObject parameters, Action cb) { - this.commandName = commandName; - this.parameters = parameters; - this.cb = cb; - } -} \ No newline at end of file diff --git a/Mage/CommandCenter/CommandCenter.cs b/Mage/CommandCenter/CommandCenter.cs deleted file mode 100644 index dcb6dae..0000000 --- a/Mage/CommandCenter/CommandCenter.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Collections.Generic; - -using Newtonsoft.Json.Linq; - - -public class CommandCenter { - private Mage mage { get { return Mage.Instance; }} - private Logger logger { get { return mage.logger("CommandCenter"); }} - - // Endpoint and credentials - private string baseUrl; - private string appName; - private string username; - private string password; - - // Current transport client - private CommandTransportClient transportClient; - - // Command Batches - private int nextQueryId = 1; - private CommandBatch currentBatch; - private CommandBatch sendingBatch; - - // - public CommandCenter(CommandTransportType transportType = CommandTransportType.HTTP) { - currentBatch = new CommandBatch(nextQueryId++); - SetTransport(transportType); - } - - // - public void SetTransport(CommandTransportType transportType) { - // Cleanup existing transport client - if (transportClient != null) { - transportClient = null; - } - - // Create new transport client instance - if (transportType == CommandTransportType.HTTP) { - transportClient = new CommandHTTPClient() as CommandTransportClient; - transportClient.SetEndpoint(baseUrl, appName, username, password); - } else if (transportType == CommandTransportType.JSONRPC) { - transportClient = new CommandJSONRPCClient() as CommandTransportClient; - transportClient.SetEndpoint(baseUrl, appName, username, password); - } else { - throw new Exception("Invalid transport type: " + transportType); - } - - // Setup event handlers - transportClient.OnSendComplete += BatchComplete; - transportClient.OnTransportError += TransportError; - } - - // - public void SetEndpoint(string baseUrl, string appName, string username = null, string password = null) { - this.baseUrl = baseUrl; - this.appName = appName; - this.username = username; - this.password = password; - - if (transportClient != null) { - transportClient.SetEndpoint(baseUrl, appName, username, password); - } - } - - // - private void SendBatch() { - mage.eventManager.emit("io.send", null); - - lock ((object)this) { - // Swap batches around locking the queue - sendingBatch = currentBatch; - currentBatch = new CommandBatch(nextQueryId++); - - // Send the batch - logger.debug("Sending batch: " + sendingBatch.queryId); - transportClient.SendBatch(sendingBatch); - } - } - - // Resend batch - public void Resend() { - mage.eventManager.emit("io.resend", null); - - logger.debug("Re-sending batch: " + sendingBatch.queryId); - transportClient.SendBatch(sendingBatch); - } - - // - private void BatchComplete() { - mage.eventManager.emit("io.response", null); - - lock ((object)this) { - sendingBatch = null; - - // Check if next queued batch should be sent as well - if (currentBatch.batchItems.Count > 0) { - SendBatch(); - } - } - } - - // - private void TransportError(string errorType, Exception error) { - logger.data(error).error("Error when sending command batch request '" + errorType + "'"); - mage.eventManager.emit("io.error." + errorType, null); - } - - // Try and send a command right away if there is nothing being sent. - public void SendCommand(string commandName, JObject parameters, Action cb) { - lock ((object)this) { - // Add command to queue - currentBatch.Queue(commandName, parameters, cb); - - // Check if we are busy and should only queue - if (sendingBatch != null) { - return; - } - - // Otherwise send the batch - SendBatch(); - } - } - - // Either send this command immediately if nothing is being sent, - // otherwise queue it and send it after the current send is complete. - public void QueueCommand(string commandName, JObject parameters, Action cb) { - lock ((object)this) { - // If we are not sending anything, send immediately - if (sendingBatch == null) { - SendCommand(commandName, parameters, cb); - return; - } - - // Otherwise queue it to current - currentBatch.Queue(commandName, parameters, cb); - } - } - - // Queue command to current batch and wait for it to get processed by another command - public void PiggyBackCommand(string commandName, JObject parameters, Action cb) { - lock ((object)this) { - currentBatch.Queue(commandName, parameters, cb); - } - } -} \ No newline at end of file diff --git a/Mage/CommandCenter/TransportClient/CommandHTTPClient.cs b/Mage/CommandCenter/TransportClient/CommandHTTPClient.cs deleted file mode 100644 index 3a7ce8f..0000000 --- a/Mage/CommandCenter/TransportClient/CommandHTTPClient.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; - -using Newtonsoft.Json.Linq; - -public class CommandHTTPClient : CommandTransportClient { - private Mage mage { get { return Mage.Instance; } } - private Logger logger { get { return mage.logger("CommandHTTPClient"); } } - - // - private string endpoint; - private string username; - private string password; - - // - public override void SetEndpoint(string baseUrl, string appName, string username = null, string password = null) { - this.endpoint = baseUrl + "/" + appName; - this.username = username; - this.password = password; - } - - // - public override void SendBatch(CommandBatch commandBatch) { - List commands = new List (); - List data = new List(); - - // Attach batch headers to post data - JArray batchHeader = new JArray(); - string sessionKey = mage.session.GetSessionKey(); - if (!string.IsNullOrEmpty(sessionKey)) { - JObject sessionHeader = new JObject(); - sessionHeader.Add("name", new JValue("mage.session")); - sessionHeader.Add("key", new JValue(sessionKey)); - batchHeader.Add(sessionHeader); - } - data.Add(batchHeader.ToString(Newtonsoft.Json.Formatting.None)); - - // Attach command names to url and parameters to post data - for (int batchId = 0; batchId < commandBatch.batchItems.Count; batchId += 1) { - CommandBatchItem commandItem = commandBatch.batchItems[batchId]; - commands.Add(commandItem.commandName); - data.Add(commandItem.parameters.ToString(Newtonsoft.Json.Formatting.None)); - logger.data(commandItem.parameters).verbose("sending command: " + commandItem.commandName); - } - - // Serialise post data - string batchUrl = endpoint + "/" + String.Join(",", commands.ToArray()) + "?queryId=" + commandBatch.queryId.ToString(); - string postData = string.Join("\n", data.ToArray()); - - // Send HTTP request - SendRequest(batchUrl, postData, (JArray responseArray) => { - // Process each command response - try { - for (int batchId = 0; batchId < responseArray.Count; batchId += 1) { - JArray commandResponse = responseArray[batchId] as JArray; - CommandBatchItem commandItem = commandBatch.batchItems[batchId]; - string commandName = commandItem.commandName; - Action commandCb = commandItem.cb; - - // Check if there are any events attached to this request - if (commandResponse.Count >= 3) { - logger.verbose("[" + commandName + "] processing events"); - mage.eventManager.emitEventList((JArray)commandResponse[2]); - } - - // Check if the response was an error - if (commandResponse[0].Type != JTokenType.Null) { - logger.verbose("[" + commandName + "] server error"); - commandCb(new Exception(commandResponse[0].ToString()), null); - return; - } - - // Pull off call result object, if it doesn't exist - logger.verbose("[" + commandName + "] call response"); - commandCb(null, commandResponse[1]); - } - } catch (Exception error) { - logger.data(error).error("Error when processing command batch responses"); - } - }); - } - - private void SendRequest(string batchUrl, string postData, Action cb) { - Dictionary headers = new Dictionary(); - if (username != null && password != null) { - string authInfo = username + ":" + password; - string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); - headers.Add("Authorization", "Basic " + encodedAuth); - } - - HTTPRequest.Post(batchUrl, "", postData, headers, mage.cookies, (Exception requestError, string responseString) => { - logger.verbose("Recieved response: " + responseString); - - // Check if there was a transport error - if (requestError != null) { - string error = "network"; - if (requestError is WebException) { - WebException webException = requestError as WebException; - HttpWebResponse webResponse = webException.Response as HttpWebResponse; - if (webResponse != null && webResponse.StatusCode == HttpStatusCode.ServiceUnavailable) { - error = "maintenance"; - } - } - - OnTransportError.Invoke(error, requestError); - return; - } - - // Parse reponse array - JArray responseArray; - try { - responseArray = JArray.Parse(responseString); - } catch (Exception parseError) { - OnTransportError.Invoke("parse", parseError); - return; - } - - // Let CommandCenter know this batch was successful - OnSendComplete.Invoke(); - - // Return array for processing - cb(responseArray); - }); - } -} \ No newline at end of file diff --git a/Mage/CommandCenter/TransportClient/CommandJSONRPCClient.cs b/Mage/CommandCenter/TransportClient/CommandJSONRPCClient.cs deleted file mode 100644 index f5e1ff3..0000000 --- a/Mage/CommandCenter/TransportClient/CommandJSONRPCClient.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; - -using Newtonsoft.Json.Linq; - -public class CommandJSONRPCClient : CommandTransportClient { - private Mage mage { get { return Mage.Instance; } } - private Logger logger { get { return mage.logger("CommandJSONRPCClient"); } } - - private JSONRPC rpcClient = new JSONRPC(); - - // - public override void SetEndpoint(string baseUrl, string appName, string username = null, string password = null) { - rpcClient.SetEndpoint(baseUrl + "/" + appName + "/jsonrpc", username, password); - } - - // - public override void SendBatch(CommandBatch commandBatch) { - // NOTE: This transport client cannot be implemented yet as JSON RPC support is - // terminally broken in MAGE (does not support queryId and response caching). - // Until this is fixed, this transport client cannot be used or completed. - logger.verbose("THIS TRANSPORT CLIENT IS NOT IMPLEMENTED"); - throw new Exception("THIS TRANSPORT CLIENT IS NOT IMPLEMENTED"); - - /* - JSONRPCBatch rpcBatch = new JSONRPCBatch(); - for (int batchId = 0; batchId < commandBatch.batchItems.Count; batchId += 1) { - CommandBatchItem commandItem = commandBatch.batchItems[batchId]; - rpcBatch.Add(batchId, commandItem.commandName, commandItem.parameters); - logger.data(commandItem.parameters).verbose("[" + commandItem.commandName + "] executing command"); - } - - // Attach any required mage headers - Dictionary headers = new Dictionary(); - - string sessionKey = mage.session.GetSessionKey(); - if (!string.IsNullOrEmpty(sessionKey)) { - headers.Add("X-MAGE-SESSION", sessionKey); - } - - // Send rpc batch - rpcClient.CallBatch(rpcBatch, headers, mage.cookies, (Exception error, JArray responseArray) => { - logger.data(responseArray).verbose("Recieved response: "); - - if (error != null) { - //TODO: OnTransportError.Invoke("", error); - return; - } - - // Process each command response - foreach (JObject responseObject in responseArray) { - int batchId = (int)responseObject["id"]; - CommandBatchItem commandItem = commandBatch.batchItems[batchId]; - string commandName = commandItem.commandName; - Action commandCb = commandItem.cb; - - // Check if there are any events attached to this request - if (responseObject["result"]["myEvents"] != null) { - logger.verbose("[" + commandName + "] processing events"); - mage.eventManager.emitEventList((JArray)responseObject["result"]["myEvents"]); - } - - // Check if the response was an error - if (responseObject["result"]["errorCode"] != null) { - logger.verbose("[" + commandName + "] server error"); - commandCb(new Exception(responseObject["result"]["errorCode"].ToString()), null); - return; - } - - // Pull off call result object, if it doesn't exist - logger.verbose("[" + commandName + "] call response"); - commandCb(null, responseObject["result"]["response"]); - } - }); - */ - } -} \ No newline at end of file diff --git a/Mage/CommandCenter/TransportClient/CommandTransportClient.cs b/Mage/CommandCenter/TransportClient/CommandTransportClient.cs deleted file mode 100644 index 728b8d1..0000000 --- a/Mage/CommandCenter/TransportClient/CommandTransportClient.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -public enum CommandTransportType { - HTTP, - JSONRPC -} - -public abstract class CommandTransportClient { - public Action OnSendComplete; - public Action OnTransportError; - - public abstract void SetEndpoint(string baseUrl, string appName, string username = null, string password = null); - public abstract void SendBatch(CommandBatch batch); -} \ No newline at end of file diff --git a/Mage/EventManager.cs b/Mage/EventManager.cs deleted file mode 100644 index b526d12..0000000 --- a/Mage/EventManager.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Newtonsoft.Json.Linq; - -public class EventManager : EventEmitter { - private Mage mage { get { return Mage.Instance; } } - private Logger logger { get { return mage.logger("eventManager"); } } - - public void emitEventList(JArray events) { - foreach (JToken responseEvent in events) { - string eventTag = null; - JToken eventData = null; - - // Copy the eventItem for processing - JArray eventItem = JArray.Parse(responseEvent.ToString()); - - // Check that event name is present - if (eventItem.Count >= 1) { - eventTag = eventItem[0].ToString(); - } - - // Check if any event data is present - if (eventItem.Count == 2) { - eventData = eventItem[1]; - } - - // Check if there were any errors, log and skip them - if (eventTag == null || eventItem.Count > 2) { - logger.data(eventItem).error("Invalid event format:"); - continue; - } - - // Emit the event - logger.debug("Emitting '" + eventTag + "'"); - mage.eventManager.emit (eventTag, eventData); - } - } -} diff --git a/Mage/Logger/LogEntry.cs b/Mage/Logger/LogEntry.cs deleted file mode 100644 index 6954fef..0000000 --- a/Mage/Logger/LogEntry.cs +++ /dev/null @@ -1,59 +0,0 @@ - -public class LogEntry { - // - public string context; - public object data; - public string message; - - public LogEntry(string _context, object _data = null) { - context = _context; - data = _data; - } - - - // - public void verbose (string _message) { - message = _message; - Logger.logEmitter.emit("verbose", this); - } - - public void debug (string _message) { - message = _message; - Logger.logEmitter.emit("debug", this); - } - - public void info (string _message) { - message = _message; - Logger.logEmitter.emit("info", this); - } - - public void notice (string _message) { - message = _message; - Logger.logEmitter.emit("notice", this); - } - - public void warning (string _message) { - message = _message; - Logger.logEmitter.emit("warning", this); - } - - public void error (string _message) { - message = _message; - Logger.logEmitter.emit("error", this); - } - - public void critical (string _message) { - message = _message; - Logger.logEmitter.emit("critical", this); - } - - public void alert (string _message) { - message = _message; - Logger.logEmitter.emit("alert", this); - } - - public void emergency (string _message) { - message = _message; - Logger.logEmitter.emit("emergency", this); - } -} diff --git a/Mage/Logger/Logger.cs b/Mage/Logger/Logger.cs deleted file mode 100644 index db54597..0000000 --- a/Mage/Logger/Logger.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; - - -public class Logger { - // - public static EventEmitter logEmitter = new EventEmitter(); - - // - public static Dictionary logWriters; - public static void SetConfig(Dictionary> config) { - // Destroy existing log writers - if (logWriters != null) { - foreach (var writer in logWriters.Values) { - writer.Dispose(); - } - logWriters = null; - } - - // Make sure we have configured something - if (config == null) { - return; - } - - // Create each writer with log levels - logWriters = new Dictionary(); - foreach (var property in config) { - string writer = property.Key; - List writerConfig = property.Value; - - switch (writer) { - case "console": - logWriters.Add(writer, new ConsoleWriter(writerConfig) as LogWriter); - break; - case "server": - logWriters.Add(writer, new ServerWriter(writerConfig) as LogWriter); - break; - default: - throw new Exception("Unknown Log Writer: " + writer); - } - } - } - - - // - private string _context; - - public Logger(string context) { - _context = context; - } - - - // - public LogEntry data (object data) { - return new LogEntry (_context, data); - } - - - // - public void verbose (string message) { - (new LogEntry (_context)).verbose (message); - } - - public void debug (string message) { - (new LogEntry (_context)).debug (message); - } - - public void info (string message) { - (new LogEntry (_context)).info (message); - } - - public void notice (string message) { - (new LogEntry (_context)).notice (message); - } - - public void warning (string message) { - (new LogEntry (_context)).warning (message); - } - - public void error (string message) { - (new LogEntry (_context)).error (message); - } - - public void critical (string message) { - (new LogEntry (_context)).critical (message); - } - - public void alert (string message) { - (new LogEntry (_context)).alert (message); - } - - public void emergency (string message) { - (new LogEntry (_context)).emergency (message); - } -} diff --git a/Mage/Logger/Writers/ConsoleWriter.cs b/Mage/Logger/Writers/ConsoleWriter.cs deleted file mode 100644 index 1d43aaf..0000000 --- a/Mage/Logger/Writers/ConsoleWriter.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; - - -public class ConsoleWriter : LogWriter { - private List config; - - public ConsoleWriter(List logLevels) { - config = logLevels; - - if (config.Contains("verbose")) { - Logger.logEmitter.on("verbose", Verbose); - } - - if (config.Contains("debug")) { - Logger.logEmitter.on("debug", Debug); - } - - if (config.Contains("info")) { - Logger.logEmitter.on("info", Info); - } - - if (config.Contains("notice")) { - Logger.logEmitter.on("notice", Notice); - } - - if (config.Contains("warning")) { - Logger.logEmitter.on("warning", Warning); - } - - if (config.Contains("error")) { - Logger.logEmitter.on("error", Error); - } - - if (config.Contains("critical")) { - Logger.logEmitter.on("critical", Critical); - } - - if (config.Contains("alert")) { - Logger.logEmitter.on("alert", Alert); - } - - if (config.Contains("emergency")) { - Logger.logEmitter.on("emergency", Emergency); - } - } - - public override void Dispose() { - if (config.Contains("verbose")) { - Logger.logEmitter.off("verbose", Verbose); - } - - if (config.Contains("debug")) { - Logger.logEmitter.off("debug", Debug); - } - - if (config.Contains("info")) { - Logger.logEmitter.off("info", Info); - } - - if (config.Contains("notice")) { - Logger.logEmitter.off("notice", Notice); - } - - if (config.Contains("warning")) { - Logger.logEmitter.off("warning", Warning); - } - - if (config.Contains("error")) { - Logger.logEmitter.off("error", Error); - } - - if (config.Contains("critical")) { - Logger.logEmitter.off("critical", Critical); - } - - if (config.Contains("alert")) { - Logger.logEmitter.off("alert", Alert); - } - - if (config.Contains("emergency")) { - Logger.logEmitter.off("emergency", Emergency); - } - } - - private string makeLogString(string channel, string context, string message) { - return String.Format("[{0}] [{1}] {2}", channel, context, message); - } - - private void Verbose(object sender, LogEntry logEntry) { - UnityEngine.Debug.Log(makeLogString("verbose", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.Log(logEntry.data); - } - } - - private void Debug(object sender, LogEntry logEntry) { - UnityEngine.Debug.Log(makeLogString("debug", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.Log(logEntry.data); - } - } - - private void Info(object sender, LogEntry logEntry) { - UnityEngine.Debug.Log(makeLogString("info", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.Log(logEntry.data); - } - } - - private void Notice(object sender, LogEntry logEntry) { - UnityEngine.Debug.Log(makeLogString("notice", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.Log(logEntry.data); - } - } - - private void Warning(object sender, LogEntry logEntry) { - UnityEngine.Debug.LogWarning(makeLogString("warning", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.LogWarning (logEntry.data); - } - } - - private void Error(object sender, LogEntry logEntry) { - UnityEngine.Debug.LogError(makeLogString("error", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.LogError(logEntry.data); - } - } - - private void Critical(object sender, LogEntry logEntry) { - UnityEngine.Debug.LogError(makeLogString("critical", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - if (logEntry.data is Exception && (logEntry.data as Exception).StackTrace != null) { - Exception excpt = logEntry.data as Exception; - UnityEngine.Debug.LogError(excpt.ToString() + ":\n" + excpt.StackTrace.ToString()); - } else { - UnityEngine.Debug.LogError(logEntry.data); - } - } - } - - private void Alert(object sender, LogEntry logEntry) { - UnityEngine.Debug.LogError(makeLogString("alert", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.LogError(logEntry.data); - } - } - - private void Emergency(object sender, LogEntry logEntry) { - UnityEngine.Debug.LogError(makeLogString("emergency", logEntry.context, logEntry.message)); - if (logEntry.data != null) { - UnityEngine.Debug.LogError(logEntry.data); - } - } -} diff --git a/Mage/Logger/Writers/LogWriter.cs b/Mage/Logger/Writers/LogWriter.cs deleted file mode 100644 index 41c71df..0000000 --- a/Mage/Logger/Writers/LogWriter.cs +++ /dev/null @@ -1,3 +0,0 @@ -public abstract class LogWriter { - public abstract void Dispose(); -} \ No newline at end of file diff --git a/Mage/Logger/Writers/ServerWriter.cs b/Mage/Logger/Writers/ServerWriter.cs deleted file mode 100644 index f77944f..0000000 --- a/Mage/Logger/Writers/ServerWriter.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - - -public class ServerWriter : LogWriter { - //private List config; - - public ServerWriter(List logLevels) { - //config = logLevels; - } - - public override void Dispose() { - } -} \ No newline at end of file diff --git a/Mage/Mage.cs b/Mage/Mage.cs deleted file mode 100644 index 613ce04..0000000 --- a/Mage/Mage.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; -using System.Net; - -using Newtonsoft.Json.Linq; - - -public class MageSetupStatus { - public bool done = false; - public Exception error = null; -} - - -public class Mage : Singleton { - // - public EventManager eventManager; - public Session session; - public CommandCenter commandCenter; - public MessageStream messageStream; - public Archivist archivist; - - private Logger _logger; - - // - string baseUrl; - string appName; - string username; - string password; - public CookieContainer cookies; - - // - private Dictionary _loggers = new Dictionary(); - public Logger logger (string context = null) { - if (string.IsNullOrEmpty(context)) { - context = "Default"; - } - - if (_loggers.ContainsKey(context)) { - return _loggers[context]; - } - - Logger newLogger = new Logger (context); - _loggers.Add (context, new Logger(context)); - return newLogger; - } - - - // Avoid putting setup logic in the contstuctor. Only things that can be - // carried between game sessions should go here. Otherwise we need to be - // able to re-initialize them inside the setup function. - public Mage() { - // Setup log writters - _logger = logger("mage"); - - // TODO: properly check the damn certificate, for now ignore invalid ones (fix issue on Android/iOS) - ServicePointManager.ServerCertificateValidationCallback += (o, cert, chain, errors) => true; - } - - // - public void setEndpoints (string baseUrl, string appName, string username = null, string password = null) { - this.baseUrl = baseUrl; - this.appName = appName; - this.username = username; - this.password = password; - - if (commandCenter != null) { - commandCenter.SetEndpoint(baseUrl, appName, username, password); - } - - if (messageStream != null) { - messageStream.SetEndpoint(baseUrl, username, password); - } - } - - // - public void Setup(Action cb) { - // Cleanup any existing internal modules - if (messageStream != null) { - messageStream.Dispose(); - } - - - // Instantiate HTTPRequestManager - HTTPRequestManager.Instantiate(); - - - // Create a shared cookie container - cookies = new CookieContainer(); - - - // Initialize mage internal modules - eventManager = new EventManager(); - session = new Session(); - commandCenter = new CommandCenter(); - messageStream = new MessageStream(); - archivist = new Archivist(); - - - // Set endpoints - commandCenter.SetEndpoint(baseUrl, appName, username, password); - messageStream.SetEndpoint(baseUrl, username, password); - - cb(null); - } - - // - public void SetupModules(List moduleNames, Action cb) { - // Setup application modules - _logger.info ("Setting up modules"); - Async.each (moduleNames, (string moduleName, Action callback) => { - _logger.info("Setting up module: " + moduleName); - - // Use reflection to find module by name - Assembly assembly = Assembly.GetExecutingAssembly(); - Type[] assemblyTypes = assembly.GetTypes(); - foreach(Type t in assemblyTypes) { - if (moduleName == t.Name) { - BindingFlags staticProperty = BindingFlags.Static | BindingFlags.GetProperty; - BindingFlags publicMethod = BindingFlags.Public | BindingFlags.InvokeMethod; - - // Grab module instance from singleton base - var singletonType = typeof(Singleton<>).MakeGenericType(t); - Object instance = singletonType.InvokeMember("Instance", staticProperty, null, null, null); - - // Setup module - var moduleType = typeof(Module<>).MakeGenericType(t); - Async.series (new List>>() { - (Action callbackInner) => { - // Setup module user commands - object[] arguments = new object[]{callbackInner}; - moduleType.InvokeMember("setupUsercommands", publicMethod, null, instance, arguments); - }, - (Action callbackInner) => { - // Setup module static data - object[] arguments = new object[]{callbackInner}; - moduleType.InvokeMember("setupStaticData", publicMethod, null, instance, arguments); - } - }, (Exception error) => { - if (error != null) { - callback(error); - return; - } - - // Check if the module has a setup method - if (t.GetMethod("Setup") == null) { - logger(moduleName).info("No setup function"); - callback(null); - return; - } - - // Invoke the setup method on the module - logger(moduleName).info("Executing setup function"); - object[] arguments = new object[]{callback}; - t.InvokeMember("Setup", publicMethod, null, instance, arguments); - }); - - return; - } - } - - // If nothing found throw an error - callback(new Exception("Can't find module " + moduleName)); - }, (Exception error) => { - if (error != null) { - _logger.data(error).error("Setup failed!"); - cb(error); - return; - } - - _logger.info("Setup complete"); - cb(null); - }); - } - - - // - public IEnumerator SetupTask(Action cb) { - // Execute async setup function - MageSetupStatus setupStatus = new MageSetupStatus(); - Setup((Exception error) => { - setupStatus.error = error; - setupStatus.done = true; - }); - - // Wait for setup to return - while (!setupStatus.done) { - yield return null; - } - - // Call callback with error if there is one - cb (setupStatus.error); - } - - // - public IEnumerator SetupModulesTask(List moduleNames, Action cb) { - // Execute async setup function - MageSetupStatus setupStatus = new MageSetupStatus (); - SetupModules(moduleNames, (Exception error) => { - setupStatus.error = error; - setupStatus.done = true; - }); - - // Wait for setup to return - while (!setupStatus.done) { - yield return null; - } - - // Call callback with error if there is one - cb (setupStatus.error); - } -} diff --git a/Mage/MessageStream/MessageStream.cs b/Mage/MessageStream/MessageStream.cs deleted file mode 100644 index 49c2227..0000000 --- a/Mage/MessageStream/MessageStream.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -using Newtonsoft.Json.Linq; - -public class MessageStream { - private Mage mage { get { return Mage.Instance; } } - private Logger logger { get { return mage.logger("messagestream"); } } - - // Endpoint and credentials - private string _endpoint; - private string _username; - private string _password; - private string _sessionKey; - - // Current transport client - private TransportClient transportClient; - - // Current message stack - private int currentMessageId; - private int largestMessageId; - private Dictionary messageQueue; - private List confirmIds; - - - // - private void initializeMessageList() { - currentMessageId = -1; - largestMessageId = -1; - messageQueue = new Dictionary(); - confirmIds = new List(); - - logger.debug ("Initialized message queue"); - } - - - // Constructor - public MessageStream (TransportType transport = TransportType.LONGPOLLING) { - // - initializeMessageList (); - - // Start transport client when session is acquired - mage.eventManager.on ("session.set", (object sender, JToken session) => { - _sessionKey = UnityEngine.WWW.EscapeURL(session["key"].ToString()); - transportClient.start(); - }); - - // Stop the message client when session is lost - mage.eventManager.on ("session.unset", (object sender, JToken reason) => { - transportClient.stop(); - initializeMessageList(); - _sessionKey = null; - }); - - // Also stop the message client when the editor is stopped - #if UNITY_EDITOR - UnityEditorPlayMode.onEditorModeChanged += (EditorPlayModeState newState) => { - if (newState == EditorPlayModeState.Stopped) { - transportClient.stop(); - initializeMessageList(); - _sessionKey = null; - } - }; - #endif - - // Set the selected transport client (or the default) - this.SetTransport(transport); - } - - - // - public void Dispose() { - // Stop the transport client if it exists - if (transportClient != null) { - transportClient.stop (); - } - - initializeMessageList(); - _sessionKey = null; - } - - - // Updates URI and credentials - public void SetEndpoint(string baseURL, string username = null, string password = null) { - _endpoint = baseURL + "/msgstream"; - _username = username; - _password = password; - } - - - // Sets up given transport client type - public void SetTransport(TransportType transport) { - // Stop existing transport client if any, when nulled out it will be collected - // by garbage collecter after existing connections have been terminated. - if (transportClient != null) { - transportClient.stop (); - transportClient = null; - } - - // Create new transport client instance - if (transport == TransportType.SHORTPOLLING) { - Func getShortPollingEndpoint = () => { - return getHttpPollingEndpoint ("shortpolling"); - }; - - transportClient = new ShortPolling(getShortPollingEndpoint, getHttpHeaders, processMessagesString, 5000) as TransportClient; - } else if (transport == TransportType.LONGPOLLING) { - Func getLongPollingEndpoint = () => { - return getHttpPollingEndpoint("longpolling"); - }; - - transportClient = new LongPolling(getLongPollingEndpoint, getHttpHeaders, processMessagesString) as TransportClient; - } else { - throw new Exception("Invalid transport type: " + transport); - } - } - - - // Returns the endpoint URL for polling transport clients i.e. longpolling and shortpolling - private string getHttpPollingEndpoint(string transport) { - string endpoint = _endpoint + "?transport=" + transport + "&sessionKey=" + _sessionKey; - if (confirmIds.Count > 0) { - endpoint += "&confirmIds=" + string.Join(",", confirmIds.ToArray()); - confirmIds.Clear(); - } - - return endpoint; - } - - - // Returns the required HTTP headers - private Dictionary getHttpHeaders() { - if (_username == null && _password == null) { - return null; - } - - Dictionary headers = new Dictionary(); - string authInfo = _username + ":" + _password; - string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); - headers.Add("Authorization", "Basic " + encodedAuth); - return headers; - } - - - // Deserilizes and processes given messagesString - private void processMessagesString(string messagesString) { - if (messagesString == "" || messagesString == null) { - return; - } - - JObject messages = JObject.Parse(messagesString); - addMessages(messages); - processMessages(); - } - - - // Add list of messages to message queue - private void addMessages(JObject messages) { - if (messages == null) { - return; - } - - int lowestMessageId = -1; - - foreach (var message in messages) { - // Check if the messageId is lower than our current messageId - int messageId = int.Parse(message.Key); - if (messageId == 0) { - messageQueue.Add(messageId, message.Value); - continue; - } - if (messageId < currentMessageId) { - continue; - } - - // Keep track of the largest messageId in the list - if (messageId > largestMessageId) { - largestMessageId = messageId; - } - - // Keep track of the lowest messageId in the list - if (lowestMessageId == -1 || messageId < lowestMessageId) { - lowestMessageId = messageId; - } - - // Check if the message exists in the queue, if not add it - if (!messageQueue.ContainsKey(messageId)) { - messageQueue.Add(messageId, message.Value); - } - } - - // If the current messageId has never been set, set it to the current lowest - if (currentMessageId == -1) { - currentMessageId = lowestMessageId; - } - } - - - // Process the message queue till we reach the end or a gap - private void processMessages() { - // Process all ordered messages in the order they appear - while (currentMessageId <= largestMessageId) { - // Check if the next messageId exists - if (!messageQueue.ContainsKey(currentMessageId)) { - break; - } - - // Process the message - mage.eventManager.emitEventList((JArray)messageQueue[currentMessageId]); - confirmIds.Add(currentMessageId.ToString()); - messageQueue.Remove(currentMessageId); - - currentMessageId += 1; - } - - // Finally emit any events that don't have an ID and thus don't need confirmation and lack order - if (messageQueue.ContainsKey(0)) { - mage.eventManager.emitEventList((JArray)messageQueue[0]); - messageQueue.Remove(0); - } - } -} diff --git a/Mage/MessageStream/TransportClient/LongPolling.cs b/Mage/MessageStream/TransportClient/LongPolling.cs deleted file mode 100644 index dd26686..0000000 --- a/Mage/MessageStream/TransportClient/LongPolling.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading; -using System.Net; - -public class LongPolling : TransportClient { - private Mage mage { get { return Mage.Instance; } } - private Logger logger { get { return mage.logger("longpolling"); } } - - // Whether or not the poller is working - private bool _running = false; - - // Required functions for poll requests - private Func _getEndpoint; - private Func> _getHeaders; - private Action _processMessages; - - // - HTTPRequest _currentRequest; - - // Required interval timer for polling delay - private int _errorInterval; - private Timer _intervalTimer; - - - // Constructor - public LongPolling(Func getEndpointFn, Func> getHeadersFn, Action processMessagesFn, int errorInterval = 5000) { - _getEndpoint = getEndpointFn; - _getHeaders = getHeadersFn; - _processMessages = processMessagesFn; - _errorInterval = errorInterval; - } - - - // Starts the poller - public override void start() { - if (_running == true) { - return; - } - - logger.debug("Starting"); - _running = true; - requestLoop (); - } - - - // Stops the poller - public override void stop() { - _running = false; - logger.debug("Stopping..."); - - if (_intervalTimer != null) { - _intervalTimer.Dispose (); - _intervalTimer = null; - } else { - logger.debug("Timer Stopped"); - } - - if (_currentRequest != null) { - _currentRequest.Abort(); - _currentRequest = null; - } else { - logger.debug("Connections Stopped"); - } - } - - - // Queues the next poll request - private void queueNextRequest(int waitFor) { - // Wait _requestInterval milliseconds till next poll - _intervalTimer = new Timer((object state) => { - requestLoop(); - }, null, waitFor, Timeout.Infinite); - } - - - // Poller request function - private void requestLoop() { - // Clear the timer - if (_intervalTimer != null) { - _intervalTimer.Dispose(); - _intervalTimer = null; - } - - // Check if the poller should be running - if (_running == false) { - logger.debug("Stopped"); - return; - } - - // Send poll request and wait for a response - string endpoint = _getEndpoint(); - logger.debug("Sending request: " + endpoint); - _currentRequest = HTTPRequest.Get(endpoint, _getHeaders(), mage.cookies, (Exception requestError, string responseString) => { - _currentRequest = null; - - // Ignore errors if we have been stopped - if (requestError != null && !_running) { - logger.debug("Stopped"); - return; - } - - if (requestError != null) { - if (requestError is HTTPRequestException) { - // Only log web exceptions if they aren't an empty response or gateway timeout - HTTPRequestException requestException = requestError as HTTPRequestException; - if (requestException.Status != 0 && requestException.Status != 504) { - logger.error("(" + requestException.Status.ToString() + ") " + requestError.Message); - } - } else { - logger.error(requestError.ToString()); - } - - queueNextRequest(_errorInterval); - return; - } - - // Call the message processer hook and re-call request loop function - try { - logger.verbose("Recieved response: " + responseString); - if (responseString != null) { - _processMessages(responseString); - } - - requestLoop(); - } catch (Exception error) { - logger.error (error.ToString()); - queueNextRequest(_errorInterval); - } - }); - } -} diff --git a/Mage/MessageStream/TransportClient/ShortPolling.cs b/Mage/MessageStream/TransportClient/ShortPolling.cs deleted file mode 100644 index 826d1a0..0000000 --- a/Mage/MessageStream/TransportClient/ShortPolling.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading; - -public class ShortPolling : TransportClient { - private Mage mage { get { return Mage.Instance; } } - private Logger logger { get { return mage.logger("shortpolling"); } } - - // Whether or not the poller is working - private bool _running = false; - - // Required functions for poll requests - private Func _getEndpoint; - private Func> _getHeaders; - private Action _processMessages; - - // Required interval timer for polling delay - private int _requestInterval; - private int _errorInterval; - private Timer _intervalTimer; - - - // Constructor - public ShortPolling(Func getEndpointFn, Func> getHeadersFn, Action processMessagesFn, int requestInterval = 5000, int errorInterval = 5000) { - _getEndpoint = getEndpointFn; - _getHeaders = getHeadersFn; - _processMessages = processMessagesFn; - _requestInterval = requestInterval; - _errorInterval = errorInterval; - } - - - // Starts the poller - public override void start() { - if (_running == true) { - return; - } - - logger.debug ("Starting"); - _running = true; - requestLoop (); - } - - - // Stops the poller - public override void stop() { - if (_intervalTimer != null) { - _intervalTimer.Dispose(); - _intervalTimer = null; - logger.debug ("Stopped"); - } else { - logger.debug ("Stopping..."); - } - _running = false; - } - - - // Queues the next poll request - private void queueNextRequest(int waitFor) { - // Wait _requestInterval milliseconds till next poll - _intervalTimer = new Timer((object state) => { - requestLoop(); - }, null, waitFor, Timeout.Infinite); - } - - - // Poller request function - private void requestLoop() { - // Clear the timer - if (_intervalTimer != null) { - _intervalTimer.Dispose(); - _intervalTimer = null; - } - - // Check if the poller should be running - if (_running == false) { - logger.debug ("Stopped"); - return; - } - - // Send poll request and wait for a response - string endpoint = _getEndpoint(); - logger.debug ("Sending request: " + endpoint); - HTTPRequest.Get(endpoint, _getHeaders(), mage.cookies, (Exception requestError, string responseString) => { - if (requestError != null) { - logger.error (requestError.ToString()); - queueNextRequest(_errorInterval); - return; - } - - // Call the message processer hook and queue the next request - try { - _processMessages(responseString); - queueNextRequest(_requestInterval); - } catch (Exception error) { - logger.data(responseString).error (error.ToString()); - queueNextRequest(_errorInterval); - } - }); - } -} diff --git a/Mage/MessageStream/TransportClient/TransportClient.cs b/Mage/MessageStream/TransportClient/TransportClient.cs deleted file mode 100644 index d7816b5..0000000 --- a/Mage/MessageStream/TransportClient/TransportClient.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -public enum TransportType { - SHORTPOLLING, - LONGPOLLING -} - -public abstract class TransportClient { - public abstract void stop (); - public abstract void start(); -} diff --git a/Mage/Module.cs b/Mage/Module.cs deleted file mode 100644 index d6eacf4..0000000 --- a/Mage/Module.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; - -using Newtonsoft.Json.Linq; - - -public class UsercommandStatus { - public bool done = false; - public Exception error = null; - public JToken result = null; -} - - -public class Module : Singleton where T : class, new() { - // - protected Mage mage { get { return Mage.Instance; } } - protected Logger logger { get { return mage.logger(this.GetType().Name); } } - - - // - protected virtual List staticTopics { get { return null; } } - public JToken staticData; - public void setupStaticData (Action cb) { - logger.info ("Setting up static data"); - - if (staticTopics == null) { - cb(null); - return; - } - - JObject queries = new JObject(); - for (int i = 0; i < staticTopics.Count; i++) { - string topic = staticTopics[i]; - - JObject query = new JObject(); - query.Add("topic", new JValue(topic)); - query.Add("index", new JObject()); - queries.Add(topic, query); - } - - staticData = null; - mage.archivist.mget (queries, null, (Exception error, JToken data)=>{ - if (error != null) { - cb(error); - return; - } - - staticData = data; - cb(null); - }); - } - - - // - protected virtual string commandPrefix { get { return null; } } - protected virtual List commands { get { return null; } } - private Dictionary>> commandHandlerActions; - private Dictionary> commandHandlerFuncs; - - public void command(string commandName, JObject arguments, Action cb) { - commandHandlerActions[commandName](arguments, cb); - } - - public UsercommandStatus command(string commandName, JObject arguments) { - return commandHandlerFuncs[commandName](arguments); - } - - private void registerCommand(string command) { - commandHandlerActions.Add(command, (JObject arguments, Action commandCb) => { - mage.commandCenter.SendCommand(commandPrefix + "." + command, arguments, (Exception error, JToken result) => { - try { - commandCb(error, result); - } catch (Exception callbackError) { - logger.data(callbackError).critical("Uncaught exception:"); - } - }); - }); - - commandHandlerFuncs.Add(command, (JObject arguments) => { - UsercommandStatus commandStatus = new UsercommandStatus(); - - mage.commandCenter.SendCommand(commandPrefix + "." + command, arguments, (Exception error, JToken result) => { - commandStatus.error = error; - commandStatus.result = result; - commandStatus.done = true; - }); - - return commandStatus; - }); - } - - public void setupUsercommands (Action cb) { - logger.info ("Setting up usercommands"); - - commandHandlerActions = new Dictionary>>(); - commandHandlerFuncs = new Dictionary>(); - - if (commands == null) { - cb(null); - return; - } - - foreach (string command in commands) { - registerCommand(command); - } - - cb(null); - } -} diff --git a/Mage/Session.cs b/Mage/Session.cs deleted file mode 100644 index 04f1e84..0000000 --- a/Mage/Session.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json.Linq; - -public class Session { - private Mage mage { get { return Mage.Instance; }} - - // - private string sessionKey; - private string actorId; - - // - public Session () { - mage.eventManager.on ("session.set", (object sender, JToken session) => { - actorId = session["actorId"].ToString(); - sessionKey = session["key"].ToString(); - }); - - mage.eventManager.on ("session.unset", (object sender, JToken reason) => { - actorId = null; - sessionKey = null; - }); - } - - // - public string GetSessionKey() { - return sessionKey; - } - - // - public string GetActorId() { - return actorId; - } -} diff --git a/Singleton/MonoSingleton.cs b/Singleton/MonoSingleton.cs deleted file mode 100644 index 1c26c28..0000000 --- a/Singleton/MonoSingleton.cs +++ /dev/null @@ -1,38 +0,0 @@ -using UnityEngine; - -public class MonoSingleton : MonoBehaviour where T : MonoBehaviour { - // Instance functions - protected static T _Instance; - public static T Instance { - get { - if (_Instance == null) { - Instantiate(); - } - - return _Instance; - } - } - - // Instantiation function if you need to pre-instantiate rather than on demand - public static void Instantiate() { - if (_Instance != null) { - return; - } - - GameObject newObject = new GameObject(typeof(T).Name); - GameObject.DontDestroyOnLoad(newObject); - - _Instance = newObject.AddComponent(); - } - - // Use this for initialization before any start methods are called - protected virtual void Awake() { - if (_Instance != null) { - GameObject.DestroyImmediate(gameObject); - return; - } - - _Instance = (T)(object)this; - GameObject.DontDestroyOnLoad(gameObject); - } -} diff --git a/Singleton/SceneInstance.cs b/Singleton/SceneInstance.cs deleted file mode 100644 index 2148080..0000000 --- a/Singleton/SceneInstance.cs +++ /dev/null @@ -1,17 +0,0 @@ -using UnityEngine; - -public class SceneInstance : MonoBehaviour where T : class { - // - protected static T _Instance = null; - public static T Instance { get { return _Instance; } } - - // Use this for initialization before any start methods are called - protected virtual void Awake () { - _Instance = (T)(object)this; - } - - // Use this for destruction - protected virtual void OnDestroy() { - _Instance = null; - } -} diff --git a/Singleton/Singleton.cs b/Singleton/Singleton.cs deleted file mode 100644 index 0634e8a..0000000 --- a/Singleton/Singleton.cs +++ /dev/null @@ -1,20 +0,0 @@ -using UnityEngine; - -public class Singleton where T : class, new() { - // Instance functions - private static T _Instance; - public static T Instance { - get { - if (_Instance == null) { - _Instance = new T(); - } - - return _Instance; - } - } - - // Hack which makes sure the _instance property is set during the T class constructor - public Singleton () { - _Instance = (T)(object)this; - } -} diff --git a/UnityEditorPlayMode/UnityEditorPlayMode.cs b/UnityEditorPlayMode/UnityEditorPlayMode.cs deleted file mode 100644 index 249c2da..0000000 --- a/UnityEditorPlayMode/UnityEditorPlayMode.cs +++ /dev/null @@ -1,64 +0,0 @@ -#if UNITY_EDITOR - -using UnityEditor; - - -public enum EditorPlayModeState -{ - Stopped, - Playing, - Paused -} - -[InitializeOnLoad] -public class UnityEditorPlayMode { - private static EditorPlayModeState currentState = EditorPlayModeState.Stopped; - public delegate void OnEditorModeChanged(EditorPlayModeState newState); - public static OnEditorModeChanged onEditorModeChanged; - - static UnityEditorPlayMode() - { - EditorApplication.playmodeStateChanged += OnUnityPlayModeChanged; - if (EditorApplication.isPaused) { - currentState = EditorPlayModeState.Paused; - } - } - - private static void OnUnityPlayModeChanged() - { - EditorPlayModeState newState = EditorPlayModeState.Stopped; - switch (currentState) { - case EditorPlayModeState.Stopped: - if (EditorApplication.isPlayingOrWillChangePlaymode) { - newState = EditorPlayModeState.Playing; - } else { - newState = EditorPlayModeState.Paused; - } - break; - case EditorPlayModeState.Playing: - if (EditorApplication.isPaused) { - newState = EditorPlayModeState.Paused; - } else if (EditorApplication.isPlaying) { - newState = EditorPlayModeState.Playing; - } else { - newState = EditorPlayModeState.Stopped; - } - break; - case EditorPlayModeState.Paused: - if (EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isPaused) { - newState = EditorPlayModeState.Playing; - } else if (EditorApplication.isPlayingOrWillChangePlaymode && EditorApplication.isPaused) { - newState = EditorPlayModeState.Paused; - } - break; - } - - if (onEditorModeChanged != null) { - onEditorModeChanged.Invoke(newState); - } - - currentState = newState; - } -} - -#endif \ No newline at end of file From 06d1113fcc49ba2ec13d2b345ba59b60871bfa3a Mon Sep 17 00:00:00 2001 From: Kevin Destrem Date: Fri, 3 Jun 2016 12:27:33 +0900 Subject: [PATCH 2/7] Add Mage folder --- MageClient/Archivist/Archivist.cs | 578 ++++++++++++++++++ MageClient/Archivist/VaultValue.cs | 96 +++ .../Command/Client/CommandHttpClient.cs | 174 ++++++ .../Command/Client/CommandJsonrpcClient.cs | 94 +++ .../Command/Client/CommandTransportClient.cs | 14 + .../Command/Client/CommandTransportType.cs | 8 + MageClient/Command/CommandBatch.cs | 23 + MageClient/Command/CommandBatchItem.cs | 20 + MageClient/Command/CommandCenter.cs | 182 ++++++ MageClient/EventManager.cs | 55 ++ MageClient/Log/LogEntry.cs | 72 +++ MageClient/Log/Logger.cs | 118 ++++ MageClient/Log/Writers/ConsoleWriter.cs | 202 ++++++ MageClient/Log/Writers/LogWriter.cs | 7 + MageClient/Log/Writers/ServerWriter.cs | 16 + MageClient/Mage.cs | 242 ++++++++ MageClient/MageSetupStatus.cs | 10 + MageClient/Message/Client/LongPolling.cs | 177 ++++++ MageClient/Message/Client/ShortPolling.cs | 138 +++++ MageClient/Message/Client/TransportClient.cs | 8 + MageClient/Message/Client/TransportType.cs | 8 + MageClient/Message/MessageStream.cs | 278 +++++++++ MageClient/Module.cs | 140 +++++ MageClient/Session.cs | 41 ++ MageClient/UserCommandStatus.cs | 13 + 25 files changed, 2714 insertions(+) create mode 100644 MageClient/Archivist/Archivist.cs create mode 100644 MageClient/Archivist/VaultValue.cs create mode 100644 MageClient/Command/Client/CommandHttpClient.cs create mode 100644 MageClient/Command/Client/CommandJsonrpcClient.cs create mode 100644 MageClient/Command/Client/CommandTransportClient.cs create mode 100644 MageClient/Command/Client/CommandTransportType.cs create mode 100644 MageClient/Command/CommandBatch.cs create mode 100644 MageClient/Command/CommandBatchItem.cs create mode 100644 MageClient/Command/CommandCenter.cs create mode 100644 MageClient/EventManager.cs create mode 100644 MageClient/Log/LogEntry.cs create mode 100644 MageClient/Log/Logger.cs create mode 100644 MageClient/Log/Writers/ConsoleWriter.cs create mode 100644 MageClient/Log/Writers/LogWriter.cs create mode 100644 MageClient/Log/Writers/ServerWriter.cs create mode 100644 MageClient/Mage.cs create mode 100644 MageClient/MageSetupStatus.cs create mode 100644 MageClient/Message/Client/LongPolling.cs create mode 100644 MageClient/Message/Client/ShortPolling.cs create mode 100644 MageClient/Message/Client/TransportClient.cs create mode 100644 MageClient/Message/Client/TransportType.cs create mode 100644 MageClient/Message/MessageStream.cs create mode 100644 MageClient/Module.cs create mode 100644 MageClient/Session.cs create mode 100644 MageClient/UserCommandStatus.cs diff --git a/MageClient/Archivist/Archivist.cs b/MageClient/Archivist/Archivist.cs new file mode 100644 index 0000000..d8ead75 --- /dev/null +++ b/MageClient/Archivist/Archivist.cs @@ -0,0 +1,578 @@ +using System; +using System.Collections.Generic; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Event; +using Wizcorp.MageSDK.Log; + +namespace Wizcorp.MageSDK.MageClient +{ + public class Archivist : EventEmitter + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("archivist"); } + } + + // Local cache of all retrieved vault values + private Dictionary cache = new Dictionary(); + + + // Constructor + public Archivist() + { + // Set data to vault value when set event received + Mage.EventManager.On("archivist:set", (sender, info) => { + var topic = (string)info["key"]["topic"]; + var index = (JObject)info["key"]["index"]; + JToken data = info["value"]["data"]; + var mediaType = (string)info["value"]["mediaType"]; + var expirationTime = (int?)info["expirationTime"]; + ValueSet(topic, index, data, mediaType, expirationTime); + }); + + // Del data inside vault value when del event received + Mage.EventManager.On("archivist:del", (sender, info) => { + var topic = (string)info["key"]["topic"]; + var index = (JObject)info["key"]["index"]; + ValueDel(topic, index); + }); + + // Touch vault value expiry when touch event received + Mage.EventManager.On("archivist:touch", (sender, info) => { + var topic = (string)info["key"]["topic"]; + var index = (JObject)info["key"]["index"]; + var expirationTime = (int?)info["expirationTime"]; + ValueTouch(topic, index, expirationTime); + }); + + // Apply changes to vault value when applyDiff event is received + Mage.EventManager.On("archivist:applyDiff", (sender, info) => { + var topic = (string)info["key"]["topic"]; + var index = (JObject)info["key"]["index"]; + var diff = (JArray)info["diff"]; + var expirationTime = (int?)info["expirationTime"]; + ValueApplyDiff(topic, index, diff, expirationTime); + }); + } + + + //////////////////////////////////////////// + // Cache Manipulation // + //////////////////////////////////////////// + + // Returns string id of a vault value for given topic and index + private static string CreateCacheKey(string topic, JObject index) + { + // Sort the keys so order of index is always the same + var indexKeys = new List(); + foreach (KeyValuePair property in index) + { + indexKeys.Add(property.Key); + } + indexKeys.Sort(); + + // Construct cache key list with correct ordering + var cacheKeys = new List(); + cacheKeys.Add(topic); + + foreach (string indexKey in indexKeys) + { + cacheKeys.Add(indexKey + "=" + index[indexKey]); + } + + // Join the cache key list into final key string + return string.Join(":", cacheKeys.ToArray()); + } + + + // Returns cache value if it exists and has not passed max allowed age + private VaultValue GetCacheValue(string cacheKeyName, int? maxAge = null) + { + lock ((object)cache) + { + if (!cache.ContainsKey(cacheKeyName)) + { + return null; + } + + VaultValue value = cache[cacheKeyName]; + double timespan = (DateTime.UtcNow - value.WrittenAt).TotalMilliseconds; + if (maxAge != null && timespan > maxAge * 1000) + { + return null; + } + + return value; + } + } + + + // Return cache dictionary + public Dictionary GetCache() + { + return cache; + } + + + // Clear out the cache entirely + public void ClearCache() + { + lock ((object)cache) + { + cache.Clear(); + } + } + + + // Remove a vault value from the cache by it's topic and index + public void DeleteCacheItem(string topic, JObject index) + { + DeleteCacheItem(CreateCacheKey(topic, index)); + } + + + // Remove a vault value from the cache by it's cache key name + public void DeleteCacheItem(string cacheKeyName) + { + lock ((object)cache) + { + Logger.Debug("Deleting cache item: " + cacheKeyName); + if (!cache.ContainsKey(cacheKeyName)) + { + return; + } + + cache.Remove(cacheKeyName); + } + } + + + //////////////////////////////////////////// + // Vault Value Manipulation // + //////////////////////////////////////////// + private void ValueSetOrDelete(JObject info) + { + var topic = (string)info["key"]["topic"]; + var index = (JObject)info["key"]["index"]; + var rawValue = (JObject)info["value"]; + + if (rawValue != null) + { + ValueSet(topic, index, rawValue["data"], (string)rawValue["mediaType"], (int?)info["expirationTime"]); + } + else + { + ValueDel(topic, index); + } + } + + private void ValueSet(string topic, JObject index, JToken data, string mediaType, int? expirationTime) + { + string cacheKeyName = CreateCacheKey(topic, index); + VaultValue cacheValue; + + // NOTE: even though some of these operations lock already, we put them inside this + // lock to ensure there is no time inconsistencies if things happen too fast. + lock ((object)cache) + { + cacheValue = GetCacheValue(cacheKeyName); + if (cacheValue == null) + { + // If it doesn't exist, create a new vault value + cacheValue = new VaultValue(topic, index); + cache.Add(cacheKeyName, cacheValue); + } + else + { + // If it exists delete existing value in preparation for set + cacheValue.Del(); + } + + // Set data to vault value + cacheValue.SetData(mediaType, data); + cacheValue.Touch(expirationTime); + } + + // Emit set event + Emit(topic + ":set", cacheValue); + } + + private void ValueAdd(string topic, JObject index, JToken data, string mediaType, int? expirationTime) + { + string cacheKeyName = CreateCacheKey(topic, index); + VaultValue cacheValue; + + // NOTE: even though some of these operations lock already, we put them inside this + // lock to ensure there is no time inconsistencies if things happen too fast. + lock ((object)cache) + { + // Check if value already exists + cacheValue = GetCacheValue(cacheKeyName); + if (cacheValue != null) + { + Logger.Error("Could not add value (already exists): " + cacheKeyName); + return; + } + + // Create new vault value + cacheValue = new VaultValue(topic, index); + cache.Add(cacheKeyName, cacheValue); + + // Set data to vault value + cacheValue.SetData(mediaType, data); + cacheValue.Touch(expirationTime); + } + + // Emit add event + Emit(topic + ":add", cacheValue); + } + + private void ValueDel(string topic, JObject index) + { + // Check if value already exists + string cacheKeyName = CreateCacheKey(topic, index); + VaultValue cacheValue = GetCacheValue(cacheKeyName); + if (cacheValue == null) + { + Logger.Warning("Could not delete value (doesn't exist): " + cacheKeyName); + return; + } + + // Do delete + cacheValue.Del(); + + // Emit touch event + Emit(topic + ":del", cacheValue); + } + + private void ValueTouch(string topic, JObject index, int? expirationTime) + { + // Check if value already exists + string cacheKeyName = CreateCacheKey(topic, index); + VaultValue cacheValue = GetCacheValue(cacheKeyName); + if (cacheValue == null) + { + Logger.Warning("Could not touch value (doesn't exist): " + cacheKeyName); + return; + } + + // Do touch + cacheValue.Touch(expirationTime); + + // Emit touch event + Emit(topic + ":touch", cacheValue); + } + + private void ValueApplyDiff(string topic, JObject index, JArray diff, int? expirationTime) + { + // Make sure value exists + string cacheKeyName = CreateCacheKey(topic, index); + VaultValue cacheValue = GetCacheValue(cacheKeyName); + if (cacheValue == null) + { + Logger.Warning("Got a diff for a non-existent value:" + cacheKeyName); + return; + } + + // Apply diff + cacheValue.ApplyDiff(diff); + cacheValue.Touch(expirationTime); + + // Emit applyDiff event + Emit(topic + ":applyDiff", cacheValue); + } + + + //////////////////////////////////////////// + // Raw Communication // + //////////////////////////////////////////// + private void RawGet(string topic, JObject index, Action cb) + { + var parameters = new JObject(); + parameters.Add("topic", topic); + parameters.Add("index", index); + + Mage.CommandCenter.SendCommand("archivist.rawGet", parameters, cb); + } + + private void RawMGet(JToken queries, JObject options, Action cb) + { + var parameters = new JObject(); + parameters.Add(new JProperty("queries", queries)); + parameters.Add("options", options); + + Mage.CommandCenter.SendCommand("archivist.rawMGet", parameters, cb); + } + + private void RawList(string topic, JObject partialIndex, JObject options, Action cb) + { + var parameters = new JObject(); + parameters.Add("topic", new JValue(topic)); + parameters.Add("partialIndex", partialIndex); + parameters.Add("options", options); + + Mage.CommandCenter.SendCommand("archivist.rawList", parameters, cb); + } + + private void RawSet(string topic, JObject index, JToken data, string mediaType, string encoding, string expirationTime, Action cb) + { + var parameters = new JObject(); + parameters.Add("topic", new JValue(topic)); + parameters.Add("index", index); + parameters.Add(new JProperty("data", data)); + parameters.Add("mediaType", new JValue(mediaType)); + parameters.Add("encoding", new JValue(encoding)); + parameters.Add("expirationTime", new JValue(expirationTime)); + + Mage.CommandCenter.SendCommand("archivist.rawSet", parameters, cb); + } + + private void RawDel(string topic, JObject index, Action cb) + { + var parameters = new JObject(); + parameters.Add("topic", new JValue(topic)); + parameters.Add("index", index); + + Mage.CommandCenter.SendCommand("archivist.rawDel", parameters, cb); + } + + + //////////////////////////////////////////// + // Exposed Operations // + //////////////////////////////////////////// + public void Get(string topic, JObject index, JObject options, Action cb) + { + // Default options + options = (options != null) ? options : new JObject(); + if (options["optional"] == null) + { + options.Add("optional", new JValue(false)); + } + + + // Check cache + string cacheKeyName = CreateCacheKey(topic, index); + VaultValue cacheValue = GetCacheValue(cacheKeyName, (int?)options["maxAge"]); + if (cacheValue != null) + { + cb(null, cacheValue.Data); + return; + } + + + // Get data from server + RawGet( + topic, + index, + (error, result) => { + if (error != null) + { + cb(error, null); + return; + } + + // Parse value + try + { + ValueSetOrDelete((JObject)result); + } + catch (Exception cacheError) + { + cb(cacheError, null); + return; + } + + // Return result + cb(null, GetCacheValue(cacheKeyName).Data); + }); + } + + public void Mget(JArray queries, JObject options, Action cb) + { + // Default options + options = (options != null) ? options : new JObject(); + if (options["optional"] == null) + { + options.Add("optional", new JValue(false)); + } + + + // Keep track of actual data we need from server + var realQueries = new JArray(); + var realQueryKeys = new Dictionary(); + var responseArray = new JArray(); + + + // Check cache + foreach (var jToken in queries) + { + var query = (JObject)jToken; + var topic = (string)query["topic"]; + var index = (JObject)query["index"]; + string cacheKeyName = CreateCacheKey(topic, index); + VaultValue cacheValue = GetCacheValue(cacheKeyName, (int?)options["maxAge"]); + if (cacheValue != null) + { + responseArray.Add(cacheValue.Data); + } + else + { + realQueryKeys.Add(cacheKeyName, responseArray.Count); + responseArray.Add(null); + realQueries.Add(query); + } + } + + + // Check if any real queries exist + if (realQueries.Count == 0) + { + cb(null, responseArray); + return; + } + + + // Get data from server + RawMGet( + realQueries, + options, + (error, results) => { + if (error != null) + { + cb(error, null); + return; + } + + try + { + var jTokens = results as JArray; + if (jTokens != null) + { + foreach (var jToken in jTokens) + { + var topicValue = (JObject)jToken; + + // Determine value cacheKeyName + var topic = (string)topicValue["key"]["topic"]; + var index = (JObject)topicValue["key"]["index"]; + string cacheKeyName = CreateCacheKey(topic, index); + + // Set value to cache + ValueSetOrDelete(topicValue); + + // Add value to response + int responseKey = realQueryKeys[cacheKeyName]; + responseArray[responseKey].Replace(GetCacheValue(cacheKeyName).Data); + } + } + } + catch (Exception cacheError) + { + cb(cacheError, null); + return; + } + + // Return result + cb(null, responseArray); + }); + } + + public void Mget(JObject queries, JObject options, Action cb) + { + // Default options + options = (options != null) ? options : new JObject(); + if (options["optional"] == null) + { + options.Add("optional", new JValue(false)); + } + + + // Keep track of actual data we need from server + var realQueries = new JObject(); + var realQueryKeys = new Dictionary(); + var responseObject = new JObject(); + + + // Check cache + foreach (KeyValuePair query in queries) + { + string cacheKeyName = CreateCacheKey(query.Value["topic"].ToString(), query.Value["index"] as JObject); + VaultValue cacheValue = GetCacheValue(cacheKeyName, (int?)options["maxAge"]); + if (cacheValue != null) + { + responseObject.Add(query.Key, cacheValue.Data); + } + else + { + realQueryKeys.Add(cacheKeyName, query.Key); + realQueries.Add(query.Key, query.Value); + } + } + + + // Check if any real queries exist + if (realQueries.Count == 0) + { + cb(null, responseObject); + return; + } + + + // Get data from server + RawMGet( + realQueries, + options, + (error, results) => { + if (error != null) + { + cb(error, null); + return; + } + + try + { + var jTokens = results as JArray; + if (jTokens != null) + { + foreach (var jToken in jTokens) + { + var topicValue = (JObject)jToken; + + // Determine value cacheKeyName + string valueTopic = topicValue["key"]["topic"].ToString(); + var valueIndex = (JObject)topicValue["key"]["index"]; + string cacheKeyName = CreateCacheKey(valueTopic, valueIndex); + + // Set value to cache + ValueSetOrDelete(topicValue); + + // Add value to response + string responseKey = realQueryKeys[cacheKeyName]; + responseObject.Add(responseKey, GetCacheValue(cacheKeyName).Data); + } + } + } + catch (Exception cacheError) + { + cb(cacheError, null); + return; + } + + // Return result + cb(null, responseObject); + }); + } + + public void List(string topic, JObject partialIndex, JObject options, Action cb) + { + RawList(topic, partialIndex, options, cb); + } + } +} \ No newline at end of file diff --git a/MageClient/Archivist/VaultValue.cs b/MageClient/Archivist/VaultValue.cs new file mode 100644 index 0000000..bd7beeb --- /dev/null +++ b/MageClient/Archivist/VaultValue.cs @@ -0,0 +1,96 @@ +using System; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Tomes; + +namespace Wizcorp.MageSDK.MageClient +{ + public class VaultValue + { + public string Topic { get; private set; } + + public JObject Index { get; private set; } + + public JToken Data { get; private set; } + + public string MediaType { get; private set; } + + public int? ExpirationTime { get; private set; } + + public DateTime WrittenAt { get; private set; } + + // + public VaultValue(string topic, JObject index) + { + Topic = topic; + Index = index; + } + + // TODO: implement multiple media-types and encoding + public void SetData(string mediaType, JToken data) + { + lock ((object)this) + { + // Detect media type + MediaType = mediaType; + + // Set data based on media type + Data = Tome.Conjure(JToken.Parse((string)data)); + + // Bump the last written time + WrittenAt = DateTime.UtcNow; + } + } + + // + public void Del() + { + lock ((object)this) + { + // Bump the last written time and check if we have data to destroy + WrittenAt = DateTime.UtcNow; + if (Data == null) + { + return; + } + + // Cleanup data + Tome.Destroy(Data); + Data = null; + MediaType = null; + + // Clear expiration time + Touch(null); + } + } + + // TODO: the actual implementation of this requires the MAGE time module, + // also we have a timer to clear the value once expired. + public void Touch(int? expirationTime) + { + lock ((object)this) + { + ExpirationTime = expirationTime; + } + } + + // + public void ApplyDiff(JArray diff) + { + lock ((object)this) + { + if (diff == null || Data == null) + { + return; + } + + // Apply diff to data + Tome.ApplyDiff(Data, diff); + + // Bump the last written time + WrittenAt = DateTime.UtcNow; + } + } + } +} \ No newline at end of file diff --git a/MageClient/Command/Client/CommandHttpClient.cs b/MageClient/Command/Client/CommandHttpClient.cs new file mode 100644 index 0000000..87ecb03 --- /dev/null +++ b/MageClient/Command/Client/CommandHttpClient.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.CommandCenter.Client; +using Wizcorp.MageSDK.Log; +using Wizcorp.MageSDK.MageClient; +using Wizcorp.MageSDK.MageClient.Command; +using Wizcorp.MageSDK.Network.Http; + +namespace Wizcorp.MageSDK.Command.Client +{ + public class CommandHttpClient : CommandTransportClient + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("CommandHTTPClient"); } + } + + // + private string endpoint; + private string password; + private string username; + + // + public override void SetEndpoint(string url, string app, string login = null, string pass = null) + { + endpoint = string.Format("{0}/{1}", url, app); + username = login; + password = pass; + } + + // + public override void SendBatch(CommandBatch commandBatch) + { + var commands = new List(); + var data = new List(); + + // Attach batch headers to post data + var batchHeader = new JArray(); + string sessionKey = Mage.Session.GetSessionKey(); + if (!string.IsNullOrEmpty(sessionKey)) + { + var sessionHeader = new JObject(); + sessionHeader.Add("name", new JValue("mage.session")); + sessionHeader.Add("key", new JValue(sessionKey)); + batchHeader.Add(sessionHeader); + } + data.Add(batchHeader.ToString(Newtonsoft.Json.Formatting.None)); + + // Attach command names to url and parameters to post data + for (var batchId = 0; batchId < commandBatch.BatchItems.Count; batchId += 1) + { + CommandBatchItem commandItem = commandBatch.BatchItems[batchId]; + commands.Add(commandItem.CommandName); + data.Add(commandItem.Parameters.ToString(Newtonsoft.Json.Formatting.None)); + Logger.Data(commandItem.Parameters).Verbose("sending command: " + commandItem.CommandName); + } + + // Serialise post data + string batchUrl = endpoint + "/" + String.Join(",", commands.ToArray()) + "?queryId=" + commandBatch.QueryId.ToString(); + string postData = string.Join("\n", data.ToArray()); + + // Send HTTP request + SendRequest( + batchUrl, + postData, + responseArray => { + // Process each command response + try + { + for (var batchId = 0; batchId < responseArray.Count; batchId += 1) + { + var commandResponse = responseArray[batchId] as JArray; + CommandBatchItem commandItem = commandBatch.BatchItems[batchId]; + string commandName = commandItem.CommandName; + Action commandCb = commandItem.Cb; + + // Check if there are any events attached to this request + if (commandResponse != null && commandResponse.Count >= 3) + { + Logger.Verbose("[" + commandName + "] processing events"); + Mage.EventManager.EmitEventList((JArray)commandResponse[2]); + } + + // Check if the response was an error + if (commandResponse != null && commandResponse[0].Type != JTokenType.Null) + { + Logger.Verbose("[" + commandName + "] server error"); + commandCb(new Exception(commandResponse[0].ToString()), null); + return; + } + + // Pull off call result object, if it doesn't exist + Logger.Verbose("[" + commandName + "] call response"); + if (commandResponse != null) + { + commandCb(null, commandResponse[1]); + } + } + } + catch (Exception error) + { + Logger.Data(error).Error("Error when processing command batch responses"); + } + }); + } + + private void SendRequest(string batchUrl, string postData, Action cb) + { + var headers = new Dictionary(); + if (username != null && password != null) + { + string authInfo = username + ":" + password; + string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + headers.Add("Authorization", "Basic " + encodedAuth); + } + + HttpRequest.Post( + batchUrl, + "", + postData, + headers, + Mage.Cookies, + (requestError, responseString) => { + Logger.Verbose("Recieved response: " + responseString); + + // Check if there was a transport error + if (requestError != null) + { + var error = "network"; + if (requestError is WebException) + { + var webException = requestError as WebException; + var webResponse = webException.Response as HttpWebResponse; + if (webResponse != null && webResponse.StatusCode == HttpStatusCode.ServiceUnavailable) + { + error = "maintenance"; + } + } + + OnTransportError.Invoke(error, requestError); + return; + } + + // Parse reponse array + JArray responseArray; + try + { + responseArray = JArray.Parse(responseString); + } + catch (Exception parseError) + { + OnTransportError.Invoke("parse", parseError); + return; + } + + // Let CommandCenter know this batch was successful + OnSendComplete.Invoke(); + + // Return array for processing + cb(responseArray); + }); + } + } +} \ No newline at end of file diff --git a/MageClient/Command/Client/CommandJsonrpcClient.cs b/MageClient/Command/Client/CommandJsonrpcClient.cs new file mode 100644 index 0000000..42186e5 --- /dev/null +++ b/MageClient/Command/Client/CommandJsonrpcClient.cs @@ -0,0 +1,94 @@ +using System; + +using Wizcorp.MageSDK.CommandCenter.Client; +using Wizcorp.MageSDK.Log; +using Wizcorp.MageSDK.MageClient; +using Wizcorp.MageSDK.MageClient.Command; +using Wizcorp.MageSDK.Network.JsonRpc; + +namespace Wizcorp.MageSDK.Command.Client +{ + public class CommandJsonrpcClient : CommandTransportClient + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("CommandJSONRPCClient"); } + } + + private Jsonrpc rpcClient = new Jsonrpc(); + + // + public override void SetEndpoint(string url, string app, string login = null, string pass = null) + { + string baseUrl = string.Format("{0}/{1}/jsonrpc", url, app); + rpcClient.SetEndpoint(baseUrl, login, pass); + } + + // + public override void SendBatch(CommandBatch commandBatch) + { + // NOTE: This transport client cannot be implemented yet as JSON RPC support is + // terminally broken in MAGE (does not support queryId and response caching). + // Until this is fixed, this transport client cannot be used or completed. + Logger.Verbose("THIS TRANSPORT CLIENT IS NOT IMPLEMENTED"); + throw new Exception("THIS TRANSPORT CLIENT IS NOT IMPLEMENTED"); + + /* + JSONRPCBatch rpcBatch = new JSONRPCBatch(); + for (int batchId = 0; batchId < commandBatch.batchItems.Count; batchId += 1) { + CommandBatchItem commandItem = commandBatch.batchItems[batchId]; + rpcBatch.Add(batchId, commandItem.commandName, commandItem.parameters); + logger.data(commandItem.parameters).verbose("[" + commandItem.commandName + "] executing command"); + } + + // Attach any required mage headers + Dictionary headers = new Dictionary(); + + string sessionKey = mage.session.GetSessionKey(); + if (!string.IsNullOrEmpty(sessionKey)) { + headers.Add("X-MAGE-SESSION", sessionKey); + } + + // Send rpc batch + rpcClient.CallBatch(rpcBatch, headers, mage.cookies, (Exception error, JArray responseArray) => { + logger.data(responseArray).verbose("Recieved response: "); + + if (error != null) { + //TODO: OnTransportError.Invoke("", error); + return; + } + + // Process each command response + foreach (JObject responseObject in responseArray) { + int batchId = (int)responseObject["id"]; + CommandBatchItem commandItem = commandBatch.batchItems[batchId]; + string commandName = commandItem.commandName; + Action commandCb = commandItem.cb; + + // Check if there are any events attached to this request + if (responseObject["result"]["myEvents"] != null) { + logger.verbose("[" + commandName + "] processing events"); + mage.eventManager.emitEventList((JArray)responseObject["result"]["myEvents"]); + } + + // Check if the response was an error + if (responseObject["result"]["errorCode"] != null) { + logger.verbose("[" + commandName + "] server error"); + commandCb(new Exception(responseObject["result"]["errorCode"].ToString()), null); + return; + } + + // Pull off call result object, if it doesn't exist + logger.verbose("[" + commandName + "] call response"); + commandCb(null, responseObject["result"]["response"]); + } + }); + */ + } + } +} \ No newline at end of file diff --git a/MageClient/Command/Client/CommandTransportClient.cs b/MageClient/Command/Client/CommandTransportClient.cs new file mode 100644 index 0000000..ac3bd71 --- /dev/null +++ b/MageClient/Command/Client/CommandTransportClient.cs @@ -0,0 +1,14 @@ +using System; + +using Wizcorp.MageSDK.MageClient.Command; + +namespace Wizcorp.MageSDK.CommandCenter.Client +{ + public abstract class CommandTransportClient + { + public Action OnSendComplete; + public Action OnTransportError; + public abstract void SetEndpoint(string baseUrl, string appName, string username = null, string password = null); + public abstract void SendBatch(CommandBatch batch); + } +} \ No newline at end of file diff --git a/MageClient/Command/Client/CommandTransportType.cs b/MageClient/Command/Client/CommandTransportType.cs new file mode 100644 index 0000000..27b26ba --- /dev/null +++ b/MageClient/Command/Client/CommandTransportType.cs @@ -0,0 +1,8 @@ +namespace Wizcorp.MageSDK.Command.Client +{ + public enum CommandTransportType + { + HTTP, + JSONRPC + } +} \ No newline at end of file diff --git a/MageClient/Command/CommandBatch.cs b/MageClient/Command/CommandBatch.cs new file mode 100644 index 0000000..e01f541 --- /dev/null +++ b/MageClient/Command/CommandBatch.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +using Newtonsoft.Json.Linq; + +namespace Wizcorp.MageSDK.MageClient.Command +{ + public class CommandBatch + { + public List BatchItems = new List(); + public int QueryId; + + public CommandBatch(int queryId) + { + QueryId = queryId; + } + + public void Queue(string commandName, JObject parameters, Action cb) + { + BatchItems.Add(new CommandBatchItem(commandName, parameters, cb)); + } + } +} \ No newline at end of file diff --git a/MageClient/Command/CommandBatchItem.cs b/MageClient/Command/CommandBatchItem.cs new file mode 100644 index 0000000..79df2c4 --- /dev/null +++ b/MageClient/Command/CommandBatchItem.cs @@ -0,0 +1,20 @@ +using System; + +using Newtonsoft.Json.Linq; + +namespace Wizcorp.MageSDK.MageClient.Command +{ + public class CommandBatchItem + { + public Action Cb; + public string CommandName; + public JObject Parameters; + + public CommandBatchItem(string commandName, JObject parameters, Action cb) + { + CommandName = commandName; + Parameters = parameters; + Cb = cb; + } + } +} \ No newline at end of file diff --git a/MageClient/Command/CommandCenter.cs b/MageClient/Command/CommandCenter.cs new file mode 100644 index 0000000..e060882 --- /dev/null +++ b/MageClient/Command/CommandCenter.cs @@ -0,0 +1,182 @@ +using System; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Command.Client; +using Wizcorp.MageSDK.CommandCenter.Client; +using Wizcorp.MageSDK.Log; + +namespace Wizcorp.MageSDK.MageClient.Command +{ + public class CommandCenter + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("CommandCenter"); } + } + + private string appName; + + // Endpoint and credentials + private string baseUrl; + private CommandBatch currentBatch; + + // Command Batches + private int nextQueryId = 1; + private string password; + private CommandBatch sendingBatch; + + // Current transport client + private CommandTransportClient transportClient; + private string username; + + // + public CommandCenter(CommandTransportType transportType = CommandTransportType.HTTP) + { + currentBatch = new CommandBatch(nextQueryId++); + SetTransport(transportType); + } + + // + public void SetTransport(CommandTransportType transportType) + { + // Cleanup existing transport client + transportClient = null; + + // Create new transport client instance + if (transportType == CommandTransportType.HTTP) + { + transportClient = new CommandHttpClient(); + transportClient.SetEndpoint(baseUrl, appName, username, password); + } + else if (transportType == CommandTransportType.JSONRPC) + { + transportClient = new CommandJsonrpcClient(); + transportClient.SetEndpoint(baseUrl, appName, username, password); + } + else + { + throw new Exception("Invalid transport type: " + transportType); + } + + // Setup event handlers + transportClient.OnSendComplete += BatchComplete; + transportClient.OnTransportError += TransportError; + } + + // + public void SetEndpoint(string url, string app, string login = null, string pass = null) + { + baseUrl = url; + appName = app; + username = login; + password = pass; + + if (transportClient != null) + { + transportClient.SetEndpoint(baseUrl, appName, username, password); + } + } + + // + private void SendBatch() + { + Mage.EventManager.Emit("io.send", null); + + lock ((object)this) + { + // Swap batches around locking the queue + sendingBatch = currentBatch; + currentBatch = new CommandBatch(nextQueryId++); + + // Send the batch + Logger.Debug("Sending batch: " + sendingBatch.QueryId); + transportClient.SendBatch(sendingBatch); + } + } + + // Resend batch + public void Resend() + { + Mage.EventManager.Emit("io.resend", null); + + Logger.Debug("Re-sending batch: " + sendingBatch.QueryId); + transportClient.SendBatch(sendingBatch); + } + + // + private void BatchComplete() + { + Mage.EventManager.Emit("io.response", null); + + lock ((object)this) + { + sendingBatch = null; + + // Check if next queued batch should be sent as well + if (currentBatch.BatchItems.Count > 0) + { + SendBatch(); + } + } + } + + // + private void TransportError(string errorType, Exception error) + { + Logger.Data(error).Error("Error when sending command batch request '" + errorType + "'"); + Mage.EventManager.Emit("io.error." + errorType, null); + } + + // Try and send a command right away if there is nothing being sent. + public void SendCommand(string commandName, JObject parameters, Action cb) + { + lock ((object)this) + { + // Add command to queue + currentBatch.Queue(commandName, parameters, cb); + + // Check if we are busy and should only queue + if (sendingBatch != null) + { + return; + } + + // Otherwise send the batch + SendBatch(); + } + } + + // Either send this command immediately if nothing is being sent, + // otherwise queue it and send it after the current send is complete. + public void QueueCommand(string commandName, JObject parameters, Action cb) + { + lock ((object)this) + { + // If we are not sending anything, send immediately + if (sendingBatch == null) + { + SendCommand(commandName, parameters, cb); + return; + } + + // Otherwise queue it to current + currentBatch.Queue(commandName, parameters, cb); + } + } + + // Queue command to current batch and wait for it to get processed by another command + public void PiggyBackCommand(string commandName, JObject parameters, Action cb) + { + lock ((object)this) + { + currentBatch.Queue(commandName, parameters, cb); + } + } + } +} \ No newline at end of file diff --git a/MageClient/EventManager.cs b/MageClient/EventManager.cs new file mode 100644 index 0000000..13a310c --- /dev/null +++ b/MageClient/EventManager.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Event; +using Wizcorp.MageSDK.Log; + +namespace Wizcorp.MageSDK.MageClient +{ + public class EventManager : EventEmitter + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("eventManager"); } + } + + public void EmitEventList(JArray events) + { + foreach (JToken responseEvent in events) + { + string eventTag = null; + JToken eventData = null; + + // Copy the eventItem for processing + JArray eventItem = JArray.Parse(responseEvent.ToString()); + + // Check that event name is present + if (eventItem.Count >= 1) + { + eventTag = eventItem[0].ToString(); + } + + // Check if any event data is present + if (eventItem.Count == 2) + { + eventData = eventItem[1]; + } + + // Check if there were any errors, log and skip them + if (eventTag == null || eventItem.Count > 2) + { + Logger.Data(eventItem).Error("Invalid event format:"); + continue; + } + + // Emit the event + Logger.Debug("Emitting '" + eventTag + "'"); + Mage.EventManager.Emit(eventTag, eventData); + } + } + } +} diff --git a/MageClient/Log/LogEntry.cs b/MageClient/Log/LogEntry.cs new file mode 100644 index 0000000..11d3087 --- /dev/null +++ b/MageClient/Log/LogEntry.cs @@ -0,0 +1,72 @@ +namespace Wizcorp.MageSDK.Log +{ + public class LogEntry + { + // + public string Context; + public object Data; + public string Message; + + public LogEntry(string context, object data = null) + { + Context = context; + Data = data; + } + + + // + public void Verbose(string message) + { + Message = message; + Logger.LogEmitter.Emit("verbose", this); + } + + public void Debug(string message) + { + Message = message; + Logger.LogEmitter.Emit("debug", this); + } + + public void Info(string message) + { + Message = message; + Logger.LogEmitter.Emit("info", this); + } + + public void Notice(string message) + { + Message = message; + Logger.LogEmitter.Emit("notice", this); + } + + public void Warning(string message) + { + Message = message; + Logger.LogEmitter.Emit("warning", this); + } + + public void Error(string message) + { + Message = message; + Logger.LogEmitter.Emit("error", this); + } + + public void Critical(string message) + { + Message = message; + Logger.LogEmitter.Emit("critical", this); + } + + public void Alert(string message) + { + Message = message; + Logger.LogEmitter.Emit("alert", this); + } + + public void Emergency(string message) + { + Message = message; + Logger.LogEmitter.Emit("emergency", this); + } + } +} \ No newline at end of file diff --git a/MageClient/Log/Logger.cs b/MageClient/Log/Logger.cs new file mode 100644 index 0000000..42bff0e --- /dev/null +++ b/MageClient/Log/Logger.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; + +using Wizcorp.MageSDK.Event; +using Wizcorp.MageSDK.Log.Writers; + +namespace Wizcorp.MageSDK.Log +{ + public class Logger + { + // + public static EventEmitter LogEmitter = new EventEmitter(); + + // + public static Dictionary LogWriters; + + // + private string context; + + public Logger(string context) + { + this.context = context; + } + + public static void SetConfig(Dictionary> config) + { + // Destroy existing log writers + if (LogWriters != null) + { + foreach (LogWriter writer in LogWriters.Values) + { + writer.Dispose(); + } + LogWriters = null; + } + + // Make sure we have configured something + if (config == null) + { + return; + } + + // Create each writer with log levels + LogWriters = new Dictionary(); + foreach (KeyValuePair> property in config) + { + string writer = property.Key; + List writerConfig = property.Value; + + switch (writer) + { + case "console": + LogWriters.Add(writer, new ConsoleWriter(writerConfig)); + break; + case "server": + LogWriters.Add(writer, new ServerWriter(writerConfig)); + break; + default: + throw new Exception("Unknown Log Writer: " + writer); + } + } + } + + + // + public LogEntry Data(object data) + { + return new LogEntry(context, data); + } + + + // + public void Verbose(string message) + { + (new LogEntry(context)).Verbose(message); + } + + public void Debug(string message) + { + (new LogEntry(context)).Debug(message); + } + + public void Info(string message) + { + (new LogEntry(context)).Info(message); + } + + public void Notice(string message) + { + (new LogEntry(context)).Notice(message); + } + + public void Warning(string message) + { + (new LogEntry(context)).Warning(message); + } + + public void Error(string message) + { + (new LogEntry(context)).Error(message); + } + + public void Critical(string message) + { + (new LogEntry(context)).Critical(message); + } + + public void Alert(string message) + { + (new LogEntry(context)).Alert(message); + } + + public void Emergency(string message) + { + (new LogEntry(context)).Emergency(message); + } + } +} \ No newline at end of file diff --git a/MageClient/Log/Writers/ConsoleWriter.cs b/MageClient/Log/Writers/ConsoleWriter.cs new file mode 100644 index 0000000..6fb3fb3 --- /dev/null +++ b/MageClient/Log/Writers/ConsoleWriter.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; + +namespace Wizcorp.MageSDK.Log.Writers +{ + public class ConsoleWriter : LogWriter + { + private List config; + + public ConsoleWriter(List logLevels) + { + config = logLevels; + + if (config.Contains("verbose")) + { + Logger.LogEmitter.On("verbose", Verbose); + } + + if (config.Contains("debug")) + { + Logger.LogEmitter.On("debug", Debug); + } + + if (config.Contains("info")) + { + Logger.LogEmitter.On("info", Info); + } + + if (config.Contains("notice")) + { + Logger.LogEmitter.On("notice", Notice); + } + + if (config.Contains("warning")) + { + Logger.LogEmitter.On("warning", Warning); + } + + if (config.Contains("error")) + { + Logger.LogEmitter.On("error", Error); + } + + if (config.Contains("critical")) + { + Logger.LogEmitter.On("critical", Critical); + } + + if (config.Contains("alert")) + { + Logger.LogEmitter.On("alert", Alert); + } + + if (config.Contains("emergency")) + { + Logger.LogEmitter.On("emergency", Emergency); + } + } + + public override void Dispose() + { + if (config.Contains("verbose")) + { + Logger.LogEmitter.Off("verbose", Verbose); + } + + if (config.Contains("debug")) + { + Logger.LogEmitter.Off("debug", Debug); + } + + if (config.Contains("info")) + { + Logger.LogEmitter.Off("info", Info); + } + + if (config.Contains("notice")) + { + Logger.LogEmitter.Off("notice", Notice); + } + + if (config.Contains("warning")) + { + Logger.LogEmitter.Off("warning", Warning); + } + + if (config.Contains("error")) + { + Logger.LogEmitter.Off("error", Error); + } + + if (config.Contains("critical")) + { + Logger.LogEmitter.Off("critical", Critical); + } + + if (config.Contains("alert")) + { + Logger.LogEmitter.Off("alert", Alert); + } + + if (config.Contains("emergency")) + { + Logger.LogEmitter.Off("emergency", Emergency); + } + } + + private string makeLogString(string channel, string context, string message) + { + return String.Format("[{0}] [{1}] {2}", channel, context, message); + } + + private void Verbose(object sender, LogEntry logEntry) + { + UnityEngine.Debug.Log(makeLogString("verbose", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.Log(logEntry.Data); + } + } + + private void Debug(object sender, LogEntry logEntry) + { + UnityEngine.Debug.Log(makeLogString("debug", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.Log(logEntry.Data); + } + } + + private void Info(object sender, LogEntry logEntry) + { + UnityEngine.Debug.Log(makeLogString("info", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.Log(logEntry.Data); + } + } + + private void Notice(object sender, LogEntry logEntry) + { + UnityEngine.Debug.Log(makeLogString("notice", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.Log(logEntry.Data); + } + } + + private void Warning(object sender, LogEntry logEntry) + { + UnityEngine.Debug.LogWarning(makeLogString("warning", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.LogWarning(logEntry.Data); + } + } + + private void Error(object sender, LogEntry logEntry) + { + UnityEngine.Debug.LogError(makeLogString("error", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.LogError(logEntry.Data); + } + } + + private void Critical(object sender, LogEntry logEntry) + { + UnityEngine.Debug.LogError(makeLogString("critical", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + if (logEntry.Data is Exception && (logEntry.Data as Exception).StackTrace != null) + { + var excpt = logEntry.Data as Exception; + UnityEngine.Debug.LogError(excpt + ":\n" + excpt.StackTrace); + } + else + { + UnityEngine.Debug.LogError(logEntry.Data); + } + } + } + + private void Alert(object sender, LogEntry logEntry) + { + UnityEngine.Debug.LogError(makeLogString("alert", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.LogError(logEntry.Data); + } + } + + private void Emergency(object sender, LogEntry logEntry) + { + UnityEngine.Debug.LogError(makeLogString("emergency", logEntry.Context, logEntry.Message)); + if (logEntry.Data != null) + { + UnityEngine.Debug.LogError(logEntry.Data); + } + } + } +} \ No newline at end of file diff --git a/MageClient/Log/Writers/LogWriter.cs b/MageClient/Log/Writers/LogWriter.cs new file mode 100644 index 0000000..4634c04 --- /dev/null +++ b/MageClient/Log/Writers/LogWriter.cs @@ -0,0 +1,7 @@ +namespace Wizcorp.MageSDK.Log.Writers +{ + public abstract class LogWriter + { + public abstract void Dispose(); + } +} \ No newline at end of file diff --git a/MageClient/Log/Writers/ServerWriter.cs b/MageClient/Log/Writers/ServerWriter.cs new file mode 100644 index 0000000..69e4cbd --- /dev/null +++ b/MageClient/Log/Writers/ServerWriter.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Wizcorp.MageSDK.Log.Writers +{ + public class ServerWriter : LogWriter + { + //private List config; + + public ServerWriter(List logLevels) + { + //config = logLevels; + } + + public override void Dispose() {} + } +} \ No newline at end of file diff --git a/MageClient/Mage.cs b/MageClient/Mage.cs new file mode 100644 index 0000000..ee84397 --- /dev/null +++ b/MageClient/Mage.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Net; + +using Wizcorp.MageSDK.Log; +using Wizcorp.MageSDK.MageClient.Message; +using Wizcorp.MageSDK.Network.Http; +using Wizcorp.MageSDK.Utils; + +namespace Wizcorp.MageSDK.MageClient +{ + public class Mage : Singleton + { + private string appName; + public Archivist Archivist; + + // + private string baseUrl; + public Command.CommandCenter CommandCenter; + public CookieContainer Cookies; + + // + public EventManager EventManager; + private Logger logger; + + // + private Dictionary loggers = new Dictionary(); + public MessageStream MessageStream; + private string password; + public Session Session; + private string username; + + + // Avoid putting setup logic in the contstuctor. Only things that can be + // carried between game sessions should go here. Otherwise we need to be + // able to re-initialize them inside the setup function. + public Mage() + { + // Setup log writters + logger = Logger("mage"); + + // TODO: properly check the damn certificate, for now ignore invalid ones (fix issue on Android/iOS) + ServicePointManager.ServerCertificateValidationCallback += (o, cert, chain, errors) => true; + } + + public Logger Logger(string context = null) + { + if (string.IsNullOrEmpty(context)) + { + context = "Default"; + } + + if (loggers.ContainsKey(context)) + { + return loggers[context]; + } + + var newLogger = new Logger(context); + loggers.Add(context, new Logger(context)); + return newLogger; + } + + // + public void SetEndpoints(string url, string app, string login = null, string pass = null) + { + baseUrl = url; + appName = app; + username = login; + password = pass; + + if (CommandCenter != null) + { + CommandCenter.SetEndpoint(baseUrl, appName, login, password); + } + + if (MessageStream != null) + { + MessageStream.SetEndpoint(baseUrl, login, password); + } + } + + // + public void Setup(Action cb) + { + // Cleanup any existing internal modules + if (MessageStream != null) + { + MessageStream.Dispose(); + } + + + // Instantiate HTTPRequestManager + HttpRequestManager.Instantiate(); + + // Create a shared cookie container + Cookies = new CookieContainer(); + + // Initialize mage internal modules + EventManager = new EventManager(); + Session = new Session(); + CommandCenter = new Command.CommandCenter(); + MessageStream = new MessageStream(); + Archivist = new Archivist(); + + // Set endpoints + CommandCenter.SetEndpoint(baseUrl, appName, username, password); + MessageStream.SetEndpoint(baseUrl, username, password); + + cb(null); + } + + // + public void SetupModules(List moduleNames, Action cb) + { + // Setup application modules + logger.Info("Setting up modules"); + Async.Each( + moduleNames, + (moduleName, callback) => { + logger.Info("Setting up module: " + moduleName); + + // Use reflection to find module by name + Assembly assembly = Assembly.GetExecutingAssembly(); + Type[] assemblyTypes = assembly.GetTypes(); + foreach (Type t in assemblyTypes) + { + if (moduleName == t.Name) + { + BindingFlags staticProperty = BindingFlags.Static | BindingFlags.GetProperty; + BindingFlags publicMethod = BindingFlags.Public | BindingFlags.InvokeMethod; + + // Grab module instance from singleton base + Type singletonType = typeof(Singleton<>).MakeGenericType(t); + Object instance = singletonType.InvokeMember("Instance", staticProperty, null, null, null); + + // Setup module + Type moduleType = typeof(Module<>).MakeGenericType(t); + Type t1 = t; + Async.Series( + new List>>() { + callbackInner => { + // Setup module user commands + var arguments = new object[] { + callbackInner + }; + moduleType.InvokeMember("SetupUsercommands", publicMethod, null, instance, arguments); + }, + callbackInner => { + // Setup module static data + var arguments = new object[] { + callbackInner + }; + moduleType.InvokeMember("SetupStaticData", publicMethod, null, instance, arguments); + } + }, + error => { + if (error != null) + { + callback(error); + return; + } + + // Check if the module has a setup method + if (t1.GetMethod("Setup") == null) + { + Logger(moduleName).Info("No setup function"); + callback(null); + return; + } + + // Invoke the setup method on the module + Logger(moduleName).Info("Executing setup function"); + var arguments = new object[] { + callback + }; + t1.InvokeMember("Setup", publicMethod, null, instance, arguments); + }); + + return; + } + } + + // If nothing found throw an error + callback(new Exception("Can't find module " + moduleName)); + }, + error => { + if (error != null) + { + logger.Data(error).Error("Setup failed!"); + cb(error); + return; + } + + logger.Info("Setup complete"); + cb(null); + }); + } + + + // + public IEnumerator SetupTask(Action cb) + { + // Execute async setup function + var setupStatus = new MageSetupStatus(); + Setup(error => { + setupStatus.Error = error; + setupStatus.Done = true; + }); + + // Wait for setup to return + while (!setupStatus.Done) + { + yield return null; + } + + // Call callback with error if there is one + cb(setupStatus.Error); + } + + // + public IEnumerator SetupModulesTask(List moduleNames, Action cb) + { + // Execute async setup function + var setupStatus = new MageSetupStatus(); + SetupModules(moduleNames, error => { + setupStatus.Error = error; + setupStatus.Done = true; + }); + + // Wait for setup to return + while (!setupStatus.Done) + { + yield return null; + } + + // Call callback with error if there is one + cb(setupStatus.Error); + } + } +} \ No newline at end of file diff --git a/MageClient/MageSetupStatus.cs b/MageClient/MageSetupStatus.cs new file mode 100644 index 0000000..68f821d --- /dev/null +++ b/MageClient/MageSetupStatus.cs @@ -0,0 +1,10 @@ +using System; + +namespace Wizcorp.MageSDK.MageClient +{ + public class MageSetupStatus + { + public bool Done = false; + public Exception Error = null; + } +} diff --git a/MageClient/Message/Client/LongPolling.cs b/MageClient/Message/Client/LongPolling.cs new file mode 100644 index 0000000..6d01b12 --- /dev/null +++ b/MageClient/Message/Client/LongPolling.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +using Wizcorp.MageSDK.Log; +using Wizcorp.MageSDK.Network.Http; + +namespace Wizcorp.MageSDK.MageClient.Message.Client +{ + public class LongPolling : TransportClient + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("longpolling"); } + } + + // Whether or not the poller is working + private bool running; + + // Required functions for poll requests + private Func getEndpoint; + private Func> getHeaders; + private Action processMessages; + + // + HttpRequest currentRequest; + + // Required interval timer for polling delay + private int errorInterval; + private Timer intervalTimer; + + + // Constructor + public LongPolling(Func getEndpointFn, Func> getHeadersFn, Action processMessagesFn, int errorInterval = 5000) + { + getEndpoint = getEndpointFn; + getHeaders = getHeadersFn; + processMessages = processMessagesFn; + this.errorInterval = errorInterval; + } + + + // Starts the poller + public override void Start() + { + if (running) + { + return; + } + + Logger.Debug("Starting"); + running = true; + RequestLoop(); + } + + + // Stops the poller + public override void Stop() + { + running = false; + Logger.Debug("Stopping..."); + + if (intervalTimer != null) + { + intervalTimer.Dispose(); + intervalTimer = null; + } + else + { + Logger.Debug("Timer Stopped"); + } + + if (currentRequest != null) + { + currentRequest.Abort(); + currentRequest = null; + } + else + { + Logger.Debug("Connections Stopped"); + } + } + + + // Queues the next poll request + private void QueueNextRequest(int waitFor) + { + // Wait _requestInterval milliseconds till next poll + intervalTimer = new Timer( + state => { + RequestLoop(); + }, + null, + waitFor, + Timeout.Infinite); + } + + + // Poller request function + private void RequestLoop() + { + // Clear the timer + if (intervalTimer != null) + { + intervalTimer.Dispose(); + intervalTimer = null; + } + + // Check if the poller should be running + if (running == false) + { + Logger.Debug("Stopped"); + return; + } + + // Send poll request and wait for a response + string endpoint = getEndpoint(); + Logger.Debug("Sending request: " + endpoint); + currentRequest = HttpRequest.Get( + endpoint, + getHeaders(), + Mage.Cookies, + (requestError, responseString) => { + currentRequest = null; + + // Ignore errors if we have been stopped + if (requestError != null && !running) + { + Logger.Debug("Stopped"); + return; + } + + if (requestError != null) + { + if (requestError is HttpRequestException) + { + // Only log web exceptions if they aren't an empty response or gateway timeout + HttpRequestException requestException = requestError as HttpRequestException; + if (requestException.Status != 0 && requestException.Status != 504) + { + Logger.Error("(" + requestException.Status.ToString() + ") " + requestError.Message); + } + } + else + { + Logger.Error(requestError.ToString()); + } + + QueueNextRequest(errorInterval); + return; + } + + // Call the message processer hook and re-call request loop function + try + { + Logger.Verbose("Recieved response: " + responseString); + if (responseString != null) + { + processMessages(responseString); + } + + RequestLoop(); + } + catch (Exception error) + { + Logger.Error(error.ToString()); + QueueNextRequest(errorInterval); + } + }); + } + } +} \ No newline at end of file diff --git a/MageClient/Message/Client/ShortPolling.cs b/MageClient/Message/Client/ShortPolling.cs new file mode 100644 index 0000000..7e984f8 --- /dev/null +++ b/MageClient/Message/Client/ShortPolling.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +using Wizcorp.MageSDK.Log; +using Wizcorp.MageSDK.Network.Http; + +namespace Wizcorp.MageSDK.MageClient.Message.Client +{ + public class ShortPolling : TransportClient + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("shortpolling"); } + } + + // Whether or not the poller is working + private bool running; + + // Required functions for poll requests + private Func getEndpoint; + private Func> getHeaders; + private Action processMessages; + + // Required interval timer for polling delay + private int requestInterval; + private int errorInterval; + private Timer intervalTimer; + + + // Constructor + public ShortPolling(Func getEndpointFn, Func> getHeadersFn, Action processMessagesFn, int requestInterval = 5000, int errorInterval = 5000) + { + getEndpoint = getEndpointFn; + getHeaders = getHeadersFn; + processMessages = processMessagesFn; + this.requestInterval = requestInterval; + this.errorInterval = errorInterval; + } + + + // Starts the poller + public override void Start() + { + if (running) + { + return; + } + + Logger.Debug("Starting"); + running = true; + RequestLoop(); + } + + + // Stops the poller + public override void Stop() + { + if (intervalTimer != null) + { + intervalTimer.Dispose(); + intervalTimer = null; + Logger.Debug("Stopped"); + } + else + { + Logger.Debug("Stopping..."); + } + running = false; + } + + + // Queues the next poll request + private void QueueNextRequest(int waitFor) + { + // Wait _requestInterval milliseconds till next poll + intervalTimer = new Timer( + state => { + RequestLoop(); + }, + null, + waitFor, + Timeout.Infinite); + } + + + // Poller request function + private void RequestLoop() + { + // Clear the timer + if (intervalTimer != null) + { + intervalTimer.Dispose(); + intervalTimer = null; + } + + // Check if the poller should be running + if (running == false) + { + Logger.Debug("Stopped"); + return; + } + + // Send poll request and wait for a response + string endpoint = getEndpoint(); + Logger.Debug("Sending request: " + endpoint); + HttpRequest.Get( + endpoint, + getHeaders(), + Mage.Cookies, + (requestError, responseString) => { + if (requestError != null) + { + Logger.Error(requestError.ToString()); + QueueNextRequest(errorInterval); + return; + } + + // Call the message processer hook and queue the next request + try + { + processMessages(responseString); + QueueNextRequest(requestInterval); + } + catch (Exception error) + { + Logger.Data(responseString).Error(error.ToString()); + QueueNextRequest(errorInterval); + } + }); + } + } +} \ No newline at end of file diff --git a/MageClient/Message/Client/TransportClient.cs b/MageClient/Message/Client/TransportClient.cs new file mode 100644 index 0000000..f78f526 --- /dev/null +++ b/MageClient/Message/Client/TransportClient.cs @@ -0,0 +1,8 @@ +namespace Wizcorp.MageSDK.MageClient.Message.Client +{ + public abstract class TransportClient + { + public abstract void Stop(); + public abstract void Start(); + } +} \ No newline at end of file diff --git a/MageClient/Message/Client/TransportType.cs b/MageClient/Message/Client/TransportType.cs new file mode 100644 index 0000000..39d907c --- /dev/null +++ b/MageClient/Message/Client/TransportType.cs @@ -0,0 +1,8 @@ +namespace Wizcorp.MageSDK.MageClient.Message.Client +{ + public enum TransportType + { + SHORTPOLLING, + LONGPOLLING + } +} diff --git a/MageClient/Message/MessageStream.cs b/MageClient/Message/MessageStream.cs new file mode 100644 index 0000000..2589ae8 --- /dev/null +++ b/MageClient/Message/MessageStream.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Log; +using Wizcorp.MageSDK.MageClient.Message.Client; + +#if UNITY_EDITOR +using Wizcorp.MageSDK.Editor; +#endif + +namespace Wizcorp.MageSDK.MageClient.Message +{ + public class MessageStream + { + private Mage Mage + { + get { return Mage.Instance; } + } + + private Logger Logger + { + get { return Mage.Logger("messagestream"); } + } + + private List confirmIds; + + // Current message stack + private int currentMessageId; + + // Endpoint and credentials + private string endpoint; + private int largestMessageId; + private Dictionary messageQueue; + private string password; + private string sessionKey; + + // Current transport client + private TransportClient transportClient; + private string username; + + + // Constructor + public MessageStream(TransportType transport = TransportType.LONGPOLLING) + { + // + InitializeMessageList(); + + // Start transport client when session is acquired + Mage.EventManager.On( + "session.set", + (sender, session) => { + sessionKey = UnityEngine.WWW.EscapeURL(session["key"].ToString()); + transportClient.Start(); + }); + + // Stop the message client when session is lost + Mage.EventManager.On( + "session.unset", + (sender, reason) => { + transportClient.Stop(); + InitializeMessageList(); + sessionKey = null; + }); + + // Also stop the message client when the editor is stopped + #if UNITY_EDITOR + UnityEditorPlayMode.OnEditorModeChanged += newState => { + if (newState == EditorPlayModeState.Stopped) + { + transportClient.Stop(); + InitializeMessageList(); + sessionKey = null; + } + }; + #endif + + // Set the selected transport client (or the default) + SetTransport(transport); + } + + + // + private void InitializeMessageList() + { + currentMessageId = -1; + largestMessageId = -1; + messageQueue = new Dictionary(); + confirmIds = new List(); + + Logger.Debug("Initialized message queue"); + } + + + // + public void Dispose() + { + // Stop the transport client if it exists + if (transportClient != null) + { + transportClient.Stop(); + } + + InitializeMessageList(); + sessionKey = null; + } + + + // Updates URI and credentials + public void SetEndpoint(string url, string login = null, string pass = null) + { + endpoint = url + "/msgstream"; + username = login; + password = pass; + } + + + // Sets up given transport client type + public void SetTransport(TransportType transport) + { + // Stop existing transport client if any, when nulled out it will be collected + // by garbage collecter after existing connections have been terminated. + if (transportClient != null) + { + transportClient.Stop(); + transportClient = null; + } + + // Create new transport client instance + if (transport == TransportType.SHORTPOLLING) + { + Func getShortPollingEndpoint = () => { + return GetHttpPollingEndpoint("shortpolling"); + }; + + transportClient = new ShortPolling(getShortPollingEndpoint, GetHttpHeaders, ProcessMessagesString); + } + else if (transport == TransportType.LONGPOLLING) + { + Func getLongPollingEndpoint = () => { + return GetHttpPollingEndpoint("longpolling"); + }; + + transportClient = new LongPolling(getLongPollingEndpoint, GetHttpHeaders, ProcessMessagesString); + } + else + { + throw new Exception("Invalid transport type: " + transport); + } + } + + + // Returns the endpoint URL for polling transport clients i.e. longpolling and shortpolling + private string GetHttpPollingEndpoint(string transport) + { + string url = endpoint + "?transport=" + transport + "&sessionKey=" + sessionKey; + if (confirmIds.Count > 0) + { + url += "&confirmIds=" + string.Join(",", confirmIds.ToArray()); + confirmIds.Clear(); + } + + return url; + } + + + // Returns the required HTTP headers + private Dictionary GetHttpHeaders() + { + if (username == null && password == null) + { + return null; + } + + var headers = new Dictionary(); + string authInfo = username + ":" + password; + string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + headers.Add("Authorization", "Basic " + encodedAuth); + return headers; + } + + + // Deserilizes and processes given messagesString + private void ProcessMessagesString(string messagesString) + { + if (string.IsNullOrEmpty(messagesString)) + { + return; + } + + JObject messages = JObject.Parse(messagesString); + AddMessages(messages); + ProcessMessages(); + } + + + // Add list of messages to message queue + private void AddMessages(JObject messages) + { + if (messages == null) + { + return; + } + + int lowestMessageId = -1; + + foreach (KeyValuePair message in messages) + { + // Check if the messageId is lower than our current messageId + int messageId = int.Parse(message.Key); + if (messageId == 0) + { + messageQueue.Add(messageId, message.Value); + continue; + } + if (messageId < currentMessageId) + { + continue; + } + + // Keep track of the largest messageId in the list + if (messageId > largestMessageId) + { + largestMessageId = messageId; + } + + // Keep track of the lowest messageId in the list + if (lowestMessageId == -1 || messageId < lowestMessageId) + { + lowestMessageId = messageId; + } + + // Check if the message exists in the queue, if not add it + if (!messageQueue.ContainsKey(messageId)) + { + messageQueue.Add(messageId, message.Value); + } + } + + // If the current messageId has never been set, set it to the current lowest + if (currentMessageId == -1) + { + currentMessageId = lowestMessageId; + } + } + + + // Process the message queue till we reach the end or a gap + private void ProcessMessages() + { + // Process all ordered messages in the order they appear + while (currentMessageId <= largestMessageId) + { + // Check if the next messageId exists + if (!messageQueue.ContainsKey(currentMessageId)) + { + break; + } + + // Process the message + Mage.EventManager.EmitEventList((JArray)messageQueue[currentMessageId]); + confirmIds.Add(currentMessageId.ToString()); + messageQueue.Remove(currentMessageId); + + currentMessageId += 1; + } + + // Finally emit any events that don't have an ID and thus don't need confirmation and lack order + if (messageQueue.ContainsKey(0)) + { + Mage.EventManager.EmitEventList((JArray)messageQueue[0]); + messageQueue.Remove(0); + } + } + } +} \ No newline at end of file diff --git a/MageClient/Module.cs b/MageClient/Module.cs new file mode 100644 index 0000000..3c91a16 --- /dev/null +++ b/MageClient/Module.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Log; +using Wizcorp.MageSDK.Utils; + +namespace Wizcorp.MageSDK.MageClient +{ + + public class Module : Singleton where T : class, new() + { + // + protected Mage Mage + { + get { return Mage.Instance; } + } + + protected Logger Logger + { + get { return Mage.Logger(GetType().Name); } + } + + // + protected virtual List StaticTopics + { + get { return null; } + } + + // + protected virtual string CommandPrefix + { + get { return null; } + } + + protected virtual List Commands + { + get { return null; } + } + + private Dictionary>> commandHandlerActions; + private Dictionary> commandHandlerFuncs; + + public JToken StaticData; + + public void SetupStaticData(Action cb) + { + Logger.Info("Setting up static data"); + + if (StaticTopics == null) + { + cb(null); + return; + } + + var queries = new JObject(); + for (var i = 0; i < StaticTopics.Count; i++) + { + string topic = StaticTopics[i]; + + var query = new JObject(); + query.Add("topic", new JValue(topic)); + query.Add("index", new JObject()); + queries.Add(topic, query); + } + + StaticData = null; + Mage.Archivist.Mget(queries, null, (error, data) => { + if (error != null) + { + cb(error); + return; + } + + StaticData = data; + cb(null); + }); + } + + public void Command(string commandName, JObject arguments, Action cb) + { + commandHandlerActions[commandName](arguments, cb); + } + + public UserCommandStatus Command(string commandName, JObject arguments) + { + return commandHandlerFuncs[commandName](arguments); + } + + private void RegisterCommand(string command) + { + commandHandlerActions.Add(command, (arguments, commandCb) => { + Mage.CommandCenter.SendCommand(CommandPrefix + "." + command, arguments, (error, result) => { + try + { + commandCb(error, result); + } + catch (Exception callbackError) + { + Logger.Data(callbackError).Critical("Uncaught exception:"); + } + }); + }); + + commandHandlerFuncs.Add(command, arguments => { + var commandStatus = new UserCommandStatus(); + + Mage.CommandCenter.SendCommand(CommandPrefix + "." + command, arguments, (error, result) => { + commandStatus.Error = error; + commandStatus.Result = result; + commandStatus.Done = true; + }); + + return commandStatus; + }); + } + + public void SetupUsercommands(Action cb) + { + Logger.Info("Setting up usercommands"); + + commandHandlerActions = new Dictionary>>(); + commandHandlerFuncs = new Dictionary>(); + + if (Commands == null) + { + cb(null); + return; + } + + foreach (string command in Commands) + { + RegisterCommand(command); + } + + cb(null); + } + } +} \ No newline at end of file diff --git a/MageClient/Session.cs b/MageClient/Session.cs new file mode 100644 index 0000000..4b30d96 --- /dev/null +++ b/MageClient/Session.cs @@ -0,0 +1,41 @@ +namespace Wizcorp.MageSDK.MageClient +{ + public class Session + { + private Mage mage + { + get { return Mage.Instance; } + } + + private string actorId; + + // + private string sessionKey; + + // + public Session() + { + mage.EventManager.On("session.set", (sender, session) => { + actorId = session["actorId"].ToString(); + sessionKey = session["key"].ToString(); + }); + + mage.EventManager.On("session.unset", (sender, reason) => { + actorId = null; + sessionKey = null; + }); + } + + // + public string GetSessionKey() + { + return sessionKey; + } + + // + public string GetActorId() + { + return actorId; + } + } +} \ No newline at end of file diff --git a/MageClient/UserCommandStatus.cs b/MageClient/UserCommandStatus.cs new file mode 100644 index 0000000..6605e7a --- /dev/null +++ b/MageClient/UserCommandStatus.cs @@ -0,0 +1,13 @@ +using System; + +using Newtonsoft.Json.Linq; + +namespace Wizcorp.MageSDK.MageClient +{ + public class UserCommandStatus + { + public bool Done = false; + public Exception Error = null; + public JToken Result = null; + } +} \ No newline at end of file From 7c247bfff516a4623d9bfc9c1f7a2b35be25e18a Mon Sep 17 00:00:00 2001 From: Kevin Destrem Date: Fri, 3 Jun 2016 12:28:12 +0900 Subject: [PATCH 3/7] Add Tomes folder --- Tomes/Tome.cs | 261 ++++++++------ Tomes/TomeArray.cs | 853 +++++++++++++++++++++++++------------------- Tomes/TomeObject.cs | 601 ++++++++++++++++++------------- Tomes/TomeValue.cs | 254 +++++++------ 4 files changed, 1144 insertions(+), 825 deletions(-) diff --git a/Tomes/Tome.cs b/Tomes/Tome.cs index 812d1e3..1ddaf8d 100644 --- a/Tomes/Tome.cs +++ b/Tomes/Tome.cs @@ -1,129 +1,184 @@ using System; using System.Collections.Generic; + using Newtonsoft.Json.Linq; -public class Tome { - public delegate void OnChanged(JToken oldValue); - public delegate void OnDestroy(); - public delegate void OnAdd(JToken key); - public delegate void OnDel(JToken key); - - // - public static JToken Conjure(JToken data, JToken root = null) { - switch (data.Type) { - case JTokenType.Array: - return new TomeArray((JArray)data, root); - case JTokenType.Object: - return new TomeObject((JObject)data, root); - default: - return new TomeValue((JValue)data, root); - } - } +namespace Wizcorp.MageSDK.Tomes +{ + public class Tome + { + public delegate void OnAdd(JToken key); - // - public static void Destroy(JToken data) { - switch (data.Type) { - case JTokenType.Array: - (data as TomeArray).Destroy(); - break; - case JTokenType.Object: - (data as TomeObject).Destroy(); - break; - default: - (data as TomeValue).Destroy(); - break; - } - } + public delegate void OnChanged(JToken oldValue); - // - public static JToken PathValue(JToken value, JArray paths) { - foreach (JToken path in paths) { - value = PathValue(value, path); - } + public delegate void OnDel(JToken key); - return value; - } - - // - public static JToken PathValue(JToken value, List paths) { - foreach (string path in paths) { - value = PathValue(value, path); + public delegate void OnDestroy(); + + // + public static JToken Conjure(JToken data, JToken root = null) + { + switch (data.Type) + { + case JTokenType.Array: + return new TomeArray((JArray)data, root); + case JTokenType.Object: + return new TomeObject((JObject)data, root); + default: + return new TomeValue((JValue)data, root); + } } - - return value; - } - // - public static JToken PathValue(JToken value, JToken key) { - if (value.Type == JTokenType.Array) { - return value[(int)key]; + // + public static void Destroy(JToken data) + { + switch (data.Type) + { + case JTokenType.Array: + var tomeArray = data as TomeArray; + if (tomeArray != null) + { + tomeArray.Destroy(); + } + break; + case JTokenType.Object: + var tomeObject = data as TomeObject; + if (tomeObject != null) + { + tomeObject.Destroy(); + } + break; + default: + var tomeValue = data as TomeValue; + if (tomeValue != null) + { + tomeValue.Destroy(); + } + break; + } } - - return value[(string)key]; - } - - // - public static JToken PathValue(JToken value, string key) { - if (value.Type == JTokenType.Array) { - return value[int.Parse(key)]; + + // + public static JToken PathValue(JToken value, JArray paths) + { + foreach (JToken path in paths) + { + value = PathValue(value, path); + } + + return value; } - - return value[key]; - } - // - public static void EmitParentChange(JToken parent) { - switch (parent.Type) { - case JTokenType.Array: - TomeArray parentArray = parent as TomeArray; - if (parentArray.onChanged != null) { - parentArray.onChanged.Invoke(null); + // + public static JToken PathValue(JToken value, List paths) + { + foreach (string path in paths) + { + value = PathValue(value, path); } - break; - case JTokenType.Object: - TomeObject parentObject = parent as TomeObject; - if (parentObject.onChanged != null) { - parentObject.onChanged.Invoke(null); + + return value; + } + + // + public static JToken PathValue(JToken value, JToken key) + { + if (value.Type == JTokenType.Array) + { + return value[(int)key]; } - break; - case JTokenType.Property: - EmitParentChange(parent.Parent); - break; - default: - throw new Exception(parent.Type.ToString() + " cannot be a parent!"); + + return value[(string)key]; } - } - // - public static void ApplyDiff(JToken root, JArray operations) { - foreach (JObject operation in operations) { - try { - JToken value = PathValue(root, (JArray)operation["chain"]); - - string op = operation["op"].ToString(); - JToken val = operation["val"]; + // + public static JToken PathValue(JToken value, string key) + { + if (value.Type == JTokenType.Array) + { + return value[int.Parse(key)]; + } + + return value[key]; + } - switch (value.Type) { + // + public static void EmitParentChange(JToken parent) + { + switch (parent.Type) + { case JTokenType.Array: - (value as TomeArray).ApplyOperation(op, val, root); + var parentArray = parent as TomeArray; + if (parentArray != null && parentArray.OnChanged != null) + { + parentArray.OnChanged.Invoke(null); + } break; case JTokenType.Object: - (value as TomeObject).ApplyOperation(op, val, root); + var parentObject = parent as TomeObject; + if (parentObject != null && parentObject.OnChanged != null) + { + parentObject.OnChanged.Invoke(null); + } break; - default: - (value as TomeValue).ApplyOperation(op, val); + case JTokenType.Property: + EmitParentChange(parent.Parent); break; + default: + throw new Exception(parent.Type.ToString() + " cannot be a parent!"); + } + } + + // + public static void ApplyDiff(JToken root, JArray operations) + { + foreach (var jToken in operations) + { + var operation = (JObject)jToken; + try + { + JToken value = PathValue(root, (JArray)operation["chain"]); + + string op = operation["op"].ToString(); + JToken val = operation["val"]; + + switch (value.Type) + { + case JTokenType.Array: + var tomeArray = value as TomeArray; + if (tomeArray != null) + { + tomeArray.ApplyOperation(op, val, root); + } + break; + case JTokenType.Object: + var tomeObject = value as TomeObject; + if (tomeObject != null) + { + tomeObject.ApplyOperation(op, val, root); + } + break; + default: + var tomeValue = value as TomeValue; + if (tomeValue != null) + { + tomeValue.ApplyOperation(op, val); + } + break; + } + } + catch (Exception diffError) + { + // TODO: NEED TO DECIDE IF TOMES SHOULD BE STANDALONE OR INSIDE MAGE. + // e.g. should it depend on mage.logger or UnityEngine.Debug for logging? + UnityEngine.Debug.LogError("Failed to apply diff operation:"); + UnityEngine.Debug.LogError(operation); + UnityEngine.Debug.LogError(diffError); + UnityEngine.Debug.LogError(root); + UnityEngine.Debug.LogError(PathValue(root, (JArray)operation["chain"])); + throw; } - } catch (Exception diffError) { - // TODO: NEED TO DECIDE IF TOMES SHOULD BE STANDALONE OR INSIDE MAGE. - // e.g. should it depend on mage.logger or UnityEngine.Debug for logging? - UnityEngine.Debug.LogError("Failed to apply diff operation:"); - UnityEngine.Debug.LogError(operation); - UnityEngine.Debug.LogError(diffError); - UnityEngine.Debug.LogError(root); - UnityEngine.Debug.LogError(PathValue(root, (JArray)operation["chain"])); - throw diffError; } } } -} +} \ No newline at end of file diff --git a/Tomes/TomeArray.cs b/Tomes/TomeArray.cs index 11f91b8..5586894 100644 --- a/Tomes/TomeArray.cs +++ b/Tomes/TomeArray.cs @@ -1,430 +1,563 @@ using System; +using System.Collections.Generic; + using Newtonsoft.Json.Linq; -public class TomeArray : JArray { - // - public Tome.OnChanged onChanged; - public Tome.OnDestroy onDestroy; - public Tome.OnAdd onAdd; - public Tome.OnDel onDel; - - // - private JToken root; +using Wizcorp.MageSDK.MageClient; + +namespace Wizcorp.MageSDK.Tomes +{ + public class TomeArray : JArray + { + public Tome.OnAdd OnAdd; + + // + public Tome.OnChanged OnChanged; + public Tome.OnDel OnDel; + public Tome.OnDestroy OnDestroy; + // + private JToken parent; - // - public TomeArray(JArray data, JToken _root) { // - root = _root; - if (root == null) { - root = this; + public TomeArray(JArray data, JToken root) + { + // + parent = root; + if (parent == null) + { + parent = this; + } + + // + for (var i = 0; i < data.Count; i += 1) + { + Add(Tome.Conjure(data[i], parent)); + } + + // + OnChanged += EmitToParents; + OnAdd += EmitChanged; + OnDel += EmitChanged; } // - for (int i = 0; i < data.Count; i += 1) { - this.Add(Tome.Conjure(data[i], root)); + private void EmitToParents(JToken oldValue) + { + if (this != parent) + { + Tome.EmitParentChange(Parent); + } } - + // - onChanged += EmitToParents; - onAdd += EmitChanged; - onDel += EmitChanged; - } - - // - private void EmitToParents(JToken oldValue) { - if (this != root) { - Tome.EmitParentChange(Parent); + private void EmitChanged(JToken key) + { + if (OnChanged != null) + { + OnChanged.Invoke(null); + } } - } - // - private void EmitChanged(JToken key) { - if (onChanged != null) { - onChanged.Invoke(null); + // + public void Assign(JToken newValue) + { + lock ((object)this) + { + switch (newValue.Type) + { + case JTokenType.Array: + var newTomeArray = new TomeArray((JArray)newValue, parent); + Replace(newTomeArray); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeArray.OnChanged; + newTomeArray.OnChanged = OnChanged; + newTomeArray.OnDestroy = OnDestroy; + OnAdd -= EmitChanged; + OnAdd += newTomeArray.OnAdd; + newTomeArray.OnAdd = OnAdd; + OnDel -= EmitChanged; + OnDel += newTomeArray.OnDel; + newTomeArray.OnDel = OnDel; + + if (newTomeArray.OnChanged != null) + { + newTomeArray.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + case JTokenType.Object: + var newTomeObject = new TomeObject((JObject)newValue, parent); + Replace(newTomeObject); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeObject.OnChanged; + newTomeObject.OnChanged = OnChanged; + newTomeObject.OnDestroy = OnDestroy; + OnAdd -= EmitChanged; + OnAdd += newTomeObject.OnAdd; + newTomeObject.OnAdd = OnAdd; + OnDel -= EmitChanged; + OnDel += newTomeObject.OnDel; + newTomeObject.OnDel = OnDel; + + if (newTomeObject.OnChanged != null) + { + newTomeObject.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + default: + var newTomeValue = new TomeValue((JValue)newValue, parent); + Replace(newTomeValue); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeValue.OnChanged; + newTomeValue.OnChanged = OnChanged; + newTomeValue.OnDestroy = OnDestroy; + + if (newTomeValue.OnChanged != null) + { + newTomeValue.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + } + } } - } + // + public void Destroy() + { + lock ((object)this) + { + foreach (JToken value in this) + { + Tome.Destroy(value); + } - // - public void Assign(JToken newValue) { - lock((object)this) { - switch (newValue.Type) { - case JTokenType.Array: - TomeArray newTomeArray = new TomeArray((JArray)newValue, root); - this.Replace(newTomeArray); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeArray.onChanged; - newTomeArray.onChanged = onChanged; - newTomeArray.onDestroy = onDestroy; - onAdd -= EmitChanged; - onAdd += newTomeArray.onAdd; - newTomeArray.onAdd = onAdd; - onDel -= EmitChanged; - onDel += newTomeArray.onDel; - newTomeArray.onDel = onDel; - - if (newTomeArray.onChanged != null) { - newTomeArray.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } + if (OnDestroy != null) + { + OnDestroy.Invoke(); } - break; - case JTokenType.Object: - TomeObject newTomeObject = new TomeObject((JObject)newValue, root); - this.Replace(newTomeObject); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeObject.onChanged; - newTomeObject.onChanged = onChanged; - newTomeObject.onDestroy = onDestroy; - onAdd -= EmitChanged; - onAdd += newTomeObject.onAdd; - newTomeObject.onAdd = onAdd; - onDel -= EmitChanged; - onDel += newTomeObject.onDel; - newTomeObject.onDel = onDel; - - if (newTomeObject.onChanged != null) { - newTomeObject.onChanged.Invoke(null); + + OnChanged = null; + OnDestroy = null; + OnAdd = null; + OnDel = null; + } + } + + // + public void Set(int index, JToken value) + { + lock ((object)this) + { + // Make sure the property exists, filling in missing indexes + if (Count <= index) + { + while (Count < index) + { + Add(Tome.Conjure(JValue.CreateNull(), parent)); } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); + + Add(Tome.Conjure(value, parent)); + if (OnAdd != null) + { + OnAdd.Invoke(index); } + return; } - break; - default: - TomeValue newTomeValue = new TomeValue((JValue)newValue, root); - this.Replace(newTomeValue); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeValue.onChanged; - newTomeValue.onChanged = onChanged; - newTomeValue.onDestroy = onDestroy; - - if (newTomeValue.onChanged != null) { - newTomeValue.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } + + // Assign the property + JToken property = this[index]; + switch (property.Type) + { + case JTokenType.Array: + var tomeArray = property as TomeArray; + if (tomeArray != null) + { + tomeArray.Assign(value); + } + break; + case JTokenType.Object: + var tomeObject = property as TomeObject; + if (tomeObject != null) + { + tomeObject.Assign(value); + } + break; + default: + if ((property as TomeValue) == null) + { + Mage.Instance.Logger("Tomes").Data(property).Error("property is not a tome value: " + index.ToString()); + UnityEngine.Debug.Log(this); + } + var tomeValue = property as TomeValue; + if (tomeValue != null) + { + tomeValue.Assign(value); + } + break; } - break; } } - } - - // - public void Destroy() { - lock((object)this) { - foreach (JToken value in this) { - Tome.Destroy(value); - } - if (onDestroy != null) { - onDestroy.Invoke(); - } - - onChanged = null; - onDestroy = null; - onAdd = null; - onDel = null; - } - } - - // - public void Set(int index, JToken value) { - lock((object)this) { - // Make sure the property exists, filling in missing indexes - if (this.Count <= index) { - while (this.Count < index) { - this.Add(Tome.Conjure(JValue.CreateNull(), root)); + // + public void Del(int index) + { + lock ((object)this) + { + JToken property = this[index]; + switch (property.Type) + { + case JTokenType.Array: + var tomeArray = property as TomeArray; + if (tomeArray != null) + { + tomeArray.Destroy(); + } + break; + case JTokenType.Object: + var tomeObject = property as TomeObject; + if (tomeObject != null) + { + tomeObject.Destroy(); + } + break; + default: + if ((property as TomeValue) == null) + { + Mage.Instance.Logger("Tomes").Data(property).Error("property is not a tome value:" + index.ToString()); + UnityEngine.Debug.Log(this); + } + var value = property as TomeValue; + if (value != null) + { + value.Destroy(); + } + break; } - this.Add(Tome.Conjure(value, root)); - if (onAdd != null) { - onAdd.Invoke(index); + this[index].Replace(JValue.CreateNull()); + if (OnDel != null) + { + OnDel.Invoke(index); } - return; } + } - // Assign the property - JToken property = this[index]; - switch (property.Type) { - case JTokenType.Array: - (property as TomeArray).Assign(value); - break; - case JTokenType.Object: - (property as TomeObject).Assign(value); - break; - default: - if ((property as TomeValue) == null) { - Mage.Instance.logger("Tomes").data(property).error("property is not a tome value: " + index.ToString()); - UnityEngine.Debug.Log(this); + // + public void Move(int fromKey, JToken newParent, JToken newKey) + { + lock ((object)this) + { + if (newParent.Type == JTokenType.Array) + { + var tomeArray = newParent as TomeArray; + if (tomeArray != null) + { + tomeArray.Set((int)newKey, this[fromKey]); + } } - (property as TomeValue).Assign(value); - break; - } - } - } - - // - public void Del(int index) { - lock((object)this) { - JToken property = this[index]; - switch (property.Type) { - case JTokenType.Array: - (property as TomeArray).Destroy(); - break; - case JTokenType.Object: - (property as TomeObject).Destroy(); - break; - default: - if ((property as TomeValue) == null) { - Mage.Instance.logger("Tomes").data(property).error("property is not a tome value:" + index.ToString()); - UnityEngine.Debug.Log(this); + else + { + var tomeObject = newParent as TomeObject; + if (tomeObject != null) + { + tomeObject.Set((string)newKey, this[fromKey]); + } } - (property as TomeValue).Destroy(); - break; - } - this[index].Replace(JValue.CreateNull()); - if (onDel != null) { - onDel.Invoke(index); + Del(fromKey); } } - } - - // - public void Move(int fromKey, JToken newParent, JToken newKey) { - lock((object)this) { - if (newParent.Type == JTokenType.Array) { - (newParent as TomeArray).Set((int)newKey, this[fromKey]); - } else { - (newParent as TomeObject).Set((string)newKey, this[fromKey]); - } - - Del(fromKey); - } - } - - // - public void Rename(int wasKey, int isKey) { - lock((object)this) { - JToken wasValue = this[wasKey]; - Del(wasKey); - Set(isKey, wasValue); - } - } - - // - public void Swap(int firstKey, JToken secondParent, JToken secondKey) { - lock((object)this) { - JToken secondValue; - if (secondParent.Type == JTokenType.Array) { - secondValue = secondParent[(int)secondKey]; - secondParent[(int)secondKey].Replace(this[firstKey]); - } else { - secondValue = secondParent[(string)secondKey]; - secondParent[(string)secondKey].Replace(this[firstKey]); - } - - this[firstKey].Replace(secondValue); - if (onChanged != null) { - onChanged.Invoke(null); + + // + public void Rename(int wasKey, int isKey) + { + lock ((object)this) + { + JToken wasValue = this[wasKey]; + Del(wasKey); + Set(isKey, wasValue); } } - } - // - public void Push(JToken item) { - lock((object)this) { - this.Set(this.Count, item); - } - } + // + public void Swap(int firstKey, JToken secondParent, JToken secondKey) + { + lock ((object)this) + { + JToken secondValue; + if (secondParent.Type == JTokenType.Array) + { + secondValue = secondParent[(int)secondKey]; + secondParent[(int)secondKey].Replace(this[firstKey]); + } + else + { + secondValue = secondParent[(string)secondKey]; + secondParent[(string)secondKey].Replace(this[firstKey]); + } - // NOTE: Tome behavior for a del operation is to replace values with null. - // However a pop operation does in fact remove the item as well as - // firing the del event. Thus we do both below. - public JToken Pop() { - lock((object)this) { - JToken last = this[this.Count - 1]; - this.Del(this.Count - 1); - this.Last.Remove(); - return last; + this[firstKey].Replace(secondValue); + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } } - } - // NOTE: Tome behavior for a del operation is to replace values with null. - // However a shift operation does in fact remove the item as well as - // firing the del event. Thus we do both below. - public JToken Shift() { - lock((object)this) { - JToken first = this[0]; - this.Del(0); - this.First.Remove(); - return first; + // + public void Push(JToken item) + { + lock ((object)this) + { + Set(Count, item); + } } - } - // - public void UnShift(JToken item) { - lock((object)this) { - this.AddFirst(Tome.Conjure(item, root)); - if (onAdd != null) { - onAdd.Invoke(0); + // NOTE: Tome behavior for a del operation is to replace values with null. + // However a pop operation does in fact remove the item as well as + // firing the del event. Thus we do both below. + public JToken Pop() + { + lock ((object)this) + { + JToken last = this[Count - 1]; + Del(Count - 1); + Last.Remove(); + return last; } } - } - // - public void Reverse() { - lock((object)this) { - JArray oldOrder = new JArray(this as JArray); - for (int i = oldOrder.Count; i > 0; i -= 1) { - this[oldOrder.Count - i].Replace(oldOrder[i - 1]); + // NOTE: Tome behavior for a del operation is to replace values with null. + // However a shift operation does in fact remove the item as well as + // firing the del event. Thus we do both below. + public JToken Shift() + { + lock ((object)this) + { + JToken first = this[0]; + Del(0); + First.Remove(); + return first; } + } - if (onChanged != null) { - onChanged.Invoke(null); + // + public void UnShift(JToken item) + { + lock ((object)this) + { + AddFirst(Tome.Conjure(item, parent)); + if (OnAdd != null) + { + OnAdd.Invoke(0); + } } } - } - // - public void Splice(int index, int deleteCount, JArray insertItems) { - lock((object)this) { - // Delete given item count starting at given index - for (int delI = index + deleteCount - 1; delI >= index; delI -= 1) { - if (delI > this.Count - 1) { - continue; + // + public void Reverse() + { + lock ((object)this) + { + var oldOrder = new JArray(this); + for (int i = oldOrder.Count; i > 0; i -= 1) + { + this[oldOrder.Count - i].Replace(oldOrder[i - 1]); } - Del(delI); - this[delI].Remove(); - } - - // Insert given items starting at given index - for (int addI = 0; addI < insertItems.Count; addI += 1) { - int insertI = index + addI; - this.Insert(insertI, Tome.Conjure(insertItems[addI])); - if (onAdd != null) { - onAdd.Invoke(insertI); + if (OnChanged != null) + { + OnChanged.Invoke(null); } } } - } + // + public void Splice(int index, int deleteCount, JArray insertItems) + { + lock ((object)this) + { + // Delete given item count starting at given index + for (int delI = index + deleteCount - 1; delI >= index; delI -= 1) + { + if (delI > Count - 1) + { + continue; + } - // We implement this as when using JArray.IndexOf(JToken) it compares the reference but not the value. - public int IndexOf(string lookFor) { - lock((object)this) { - for (int i = 0; i < this.Count; i += 1) { - JToken value = this[i]; - if (value.Type == JTokenType.String && (string)value == lookFor) { - return i; + Del(delI); + this[delI].Remove(); } - } - return -1; + // Insert given items starting at given index + for (var addI = 0; addI < insertItems.Count; addI += 1) + { + int insertI = index + addI; + Insert(insertI, Tome.Conjure(insertItems[addI])); + if (OnAdd != null) + { + OnAdd.Invoke(insertI); + } + } + } } - } - // - public void ApplyOperation(string op, JToken val, JToken root) { - lock((object)this) { - switch (op) { - case "assign": - Assign(val); - break; - - case "set": - Set((int)val["key"], val["val"]); - break; - - case "del": - Del((int)val); - break; - - case "move": - int fromKey = (int)val["key"]; - JToken newParent = Tome.PathValue(root, val["newParent"] as JArray); - JToken toKey = (val["newKey"] != null) ? val["newKey"] : new JValue(fromKey); - Move(fromKey, newParent, toKey); - break; - - case "rename": - foreach (var property in val as JObject) { - int wasKey = int.Parse(property.Key); - int isKey = (int)property.Value; - Rename(wasKey, isKey); - } - break; - - case "swap": - int firstKey = (int)val["key"]; - JToken secondParent = Tome.PathValue(root, val["newParent"] as JArray); - JToken secondKey = (val["newKey"] != null) ? val["newKey"] : new JValue(firstKey); - Swap(firstKey, secondParent, secondKey); - break; - - case "push": - foreach (JToken item in val as JArray) { - Push(item); + // We implement this as when using JArray.IndexOf(JToken) it compares the reference but not the value. + public int IndexOf(string lookFor) + { + lock ((object)this) + { + for (var i = 0; i < Count; i += 1) + { + JToken value = this[i]; + if (value.Type == JTokenType.String && (string)value == lookFor) + { + return i; + } } - break; - case "pop": - Pop(); - break; + return -1; + } + } - case "shift": - Shift(); - break; - case "unshift": - JArray unshiftItems = val as JArray; - for (int i = unshiftItems.Count; i > 0; i -= 1) { - UnShift(unshiftItems[i - 1]); + // + public void ApplyOperation(string op, JToken val, JToken root) + { + lock ((object)this) + { + switch (op) + { + case "assign": + Assign(val); + break; + + case "set": + Set((int)val["key"], val["val"]); + break; + + case "del": + Del((int)val); + break; + + case "move": + var fromKey = (int)val["key"]; + JToken newParent = Tome.PathValue(root, val["newParent"] as JArray); + JToken toKey = (val["newKey"] != null) ? val["newKey"] : new JValue(fromKey); + Move(fromKey, newParent, toKey); + break; + + case "rename": + var keyValuePairs = val as JObject; + if (keyValuePairs != null) + { + foreach (KeyValuePair property in keyValuePairs) + { + int wasKey = int.Parse(property.Key); + var isKey = (int)property.Value; + Rename(wasKey, isKey); + } + } + break; + + case "swap": + var firstKey = (int)val["key"]; + JToken secondParent = Tome.PathValue(root, val["newParent"] as JArray); + JToken secondKey = (val["newKey"] != null) ? val["newKey"] : new JValue(firstKey); + Swap(firstKey, secondParent, secondKey); + break; + + case "push": + var jArray = val as JArray; + if (jArray != null) + { + foreach (JToken item in jArray) + { + Push(item); + } + } + break; + + case "pop": + Pop(); + break; + + case "shift": + Shift(); + break; + + case "unshift": + var unshiftItems = val as JArray; + if (unshiftItems != null) + { + for (int i = unshiftItems.Count; i > 0; i -= 1) + { + UnShift(unshiftItems[i - 1]); + } + } + break; + + case "reverse": + Reverse(); + break; + + case "splice": + var index = (int)val[0]; + var deleteCount = (int)val[1]; + + var items = new JArray(val as JArray); + items.First.Remove(); + items.First.Remove(); + + Splice(index, deleteCount, items); + break; + + default: + throw new Exception("TomeArray - Unsupported operation: " + op); } - break; - - case "reverse": - Reverse(); - break; - - case "splice": - int index = (int)val[0]; - int deleteCount = (int)val[1]; - - JArray items = new JArray(val as JArray); - items.First.Remove(); - items.First.Remove(); - - Splice(index, deleteCount, items); - break; - - default: - throw new Exception("TomeArray - Unsupported operation: " + op); } } } diff --git a/Tomes/TomeObject.cs b/Tomes/TomeObject.cs index 6a5269d..1e3ba64 100644 --- a/Tomes/TomeObject.cs +++ b/Tomes/TomeObject.cs @@ -1,297 +1,398 @@ using System; using System.Collections.Generic; + using Newtonsoft.Json.Linq; -public class TomeObject : JObject { - // - public Tome.OnChanged onChanged; - public Tome.OnDestroy onDestroy; - public Tome.OnAdd onAdd; - public Tome.OnDel onDel; - - // - private JToken root; +using Wizcorp.MageSDK.MageClient; +namespace Wizcorp.MageSDK.Tomes +{ + public class TomeObject : JObject + { + public Tome.OnAdd OnAdd; - // - public TomeObject(JObject data, JToken _root) { // - root = _root; - if (root == null) { - root = this; - } - + public Tome.OnChanged OnChanged; + public Tome.OnDel OnDel; + public Tome.OnDestroy OnDestroy; + + // + private JToken parent; + + // - foreach (JProperty property in data.Properties()) { - this.Add(property.Name, Tome.Conjure(property.Value, root)); + public TomeObject(JObject data, JToken root) + { + // + parent = root; + if (parent == null) + { + parent = this; + } + + // + foreach (JProperty property in data.Properties()) + { + Add(property.Name, Tome.Conjure(property.Value, parent)); + } + + // + OnChanged += EmitToParents; + OnAdd += EmitChanged; + OnDel += EmitChanged; } // - onChanged += EmitToParents; - onAdd += EmitChanged; - onDel += EmitChanged; - } - - // - private void EmitToParents(JToken oldValue) { - if (this != root) { - Tome.EmitParentChange(Parent); + private void EmitToParents(JToken oldValue) + { + if (this != parent) + { + Tome.EmitParentChange(Parent); + } } - } - - // - private void EmitChanged(JToken key) { - if (onChanged != null) { - onChanged.Invoke(null); + + // + private void EmitChanged(JToken key) + { + if (OnChanged != null) + { + OnChanged.Invoke(null); + } } - } - // - public void Assign(JToken newValue) { - lock((object)this) { - switch (newValue.Type) { - case JTokenType.Array: - TomeArray newTomeArray = new TomeArray((JArray)newValue, root); - this.Replace(newTomeArray); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeArray.onChanged; - newTomeArray.onChanged = onChanged; - newTomeArray.onDestroy = onDestroy; - onAdd -= EmitChanged; - onAdd += newTomeArray.onAdd; - newTomeArray.onAdd = onAdd; - onDel -= EmitChanged; - onDel += newTomeArray.onDel; - newTomeArray.onDel = onDel; - - if (newTomeArray.onChanged != null) { - newTomeArray.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } - } - break; - case JTokenType.Object: - TomeObject newTomeObject = new TomeObject((JObject)newValue, root); - this.Replace(newTomeObject); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeObject.onChanged; - newTomeObject.onChanged = onChanged; - newTomeObject.onDestroy = onDestroy; - onAdd -= EmitChanged; - onAdd += newTomeObject.onAdd; - newTomeObject.onAdd = onAdd; - onDel -= EmitChanged; - onDel += newTomeObject.onDel; - newTomeObject.onDel = onDel; - - if (newTomeObject.onChanged != null) { - newTomeObject.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } - } - break; - default: - TomeValue newTomeValue = new TomeValue((JValue)newValue, root); - this.Replace(newTomeValue); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeValue.onChanged; - newTomeValue.onChanged = onChanged; - newTomeValue.onDestroy = onDestroy; - - if (newTomeValue.onChanged != null) { - newTomeValue.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } + // + public void Assign(JToken newValue) + { + lock ((object)this) + { + switch (newValue.Type) + { + case JTokenType.Array: + var newTomeArray = new TomeArray((JArray)newValue, parent); + Replace(newTomeArray); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeArray.OnChanged; + newTomeArray.OnChanged = OnChanged; + newTomeArray.OnDestroy = OnDestroy; + OnAdd -= EmitChanged; + OnAdd += newTomeArray.OnAdd; + newTomeArray.OnAdd = OnAdd; + OnDel -= EmitChanged; + OnDel += newTomeArray.OnDel; + newTomeArray.OnDel = OnDel; + + if (newTomeArray.OnChanged != null) + { + newTomeArray.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + case JTokenType.Object: + var newTomeObject = new TomeObject((JObject)newValue, parent); + Replace(newTomeObject); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeObject.OnChanged; + newTomeObject.OnChanged = OnChanged; + newTomeObject.OnDestroy = OnDestroy; + OnAdd -= EmitChanged; + OnAdd += newTomeObject.OnAdd; + newTomeObject.OnAdd = OnAdd; + OnDel -= EmitChanged; + OnDel += newTomeObject.OnDel; + newTomeObject.OnDel = OnDel; + + if (newTomeObject.OnChanged != null) + { + newTomeObject.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + default: + var newTomeValue = new TomeValue((JValue)newValue, parent); + Replace(newTomeValue); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeValue.OnChanged; + newTomeValue.OnChanged = OnChanged; + newTomeValue.OnDestroy = OnDestroy; + + if (newTomeValue.OnChanged != null) + { + newTomeValue.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; } - break; } } - } - - // - public void Destroy() { - lock((object)this) { - foreach (var property in this) { - Tome.Destroy(property.Value); - } - if (onDestroy != null) { - onDestroy.Invoke(); - } + // + public void Destroy() + { + lock ((object)this) + { + foreach (KeyValuePair property in this) + { + Tome.Destroy(property.Value); + } - onChanged = null; - onDestroy = null; - onAdd = null; - onDel = null; - } - } - - // - public void Set(string propertyName, JToken value) { - lock((object)this) { - // Make sure the property exists - if (this[propertyName] == null) { - this.Add(propertyName, Tome.Conjure(value, root)); - if (onAdd != null) { - onAdd.Invoke(propertyName); + if (OnDestroy != null) + { + OnDestroy.Invoke(); } - return; + + OnChanged = null; + OnDestroy = null; + OnAdd = null; + OnDel = null; } + } + + // + public void Set(string propertyName, JToken value) + { + lock ((object)this) + { + // Make sure the property exists + if (this[propertyName] == null) + { + Add(propertyName, Tome.Conjure(value, parent)); + if (OnAdd != null) + { + OnAdd.Invoke(propertyName); + } + return; + } - // Assign the property - JToken property = this[propertyName]; - switch (property.Type) { - case JTokenType.Array: - (property as TomeArray).Assign(value); - break; - case JTokenType.Object: - (property as TomeObject).Assign(value); - break; - default: - if ((property as TomeValue) == null) { - Mage.Instance.logger("Tomes").data(property).error("property is not a tome value: " + propertyName.ToString()); - UnityEngine.Debug.Log(this); + // Assign the property + JToken property = this[propertyName]; + switch (property.Type) + { + case JTokenType.Array: + var tomeArray = property as TomeArray; + if (tomeArray != null) + { + tomeArray.Assign(value); + } + break; + case JTokenType.Object: + var tomeObject = property as TomeObject; + if (tomeObject != null) + { + tomeObject.Assign(value); + } + break; + default: + if ((property as TomeValue) == null) + { + Mage.Instance.Logger("Tomes").Data(property).Error("property is not a tome value: " + propertyName); + UnityEngine.Debug.Log(this); + } + var tomeValue = property as TomeValue; + if (tomeValue != null) + { + tomeValue.Assign(value); + } + break; } - (property as TomeValue).Assign(value); - break; } } - } - // - public void Del(string propertyName) { - lock((object)this) { - JToken property = this[propertyName]; - switch (property.Type) { - case JTokenType.Array: - (property as TomeArray).Destroy(); - break; - case JTokenType.Object: - (property as TomeObject).Destroy(); - break; - default: - if ((property as TomeValue) == null) { - Mage.Instance.logger("Tomes").data(property).error("property is not a tome value: " + propertyName.ToString()); - UnityEngine.Debug.Log(this); + // + public void Del(string propertyName) + { + lock ((object)this) + { + JToken property = this[propertyName]; + switch (property.Type) + { + case JTokenType.Array: + var tomeArray = property as TomeArray; + if (tomeArray != null) + { + tomeArray.Destroy(); + } + break; + case JTokenType.Object: + var tomeObject = property as TomeObject; + if (tomeObject != null) + { + tomeObject.Destroy(); + } + break; + default: + if ((property as TomeValue) == null) + { + Mage.Instance.Logger("Tomes").Data(property).Error("property is not a tome value: " + propertyName); + UnityEngine.Debug.Log(this); + } + var tomeValue = property as TomeValue; + if (tomeValue != null) + { + tomeValue.Destroy(); + } + break; } - (property as TomeValue).Destroy(); - break; - } - this.Remove(propertyName); - if (onDel != null) { - onDel.Invoke(propertyName); + Remove(propertyName); + if (OnDel != null) + { + OnDel.Invoke(propertyName); + } } } - } - // - public void Move(string fromKey, JToken toParent, JToken toKey) { - lock((object)this) { - if (toParent.Type == JTokenType.Array) { - (toParent as TomeArray).Set((int)toKey, this[fromKey]); - } else { - (toParent as TomeObject).Set((string)toKey, this[fromKey]); - } + // + public void Move(string fromKey, JToken toParent, JToken toKey) + { + lock ((object)this) + { + if (toParent.Type == JTokenType.Array) + { + var tomeArray = toParent as TomeArray; + if (tomeArray != null) + { + tomeArray.Set((int)toKey, this[fromKey]); + } + } + else + { + var tomeObject = toParent as TomeObject; + if (tomeObject != null) + { + tomeObject.Set((string)toKey, this[fromKey]); + } + } - Del(fromKey); + Del(fromKey); + } } - } - // - public void Rename(string wasKey, string isKey) { - lock((object)this) { - JToken wasValue = this[wasKey]; - Del(wasKey); - Set(isKey, wasValue); + // + public void Rename(string wasKey, string isKey) + { + lock ((object)this) + { + JToken wasValue = this[wasKey]; + Del(wasKey); + Set(isKey, wasValue); + } } - } - // - public void Swap(string firstKey, JToken secondParent, JToken secondKey) { - lock((object)this) { - JToken secondValue; - if (secondParent.Type == JTokenType.Array) { - secondValue = secondParent[(int)secondKey]; - secondParent[(int)secondKey].Replace(this[firstKey]); - } else { - secondValue = secondParent[(string)secondKey]; - secondParent[(string)secondKey].Replace(this[firstKey]); - } + // + public void Swap(string firstKey, JToken secondParent, JToken secondKey) + { + lock ((object)this) + { + JToken secondValue; + if (secondParent.Type == JTokenType.Array) + { + secondValue = secondParent[(int)secondKey]; + secondParent[(int)secondKey].Replace(this[firstKey]); + } + else + { + secondValue = secondParent[(string)secondKey]; + secondParent[(string)secondKey].Replace(this[firstKey]); + } - this[firstKey].Replace(secondValue); - if (onChanged != null) { - onChanged.Invoke(null); + this[firstKey].Replace(secondValue); + if (OnChanged != null) + { + OnChanged.Invoke(null); + } } } - } - // - public void ApplyOperation(string op, JToken val, JToken root) { - lock((object)this) { - switch (op) { - case "assign": - Assign(val); - break; - - case "set": - Set((string)val["key"], val["val"]); - break; - - case "del": - Del((string)val); - break; - - case "move": - string fromKey = (string)val["key"]; - JToken toParent = Tome.PathValue(root, val["newParent"] as JArray); - JToken toKey = (val["newKey"] != null) ? val["newKey"] : new JValue(fromKey); - Move(fromKey, toParent, toKey); - break; - - case "rename": - foreach (var property in val as JObject) { - string wasKey = property.Key; - string isKey = (string)property.Value; - Rename(wasKey, isKey); - } - break; + // + public void ApplyOperation(string op, JToken val, JToken root) + { + lock ((object)this) + { + switch (op) + { + case "assign": + Assign(val); + break; + + case "set": + Set((string)val["key"], val["val"]); + break; - case "swap": - string firstKey = (string)val["key"]; - JToken secondParent = Tome.PathValue(root, val["newParent"] as JArray); - JToken secondKey = (val["newKey"] != null) ? val["newKey"] : new JValue(firstKey); - Swap(firstKey, secondParent, secondKey); - break; + case "del": + Del((string)val); + break; - default: - throw new Exception("TomeObject - Unsupported operation: " + op); + case "move": + var fromKey = (string)val["key"]; + JToken toParent = Tome.PathValue(root, val["newParent"] as JArray); + JToken toKey = (val["newKey"] != null) ? val["newKey"] : new JValue(fromKey); + Move(fromKey, toParent, toKey); + break; + + case "rename": + var keyValuePairs = val as JObject; + if (keyValuePairs != null) + { + foreach (KeyValuePair property in keyValuePairs) + { + string wasKey = property.Key; + var isKey = (string)property.Value; + Rename(wasKey, isKey); + } + } + break; + + case "swap": + var firstKey = (string)val["key"]; + JToken secondParent = Tome.PathValue(root, val["newParent"] as JArray); + JToken secondKey = (val["newKey"] != null) ? val["newKey"] : new JValue(firstKey); + Swap(firstKey, secondParent, secondKey); + break; + + default: + throw new Exception("TomeObject - Unsupported operation: " + op); + } } } } diff --git a/Tomes/TomeValue.cs b/Tomes/TomeValue.cs index 001437c..3a66ea8 100644 --- a/Tomes/TomeValue.cs +++ b/Tomes/TomeValue.cs @@ -1,132 +1,162 @@ using System; -using Newtonsoft.Json.Linq; - -public class TomeValue : JValue { - // - public Tome.OnChanged onChanged; - public Tome.OnDestroy onDestroy; - // - private JToken root; +using Newtonsoft.Json.Linq; +namespace Wizcorp.MageSDK.Tomes +{ + public class TomeValue : JValue + { + // + public Tome.OnChanged OnChanged; + public Tome.OnDestroy OnDestroy; - // - public TomeValue(JValue value, JToken _root) : base(value) { // - root = _root; - if (root == null) { - root = this; - } - + private JToken parent; + // - onChanged += EmitToParents; - } + public TomeValue(JValue value, JToken root) : base(value) + { + // + parent = root; + if (parent == null) + { + parent = this; + } - // - private void EmitToParents(JToken oldValue) { - if (this != root) { - Tome.EmitParentChange(Parent); + // + OnChanged += EmitToParents; } - } - - // - public void Assign(JToken newValue) { - lock((object)this) { - switch (newValue.Type) { - case JTokenType.Array: - TomeArray newTomeArray = new TomeArray((JArray)newValue, root); - this.Replace(newTomeArray); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeArray.onChanged; - newTomeArray.onChanged = onChanged; - newTomeArray.onDestroy = onDestroy; - - if (newTomeArray.onChanged != null) { - newTomeArray.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } - } - break; - case JTokenType.Object: - TomeObject newTomeObject = new TomeObject((JObject)newValue, root); - this.Replace(newTomeObject); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeObject.onChanged; - newTomeObject.onChanged = onChanged; - newTomeObject.onDestroy = onDestroy; - - if (newTomeObject.onChanged != null) { - newTomeObject.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } - } - break; - default: - TomeValue newTomeValue = new TomeValue((JValue)newValue, root); - this.Replace(newTomeValue); - - if (this.Parent == null) { - // If replace was successfuly move over event handlers and call new onChanged handler - // The instance in which replace would not be successful, is when the old and new values are the same - onChanged -= EmitToParents; - onChanged += newTomeValue.onChanged; - newTomeValue.onChanged = onChanged; - newTomeValue.onDestroy = onDestroy; - - if (newTomeValue.onChanged != null) { - newTomeValue.onChanged.Invoke(null); - } - } else { - // Otherwise call original onChanged handler - if (onChanged != null) { - onChanged.Invoke(null); - } - } - break; + // + private void EmitToParents(JToken oldValue) + { + if (!Equals(this, parent)) + { + Tome.EmitParentChange(Parent); } } - } - // - public void Destroy() { - lock((object)this) { - if (onDestroy != null) { - onDestroy.Invoke(); + // + public void Assign(JToken newValue) + { + lock ((object)this) + { + switch (newValue.Type) + { + case JTokenType.Array: + var newTomeArray = new TomeArray((JArray)newValue, parent); + Replace(newTomeArray); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeArray.OnChanged; + newTomeArray.OnChanged = OnChanged; + newTomeArray.OnDestroy = OnDestroy; + + if (newTomeArray.OnChanged != null) + { + newTomeArray.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + case JTokenType.Object: + var newTomeObject = new TomeObject((JObject)newValue, parent); + Replace(newTomeObject); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeObject.OnChanged; + newTomeObject.OnChanged = OnChanged; + newTomeObject.OnDestroy = OnDestroy; + + if (newTomeObject.OnChanged != null) + { + newTomeObject.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + default: + var newTomeValue = new TomeValue((JValue)newValue, parent); + Replace(newTomeValue); + + if (Parent == null) + { + // If replace was successfuly move over event handlers and call new onChanged handler + // The instance in which replace would not be successful, is when the old and new values are the same + OnChanged -= EmitToParents; + OnChanged += newTomeValue.OnChanged; + newTomeValue.OnChanged = OnChanged; + newTomeValue.OnDestroy = OnDestroy; + + if (newTomeValue.OnChanged != null) + { + newTomeValue.OnChanged.Invoke(null); + } + } + else + { + // Otherwise call original onChanged handler + if (OnChanged != null) + { + OnChanged.Invoke(null); + } + } + break; + } } - - onChanged = null; - onDestroy = null; } - } + // + public void Destroy() + { + lock ((object)this) + { + if (OnDestroy != null) + { + OnDestroy.Invoke(); + } - // - public void ApplyOperation(string op, JToken val) { - lock((object)this) { - switch (op) { - case "assign": - Assign(val); - break; + OnChanged = null; + OnDestroy = null; + } + } - default: - throw new Exception("TomeValue - Unsupported operation: " + op); + // + public void ApplyOperation(string op, JToken val) + { + lock ((object)this) + { + switch (op) + { + case "assign": + Assign(val); + break; + + default: + throw new Exception("TomeValue - Unsupported operation: " + op); + } } } } From 7519ab8eea936cadeb8f230ac5f4d7ab4f80c358 Mon Sep 17 00:00:00 2001 From: Kevin Destrem Date: Fri, 3 Jun 2016 12:28:53 +0900 Subject: [PATCH 4/7] Add Network folder (http/jsonrpc) --- Network/Http/HttpRequest.cs | 151 +++++++++++++++++++++++++++ Network/Http/HttpRequestException.cs | 14 +++ Network/Http/HttpRequestManager.cs | 36 +++++++ Network/JsonRpc/Jsonrpc.cs | 147 ++++++++++++++++++++++++++ Network/JsonRpc/JsonrpcBatch.cs | 36 +++++++ 5 files changed, 384 insertions(+) create mode 100644 Network/Http/HttpRequest.cs create mode 100644 Network/Http/HttpRequestException.cs create mode 100644 Network/Http/HttpRequestManager.cs create mode 100644 Network/JsonRpc/Jsonrpc.cs create mode 100644 Network/JsonRpc/JsonrpcBatch.cs diff --git a/Network/Http/HttpRequest.cs b/Network/Http/HttpRequest.cs new file mode 100644 index 0000000..7c72a8a --- /dev/null +++ b/Network/Http/HttpRequest.cs @@ -0,0 +1,151 @@ +#if UNITY_5 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Text; + +using UnityEngine; + +namespace Wizcorp.MageSDK.Network.Http +{ + public class HttpRequest + { + private Action cb; + private WWW request; + + // + public double Timeout = 100 * 1000; + private Stopwatch timeoutTimer = new Stopwatch(); + + + // + public HttpRequest(string url, byte[] postData, Dictionary headers, Action cb) + { + // Start timeout timer + timeoutTimer.Start(); + + // Queue constructor for main thread execution + HttpRequestManager.Queue(Constructor(url, postData, headers, cb)); + } + + + // + private IEnumerator Constructor(string url, byte[] postData, Dictionary headers, Action cb) + { + this.cb = cb; + request = new WWW(url, postData, headers); + + HttpRequestManager.Queue(WaitLoop()); + yield break; + } + + + // + private IEnumerator WaitLoop() + { + while (!request.isDone) + { + if (timeoutTimer.ElapsedMilliseconds >= Timeout) + { + // Timed out abort the request with timeout error + cb(new Exception("Request timed out"), null); + Abort(); + yield break; + } + else if (request == null) + { + // Check if we destroyed the request due to an abort + yield break; + } + + // Otherwise continue to wait + yield return null; + } + + // Stop the timeout timer + timeoutTimer.Stop(); + + // Check if there is a callback + if (cb == null) + { + yield break; + } + + // Check if there was an error with the request + if (request.error != null) + { + var statusCode = 0; + if (request.responseHeaders.ContainsKey("STATUS")) + { + statusCode = int.Parse(request.responseHeaders["STATUS"].Split(' ')[1]); + } + + cb(new HttpRequestException(request.error, statusCode), null); + yield break; + } + + // Otherwise return the response + cb(null, request.text); + } + + + // Abort request + public void Abort() + { + using (WWW _request = request) { + request = null; + + if (_request != null) + { + _request.Dispose(); + } + } + timeoutTimer.Stop(); + } + + + // Create GET request and return it + public static HttpRequest Get(string url, Dictionary headers, CookieContainer cookies, Action cb) + { + // TODO: COOKIE SUPPORT + + // Create request and return it + // The callback will be called when the request is complete + return new HttpRequest(url, null, headers, cb); + } + + // Create POST request and return it + public static HttpRequest Post( + string url, string contentType, string postData, Dictionary headers, CookieContainer cookies, + Action cb) + { + byte[] binaryPostData = Encoding.UTF8.GetBytes(postData); + return Post(url, contentType, binaryPostData, headers, cookies, cb); + } + + // Create POST request and return it + public static HttpRequest Post( + string url, string contentType, byte[] postData, Dictionary headers, CookieContainer cookies, + Action cb) + { + var headersCopy = new Dictionary(headers); + + // TODO: COOKIE SUPPORT + + // Set content type if provided + if (contentType != null) + { + headersCopy.Add("ContentType", contentType); + } + + // Create request and return it + // The callback will be called when the request is complete + return new HttpRequest(url, postData, headersCopy, cb); + } + } +} + +#endif \ No newline at end of file diff --git a/Network/Http/HttpRequestException.cs b/Network/Http/HttpRequestException.cs new file mode 100644 index 0000000..6b80c11 --- /dev/null +++ b/Network/Http/HttpRequestException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Wizcorp.MageSDK.Network.Http +{ + public class HttpRequestException : Exception + { + public int Status; + + public HttpRequestException(string message, int status) : base(message) + { + Status = status; + } + } +} \ No newline at end of file diff --git a/Network/Http/HttpRequestManager.cs b/Network/Http/HttpRequestManager.cs new file mode 100644 index 0000000..cc52c6d --- /dev/null +++ b/Network/Http/HttpRequestManager.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; + +using Wizcorp.MageSDK.Utils; + +namespace Wizcorp.MageSDK.Network.Http +{ + public class HttpRequestManager : MonoSingleton + { + // + private static List queued = new List(); + + // + public static void Queue(IEnumerator coroutine) + { + lock ((object)queued) + { + queued.Add(coroutine); + } + } + + // + void Update() + { + lock ((object)queued) + { + for (var i = 0; i < queued.Count; i += 1) + { + StartCoroutine(queued[i]); + } + + queued.Clear(); + } + } + } +} \ No newline at end of file diff --git a/Network/JsonRpc/Jsonrpc.cs b/Network/JsonRpc/Jsonrpc.cs new file mode 100644 index 0000000..4bcc5fe --- /dev/null +++ b/Network/JsonRpc/Jsonrpc.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Network.Http; + +namespace Wizcorp.MageSDK.Network.JsonRpc +{ + public class Jsonrpc + { + // Endpoint and Basic Authentication + private string endpoint; + private string password = null; + private string username = null; + + public void SetEndpoint(string endpoint, string username = null, string password = null) + { + this.endpoint = endpoint; + this.username = username; + this.password = password; + } + + public void Call(string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) + { + Call(JValue.CreateNull(), methodName, parameters, headers, cookies, cb); + } + + public void Call(string id, string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) + { + Call(new JValue(id), methodName, parameters, headers, cookies, cb); + } + + public void Call(int id, string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) + { + Call(new JValue(id), methodName, parameters, headers, cookies, cb); + } + + public void Call(JValue id, string methodName, JObject parameters, Dictionary headers, CookieContainer cookies, Action cb) + { + // Setup JSON request object + var requestObject = new JObject(); + requestObject.Add("jsonrpc", new JValue("2.0")); + + requestObject.Add("id", id); + requestObject.Add("method", new JValue(methodName)); + requestObject.Add("params", parameters); + + // Serialize JSON request object into string + string postData; + try + { + postData = requestObject.ToString(); + } + catch (Exception serializeError) + { + cb(serializeError, null); + return; + } + + // Send request + SendRequest(postData, headers, cookies, (requestError, responseString) => { + if (requestError != null) + { + cb(requestError, null); + return; + } + + // Deserialize the JSON response + JObject responseObject; + try + { + responseObject = JObject.Parse(responseString); + } + catch (Exception parseError) + { + cb(parseError, null); + return; + } + + cb(null, responseObject); + }); + } + + public void CallBatch(JsonrpcBatch rpcBatch, Dictionary headers, CookieContainer cookies, Action cb) + { + // Serialize JSON request object into string + string postData; + try + { + postData = rpcBatch.Batch.ToString(); + } + catch (Exception serializeError) + { + cb(serializeError, null); + return; + } + + // Send request + SendRequest(postData, headers, cookies, (requestError, responseString) => { + if (requestError != null) + { + cb(requestError, null); + return; + } + + // Deserialize the JSON response + JArray responseArray; + try + { + responseArray = JArray.Parse(responseString); + } + catch (Exception parseError) + { + cb(parseError, null); + return; + } + + cb(null, responseArray); + }); + } + + private void SendRequest(string postData, Dictionary headers, CookieContainer cookies, Action cb) + { + // Make sure the endpoint is set + if (string.IsNullOrEmpty(endpoint)) + { + cb(new Exception("Endpoint has not been set"), null); + return; + } + + // Make a copy of the provided headers and add additional required headers + var _headers = new Dictionary(headers); + if (username != null && password != null) + { + string authInfo = username + ":" + password; + string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + _headers.Add("Authorization", "Basic " + encodedAuth); + } + + // Send HTTP post to JSON rpc endpoint + HttpRequest.Post(endpoint, "application/json", postData, _headers, cookies, cb); + } + } +} \ No newline at end of file diff --git a/Network/JsonRpc/JsonrpcBatch.cs b/Network/JsonRpc/JsonrpcBatch.cs new file mode 100644 index 0000000..5515fee --- /dev/null +++ b/Network/JsonRpc/JsonrpcBatch.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json.Linq; + +namespace Wizcorp.MageSDK.Network.JsonRpc +{ + public abstract class JsonrpcBatch + { + public JArray Batch = new JArray(); + + public void Add(string methodName, JObject parameters) + { + Add(JValue.CreateNull(), methodName, parameters); + } + + public void Add(string id, string methodName, JObject parameters) + { + Add(new JValue(id), methodName, parameters); + } + + public void Add(int id, string methodName, JObject parameters) + { + Add(new JValue(id), methodName, parameters); + } + + public void Add(JValue id, string methodName, JObject parameters) + { + var requestObject = new JObject(); + requestObject.Add("jsonrpc", new JValue("2.0")); + + requestObject.Add("id", id); + requestObject.Add("method", new JValue(methodName)); + requestObject.Add("params", parameters); + + Batch.Add(requestObject); + } + } +} \ No newline at end of file From c7a3a21fdae87578831847f93385d4bba8f12359 Mon Sep 17 00:00:00 2001 From: Kevin Destrem Date: Fri, 3 Jun 2016 12:29:35 +0900 Subject: [PATCH 5/7] Add the remaining folders (Utils, Event, Editor) --- EditorScript/EditorPlayModeState.cs | 9 +++ EditorScript/UnityEditorPlayMode.cs | 75 +++++++++++++++++++++++ Event/EventEmitter.cs | 71 ++++++++++++++++++++++ Utils/Async.cs | 94 +++++++++++++++++++++++++++++ Utils/MonoSingleton.cs | 51 ++++++++++++++++ Utils/SceneInstance.cs | 28 +++++++++ Utils/Singleton.cs | 20 ++++++ 7 files changed, 348 insertions(+) create mode 100644 EditorScript/EditorPlayModeState.cs create mode 100644 EditorScript/UnityEditorPlayMode.cs create mode 100644 Event/EventEmitter.cs create mode 100644 Utils/Async.cs create mode 100644 Utils/MonoSingleton.cs create mode 100644 Utils/SceneInstance.cs create mode 100644 Utils/Singleton.cs diff --git a/EditorScript/EditorPlayModeState.cs b/EditorScript/EditorPlayModeState.cs new file mode 100644 index 0000000..84f2016 --- /dev/null +++ b/EditorScript/EditorPlayModeState.cs @@ -0,0 +1,9 @@ +namespace Wizcorp.MageSDK.Editor +{ + public enum EditorPlayModeState + { + Stopped, + Playing, + Paused + } +} \ No newline at end of file diff --git a/EditorScript/UnityEditorPlayMode.cs b/EditorScript/UnityEditorPlayMode.cs new file mode 100644 index 0000000..25be505 --- /dev/null +++ b/EditorScript/UnityEditorPlayMode.cs @@ -0,0 +1,75 @@ +#if UNITY_EDITOR + +using UnityEditor; + +namespace Wizcorp.MageSDK.Editor +{ + [InitializeOnLoad] + public static class UnityEditorPlayMode + { + public delegate void EditorModeChanged(EditorPlayModeState newState); + + private static EditorPlayModeState currentState = EditorPlayModeState.Stopped; + public static EditorModeChanged OnEditorModeChanged; + + static UnityEditorPlayMode() + { + EditorApplication.playmodeStateChanged += OnUnityPlayModeChanged; + if (EditorApplication.isPaused) + { + currentState = EditorPlayModeState.Paused; + } + } + + private static void OnUnityPlayModeChanged() + { + var newState = EditorPlayModeState.Stopped; + switch (currentState) + { + case EditorPlayModeState.Stopped: + if (EditorApplication.isPlayingOrWillChangePlaymode) + { + newState = EditorPlayModeState.Playing; + } + else + { + newState = EditorPlayModeState.Paused; + } + break; + case EditorPlayModeState.Playing: + if (EditorApplication.isPaused) + { + newState = EditorPlayModeState.Paused; + } + else if (EditorApplication.isPlaying) + { + newState = EditorPlayModeState.Playing; + } + else + { + newState = EditorPlayModeState.Stopped; + } + break; + case EditorPlayModeState.Paused: + if (EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isPaused) + { + newState = EditorPlayModeState.Playing; + } + else if (EditorApplication.isPlayingOrWillChangePlaymode && EditorApplication.isPaused) + { + newState = EditorPlayModeState.Paused; + } + break; + } + + if (OnEditorModeChanged != null) + { + OnEditorModeChanged.Invoke(newState); + } + + currentState = newState; + } + } +} + +#endif \ No newline at end of file diff --git a/Event/EventEmitter.cs b/Event/EventEmitter.cs new file mode 100644 index 0000000..380dd2f --- /dev/null +++ b/Event/EventEmitter.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Wizcorp.MageSDK.Event +{ + public class EventEmitter + { + private EventHandlerList eventsList = new EventHandlerList(); + + // + private Dictionary eventTags = new Dictionary(); + + // + public void On(string eventTag, Action handler) + { + if (!eventTags.ContainsKey(eventTag)) + { + eventTags.Add(eventTag, new object()); + } + + eventsList.AddHandler(eventTags[eventTag], handler); + } + + // + public void Once(string eventTag, Action handler) + { + Action handlerWrapper = null; + handlerWrapper = (obj, arguments) => { + eventsList.RemoveHandler(eventTags[eventTag], handlerWrapper); + handler(obj, arguments); + }; + + On(eventTag, handlerWrapper); + } + + // + public void Emit(string eventTag, object sender, T arguments) + { + if (!eventTags.ContainsKey(eventTag)) + { + return; + } + + var execEventList = (Action)eventsList[eventTags[eventTag]]; + execEventList(sender, arguments); + } + + public void Emit(string eventTag, T arguments) + { + Emit(eventTag, null, arguments); + } + + // + public void Off(string eventTag, Action handler) + { + eventsList.RemoveHandler(eventTags[eventTag], handler); + } + + // + public void RemoveAllListeners() + { + // Destroy all event handlers + eventsList.Dispose(); + eventsList = new EventHandlerList(); + + // Destroy all event tags + eventTags = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/Utils/Async.cs b/Utils/Async.cs new file mode 100644 index 0000000..b1b1e9e --- /dev/null +++ b/Utils/Async.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; + +namespace Wizcorp.MageSDK.Utils +{ + /// + /// Async is a utility class which provides straight-forward, powerful functions for working with asynchronous C#. + /// + /// Async provides around 20 functions that include the usual 'functional' suspects (map, reduce, filter, each…) as well as + /// some common patterns for asynchronous control flow (parallel, series, waterfall…). All these functions assume you follow + /// the convention of providing a single callback as the last argument of your async function. + /// + public static class Async + { + public static void Each(List items, Action> fn, Action cb) + { + if (items == null || items.Count == 0) + { + cb(null); + return; + } + + var currentItemI = 0; + Action iterate = null; + iterate = () => { + if (currentItemI >= items.Count) + { + cb(null); + return; + } + + // Execute the given function on this item + fn( + items[currentItemI], + error => { + // Stop iteration if there was an error + if (error != null) + { + cb(error); + return; + } + + // Continue to next item + currentItemI++; + iterate(); + }); + }; + + // Begin the iteration + iterate(); + } + + public static void Series(List>> actionItems, Action cb) + { + bool isEmpty = actionItems == null || actionItems.Count == 0; + if (isEmpty) + { + cb(null); + return; + } + + var currentItemI = 0; + Action iterate = null; + iterate = () => { + if (currentItemI >= actionItems.Count) + { + cb(null); + return; + } + + // Shift an element from the list + Action> actionItem = actionItems[currentItemI]; + + // Execute the given function on this item + actionItem( + error => { + // Stop iteration if there was an error + if (error != null) + { + cb(error); + return; + } + + // Continue to next item + currentItemI++; + iterate(); + }); + }; + + // Begin the iteration + iterate(); + } + } +} \ No newline at end of file diff --git a/Utils/MonoSingleton.cs b/Utils/MonoSingleton.cs new file mode 100644 index 0000000..75ab8f8 --- /dev/null +++ b/Utils/MonoSingleton.cs @@ -0,0 +1,51 @@ +using UnityEngine; + +namespace Wizcorp.MageSDK.Utils +{ + public class MonoSingleton : MonoBehaviour + where T : MonoBehaviour + { + public static T Instance + { + get + { + if (_Instance == null) + { + Instantiate(); + } + + return _Instance; + } + } + + // Instance functions + protected static T _Instance; + + // Instantiation function if you need to pre-instantiate rather than on demand + public static void Instantiate() + { + if (_Instance != null) + { + return; + } + + var newObject = new GameObject(typeof(T).Name); + DontDestroyOnLoad(newObject); + + _Instance = newObject.AddComponent(); + } + + // Use this for initialization before any start methods are called + protected virtual void Awake() + { + if (_Instance != null) + { + DestroyImmediate(gameObject); + return; + } + + _Instance = (T)(object)this; + DontDestroyOnLoad(gameObject); + } + } +} \ No newline at end of file diff --git a/Utils/SceneInstance.cs b/Utils/SceneInstance.cs new file mode 100644 index 0000000..f702f1c --- /dev/null +++ b/Utils/SceneInstance.cs @@ -0,0 +1,28 @@ +using UnityEngine; + +namespace Wizcorp.MageSDK.Utils +{ + public class SceneInstance : MonoBehaviour + where T : class + { + public static T Instance + { + get { return instance; } + } + + // + private static T instance; + + // Use this for initialization before any start methods are called + protected virtual void Awake() + { + instance = (T)(object)this; + } + + // Use this for destruction + protected virtual void OnDestroy() + { + instance = null; + } + } +} \ No newline at end of file diff --git a/Utils/Singleton.cs b/Utils/Singleton.cs new file mode 100644 index 0000000..2cd7c4a --- /dev/null +++ b/Utils/Singleton.cs @@ -0,0 +1,20 @@ +namespace Wizcorp.MageSDK.Utils +{ + public class Singleton + where T : class, new() + { + public static T Instance + { + get { return instance ?? (instance = new T()); } + } + + // Instance functions + private static T instance; + + // Hack which makes sure the _instance property is set during the T class constructor + protected Singleton() + { + instance = (T)(object)this; + } + } +} \ No newline at end of file From 0eae49e0489cdde2e6bdb1caf9c6a633004a05d4 Mon Sep 17 00:00:00 2001 From: Kevin Destrem Date: Fri, 3 Jun 2016 16:01:27 +0900 Subject: [PATCH 6/7] Remove warning, improve singletons and few other fixes --- AssemblyInfo.cs | 6 + .../Command/Client/CommandHttpClient.cs | 147 ++++++++---------- MageClient/Mage.cs | 21 +-- MageClient/Message/Client/LongPolling.cs | 87 +++++------ MageClient/Message/Client/ShortPolling.cs | 48 +++--- MageClient/Message/MessageStream.cs | 56 +++---- MageClient/Module.cs | 7 +- Network/Http/HttpRequest.cs | 10 +- Utils/Async.cs | 50 +++--- Utils/MonoSingleton.cs | 32 ++-- Utils/Singleton.cs | 16 +- 11 files changed, 228 insertions(+), 252 deletions(-) create mode 100644 AssemblyInfo.cs diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..dd86131 --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Runtime.InteropServices; + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/MageClient/Command/Client/CommandHttpClient.cs b/MageClient/Command/Client/CommandHttpClient.cs index 87ecb03..d30c567 100644 --- a/MageClient/Command/Client/CommandHttpClient.cs +++ b/MageClient/Command/Client/CommandHttpClient.cs @@ -70,48 +70,45 @@ public override void SendBatch(CommandBatch commandBatch) string postData = string.Join("\n", data.ToArray()); // Send HTTP request - SendRequest( - batchUrl, - postData, - responseArray => { - // Process each command response - try + SendRequest(batchUrl, postData, responseArray => { + // Process each command response + try + { + for (var batchId = 0; batchId < responseArray.Count; batchId += 1) { - for (var batchId = 0; batchId < responseArray.Count; batchId += 1) + var commandResponse = responseArray[batchId] as JArray; + CommandBatchItem commandItem = commandBatch.BatchItems[batchId]; + string commandName = commandItem.CommandName; + Action commandCb = commandItem.Cb; + + // Check if there are any events attached to this request + if (commandResponse != null && commandResponse.Count >= 3) { - var commandResponse = responseArray[batchId] as JArray; - CommandBatchItem commandItem = commandBatch.BatchItems[batchId]; - string commandName = commandItem.CommandName; - Action commandCb = commandItem.Cb; - - // Check if there are any events attached to this request - if (commandResponse != null && commandResponse.Count >= 3) - { - Logger.Verbose("[" + commandName + "] processing events"); - Mage.EventManager.EmitEventList((JArray)commandResponse[2]); - } - - // Check if the response was an error - if (commandResponse != null && commandResponse[0].Type != JTokenType.Null) - { - Logger.Verbose("[" + commandName + "] server error"); - commandCb(new Exception(commandResponse[0].ToString()), null); - return; - } - - // Pull off call result object, if it doesn't exist - Logger.Verbose("[" + commandName + "] call response"); - if (commandResponse != null) - { - commandCb(null, commandResponse[1]); - } + Logger.Verbose("[" + commandName + "] processing events"); + Mage.EventManager.EmitEventList((JArray)commandResponse[2]); + } + + // Check if the response was an error + if (commandResponse != null && commandResponse[0].Type != JTokenType.Null) + { + Logger.Verbose("[" + commandName + "] server error"); + commandCb(new Exception(commandResponse[0].ToString()), null); + return; + } + + // Pull off call result object, if it doesn't exist + Logger.Verbose("[" + commandName + "] call response"); + if (commandResponse != null) + { + commandCb(null, commandResponse[1]); } } - catch (Exception error) - { - Logger.Data(error).Error("Error when processing command batch responses"); - } - }); + } + catch (Exception error) + { + Logger.Data(error).Error("Error when processing command batch responses"); + } + }); } private void SendRequest(string batchUrl, string postData, Action cb) @@ -124,51 +121,45 @@ private void SendRequest(string batchUrl, string postData, Action cb) headers.Add("Authorization", "Basic " + encodedAuth); } - HttpRequest.Post( - batchUrl, - "", - postData, - headers, - Mage.Cookies, - (requestError, responseString) => { - Logger.Verbose("Recieved response: " + responseString); - - // Check if there was a transport error - if (requestError != null) + HttpRequest.Post(batchUrl, "", postData, headers, Mage.Cookies, (requestError, responseString) => { + Logger.Verbose("Recieved response: " + responseString); + + // Check if there was a transport error + if (requestError != null) + { + var error = "network"; + if (requestError is WebException) { - var error = "network"; - if (requestError is WebException) + var webException = requestError as WebException; + var webResponse = webException.Response as HttpWebResponse; + if (webResponse != null && webResponse.StatusCode == HttpStatusCode.ServiceUnavailable) { - var webException = requestError as WebException; - var webResponse = webException.Response as HttpWebResponse; - if (webResponse != null && webResponse.StatusCode == HttpStatusCode.ServiceUnavailable) - { - error = "maintenance"; - } + error = "maintenance"; } - - OnTransportError.Invoke(error, requestError); - return; - } - - // Parse reponse array - JArray responseArray; - try - { - responseArray = JArray.Parse(responseString); - } - catch (Exception parseError) - { - OnTransportError.Invoke("parse", parseError); - return; } - // Let CommandCenter know this batch was successful - OnSendComplete.Invoke(); - - // Return array for processing - cb(responseArray); - }); + OnTransportError.Invoke(error, requestError); + return; + } + + // Parse reponse array + JArray responseArray; + try + { + responseArray = JArray.Parse(responseString); + } + catch (Exception parseError) + { + OnTransportError.Invoke("parse", parseError); + return; + } + + // Let CommandCenter know this batch was successful + OnSendComplete.Invoke(); + + // Return array for processing + cb(responseArray); + }); } } } \ No newline at end of file diff --git a/MageClient/Mage.cs b/MageClient/Mage.cs index ee84397..8f19042 100644 --- a/MageClient/Mage.cs +++ b/MageClient/Mage.cs @@ -133,26 +133,20 @@ public void SetupModules(List moduleNames, Action cb) // Grab module instance from singleton base Type singletonType = typeof(Singleton<>).MakeGenericType(t); - Object instance = singletonType.InvokeMember("Instance", staticProperty, null, null, null); + object instance = singletonType.InvokeMember("Instance", staticProperty, null, null, null); // Setup module Type moduleType = typeof(Module<>).MakeGenericType(t); Type t1 = t; Async.Series( new List>>() { + // Setup module user commands callbackInner => { - // Setup module user commands - var arguments = new object[] { - callbackInner - }; - moduleType.InvokeMember("SetupUsercommands", publicMethod, null, instance, arguments); + moduleType.InvokeMember("SetupUsercommands", publicMethod, null, instance, new object[] { callbackInner }); }, + // Setup module static data callbackInner => { - // Setup module static data - var arguments = new object[] { - callbackInner - }; - moduleType.InvokeMember("SetupStaticData", publicMethod, null, instance, arguments); + moduleType.InvokeMember("SetupStaticData", publicMethod, null, instance, new object[] { callbackInner }); } }, error => { @@ -172,10 +166,7 @@ public void SetupModules(List moduleNames, Action cb) // Invoke the setup method on the module Logger(moduleName).Info("Executing setup function"); - var arguments = new object[] { - callback - }; - t1.InvokeMember("Setup", publicMethod, null, instance, arguments); + t1.InvokeMember("Setup", publicMethod, null, instance, new object[] { callback }); }); return; diff --git a/MageClient/Message/Client/LongPolling.cs b/MageClient/Message/Client/LongPolling.cs index 6d01b12..0ad438e 100644 --- a/MageClient/Message/Client/LongPolling.cs +++ b/MageClient/Message/Client/LongPolling.cs @@ -9,12 +9,12 @@ namespace Wizcorp.MageSDK.MageClient.Message.Client { public class LongPolling : TransportClient { - private Mage Mage + private static Mage Mage { get { return Mage.Instance; } } - private Logger Logger + private static Logger Logger { get { return Mage.Logger("longpolling"); } } @@ -28,7 +28,7 @@ private Logger Logger private Action processMessages; // - HttpRequest currentRequest; + private HttpRequest currentRequest; // Required interval timer for polling delay private int errorInterval; @@ -121,57 +121,54 @@ private void RequestLoop() // Send poll request and wait for a response string endpoint = getEndpoint(); Logger.Debug("Sending request: " + endpoint); - currentRequest = HttpRequest.Get( - endpoint, - getHeaders(), - Mage.Cookies, - (requestError, responseString) => { - currentRequest = null; - - // Ignore errors if we have been stopped - if (requestError != null && !running) - { - Logger.Debug("Stopped"); - return; - } + currentRequest = HttpRequest.Get(endpoint, getHeaders(), Mage.Cookies, (requestError, responseString) => { + currentRequest = null; - if (requestError != null) + // Ignore errors if we have been stopped + if (requestError != null && !running) + { + Logger.Debug("Stopped"); + return; + } + + if (requestError != null) + { + var exception = requestError as HttpRequestException; + if (exception != null) { - if (requestError is HttpRequestException) + // Only log web exceptions if they aren't an empty response or gateway timeout + HttpRequestException requestException = exception; + if (requestException.Status != 0 && requestException.Status != 504) { - // Only log web exceptions if they aren't an empty response or gateway timeout - HttpRequestException requestException = requestError as HttpRequestException; - if (requestException.Status != 0 && requestException.Status != 504) - { - Logger.Error("(" + requestException.Status.ToString() + ") " + requestError.Message); - } + Logger.Error("(" + requestException.Status.ToString() + ") " + exception.Message); } - else - { - Logger.Error(requestError.ToString()); - } - - QueueNextRequest(errorInterval); - return; } - - // Call the message processer hook and re-call request loop function - try + else { - Logger.Verbose("Recieved response: " + responseString); - if (responseString != null) - { - processMessages(responseString); - } - - RequestLoop(); + Logger.Error(requestError.ToString()); } - catch (Exception error) + + QueueNextRequest(errorInterval); + return; + } + + // Call the message processer hook and re-call request loop function + try + { + Logger.Verbose("Recieved response: " + responseString); + if (responseString != null) { - Logger.Error(error.ToString()); - QueueNextRequest(errorInterval); + processMessages(responseString); } - }); + + RequestLoop(); + } + catch (Exception error) + { + Logger.Error(error.ToString()); + QueueNextRequest(errorInterval); + } + }); } } } \ No newline at end of file diff --git a/MageClient/Message/Client/ShortPolling.cs b/MageClient/Message/Client/ShortPolling.cs index 7e984f8..43ca6f0 100644 --- a/MageClient/Message/Client/ShortPolling.cs +++ b/MageClient/Message/Client/ShortPolling.cs @@ -9,12 +9,12 @@ namespace Wizcorp.MageSDK.MageClient.Message.Client { public class ShortPolling : TransportClient { - private Mage Mage + private static Mage Mage { get { return Mage.Instance; } } - private Logger Logger + private static Logger Logger { get { return Mage.Logger("shortpolling"); } } @@ -109,30 +109,26 @@ private void RequestLoop() // Send poll request and wait for a response string endpoint = getEndpoint(); Logger.Debug("Sending request: " + endpoint); - HttpRequest.Get( - endpoint, - getHeaders(), - Mage.Cookies, - (requestError, responseString) => { - if (requestError != null) - { - Logger.Error(requestError.ToString()); - QueueNextRequest(errorInterval); - return; - } - - // Call the message processer hook and queue the next request - try - { - processMessages(responseString); - QueueNextRequest(requestInterval); - } - catch (Exception error) - { - Logger.Data(responseString).Error(error.ToString()); - QueueNextRequest(errorInterval); - } - }); + HttpRequest.Get(endpoint, getHeaders(), Mage.Cookies, (requestError, responseString) => { + if (requestError != null) + { + Logger.Error(requestError.ToString()); + QueueNextRequest(errorInterval); + return; + } + + // Call the message processer hook and queue the next request + try + { + processMessages(responseString); + QueueNextRequest(requestInterval); + } + catch (Exception error) + { + Logger.Data(responseString).Error(error.ToString()); + QueueNextRequest(errorInterval); + } + }); } } } \ No newline at end of file diff --git a/MageClient/Message/MessageStream.cs b/MageClient/Message/MessageStream.cs index 2589ae8..210c902 100644 --- a/MageClient/Message/MessageStream.cs +++ b/MageClient/Message/MessageStream.cs @@ -15,12 +15,12 @@ namespace Wizcorp.MageSDK.MageClient.Message { public class MessageStream { - private Mage Mage + private static Mage Mage { get { return Mage.Instance; } } - private Logger Logger + private static Logger Logger { get { return Mage.Logger("messagestream"); } } @@ -49,21 +49,17 @@ public MessageStream(TransportType transport = TransportType.LONGPOLLING) InitializeMessageList(); // Start transport client when session is acquired - Mage.EventManager.On( - "session.set", - (sender, session) => { - sessionKey = UnityEngine.WWW.EscapeURL(session["key"].ToString()); - transportClient.Start(); - }); + Mage.EventManager.On("session.set", (sender, session) => { + sessionKey = UnityEngine.WWW.EscapeURL(session["key"].ToString()); + transportClient.Start(); + }); // Stop the message client when session is lost - Mage.EventManager.On( - "session.unset", - (sender, reason) => { - transportClient.Stop(); - InitializeMessageList(); - sessionKey = null; - }); + Mage.EventManager.On("session.unset", (sender, reason) => { + transportClient.Stop(); + InitializeMessageList(); + sessionKey = null; + }); // Also stop the message client when the editor is stopped #if UNITY_EDITOR @@ -129,25 +125,17 @@ public void SetTransport(TransportType transport) } // Create new transport client instance - if (transport == TransportType.SHORTPOLLING) - { - Func getShortPollingEndpoint = () => { - return GetHttpPollingEndpoint("shortpolling"); - }; - - transportClient = new ShortPolling(getShortPollingEndpoint, GetHttpHeaders, ProcessMessagesString); - } - else if (transport == TransportType.LONGPOLLING) - { - Func getLongPollingEndpoint = () => { - return GetHttpPollingEndpoint("longpolling"); - }; - - transportClient = new LongPolling(getLongPollingEndpoint, GetHttpHeaders, ProcessMessagesString); - } - else - { - throw new Exception("Invalid transport type: " + transport); + switch (transport) { + case TransportType.SHORTPOLLING: + Func getShortPollingEndpoint = () => GetHttpPollingEndpoint("shortpolling"); + transportClient = new ShortPolling(getShortPollingEndpoint, GetHttpHeaders, ProcessMessagesString); + break; + case TransportType.LONGPOLLING: + Func getLongPollingEndpoint = () => GetHttpPollingEndpoint("longpolling"); + transportClient = new LongPolling(getLongPollingEndpoint, GetHttpHeaders, ProcessMessagesString); + break; + default: + throw new Exception("Invalid transport type: " + transport); } } diff --git a/MageClient/Module.cs b/MageClient/Module.cs index 3c91a16..0c942f3 100644 --- a/MageClient/Module.cs +++ b/MageClient/Module.cs @@ -59,9 +59,10 @@ public void SetupStaticData(Action cb) { string topic = StaticTopics[i]; - var query = new JObject(); - query.Add("topic", new JValue(topic)); - query.Add("index", new JObject()); + var query = new JObject { + {"topic", new JValue(topic)}, + {"index", new JObject()} + }; queries.Add(topic, query); } diff --git a/Network/Http/HttpRequest.cs b/Network/Http/HttpRequest.cs index 7c72a8a..1cfbd32 100644 --- a/Network/Http/HttpRequest.cs +++ b/Network/Http/HttpRequest.cs @@ -95,14 +95,10 @@ private IEnumerator WaitLoop() // Abort request public void Abort() { - using (WWW _request = request) { - request = null; + WWW webRequest = request; + request = null; - if (_request != null) - { - _request.Dispose(); - } - } + webRequest.Dispose(); timeoutTimer.Stop(); } diff --git a/Utils/Async.cs b/Utils/Async.cs index b1b1e9e..ea77a4a 100644 --- a/Utils/Async.cs +++ b/Utils/Async.cs @@ -12,64 +12,62 @@ namespace Wizcorp.MageSDK.Utils /// public static class Async { - public static void Each(List items, Action> fn, Action cb) + public static void Each(List items, Action> fn, Action callback) { if (items == null || items.Count == 0) { - cb(null); + callback(null); return; } - var currentItemI = 0; + var currentItem = 0; Action iterate = null; iterate = () => { - if (currentItemI >= items.Count) + if (currentItem >= items.Count) { - cb(null); + callback(null); return; } // Execute the given function on this item - fn( - items[currentItemI], - error => { - // Stop iteration if there was an error - if (error != null) - { - cb(error); - return; - } + fn(items[currentItem], error => { + // Stop iteration if there was an error + if (error != null) + { + callback(error); + return; + } - // Continue to next item - currentItemI++; - iterate(); - }); + // Continue to next item + currentItem++; + iterate(); + }); }; // Begin the iteration iterate(); } - public static void Series(List>> actionItems, Action cb) + public static void Series(List>> actionItems, Action callback) { bool isEmpty = actionItems == null || actionItems.Count == 0; if (isEmpty) { - cb(null); + callback(null); return; } - var currentItemI = 0; + var currentItem = 0; Action iterate = null; iterate = () => { - if (currentItemI >= actionItems.Count) + if (currentItem >= actionItems.Count) { - cb(null); + callback(null); return; } // Shift an element from the list - Action> actionItem = actionItems[currentItemI]; + Action> actionItem = actionItems[currentItem]; // Execute the given function on this item actionItem( @@ -77,12 +75,12 @@ public static void Series(List>> actionItems, Action : MonoBehaviour - where T : MonoBehaviour + public class MonoSingleton : MonoBehaviour where T : MonoBehaviour { public static T Instance { get { - if (_Instance == null) + if (instance == null) { Instantiate(); } - return _Instance; + return instance; } } // Instance functions - protected static T _Instance; + private static T instance; // Instantiation function if you need to pre-instantiate rather than on demand public static void Instantiate() { - if (_Instance != null) - { - return; - } - var newObject = new GameObject(typeof(T).Name); DontDestroyOnLoad(newObject); - _Instance = newObject.AddComponent(); + instance = newObject.AddComponent(); } // Use this for initialization before any start methods are called protected virtual void Awake() { - if (_Instance != null) + if (instance != null) { DestroyImmediate(gameObject); return; } - _Instance = (T)(object)this; + try + { + instance = (T)(object)this; + } + catch (InvalidCastException e) + { + Debug.LogError(e); + } + DontDestroyOnLoad(gameObject); } } diff --git a/Utils/Singleton.cs b/Utils/Singleton.cs index 2cd7c4a..e22af33 100644 --- a/Utils/Singleton.cs +++ b/Utils/Singleton.cs @@ -1,7 +1,8 @@ -namespace Wizcorp.MageSDK.Utils +using System; + +namespace Wizcorp.MageSDK.Utils { - public class Singleton - where T : class, new() + public class Singleton where T : class, new() { public static T Instance { @@ -14,7 +15,14 @@ public static T Instance // Hack which makes sure the _instance property is set during the T class constructor protected Singleton() { - instance = (T)(object)this; + try + { + instance = (T)(object)this; + } + catch (InvalidCastException e) + { + UnityEngine.Debug.LogError(e); + } } } } \ No newline at end of file From c34a449d5794a16a9935316247c335726d3062ef Mon Sep 17 00:00:00 2001 From: Kevin Destrem Date: Mon, 6 Jun 2016 17:04:14 +0900 Subject: [PATCH 7/7] Refactor the MessageStream --- MageClient/Message/Client/LongPolling.cs | 174 ------------ MageClient/Message/Client/ShortPolling.cs | 134 --------- MageClient/Message/Client/TransportClient.cs | 8 - MageClient/Message/MessageStream.cs | 264 +++--------------- MageClient/Message/MessageStreamConfig.cs | 46 +++ .../Message/{Client => }/TransportType.cs | 2 +- MageClient/Message/Transports/LongPolling.cs | 83 ++++++ MageClient/Message/Transports/ShortPolling.cs | 76 +++++ .../Message/Transports/TransportClient.cs | 245 ++++++++++++++++ 9 files changed, 489 insertions(+), 543 deletions(-) delete mode 100644 MageClient/Message/Client/LongPolling.cs delete mode 100644 MageClient/Message/Client/ShortPolling.cs delete mode 100644 MageClient/Message/Client/TransportClient.cs create mode 100644 MageClient/Message/MessageStreamConfig.cs rename MageClient/Message/{Client => }/TransportType.cs (54%) create mode 100644 MageClient/Message/Transports/LongPolling.cs create mode 100644 MageClient/Message/Transports/ShortPolling.cs create mode 100644 MageClient/Message/Transports/TransportClient.cs diff --git a/MageClient/Message/Client/LongPolling.cs b/MageClient/Message/Client/LongPolling.cs deleted file mode 100644 index 0ad438e..0000000 --- a/MageClient/Message/Client/LongPolling.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; - -using Wizcorp.MageSDK.Log; -using Wizcorp.MageSDK.Network.Http; - -namespace Wizcorp.MageSDK.MageClient.Message.Client -{ - public class LongPolling : TransportClient - { - private static Mage Mage - { - get { return Mage.Instance; } - } - - private static Logger Logger - { - get { return Mage.Logger("longpolling"); } - } - - // Whether or not the poller is working - private bool running; - - // Required functions for poll requests - private Func getEndpoint; - private Func> getHeaders; - private Action processMessages; - - // - private HttpRequest currentRequest; - - // Required interval timer for polling delay - private int errorInterval; - private Timer intervalTimer; - - - // Constructor - public LongPolling(Func getEndpointFn, Func> getHeadersFn, Action processMessagesFn, int errorInterval = 5000) - { - getEndpoint = getEndpointFn; - getHeaders = getHeadersFn; - processMessages = processMessagesFn; - this.errorInterval = errorInterval; - } - - - // Starts the poller - public override void Start() - { - if (running) - { - return; - } - - Logger.Debug("Starting"); - running = true; - RequestLoop(); - } - - - // Stops the poller - public override void Stop() - { - running = false; - Logger.Debug("Stopping..."); - - if (intervalTimer != null) - { - intervalTimer.Dispose(); - intervalTimer = null; - } - else - { - Logger.Debug("Timer Stopped"); - } - - if (currentRequest != null) - { - currentRequest.Abort(); - currentRequest = null; - } - else - { - Logger.Debug("Connections Stopped"); - } - } - - - // Queues the next poll request - private void QueueNextRequest(int waitFor) - { - // Wait _requestInterval milliseconds till next poll - intervalTimer = new Timer( - state => { - RequestLoop(); - }, - null, - waitFor, - Timeout.Infinite); - } - - - // Poller request function - private void RequestLoop() - { - // Clear the timer - if (intervalTimer != null) - { - intervalTimer.Dispose(); - intervalTimer = null; - } - - // Check if the poller should be running - if (running == false) - { - Logger.Debug("Stopped"); - return; - } - - // Send poll request and wait for a response - string endpoint = getEndpoint(); - Logger.Debug("Sending request: " + endpoint); - currentRequest = HttpRequest.Get(endpoint, getHeaders(), Mage.Cookies, (requestError, responseString) => { - currentRequest = null; - - // Ignore errors if we have been stopped - if (requestError != null && !running) - { - Logger.Debug("Stopped"); - return; - } - - if (requestError != null) - { - var exception = requestError as HttpRequestException; - if (exception != null) - { - // Only log web exceptions if they aren't an empty response or gateway timeout - HttpRequestException requestException = exception; - if (requestException.Status != 0 && requestException.Status != 504) - { - Logger.Error("(" + requestException.Status.ToString() + ") " + exception.Message); - } - } - else - { - Logger.Error(requestError.ToString()); - } - - QueueNextRequest(errorInterval); - return; - } - - // Call the message processer hook and re-call request loop function - try - { - Logger.Verbose("Recieved response: " + responseString); - if (responseString != null) - { - processMessages(responseString); - } - - RequestLoop(); - } - catch (Exception error) - { - Logger.Error(error.ToString()); - QueueNextRequest(errorInterval); - } - }); - } - } -} \ No newline at end of file diff --git a/MageClient/Message/Client/ShortPolling.cs b/MageClient/Message/Client/ShortPolling.cs deleted file mode 100644 index 43ca6f0..0000000 --- a/MageClient/Message/Client/ShortPolling.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; - -using Wizcorp.MageSDK.Log; -using Wizcorp.MageSDK.Network.Http; - -namespace Wizcorp.MageSDK.MageClient.Message.Client -{ - public class ShortPolling : TransportClient - { - private static Mage Mage - { - get { return Mage.Instance; } - } - - private static Logger Logger - { - get { return Mage.Logger("shortpolling"); } - } - - // Whether or not the poller is working - private bool running; - - // Required functions for poll requests - private Func getEndpoint; - private Func> getHeaders; - private Action processMessages; - - // Required interval timer for polling delay - private int requestInterval; - private int errorInterval; - private Timer intervalTimer; - - - // Constructor - public ShortPolling(Func getEndpointFn, Func> getHeadersFn, Action processMessagesFn, int requestInterval = 5000, int errorInterval = 5000) - { - getEndpoint = getEndpointFn; - getHeaders = getHeadersFn; - processMessages = processMessagesFn; - this.requestInterval = requestInterval; - this.errorInterval = errorInterval; - } - - - // Starts the poller - public override void Start() - { - if (running) - { - return; - } - - Logger.Debug("Starting"); - running = true; - RequestLoop(); - } - - - // Stops the poller - public override void Stop() - { - if (intervalTimer != null) - { - intervalTimer.Dispose(); - intervalTimer = null; - Logger.Debug("Stopped"); - } - else - { - Logger.Debug("Stopping..."); - } - running = false; - } - - - // Queues the next poll request - private void QueueNextRequest(int waitFor) - { - // Wait _requestInterval milliseconds till next poll - intervalTimer = new Timer( - state => { - RequestLoop(); - }, - null, - waitFor, - Timeout.Infinite); - } - - - // Poller request function - private void RequestLoop() - { - // Clear the timer - if (intervalTimer != null) - { - intervalTimer.Dispose(); - intervalTimer = null; - } - - // Check if the poller should be running - if (running == false) - { - Logger.Debug("Stopped"); - return; - } - - // Send poll request and wait for a response - string endpoint = getEndpoint(); - Logger.Debug("Sending request: " + endpoint); - HttpRequest.Get(endpoint, getHeaders(), Mage.Cookies, (requestError, responseString) => { - if (requestError != null) - { - Logger.Error(requestError.ToString()); - QueueNextRequest(errorInterval); - return; - } - - // Call the message processer hook and queue the next request - try - { - processMessages(responseString); - QueueNextRequest(requestInterval); - } - catch (Exception error) - { - Logger.Data(responseString).Error(error.ToString()); - QueueNextRequest(errorInterval); - } - }); - } - } -} \ No newline at end of file diff --git a/MageClient/Message/Client/TransportClient.cs b/MageClient/Message/Client/TransportClient.cs deleted file mode 100644 index f78f526..0000000 --- a/MageClient/Message/Client/TransportClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Wizcorp.MageSDK.MageClient.Message.Client -{ - public abstract class TransportClient - { - public abstract void Stop(); - public abstract void Start(); - } -} \ No newline at end of file diff --git a/MageClient/Message/MessageStream.cs b/MageClient/Message/MessageStream.cs index 210c902..213803f 100644 --- a/MageClient/Message/MessageStream.cs +++ b/MageClient/Message/MessageStream.cs @@ -1,11 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; -using Newtonsoft.Json.Linq; - -using Wizcorp.MageSDK.Log; -using Wizcorp.MageSDK.MageClient.Message.Client; +using Wizcorp.MageSDK.MageClient.Message.Transports; #if UNITY_EDITOR using Wizcorp.MageSDK.Editor; @@ -15,252 +10,69 @@ namespace Wizcorp.MageSDK.MageClient.Message { public class MessageStream { - private static Mage Mage - { - get { return Mage.Instance; } - } + private TransportClient transport; + private const string MessageStreamPath = "msgstream"; - private static Logger Logger + public MessageStream(TransportType type = TransportType.LONGPOLLING) { - get { return Mage.Logger("messagestream"); } - } - - private List confirmIds; - - // Current message stack - private int currentMessageId; - - // Endpoint and credentials - private string endpoint; - private int largestMessageId; - private Dictionary messageQueue; - private string password; - private string sessionKey; - - // Current transport client - private TransportClient transportClient; - private string username; - - - // Constructor - public MessageStream(TransportType transport = TransportType.LONGPOLLING) - { - // - InitializeMessageList(); - - // Start transport client when session is acquired - Mage.EventManager.On("session.set", (sender, session) => { - sessionKey = UnityEngine.WWW.EscapeURL(session["key"].ToString()); - transportClient.Start(); - }); - - // Stop the message client when session is lost - Mage.EventManager.On("session.unset", (sender, reason) => { - transportClient.Stop(); - InitializeMessageList(); - sessionKey = null; - }); - - // Also stop the message client when the editor is stopped - #if UNITY_EDITOR - UnityEditorPlayMode.OnEditorModeChanged += newState => { - if (newState == EditorPlayModeState.Stopped) - { - transportClient.Stop(); - InitializeMessageList(); - sessionKey = null; - } - }; - #endif - - // Set the selected transport client (or the default) - SetTransport(transport); - } - + RegisterListener(); - // - private void InitializeMessageList() - { - currentMessageId = -1; - largestMessageId = -1; - messageQueue = new Dictionary(); - confirmIds = new List(); - - Logger.Debug("Initialized message queue"); - } - - - // - public void Dispose() - { - // Stop the transport client if it exists - if (transportClient != null) - { - transportClient.Stop(); - } - - InitializeMessageList(); - sessionKey = null; - } - - - // Updates URI and credentials - public void SetEndpoint(string url, string login = null, string pass = null) - { - endpoint = url + "/msgstream"; - username = login; - password = pass; - } - - - // Sets up given transport client type - public void SetTransport(TransportType transport) - { - // Stop existing transport client if any, when nulled out it will be collected - // by garbage collecter after existing connections have been terminated. - if (transportClient != null) + switch (type) { - transportClient.Stop(); - transportClient = null; - } - - // Create new transport client instance - switch (transport) { - case TransportType.SHORTPOLLING: - Func getShortPollingEndpoint = () => GetHttpPollingEndpoint("shortpolling"); - transportClient = new ShortPolling(getShortPollingEndpoint, GetHttpHeaders, ProcessMessagesString); - break; case TransportType.LONGPOLLING: - Func getLongPollingEndpoint = () => GetHttpPollingEndpoint("longpolling"); - transportClient = new LongPolling(getLongPollingEndpoint, GetHttpHeaders, ProcessMessagesString); + transport = new LongPolling(); + break; + case TransportType.SHORTPOLLING: + transport = new ShortPolling(); break; default: - throw new Exception("Invalid transport type: " + transport); + throw new Exception("Unknown TransportType for the message stream"); } } - - // Returns the endpoint URL for polling transport clients i.e. longpolling and shortpolling - private string GetHttpPollingEndpoint(string transport) - { - string url = endpoint + "?transport=" + transport + "&sessionKey=" + sessionKey; - if (confirmIds.Count > 0) - { - url += "&confirmIds=" + string.Join(",", confirmIds.ToArray()); - confirmIds.Clear(); - } - - return url; - } - - - // Returns the required HTTP headers - private Dictionary GetHttpHeaders() + // Updates URI and credentials + public void SetEndpoint(string url, string login = null, string pass = null) { - if (username == null && password == null) - { - return null; - } - - var headers = new Dictionary(); - string authInfo = username + ":" + password; - string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); - headers.Add("Authorization", "Basic " + encodedAuth); - return headers; + string streamUrl = string.Format("{0}/{1}", url, MessageStreamPath); + transport.Config = new MessageStreamConfig(streamUrl, login, pass); } - - // Deserilizes and processes given messagesString - private void ProcessMessagesString(string messagesString) + // Stop the transport client if it exists + public void Dispose() { - if (string.IsNullOrEmpty(messagesString)) + if (transport == null) { return; } - JObject messages = JObject.Parse(messagesString); - AddMessages(messages); - ProcessMessages(); + transport.Stop(); + transport.Reset(); + transport.Config.Session = null; } - // Add list of messages to message queue - private void AddMessages(JObject messages) + private void RegisterListener() { - if (messages == null) - { - return; - } - - int lowestMessageId = -1; - - foreach (KeyValuePair message in messages) - { - // Check if the messageId is lower than our current messageId - int messageId = int.Parse(message.Key); - if (messageId == 0) - { - messageQueue.Add(messageId, message.Value); - continue; - } - if (messageId < currentMessageId) - { - continue; - } - - // Keep track of the largest messageId in the list - if (messageId > largestMessageId) - { - largestMessageId = messageId; - } - - // Keep track of the lowest messageId in the list - if (lowestMessageId == -1 || messageId < lowestMessageId) - { - lowestMessageId = messageId; - } - - // Check if the message exists in the queue, if not add it - if (!messageQueue.ContainsKey(messageId)) - { - messageQueue.Add(messageId, message.Value); - } - } - - // If the current messageId has never been set, set it to the current lowest - if (currentMessageId == -1) - { - currentMessageId = lowestMessageId; - } - } + // Start transport client when session is acquired + Mage.Instance.EventManager.On("session.set", (sender, session) => { + transport.Config.Session = UnityEngine.WWW.EscapeURL(session["key"].ToString()); + transport.Start(); + }); + // Stop the message client when session is lost + Mage.Instance.EventManager.On("session.unset", (sender, reason) => { + Dispose(); + }); - // Process the message queue till we reach the end or a gap - private void ProcessMessages() - { - // Process all ordered messages in the order they appear - while (currentMessageId <= largestMessageId) - { - // Check if the next messageId exists - if (!messageQueue.ContainsKey(currentMessageId)) + // Also stop the message client when the editor is stopped + #if UNITY_EDITOR + UnityEditorPlayMode.OnEditorModeChanged += newState => { + if (newState == EditorPlayModeState.Stopped) { - break; + Dispose(); } - - // Process the message - Mage.EventManager.EmitEventList((JArray)messageQueue[currentMessageId]); - confirmIds.Add(currentMessageId.ToString()); - messageQueue.Remove(currentMessageId); - - currentMessageId += 1; - } - - // Finally emit any events that don't have an ID and thus don't need confirmation and lack order - if (messageQueue.ContainsKey(0)) - { - Mage.EventManager.EmitEventList((JArray)messageQueue[0]); - messageQueue.Remove(0); - } + }; + #endif } } } \ No newline at end of file diff --git a/MageClient/Message/MessageStreamConfig.cs b/MageClient/Message/MessageStreamConfig.cs new file mode 100644 index 0000000..72d005b --- /dev/null +++ b/MageClient/Message/MessageStreamConfig.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Wizcorp.MageSDK.MageClient.Message +{ + public class MessageStreamConfig + { + private string endpoint; + private string username; + private string password; + public string Session { private get; set; } + + public MessageStreamConfig(string endpoint, string username, string password) + { + this.endpoint = endpoint; + this.username = username; + this.password = password; + } + + public Dictionary GetHeaders() + { + var headers = new Dictionary(); + string authInfo = string.Format("{0}:{1}", username, password); + string encodedAuth = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + headers.Add("Authorization", "Basic " + encodedAuth); + return headers; + } + + public string GetEndpoint(string transport, List confirmIds = null) + { + string url = endpoint + "?transport=" + transport; + if (!string.IsNullOrEmpty(Session)) + { + url += "&sessionKey=" + Session; + } + + if (confirmIds != null && confirmIds.Count > 0) + { + url += "&confirmIds=" + string.Join(",", confirmIds.ToArray()); + } + + return url; + } + } +} \ No newline at end of file diff --git a/MageClient/Message/Client/TransportType.cs b/MageClient/Message/TransportType.cs similarity index 54% rename from MageClient/Message/Client/TransportType.cs rename to MageClient/Message/TransportType.cs index 39d907c..99d9101 100644 --- a/MageClient/Message/Client/TransportType.cs +++ b/MageClient/Message/TransportType.cs @@ -1,4 +1,4 @@ -namespace Wizcorp.MageSDK.MageClient.Message.Client +namespace Wizcorp.MageSDK.MageClient.Message { public enum TransportType { diff --git a/MageClient/Message/Transports/LongPolling.cs b/MageClient/Message/Transports/LongPolling.cs new file mode 100644 index 0000000..8a1f3be --- /dev/null +++ b/MageClient/Message/Transports/LongPolling.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; + +using Wizcorp.MageSDK.Network.Http; + +namespace Wizcorp.MageSDK.MageClient.Message.Transports +{ + public class LongPolling : TransportClient + { + private List requestedIds; + public LongPolling(int errorInterval = 5000) : base(errorInterval) {} + + protected override void Request() + { + // Clear the timer + if (IntervalTimer != null) + { + IntervalTimer.Dispose(); + IntervalTimer = null; + } + + // Check if the poller should be running + if (Running == false) + { + UnityEngine.Debug.Log(this + "Stopped"); + return; + } + + // Get Request parameters + requestedIds = new List(ConfirmIds); + string endpoint = Config.GetEndpoint("longpolling", requestedIds); + Dictionary headers = Config.GetHeaders(); + + // Send poll request and wait for a response + UnityEngine.Debug.Log(this + "Sending request: " + endpoint); + CurrentRequest = HttpRequest.Get(endpoint, headers, Mage.Instance.Cookies, Received); + } + + protected override void Received(Exception requestError, string responseString) + { + CurrentRequest = null; + + // Ignore errors if we have been stopped + if (requestError != null && !Running) + { + UnityEngine.Debug.Log(this + "Stopped"); + return; + } + + // Manage Network Exception + if (requestError != null) + { + RequestException(requestError); + return; + } + + // No error, we can clean messages Ids + CleanConfirmIds(requestedIds); + + // Call the message processer hook and re-call request loop function + try + { + UnityEngine.Debug.Log(this + "Received response: " + responseString); + if (responseString != null) + { + ProcessMessagesString(responseString); + } + + Request(); + } + catch (Exception error) + { + UnityEngine.Debug.LogError(error.ToString()); + NextRequest(ErrorInterval); + } + } + + public override string ToString() + { + return "[MessageStream.LongPolling]"; + } + } +} \ No newline at end of file diff --git a/MageClient/Message/Transports/ShortPolling.cs b/MageClient/Message/Transports/ShortPolling.cs new file mode 100644 index 0000000..fc287cf --- /dev/null +++ b/MageClient/Message/Transports/ShortPolling.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; + +using Wizcorp.MageSDK.Network.Http; + +namespace Wizcorp.MageSDK.MageClient.Message.Transports +{ + public class ShortPolling : TransportClient + { + private List requestedIds; + public ShortPolling(int errorInterval = 5000, int requestInterval = 5000) : base(errorInterval, requestInterval) {} + + protected override void Request() + { + // Clear the timer + if (IntervalTimer != null) + { + IntervalTimer.Dispose(); + IntervalTimer = null; + } + + // Check if the poller should be running + if (Running == false) + { + UnityEngine.Debug.Log(this + "Stopped"); + return; + } + + // Get Request parameters + requestedIds = new List(ConfirmIds); + string endpoint = Config.GetEndpoint("longpolling", requestedIds); + Dictionary headers = Config.GetHeaders(); + + // Send poll request and wait for a response + UnityEngine.Debug.Log(this + "Sending request: " + endpoint); + CurrentRequest = HttpRequest.Get(endpoint, headers, Mage.Instance.Cookies, Received); + } + + protected override void Received(Exception requestError, string responseString) + { + // Ignore errors if we have been stopped + if (requestError != null && !Running) + { + UnityEngine.Debug.Log(this + "Stopped"); + return; + } + + // Manage Network Exception + if (requestError != null) + { + RequestException(requestError); + return; + } + + // No error, we can clean messages Ids + CleanConfirmIds(requestedIds); + + // Call the message processer hook and queue the next request + try + { + ProcessMessagesString(responseString); + NextRequest(RequestInterval); + } + catch (Exception error) + { + UnityEngine.Debug.LogError(error.ToString()); + NextRequest(ErrorInterval); + } + } + + public override string ToString() + { + return "[MessageStream.ShortPolling]"; + } + } +} \ No newline at end of file diff --git a/MageClient/Message/Transports/TransportClient.cs b/MageClient/Message/Transports/TransportClient.cs new file mode 100644 index 0000000..e42bb88 --- /dev/null +++ b/MageClient/Message/Transports/TransportClient.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +using Newtonsoft.Json.Linq; + +using Wizcorp.MageSDK.Network.Http; + +namespace Wizcorp.MageSDK.MageClient.Message.Transports +{ + public abstract class TransportClient + { + public MessageStreamConfig Config { get; set; } + + // + protected List ConfirmIds; + + // Current message stack + private int currentMessageId; + protected HttpRequest CurrentRequest; + protected int ErrorInterval; + protected Timer IntervalTimer; + + // Endpoint and credentials + private int largestMessageId; + private Dictionary messageQueue; + + // Required interval timer for polling delay + protected int RequestInterval; + + protected bool Running; + + + protected TransportClient(int errorInterval = 5000, int requestInterval = 5000) + { + currentMessageId = -1; + largestMessageId = -1; + RequestInterval = requestInterval; + ErrorInterval = errorInterval; + messageQueue = new Dictionary(); + ConfirmIds = new List(); + } + + public override string ToString() + { + return "[MessageStream.Transport]"; + } + + #region Public API + + public void Reset() + { + currentMessageId = -1; + largestMessageId = -1; + messageQueue = new Dictionary(); + ConfirmIds = new List(); + + UnityEngine.Debug.Log(this + "Initialized message queue"); + } + + public void Start() + { + if (Running) + { + return; + } + + UnityEngine.Debug.Log(this + "Starting"); + Running = true; + Request(); + } + + public void Stop() + { + Running = false; + UnityEngine.Debug.Log(this + "Stopping..."); + + if (IntervalTimer != null) + { + IntervalTimer.Dispose(); + IntervalTimer = null; + } + else + { + UnityEngine.Debug.Log(this + "Timer Stopped"); + } + + if (CurrentRequest != null) + { + CurrentRequest.Abort(); + CurrentRequest = null; + } + else + { + UnityEngine.Debug.Log(this + "Connections Stopped"); + } + } + + #endregion + + #region Network + + protected void CleanConfirmIds(List messageIds) + { + //UnityEngine.Debug.Log(this + "Request Process, ConfirmIds to clean: " + string.Join(",", messageIds.ToArray())); + ConfirmIds.RemoveAll(messageIds.Contains); + } + + protected void RequestException(Exception requestError) + { + var exception = requestError as HttpRequestException; + if (exception != null) + { + // Only log web exceptions if they aren't an empty response or gateway timeout + HttpRequestException requestException = exception; + if (requestException.Status != 0 && requestException.Status != 504) + { + UnityEngine.Debug.Log("(" + requestException.Status.ToString() + ") " + exception.Message); + } + } + else + { + UnityEngine.Debug.Log(requestError.ToString()); + } + + NextRequest(ErrorInterval); + } + + // Queues the next poll request + protected void NextRequest(int waitFor) + { + IntervalTimer = new Timer( + state => { + Request(); + }, + null, + waitFor, + Timeout.Infinite); + } + + // Method used to reveived the message from the server + protected abstract void Request(); + + protected abstract void Received(Exception requestError, string responseString); + + #endregion + + #region Process Messages Received + + // Deserializes and processes given messagesString + protected void ProcessMessagesString(string messagesString) + { + if (string.IsNullOrEmpty(messagesString)) + { + return; + } + + JObject messages = JObject.Parse(messagesString); + AddMessages(messages); + ProcessMessages(); + } + + // Add list of messages to message queue + private void AddMessages(JObject messages) + { + if (messages == null) + { + return; + } + + int lowestMessageId = -1; + + foreach (KeyValuePair message in messages) + { + // Check if the messageId is lower than our current messageId + int messageId = int.Parse(message.Key); + if (messageId == 0) + { + messageQueue.Add(messageId, message.Value); + continue; + } + + if (messageId < currentMessageId) + { + continue; + } + + // Keep track of the largest messageId in the list + if (messageId > largestMessageId) + { + largestMessageId = messageId; + } + + // Keep track of the lowest messageId in the list + if (lowestMessageId == -1 || messageId < lowestMessageId) + { + lowestMessageId = messageId; + } + + // Check if the message exists in the queue, if not add it + if (!messageQueue.ContainsKey(messageId)) + { + messageQueue.Add(messageId, message.Value); + } + } + + // If the current messageId has never been set, set it to the current lowest + if (currentMessageId == -1) + { + currentMessageId = lowestMessageId; + } + } + + // Process the message queue till we reach the end or a gap + private void ProcessMessages() + { + // Process all ordered messages in the order they appear + while (currentMessageId <= largestMessageId) + { + // Check if the next messageId exists + if (!messageQueue.ContainsKey(currentMessageId)) + { + UnityEngine.Debug.LogWarning(this + "ProcessMessages " + currentMessageId + " already exist"); + break; + } + + // Process the message + Mage.Instance.EventManager.EmitEventList((JArray)messageQueue[currentMessageId]); + ConfirmIds.Add(currentMessageId.ToString()); + messageQueue.Remove(currentMessageId); + + currentMessageId += 1; + } + + // Finally emit any events that don't have an ID and thus don't need confirmation and lack order + if (messageQueue.ContainsKey(0)) + { + Mage.Instance.EventManager.EmitEventList((JArray)messageQueue[0]); + messageQueue.Remove(0); + } + } + + #endregion + } +} \ No newline at end of file