Vachan/Vachan.js


import {promiseMap, promiseMapKeys, promiseMapValues} from './promiseMap';
import PLazy from './lazy';

class TimeoutError extends Error {
	constructor(message = 'Timed Out') {
		super(message);
		this.code = 'E_TIMEOUT';
	}

	static ms(interval) {
		return new TimeoutError(`Promise timed out after ${interval} ms`);
	}
}

/**
 * Promise utility functions
 */
class Vachan {
	static logger = console;

	/**
	 * identity function is to make sure returned value is a promise.
	 * returns the following if the input is a:
	 * - promise: returns the promise itself
	 * - function: executes the function and returns the result wrapped in a promise
	 * - any: returns the input wrapped in a promise
	 *
	 * @param {function|Promise<*>|any} promise
	 * @return {Promise<*>}
	 */
	static identity(promise) {
		if (promise && promise.then) {
			return promise;
		}
		if (typeof promise === 'function') {
			try {
				return Vachan.identity(promise());
			}
			catch (e) {
				return Promise.reject(e);
			}
		}
		return Promise.resolve(promise);
	}

	/**
	 * Execute a promise / function, and exit when it completes
	 * @param {Promise<*>|function} promise
	 * @param {object} [options]
	 */
	static exit(promise, options = {}) {
		this.identity(promise).then(() => {
			process.exit(0);
		}).catch((err) => {
			if (!options.silent) {
				Vachan.logger.error(err);
			}
			process.exit(1);
		});
	}

	/**
	 * Execute a promise / function, without caring about its results
	 * @param {Promise<*>|function} promise
	 * @param {object} [options]
	 */
	static exec(promise, options = {}) {
		this.identity(promise).then(() => {}).catch((err) => {
			if (!options.silent) {
				Vachan.logger.error(err);
			}
		});
	}

	/**
	 * create a lazy promise from an executor function ((resolve, reject) => {})
	 * a lazy promise defers execution till .then() or .catch() is called
	 *
	 * @param {function( function(any):void, function(Error):void):void} executor
	 * function(resolve, reject) {},
	 * same as promise constructor
	 * @return {Promise<*>} a lazy promise
	 */
	static lazy(executor) {
		return new PLazy(executor);
	}

	/**
	 * create a lazy promise from an async function
	 * a lazy promise defers execution till .then() or .catch() is called
	 */
	static lazyFrom(asyncFunction) {
		return PLazy.from(asyncFunction);
	}

	/**
	 * Returns a promise that resolves after the specified duration
	 * Can be used to delay / sleep
	 *   Example: await Vachan.sleep(2000);
	 *
	 * @param {number} duration milliseconds to delay for
	 */
	static sleep(duration) {
		return new Promise((resolve) => {
			setTimeout(resolve, duration);
		});
	}

	/**
	 * Promise.finally polyfill
	 * Invoked when the promise is settled regardless of outcome
	 * @see https://github.com/sindresorhus/p-finally
	 * @param {Promise<*>} promise
	 * @param {function} onFinally
	 * @return {Promise<*>} Returns a Promise.
	 */
	static finally(promise, onFinally) {
		if (promise.finally) {
			return promise.finally(onFinally);
		}

		onFinally = onFinally || (() => {});
		return promise.then(
			val => Promise.resolve(onFinally()).then(() => val),
			err => Promise.resolve(onFinally()).then(() => {
				throw err;
			})
		);
	}

	/**
	 * Returns a promise the rejects on specified timeout
	 *
	 * @param {Promise<*>|function} promise A Promise or an async function
	 * @param {object|number} [options] can be {timeout} or a number
	 *  timeout: Milliseconds before timing out
	 */
	static timeout(promise, options = {}) {
		return new Promise((resolve, reject) => {
			const timeout = (typeof options === 'number') ? options : options.timeout;

			const timer = setTimeout(() => {
				reject(TimeoutError.ms(timeout));
			}, timeout);

			Vachan.finally(
				Vachan.identity(promise).then(resolve, reject),
				() => { clearTimeout(timer) },
			);
		});
	}

	/* eslint-disable max-len */
	/**
	 * Returns a Promise that resolves when condition returns true.
	 * Rejects if condition throws or returns a Promise that rejects.
	 * @see https://github.com/sindresorhus/p-wait-for
	 * @param {(function():boolean | Promise<boolean>)} conditionFn function that returns a boolean
	 * @param {object|number} [options] can be {interval, timeout} or a number
	 * @param {number} options.interval: Number of milliseconds to wait before retrying condition (default 50)
	 * @param {number} options.timeout: will reject the promise on timeout (in ms)
	 */
	/* eslint-enable max-len */
	static waitFor(conditionFn, options = {}) {
		const promise = new Promise((resolve, reject) => {
			const interval = (typeof options === 'number') ? options : (options.interval || 50);
			const check = () => {
				Vachan.identity(conditionFn).then((val) => {
					if (val) {
						resolve();
					}
					else {
						setTimeout(check, interval);
					}
				}).catch(reject);
			};

			check();
		});

		if (options.timeout) {
			return Vachan.timeout(promise, options.timeout);
		}
		return promise;
	}

	/**
	 * Returns an async function that delays calling fn
	 * until after wait milliseconds have elapsed since the last time it was called
	 * @see https://github.com/sindresorhus/p-debounce
	 * @param {function} fn function to debounce
	 * @param {number} delay ms to wait before calling fn.
	 * @param {object} [options] object of {leading, fixed}
	 * @param {boolean} options.leading: (default false)
	 * 		Call the fn on the leading edge of the timeout.
	 * 		Meaning immediately, instead of waiting for wait milliseconds.
	 * @param {boolean} options.fixed: fixed delay, each call won't reset the timer to 0
	 */
	static debounce(fn, delay, options = {}) {
		let leadingVal;
		let timer;
		let resolveList = [];

		return function (...args) {
			return new Promise((resolve) => {
				const runImmediately = options.leading && !timer;

				if (timer && options.fixed) {
					resolveList.push(resolve);
					return;
				}

				clearTimeout(timer);
				timer = setTimeout(() => {
					timer = null;
					const res = options.leading ? leadingVal : fn(...args);

					for (resolve of resolveList) {
						resolve(res);
					}

					resolveList = [];
				}, delay);

				if (runImmediately) {
					leadingVal = fn(...args);
					resolve(leadingVal);
				}
				else {
					resolveList.push(resolve);
				}
			});
		};
	}
}

Vachan.map = promiseMap;
Vachan.mapKeys = promiseMapKeys;
Vachan.mapValues = promiseMapValues;

export default Vachan;