misc.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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 = RequestInit & Partial<{
  60. /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
  61. timeout: number;
  62. }>;
  63. /** Calls the fetch API with special options like a timeout */
  64. export async function fetchAdvanced(url: string, options: FetchAdvancedOpts = {}) {
  65. const { timeout = 10000 } = options;
  66. const controller = new AbortController();
  67. const id = setTimeout(() => controller.abort(), timeout);
  68. const res = await fetch(url, {
  69. ...options,
  70. signal: controller.signal,
  71. });
  72. clearTimeout(id);
  73. return res;
  74. }
  75. /**
  76. * Inserts the passed values into a string at the respective placeholders.
  77. * The placeholder format is `%n`, where `n` is the 1-indexed argument number.
  78. * @param input The string to insert the values into
  79. * @param values The values to insert, in order, starting at `%1`
  80. */
  81. export function insertValues(input: string, ...values: Stringifiable[]) {
  82. return input.replace(/%\d/gm, (match) => {
  83. const argIndex = Number(match.substring(1)) - 1;
  84. return (values[argIndex] ?? match)?.toString();
  85. });
  86. }
  87. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string */
  88. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "base64"): Promise<string>
  89. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as an ArrayBuffer */
  90. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  91. /** 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 */
  92. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "base64" | "arrayBuffer" = "base64"): Promise<ArrayBuffer | string> {
  93. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  94. const comp = new CompressionStream(compressionFormat);
  95. const writer = comp.writable.getWriter();
  96. writer.write(byteArray);
  97. writer.close();
  98. const buf = await (new Response(comp.readable).arrayBuffer());
  99. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  100. }
  101. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a string */
  102. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
  103. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to an ArrayBuffer */
  104. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  105. /** 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 */
  106. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "string" | "arrayBuffer" = "string"): Promise<ArrayBuffer | string> {
  107. const byteArray = typeof input === "string" ? str2ab(input) : input;
  108. const decomp = new DecompressionStream(compressionFormat);
  109. const writer = decomp.writable.getWriter();
  110. writer.write(byteArray);
  111. writer.close();
  112. const buf = await (new Response(decomp.readable).arrayBuffer());
  113. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  114. }
  115. /** Converts an ArrayBuffer to a base64-encoded string */
  116. function ab2str(buf: ArrayBuffer) {
  117. return getUnsafeWindow().btoa(
  118. new Uint8Array(buf)
  119. .reduce((data, byte) => data + String.fromCharCode(byte), "")
  120. );
  121. }
  122. /** Converts a base64-encoded string to an ArrayBuffer representation of its bytes */
  123. function str2ab(str: string) {
  124. return Uint8Array.from(getUnsafeWindow().atob(str), c => c.charCodeAt(0));
  125. }