]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/blobdiff - src/utils/Utils.ts
chore(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / utils / Utils.ts
index fe059e0688cd5672f8e25fd977113aa394b7bb11..c8a8b14d3fe54e358b695c3159f933bcab9ecb9f 100644 (file)
@@ -1,5 +1,4 @@
-import { getRandomValues, randomBytes, randomUUID } from 'node:crypto'
-import { env, nextTick } from 'node:process'
+import type { CircularBuffer } from 'mnemonist'
 
 import {
   formatDuration,
@@ -10,22 +9,108 @@ import {
   millisecondsToMinutes,
   millisecondsToSeconds,
   minutesToSeconds,
-  secondsToMilliseconds
+  secondsToMilliseconds,
 } from 'date-fns'
-import type { CircularBuffer } from 'mnemonist'
-import { is } from 'rambda'
+import { getRandomValues, randomBytes, randomUUID } from 'node:crypto'
+import { env } from 'node:process'
 
 import {
   type JsonType,
   MapStringifyFormat,
   type TimestampedData,
-  WebSocketCloseEventStatusString
+  WebSocketCloseEventStatusString,
 } from '../types/index.js'
 
+type NonEmptyArray<T> = [T, ...T[]]
+type ReadonlyNonEmptyArray<T> = readonly [T, ...(readonly T[])]
+
 export const logPrefix = (prefixString = ''): string => {
   return `${new Date().toLocaleString()}${prefixString}`
 }
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const once = <T extends (...args: any[]) => any>(fn: T): T => {
+  let hasBeenCalled = false
+  let result: ReturnType<T>
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (this: any, ...args: Parameters<T>): ReturnType<T> {
+    if (!hasBeenCalled) {
+      hasBeenCalled = true
+      result = fn.apply(this, args) as ReturnType<T>
+    }
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return result
+  } as T
+}
+
+export const has = (property: PropertyKey, object: null | object | undefined): boolean => {
+  if (object == null) {
+    return false
+  }
+  return Object.hasOwn(object, property)
+}
+
+const type = (value: unknown): string => {
+  if (value === null) return 'Null'
+  if (value === undefined) return 'Undefined'
+  if (Number.isNaN(value)) return 'NaN'
+  if (Array.isArray(value)) return 'Array'
+  return Object.prototype.toString.call(value).slice(8, -1)
+}
+
+export const isEmpty = (value: unknown): boolean => {
+  const valueType = type(value)
+  if (['NaN', 'Null', 'Number', 'Undefined'].includes(valueType)) {
+    return false
+  }
+  if (!value) return true
+
+  if (valueType === 'Object') {
+    return Object.keys(value as Record<string, unknown>).length === 0
+  }
+
+  if (valueType === 'Array') {
+    return (value as unknown[]).length === 0
+  }
+
+  if (valueType === 'Map') {
+    return (value as Map<unknown, unknown>).size === 0
+  }
+
+  if (valueType === 'Set') {
+    return (value as Set<unknown>).size === 0
+  }
+
+  return false
+}
+
+const isObject = (value: unknown): value is object => {
+  return type(value) === 'Object'
+}
+
+export const mergeDeepRight = <T extends Record<string, unknown>>(
+  target: T,
+  source: Partial<T>
+): T => {
+  const output = { ...target }
+
+  if (isObject(target) && isObject(source)) {
+    Object.keys(source).forEach(key => {
+      if (isObject(source[key])) {
+        if (!(key in target)) {
+          Object.assign(output, { [key]: source[key] })
+        } else {
+          output[key] = mergeDeepRight(target[key], source[key])
+        }
+      } else {
+        Object.assign(output, { [key]: source[key] })
+      }
+    })
+  }
+
+  return output
+}
+
 export const generateUUID = (): `${string}-${string}-${string}-${string}-${string}` => {
   return randomUUID()
 }
@@ -65,7 +150,7 @@ export const formatDurationMilliSeconds = (duration: number): string => {
     days,
     hours,
     minutes,
-    seconds
+    seconds,
   })
 }
 
@@ -76,15 +161,15 @@ export const formatDurationSeconds = (duration: number): string => {
 // More efficient time validation function than the one provided by date-fns
 export const isValidDate = (date: Date | number | undefined): date is Date | number => {
   if (typeof date === 'number') {
-    return !isNaN(date)
+    return !Number.isNaN(date)
   } else if (isDate(date)) {
-    return !isNaN(date.getTime())
+    return !Number.isNaN(date.getTime())
   }
   return false
 }
 
 export const convertToDate = (
-  value: Date | string | number | undefined | null
+  value: Date | null | number | string | undefined
 ): Date | undefined => {
   if (value == null) {
     return undefined
@@ -94,8 +179,8 @@ export const convertToDate = (
   }
   if (typeof value === 'string' || typeof value === 'number') {
     const valueToDate = new Date(value)
-    if (isNaN(valueToDate.getTime())) {
-      throw new Error(`Cannot convert to date: '${value}'`)
+    if (Number.isNaN(valueToDate.getTime())) {
+      throw new Error(`Cannot convert to date: '${value.toString()}'`)
     }
     return valueToDate
   }
@@ -113,10 +198,11 @@ export const convertToInt = (value: unknown): number => {
   }
   let changedValue: number = value as number
   if (typeof value === 'string') {
-    changedValue = parseInt(value)
+    changedValue = Number.parseInt(value)
   }
-  if (isNaN(changedValue)) {
-    throw new Error(`Cannot convert to integer: '${String(value)}'`)
+  if (Number.isNaN(changedValue)) {
+    // eslint-disable-next-line @typescript-eslint/no-base-to-string
+    throw new Error(`Cannot convert to integer: '${value.toString()}'`)
   }
   return changedValue
 }
@@ -127,10 +213,11 @@ export const convertToFloat = (value: unknown): number => {
   }
   let changedValue: number = value as number
   if (typeof value === 'string') {
-    changedValue = parseFloat(value)
+    changedValue = Number.parseFloat(value)
   }
-  if (isNaN(changedValue)) {
-    throw new Error(`Cannot convert to float: '${String(value)}'`)
+  if (Number.isNaN(changedValue)) {
+    // eslint-disable-next-line @typescript-eslint/no-base-to-string
+    throw new Error(`Cannot convert to float: '${value.toString()}'`)
   }
   return changedValue
 }
@@ -163,13 +250,12 @@ export const getRandomFloat = (max = Number.MAX_VALUE, min = 0): number => {
 /**
  * Rounds the given number to the given scale.
  * The rounding is done using the "round half away from zero" method.
- *
  * @param numberValue - The number to round.
  * @param scale - The scale to round to.
  * @returns The rounded number.
  */
 export const roundTo = (numberValue: number, scale: number): number => {
-  const roundPower = Math.pow(10, scale)
+  const roundPower = 10 ** scale
   return Math.round(numberValue * roundPower * (1 + Number.EPSILON)) / roundPower
 }
 
@@ -187,7 +273,7 @@ export const getRandomFloatFluctuatedRounded = (
 ): number => {
   if (fluctuationPercent < 0 || fluctuationPercent > 100) {
     throw new RangeError(
-      `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent}`
+      `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent.toString()}`
     )
   }
   if (fluctuationPercent === 0) {
@@ -209,34 +295,32 @@ export const clone = <T>(object: T): T => {
   return structuredClone<T>(object)
 }
 
+type AsyncFunctionType<A extends unknown[], R> = (...args: A) => PromiseLike<R>
+
 /**
  * Detects whether the given value is an asynchronous function or not.
- *
  * @param fn - Unknown value.
  * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
  * @internal
  */
-export const isAsyncFunction = (fn: unknown): fn is (...args: unknown[]) => Promise<unknown> => {
-  return is(Function, fn) && fn.constructor.name === 'AsyncFunction'
-}
-
-export const isObject = (value: unknown): value is object => {
-  return value != null && !Array.isArray(value) && is(Object, value)
-}
-
-export const hasOwnProp = (value: unknown, property: PropertyKey): boolean => {
-  return isObject(value) && Object.hasOwn(value, property)
+export const isAsyncFunction = (fn: unknown): fn is AsyncFunctionType<unknown[], unknown> => {
+  // eslint-disable-next-line @typescript-eslint/no-empty-function
+  return fn?.constructor === (async () => {}).constructor
 }
 
 export const isCFEnvironment = (): boolean => {
   return env.VCAP_APPLICATION != null
 }
 
-export const isNotEmptyString = (value: unknown): value is string => {
+declare const nonEmptyString: unique symbol
+type NonEmptyString = string & { [nonEmptyString]: true }
+export const isNotEmptyString = (value: unknown): value is NonEmptyString => {
   return typeof value === 'string' && value.trim().length > 0
 }
 
-export const isNotEmptyArray = (value: unknown): value is unknown[] => {
+export const isNotEmptyArray = <T>(
+  value: unknown
+): value is NonEmptyArray<T> | ReadonlyNonEmptyArray<T> => {
   return Array.isArray(value) && value.length > 0
 }
 
@@ -245,20 +329,18 @@ export const insertAt = (str: string, subStr: string, pos: number): string =>
 
 /**
  * Computes the retry delay in milliseconds using an exponential backoff algorithm.
- *
  * @param retryNumber - the number of retries that have already been attempted
  * @param delayFactor - the base delay factor in milliseconds
  * @returns delay in milliseconds
  */
 export const exponentialDelay = (retryNumber = 0, delayFactor = 100): number => {
-  const delay = Math.pow(2, retryNumber) * delayFactor
+  const delay = 2 ** retryNumber * delayFactor
   const randomSum = delay * 0.2 * secureRandom() // 0-20% of the delay
   return delay + randomSum
 }
 
 /**
  * Generates a cryptographically secure random number in the [0,1[ range
- *
  * @returns A number in the [0,1[ range
  */
 export const secureRandom = (): number => {
@@ -266,29 +348,32 @@ export const secureRandom = (): number => {
 }
 
 export const JSONStringify = <
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
   T extends
-  | JsonType
-  | Array<Record<string, unknown>>
-  | Set<Record<string, unknown>>
-  | Map<string, Record<string, unknown>>
+    | JsonType
+    | Map<string, Record<string, unknown>>
+    | Record<string, unknown>[]
+    | Set<Record<string, unknown>>
 >(
     object: T,
-    space?: string | number,
+    space?: number | string,
     mapFormat?: MapStringifyFormat
   ): string => {
   return JSON.stringify(
     object,
     (_, value: Record<string, unknown>) => {
-      if (is(Map, value)) {
+      if (value instanceof Map) {
         switch (mapFormat) {
           case MapStringifyFormat.object:
-            return { ...Object.fromEntries<Map<string, Record<string, unknown>>>(value.entries()) }
+            return {
+              ...Object.fromEntries<Map<string, Record<string, unknown>>>(value.entries()),
+            }
           case MapStringifyFormat.array:
           default:
             return [...value]
         }
-      } else if (is(Set, value)) {
-        return [...value] as JsonType[]
+      } else if (value instanceof Set) {
+        return [...value] as Record<string, unknown>[]
       }
       return value
     },
@@ -298,7 +383,6 @@ export const JSONStringify = <
 
 /**
  * Converts websocket error code to human readable string message
- *
  * @param code - websocket error code
  * @returns human readable string message
  */
@@ -326,6 +410,9 @@ export const getWebSocketCloseEventStatusString = (code: number): string => {
 }
 
 export const isArraySorted = <T>(array: T[], compareFn: (a: T, b: T) => number): boolean => {
+  if (array.length <= 1) {
+    return true
+  }
   for (let index = 0; index < array.length - 1; ++index) {
     if (compareFn(array[index], array[index + 1]) > 0) {
       return false
@@ -334,8 +421,8 @@ export const isArraySorted = <T>(array: T[], compareFn: (a: T, b: T) => number):
   return true
 }
 
-export const throwErrorInNextTick = (error: Error): void => {
-  nextTick(() => {
+export const queueMicrotaskErrorThrowing = (error: Error): void => {
+  queueMicrotask(() => {
     throw error
   })
 }