@alexaegis/common
Version:
Common utility functions
576 lines (575 loc) • 20.1 kB
JavaScript
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