1 import { getRandomValues
, randomBytes
, randomInt
, randomUUID
} from
'node:crypto'
2 import { env
, nextTick
} from
'node:process'
10 millisecondsToMinutes
,
11 millisecondsToSeconds
,
16 import { Constants
} from
'./Constants.js'
17 import { type TimestampedData
, WebSocketCloseEventStatusString
} from
'../types/index.js'
19 export const logPrefix
= (prefixString
= ''): string => {
20 return `${new Date().toLocaleString()}${prefixString}`
23 export const generateUUID
= (): string => {
27 export const validateUUID
= (uuid
: string): boolean => {
28 return /^[0-9a
-fA
-F
]{8}-[0-9a
-fA
-F
]{4}-4[0-9a
-fA
-F
]{3}-[0-9a
-fA
-F
]{4}-[0-9a
-fA
-F
]{12}$
/.test(uuid
)
31 export const sleep
= async (milliSeconds
: number): Promise
<NodeJS
.Timeout
> => {
32 return await new Promise
<NodeJS
.Timeout
>((resolve
) =>
33 setTimeout(resolve
as () => void, milliSeconds
)
37 export const formatDurationMilliSeconds
= (duration
: number): string => {
38 duration
= convertToInt(duration
)
40 throw new RangeError('Duration cannot be negative')
42 const days
= Math.floor(duration
/ (24 * 3600 * 1000))
43 const hours
= Math.floor(millisecondsToHours(duration
) - days
* 24)
44 const minutes
= Math.floor(
45 millisecondsToMinutes(duration
) - days
* 24 * 60 - hoursToMinutes(hours
)
47 const seconds
= Math.floor(
48 millisecondsToSeconds(duration
) -
50 hoursToSeconds(hours
) -
51 minutesToSeconds(minutes
)
53 if (days
=== 0 && hours
=== 0 && minutes
=== 0 && seconds
=== 0) {
54 return formatDuration({ seconds
}, { zero
: true })
56 return formatDuration({
64 export const formatDurationSeconds
= (duration
: number): string => {
65 return formatDurationMilliSeconds(secondsToMilliseconds(duration
))
68 // More efficient time validation function than the one provided by date-fns
69 export const isValidTime
= (date
: unknown
): boolean => {
70 if (typeof date
=== 'number') {
72 } else if (isDate(date
)) {
73 return !isNaN(date
.getTime())
78 export const convertToDate
= (
79 value
: Date | string | number | null | undefined
80 ): Date | null | undefined => {
87 if (isString(value
) || typeof value
=== 'number') {
88 const valueToDate
= new Date(value
)
89 if (isNaN(valueToDate
.getTime())) {
90 throw new Error(`Cannot convert to date: '${value}'`)
96 export const convertToInt
= (value
: unknown
): number => {
100 let changedValue
: number = value
as number
101 if (Number.isSafeInteger(value
)) {
102 return value
as number
104 if (typeof value
=== 'number') {
105 return Math.trunc(value
)
107 if (isString(value
)) {
108 changedValue
= parseInt(value
as string)
110 if (isNaN(changedValue
)) {
111 throw new Error(`Cannot convert to integer: '${String(value)}'`)
116 export const convertToFloat
= (value
: unknown
): number => {
120 let changedValue
: number = value
as number
121 if (isString(value
)) {
122 changedValue
= parseFloat(value
as string)
124 if (isNaN(changedValue
)) {
125 throw new Error(`Cannot convert to float: '${String(value)}'`)
130 export const convertToBoolean
= (value
: unknown
): boolean => {
134 if (typeof value
=== 'boolean') {
136 } else if (isString(value
) && ((value
as string).toLowerCase() === 'true' || value
=== '1')) {
138 } else if (typeof value
=== 'number' && value
=== 1) {
145 export const getRandomFloat
= (max
= Number.MAX_VALUE
, min
= 0): number => {
147 throw new RangeError('Invalid interval')
149 if (max
- min
=== Infinity) {
150 throw new RangeError('Invalid interval')
152 return (randomBytes(4).readUInt32LE() / 0xffffffff) * (max
- min
) + min
155 export const getRandomInteger
= (max
= Constants
.MAX_RANDOM_INTEGER
, min
= 0): number => {
156 max
= Math.floor(max
)
157 if (min
!= null && min
!== 0) {
159 return Math.floor(randomInt(min
, max
+ 1))
161 return Math.floor(randomInt(max
+ 1))
165 * Rounds the given number to the given scale.
166 * The rounding is done using the "round half away from zero" method.
168 * @param numberValue - The number to round.
169 * @param scale - The scale to round to.
170 * @returns The rounded number.
172 export const roundTo
= (numberValue
: number, scale
: number): number => {
173 const roundPower
= Math.pow(10, scale
)
174 return Math.round(numberValue
* roundPower
* (1 + Number.EPSILON
)) / roundPower
177 export const getRandomFloatRounded
= (max
= Number.MAX_VALUE
, min
= 0, scale
= 2): number => {
178 if (min
!= null && min
!== 0) {
179 return roundTo(getRandomFloat(max
, min
), scale
)
181 return roundTo(getRandomFloat(max
), scale
)
184 export const getRandomFloatFluctuatedRounded
= (
186 fluctuationPercent
: number,
189 if (fluctuationPercent
< 0 || fluctuationPercent
> 100) {
190 throw new RangeError(
191 `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent}`
194 if (fluctuationPercent
=== 0) {
195 return roundTo(staticValue
, scale
)
197 const fluctuationRatio
= fluctuationPercent
/ 100
198 return getRandomFloatRounded(
199 staticValue
* (1 + fluctuationRatio
),
200 staticValue
* (1 - fluctuationRatio
),
205 export const extractTimeSeriesValues
= (timeSeries
: TimestampedData
[]): number[] => {
206 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
)
209 export const isObject
= (item
: unknown
): boolean => {
210 return item
!= null && typeof item
=== 'object' && !Array.isArray(item
)
221 | { [key
: string]: CloneableData
}
223 type FormatKey
= (key
: string) => string
225 const deepClone
= <I
extends CloneableData
, O
extends CloneableData
= I
>(
227 formatKey
?: FormatKey
,
228 refs
: Map
<I
, O
> = new Map
<I
, O
>()
230 const ref
= refs
.get(value
)
231 if (ref
!== undefined) {
234 if (Array.isArray(value
)) {
235 const clone
: CloneableData
[] = []
236 refs
.set(value
, clone
as O
)
237 for (let i
= 0; i
< value
.length
; i
++) {
238 clone
[i
] = deepClone(value
[i
], formatKey
, refs
)
242 if (value
instanceof Date) {
243 return new Date(value
.valueOf()) as O
245 if (typeof value
!== 'object' || value
=== null) {
246 return value
as unknown
as O
248 const clone
: Record
<string, CloneableData
> = {}
249 refs
.set(value
, clone
as O
)
250 for (const key
of Object.keys(value
)) {
251 clone
[typeof formatKey
=== 'function' ? formatKey(key
) : key
] = deepClone(
260 export const cloneObject
= <T
>(object
: T
): T
=> {
261 return deepClone(object
as CloneableData
) as T
264 export const hasOwnProp
= (object
: unknown
, property
: PropertyKey
): boolean => {
265 return isObject(object
) && Object.hasOwn(object
as object
, property
)
268 export const isCFEnvironment
= (): boolean => {
269 return env
.VCAP_APPLICATION
!= null
272 export const isIterable
= <T
>(obj
: T
): boolean => {
273 return obj
!= null ? typeof obj
[Symbol
.iterator
as keyof T
] === 'function' : false
276 const isString
= (value
: unknown
): boolean => {
277 return typeof value
=== 'string'
280 export const isEmptyString
= (value
: unknown
): boolean => {
281 return value
== null || (isString(value
) && (value
as string).trim().length
=== 0)
284 export const isNotEmptyString
= (value
: unknown
): boolean => {
285 return isString(value
) && (value
as string).trim().length
> 0
288 export const isEmptyArray
= (object
: unknown
): boolean => {
289 return Array.isArray(object
) && object
.length
=== 0
292 export const isNotEmptyArray
= (object
: unknown
): boolean => {
293 return Array.isArray(object
) && object
.length
> 0
296 export const isEmptyObject
= (obj
: object
): boolean => {
297 if (obj
?.constructor
!== Object) {
300 // Iterates over the keys of an object, if
301 // any exist, return false.
302 // eslint-disable-next-line no-unreachable-loop
303 for (const _
in obj
) {
309 export const insertAt
= (str
: string, subStr
: string, pos
: number): string =>
310 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`
313 * Computes the retry delay in milliseconds using an exponential backoff algorithm.
315 * @param retryNumber - the number of retries that have already been attempted
316 * @param delayFactor - the base delay factor in milliseconds
317 * @returns delay in milliseconds
319 export const exponentialDelay
= (retryNumber
= 0, delayFactor
= 100): number => {
320 const delay
= Math.pow(2, retryNumber
) * delayFactor
321 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
322 return delay
+ randomSum
326 * Generates a cryptographically secure random number in the [0,1[ range
328 * @returns A number in the [0,1[ range
330 export const secureRandom
= (): number => {
331 return getRandomValues(new Uint32Array(1))[0] / 0x100000000
334 export const JSONStringifyWithMapSupport
= (
335 obj
: Record
<string, unknown
> | Array<Record
<string, unknown
>> | Map
<unknown
, unknown
>,
338 return JSON
.stringify(
340 (_
, value
: Record
<string, unknown
>) => {
341 if (value
instanceof Map
) {
354 * Converts websocket error code to human readable string message
356 * @param code - websocket error code
357 * @returns human readable string message
359 export const getWebSocketCloseEventStatusString
= (code
: number): string => {
360 if (code
>= 0 && code
<= 999) {
362 } else if (code
>= 1016) {
364 return '(For WebSocket standard)'
365 } else if (code
<= 2999) {
366 return '(For WebSocket extensions)'
367 } else if (code
<= 3999) {
368 return '(For libraries and frameworks)'
369 } else if (code
<= 4999) {
370 return '(For applications)'
374 WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
] !==
377 return WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
]
382 export const isArraySorted
= <T
>(array
: T
[], compareFn
: (a
: T
, b
: T
) => number): boolean => {
383 for (let index
= 0; index
< array
.length
- 1; ++index
) {
384 if (compareFn(array
[index
], array
[index
+ 1]) > 0) {
391 // eslint-disable-next-line @typescript-eslint/no-explicit-any
392 export const once
= <T
, A
extends any[], R
>(
393 fn
: (...args
: A
) => R
,
395 ): ((...args
: A
) => R
) => {
397 return (...args
: A
) => {
399 result
= fn
.apply
<T
, A
, R
>(context
, args
)
400 ;(fn
as unknown
as undefined) = (context
as unknown
as undefined) = undefined
406 export const min
= (...args
: number[]): number =>
407 args
.reduce((minimum
, num
) => (minimum
< num
? minimum
: num
), Infinity)
409 export const max
= (...args
: number[]): number =>
410 args
.reduce((maximum
, num
) => (maximum
> num
? maximum
: num
), -Infinity)
412 export const throwErrorInNextTick
= (error
: Error): void => {