All Downloads are FREE. Search and download functionalities are using the official Maven repository.

functional-commons.src.functional-commons.js Maven / Gradle / Ivy

'use strict'
const {promisify: p} = require('util')
const makeThrottledFunction = require('./make-throttled-function')

/**
 * @template T
 * @param {(...args: any[]) => T} syncCreationFunction
 * @returns {(...args: any[]) => T}
 */
function cacheFunctionSync(syncCreationFunction) {
  const cacheMap = new Map()

  return (...args) => {
    const key = JSON.stringify(args)

    const cachedValue = cacheMap.get(key)
    if (cachedValue) {
      return cachedValue
    }
    const newValue = syncCreationFunction(...args)
    if (typeof newValue.then === 'function')
      throw new Error('your function returned a promise. Maybe you want to use cacheFunctionAsync?')

    cacheMap.set(key, newValue)

    return newValue
  }
}

/**
 *
 * @param {(...args: any[]) => PromiseLike} asyncCreationFunction
 * @param {{?expireAfterMs: number, ?nowService: {now: () => number}}} [options]
 *
 * @returns {(...args: any[]) => PromiseLike}
 */
function cacheFunctionAsync(
  asyncCreationFunction,
  {expireAfterMs = undefined, nowService = Date} = {},
) {
  const cacheMap = new Map()

  return async (...args) => {
    const key = JSON.stringify(args)

    const cachedValue = cacheMap.get(key)
    if (cachedValue) {
      const {value, creationTime} = cachedValue

      if (expireAfterMs === undefined || nowService.now() < creationTime + expireAfterMs) {
        return value
      }
    }
    const newValue = await asyncCreationFunction(...args)

    cacheMap.set(key, {value: newValue, creationTime: nowService.now()})

    return newValue
  }
}

/**
 * @param {number} from
 * @param {number} to
 * @param {?number} step (default 1)
 *
 * @returns {number[]}
 */
function range(from, to, step = 1) {
  if (from >= to) return []

  return Array(Math.ceil((to - from) / step))
    .fill(0)
    .map((_, i) => i * step + from)
}

/**
 *
 * @param {number[]} numbers
 * @returns {number}
 */
function sum(numbers) {
  return numbers.reduce((a, b) => a + b, 0)
}

/**
 *
 * @param {any} err
 */
function throw_(err) {
  throw err
}

/**
 * @param {[string, any][]} entries
 *
 * @returns {{[x: string]: any}}
 */
function objectFromEntries(entries) {
  return entries.reduce((acc, [name, value]) => ((acc[name] = value), acc), {})
}

function group(prop, list) {
  return list.reduce(function (grouped, item) {
    const key = item[prop]

    grouped[key] = grouped[key] || []

    grouped[key].push(item)

    return grouped
  }, {})
}

const groupBy = (prop) => (list) => group(prop, list)

/**
 * @param {{[x: string]: any}} object
 * @param {([string, any]) => [string, any]} mapFunction
 *
 * @returns {{[x: string]: any}}
 */
function mapObject(object, mapFunction) {
  return objectFromEntries(Object.entries(object).map(([key, value]) => mapFunction(key, value)))
}

/**
 * @param {{[x: string]: any}} object
 * @param {(value: any) => any}  mapFunction
 *
 * @returns {{[x: string]: any}}
 */
function mapValues(object, mapFunction) {
  return mapObject(object, (key, value) => [key, mapFunction(value)])
}

/**
 * @param {{[x: string]: any}} object
 * @param {(value: any) => any}  mapFunction
 *
 * @returns {{[x: string]: any}}
 */
function mapKeys(object, mapFunction) {
  return mapObject(object, (key, value) => [mapFunction(key), value])
}

/**
 *
 * @param {object} object
 * @param {(key:string) => boolean} filterFunc
 *
 * @returns {object}
 */
function filterKeys(object, filterFunc) {
  const ret = {}

  for (const [k, v] of Object.entries(object)) {
    if (filterFunc(k)) {
      ret[k] = v
    }
  }

  return ret
}

/**
 * @param {object} object
 * @param {(value:any) => boolean} filterFunc
 * @returns {object}
 */
function filterValues(object, filterFunc) {
  const ret = {}

  for (const [k, v] of Object.entries(object)) {
    if (filterFunc(v)) {
      ret[k] = v
    }
  }

  return ret
}

/**
 * @param {object} object
 * @param {(entry: [string, any]) => boolean} filterFunc
 * @returns {object}
 */
function filterEntries(object, filterFunc) {
  const ret = {}

  for (const entry of Object.entries(object)) {
    if (filterFunc(entry)) {
      ret[entry[0]] = entry[1]
    }
  }

  return ret
}

/**
 * @param {number} ms
 * @param {() => any} errFactory
 *
 * @returns {Promise}
 */
async function failAfter(ms, errFactory) {
  await p(setTimeout)(ms)

  throw errFactory()
}

/**
 * @template T
 * @param {PromiseLike} promise
 *
 * @returns {PromiseLike<[any|undefined, T|undefined]>} a 2-tuple where the first element is the error if promise is rejected,
 *   or undefined if resolved,
 *   and the second value is the value resolved by the promise, or undefined if rejected
 */
function presult(promise) {
  return promise.then(
    (v) => [undefined, v],
    (err) => [err],
  )
}

/**
 * @template T
 * @param {PromiseLike} promise
 *
 * @returns {PromiseLike<[any|undefined, T|undefined]>}
 */
function unwrapPresult(presultPromise) {
  return presultPromise.then(([err, v]) => (err != null ? Promise.reject(err) : v))
}
/**
 *
 * @param {number} ms
 * @returns {PromiseLike}
 */
function delay(ms) {
  return p(setTimeout)(ms)
}

/**
 * @template T, V
 * @param {Promise|((hasAborted: () => boolean) => Promise)} promiseOrPromiseFunc
 * @param {number} timeout
 * @param {V} value
 * @returns {Promise}
 */
function ptimeoutWithValue(promiseOrPromiseFunc, timeout, value) {
  return ptimeoutWithFunction(promiseOrPromiseFunc, timeout, () => value)
}

/**
 * @template T
 * @param {Promise|((hasAborted: () => boolean) => Promise)} promiseOrPromiseFunc
 * @param {number} timeout
 * @param {any} error
 * @returns {Promise}
 */
function ptimeoutWithError(promiseOrPromiseFunc, timeout, error) {
  return ptimeoutWithFunction(promiseOrPromiseFunc, timeout, () => Promise.reject(error))
}

/**
 * @template T, V
 * @param {Promise|((hasAborted: () => boolean) => Promise)} promiseOrPromiseFunc
 * @param {number} timeout
 * @param {() => Promise} func
 * @returns {Promise}
 */
async function ptimeoutWithFunction(promiseOrPromiseFunc, timeout, func) {
  let promiseResolved = false
  const hasAborted = () => promiseResolved

  const promise = promiseOrPromiseFunc.then
    ? promiseOrPromiseFunc
    : promiseOrPromiseFunc(hasAborted)

  let cancel
  const v = await Promise.race([
    promise.then(
      (v) => ((promiseResolved = true), cancel && clearTimeout(cancel), v),
      (err) => ((promiseResolved = true), cancel && clearTimeout(cancel), Promise.reject(err)),
    ),
    new Promise(
      (res) =>
        (cancel = setTimeout(() => {
          if (promiseResolved) res(undefined)
          else {
            cancel = undefined
            promiseResolved = true
            res(func())
          }
        }, timeout)),
    ),
  ])

  return v
}

/**
 *
 * @param {{[x: string]: any}|string} error
 * @param {{[x: string]: any}} [properties=undefined]
 *
 * @returns {{[x: string]: any}}
 */
function makeError(error, properties) {
  if (typeof error === 'string') {
    error = new Error(error)
  }
  if (!properties) return error

  return Object.assign(error, properties)
}

/**
 *
 * @param  {...any[]} arrays
 * @returns {any[][]}
 */
function zip(...arrays) {
  const maxLength = arrays.reduce(
    (maxTillNow, array) => (array.length > maxTillNow ? array.length : maxTillNow),
    0,
  )

  const zipResult = []

  for (const i of range(0, maxLength)) {
    zipResult.push(arrays.map((array) => array[i]))
  }

  return zipResult
}

/**
 * @template T, V
 * @param {(T[]|T)[]} array
 * @param {T => V} [mapperFunction]
 * @returns {V[]}
 */
function flatMap(array, mapperFunction = undefined) {
  const flatResult = []

  for (const a of array) {
    if (Array.isArray(a)) {
      for (const aa of a) {
        flatResult.push(mapperFunction ? mapperFunction(aa) : aa)
      }
    } else {
      flatResult.push(mapperFunction ? mapperFunction(a) : a)
    }
  }

  return flatResult
}

function minus(bigArray = [], smallArray = [], keyMapper = (x) => x) {
  return bigArray.filter((item) => !smallArray.map(keyMapper).includes(keyMapper(item)))
}

function diff(left = [], right = [], keyMapper = (x) => x) {
  const intersection = left.filter((item) => right.map(keyMapper).includes(keyMapper(item)))
  return minus(left.concat(right), intersection, keyMapper)
}

const resolveSymbol = Symbol('resolve-promise')
const rejectSymbol = Symbol('reject-promise')

/**
 * @returns {Promise}
 */
function makeResolveablePromise() {
  let exteriorResolve
  let exteriorReject
  const promise = new Promise(
    (resolve, reject) => ([exteriorResolve, exteriorReject] = [resolve, reject]),
  )

  promise[resolveSymbol] = exteriorResolve
  promise[rejectSymbol] = exteriorReject

  return promise
}

/**
 *
 * @param {Promise} promise
 * @param {any} value
 */
function resolveResolveablePromise(promise, value) {
  promise[resolveSymbol](value)
}

/**
 *
 * @param {Promise} promise
 * @param {any} err
 */
function rejectResolveablePromise(promise, err) {
  promise[rejectSymbol](err)
}

/**
 *
 * @param {object} object
 * @param {string[] | {[x: String]: any}} objectKeys
 *
 * @returns {object}
 */
function pick(object, objectKeys) {
  if (object === undefined) return undefined
  if (object === null) return null

  const keys = Array.isArray(objectKeys) ? objectKeys : Object.keys(objectKeys || {})

  const ret = {}
  for (const key of keys) {
    if (!(key in object)) continue

    ret[key] = object[key]
  }

  return ret
}

function isFunction(functionToCheck) {
  return functionToCheck && typeof functionToCheck == 'function'
}

async function resolve(object) {
  return object
}

/**
 * This function should work like bluebird.props
 * The input is object with fields that their values can be promises (or regular values) and the output is
 * an object with the same keys and the resolved values.
 * @param {} object
 */
async function promiseProps(object) {
  object = await resolve(object)
  if (typeof object != 'object') {
    throw new Error('object must be of type object')
  }
  const result = {}
  const promises = []
  for (let key of Object.keys(object)) {
    async function resolveProperty() {
      return object[key]
    }
    async function populate() {
      const value = await resolveProperty()
      result[key] = value
    }
    promises.push(populate())
  }
  await Promise.all(promises)
  return result
}

/**
 * This function mimics the bluebird.map function.
 * @param {} collection - Collection of elemenst (or promise that resolves to a collection of elements)
 * @param {*} mapper - function(any item, int index, int length)
 * @param {Object {concurrency: int=Infinity}} options
 */
async function promiseMap(collection, mapper, {concurrency} = {concurrency: Infinity}) {
  if (concurrency != Infinity) {
    if (concurrency <= 0 || concurrency > 100) {
      throw new Error('concurrency should be in range: 1..100 or Infinity')
    }
  }

  if (!isFunction(mapper)) {
    throw new Error('mapper must be a function')
  }
  collection = await resolve(collection)
  if (!Array.isArray(collection)) {
    throw new Error('collection must be array')
  }
  if (collection.length == 0) {
    return []
  }
  const promises = []
  const result = new Array(collection.length)
  if (concurrency == Infinity) {
    for (let i = 0; i < collection.length; i++) {
      async function resolveElement() {
        result[i] = await mapper(await resolve(collection[i]), i, collection.length)
      }
      promises.push(resolveElement())
    }
  } else {
    const collectionWithIndexes = []
    for (let i = 0; i < collection.length; i++) {
      collectionWithIndexes.push({object: collection[i], index: i})
    }

    for (let i = 0; i < concurrency; i++) {
      async function worker() {
        while (collectionWithIndexes.length > 0) {
          const element = await resolve(collectionWithIndexes.pop())
          result[element.index] = await mapper(element.object, element.index, collection.length)
        }
      }
      promises.push(worker())
    }
  }
  await Promise.all(promises)
  return result
}

module.exports = {
  cacheFunctionSync,
  cacheFunctionAsync,
  throw_,
  range,
  sum,
  objectFromEntries,
  mapObject,
  mapValues,
  mapKeys,
  filterKeys,
  filterValues,
  filterEntries,
  failAfter,
  presult,
  unwrapPresult,
  ptimeoutWithValue,
  ptimeoutWithError,
  ptimeoutWithFunction,
  makeError,
  zip,
  delay,
  flatMap,
  makeThrottledFunction,
  makeResolveablePromise,
  resolveResolveablePromise,
  rejectResolveablePromise,
  pick,
  minus,
  diff,
  group,
  groupBy,
  promiseMap,
  promiseProps,
  isFunction,
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy