misc.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { getUnsafeWindow } from "./dom.js";
  2. import type { Stringifiable } from "./types.js";
  3. /**
  4. * Automatically appends an `s` to the passed {@linkcode word}, if {@linkcode num} is not equal to 1
  5. * @param word A word in singular form, to auto-convert to plural
  6. * @param num If this is an array or NodeList, the amount of items is used
  7. */
  8. export function autoPlural(word: Stringifiable, num: number | unknown[] | NodeList): string {
  9. if(Array.isArray(num) || num instanceof NodeList)
  10. num = num.length;
  11. return `${word}${num === 1 ? "" : "s"}`;
  12. }
  13. /** Pauses async execution for the specified time in ms */
  14. export function pauseFor(time: number): Promise<void> {
  15. return new Promise<void>((res) => {
  16. setTimeout(() => res(), time);
  17. });
  18. }
  19. /**
  20. * Calls the passed {@linkcode func} after the specified {@linkcode timeout} in ms (defaults to 300).
  21. * Any subsequent calls to this function will reset the timer and discard all previous calls.
  22. * @param func The function to call after the timeout
  23. * @param timeout The time in ms to wait before calling the function
  24. * @param edge Whether to call the function at the very first call ("rising" edge) or the very last call ("falling" edge, default)
  25. */
  26. export function debounce<
  27. TFunc extends (...args: TArgs[]) => void, // eslint-disable-next-line @typescript-eslint/no-explicit-any
  28. TArgs = any,
  29. > (
  30. func: TFunc,
  31. timeout = 300,
  32. edge: "rising" | "falling" = "falling"
  33. ): (...args: TArgs[]) => void {
  34. let timer: NodeJS.Timeout | undefined;
  35. return function(...args: TArgs[]) {
  36. if(edge === "rising") {
  37. if(!timer) {
  38. func.apply(this, args);
  39. timer = setTimeout(() => timer = undefined, timeout);
  40. }
  41. }
  42. else {
  43. clearTimeout(timer);
  44. timer = setTimeout(() => func.apply(this, args), timeout);
  45. }
  46. };
  47. }
  48. /** Options for the `fetchAdvanced()` function */
  49. export type FetchAdvancedOpts = Omit<
  50. RequestInit & Partial<{
  51. /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
  52. timeout: number;
  53. }>,
  54. "signal"
  55. >;
  56. /** Calls the fetch API with special options like a timeout */
  57. export async function fetchAdvanced(input: RequestInfo | URL, options: FetchAdvancedOpts = {}): Promise<Response> {
  58. const { timeout = 10000 } = options;
  59. let signalOpts: Partial<RequestInit> = {},
  60. id: NodeJS.Timeout | undefined = undefined;
  61. if(timeout >= 0) {
  62. const controller = new AbortController();
  63. id = setTimeout(() => controller.abort(), timeout);
  64. signalOpts = { signal: controller.signal };
  65. }
  66. const res = await fetch(input, {
  67. ...options,
  68. ...signalOpts,
  69. });
  70. clearTimeout(id);
  71. return res;
  72. }
  73. /**
  74. * Inserts the passed values into a string at the respective placeholders.
  75. * The placeholder format is `%n`, where `n` is the 1-indexed argument number.
  76. * @param input The string to insert the values into
  77. * @param values The values to insert, in order, starting at `%1`
  78. */
  79. export function insertValues(input: string, ...values: Stringifiable[]): string {
  80. return input.replace(/%\d/gm, (match) => {
  81. const argIndex = Number(match.substring(1)) - 1;
  82. return (values[argIndex] ?? match)?.toString();
  83. });
  84. }
  85. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string */
  86. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
  87. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as an ArrayBuffer */
  88. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  89. /** 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 */
  90. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "string" | "arrayBuffer" = "string"): Promise<ArrayBuffer | string> {
  91. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  92. const comp = new CompressionStream(compressionFormat);
  93. const writer = comp.writable.getWriter();
  94. writer.write(byteArray);
  95. writer.close();
  96. const buf = await (new Response(comp.readable).arrayBuffer());
  97. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  98. }
  99. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a string */
  100. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
  101. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to an ArrayBuffer */
  102. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  103. /** 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 */
  104. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "string" | "arrayBuffer" = "string"): Promise<ArrayBuffer | string> {
  105. const byteArray = typeof input === "string" ? str2ab(input) : input;
  106. const decomp = new DecompressionStream(compressionFormat);
  107. const writer = decomp.writable.getWriter();
  108. writer.write(byteArray);
  109. writer.close();
  110. const buf = await (new Response(decomp.readable).arrayBuffer());
  111. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  112. }
  113. /** Converts an ArrayBuffer to a base64-encoded string */
  114. function ab2str(buf: ArrayBuffer) {
  115. return getUnsafeWindow().btoa(
  116. new Uint8Array(buf)
  117. .reduce((data, byte) => data + String.fromCharCode(byte), "")
  118. );
  119. }
  120. /** Converts a base64-encoded string to an ArrayBuffer representation of its bytes */
  121. function str2ab(str: string) {
  122. return Uint8Array.from(getUnsafeWindow().atob(str), c => c.charCodeAt(0));
  123. }
  124. /**
  125. * Creates a hash / checksum of the given {@linkcode input} string or ArrayBuffer using the specified {@linkcode algorithm} ("SHA-256" by default).
  126. *
  127. * ⚠️ Uses the SubtleCrypto API so it needs to run in a secure context (HTTPS).
  128. * ⚠️ If you use this for cryptography, make sure to use a secure algorithm (under no circumstances use SHA-1) and to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) your input data.
  129. */
  130. export async function computeHash(input: string | ArrayBuffer, algorithm = "SHA-256") {
  131. let data: ArrayBuffer;
  132. if(typeof input === "string") {
  133. const encoder = new TextEncoder();
  134. data = encoder.encode(input);
  135. }
  136. else
  137. data = input;
  138. const hashBuffer = await crypto.subtle.digest(algorithm, data);
  139. const hashArray = Array.from(new Uint8Array(hashBuffer));
  140. const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
  141. return hashHex;
  142. }