/**
* 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 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} - 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;
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 = new Array(limit);
}
for (var i = 0; i < batchValues.length; i++) {
batches = batchValues;
}
var responses = new Array(numBatches);
var deferred = $.Deferred();
var sendQuery = function(idx) {
if (idx === numBatches) {
deferred.resolve(responses);
return;
}
query = batches;
mwApi.get(query).done(function(response) {
responses = 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 ], 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} - 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} - 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 = $.Deferred();
var idx = batchIdx * batchSize + i;
var promise = worker(list);
promise.then(incrementSuccesses, incrementFailures).always(function() {
finalBatchPromises.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);
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.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.apply(null, args);
} else {
return $.Deferred().reject.apply(null, .concat(otherArgs));
}
});
};
};
window.ApiQueryContinuous = ApiQueryContinuous;
window.ApiMassQuery = ApiMassQuery;
window.ApiBatchOperation = ApiBatchOperation;
window.ApiWithRetry = ApiWithRetry;