User:SD0001/libApi.js

/** * 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,  * * @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} - 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} - 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;