Source: functions.js

/*!
 * Module Functions
 */

/**
 * @namespace Functions
 */

const MODULE_NAME = 'Functions';



//###[ IMPORTS ]########################################################################################################

import {orDefault, isFunction, isPlainObject, assert, hasValue} from './basic.js';
import {schedule, reschedule} from './timers.js';



//###[ EXPORTS ]########################################################################################################

/**
 * @namespace Functions:throttle
 */

/**
 * Returns a throttled function (based on an unthrottled one), which executes only once in a timeframe at most.
 * This is especially helpful to react to events, that might come in avalanches in an orderly and performant way,
 * let's say changing layout due to a resizing or scrolling event.
 *
 * If you are trying to react to events, that occur a lot, in a synchronous fashion, meaning, that you rely on values
 * and data having been updated after the event, so there is a clear time arrow of things happening in order, you might
 * need to set hasLeadingExecution and/or hasTrailingExecution to true to better cover those cases.
 *
 * Be aware that the precision of this method relies in part on the client's cpu, so this is implementation might
 * not be right if you need a razor sharp exact amount of calls in a given time every time, this is a more simple
 * and fuzzy implementation for basic purposes, which should cover 90% of your needs.
 * For a more precise and battle-tested version, see lodash's complex implementation:
 * https://www.npmjs.com/package/lodash.throttle
 *
 * @param {Number} ms - the timeframe for one execution at most in milliseconds
 * @param {Function} func - the function to throttle
 * @param {?Boolean} [hasLeadingExecution=false] - defines that the function call that starts a timeframe, does not count, so that during the following frame another call is possible
 * @param {?Boolean} [hasTrailingExecution=false] - defines if the function is executed at the end of the timeframe (will only happen, if there were more than one calls to the function in the time frame)
 * @throws error if ms is no number > 0 or func is not a function
 * @returns {Function} the throttling function (parameters will be handed as is to the throttled function)
 *
 * @memberof Functions:throttle
 * @alias throttle
 * @example
 * window.addEventListener('resize', throttle(400, function(){ console.log(`the viewport is now ${window.innerWidth}px wide`); }));
 */
export function throttle(ms, func, hasLeadingExecution=false, hasTrailingExecution=false){
	ms = orDefault(ms, 0, 'int');
	hasLeadingExecution = orDefault(hasLeadingExecution, false, 'bool');
	hasTrailingExecution = orDefault(hasTrailingExecution, false, 'bool');

	assert(ms > 0, `${MODULE_NAME}:throttle | ms must be > 0`);
	assert(isFunction(func), `${MODULE_NAME}:throttle | no function given`);

	let
		frameHasStarted = false,
		callsInFrame = 0
	;

	return function(){
		const args = Array.from(arguments);

		if( !frameHasStarted ){
			frameHasStarted = true;
			if( !hasLeadingExecution ){
				callsInFrame++;
			}

			func.apply(this, args);

			 schedule(ms, () => {
				if( hasTrailingExecution && (callsInFrame > 1) ){
					func.apply(this, args);
				}

				frameHasStarted = false;
				callsInFrame = 0;
			});
		} else if( callsInFrame === 0 ){
			callsInFrame++;
			func.apply(this, args);
		} else {
			callsInFrame++;
		}
	};
}



/**
 * @namespace Functions:debounce
 */

/**
 * Hold the execution of a function until it has not been called for a specific timeframe.
 *
 * This is a basic implementation for 90% of all cases, if you need more options and more control
 * over details, have a look at lodash's implementation:
 * https://www.npmjs.com/package/lodash.debounce
 *
 * @param {Number} ms - timeframe in milliseconds without call before execution
 * @param {Function} func - the function to delay the execution of
 * @throws error if ms is no number > 0 or func is not a function
 * @returns {Function} the debounced function (parameters will be handed as is to the provided function)
 *
 * @memberof Functions:debounce
 * @alias debounce
 * @example
 * document.querySelector('input[name=search]').addEventListener('change', debounce(1000, function(){ refreshSearch(); }));
 */
export function debounce(ms, func){
	ms = orDefault(ms, 0, 'int');

	assert(ms > 0, `${MODULE_NAME}:debounce | ms must be > 0`);
	assert(isFunction(func), `${MODULE_NAME}:debounce | no function given`);

	let	debounceTimer;

	return function(){
		debounceTimer = reschedule(debounceTimer, ms, () => { func.apply(this, Array.from(arguments)); });
	};
}



/**
 * @namespace Functions:defer
 */

/**
 * Defer the execution of a function until the callstack is empty.
 * This works identical to setTimeout(function(){}, 1);
 *
 * @param {Function} func - the function to defer
 * @param {?Number} [delay=1] - the delay to apply in milliseconds, 1 is a non-minifiable value to target the next tick, but you may define any millisecond value you want, to manually delay the function execution
 * @throws error if func is not a function or delay is no number > 0
 * @returns {Function} the deferred function; the deferred function returns the timer id, in case you want to cancel execution
 *
 * @memberof Functions:defer
 * @alias defer
 * @example
 * defer(function(){ doOnNextTick(); })();
 * defer(function(){ doInTwoSeconds(); }, 2000)();
 */
export function defer(func, delay=1){
	delay = orDefault(delay, 1, 'int');

	assert(isFunction(func), `${MODULE_NAME}:defer | no function given`);
	assert(delay > 0, `${MODULE_NAME}:defer | delay must be > 0`);

	return function(){
		return schedule(delay, () => { func.apply(this, Array.from(arguments)); }).id;
	};
}



/**
 * @namespace Functions:kwargs
 */

/**
 * This function creates a function where we can set all parameters as a config object by name for each
 * call, while also allowing to set default values for parameters on function creation.
 *
 * This is heavily inspired by Python's way of handling parameters, therefore the name.
 *
 * This enables you to overload complex function signatures with tailor-fit version for your use cases and
 * to call functions with specific parameter sets in a very readable way, without adding empty values to the
 * list of parameters. So you just define what you want to set and those parts are clearly named.
 *
 * Each parameter you pass to the created kwargs function may be one of two variants:
 * - either it is not a plain object; in that case the parameter is passed to the original function as is at the
 *   position the parameter is declared in the call
 * - or the parameter is a plain object, in which case we treat the parameter as kwargs and try to match keys
 *   to parameters; in case you ever have to pass a plain object as-is: setting "kwargs: false" in the object
 *   tells the parser to skip matching props to parameters
 *
 * You can even mix these types. If two parameters describe the same value in the call, the last declaration wins.
 *
 * @param {Function} func - the function to provide kwargs to
 * @param {?Object} [defaults=null] - the default kwargs to apply to func, essentially setting default values for all given keys fitting parameters of the function
 * @throws error if func is not a function or parameter names of func could not be identified
 * @returns {Function} new function accepting mixed args, also being possible kwarg dicts
 *
 * @memberof Functions:kwargs
 * @alias kwargs
 * @example
 * const fTest = function(tick, trick, track){ console.log(tick, trick, track); };
 * const fKwargsTest = kwargs(fTest, {track : 'defaultTrack'});
 * fKwargsTest({tick : 'tiick', trick : 'trick'});
 * => "tiick, trick, defaultTrack"
 * kwargs(fTest, {track : 'defaultTrack'})('argumentTick', {trick : 'triick', track : 'trACK'});
 * => "argumentTick, triick, trACK"
 * kwargs(fTest, {track : 'defaultTrack'})('argumentTick', {trick : 'triick', track : 'track'}, 'trackkkk');
 * => "argumentTick, triick, trackkkk"
 */
export function kwargs(func, defaults=null){
	defaults = isPlainObject(defaults) ? defaults : {};

	assert(isFunction(func), `${MODULE_NAME}:kwargs | no function given`);

	const
		argNamesString = func.toString().match(/\(([^)]+)/)[1],
		argNames = argNamesString ? argNamesString.split(',').map(argName => `${argName}`.trim()) : []
	;

	assert(argNames.length > 0, `${MODULE_NAME}:kwargs | could not identify parameter names in "${func.toString()}" using parameter string "${argNamesString}"`);

	return function(){
		const applicableArgs = [];

		Array.from(arguments).forEach((arg, argIndex) => {
			if(
				isPlainObject(arg)
				// if object contains falsy property "kwargs" leave it as is
				&& (!hasValue(arg.kwargs) || !!arg.kwargs)
			){
				argNames.forEach((argName, argNameIndex) => {
					if( hasValue(arg[argName]) ){
						applicableArgs[argNameIndex] = arg[argName];
					}
				});
			} else {
				applicableArgs[argIndex] = arg;
			}
		});

		argNames.forEach((argName, argNameIndex) => {
			if(
				!hasValue(applicableArgs[argNameIndex])
				&& hasValue(defaults[argName])
			){
				applicableArgs[argNameIndex] = defaults[argName];
			}
		});

		return func.apply(this, applicableArgs);
	};
}