
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