-import { getRandomValues, randomBytes, randomUUID } from 'node:crypto'
-import { env, nextTick } from 'node:process'
+import type { CircularBuffer } from 'mnemonist'
import {
formatDuration,
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()
}
days,
hours,
minutes,
- seconds
+ seconds,
})
}
// 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
}
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
}
}
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
}
}
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
}
/**
* 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
}
): 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) {
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
}
/**
* 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 => {
}
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
},
/**
* Converts websocket error code to human readable string message
- *
* @param code - websocket error code
* @returns human readable string message
*/
}
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
return true
}
-export const throwErrorInNextTick = (error: Error): void => {
- nextTick(() => {
+export const queueMicrotaskErrorThrowing = (error: Error): void => {
+ queueMicrotask(() => {
throw error
})
}