promisify.js

'use strict';

const Aigle = require('./aigle');
const { INTERNAL, callThen } = require('./internal/util');

const globalSetImmediate = typeof setImmediate === 'function' ? setImmediate : {};
const custom =
  (() => {
    try {
      return require('util').promisify.custom;
    } catch (e) {
      return;
    }
  })() || {};

module.exports = promisify;

/**
 * @param {Object|Function} fn
 * @param {string|number|Object} [fn]
 * @param {Object} [fn.context]
 * @example
 * const func = (a, b, c, callback) => callback(null, a + b + c);
 * Aigle.promisify(func)(1, 2, 3)
 *   .then(value => console.log(value)); // 6
 *
 * @example
 * const obj = {
 *   val: 1,
 *   get(callback) {
 *     callback(null, this.val);
 *   }
 * };
 *
 * // using bind
 * Aigle.promisify(obj.get.bind(obj))().then(console.log);
 *
 * // using context
 * Aigle.promisify(obj.get, { context: obj })().then(console.log);
 *
 * // using shorthand
 * Aigle.promisify(obj, 'get')().then(console.log);
 */
function promisify(fn, opts) {
  switch (typeof fn) {
    case 'object':
      switch (typeof opts) {
        case 'string':
        case 'number':
          if (typeof fn[opts] !== 'function') {
            throw new TypeError('Function not found key: ' + opts);
          }
          if (fn[opts].__isPromisified__) {
            return fn[opts];
          }
          return makeFunctionByKey(fn, opts);
        default:
          throw new TypeError('Second argument is invalid');
      }
    case 'function':
      if (fn.__isPromisified__) {
        return fn;
      }
      const ctx = opts && opts.context !== undefined ? opts.context : undefined;
      return makeFunction(fn, ctx);
    default:
      throw new TypeError('Type of first argument is not function');
  }
}

/**
 * @private
 * @param {Aigle} promise
 */
function makeCallback(promise) {
  return (err, res) => (err ? promise._reject(err) : promise._resolve(res));
}

/**
 * @private
 * @param {Object} obj
 * @param {string} key
 */
function makeFunctionByKey(obj, key) {
  promisified.__isPromisified__ = true;
  return promisified;

  function promisified(arg) {
    const promise = new Aigle(INTERNAL);
    const callback = makeCallback(promise);
    let l = arguments.length;
    switch (l) {
      case 0:
        obj[key](callback);
        break;
      case 1:
        obj[key](arg, callback);
        break;
      default:
        const args = Array(l);
        while (l--) {
          args[l] = arguments[l];
        }
        args[args.length] = callback;
        obj[key].apply(obj, args);
        break;
    }
    return promise;
  }
}

/**
 * @private
 * @param {function} fn
 * @param {*} [ctx]
 */
function makeFunction(fn, ctx) {
  const func = fn[custom];
  if (func) {
    nativePromisified.__isPromisified__ = true;
    return nativePromisified;
  }
  switch (fn) {
    case setTimeout:
      return Aigle.delay;
    case globalSetImmediate:
      return Aigle.resolve;
  }
  promisified.__isPromisified__ = true;
  return promisified;

  function nativePromisified(arg) {
    const promise = new Aigle(INTERNAL);
    let l = arguments.length;
    let p;
    switch (l) {
      case 0:
        p = func.call(ctx || this);
        break;
      case 1:
        p = func.call(ctx || this, arg);
        break;
      default:
        const args = Array(l);
        while (l--) {
          args[l] = arguments[l];
        }
        p = func.apply(ctx || this, args);
        break;
    }
    callThen(p, promise);
    return promise;
  }

  function promisified(arg) {
    const promise = new Aigle(INTERNAL);
    const callback = makeCallback(promise);
    let l = arguments.length;
    switch (l) {
      case 0:
        fn.call(ctx || this, callback);
        break;
      case 1:
        fn.call(ctx || this, arg, callback);
        break;
      default:
        const args = Array(l);
        while (l--) {
          args[l] = arguments[l];
        }
        args[args.length] = callback;
        fn.apply(ctx || this, args);
        break;
    }
    return promise;
  }
}