misc.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { getUnsafeWindow } from "./dom";
  2. /** Represents any value that is either a string itself or can be converted to one (implicitly and explicitly) because it has a toString() method */
  3. export type Stringifiable = string | { toString(): string };
  4. /**
  5. * A type that offers autocomplete for the passed union but also allows any arbitrary value of the same type to be passed.
  6. * Supports unions of strings, numbers and objects.
  7. */
  8. export type LooseUnion<TUnion extends string | number | object> =
  9. (TUnion) | (
  10. TUnion extends string
  11. ? (string & {})
  12. : (
  13. TUnion extends number
  14. ? (number & {})
  15. : (
  16. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  17. TUnion extends Record<keyof any, unknown>
  18. ? (object & {})
  19. : never
  20. )
  21. )
  22. );
  23. /**
  24. * A type that allows all strings except for empty ones
  25. * @example
  26. * function foo<T extends string>(bar: NonEmptyString<T>) {
  27. * console.log(bar);
  28. * }
  29. */
  30. export type NonEmptyString<TString extends string> = TString extends "" ? never : TString;
  31. /**
  32. * Automatically appends an `s` to the passed {@linkcode word}, if {@linkcode num} is not equal to 1
  33. * @param word A word in singular form, to auto-convert to plural
  34. * @param num If this is an array or NodeList, the amount of items is used
  35. */
  36. export function autoPlural(word: Stringifiable, num: number | unknown[] | NodeList) {
  37. if(Array.isArray(num) || num instanceof NodeList)
  38. num = num.length;
  39. return `${word}${num === 1 ? "" : "s"}`;
  40. }
  41. /** Pauses async execution for the specified time in ms */
  42. export function pauseFor(time: number) {
  43. return new Promise<void>((res) => {
  44. setTimeout(() => res(), time);
  45. });
  46. }
  47. /**
  48. * Calls the passed {@linkcode func} after the specified {@linkcode timeout} in ms (defaults to 300).
  49. * Any subsequent calls to this function will reset the timer and discard all previous calls.
  50. * @param func The function to call after the timeout
  51. * @param timeout The time in ms to wait before calling the function
  52. * @param edge Whether to call the function at the very first call ("rising" edge) or the very last call ("falling" edge, default)
  53. */
  54. export function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(func: TFunc, timeout = 300, edge: "rising" | "falling" = "falling") { // eslint-disable-line @typescript-eslint/no-explicit-any
  55. let timer: NodeJS.Timeout | undefined;
  56. return function(...args: TArgs[]) {
  57. if(edge === "rising") {
  58. if(!timer) {
  59. func.apply(this, args);
  60. timer = setTimeout(() => timer = undefined, timeout);
  61. }
  62. }
  63. else {
  64. clearTimeout(timer);
  65. timer = setTimeout(() => func.apply(this, args), timeout);
  66. }
  67. };
  68. }
  69. /** Options for the `fetchAdvanced()` function */
  70. export type FetchAdvancedOpts = Omit<
  71. RequestInit & Partial<{
  72. /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
  73. timeout: number;
  74. }>,
  75. "signal"
  76. >;
  77. /** Calls the fetch API with special options like a timeout */
  78. export async function fetchAdvanced(input: RequestInfo | URL, options: FetchAdvancedOpts = {}) {
  79. const { timeout = 10000 } = options;
  80. let signalOpts: Partial<RequestInit> = {},
  81. id: NodeJS.Timeout | undefined = undefined;
  82. if(timeout >= 0) {
  83. const controller = new AbortController();
  84. id = setTimeout(() => controller.abort(), timeout);
  85. signalOpts = { signal: controller.signal };
  86. }
  87. const res = await fetch(input, {
  88. ...options,
  89. ...signalOpts,
  90. });
  91. clearTimeout(id);
  92. return res;
  93. }
  94. /**
  95. * Inserts the passed values into a string at the respective placeholders.
  96. * The placeholder format is `%n`, where `n` is the 1-indexed argument number.
  97. * @param input The string to insert the values into
  98. * @param values The values to insert, in order, starting at `%1`
  99. */
  100. export function insertValues(input: string, ...values: Stringifiable[]) {
  101. return input.replace(/%\d/gm, (match) => {
  102. const argIndex = Number(match.substring(1)) - 1;
  103. return (values[argIndex] ?? match)?.toString();
  104. });
  105. }
  106. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string */
  107. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
  108. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as an ArrayBuffer */
  109. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  110. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string or ArrayBuffer, depending on what {@linkcode outputType} is set to */
  111. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "string" | "arrayBuffer" = "string"): Promise<ArrayBuffer | string> {
  112. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  113. const comp = new CompressionStream(compressionFormat);
  114. const writer = comp.writable.getWriter();
  115. writer.write(byteArray);
  116. writer.close();
  117. const buf = await (new Response(comp.readable).arrayBuffer());
  118. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  119. }
  120. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a string */
  121. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
  122. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to an ArrayBuffer */
  123. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  124. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a string or ArrayBuffer, depending on what {@linkcode outputType} is set to */
  125. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "string" | "arrayBuffer" = "string"): Promise<ArrayBuffer | string> {
  126. const byteArray = typeof input === "string" ? str2ab(input) : input;
  127. const decomp = new DecompressionStream(compressionFormat);
  128. const writer = decomp.writable.getWriter();
  129. writer.write(byteArray);
  130. writer.close();
  131. const buf = await (new Response(decomp.readable).arrayBuffer());
  132. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  133. }
  134. /** Converts an ArrayBuffer to a base64-encoded string */
  135. function ab2str(buf: ArrayBuffer) {
  136. return getUnsafeWindow().btoa(
  137. new Uint8Array(buf)
  138. .reduce((data, byte) => data + String.fromCharCode(byte), "")
  139. );
  140. }
  141. /** Converts a base64-encoded string to an ArrayBuffer representation of its bytes */
  142. function str2ab(str: string) {
  143. return Uint8Array.from(getUnsafeWindow().atob(str), c => c.charCodeAt(0));
  144. }