MDL-64348 javascript: change template loading to a buffer
authorRyan Wyllie <ryan@moodle.com>
Tue, 11 Dec 2018 06:53:00 +0000 (14:53 +0800)
committerRyan Wyllie <ryan@moodle.com>
Mon, 7 Jan 2019 01:03:06 +0000 (09:03 +0800)
Changed the template loading to buffer the requests for templates
so that they can be sent in batches to the server to save large
volumes of network requests.

lib/amd/build/templates.min.js
lib/amd/src/templates.js

index 8d03918..b5be0b1 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 091d989..748cea5 100644 (file)
@@ -59,6 +59,191 @@ define([
     /** @var {Object} iconSystem - Object extending core/iconsystem */
     var iconSystem = {};
 
+    /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */
+    var loadTemplateBuffer = [];
+
+    /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */
+    var isLoadingTemplates = false;
+
+    /**
+     * Search the various caches for a template promise for the given search key.
+     * The search key should be in the format <theme>/<component>/<template> e.g. boost/core/modal.
+     *
+     * If the template is found in any of the caches it will populate the other caches with
+     * the same data as well.
+     *
+     * @param {String} searchKey The template search key in the format <theme>/<component>/<template> e.g. boost/core/modal
+     * @return {Object} jQuery promise resolved with the template source
+     */
+    var getTemplatePromiseFromCache = function(searchKey) {
+        // First try the cache of promises.
+        if (searchKey in templatePromises) {
+            return templatePromises[searchKey];
+        }
+
+        // Check the module cache.
+        if (searchKey in templateCache) {
+            // Add this to the promises cache for future.
+            templatePromises[searchKey] = $.Deferred().resolve(templateCache[searchKey]).promise();
+            return templatePromises[searchKey];
+        }
+
+        // Now try local storage.
+        var cached = storage.get('core_template/' + searchKey);
+        if (cached) {
+            // Add this to the module cache for future.
+            templateCache[searchKey] = cached;
+            // Add this to the promises cache for future.
+            templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
+            return templatePromises[searchKey];
+        }
+
+        return null;
+    };
+
+    /**
+     * Take all of the templates waiting in the buffer and load them from the server
+     * or from the cache.
+     *
+     * All of the templates that need to be loaded from the server will be batched up
+     * and sent in a single network request.
+     */
+    var processLoadTemplateBuffer = function() {
+        if (!loadTemplateBuffer.length) {
+            return;
+        }
+
+        if (isLoadingTemplates) {
+            return;
+        }
+
+        isLoadingTemplates = true;
+        // Grab any templates waiting in the buffer.
+        var templatesToLoad = loadTemplateBuffer.slice();
+        // This will be resolved with the list of promises for the server request.
+        var serverRequestsDeferred = $.Deferred();
+        var requests = [];
+        // Get a list of promises for each of the templates we need to load.
+        var templatePromises = templatesToLoad.map(function(templateData) {
+            var component = templateData.component;
+            var name = templateData.name;
+            var searchKey = templateData.searchKey;
+            var theme = templateData.theme;
+            var templateDeferred = templateData.deferred;
+            var promise = null;
+
+            // Double check to see if this template happened to have landed in the
+            // cache as a dependency of an earlier template.
+            var cachedPromise = getTemplatePromiseFromCache(searchKey);
+            if (cachedPromise) {
+                // We've seen this template so immediately resolve the existing promise.
+                promise = cachedPromise;
+            } else {
+                // We haven't seen this template yet so we need to request it from
+                // the server.
+                requests.push({
+                    methodname: 'core_output_load_template_with_dependencies',
+                    args: {
+                        component: component,
+                        template: name,
+                        themename: theme
+                    }
+                });
+                // Remember the index in the requests list for this template so that
+                // we can get the appropriate promise back.
+                var index = requests.length - 1;
+
+                // The server deferred will be resolved with a list of all of the promises
+                // that were sent in the order that they were added to the requests array.
+                promise = serverRequestsDeferred.promise()
+                    .then(function(promises) {
+                        // The promise for this template will be the one that matches the index
+                        // for it's entry in the requests array.
+                        //
+                        // Make sure the promise is added to the promises cache for this template
+                        // search key so that we don't request it again.
+                        templatePromises[searchKey] = promises[index].then(function(response) {
+                            var templateSource = null;
+
+                            // Process all of the template dependencies for this template and add
+                            // them to the caches so that we don't request them again later.
+                            response.templates.forEach(function(data) {
+                                // Generate the search key for this template in the response so that we
+                                // can add it to the caches.
+                                var tempSearchKey = [theme, data.component, data.name].join('/');
+                                // Cache all of the dependent templates because we'll need them to render
+                                // the requested template.
+                                templateCache[tempSearchKey] = data.value;
+                                storage.set('core_template/' + tempSearchKey, data.value);
+
+                                if (data.component == component && data.name == name) {
+                                    // This is the original template that was requested so remember it to return.
+                                    templateSource = data.value;
+                                }
+                            });
+
+                            if (response.strings.length) {
+                                // If we have strings that the template needs then warm the string cache
+                                // with them now so that we don't need to re-fetch them.
+                                str.cache_strings(response.strings.map(function(data) {
+                                    return {
+                                        component: data.component,
+                                        key: data.name,
+                                        value: data.value
+                                    };
+                                }));
+                            }
+
+                            // Return the original template source that the user requested.
+                            return templateSource;
+                        });
+
+                        return templatePromises[searchKey];
+                    });
+            }
+
+            return promise
+                .then(function(source) {
+                    // When we've successfully loaded the template then resolve the deferred
+                    // in the buffer so that all of the calling code can proceed.
+                    return templateDeferred.resolve(source);
+                })
+                .catch(function(error) {
+                    // If there was an error loading the template then reject the deferred
+                    // in the buffer so that all of the calling code can proceed.
+                    templateDeferred.reject(error);
+                    // Rethrow for anyone else listening.
+                    throw error;
+                });
+        });
+
+        if (requests.length) {
+            // We have requests to send so resolve the deferred with the promises.
+            serverRequestsDeferred.resolve(ajax.call(requests, true, false));
+        } else {
+            // Nothing to load so we can resolve our deferred.
+            serverRequestsDeferred.resolve();
+        }
+
+        // Once we've finished loading all of the templates then recurse to process
+        // any templates that may have been added to the buffer in the time that we
+        // were fetching.
+        $.when.apply(null, templatePromises)
+            .then(function() {
+                // Remove the templates we've loaded from the buffer.
+                loadTemplateBuffer.splice(0, templatesToLoad.length);
+                isLoadingTemplates = false;
+                processLoadTemplateBuffer();
+                return;
+            })
+            .catch(function() {
+                // Remove the templates we've loaded from the buffer.
+                loadTemplateBuffer.splice(0, templatesToLoad.length);
+                isLoadingTemplates = false;
+                processLoadTemplateBuffer();
+            });
+    };
+
     /**
      * Constructor
      *
@@ -85,7 +270,7 @@ define([
     Renderer.prototype.currentThemeName = '';
 
     /**
-     * Load a template from the cache or local storage or ajax request.
+     * Load a template.
      *
      * @method getTemplate
      * @private
@@ -95,75 +280,44 @@ define([
      * @return {Promise} JQuery promise object resolved when the template has been fetched.
      */
     Renderer.prototype.getTemplate = function(templateName) {
-        var parts = templateName.split('/');
-        var component = parts.shift();
-        var name = parts.shift();
         var currentTheme = this.currentThemeName;
         var searchKey = currentTheme + '/' + templateName;
 
-        // First try request variables.
-        if (searchKey in templatePromises) {
-            return templatePromises[searchKey];
+        // If we haven't already seen this template then buffer it.
+        var cachedPromise = getTemplatePromiseFromCache(searchKey);
+        if (cachedPromise) {
+            return cachedPromise;
         }
 
-        // Check the module template cache.
-        if (searchKey in templateCache) {
-            templatePromises[searchKey] = $.Deferred().resolve(templateCache[searchKey]).promise();
-            return templatePromises[searchKey];
-        }
-
-        // Now try local storage.
-        var cached = storage.get('core_template/' + searchKey);
-
-        if (cached) {
-            templateCache[searchKey] = cached;
-            templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
-            return templatePromises[searchKey];
+        // Check the buffer to seee if this template has already been added.
+        var existingBufferRecords = loadTemplateBuffer.filter(function(record) {
+            return record.searchKey == searchKey;
+        });
+        if (existingBufferRecords.length) {
+            // This template is already in the buffer so just return the existing
+            // promise. No need to add it to the buffer again.
+            return existingBufferRecords[0].deferred.promise();
         }
 
-        // Oh well - load via ajax.
-        var promises = ajax.call([{
-            methodname: 'core_output_load_template_with_dependencies',
-            args: {
-                component: component,
-                template: name,
-                themename: currentTheme
-            }
-        }], true, false);
-
-        templatePromises[searchKey] = promises[0].then(
-            function(response) {
-                var templateSource = null;
-
-                response.templates.forEach(function(data) {
-                    // Cache all of the dependent templates because we'll need them to render
-                    // the requested template.
-                    var searchKey = [currentTheme, data.component, data.name].join('/');
-                    templateCache[searchKey] = data.value;
-                    storage.set('core_template/' + searchKey, data.value);
-
-                    if (data.component == component && data.name == name) {
-                        // This is the template that was requested so remember it to return.
-                        templateSource = data.value;
-                    }
-                });
-
-                if (response.strings.length) {
-                    // If we have strings that the template needs then warm the string cache
-                    // with them now so that we don't need to re-fetch them.
-                    str.cache_strings(response.strings.map(function(data) {
-                        return {
-                            component: data.component,
-                            key: data.name,
-                            value: data.value
-                        };
-                    }));
-                }
+        // This is the first time this has been requested so let's add it to the buffer
+        // to be loaded.
+        var parts = templateName.split('/');
+        var component = parts.shift();
+        var name = parts.shift();
+        var deferred = $.Deferred();
+
+        // Add this template to the buffer to be loaded.
+        loadTemplateBuffer.push({
+            component: component,
+            name: name,
+            theme: currentTheme,
+            searchKey: searchKey,
+            deferred: deferred
+        });
 
-                return templateSource;
-            }
-        );
-        return templatePromises[searchKey];
+        // We know there is at least one thing in the buffer so kick off a processing run.
+        processLoadTemplateBuffer();
+        return deferred.promise();
     };
 
     /**