Warning: This is an old version. The latest stable version is v1.1.0.

Source: core.js

/* eslint no-unused-vars: [ "error", { "args": "none" } ] */
// Session has abstract methods with parameters only used in subclasses

/**
 * A request parameter value that can potentially be put in a list:
 * a title, user name, namespace number, etc.
 *
 * @typedef ListableParam
 * @type {string|number}
 */

/**
 * A request parameter value that cannot be put in a list:
 * a boolean toggle, a Blob or File (POST requests only),
 * or null or undefined as a default fallback for not sending a parameter at all.
 *
 * @typedef UnlistableParam
 * @type {boolean|Blob|File|null|undefined}
 */

/**
 * A single request parameter value.
 *
 * @typedef SingleParam
 * @type {ListableParam|UnlistableParam}
 */

/**
 * A request parameter value that is a list of values
 * (several titles, namespace numbers, etc.),
 * which may potentially be combined with other lists in a single request
 * (if specified as a set) or not (if specified as an array).
 *
 * @typedef ListParam
 * @type {Array<ListableParam>|Set<ListableParam>}
 */

/**
 * A request parameter value of any kind.
 *
 * @typedef Param
 * @type {ListParam|SingleParam}
 */

/**
 * Request parameters for {@link Session#request} and related methods.
 * Each parameter may be a string, number, boolean, null, or undefined,
 * or an array or set of strings or numbers.
 * Parameters with values false, null, or undefined are completely removed
 * when the request is sent out.
 * In POST requests, a parameter may also be a Blob or File.
 *
 * @typedef Params
 * @type {Object<string, Param>}
 */

/**
 * Request options for {@link Session#request} and related methods.
 * The actual effective options are merged from
 * the builtin {@link DEFAULT_OPTIONS},
 * the default options passed into the {@link Session} constructor,
 * and the options given with a particular request call.
 *
 * @typedef Options
 * @type {Object}
 * @property {string} [method] The method, either GET (default) or POST.
 * @property {string|null} [tokenType] Include a token parameter of this type,
 * automatically getting it from the API if necessary.
 * The most common token type is 'csrf' (some actions use a different type);
 * you will also want to set the method option to POST.
 * @property {string} [tokenName] The name of the token parameter.
 * Only used if the tokenType option is not null.
 * Defaults to 'token', but some modules need a different name
 * (e.g. action=login needs 'lgtoken').
 * @property {string} [userAgent] The User-Agent header to send.
 * (Usually specified as a default option in the constructor.)
 * @property {number} [maxRetriesSeconds] The maximum duration for automatic retries,
 * i.e. a time interval (in seconds) during which the request will be automatically repeated
 * according to the Retry-After response header if it is present.
 * Defaults to 65 seconds; set to 0 to disable automatic retries.
 * (Can also be a fractional number for sub-second precision.)
 * @property {number} [retryAfterMaxlagSeconds] Default Retry-After header value
 * in case of a maxlag error. Only used when the response is missing the header.
 * Since MediaWiki usually sends this header for maxlag errors, this option is rarely used.
 * Defaults to five seconds, which is the recommended maxlag value for bots.
 * @property {number} [retryAfterReadonlySeconds] Default Retry-After header value
 * in case of a readonly error. Only used when the response is missing the header.
 * MediaWiki does not usually send this header for readonly errors,
 * so this option is more important than the retryAfterMaxlagSeconds option.
 * The default of 30 seconds is thought to be appropriate for Wikimedia wikis;
 * for third-party wikis, higher values may be useful
 * (remember to also increase the maxRetriesSeconds option accordingly).
 * @property {Function} [warn] A handler for warnings from this API request.
 * Called with a single instance of a subclass of Error, such as {@link ApiWarnings}.
 * The default is console.warn (interactive CLI applications may wish to change this).
 * @property {boolean} [dropTruncatedResultWarning]
 * Whether to drop warnings about truncated results instead of passing them to the warn handler.
 * Occasionally, an API result may not fit into a single network response;
 * in such cases, the API will add a warning about the result being truncated,
 * as well as continuation parameters that will result in the remaining information
 * being included in the next request, if continuation is followed.
 * If you follow continuation and are prepared to merge truncated responses back together,
 * you don’t need to see this warning and can use this option to suppress it.
 * This option defaults to false in {@link Session#request} (i.e. treat the warning like any other),
 * but to true in {@link Session#requestAndContinueReducingBatch}.
 * @property {string} [authorization] Value for the Authorization request header.
 * This option can be used to authenticate requests using OAuth 2.0.
 * For an owner-only client / consumer, where you have an access token,
 * you can set this option to `Bearer ${ accessToken }` directly.
 * Otherwise, use the m3api-oauth2 extension package.
 * @property {Object.<string, ErrorHandler>} [errorHandlers] Internal option.
 * Define handlers for API errors, which can retry the request if appropriate.
 * This option is only part of the internal interface, not of the stable, public interface.
 * @property {number} [retryUntil] Internal option.
 * Retry until the given timestamp (in terms of the performance.now() clock).
 * Takes precedence over the maxRetriesSeconds option.
 * This option is only part of the internal interface, not of the stable, public interface.
 */

/**
 * An error handler callback, which can be registered in the errorHandlers option.
 *
 * The callback is called if an API request results in an error
 * and the callback has been registered for that error code.
 * It may retry the request or perform any other action.
 *
 * @callback ErrorHandler
 * @param {Session} session The session to which the request belongs.
 * @param {Params} params The request parameters.
 * @param {Options} options The request options.
 * The retryUntil option is always set here,
 * and the error handler should not retry the request if this timestamp has already passed.
 * @param {InternalResponse} internalResponse The full response sent by the server.
 * @param {Object} error The specific error returned to the API that matched this handler.
 * @return {Object|null|Promise<Object|null>} A synchronous or asynchronous result.
 * If the handler returns an object (or a promise resolving to an object),
 * that object is used as the result of the API request;
 * this can be used to retry the request
 * (the handler makes another request to the session with the same params and options,
 * and returns its result).
 * If the handler returns null (or a promise resolving to null),
 * the error could not be handled;
 * m3api will call error handlers for the remaining errors (if any)
 * and eventually throw ApiErrors if none of them returned an object either.
 */

/**
 * The internal representation of a full server response,
 * returned by {@link Session#internalGet} and {@link Session#internalPost}.
 *
 * @protected
 * @typedef InternalResponse
 * @type {Object}
 * @property {number} status The HTTP status code (e.g. 200 OK).
 * @property {Object} headers The response headers.
 * Header names must be all-lowercase.
 * (Set-Cookie is not expected to be included.)
 * @property {Object} body JSON-decoded response body.
 */

/**
 * Default options for requests across all sessions.
 *
 * Packages extending m3api’s capabilities (“extension packages”)
 * may add their own options here,
 * conventionally prefixed with the package name and a slash.
 * For example, a package named 'abc' may add options 'abc/x' and 'abc/y',
 * while a package named '@abc/def' may add '@abc/def/x' and '@abc/def/y'.
 * Extension packages are encouraged to use a single options object
 * for their own options as well as ones that are passed through to m3api,
 * rather than e.g. separate options or individual parameters;
 * both kinds of options can then have per-session and global defaults.
 *
 * Changing or removing any default options here is strongly discouraged,
 * and may result in unpredictable behavior.
 *
 * @type {Options}
 */
const DEFAULT_OPTIONS = {
	method: 'GET',
	tokenType: null,
	tokenName: 'token',
	maxRetriesSeconds: 65,
	retryAfterMaxlagSeconds: 5,
	retryAfterReadonlySeconds: 30,
	warn: console.warn,
	dropTruncatedResultWarning: false,
	authorization: null,
	errorHandlers: {
		maxlag: ( session, params, options, internalResponse, error ) => {
			if ( 'retry-after' in internalResponse.headers ) {
				return null; // header takes precedence over option
			}
			const { retryAfterMaxlagSeconds } = {
				...DEFAULT_OPTIONS,
				...session.defaultOptions,
				...options,
			};
			return retryIfBefore( session, params, options, retryAfterMaxlagSeconds );
		},
		readonly: ( session, params, options, internalResponse, error ) => {
			if ( 'retry-after' in internalResponse.headers ) {
				return null; // header takes precedence over option
			}
			const { retryAfterReadonlySeconds } = {
				...DEFAULT_OPTIONS,
				...session.defaultOptions,
				...options,
			};
			return retryIfBefore( session, params, options, retryAfterReadonlySeconds );
		},
		badtoken: ( session, params, options, internalResponse, error ) => {
			const { tokenType } = {
				...DEFAULT_OPTIONS,
				...session.defaultOptions,
				...options,
			};
			if ( tokenType === null ) {
				return null; // bad token was supplied manually, nothing for us to do
			}
			session.tokens.clear();
			return retryIfBefore( session, params, options, 0 /* no delay */ );
		},
	},
};

const DEFAULT_USER_AGENT = 'm3api/0.8.4 (https://www.npmjs.com/package/m3api)';

const TRUNCATED_RESULT = /^This result was truncated because it would otherwise  ?be larger than the limit of .* bytes\.?$/;

/**
 * @private
 * @param {Object} params
 * @return {Array.<Object>} [urlParams, bodyParams]
 */
function splitPostParameters( params ) {
	const urlParams = {};
	const bodyParams = {};
	for ( const [ key, value ] of Object.entries( params ) ) {
		if ( key === 'action' || key === 'origin' || key === 'crossorigin' ) {
			urlParams[ key ] = value;
		} else {
			bodyParams[ key ] = value;
		}
	}
	return [ urlParams, bodyParams ];
}

/**
 * @private
 * @param {Session} session
 * @param {Params} params
 * @param {Options} options
 * @param {number} retryAfterSeconds
 * @return {Promise<Object>|null}
 */
function retryIfBefore( session, params, options, retryAfterSeconds ) {
	const retryAfterMillis = 1000 * retryAfterSeconds;
	if ( performance.now() + retryAfterMillis <= options.retryUntil ) {
		return new Promise( ( resolve ) => {
			setTimeout( resolve, retryAfterMillis );
		} ).then( () => session.request( params, options ) );
	} else {
		return null;
	}
}

/**
 * Return whether the given warning is *not* a truncatedresult warning.
 *
 * @private
 * @param {Object} warning
 * @return {boolean}
 */
function notTruncatedResultWarning( warning ) {
	return warning.code ?
		warning.code !== 'truncatedresult' :
		!TRUNCATED_RESULT.test( warning.warnings || warning[ '*' ] );
}

/**
 * Return the errors of a response (if any).
 *
 * @private
 * @param {Object} response
 * @return {Array.<Object>}
 */
function responseErrors( response ) {
	if ( 'error' in response ) {
		return [ response.error ];
	}
	if ( 'errors' in response ) {
		return response.errors;
	}
	return [];
}

/**
 * Return the warnings of a response (if any).
 *
 * @private
 * @param {Object} response
 * @return {Array.<Object>}
 */
function responseWarnings( response ) {
	let warnings = response.warnings;
	if ( !warnings ) {
		return [];
	}

	if ( !Array.isArray( warnings ) ) {
		const bcWarnings = Object.entries( warnings );
		if ( bcWarnings[ 0 ][ 0 ] === 'main' ) {
			// move to end of list
			bcWarnings.push( bcWarnings.shift() );
		}
		warnings = [];
		for ( const [ module, warning ] of bcWarnings ) {
			warning.module = module;
			warnings.push( warning );
		}
	}
	return warnings;
}

/**
 * An Error wrapping one or more API errors.
 */
class ApiErrors extends Error {

	/**
	 * @param {Object[]} errors The error objects from the API.
	 * Must be nonempty, and each error must contain at least a code.
	 * Other error members depend on the errorformat of the request.
	 * @param {...*} params Any other params for the Error constructor.
	 * (Not including the message: the first error code is used for that.)
	 */
	constructor( errors, ...params ) {
		super( errors[ 0 ].code, ...params );

		if ( Error.captureStackTrace ) {
			Error.captureStackTrace( this, ApiErrors );
		}

		this.name = 'ApiErrors';

		/**
		 * The error objects from the API.
		 *
		 * @member {Object[]}
		 */
		this.errors = errors;
	}

}

/**
 * An Error wrapping one or more API warnings.
 */
class ApiWarnings extends Error {

	/**
	 * @param {Object[]} warnings The warning objects from the API.
	 * Must be nonempty; the warning members depend on the errorformat of the request.
	 * @param {...*} params Any other params for the Error constructor.
	 * (Not including the message: the first warning is used for that.)
	 */
	constructor( warnings, ...params ) {
		super(
			warnings[ 0 ].code || warnings[ 0 ].warnings || warnings[ 0 ][ '*' ],
			...params,
		);

		if ( Error.captureStackTrace ) {
			Error.captureStackTrace( this, ApiWarnings );
		}

		this.name = 'ApiWarnings';

		/**
		 * The warning objects from the API.
		 *
		 * @member {Object[]}
		 */
		this.warnings = warnings;
	}

}

/**
 * Decorate the given warn handler so that warnings about truncated results are dropped.
 *
 * Most of the time, you should use the dropTruncatedResultWarning request option
 * instead of using this function directly.
 *
 * @param {Function} warn The original warn function.
 * @return {Function} A new function that, when called,
 * will call the original warn functions,
 * but with all truncated result warnings dropped;
 * when there are no other warnings, the original function is not called.
 */
function makeWarnDroppingTruncatedResultWarning( warn ) {
	return function ( error ) {
		if ( error instanceof ApiWarnings ) {
			const warnings = error.warnings.filter( notTruncatedResultWarning );
			if ( warnings.length > 0 ) {
				return warn( warnings.length === error.warnings.length ?
					error :
					new ApiWarnings( warnings ) );
			}
		} else {
			return warn( error );
		}
	};
}

/**
 * An Error used as a warning when a request with no custom user agent is made.
 */
class DefaultUserAgentWarning extends Error {

	/**
	 * @param {...*} params Any additional params for the Error constructor,
	 * not including the message (which is hard-coded).
	 */
	constructor( ...params ) {
		super(
			'm3api: Sending request with default User-Agent. ' +
				'You should set the userAgent request option, ' +
				'either as a default option for the session (third constructor argument) ' +
				'or as a custom option for each request (second request argument). ' +
				'See w.wiki/9mMA for the User-Agent policy.',
			...params,
		);

		if ( Error.captureStackTrace ) {
			Error.captureStackTrace( this, DefaultUserAgentWarning );
		}

		this.name = 'DefaultUserAgentWarning';
	}

}

/**
 * A session to make API requests.
 */
class Session {

	/**
	 * @param {string} apiUrl The URL to the api.php endpoint,
	 * such as {@link https://en.wikipedia.org/w/api.php}.
	 * Can also be just the domain, such as en.wikipedia.org.
	 * @param {Params} [defaultParams] Parameters to include in every API request.
	 * You are strongly encouraged to specify formatversion: 2 here;
	 * other useful global parameters include uselang, errorformat, maxlag.
	 * @param {Options} [defaultOptions] Options to set for each request.
	 * You are strongly encouraged to specify a userAgent according to the
	 * {@link https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:User-Agent_policy User-Agent policy}.
	 */
	constructor( apiUrl, defaultParams = {}, defaultOptions = {} ) {
		if ( !apiUrl.includes( '/' ) ) {
			apiUrl = `https://${ apiUrl }/w/api.php`;
		}

		/**
		 * The URL to the api.php endpoint.
		 * Must not be reassigned.
		 *
		 * @member {string}
		 */
		this.apiUrl = apiUrl;

		/**
		 * Parameters to include in every API request.
		 * Can be modified after construction,
		 * e.g. to add assert=user after logging in.
		 *
		 * @member {Object}
		 */
		this.defaultParams = defaultParams;

		/**
		 * Options to set for each request.
		 * Can be modified after construction.
		 *
		 * @member {Options}
		 */
		this.defaultOptions = defaultOptions;

		/**
		 * Saved/cached tokens.
		 * Can be modified after construction,
		 * particularly to call `clear()` after logging in or out;
		 * apart from that, however,
		 * using the tokenType/tokenName options or {@link Session#getToken}
		 * is generally more convenient.
		 *
		 * @member {Map}
		 */
		this.tokens = new Map();
	}

	/**
	 * Make an API request.
	 *
	 * @param {Params} params The parameters.
	 * Default parameters from the constructor are added to these,
	 * with per-request parameters overriding default parameters in case of collision.
	 * @param {Options} [options] Other options for the request.
	 * The per-request options extend and override the options passed into the constructor,
	 * which in turn extend and override the builtin {@link DEFAULT_OPTIONS}.
	 * @return {Object}
	 * @throws {ApiErrors}
	 */
	async request( params, options = {} ) {
		const {
			method,
			tokenType,
			tokenName,
			maxRetriesSeconds,
			warn,
			dropTruncatedResultWarning,
			retryUntil = performance.now() + maxRetriesSeconds * 1000,
		} = {
			...DEFAULT_OPTIONS,
			...this.defaultOptions,
			...options,
		};
		const retryOptions = { ...options, retryUntil };

		let tokenParams = null;
		if ( tokenType !== null ) {
			tokenParams = { [ tokenName ]: await this.getToken( tokenType, retryOptions ) };
		}
		const allParams = this.transformParams( {
			...this.defaultParams,
			...tokenParams,
			...params,
			format: 'json',
		} );
		const requestHeaders = this.getRequestHeaders( options );

		let result;
		if ( method === 'GET' ) {
			result = this.internalGet( this.apiUrl, allParams, requestHeaders );
		} else if ( method === 'POST' ) {
			const [ urlParams, bodyParams ] = splitPostParameters( allParams );
			result = this.internalPost( this.apiUrl, urlParams, bodyParams, requestHeaders );
		} else {
			throw new Error( `Unknown request method: ${ method }` );
		}
		const internalResponse = await result;
		const {
			status,
			headers: responseHeaders,
			body,
		} = internalResponse;

		if ( status !== 200 && !( 'mediawiki-api-error' in responseHeaders ) ) {
			throw new Error( `API request returned non-200 HTTP status code: ${ status }` );
		}

		if ( 'retry-after' in responseHeaders ) {
			const retryAfterSeconds = parseInt( responseHeaders[ 'retry-after' ] );
			const retryResult = await retryIfBefore(
				this, params, retryOptions, retryAfterSeconds );
			if ( retryResult !== null ) {
				return retryResult;
			}
		}

		const errors = responseErrors( body );

		for ( const error of errors ) {
			const { [ error.code ]: handler } = {
				...DEFAULT_OPTIONS.errorHandlers,
				...this.defaultOptions.errorHandlers,
				...options.errorHandlers,
			};
			if ( handler ) {
				const handlerResult = await handler(
					this, params, retryOptions, internalResponse, error );
				if ( handlerResult !== null ) {
					return handlerResult;
				}
			}
		}

		if ( errors.length > 0 ) {
			throw new ApiErrors( errors );
		}

		const warnings = responseWarnings( body );
		if ( warnings.length > 0 ) {
			const actualWarn = dropTruncatedResultWarning ?
				makeWarnDroppingTruncatedResultWarning( warn ) :
				warn;
			actualWarn( new ApiWarnings( warnings ) );
		}

		return body;
	}

	/**
	 * Make a series of API requests, following API continuation.
	 *
	 * @param {Params} params Same as for {@link Session#request}.
	 * Continuation parameters will be added automatically.
	 * @param {Options} [options] Same as for {@link Session#request}.
	 * @yield {Object}
	 * @throws {ApiErrors}
	 */
	async * requestAndContinue( params, options = {} ) {
		let continueParams = { continue: undefined };
		do {
			const response = await this.request( {
				...params,
				...continueParams,
			}, options );
			continueParams = response.continue && { ...response.continue };
			yield response;
		} while ( continueParams !== undefined );
	}

	/**
	 * Make a series of API requests, following API continuation,
	 * accumulating responses and yielding one result per batch.
	 *
	 * This works conceptually similar to Array.reduce(), but repeatedly,
	 * with each batch of responses corresponding to one array.
	 * At the beginning of each batch, an initial value is generated,
	 * and then for each response in the batch,
	 * a reducer is called with the current value and that response.
	 * (The current value starts out as the initial value;
	 * afterwards, it’s the reducer’s return value for the previous response.)
	 * At the end of each batch, the current value is yielded,
	 * and the process starts over with a new initial value.
	 *
	 * The reducer will typically extract some kind of pages or other entries from the response,
	 * add them to the current value, possibly merging them with existing entries there,
	 * and then return the updated value.
	 * The initial callback defaults to producing empty objects,
	 * but other values are also possible: sets or maps may be useful.
	 *
	 * @param {Params} params Same as for {@link Session#request}.
	 * @param {Options} options Same as for {@link Session#request}. (But not optional here!)
	 * The dropTruncatedResultWarning option defaults to true here,
	 * since continuation will produce the rest of the truncated result automatically.
	 * @param {Function} reducer A callback like for Array.reduce().
	 * Called with two arguments, the current value and the current response.
	 * @param {Function} [initial] A callback producing initial values.
	 * Called with no arguments. Defaults to producing empty objects.
	 * @yield {*} The last reducer return value for each batch.
	 * Typically, the initial and reducer callbacks will have the same return type,
	 * which will then also be the return type of this function, such as Object, Map, or Set.
	 */
	async * requestAndContinueReducingBatch( params, options, reducer, initial = () => ( {} ) ) {
		options = {
			dropTruncatedResultWarning: true,
			...options,
		};

		let accumulator = initial();
		for await ( const response of this.requestAndContinue( params, options ) ) {
			const complete = responseBoolean( response.batchcomplete );
			accumulator = reducer( accumulator, response );
			if ( complete ) {
				yield accumulator;
				accumulator = initial();
			}
		}
	}

	/**
	 * Get a token of the specified type.
	 *
	 * Though this method is public, it should generally not be used directly:
	 * call {@link Session#request} with the tokenType/tokenName options instead.
	 *
	 * @param {string} type
	 * @param {Options} options Options for the request to get the token.
	 * @return {string}
	 */
	async getToken( type, options ) {
		if ( !this.tokens.has( type ) ) {
			const params = {
				action: 'query',
				meta: set( 'tokens' ),
				type: set( type ),
			};
			options = {
				...options,
				method: 'GET',
				tokenType: null,
				dropTruncatedResultWarning: true,
			};
			for await ( const response of this.requestAndContinue( params, options ) ) {
				try {
					const token = response.query.tokens[ type + 'token' ];
					if ( typeof token === 'string' ) {
						this.tokens.set( type, token );
						break;
					}
					// if token not found in this response, follow continuation
				} catch ( _ ) {
				}
			}
		}
		return this.tokens.get( type );
	}

	/**
	 * Get the effective request headers for these options.
	 *
	 * @protected
	 * @param {Options} options
	 * @return {Object}
	 */
	getRequestHeaders( options ) {
		const requestHeaders = {
			'user-agent': this.getUserAgent( options ),
		};
		const { authorization } = {
			...DEFAULT_OPTIONS,
			...this.defaultOptions,
			...options,
		};
		if ( authorization ) {
			requestHeaders.authorization = authorization;
		}
		return requestHeaders;
	}

	/**
	 * Get the effective user agent string for these options.
	 *
	 * @protected
	 * @param {Options} options
	 * @return {string}
	 */
	getUserAgent( options ) {
		const {
			userAgent,
			warn,
		} = {
			...DEFAULT_OPTIONS,
			...this.defaultOptions,
			...options,
		};

		if ( userAgent ) {
			return `${ userAgent } ${ DEFAULT_USER_AGENT }`;
		} else {
			if ( !this.warnedDefaultUserAgent ) {
				warn( new DefaultUserAgentWarning() );
				/** @private */
				this.warnedDefaultUserAgent = true;
			}
			return DEFAULT_USER_AGENT;
		}
	}

	/**
	 * @private
	 * @param {Params} params
	 * @return {Object}
	 */
	transformParams( params ) {
		const transformedParams = {};
		for ( const [ key, value ] of Object.entries( params ) ) {
			const transformedParamValue = this.transformParamValue( value );
			if ( transformedParamValue !== undefined ) {
				transformedParams[ key ] = transformedParamValue;
			}
		}
		return transformedParams;
	}

	/**
	 * @private
	 * @param {Param} value
	 * @return {string|undefined}
	 */
	transformParamValue( value ) {
		if ( value instanceof Set ) {
			value = [ ...value ];
		}
		if ( Array.isArray( value ) ) {
			return this.transformParamArray( value );
		} else {
			return this.transformParamSingle( value );
		}
	}

	/**
	 * @private
	 * @param {Array<ListableParam>} value
	 * @return {string}
	 */
	transformParamArray( value ) {
		if ( value.some( ( element ) => String.prototype.includes.call( element, '|' ) ) ) {
			return '\x1f' + value.join( '\x1f' );
		} else {
			return value.join( '|' );
		}
	}

	/**
	 * @private
	 * @param {*} value
	 * @return {*} string|undefined for string|number|boolean|null|undefined value,
	 * the value unmodified otherwise
	 */
	transformParamSingle( value ) {
		if ( typeof value === 'number' ) {
			return String( value );
		}
		if ( value === true ) {
			return '';
		}
		if ( value === false || value === null || value === undefined ) {
			return undefined;
		}
		return value;
	}

	/**
	 * Actually make a GET request.
	 *
	 * @abstract
	 * @protected
	 * @param {string} apiUrl
	 * @param {Object} params
	 * @param {Object} headers Header names must be all-lowercase.
	 * @return {Promise<InternalResponse>}
	 */
	internalGet( apiUrl, params, headers ) {
		throw new Error( 'Abstract method internalGet not implemented!' );
	}

	/**
	 * Actually make a POST request.
	 *
	 * @abstract
	 * @protected
	 * @param {string} apiUrl
	 * @param {Object} urlParams
	 * @param {Object} bodyParams
	 * @param {Object} headers Header names must be all-lowercase.
	 * @return {Promise<InternalResponse>}
	 */
	internalPost( apiUrl, urlParams, bodyParams, headers ) {
		throw new Error( 'Abstract method internalPost not implemented!' );
	}

}

/**
 * Convenience function to get a boolean from an API response value.
 *
 * Works for formatversion=1 booleans
 * (absent means false, empty string means true)
 * as well as formatversion=2 booleans
 * (absent or false means false, true means true).
 * Mostly useful in library code,
 * when you don’t know the formatversion of the response.
 * (If you control the request parameters, just use formatversion=2.)
 *
 * @param {boolean|''|undefined} value A value from an API response
 * (e.g. response.query.general.rtl).
 * @return {boolean}
 */
function responseBoolean( value ) {
	return ( value && '' ) === '';
}

/**
 * Convenience function to create a Set.
 *
 * The two invocations
 *
 *     new Set( [ 'a', 'b' ] )
 *     set( 'a', 'b' )
 *
 * are equivalent, but the second one is shorter and easier to type.
 *
 * @param {...*} elements
 * @return {Set}
 */
function set( ...elements ) {
	return new Set( elements );
}

// note: exports that are useful to end-users / applications
// should be re-exported from browser.js and node.js
export {
	DEFAULT_OPTIONS,
	ApiErrors,
	ApiWarnings,
	DefaultUserAgentWarning,
	Session,
	makeWarnDroppingTruncatedResultWarning,
	responseBoolean,
	set,
};