Asynchronous, cached loading of partial MVC views

I have a scenario where an MVC Razr page contains several components that need to be loaded using integrations. These are slow, prone to throttling from the source system and I want the page to load quickly.

The integration interfaces are asynchronous and I could override them and wait, but again I want the page to load as quickly as possible so the more threads the better.

Here, I am loading the views using jQuery and AJAX instead and caching the result data into local storage on the client machine.

The markup is easy:

<div id="sample-component" 
     data-action="/targetController/targetAction" 
     data-refresh="5" 
     data-script="methodToCallOnSuccess()"></div>

The data-action points to the controller action that will respond, the data-refresh sets the timeout in minutes (to automatically refresh and to tell when the cached data is stale) and the data-script is the method (optional) to call when the contents has rendered. You cannot add $(document).ready tags in the view, this will not evaluate.

The data-refresh could be set to 1, and then automatically refresh the data every minute as the page is kept open without the need to refresh the entire page.

The logic is controlled by the components script module, which has the following methods:

components.reload(id): Reloads the HTML content within the DIV with the specified ID and refreshes the cache.

components.debug(): Disables caching during the browser session. Useful during development as the action is called on each page load.

The other code is automatically called on load. Every element that has a data-action attribute will be rendered. The source code for the script module is below. The "log" calls below are for my logging framework, replace with your own or simply use console.log.

Hope this helps!

/// <reference path="../jquery-3.3.1.intellisense.js"/>

var components = window.components || {};

(function (module, $) {

    //#region Public functions

    module.reload = function(name, content, postLoadScript) {
        /// <summary>
        /// Reloads the component with the specified name.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="content">The content.</param>
        /// <param name="script">The script to execute after content is loaded.</param>

        var component = $("#" + name);

        if (!component) {
            log.add("Component '" + name + "' not found", 
                          "components.reload", 
                          new Error());
            return;
        }

        if (content && content.length > 0) {
            // Content submitted, so set.
            component.html(content);
            if (postLoadScript != null) {
                postLoadScript();
            }

            localStorage.setItem(component.attr("id"), content);
            localStorage.setItem(component.attr("id") + "-cache-time", 
                         new Date().toISOString());
            return;
        }

        // Load from data action
        load(component);
        if (postLoadScript != null) {
            postLoadScript();
        }
    }

    module.debug = function () {
        /// <summary>
        /// Enables debug and disables page level caching for components, forcing a refresh directly on apge load.
        /// </summary>

        sessionStorage.componentEnableDebug = true;
    };

    
    //#endregion

    //#region Private functions

    function load(element) {
        /// <summary>
        /// Loads the data for the specified component.
        /// </summary>
        /// <param name="element">The container element for the component.</param>

        var id = element.attr("id");
        if (!element.hasClass("loaded")) {
            element.addClass("loading");
        }

        // Load with live data
        $.ajax({
            url: element.attr("data-action"),
            type: "GET",
            xhrFields: { withCredentials: true },
            success: function (html) {
                // Render new data
                element.html(html);
                element.addClass("loaded");

                runScripts(element);

                // Save data for future page reloads
                localStorage.setItem(id, html);
                localStorage.setItem(id + "-cache-time", new Date().toISOString());
                log.add("Refreshed " + id, "components.load");

                // Auto refresh may be needed
                if (element.attr("data-refresh") != null && 
                                             element.attr("data-refresh") !== "0") {
              
                    // Create a timer to the value set by the component
                    var timeout = (parseInt(element.attr("data-refresh")) * 60 * 1000) + 1000;
                    if (timeout > 0) {
                        setTimeout(function() {
                                load(element);
                            },
                            timeout);
                    }
                }

                element.removeClass("loading");
            },
            error: function (e,ee) {
                log.add("Unable to get view data for " + id + ".", 
                              "components.load", ee);
                element.hide();
            }
        });
    }

    function expired(name, timeout) {
        /// <summary>
        /// Checks if cache has expired for the specified component.
        /// </summary>
        /// <param name="name">The name of the component.</param>
        /// <param name="timeout">The timeout in minutes.</param>
        /// <returns>true if expired, false if not.</returns>

        if (sessionStorage.componentEnableDebug === "true") {
            return true;
        }

        if (timeout == null || timeout.length === 0) {
            return false;
        }

        var cacheTime = localStorage.getItem(name + "-cache-time");
        if (cacheTime != null && cacheTime.length > 0) {
            var now = new Date();
            var added = null;
            try {
                added = new Date(cacheTime);
            } catch (e) {
            }

            if (added != null) {
                var diffMs = (now - added); // milliseconds between now & last added
                var diffDays = Math.round(diffMs / 86400000); // days
                var diffHrs = Math.round((diffMs % 86400000) / 3600000); // hours
                var diffMins = Math.floor(((diffMs % 86400000) % 3600000) / 60000); // minutes

                if (diffDays === 0 && diffHrs === 0 && diffMins <= parseInt(timeout)) {
                    return false;
                }
            }
        }

        return true;
    }

    function runScripts(element) {
        /// <summary>
        /// Evaluates any load scripts of the component if configured.
        /// </summary>
        /// <param name="element">The container element for the component.</param>

        // Do we need to execute any scripts?
        if (element.attr("data-script") !== "") {
            try {
                eval(element.attr("data-script"));
            } catch (ee) {
                entryPoint.log.add("Unable to evaluate script.", "entryPoint.components.save", ee);
            }
        }
    }

    function init() {
        /// <summary>
        /// Loads on page start. Will loop all DIV elements that have actions defined and load these asynchronously
        /// in multiple threads.
        /// </summary>

        try {
            // This syntax picks up all elements where data-action is not null
            // Any element with 'data-action' should call an asynchronous action
            // using AJAX
            $("[data-action!=\"\"][data-action]").each(function () {
                var element = $(this);
                if (element.attr("data-action")) {
                    var id = element.attr("id");

                    // Pre-loaded?
                    if (element.hasClass("loaded")) {
                        return;
                    }

                    // Can we fill temporary data?
                    if (sessionStorage.componentEnableDebug !== "true" && 
                        localStorage.getItem(id) != null) {

                        element.addClass("loaded");

                        // Fill with the temporary data, makes load time appear much faster
                        element.html(localStorage.getItem(id));

                       runScripts(element);

                        log.add("Loaded " + id + " from cache.", "components.init");
                    }

                    if (expired(id, element.attr("data-refresh"))) {
                        load(element);
                    }
                }
            });
        } catch (e) {
            log.add("Unable to initialize component", "components.init", e);
        }
    }

    //#endregion

    //#region Init

    init();

    //#endregion

})(components, jQuery);