1 import { getRandomValues
, randomBytes
, randomInt
, randomUUID
} from
'node:crypto'
2 import { env
, nextTick
} from
'node:process'
10 millisecondsToMinutes
,
11 millisecondsToSeconds
,
18 type ProtocolResponse
,
20 WebSocketCloseEventStatusString
21 } from
'../types/index.js'
22 import { Constants
} from
'./Constants.js'
24 export const logPrefix
= (prefixString
= ''): string => {
25 return `${new Date().toLocaleString()}${prefixString}`
28 export const generateUUID
= (): `${string}-${string}-${string}-${string}-${string}` => {
32 export const validateUUID
= (
33 uuid
: `${string}-${string}-${string}-${string}-${string}`
34 ): uuid
is `${string}-${string}-${string}-${string}-${string}` => {
35 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
)
38 export const sleep
= async (milliSeconds
: number): Promise
<NodeJS
.Timeout
> => {
39 return await new Promise
<NodeJS
.Timeout
>(resolve
=>
40 setTimeout(resolve
as () => void, milliSeconds
)
44 export const formatDurationMilliSeconds
= (duration
: number): string => {
45 duration
= convertToInt(duration
)
47 throw new RangeError('Duration cannot be negative')
49 const days
= Math.floor(duration
/ (24 * 3600 * 1000))
50 const hours
= Math.floor(millisecondsToHours(duration
) - days
* 24)
51 const minutes
= Math.floor(
52 millisecondsToMinutes(duration
) - days
* 24 * 60 - hoursToMinutes(hours
)
54 const seconds
= Math.floor(
55 millisecondsToSeconds(duration
) -
57 hoursToSeconds(hours
) -
58 minutesToSeconds(minutes
)
60 if (days
=== 0 && hours
=== 0 && minutes
=== 0 && seconds
=== 0) {
61 return formatDuration({ seconds
}, { zero
: true })
63 return formatDuration({
71 export const formatDurationSeconds
= (duration
: number): string => {
72 return formatDurationMilliSeconds(secondsToMilliseconds(duration
))
75 // More efficient time validation function than the one provided by date-fns
76 export const isValidDate
= (date
: Date | number | undefined): date
is Date | number => {
77 if (typeof date
=== 'number') {
79 } else if (isDate(date
)) {
80 return !isNaN(date
.getTime())
85 export const convertToDate
= (
86 value
: Date | string | number | undefined | null
87 ): Date | undefined => {
94 if (isString(value
) || typeof value
=== 'number') {
95 const valueToDate
= new Date(value
)
96 if (isNaN(valueToDate
.getTime())) {
97 throw new Error(`Cannot convert to date: '${value}'`)
103 export const convertToInt
= (value
: unknown
): number => {
107 if (Number.isSafeInteger(value
)) {
108 return value
as number
110 if (typeof value
=== 'number') {
111 return Math.trunc(value
)
113 let changedValue
: number = value
as number
114 if (isString(value
)) {
115 changedValue
= parseInt(value
)
117 if (isNaN(changedValue
)) {
118 throw new Error(`Cannot convert to integer: '${String(value)}'`)
123 export const convertToFloat
= (value
: unknown
): number => {
127 let changedValue
: number = value
as number
128 if (isString(value
)) {
129 changedValue
= parseFloat(value
)
131 if (isNaN(changedValue
)) {
132 throw new Error(`Cannot convert to float: '${String(value)}'`)
137 export const convertToBoolean
= (value
: unknown
): boolean => {
141 if (typeof value
=== 'boolean') {
143 } else if (isString(value
) && (value
.toLowerCase() === 'true' || value
=== '1')) {
145 } else if (typeof value
=== 'number' && value
=== 1) {
152 export const getRandomFloat
= (max
= Number.MAX_VALUE
, min
= 0): number => {
154 throw new RangeError('Invalid interval')
156 if (max
- min
=== Infinity) {
157 throw new RangeError('Invalid interval')
159 return (randomBytes(4).readUInt32LE() / 0xffffffff) * (max
- min
) + min
162 export const getRandomInteger
= (max
= Constants
.MAX_RANDOM_INTEGER
, min
= 0): number => {
163 max
= Math.floor(max
)
166 return Math.floor(randomInt(min
, max
+ 1))
168 return Math.floor(randomInt(max
+ 1))
172 * Rounds the given number to the given scale.
173 * The rounding is done using the "round half away from zero" method.
175 * @param numberValue - The number to round.
176 * @param scale - The scale to round to.
177 * @returns The rounded number.
179 export const roundTo
= (numberValue
: number, scale
: number): number => {
180 const roundPower
= Math.pow(10, scale
)
181 return Math.round(numberValue
* roundPower
* (1 + Number.EPSILON
)) / roundPower
184 export const getRandomFloatRounded
= (max
= Number.MAX_VALUE
, min
= 0, scale
= 2): number => {
186 return roundTo(getRandomFloat(max
, min
), scale
)
188 return roundTo(getRandomFloat(max
), scale
)
191 export const getRandomFloatFluctuatedRounded
= (
193 fluctuationPercent
: number,
196 if (fluctuationPercent
< 0 || fluctuationPercent
> 100) {
197 throw new RangeError(
198 `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent}`
201 if (fluctuationPercent
=== 0) {
202 return roundTo(staticValue
, scale
)
204 const fluctuationRatio
= fluctuationPercent
/ 100
205 return getRandomFloatRounded(
206 staticValue
* (1 + fluctuationRatio
),
207 staticValue
* (1 - fluctuationRatio
),
212 export const extractTimeSeriesValues
= (timeSeries
: TimestampedData
[]): number[] => {
213 return timeSeries
.map(timeSeriesItem
=> timeSeriesItem
.value
)
216 export const clone
= <T
>(object
: T
): T
=> {
217 return structuredClone
<T
>(object
)
221 * Detects whether the given value is an asynchronous function or not.
223 * @param fn - Unknown value.
224 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
227 export const isAsyncFunction
= (fn
: unknown
): fn
is (...args
: unknown
[]) => Promise
<unknown
> => {
228 return typeof fn
=== 'function' && fn
.constructor
.name
=== 'AsyncFunction'
231 export const isObject
= (value
: unknown
): value
is object
=> {
232 return value
!= null && typeof value
=== 'object' && !Array.isArray(value
)
235 export const isEmptyObject
= (object
: object
): object
is EmptyObject
=> {
236 if (object
.constructor
!== Object) {
239 // Iterates over the keys of an object, if
240 // any exist, return false.
241 // eslint-disable-next-line no-unreachable-loop
242 for (const _
in object
) {
248 export const hasOwnProp
= (value
: unknown
, property
: PropertyKey
): boolean => {
249 return isObject(value
) && Object.hasOwn(value
, property
)
252 export const isCFEnvironment
= (): boolean => {
253 return env
.VCAP_APPLICATION
!= null
256 const isString
= (value
: unknown
): value
is string => {
257 return typeof value
=== 'string'
260 export const isEmptyString
= (value
: unknown
): value
is '' | undefined | null => {
261 return value
== null || (isString(value
) && value
.trim().length
=== 0)
264 export const isNotEmptyString
= (value
: unknown
): value
is string => {
265 return isString(value
) && value
.trim().length
> 0
268 export const isEmptyArray
= (value
: unknown
): value
is [] => {
269 return Array.isArray(value
) && value
.length
=== 0
272 export const isNotEmptyArray
= (value
: unknown
): value
is unknown
[] => {
273 return Array.isArray(value
) && value
.length
> 0
276 export const insertAt
= (str
: string, subStr
: string, pos
: number): string =>
277 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`
280 * Computes the retry delay in milliseconds using an exponential backoff algorithm.
282 * @param retryNumber - the number of retries that have already been attempted
283 * @param delayFactor - the base delay factor in milliseconds
284 * @returns delay in milliseconds
286 export const exponentialDelay
= (retryNumber
= 0, delayFactor
= 100): number => {
287 const delay
= Math.pow(2, retryNumber
) * delayFactor
288 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
289 return delay
+ randomSum
293 * Generates a cryptographically secure random number in the [0,1[ range
295 * @returns A number in the [0,1[ range
297 export const secureRandom
= (): number => {
298 return getRandomValues(new Uint32Array(1))[0] / 0x100000000
301 export const JSONStringifyWithMapSupport
= (
303 | Record
<string, unknown
>
304 | Array<Record
<string, unknown
>>
305 | Map
<unknown
, unknown
>
307 space
?: string | number
309 return JSON
.stringify(
311 (_
, value
: Record
<string, unknown
>) => {
312 if (value
instanceof Map
) {
325 * Converts websocket error code to human readable string message
327 * @param code - websocket error code
328 * @returns human readable string message
330 export const getWebSocketCloseEventStatusString
= (code
: number): string => {
331 if (code
>= 0 && code
<= 999) {
333 } else if (code
>= 1016) {
335 return '(For WebSocket standard)'
336 } else if (code
<= 2999) {
337 return '(For WebSocket extensions)'
338 } else if (code
<= 3999) {
339 return '(For libraries and frameworks)'
340 } else if (code
<= 4999) {
341 return '(For applications)'
345 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
346 WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
] != null
348 return WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
]
353 export const isArraySorted
= <T
>(array
: T
[], compareFn
: (a
: T
, b
: T
) => number): boolean => {
354 for (let index
= 0; index
< array
.length
- 1; ++index
) {
355 if (compareFn(array
[index
], array
[index
+ 1]) > 0) {
362 // eslint-disable-next-line @typescript-eslint/no-explicit-any
363 export const once
= <T
, A
extends any[], R
>(
364 fn
: (...args
: A
) => R
,
366 ): ((...args
: A
) => R
) => {
368 return (...args
: A
) => {
369 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
371 result
= fn
.apply
<T
, A
, R
>(context
, args
)
372 ;(fn
as unknown
as undefined) = (context
as unknown
as undefined) = undefined
378 export const min
= (...args
: number[]): number =>
379 args
.reduce((minimum
, num
) => (minimum
< num
? minimum
: num
), Infinity)
381 export const max
= (...args
: number[]): number =>
382 args
.reduce((maximum
, num
) => (maximum
> num
? maximum
: num
), -Infinity)
384 export const throwErrorInNextTick
= (error
: Error): void => {