Monday, January 09, 2017

Better client-server communication: replacing AJAX with WebSockets


We have quite complex reporting application, that was done several dozens AJAX requests from client side to generate a single report. Typically it is either JSON of query results from Google BigQuery or HTML generated by a partial form based on such query results.
Each query could take several seconds to fulfill,  and in view of browser limitations for the number of simultaneous connections, the throttling was visually annoying.

I've got a request to parallelize the requests whenever possible. There were choice of server pulling vs. WebSockets pipeline, so latest is definitely more nice and interesting.

Below is the skeleton of the solution in C# and JavaScript.

The server implementation has several interesting moments: except of WebSockets itself, it provides the partial views by rendering them server-side, preserving the context and locale of ASP.NET main thread. Not sure if it is nice approach, but it works so far.

Service implementation:
    public class ReportsApiController : ApiController    {        [System.Web.Http.HttpGet]        public HttpResponseMessage GetQueryResultByWebSocket()        {            HttpContext.Current.AcceptWebSocketRequest(new MyWebSocketHandler(this));            return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);        }
                           ....
    }
    internal class MyWebSocketHandler : WebSocketHandler
    {
        private static WebSocketCollection _connectedClients;
        private ApiController _controller;
        static MyWebSocketHandler()
        {
            _connectedClients = new WebSocketCollection();
        }
        public MyWebSocketHandler(ApiController controller)
        {
            this._controller = controller;
        }
        public override void OnOpen()
        {
            base.OnOpen();
            _connectedClients.Add(this);
        }
        public override void OnClose()
        {
            _connectedClients.Remove(this);
            base.OnClose();
        }
        public override void OnMessage(string message)
        {
            HttpContext ctx = HttpContext.Current;
            var currentCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
            var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture;
            new Task(() =>
            {
                HttpContext.Current = ctx;
                System.Threading.Thread.CurrentThread.CurrentCulture = currentCulture;
                System.Threading.Thread.CurrentThread.CurrentUICulture = currentUICulture;
                var request =
                    JsonConvert.DeserializeObject(message);
                Debug.WriteLine("Received #{0} @{1} t:{2}", request.id, DateTime.Now,
                    Thread.CurrentThread.ManagedThreadId);
                try
                {
                    if (request.url == "/api/ReportsApi/GetQueryResult")
                    {
                        var data = MyQueriesModel.GetQueryResult(request.queryParams);
                        UmiWebSocketResponse result = new UmiWebSocketResponse
                        {
                            data = data,
                            error = null,
                            id = request.id,
                            timeStampSent = request.timeStampSent
                        };
                        Debug.WriteLine("Sent #{0} @{1} t:{2}", request.id, DateTime.Now,
                            Thread.CurrentThread.ManagedThreadId);
                        this.Send(JsonConvert.SerializeObject(result));
                    }
                    else if (request.url == "/api/ReportsApi/GetQueryObjResult")
                    {
                        var jss = new JavaScriptSerializer();
                        jss.MaxJsonLength = int.MaxValue;
                        var data = jss.Serialize(UmiQueriesModel.GetQueryObj(request.queryParams));
                        UmiWebSocketResponse result = new UmiWebSocketResponse
                        {
                            data = data,
                            error = null,
                            id = request.id,
                            timeStampSent = request.timeStampSent
                        };
                        Debug.WriteLine("Sent #{0} @{1} t:{2}", request.id, DateTime.Now,
                            Thread.CurrentThread.ManagedThreadId);
                        this.Send(JsonConvert.SerializeObject(result));
                    }
                    else if (request.url == "/api/ReportsApi/GetResource")
                    {
                        var jss = new JavaScriptSerializer();
                        jss.MaxJsonLength = int.MaxValue;
                        var prefix = "MyLibrary.Resources.Reports.";
                        try
                        {
                            var fullNames = request.queryParams.QueryName.Split(',');
                            var data = new Dictionary();
                            foreach (var fullName in fullNames)
                            {
                                int idx = fullName.LastIndexOf('.');
                                var typeName = fullName.Substring(0, idx);
                                var fullTypeName = typeName;
                                var resourceName = fullName.Substring(idx + 1);
                                if (!fullTypeName.StartsWith(prefix))
                                    fullTypeName = prefix + fullTypeName;
                                try
                                {
                                    var rm = new ResourceManager(fullTypeName, Assembly.GetExecutingAssembly());
                                    data.Add(typeName + "." + resourceName, rm.GetString(resourceName));
                                }
                                catch (Exception ex)
                                {
                                    Debug.WriteLine("Error performing resource request: {0}",
                                        request.queryParams == null ? "null" : request.queryParams.QueryName);
                                    data.Add(typeName + "." + resourceName, "");
                                }
                            }
                            UmiWebSocketResponse result = new UmiWebSocketResponse
                            {
                                data = jss.Serialize(data),
                                error = null,
                                id = request.id,
                                timeStampSent = request.timeStampSent
                            };
                            Debug.WriteLine("Sent #{0} @{1} t:{2}", request.id, DateTime.Now,
                                Thread.CurrentThread.ManagedThreadId);
                            this.Send(jss.Serialize(result));
                        }
                        catch (Exception ex)
                        {
                            Debug.WriteLine("Error performing resource request: {0}",
                                request.queryParams == null ? "null" : request.queryParams.QueryName);
                            UmiWebSocketResponse result = new UmiWebSocketResponse
                            {
                                data = "",
                                error = "Error performing resource request",
                                id = request.id,
                                timeStampSent = request.timeStampSent
                            };
                            Debug.WriteLine("Sent #{0} @{1} t:{2}", request.id, DateTime.Now,
                                Thread.CurrentThread.ManagedThreadId);
                            this.Send(jss.Serialize(result));
                        }
                    }
                    else
                    {
                        var routeToSearch = request.url.Replace("/PartialViews/", "");
                        var methods = Assembly.GetExecutingAssembly().GetTypes()
                          .Where(t => t.IsSubclassOf(typeof(PartialViewsController)))
                          .SelectMany(t => t.GetMethods())
                          .Where(m =>
                          {
                              if (m.Name == routeToSearch)
                                  return true;
                              System.Web.Mvc.RouteAttribute attr =
                                  m.GetCustomAttributes(typeof(System.Web.Mvc.RouteAttribute), false).FirstOrDefault() as System.Web.Mvc.RouteAttribute;
                              if (attr == null)
                                  return false;
                              return attr.Template == routeToSearch;
                          })
                          .ToArray();
                        if (methods.Length > 0)
                        {
                            var controller = Activator.CreateInstance(methods[0].DeclaringType) as PartialViewsController;
                            string baseUrl = HttpContext.Current
                                .Request
                                .Url
                                .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped);
                            var routeData = new RouteData();
                            routeData.Values.Add("controller", controller.GetType().Name.Replace("Controller", ""));
                            var controllerContext = new ControllerContext(
                                new HttpContextWrapper(
                                    new HttpContext(
                                        new HttpRequest(null, baseUrl + request.url, null),
                                        new HttpResponse(null))),
                                routeData, controller);
                            controller.ControllerContext = controllerContext;
                            PartialViewResult pvr;
                            if (methods[0].GetParameters().Length == 2)
                            {
                                pvr = methods[0].Invoke(controller, new object[] { request.queryParams, Type.Missing }) as PartialViewResult;
                            }
                            else if (methods[0].GetParameters().Length == 1)
                            {
                                pvr = methods[0].Invoke(controller, new object[] { request.queryParams }) as PartialViewResult;
                            }
                            else
                            {
                                pvr = methods[0].Invoke(controller, new object[0]) as PartialViewResult;
                            }
                            var data = pvr == null ? null : ConvertPartialViewToString(controller.ControllerContext, pvr);
                            UmiWebSocketResponse result = new UmiWebSocketResponse
                            {
                                data = data,
                                error = null,
                                id = request.id,
                                timeStampSent = request.timeStampSent
                            };
                            Debug.WriteLine("Sent #{0} @{1} t:{2}", request.id, DateTime.Now,
                                Thread.CurrentThread.ManagedThreadId);
                            this.Send(JsonConvert.SerializeObject(result));
                        }
                    }
                }
                catch (Exception ex)
                {
                    UmiWebSocketResponse errorResult = new UmiWebSocketResponse
                    {
                        error = ex.Message,
                        data = null,
                        id = request.id,
                        timeStampSent = request.timeStampSent
                    };
                    this.Send(JsonConvert.SerializeObject(errorResult));
                }
            }).Start();
        }
        private string ConvertPartialViewToString(ControllerContext controllerContext, PartialViewResult partialView)
        {
            using (var sw = new StringWriter())
            {
                partialView.View = ViewEngines.Engines
                  .FindPartialView(controllerContext, partialView.ViewName).View;
                var vc = new ViewContext(
                  controllerContext, partialView.View, partialView.ViewData, partialView.TempData, sw);
                partialView.View.Render(vc, sw);
                var partialViewString = sw.GetStringBuilder().ToString();
                return partialViewString;
            }
        }
    }



Client code sample:
    my.infrastructure.webSocketHandler.send({ dataType: 'html', url: "/PartialViews/MyPartialView" }, function (result) {        // do something with result    });


JavaScript client library implementation:

my.infrastructure.useWebSocket = true;
my.infrastructure.webSocketHandler = (function () {
    var uri = (location.protocol === "https:" ? "wss://" : "ws://").concat(window.location.hostname).concat("/api/reportsApi/GetQueryResultByWebSocket");
    var requests = {
    };
    var counter = 0;
    var websocket;
    if (my.infrastructure.useWebSocket) {
        websocket = new WebSocket(uri);
        websocket.onopen = function () {
            console.log("Connected to Web Socket");
        };
        websocket.onerror = function (event) {
            console.log("ERROR CONNECTING TO WEB SOCKET");
        };
        websocket.onmessage = function (event) {
            var message = JSON.parse(event.data);
            message.timeStampReceived = new Date();
            var id = message.id;
            if (requests[id]) {
                var request = requests[id];
                console.log(message.timeStampReceived.toISOString() + ": Web Socket response received from " + request.request.url + " (queryName: " + (request.request.queryParams ? request.request.queryParams.queryName : "undefined") + "): "
                    //+ JSON.stringify(message)
                    );
                if (message.Error) {
                    request.onError(message.error);
                } else {
                    var data = message.data;
                    request.onSuccess(data);
                }
                delete requests[request[id]];
            }
        };
    }
    function sendRequest(settings, onSuccess, onError) {
        if (!my.infrastructure.useWebSocket) {
            // do some AJAX if asked for
        }
        var id = counter++;
        var message = {
            id: id,
            queryParams: settings.data,
            url: settings.url,
            type: settings.type,
            dataType: settings.dataType,
            timeStampSent: new Date()
        };
        var messageStr = JSON.stringify(message);
        requests[id] = {
            onSuccess: onSuccess, onError: onError, request: message
        };
        waitForSocketConnection(websocket, function () {
            console.log(message.timeStampSent.toISOString() + ": Web Socket message sent to " + settings.url + " (queryName: " + (message.queryParams ? message.queryParams.queryName : "undefined") + "): "
                //+ messageStr
                );
            websocket.send(messageStr);
        });
    }
    function abortAll() {
        requests = {
        };
    }
    function abortQuery(queryName) {
        $.each(requests, function (index, request) {
            if (request && request.request.queryName === queryName) {
                delete requests[request[id]];
            }
        });
    }
    function waitForSocketConnection(socket, callback) {
        var retryCounter = 3000; // 5 minutes
        setTimeout(
            function () {
                if (socket.readyState === 1) {
                    if (callback != null) {
                        callback();
                    }
                    return;
                } else {
                    if (retryCounter-- > 0) {
                        console.log("Waiting for Web Socket connection...");
                        waitForSocketConnection(socket, callback);
                    }
                }
            }, 100);
    }
    return {
        send: sendRequest,
        abortAll: abortAll,
        abortQuery: abortQuery
    }
})();

Sunday, January 01, 2017

Diving into Angular 2 and NODE.JS world


Hi Everybody,

I will post here my findings in a way of learning Angular 2, VS.CODE and probably MongoDB. So keep updated.

Avoiding the dependencies versioning hell

Once I was stuck with non-compilable NPM-based project because of newly added libraries have brought their incompatible dependencies' version. It was sad and I expected a lot of manual fixes.
But the life is better! Thanks josh3736 from StackOverflow:


Looks like npm-check-updates is the only way to make this happen now.
npm i -g npm-check-updates
npm-check-updates -u
npm install

http://stackoverflow.com/questions/16073603/how-do-i-update-each-dependency-in-package-json-to-the-latest-version