@alexaegis/common
Version:
Common utility functions
431 lines (430 loc) • 13.5 kB
JavaScript
const isPromiseFulfilled = (promiseResult) => {
return promiseResult.status === "fulfilled";
};
const asyncFilterMap = async (array, map) => {
const checks = await Promise.allSettled(array.map(map));
return checks.filter(isPromiseFulfilled).map((item) => item.value).filter(isNotNullish);
};
const filterMark = {};
const asyncFilter = async (array, predicate) => {
const checks = await Promise.allSettled(
array.map((item, i) => {
return predicate(item, i).then((result) => result ? item : filterMark);
})
);
return checks.filter(isPromiseFulfilled).map((item) => item.value).filter((result) => result !== filterMark);
};
const asyncMap = async (array, map) => {
return Promise.all(array.map(map));
};
const 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;
};
const DEFAULT_ES_TARGET_YEAR = 2022;
const DEFAULT_ES_TARGET_NAME = `es${DEFAULT_ES_TARGET_YEAR.toString()}`;
const 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;
}, {});
};
const identity = (r) => r;
const identityAsync = async (value, mode = "micro") => new Promise((resolve) => {
if (mode === "micro") {
resolve(value);
} else {
setTimeout(() => {
resolve(value);
}, 0);
}
});
const normalizeMemoizeOptions = (options) => {
return {
argHasher: options?.argHasher ?? JSON.stringify,
thisContext: options?.thisContext,
maxCacheEntries: options?.maxCacheEntries ?? Number.POSITIVE_INFINITY,
cache: options?.cache ?? /* @__PURE__ */ new Map()
};
};
const 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;
}
};
};
const noop = () => void 0;
const noopAsync = (mode = "micro") => new Promise((resolve) => {
if (mode === "micro") {
resolve(void 0);
} else {
setTimeout(() => {
resolve(void 0);
}, 0);
}
});
const normalizeRegExpLikeToRegExp = (regExpLike) => {
return typeof regExpLike === "string" ? new RegExp(regExpLike) : regExpLike;
};
const sleep = (ms) => {
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
};
const yes = () => true;
const yesAsync = (mode = "micro") => new Promise((resolve) => {
if (mode === "micro") {
resolve(true);
} else {
setTimeout(() => {
resolve(true);
}, 0);
}
});
const closestNumber = (numbers, target) => numbers.reduce(
(prev, curr) => Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev
);
function random(min, max) {
if (min > max) {
[min, max] = [max, min];
}
return Math.floor(Math.random() * (max - min + 1) + min);
}
const 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);
};
function isObject(item) {
return item !== null && item !== void 0 && typeof item === "object" && !Array.isArray(item);
}
const 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;
};
const defaultDropKeyMatcher = (value, _key) => value === void 0;
const 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;
};
const 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) {
const matcher = typeof options.dropKeys === "function" ? options.dropKeys : defaultDropKeyMatcher;
dropKeys(merged, matcher);
}
return merged;
};
const deepMerge = (sources, options) => {
return deepMergeInternal(sources, options);
};
const fillStringWithTemplateVariables = (value, variables) => {
return Object.entries(variables).reduce((acc, [variableKey, variableValue]) => {
return acc.replaceAll("${" + variableKey + "}", variableValue);
}, value);
};
const fillObjectWithTemplateVariables = (target, variables) => {
return deepMapObject(target, (_key, value) => {
return typeof value === "string" ? fillStringWithTemplateVariables(value, variables) : value;
});
};
const isNotNullish = (o) => o !== void 0 && o !== null;
const isNullish = (o) => o === void 0 || o === null;
const isPromiseLike = (candidate) => {
return isNotNullish(candidate) && typeof candidate === "object" && typeof candidate["then"] === "function" && typeof candidate["catch"] === "function";
};
const mapObject = (o, map) => {
return Object.fromEntries(
Object.entries(o).map(([key, value]) => {
return [key, map(value, key)];
})
);
};
const drySync = (isDry, whenWet, dryDefault = true) => {
return isDry ? () => identity(dryDefault) : whenWet;
};
const dry = (isDry, whenWet, dryDefault = true) => {
return isDry ? () => identityAsync(dryDefault) : whenWet;
};
const normalizeDryOption = (options) => {
return {
dry: options?.dry ?? false
};
};
const normalizeForceOption = (options) => {
return {
force: options?.force ?? false
};
};
const normalizeSafeOption = (options) => {
return {
safe: options?.safe ?? false
};
};
const measureRegExpSpecificity = (r, v) => {
return [...v].filter((c) => r.includes(c)).length;
};
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;
}
const arrayToObject = (a) => {
return a.reduce((acc, next) => {
acc[next] = next;
return acc;
}, {});
};
const regexpSorterOne = (a, b) => a.test(b.source) || b.test(a.source) ? -1 : a.source.localeCompare(b.source);
const findMostSensibleMatch = (matchers, key) => {
const matcherIndices = matchers.map((matcher, i) => matcher.test(key) ? i : void 0).filter(isNotNullish);
const deviations = matcherIndices.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);
const deviation = Math.abs(scenarioSortIndex - matcherIndice);
return {
deviation,
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
// The smaller the better
);
return deviations[0]?.matcherIndice ?? -1;
};
const findMostSensibleMatchIdeaTwo = (matchers, key) => {
const matchingIndices = matchers.map(
(matcher) => matcher.test(key) ? new RegExp(key) : matcher
);
const sortedMatchers = [...matchingIndices];
sortedMatchers.sort(regexpSorterOne);
return -1;
};
const 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") {
const subOrdering = sortPreferences.filter((pref) => typeof pref === "object").find((preference) => new RegExp(preference.key).test(key));
return [key, sortObject(value, subOrdering?.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);
};
const capitalize = (s) => {
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
};
const splitByCasing = (input) => {
return input ? input.split(/(?=[A-Z][a-z])|[\s_-]+/).map((word) => word.toLowerCase()) : [];
};
function camelCase(input) {
const [first, ...rest] = splitByCasing(input);
return (first ?? "") + rest.map(capitalize).join("");
}
function kebabCase(input) {
return splitByCasing(input).join("-");
}
function snakeCase(input) {
return splitByCasing(input).join("_");
}
function upperSnakeCase(input) {
return splitByCasing(input).join("_").toUpperCase();
}
function pascalCase(input) {
return splitByCasing(input).map(capitalize).join("");
}
function trainCase(input) {
return splitByCasing(input).map(capitalize).join("-");
}
export {
DEFAULT_ES_TARGET_NAME,
DEFAULT_ES_TARGET_YEAR,
asyncFilter,
asyncFilterMap,
asyncMap,
bufferedAllSettled,
camelCase,
capitalize,
closestNumber,
deepFreeze,
deepMapObject,
deepMerge,
defaultDropKeyMatcher,
dropKeys,
dry,
drySync,
fillObjectWithTemplateVariables,
fillStringWithTemplateVariables,
findMostSensibleMatch,
findMostSensibleMatchIdeaTwo,
groupBy,
identity,
identityAsync,
isNotNullish,
isNullish,
isObject,
isPromiseFulfilled,
isPromiseLike,
kebabCase,
mapObject,
measureRegExpSpecificity,
memoize,
noop,
noopAsync,
normalizeDryOption,
normalizeForceOption,
normalizeMemoizeOptions,
normalizeRegExpLikeToRegExp,
normalizeSafeOption,
pascalCase,
random,
shuffleArray,
sleep,
snakeCase,
sortObject,
splitByCasing,
trainCase,
upperSnakeCase,
yes,
yesAsync
};