misc.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { getUnsafeWindow } from "./dom";
  2. /** Represents any value that is either a string itself or can be converted to one (implicitly or 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. */
  51. export function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(func: TFunc, timeout = 300) { // eslint-disable-line @typescript-eslint/no-explicit-any
  52. let timer: number | undefined;
  53. return function(...args: TArgs[]) {
  54. clearTimeout(timer);
  55. timer = setTimeout(() => func.apply(this, args), timeout) as unknown as number;
  56. };
  57. }
  58. /** Options for the `fetchAdvanced()` function */
  59. export type FetchAdvancedOpts = Omit<
  60. RequestInit & Partial<{
  61. /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
  62. timeout: number;
  63. }>,
  64. "signal"
  65. >;
  66. /** Calls the fetch API with special options like a timeout */
  67. export async function fetchAdvanced(input: RequestInfo | URL, options: FetchAdvancedOpts = {}) {
  68. const { timeout = 10000 } = options;
  69. let signalOpts: Partial<RequestInit> = {},
  70. id: NodeJS.Timeout | undefined = undefined;
  71. if(timeout >= 0) {
  72. const controller = new AbortController();
  73. id = setTimeout(() => controller.abort(), timeout);
  74. signalOpts = { signal: controller.signal };
  75. }
  76. const res = await fetch(input, {
  77. ...options,
  78. ...signalOpts,
  79. });
  80. clearTimeout(id);
  81. return res;
  82. }
  83. /**
  84. * Inserts the passed values into a string at the respective placeholders.
  85. * The placeholder format is `%n`, where `n` is the 1-indexed argument number.
  86. * @param input The string to insert the values into
  87. * @param values The values to insert, in order, starting at `%1`
  88. */
  89. export function insertValues(input: string, ...values: Stringifiable[]) {
  90. return input.replace(/%\d/gm, (match) => {
  91. const argIndex = Number(match.substring(1)) - 1;
  92. return (values[argIndex] ?? match)?.toString();
  93. });
  94. }
  95. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string */
  96. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "base64"): Promise<string>
  97. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as an ArrayBuffer */
  98. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  99. /** 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 */
  100. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "base64" | "arrayBuffer" = "base64"): Promise<ArrayBuffer | string> {
  101. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  102. const comp = new CompressionStream(compressionFormat);
  103. const writer = comp.writable.getWriter();
  104. writer.write(byteArray);
  105. writer.close();
  106. const buf = await (new Response(comp.readable).arrayBuffer());
  107. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  108. }
  109. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a base64 string */
  110. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "base64"): Promise<string>
  111. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to an ArrayBuffer */
  112. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  113. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a base64 string or ArrayBuffer, depending on what {@linkcode outputType} is set to */
  114. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "base64" | "arrayBuffer" = "base64"): Promise<ArrayBuffer | string> {
  115. const byteArray = typeof input === "string" ? str2ab(input) : input;
  116. const decomp = new DecompressionStream(compressionFormat);
  117. const writer = decomp.writable.getWriter();
  118. writer.write(byteArray);
  119. writer.close();
  120. const buf = await (new Response(decomp.readable).arrayBuffer());
  121. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  122. }
  123. /** Converts an ArrayBuffer to a base64-encoded string */
  124. function ab2str(buf: ArrayBuffer) {
  125. return getUnsafeWindow().btoa(
  126. new Uint8Array(buf)
  127. .reduce((data, byte) => data + String.fromCharCode(byte), "")
  128. );
  129. }
  130. /** Converts a base64-encoded string to an ArrayBuffer representation of its bytes */
  131. function str2ab(str: string) {
  132. return Uint8Array.from(getUnsafeWindow().atob(str), c => c.charCodeAt(0));
  133. }