UNPKG

@alexaegis/common

Version:

Common utility functions

576 lines (575 loc) 20.1 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region src/async/is-promise-fulfilled.function.ts var isPromiseFulfilled = (promiseResult) => { return promiseResult.status === "fulfilled"; }; //#endregion //#region src/async/async-filter-map.function.ts var asyncFilterMap = async (array, map) => { return (await Promise.allSettled(array.map(map))).filter(isPromiseFulfilled).map((item) => item.value).filter(isNotNullish); }; //#endregion //#region src/async/async-filter.function.ts var filterMark = {}; var asyncFilter = async (array, predicate) => { return (await Promise.allSettled(array.map((item, i) => { return predicate(item, i).then((result) => result ? item : filterMark); }))).filter(isPromiseFulfilled).map((item) => item.value).filter((result) => result !== filterMark); }; //#endregion //#region src/async/async-map.function.ts var asyncMap = async (array, map) => { return Promise.all(array.map(map)); }; //#endregion //#region src/async/buffered-all-settled.function.ts /** * This function will process async tasks in batches, limited by the bufferSize. * Since it needs to have control over when promises start (and since promises * are always hot) instead of promises you need to pass functions that return * the promises. * * @param tasks an array of Promise factories (async functions) * @param bufferSize (default 25) at most, this many tasks will be running at a time * @returns the results of each promise in the order they resolved or rejected */ var bufferedAllSettled = async (tasks, bufferSize = 25) => { const buffer = /* @__PURE__ */ new Map(); const results = []; while (tasks.length > 0) { while (buffer.size < bufferSize) { const task = tasks.shift(); const promise = task().then((value) => { results.push({ status: "fulfilled", value }); buffer.delete(task); return value; }).catch((error) => { results.push({ status: "rejected", reason: error }); buffer.delete(task); return error; }); buffer.set(task, promise); } await Promise.any(buffer.values()); } await Promise.allSettled(buffer.values()); return results; }; //#endregion //#region src/es/default-es-target.const.ts var DEFAULT_ES_TARGET_YEAR = 2022; var DEFAULT_ES_TARGET_NAME = `es${DEFAULT_ES_TARGET_YEAR.toString()}`; //#endregion //#region src/functions/group-by.function.ts /** * groups an array of elements into multiple arrays on an object where the * key is the string returned by the groupKey function. * * If the groupKey function returns undefined, the value won't be included * in the result. This can be used to filter out values from the initial array. * * There's also an ECMAScript proposal that will add a very similar function * on Object and Array: https://github.com/tc39/proposal-array-grouping * * This function can be deprecated once it's widely released. */ var groupBy = (elements, groupKey) => { return elements.reduce((acc, next) => { const key = groupKey(next); if (key) { let group = acc[key]; if (group) group.push(next); else { const newGroup = [next]; group = newGroup; acc[key] = newGroup; } } return acc; }, {}); }; //#endregion //#region src/functions/identity.function.ts /** * Returns whatever you pass to it. */ var identity = (r) => r; /** * Returns whatever you pass to it. Asynchronously. While the regular * `identity` function is already awaitable, this one is always thenable. */ var identityAsync = async (value, mode = "micro") => new Promise((resolve) => { if (mode === "micro") resolve(value); else setTimeout(() => { resolve(value); }, 0); }); //#endregion //#region src/functions/memoize.function.options.ts var normalizeMemoizeOptions = (options) => { return { argHasher: options?.argHasher ?? JSON.stringify, thisContext: options?.thisContext, maxCacheEntries: options?.maxCacheEntries ?? Number.POSITIVE_INFINITY, cache: options?.cache ?? /* @__PURE__ */ new Map() }; }; //#endregion //#region src/functions/memoize.function.ts /** * By default the cache does not start growing, the limit is infinite. * To set otherwise, use the options object. * * @param fn to be memoized * @param rawOptions options to customize cache behavior * @returns the memoized version of the function that will either return a cached value * or call the original function when one does not exist. */ var memoize = (fn, rawOptions) => { const options = normalizeMemoizeOptions(rawOptions); const dropQueue = []; return (...args) => { const hash = options.argHasher(args); if (options.cache.has(hash)) return options.cache.get(hash); else { const result = fn.apply(options.thisContext, args); options.cache.set(hash, result); if (options.maxCacheEntries > 0 && options.maxCacheEntries < Number.POSITIVE_INFINITY) { dropQueue.push(hash); if (dropQueue.length > options.maxCacheEntries) { const cacheToDrop = dropQueue.shift(); options.cache.delete(cacheToDrop); } } return result; } }; }; //#endregion //#region src/functions/noop.function.ts /** * Does nothing. */ var noop = () => void 0; /** * Does nothing asynchronously as a macro-task. * */ var noopAsync = (mode = "micro") => new Promise((resolve) => { if (mode === "micro") resolve(void 0); else setTimeout(() => { resolve(void 0); }, 0); }); //#endregion //#region src/functions/normalize-regexplike-into-regexp.function.ts var normalizeRegExpLikeToRegExp = (regExpLike) => { return typeof regExpLike === "string" ? new RegExp(regExpLike) : regExpLike; }; //#endregion //#region src/functions/sleep.function.ts var sleep = (ms) => { return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve(); }; //#endregion //#region src/functions/yes.function.ts var yes = () => true; var yesAsync = (mode = "micro") => new Promise((resolve) => { if (mode === "micro") resolve(true); else setTimeout(() => { resolve(true); }, 0); }); //#endregion //#region src/math/closest-number.function.ts /** * It biases towards the smaller number. If the `<` would be `<=` then it * would bias towards the larger. */ var closestNumber = (numbers, target) => numbers.reduce((prev, curr) => Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev); //#endregion //#region src/math/random.function.ts function random(min, max) { if (min > max) [min, max] = [max, min]; return Math.floor(Math.random() * (max - min + 1) + min); } //#endregion //#region src/objects/deep-freeze.function.ts /** * Applies Object.freeze deeply, as seen on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze * * @returns the same object but frozen. */ var deepFreeze = (object, dontFreeze = /* @__PURE__ */ new Set()) => { dontFreeze.add(object); const propNames = Reflect.ownKeys(object); for (const name of propNames) { const value = object[name]; if ((value && typeof value === "object" || typeof value === "function") && !dontFreeze.has(value)) deepFreeze(value, dontFreeze); } return Object.freeze(object); }; //#endregion //#region src/objects/is-object.function.ts /** * Simple object check. * @param item * @returns {boolean} */ function isObject(item) { return item !== null && item !== void 0 && typeof item === "object" && !Array.isArray(item); } //#endregion //#region src/objects/deep-map-object.function.ts var deepMapObject = (o, mapper) => { const target = { ...o }; if (isObject(target)) for (const key in target) { const value = target[key]; if (isObject(value)) Object.assign(target, { [key]: deepMapObject(value, mapper) }); else { const mapResult = mapper(key, value); if (mapResult === void 0) delete target[key]; else Object.assign(target, { [key]: mapResult }); } } return target; }; //#endregion //#region src/objects/drop-keys.function.ts var defaultDropKeyMatcher = (value, _key) => value === void 0; /** * Drops all keys that resolve to true using a matcher. * By default it drops keys set as undefined * * Mutates the object! Use `structuredClone` before if you don't want that. */ var dropKeys = (t, matcher = defaultDropKeyMatcher) => { for (const key in t) { if (matcher(t[key], key)) delete t[key]; if (isObject(t[key])) dropKeys(t[key], matcher); } return t; }; //#endregion //#region src/objects/deep-merge.function.ts var deepMergeInternal = (sources, options, visited = /* @__PURE__ */ new Set()) => { const firstSource = sources.shift(); visited.add(firstSource); const merged = structuredClone(firstSource); for (const source of sources) { visited.add(source); if (Array.isArray(source) && Array.isArray(merged)) { for (const element of source) if (!merged.includes(element)) merged.push(element); } else if (isObject(source) && isObject(merged)) for (const key in source) { const sourceValue = source[key]; if (Object.hasOwn(merged, key) && merged[key] === void 0 && options?.preferUndefined) continue; if (isObject(sourceValue)) if (merged[key]) merged[key] = deepMergeInternal([merged[key], structuredClone(sourceValue)], options, visited); else Object.assign(merged, { [key]: structuredClone(sourceValue) }); else Object.assign(merged, { [key]: structuredClone(sourceValue) }); } } if (options?.dropKeys) dropKeys(merged, typeof options.dropKeys === "function" ? options.dropKeys : defaultDropKeyMatcher); return merged; }; /** * Merges multiple objects in order into the first argument. * It mutates the target! Arrays are merged naively using === equality. * * Keys that are explicitly set to `undefined` among sources are dropped from * the target object. */ var deepMerge = (sources, options) => { return deepMergeInternal(sources, options); }; //#endregion //#region src/objects/fill-string-with-template-variables.function.ts /** * Maps and fills a single string value in an object based on a variableMap. * * If the target object has a string value containing `"${variableName}"`, the * corresponding value from the variableMap will be substitued into it. */ var fillStringWithTemplateVariables = (value, variables) => { return Object.entries(variables).reduce((acc, [variableKey, variableValue]) => { return acc.replaceAll("${" + variableKey + "}", variableValue); }, value); }; //#endregion //#region src/objects/fill-object-with-template-variables.function.ts /** * Maps and fills every string value in an object based on a variableMap. * * If the target object has a string value containing `"${variableName}"`, the * corresponding value from the variableMap will be substitued into it. */ var fillObjectWithTemplateVariables = (target, variables) => { return deepMapObject(target, (_key, value) => { return typeof value === "string" ? fillStringWithTemplateVariables(value, variables) : value; }); }; //#endregion //#region src/objects/is-not-nullish.function.ts var isNotNullish = (o) => o !== void 0 && o !== null; //#endregion //#region src/objects/is-nullish.function.ts var isNullish = (o) => o === void 0 || o === null; //#endregion //#region src/objects/is-promise-like.function.ts var isPromiseLike = (candidate) => { return isNotNullish(candidate) && typeof candidate === "object" && typeof candidate["then"] === "function" && typeof candidate["catch"] === "function"; }; //#endregion //#region src/objects/map-object.function.ts var mapObject = (o, map) => { return Object.fromEntries(Object.entries(o).map(([key, value]) => { return [key, map(value, key)]; })); }; //#endregion //#region src/options/dry.function.ts /** * Conditionally dries up a function. When a function is dry it will * no longer be called, instead it will return true */ var drySync = (isDry, whenWet, dryDefault = true) => { return isDry ? () => identity(dryDefault) : whenWet; }; var dry = (isDry, whenWet, dryDefault = true) => { return isDry ? () => identityAsync(dryDefault) : whenWet; }; //#endregion //#region src/options/dry.option.ts var normalizeDryOption = (options) => { return { dry: options?.dry ?? false }; }; //#endregion //#region src/options/force.option.ts var normalizeForceOption = (options) => { return { force: options?.force ?? false }; }; //#endregion //#region src/options/safe.option.ts var normalizeSafeOption = (options) => { return { safe: options?.safe ?? false }; }; //#endregion //#region src/sorting/regex-specificity.function.ts /** * Getting a proper specificity measure is a hard problem, this is but a very * simple solution, that is good-enough for this usecase. * It's just counting how many of the matched words letters are actually in * the regex. * * For RegExp's pass in the `.source` */ var measureRegExpSpecificity = (r, v) => { return [...v].filter((c) => r.includes(c)).length; }; //#endregion //#region src/sorting/shuffle-array.function.ts /** * Randomizes the placement of elements within an array, shuffling it. * * It mutates the input array! */ function shuffleArray(array) { let currentIndex = array.length; while (0 !== currentIndex) { const randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; const temporaryValue = array[currentIndex]; const randomValue = array[randomIndex]; if (isNotNullish(temporaryValue) && isNotNullish(randomValue)) { array[currentIndex] = randomValue; array[randomIndex] = temporaryValue; } } return array; } //#endregion //#region src/sorting/sort-object.function.ts var arrayToObject = (a) => { return a.reduce((acc, next) => { acc[next] = next; return acc; }, {}); }; var regexpSorterOne = (a, b) => a.test(b.source) || b.test(a.source) ? -1 : a.source.localeCompare(b.source); /** * * Among multiple `matchers` like ['.*', 'b', '.*'], when trying to fit in * a `key`, which slot would maintain order the best? For example fitting * 'a' into the above matcher order, it should go to the left slot, but fitting * 'c' should make it go to the right slot. */ var findMostSensibleMatch = (matchers, key) => { const deviations = matchers.map((matcher, i) => matcher.test(key) ? i : void 0).filter(isNotNullish).map((matcherIndice) => { const scenario = [...matchers]; const originalMatcher = scenario[matcherIndice]; const matcherSpecificity = measureRegExpSpecificity(originalMatcher.source, key); scenario[matcherIndice] = new RegExp(key); scenario.sort(regexpSorterOne); const scenarioSortIndex = scenario.findIndex((v) => v.source === key); return { deviation: Math.abs(scenarioSortIndex - matcherIndice), originalMatcher, matcherIndice, matcherSpecificity }; }); deviations.sort((a, b) => a.deviation === b.deviation ? a.matcherSpecificity === b.matcherSpecificity ? a.matcherIndice === b.matcherIndice ? a.originalMatcher.source.localeCompare(b.originalMatcher.source) : b.matcherIndice - a.matcherIndice : b.matcherSpecificity - a.matcherSpecificity : a.deviation - b.deviation); return deviations[0]?.matcherIndice ?? -1; }; var findMostSensibleMatchIdeaTwo = (matchers, key) => { [...matchers.map((matcher) => matcher.test(key) ? new RegExp(key) : matcher)].sort(regexpSorterOne); return -1; }; /** * Creates a copy of an object with its keys ordered according to a * preferred ordering */ var sortObject = (o, sortPreferences = []) => { const plainLevelOrder = sortPreferences.map((pref) => typeof pref === "object" ? pref.key : pref); const regexpLevelOrder = plainLevelOrder.map((pref) => new RegExp(pref)); const isArray = Array.isArray(o); let obj = o; if (isArray) obj = arrayToObject(o); const ordered = Object.entries(obj).map(([key, value]) => { let order = Number.POSITIVE_INFINITY; const plainIndex = plainLevelOrder.indexOf(key); if (plainIndex >= 0) order = plainIndex; else { const regexpIndices = regexpLevelOrder.map((orderingRegExp, i) => orderingRegExp.test(key) ? i : -1).filter((index) => index > -1); if (regexpIndices.length > 1) order = findMostSensibleMatch(regexpLevelOrder, key); else if (regexpIndices[0]) order = regexpIndices[0]; } if (value !== void 0 && value !== null && typeof value === "object") return [ key, sortObject(value, sortPreferences.filter((pref) => typeof pref === "object").find((preference) => new RegExp(preference.key).test(key))?.order), order ]; else return [ key, value, order ]; }).sort(([ak, _av, aOrder], [bk, _bv, bOrder]) => { return aOrder >= 0 && bOrder >= 0 && aOrder !== bOrder ? aOrder - bOrder : ak.localeCompare(bk); }); return isArray ? ordered.map((item) => item[1]) : Object.fromEntries(ordered); }; //#endregion //#region src/string/capitalize.function.ts /** * Turns the first letter into upper case and the rest lowercase */ var capitalize = (s) => { return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); }; //#endregion //#region src/string/case-conversion.ts /** * Splits a string apart into it's individial words. It treats ' ', '_' and '-' * as word boundaries as well as when a lower case character is followed by an * upper case character. * * An empty string will result in an empty array. */ var splitByCasing = (input) => { return input ? input.split(/(?=[A-Z][a-z])|[\s_-]+/).map((word) => word.toLowerCase()) : []; }; /** * Turns an input string to camelCase from any input casing */ function camelCase(input) { const [first, ...rest] = splitByCasing(input); return (first ?? "") + rest.map(capitalize).join(""); } /** * Turns an input string to kebab-case from any input casing */ function kebabCase(input) { return splitByCasing(input).join("-"); } /** * Turns an input string to snake_case from any input casing */ function snakeCase(input) { return splitByCasing(input).join("_"); } /** * Turns an input string to UPPER_SNAKE_CASE from any input casing */ function upperSnakeCase(input) { return splitByCasing(input).join("_").toUpperCase(); } /** * Turns an input string to PascalCase from any input casing */ function pascalCase(input) { return splitByCasing(input).map(capitalize).join(""); } /** * Turns an input string to Train-Case from any input casing */ function trainCase(input) { return splitByCasing(input).map(capitalize).join("-"); } //#endregion exports.DEFAULT_ES_TARGET_NAME = DEFAULT_ES_TARGET_NAME; exports.DEFAULT_ES_TARGET_YEAR = DEFAULT_ES_TARGET_YEAR; exports.asyncFilter = asyncFilter; exports.asyncFilterMap = asyncFilterMap; exports.asyncMap = asyncMap; exports.bufferedAllSettled = bufferedAllSettled; exports.camelCase = camelCase; exports.capitalize = capitalize; exports.closestNumber = closestNumber; exports.deepFreeze = deepFreeze; exports.deepMapObject = deepMapObject; exports.deepMerge = deepMerge; exports.defaultDropKeyMatcher = defaultDropKeyMatcher; exports.dropKeys = dropKeys; exports.dry = dry; exports.drySync = drySync; exports.fillObjectWithTemplateVariables = fillObjectWithTemplateVariables; exports.fillStringWithTemplateVariables = fillStringWithTemplateVariables; exports.findMostSensibleMatch = findMostSensibleMatch; exports.findMostSensibleMatchIdeaTwo = findMostSensibleMatchIdeaTwo; exports.groupBy = groupBy; exports.identity = identity; exports.identityAsync = identityAsync; exports.isNotNullish = isNotNullish; exports.isNullish = isNullish; exports.isObject = isObject; exports.isPromiseFulfilled = isPromiseFulfilled; exports.isPromiseLike = isPromiseLike; exports.kebabCase = kebabCase; exports.mapObject = mapObject; exports.measureRegExpSpecificity = measureRegExpSpecificity; exports.memoize = memoize; exports.noop = noop; exports.noopAsync = noopAsync; exports.normalizeDryOption = normalizeDryOption; exports.normalizeForceOption = normalizeForceOption; exports.normalizeMemoizeOptions = normalizeMemoizeOptions; exports.normalizeRegExpLikeToRegExp = normalizeRegExpLikeToRegExp; exports.normalizeSafeOption = normalizeSafeOption; exports.pascalCase = pascalCase; exports.random = random; exports.shuffleArray = shuffleArray; exports.sleep = sleep; exports.snakeCase = snakeCase; exports.sortObject = sortObject; exports.splitByCasing = splitByCasing; exports.trainCase = trainCase; exports.upperSnakeCase = upperSnakeCase; exports.yes = yes; exports.yesAsync = yesAsync; //# sourceMappingURL=index.cjs.map