Jump to content

User:SD0001/libApi.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * libApi for browser JS
 * Library of functions to work with mw.Api easily.
 *
 * For usage of equivalent functions in Node.js, use the mwn bot 
 * framework, <https://github.com/siddharthvp/mwn>
 * 
 * @author SD0001
 *
 */

/**
 * Send an API query that automatically continues till the limit is reached.
 *
 * @param {mw.Api} mwApi - the mw.Api object used for API calls
 * @param {Object} query - The API query
 * @param {number} [limit=10] - limit on the maximum number of API calls to go through
 * @returns {jQuery.Promise<Object[]>} - resolved with an array of responses of individual calls.
 */
var ApiQueryContinuous = function(mwApi, query, limit) {
	limit = limit || 10;
	var responses = [];
	var callApi = function(query, count) {
		return mwApi.get(query).then(function(response) {
            responses.push(response);
            if (response.continue && count < limit) {
                return callApi($.extend({}, query, response.continue), count + 1);
            } else {
				return responses;
            }
        });
    };
	return callApi(query, 1);
};


/**
 * Function for using API action=query with more than 50/500 items in multi-input fields.
 *
 * Several fields in the query API take multiple inputs but with a limit of 50 (or 500 for bots).
 * Example: the fields titles, pageids and revids in any query, ususers in list=users,
 * clcategories in prop=categories, etc.
 *
 * This function allows you to send a query as if this limit didn't exist. The array given to
 * the multi-input field is split into batches of 50 (500 for bots) and individual queries are
 * sent sequentially for each batch. A promise is returned finally resolved with the array of
 * responses of each API call.
 *
 * @param {mw.Api} mwApi - mw.Api object to use for the API calls
 * @param {Object} query - the query object, the multi-input field should be an array
 * @param {string} [batchFieldName=titles] - the name of the multi-input field
 * @returns {jQuery.Promise<Object[]>} - promise resolved when all the API queries have settled,
 * with the array of responses.
 * @requires mediawiki.api
 */
var ApiMassQuery = function(mwApi, query, batchFieldName) {
	batchFieldName = batchFieldName || 'titles';
	var batchValues = query[batchFieldName];
	var hasApiHighLimit = (mw.config.get('wgUserGroups').indexOf('sysop') !== -1 ||
		mw.config.get('wgUserGroups').indexOf('bot') !== -1);
	var limit = hasApiHighLimit ? 500 : 50;
	var numBatches = Math.ceil(batchValues.length / limit);
	var batches = new Array(numBatches);
	for (var i = 0; i < numBatches; i++) {
		batches[i] = new Array(limit);
	}
	for (var i = 0; i < batchValues.length; i++) {
		batches[Math.floor(i/limit)][i % limit] = batchValues[i];
	}
	var responses = new Array(numBatches);
	var deferred = $.Deferred();

	var sendQuery = function(idx) {
		if (idx === numBatches) {
			deferred.resolve(responses);
			return;
		}
		query[batchFieldName] = batches[idx];
		mwApi.get(query).done(function(response) {
			responses[idx] = response;
		}).always(function() {
			sendQuery(idx + 1);
		});
	};
	sendQuery(0);
	return deferred;
};


/**
 * Execute an asynchronous function on a large number of pages (or other arbitrary items).
 * Similar to Morebits.batchOperation in [[MediaWiki:Gadget-morebits.js]], but designed for
 * working with promises.
 *
 * @param {Array} list - list of items to execute actions upon. The array would
 * usually be of page names (strings).
 * @param {Function} worker - function to execute upon each item in the list. Must
 * return a promise.
 * @param {number} [batchSize=50] - number of concurrent operations to take place.
 * Set this to 1 for sequential operations. Default 50. Set this according to how
 * expensive the API calls made by worker are.
 * @param {HTMLElement} [statusElement] - HTML element in which to show the status
 * message
 * @returns {jQuery.Promise} - resolved when all API calls have finished.
 */
var ApiBatchOperation = function(list, worker, batchSize, statusElement) {
	batchSize = batchSize || 50;
	if (statusElement) {
		statusElement.textContent = `Finished 0/${list.length} (0%) tasks, of which 0 (0%) were successful, and 0 failed.`;
	}
	var successes = 0, failures = 0;
	var incrementSuccesses = function() { successes++; };
	var incrementFailures = function() { failures++; };
	var updateStatusText = function() {
		var percentageFinished = Math.round((successes + failures) / list.length * 100);
		var percentageSuccesses = Math.round(successes / (successes + failures) * 100);
		var statusText = `Finished ${successes + failures}/${list.length} (${percentageFinished}%) tasks, of which ${successes} (${percentageSuccesses}%) were successful, and ${failures} failed.`;
		if (statusElement) {
			statusElement.textContent = statusText;
		} else {
			console.log(statusText);
		}
	}
	var returnPromise = $.Deferred();
	var numBatches = Math.ceil(list.length / batchSize);
	var sendBatch = function(batchIdx) {
		if (batchIdx === numBatches - 1) { // last batch
			var cnt = 0;
			var numItemsInLastBatch = list.length - batchIdx * batchSize;
			var finalBatchPromises = new Array(numItemsInLastBatch);
			for (var i = 0; i < numItemsInLastBatch; i++) {
				finalBatchPromises[i] = $.Deferred();
				var idx = batchIdx * batchSize + i;
				var promise = worker(list[idx]);
				promise.then(incrementSuccesses, incrementFailures).always(function() {
					finalBatchPromises[cnt++].resolve();
					updateStatusText();
				});
			}
			$.when.apply($, finalBatchPromises).then(returnPromise.resolve);
			return;
		}
		for (var i = 0; i < batchSize; i++) {
			var idx = batchIdx * batchSize + i;
			var promise = worker(list[idx]);
			promise.then(incrementSuccesses, incrementFailures).always(updateStatusText);
			if (i === batchSize - 1) { // last item in batch: trigger the next batch's API calls
				promise.always(function() {
					sendBatch(batchIdx + 1);
				});
			}
		}
	};
	sendBatch(0);
	return returnPromise;
};

/**
 * @class
 * ** UNTESTED **
 * Re-try an API request one more time if it fails.
 * Or neither to unconditionally attempt a retry on failure.
 * @param {mw.Api} mwApi - the mw.Api object to send API calls with
 */
var ApiWithRetry = function(mwApi) {
	// Set either on of these:
	this.retryOnErrors = null;
	this.dontRetryOnErrors = null;

	/**
	 * @param {string} method - mw.Api method to use
	 * @param {...*} method arguments
	 */
	this.send = function(method) {
		var args = Array.prototype.slice.call(arguments, 1);
		return mwApi[method].apply(null, args).catch(function(errorCode) {
			var otherArgs = Array.prototype.slice.call(arguments, 1);

			if ((this.dontRetryOnErrors && !this.dontRetryOnErrors.includes(errorCode)) ||
				(this.retryOnErrors && this.retryOnErrors.includes(errorCode)) ||
				(!this.retryOnErrors && !this.dontRetryOnErrors)) {
				return mwApi[method].apply(null, args);
			} else {
				return $.Deferred().reject.apply(null, [errorCode].concat(otherArgs));
			}
		});
	};
};


window.ApiQueryContinuous = ApiQueryContinuous;
window.ApiMassQuery = ApiMassQuery;
window.ApiBatchOperation = ApiBatchOperation;
window.ApiWithRetry = ApiWithRetry;