1 import type { CircularBuffer
} from
'mnemonist'
10 millisecondsToSeconds
,
12 secondsToMilliseconds
,
14 import { getRandomValues
, randomBytes
, randomUUID
} from
'node:crypto'
15 import { env
} from
'node:process'
21 WebSocketCloseEventStatusString
,
22 } from
'../types/index.js'
24 type NonEmptyArray
<T
> = [T
, ...T
[]]
25 type ReadonlyNonEmptyArray
<T
> = readonly [T
, ...(readonly T
[])]
27 export const logPrefix
= (prefixString
= ''): string => {
28 return `${new Date().toLocaleString()}${prefixString}`
31 // eslint-disable-next-line @typescript-eslint/no-explicit-any
32 export const once
= <T
extends (...args
: any[]) => any>(fn
: T
): T
=> {
33 let hasBeenCalled
= false
34 let result
: ReturnType
<T
>
35 // eslint-disable-next-line @typescript-eslint/no-explicit-any
36 return function (this: any, ...args
: Parameters
<T
>): ReturnType
<T
> {
39 result
= fn
.apply(this, args
) as ReturnType
<T
>
41 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
46 export const has
= (property
: PropertyKey
, object
: null | object
| undefined): boolean => {
50 return Object.hasOwn(object
, property
)
53 const type = (value
: unknown
): string => {
54 if (value
=== null) return 'Null'
55 if (value
=== undefined) return 'Undefined'
56 if (Number.isNaN(value
)) return 'NaN'
57 if (Array.isArray(value
)) return 'Array'
58 return Object.prototype
.toString
.call(value
).slice(8, -1)
61 export const isEmpty
= (value
: unknown
): boolean => {
62 const valueType
= type(value
)
63 if (['NaN', 'Null', 'Number', 'Undefined'].includes(valueType
)) {
66 if (!value
) return true
68 if (valueType
=== 'Object') {
69 return Object.keys(value
as Record
<string, unknown
>).length
=== 0
72 if (valueType
=== 'Array') {
73 return (value
as unknown
[]).length
=== 0
76 if (valueType
=== 'Map') {
77 return (value
as Map
<unknown
, unknown
>).size
=== 0
80 if (valueType
=== 'Set') {
81 return (value
as Set
<unknown
>).size
=== 0
87 const isObject
= (value
: unknown
): value
is object
=> {
88 return type(value
) === 'Object'
91 export const mergeDeepRight
= <T
extends Record
<string, unknown
>>(
95 const output
= { ...target
}
97 if (isObject(target
) && isObject(source
)) {
98 Object.keys(source
).forEach(key
=> {
99 if (isObject(source
[key
])) {
100 if (!(key
in target
)) {
101 Object.assign(output
, { [key
]: source
[key
] })
103 output
[key
] = mergeDeepRight(target
[key
], source
[key
])
106 Object.assign(output
, { [key
]: source
[key
] })
114 export const generateUUID
= (): `${string}-${string}-${string}-${string}-${string}` => {
118 export const validateUUID
= (
119 uuid
: `${string}-${string}-${string}-${string}-${string}`
120 ): uuid
is `${string}-${string}-${string}-${string}-${string}` => {
121 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
)
124 export const sleep
= async (milliSeconds
: number): Promise
<NodeJS
.Timeout
> => {
125 return await new Promise
<NodeJS
.Timeout
>(resolve
=>
126 setTimeout(resolve
as () => void, milliSeconds
)
130 export const formatDurationMilliSeconds
= (duration
: number): string => {
131 duration
= convertToInt(duration
)
133 throw new RangeError('Duration cannot be negative')
135 const days
= Math.floor(duration
/ (24 * 3600 * 1000))
136 const hours
= Math.floor(millisecondsToHours(duration
) - days
* 24)
137 const minutes
= Math.floor(
138 millisecondsToMinutes(duration
) - days
* 24 * 60 - hoursToMinutes(hours
)
140 const seconds
= Math.floor(
141 millisecondsToSeconds(duration
) -
143 hoursToSeconds(hours
) -
144 minutesToSeconds(minutes
)
146 if (days
=== 0 && hours
=== 0 && minutes
=== 0 && seconds
=== 0) {
147 return formatDuration({ seconds
}, { zero
: true })
149 return formatDuration({
157 export const formatDurationSeconds
= (duration
: number): string => {
158 return formatDurationMilliSeconds(secondsToMilliseconds(duration
))
161 // More efficient time validation function than the one provided by date-fns
162 export const isValidDate
= (date
: Date | number | undefined): date
is Date | number => {
163 if (typeof date
=== 'number') {
164 return !Number.isNaN(date
)
165 } else if (isDate(date
)) {
166 return !Number.isNaN(date
.getTime())
171 export const convertToDate
= (
172 value
: Date | null | number | string | undefined
173 ): Date | undefined => {
180 if (typeof value
=== 'string' || typeof value
=== 'number') {
181 const valueToDate
= new Date(value
)
182 if (Number.isNaN(valueToDate
.getTime())) {
183 throw new Error(`Cannot convert to date: '${value.toString()}'`)
189 export const convertToInt
= (value
: unknown
): number => {
193 if (Number.isSafeInteger(value
)) {
194 return value
as number
196 if (typeof value
=== 'number') {
197 return Math.trunc(value
)
199 let changedValue
: number = value
as number
200 if (typeof value
=== 'string') {
201 changedValue
= Number.parseInt(value
)
203 if (Number.isNaN(changedValue
)) {
204 // eslint-disable-next-line @typescript-eslint/no-base-to-string
205 throw new Error(`Cannot convert to integer: '${value.toString()}'`)
210 export const convertToFloat
= (value
: unknown
): number => {
214 let changedValue
: number = value
as number
215 if (typeof value
=== 'string') {
216 changedValue
= Number.parseFloat(value
)
218 if (Number.isNaN(changedValue
)) {
219 // eslint-disable-next-line @typescript-eslint/no-base-to-string
220 throw new Error(`Cannot convert to float: '${value.toString()}'`)
225 export const convertToBoolean
= (value
: unknown
): boolean => {
229 if (typeof value
=== 'boolean') {
231 } else if (typeof value
=== 'string' && (value
.toLowerCase() === 'true' || value
=== '1')) {
233 } else if (typeof value
=== 'number' && value
=== 1) {
240 export const getRandomFloat
= (max
= Number.MAX_VALUE
, min
= 0): number => {
242 throw new RangeError('Invalid interval')
244 if (max
- min
=== Number.POSITIVE_INFINITY
) {
245 throw new RangeError('Invalid interval')
247 return (randomBytes(4).readUInt32LE() / 0xffffffff) * (max
- min
) + min
251 * Rounds the given number to the given scale.
252 * The rounding is done using the "round half away from zero" method.
253 * @param numberValue - The number to round.
254 * @param scale - The scale to round to.
255 * @returns The rounded number.
257 export const roundTo
= (numberValue
: number, scale
: number): number => {
258 const roundPower
= 10 ** scale
259 return Math.round(numberValue
* roundPower
* (1 + Number.EPSILON
)) / roundPower
262 export const getRandomFloatRounded
= (max
= Number.MAX_VALUE
, min
= 0, scale
= 2): number => {
264 return roundTo(getRandomFloat(max
, min
), scale
)
266 return roundTo(getRandomFloat(max
), scale
)
269 export const getRandomFloatFluctuatedRounded
= (
271 fluctuationPercent
: number,
274 if (fluctuationPercent
< 0 || fluctuationPercent
> 100) {
275 throw new RangeError(
276 `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent.toString()}`
279 if (fluctuationPercent
=== 0) {
280 return roundTo(staticValue
, scale
)
282 const fluctuationRatio
= fluctuationPercent
/ 100
283 return getRandomFloatRounded(
284 staticValue
* (1 + fluctuationRatio
),
285 staticValue
* (1 - fluctuationRatio
),
290 export const extractTimeSeriesValues
= (timeSeries
: CircularBuffer
<TimestampedData
>): number[] => {
291 return (timeSeries
.toArray() as TimestampedData
[]).map(timeSeriesItem
=> timeSeriesItem
.value
)
294 export const clone
= <T
>(object
: T
): T
=> {
295 return structuredClone
<T
>(object
)
298 type AsyncFunctionType
<A
extends unknown
[], R
> = (...args
: A
) => PromiseLike
<R
>
301 * Detects whether the given value is an asynchronous function or not.
302 * @param fn - Unknown value.
303 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
306 export const isAsyncFunction
= (fn
: unknown
): fn
is AsyncFunctionType
<unknown
[], unknown
> => {
307 // eslint-disable-next-line @typescript-eslint/no-empty-function
308 return fn
?.constructor
=== (async () => {}).constructor
311 export const isCFEnvironment
= (): boolean => {
312 return env
.VCAP_APPLICATION
!= null
315 declare const nonEmptyString
: unique symbol
316 type NonEmptyString
= string & { [nonEmptyString
]: true }
317 export const isNotEmptyString
= (value
: unknown
): value
is NonEmptyString
=> {
318 return typeof value
=== 'string' && value
.trim().length
> 0
321 export const isNotEmptyArray
= <T
>(
323 ): value
is NonEmptyArray
<T
> | ReadonlyNonEmptyArray
<T
> => {
324 return Array.isArray(value
) && value
.length
> 0
327 export const insertAt
= (str
: string, subStr
: string, pos
: number): string =>
328 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`
331 * Computes the retry delay in milliseconds using an exponential backoff algorithm.
332 * @param retryNumber - the number of retries that have already been attempted
333 * @param delayFactor - the base delay factor in milliseconds
334 * @returns delay in milliseconds
336 export const exponentialDelay
= (retryNumber
= 0, delayFactor
= 100): number => {
337 const delay
= 2 ** retryNumber
* delayFactor
338 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
339 return delay
+ randomSum
343 * Generates a cryptographically secure random number in the [0,1[ range
344 * @returns A number in the [0,1[ range
346 export const secureRandom
= (): number => {
347 return getRandomValues(new Uint32Array(1))[0] / 0x100000000
350 export const JSONStringify
= <
351 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
354 | Map
<string, Record
<string, unknown
>>
355 | Record
<string, unknown
>[]
356 | Set
<Record
<string, unknown
>>
359 space
?: number | string,
360 mapFormat
?: MapStringifyFormat
362 return JSON
.stringify(
364 (_
, value
: Record
<string, unknown
>) => {
365 if (value
instanceof Map
) {
367 case MapStringifyFormat
.object
:
369 ...Object.fromEntries
<Map
<string, Record
<string, unknown
>>>(value
.entries()),
371 case MapStringifyFormat
.array
:
375 } else if (value
instanceof Set
) {
376 return [...value
] as Record
<string, unknown
>[]
385 * Converts websocket error code to human readable string message
386 * @param code - websocket error code
387 * @returns human readable string message
389 export const getWebSocketCloseEventStatusString
= (code
: number): string => {
390 if (code
>= 0 && code
<= 999) {
392 } else if (code
>= 1016) {
394 return '(For WebSocket standard)'
395 } else if (code
<= 2999) {
396 return '(For WebSocket extensions)'
397 } else if (code
<= 3999) {
398 return '(For libraries and frameworks)'
399 } else if (code
<= 4999) {
400 return '(For applications)'
404 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
405 WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
] != null
407 return WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
]
412 export const isArraySorted
= <T
>(array
: T
[], compareFn
: (a
: T
, b
: T
) => number): boolean => {
413 if (array
.length
<= 1) {
416 for (let index
= 0; index
< array
.length
- 1; ++index
) {
417 if (compareFn(array
[index
], array
[index
+ 1]) > 0) {
424 export const queueMicrotaskErrorThrowing
= (error
: Error): void => {
425 queueMicrotask(() => {