misc.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. /**
  2. * @module lib/misc
  3. * This module contains miscellaneous functions that don't fit in another category - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#misc)
  4. */
  5. import type { ListWithLength, Prettify, Stringifiable } from "./types.js";
  6. /** Which plural form to use when auto-pluralizing */
  7. export type PluralType = "auto" | "-s" | "-ies";
  8. /**
  9. * Automatically pluralizes the given string by adding an `-s` or `-ies` to the passed {@linkcode term}, if {@linkcode num} is not equal to 1.
  10. * By default, words ending in `-y` will have it replaced with `-ies`, and all other words will simply have `-s` appended.
  11. * {@linkcode pluralType} will default to `auto` if invalid and {@linkcode num} is set to 2 if it resolves to `NaN`.
  12. * @param term The term, written in singular form, to auto-convert to plural form
  13. * @param num A number, or list-like value that has either a `length`, `count` or `size` property, like an array, Map or NodeList - does not support iterables, they need to be converted to an array first
  14. * @param pluralType Which plural form to use when auto-pluralizing. Defaults to `"auto"`, which removes the last char and uses `-ies` for words ending in `y` and simply appends `-s` for all other words
  15. */
  16. export function autoPlural(term: Stringifiable, num: number | ListWithLength, pluralType: PluralType = "auto"): string {
  17. let n = num;
  18. if(typeof n !== "number")
  19. n = getListLength(n, false);
  20. if(!["-s", "-ies"].includes(pluralType))
  21. pluralType = "auto";
  22. if(isNaN(n))
  23. n = 2;
  24. const pType: Exclude<PluralType, "auto"> = pluralType === "auto"
  25. ? String(term).endsWith("y") ? "-ies" : "-s"
  26. : pluralType;
  27. switch(pType) {
  28. case "-s":
  29. return `${term}${n === 1 ? "" : "s"}`;
  30. case "-ies":
  31. return `${String(term).slice(0, -1)}${n === 1 ? "y" : "ies"}`;
  32. default:
  33. return String(term);
  34. }
  35. }
  36. /**
  37. * Inserts the passed values into a string at the respective placeholders.
  38. * The placeholder format is `%n`, where `n` is the 1-indexed argument number.
  39. * @param input The string to insert the values into
  40. * @param values The values to insert, in order, starting at `%1`
  41. */
  42. export function insertValues(input: string, ...values: Stringifiable[]): string {
  43. return input.replace(/%\d/gm, (match) => {
  44. const argIndex = Number(match.substring(1)) - 1;
  45. return (values[argIndex] ?? match)?.toString();
  46. });
  47. }
  48. /**
  49. * Pauses async execution for the specified time in ms.
  50. * If an `AbortSignal` is passed, the pause will be aborted when the signal is triggered.
  51. * By default, this will resolve the promise, but you can set {@linkcode rejectOnAbort} to true to reject it instead.
  52. */
  53. export function pauseFor(time: number, signal?: AbortSignal, rejectOnAbort = false): Promise<void> {
  54. return new Promise<void>((res, rej) => {
  55. const timeout = setTimeout(() => res(), time);
  56. signal?.addEventListener("abort", () => {
  57. clearTimeout(timeout);
  58. rejectOnAbort ? rej(new Error("The pause was aborted")) : res();
  59. });
  60. });
  61. }
  62. /** Options for the `fetchAdvanced()` function */
  63. export type FetchAdvancedOpts = Prettify<
  64. Partial<{
  65. /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
  66. timeout: number;
  67. }> & RequestInit
  68. >;
  69. /** Calls the fetch API with special options like a timeout */
  70. export async function fetchAdvanced(input: string | RequestInfo | URL, options: FetchAdvancedOpts = {}): Promise<Response> {
  71. const { timeout = 10000 } = options;
  72. const ctl = new AbortController();
  73. const { signal, ...restOpts } = options;
  74. signal?.addEventListener("abort", () => ctl.abort());
  75. let sigOpts: Partial<RequestInit> = {},
  76. id: ReturnType<typeof setTimeout> | undefined = undefined;
  77. if(timeout >= 0) {
  78. id = setTimeout(() => ctl.abort(), timeout);
  79. sigOpts = { signal: ctl.signal };
  80. }
  81. try {
  82. const res = await fetch(input, {
  83. ...restOpts,
  84. ...sigOpts,
  85. });
  86. typeof id !== "undefined" && clearTimeout(id);
  87. return res;
  88. }
  89. catch(err) {
  90. typeof id !== "undefined" && clearTimeout(id);
  91. throw new Error("Error while calling fetch", { cause: err });
  92. }
  93. }
  94. /**
  95. * A ValueGen value is either its type, a promise that resolves to its type, or a function that returns its type, either synchronous or asynchronous.
  96. * ValueGen allows for the utmost flexibility when applied to any type, as long as {@linkcode consumeGen()} is used to get the final value.
  97. * @template TValueType The type of the value that the ValueGen should yield
  98. */
  99. export type ValueGen<TValueType> = TValueType | Promise<TValueType> | (() => TValueType | Promise<TValueType>);
  100. /**
  101. * Turns a {@linkcode ValueGen} into its final value.
  102. * @template TValueType The type of the value that the ValueGen should yield
  103. */
  104. export async function consumeGen<TValueType>(valGen: ValueGen<TValueType>): Promise<TValueType> {
  105. return await (typeof valGen === "function"
  106. ? (valGen as (() => Promise<TValueType> | TValueType))()
  107. : valGen
  108. ) as TValueType;
  109. }
  110. /**
  111. * A StringGen value is either a string, anything that can be converted to a string, or a function that returns one of the previous two, either synchronous or asynchronous, or a promise that returns a string.
  112. * StringGen allows for the utmost flexibility when dealing with strings, as long as {@linkcode consumeStringGen()} is used to get the final string.
  113. */
  114. export type StringGen = ValueGen<Stringifiable>;
  115. /**
  116. * Turns a {@linkcode StringGen} into its final string value.
  117. * @template TStrUnion The union of strings that the StringGen should yield - this allows for finer type control compared to {@linkcode consumeGen()}
  118. */
  119. export async function consumeStringGen<TStrUnion extends string>(strGen: StringGen): Promise<TStrUnion> {
  120. return (
  121. typeof strGen === "string"
  122. ? strGen
  123. : String(
  124. typeof strGen === "function"
  125. ? await strGen()
  126. : strGen
  127. )
  128. ) as TStrUnion;
  129. }
  130. /**
  131. * Returns the length of the given list-like object (anything with a numeric `length`, `size` or `count` property, like an array, Map or NodeList).
  132. * If the object doesn't have any of these properties, it will return 0 by default.
  133. * Set {@linkcode zeroOnInvalid} to false to return NaN instead of 0 if the object doesn't have any of the properties.
  134. */
  135. export function getListLength(obj: ListWithLength, zeroOnInvalid = true): number {
  136. // will I go to ternary hell for this?
  137. return "length" in obj
  138. ? obj.length
  139. : "size" in obj
  140. ? obj.size
  141. : "count" in obj
  142. ? obj.count
  143. : zeroOnInvalid
  144. ? 0
  145. : NaN;
  146. }
  147. /**
  148. * Turns the passed object into a "pure" object without a prototype chain, meaning it won't have any default properties like `toString`, `__proto__`, `__defineGetter__`, etc.
  149. * This makes the object immune to prototype pollution attacks and allows for cleaner object literals, at the cost of being harder to work with in some cases.
  150. * It also effectively transforms a `Stringifiable` value into one that will throw a TypeError when stringified instead of defaulting to `[object Object]`
  151. */
  152. export function purifyObj<TObj extends object>(obj: TObj): TObj {
  153. return Object.assign(Object.create(null), obj);
  154. }