diff --git a/src/classes/site.ts b/src/classes/site.ts index a78ff9911..00bb1abe2 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -26,7 +26,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; -import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils'; import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../configconstants'; import { Md5 } from 'ts-md5/dist/md5'; @@ -113,6 +113,17 @@ export interface CoreSiteWSPreSets { * @type {string} */ typeExpected?: string; + + /** + * Wehther a pending request in the queue matching the same function and arguments can be reused instead of adding + * a new request to the queue. Defaults to true for read requests. + */ + reusePending?: boolean; + + /** + * Whether the request will be be sent immediately as a single request. Defaults to false. + */ + skipQueue?: boolean; } /** @@ -144,6 +155,18 @@ export interface LocalMobileResponse { coreSupported?: boolean; } +/** + * Info of a request waiting in the queue. + */ +interface RequestQueueItem { + cacheId: string; + method: string; + data: any; + preSets: CoreSiteWSPreSets; + wsPreSets: CoreWSPreSets; + deferred: PromiseDefer; +} + /** * Class that represents a site (combination of site + user). * It will have all the site data and provide utility functions regarding a site. @@ -151,6 +174,9 @@ export interface LocalMobileResponse { * the tables are created in all the sites, not just the current one. */ export class CoreSite { + static REQUEST_QUEUE_DELAY = 50; // Maximum number of miliseconds to wait before processing the queue. + static REQUEST_QUEUE_LIMIT = 10; // Maximum number of requests allowed in the queue. + // List of injected services. This class isn't injectable, so it cannot use DI. protected appProvider: CoreAppProvider; protected dbProvider: CoreDbProvider; @@ -186,6 +212,8 @@ export class CoreSite { protected lastAutoLogin = 0; protected offlineDisabled = false; protected ongoingRequests: { [cacheId: string]: Promise } = {}; + protected requestQueue: RequestQueueItem[] = []; + protected requestQueueTimeout = null; /** * Create a site. @@ -476,6 +504,9 @@ export class CoreSite { if (typeof preSets.saveToCache == 'undefined') { preSets.saveToCache = true; } + if (typeof preSets.reusePending == 'undefined') { + preSets.reusePending = true; + } return this.request(method, data, preSets); } @@ -592,7 +623,7 @@ export class CoreSite { const promise = this.getFromCache(method, data, preSets, false, originalData).catch(() => { // Do not pass those options to the core WS factory. - return this.wsProvider.call(method, data, wsPreSets).then((response) => { + return this.callOrEnqueueRequest(method, data, preSets, wsPreSets).then((response) => { if (preSets.saveToCache) { this.saveToCache(method, data, response, preSets); } @@ -707,6 +738,146 @@ export class CoreSite { }); } + /** + * Adds a request to the queue or calls it immediately when not using the queue. + * + * @param {string} method The WebService method to be called. + * @param {any} data Arguments to pass to the method. + * @param {CoreSiteWSPreSets} preSets Extra options related to the site. + * @param {CoreWSPreSets} wsPreSets Extra options related to the WS call. + * @returns {Promise} Promise resolved with the response when the WS is called. + */ + protected callOrEnqueueRequest(method: string, data: any, preSets: CoreSiteWSPreSets, wsPreSets: CoreWSPreSets): Promise { + if (preSets.skipQueue || !this.wsAvailable('tool_mobile_call_external_functions')) { + return this.wsProvider.call(method, data, wsPreSets); + } + + const cacheId = this.getCacheId(method, data); + + // Check if there is an identical request waiting in the queue (read requests only by default). + if (preSets.reusePending) { + const request = this.requestQueue.find((request) => request.cacheId == cacheId); + if (request) { + return request.deferred.promise; + } + } + + const request: RequestQueueItem = { + cacheId, + method, + data, + preSets, + wsPreSets, + deferred: {} + }; + + request.deferred.promise = new Promise((resolve, reject): void => { + request.deferred.resolve = resolve; + request.deferred.reject = reject; + }); + + this.requestQueue.push(request); + + if (this.requestQueue.length >= CoreSite.REQUEST_QUEUE_LIMIT) { + this.processRequestQueue(); + } else if (!this.requestQueueTimeout) { + this.requestQueueTimeout = setTimeout(this.processRequestQueue.bind(this), CoreSite.REQUEST_QUEUE_DELAY); + } + + return request.deferred.promise; + } + + /** + * Call the enqueued web service requests. + */ + protected processRequestQueue(): void { + this.logger.debug(`Processing request queue (${this.requestQueue.length} requests)`); + + // Clear timeout if set. + if (this.requestQueueTimeout) { + clearTimeout(this.requestQueueTimeout); + this.requestQueueTimeout = null; + } + + // Extract all requests from the queue. + const requests = this.requestQueue; + this.requestQueue = []; + + if (requests.length == 1) { + // Only one request, do a regular web service call. + this.wsProvider.call(requests[0].method, requests[0].data, requests[0].wsPreSets).then((data) => { + requests[0].deferred.resolve(data); + }).catch((error) => { + requests[0].deferred.reject(error); + }); + + return; + } + + const data = { + requests: requests.map((request) => { + const args = {}; + const settings = {}; + + // Separate WS settings from function arguments. + Object.keys(request.data).forEach((key) => { + let value = request.data[key]; + const match = /^moodlews(setting.*)$/.exec(key); + if (match) { + if (match[1] == 'settingfilter' || match[1] == 'settingfileurl') { + // Undo special treatment of these settings in CoreWSProvider.convertValuesToString. + value = (value == 'true' ? '1' : '0'); + } + settings[match[1]] = value; + } else { + args[key] = value; + } + }); + + return { + function: request.method, + arguments: JSON.stringify(args), + ...settings + }; + }) + }; + + const wsPresets: CoreWSPreSets = { + siteUrl: this.siteUrl, + wsToken: this.token, + }; + + this.wsProvider.call('tool_mobile_call_external_functions', data, wsPresets).then((data) => { + if (!data || !data.responses) { + return Promise.reject(null); + } + + requests.forEach((request, i) => { + const response = data.responses[i]; + + if (!response) { + // Request not executed, enqueue again. + this.callOrEnqueueRequest(request.method, request.data, request.preSets, request.wsPreSets); + } else if (response.error) { + request.deferred.reject(this.textUtils.parseJSON(response.exception)); + } else { + let responseData = this.textUtils.parseJSON(response.data); + // Match the behaviour of CoreWSProvider.call when no response is expected. + if (!responseData && (typeof wsPresets.responseExpected == 'undefined' || wsPresets.responseExpected)) { + responseData = {}; + } + request.deferred.resolve(responseData); + } + }); + + }).catch((error) => { + // Error not specific to a single request, reject all promises. + requests.forEach((request) => { + request.deferred.reject(error); + }); + }); + } + /** * Check if a WS is available in this site. *