1 import { getRandomValues
, randomBytes
, randomUUID
} from
'node:crypto'
2 import { env
, nextTick
} from
'node:process'
10 millisecondsToMinutes
,
11 millisecondsToSeconds
,
15 import type { CircularBuffer
} from
'mnemonist'
16 import { is } from
'rambda'
22 WebSocketCloseEventStatusString
23 } from
'../types/index.js'
25 export const logPrefix
= (prefixString
= ''): string => {
26 return `${new Date().toLocaleString()}${prefixString}`
29 export const generateUUID
= (): `${string}-${string}-${string}-${string}-${string}` => {
33 export const validateUUID
= (
34 uuid
: `${string}-${string}-${string}-${string}-${string}`
35 ): uuid
is `${string}-${string}-${string}-${string}-${string}` => {
36 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
)
39 export const sleep
= async (milliSeconds
: number): Promise
<NodeJS
.Timeout
> => {
40 return await new Promise
<NodeJS
.Timeout
>(resolve
=>
41 setTimeout(resolve
as () => void, milliSeconds
)
45 export const formatDurationMilliSeconds
= (duration
: number): string => {
46 duration
= convertToInt(duration
)
48 throw new RangeError('Duration cannot be negative')
50 const days
= Math.floor(duration
/ (24 * 3600 * 1000))
51 const hours
= Math.floor(millisecondsToHours(duration
) - days
* 24)
52 const minutes
= Math.floor(
53 millisecondsToMinutes(duration
) - days
* 24 * 60 - hoursToMinutes(hours
)
55 const seconds
= Math.floor(
56 millisecondsToSeconds(duration
) -
58 hoursToSeconds(hours
) -
59 minutesToSeconds(minutes
)
61 if (days
=== 0 && hours
=== 0 && minutes
=== 0 && seconds
=== 0) {
62 return formatDuration({ seconds
}, { zero
: true })
64 return formatDuration({
72 export const formatDurationSeconds
= (duration
: number): string => {
73 return formatDurationMilliSeconds(secondsToMilliseconds(duration
))
76 // More efficient time validation function than the one provided by date-fns
77 export const isValidDate
= (date
: Date | number | undefined): date
is Date | number => {
78 if (typeof date
=== 'number') {
80 } else if (isDate(date
)) {
81 return !isNaN(date
.getTime())
86 export const convertToDate
= (
87 value
: Date | string | number | undefined | null
88 ): Date | undefined => {
95 if (typeof value
=== 'string' || typeof value
=== 'number') {
96 const valueToDate
= new Date(value
)
97 if (isNaN(valueToDate
.getTime())) {
98 throw new Error(`Cannot convert to date: '${value}'`)
104 export const convertToInt
= (value
: unknown
): number => {
108 if (Number.isSafeInteger(value
)) {
109 return value
as number
111 if (typeof value
=== 'number') {
112 return Math.trunc(value
)
114 let changedValue
: number = value
as number
115 if (typeof value
=== 'string') {
116 changedValue
= Number.parseInt(value
)
118 if (isNaN(changedValue
)) {
119 throw new Error(`Cannot convert to integer: '${String(value)}'`)
124 export const convertToFloat
= (value
: unknown
): number => {
128 let changedValue
: number = value
as number
129 if (typeof value
=== 'string') {
130 changedValue
= Number.parseFloat(value
)
132 if (isNaN(changedValue
)) {
133 throw new Error(`Cannot convert to float: '${String(value)}'`)
138 export const convertToBoolean
= (value
: unknown
): boolean => {
142 if (typeof value
=== 'boolean') {
144 } else if (typeof value
=== 'string' && (value
.toLowerCase() === 'true' || value
=== '1')) {
146 } else if (typeof value
=== 'number' && value
=== 1) {
153 export const getRandomFloat
= (max
= Number.MAX_VALUE
, min
= 0): number => {
155 throw new RangeError('Invalid interval')
157 if (max
- min
=== Number.POSITIVE_INFINITY
) {
158 throw new RangeError('Invalid interval')
160 return (randomBytes(4).readUInt32LE() / 0xffffffff) * (max
- min
) + min
164 * Rounds the given number to the given scale.
165 * The rounding is done using the "round half away from zero" method.
167 * @param numberValue - The number to round.
168 * @param scale - The scale to round to.
169 * @returns The rounded number.
171 export const roundTo
= (numberValue
: number, scale
: number): number => {
172 const roundPower
= Math.pow(10, scale
)
173 return Math.round(numberValue
* roundPower
* (1 + Number.EPSILON
)) / roundPower
176 export const getRandomFloatRounded
= (max
= Number.MAX_VALUE
, min
= 0, scale
= 2): number => {
178 return roundTo(getRandomFloat(max
, min
), scale
)
180 return roundTo(getRandomFloat(max
), scale
)
183 export const getRandomFloatFluctuatedRounded
= (
185 fluctuationPercent
: number,
188 if (fluctuationPercent
< 0 || fluctuationPercent
> 100) {
189 throw new RangeError(
190 `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent}`
193 if (fluctuationPercent
=== 0) {
194 return roundTo(staticValue
, scale
)
196 const fluctuationRatio
= fluctuationPercent
/ 100
197 return getRandomFloatRounded(
198 staticValue
* (1 + fluctuationRatio
),
199 staticValue
* (1 - fluctuationRatio
),
204 export const extractTimeSeriesValues
= (timeSeries
: CircularBuffer
<TimestampedData
>): number[] => {
205 return (timeSeries
.toArray() as TimestampedData
[]).map(timeSeriesItem
=> timeSeriesItem
.value
)
208 export const clone
= <T
>(object
: T
): T
=> {
209 return structuredClone
<T
>(object
)
213 * Detects whether the given value is an asynchronous function or not.
215 * @param fn - Unknown value.
216 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
219 export const isAsyncFunction
= (fn
: unknown
): fn
is (...args
: unknown
[]) => Promise
<unknown
> => {
220 return is(Function, fn
) && fn
.constructor
.name
=== 'AsyncFunction'
223 export const isObject
= (value
: unknown
): value
is object
=> {
224 return value
!= null && !Array.isArray(value
) && is(Object, value
)
227 export const hasOwnProp
= (value
: unknown
, property
: PropertyKey
): boolean => {
228 return isObject(value
) && Object.hasOwn(value
, property
)
231 export const isCFEnvironment
= (): boolean => {
232 return env
.VCAP_APPLICATION
!= null
235 export const isNotEmptyString
= (value
: unknown
): value
is string => {
236 return typeof value
=== 'string' && value
.trim().length
> 0
239 export const isNotEmptyArray
= (value
: unknown
): value
is unknown
[] => {
240 return Array.isArray(value
) && value
.length
> 0
243 export const insertAt
= (str
: string, subStr
: string, pos
: number): string =>
244 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`
247 * Computes the retry delay in milliseconds using an exponential backoff algorithm.
249 * @param retryNumber - the number of retries that have already been attempted
250 * @param delayFactor - the base delay factor in milliseconds
251 * @returns delay in milliseconds
253 export const exponentialDelay
= (retryNumber
= 0, delayFactor
= 100): number => {
254 const delay
= Math.pow(2, retryNumber
) * delayFactor
255 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
256 return delay
+ randomSum
260 * Generates a cryptographically secure random number in the [0,1[ range
262 * @returns A number in the [0,1[ range
264 export const secureRandom
= (): number => {
265 return getRandomValues(new Uint32Array(1))[0] / 0x100000000
268 export const JSONStringify
= <
271 | Array<Record
<string, unknown
>>
272 | Set
<Record
<string, unknown
>>
273 | Map
<string, Record
<string, unknown
>>
276 space
?: string | number,
277 mapFormat
?: MapStringifyFormat
279 return JSON
.stringify(
281 (_
, value
: Record
<string, unknown
>) => {
282 if (is(Map
, value
)) {
284 case MapStringifyFormat
.object
:
286 ...Object.fromEntries
<Map
<string, Record
<string, unknown
>>>(value
.entries())
288 case MapStringifyFormat
.array
:
292 } else if (is(Set
, value
)) {
293 return [...value
] as JsonType
[]
302 * Converts websocket error code to human readable string message
304 * @param code - websocket error code
305 * @returns human readable string message
307 export const getWebSocketCloseEventStatusString
= (code
: number): string => {
308 if (code
>= 0 && code
<= 999) {
310 } else if (code
>= 1016) {
312 return '(For WebSocket standard)'
313 } else if (code
<= 2999) {
314 return '(For WebSocket extensions)'
315 } else if (code
<= 3999) {
316 return '(For libraries and frameworks)'
317 } else if (code
<= 4999) {
318 return '(For applications)'
322 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
323 WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
] != null
325 return WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
]
330 export const isArraySorted
= <T
>(array
: T
[], compareFn
: (a
: T
, b
: T
) => number): boolean => {
331 if (array
.length
<= 1) {
334 for (let index
= 0; index
< array
.length
- 1; ++index
) {
335 if (compareFn(array
[index
], array
[index
+ 1]) > 0) {
342 export const throwErrorInNextTick
= (error
: Error): void => {