1 import { randomBytes
, randomInt
, randomUUID
, webcrypto
} from
'node:crypto';
2 import { env
} from
'node:process';
3 import { inspect
} from
'node:util';
11 millisecondsToMinutes
,
12 millisecondsToSeconds
,
14 secondsToMilliseconds
,
17 import { Constants
} from
'./Constants';
18 import { type TimestampedData
, WebSocketCloseEventStatusString
} from
'../types';
20 export const logPrefix
= (prefixString
= ''): string => {
21 return `${new Date().toLocaleString()}${prefixString}`;
24 export const generateUUID
= (): string => {
28 export const validateUUID
= (uuid
: string): boolean => {
29 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(
34 export const sleep
= async (milliSeconds
: number): Promise
<NodeJS
.Timeout
> => {
35 return new Promise
<NodeJS
.Timeout
>((resolve
) => setTimeout(resolve
as () => void, milliSeconds
));
38 export const formatDurationMilliSeconds
= (duration
: number): string => {
39 duration
= convertToInt(duration
);
41 throw new RangeError('Duration cannot be negative');
43 const days
= Math.floor(duration
/ (24 * 3600 * 1000));
44 const hours
= Math.floor(millisecondsToHours(duration
) - days
* 24);
45 const minutes
= Math.floor(
46 millisecondsToMinutes(duration
) - days
* 24 * 60 - hoursToMinutes(hours
),
48 const seconds
= Math.floor(
49 millisecondsToSeconds(duration
) -
51 hoursToSeconds(hours
) -
52 minutesToSeconds(minutes
),
54 if (days
=== 0 && hours
=== 0 && minutes
=== 0 && seconds
=== 0) {
55 return formatDuration({ seconds
}, { zero
: true });
57 return formatDuration({
65 export const formatDurationSeconds
= (duration
: number): string => {
66 return formatDurationMilliSeconds(secondsToMilliseconds(duration
));
69 // More efficient time validation function than the one provided by date-fns
70 export const isValidTime
= (date
: unknown
): boolean => {
71 if (typeof date
=== 'number') {
73 } else if (isDate(date
)) {
74 return !isNaN((date
as Date).getTime());
79 export const convertToDate
= (value
: Date | string | number | undefined): Date | undefined => {
80 if (isNullOrUndefined(value
)) {
81 return value
as undefined;
86 if (isString(value
) || typeof value
=== 'number') {
87 const valueToDate
= new Date(value
as string | number);
88 if (isNaN(valueToDate
.getTime())) {
89 throw new Error(`Cannot convert to date: '${value as string | number}'`);
95 export const convertToInt
= (value
: unknown
): number => {
99 let changedValue
: number = value
as number;
100 if (Number.isSafeInteger(value
)) {
101 return value
as number;
103 if (typeof value
=== 'number') {
104 return Math.trunc(value
);
106 if (isString(value
)) {
107 changedValue
= parseInt(value
as string);
109 if (isNaN(changedValue
)) {
110 throw new Error(`Cannot convert to integer: '${String(value)}'`);
115 export const convertToFloat
= (value
: unknown
): number => {
119 let changedValue
: number = value
as number;
120 if (isString(value
)) {
121 changedValue
= parseFloat(value
as string);
123 if (isNaN(changedValue
)) {
124 throw new Error(`Cannot convert to float: '${String(value)}'`);
129 export const convertToBoolean
= (value
: unknown
): boolean => {
133 if (typeof value
=== 'boolean') {
135 } else if (isString(value
) && ((value
as string).toLowerCase() === 'true' || value
=== '1')) {
137 } else if (typeof value
=== 'number' && value
=== 1) {
144 export const getRandomFloat
= (max
= Number.MAX_VALUE
, min
= 0): number => {
146 throw new RangeError('Invalid interval');
148 if (max
- min
=== Infinity) {
149 throw new RangeError('Invalid interval');
151 return (randomBytes(4).readUInt32LE() / 0xffffffff) * (max
- min
) + min
;
154 export const getRandomInteger
= (max
= Constants
.MAX_RANDOM_INTEGER
, min
= 0): number => {
155 max
= Math.floor(max
);
156 if (!isNullOrUndefined(min
) && min
!== 0) {
157 min
= Math.ceil(min
);
158 return Math.floor(randomInt(min
, max
+ 1));
160 return Math.floor(randomInt(max
+ 1));
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
: Array<TimestampedData
>): number[] => {
205 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
208 export const isObject
= (item
: unknown
): boolean => {
210 isNullOrUndefined(item
) === false && typeof item
=== 'object' && Array.isArray(item
) === false
222 | { [key
: string]: CloneableData
};
224 type FormatKey
= (key
: string) => string;
226 const deepClone
= <I
extends CloneableData
, O
extends CloneableData
= I
>(
228 formatKey
?: FormatKey
,
229 refs
: Map
<I
, O
> = new Map
<I
, O
>(),
231 const ref
= refs
.get(value
);
232 if (ref
!== undefined) {
235 if (Array.isArray(value
)) {
236 const clone
: CloneableData
[] = [];
237 refs
.set(value
, clone
as O
);
238 for (let i
= 0; i
< value
.length
; i
++) {
239 clone
[i
] = deepClone(value
[i
], formatKey
, refs
);
243 if (value
instanceof Date) {
244 return new Date(value
.valueOf()) as O
;
246 if (typeof value
!== 'object' || value
=== null) {
247 return value
as unknown
as O
;
249 const clone
: Record
<string, CloneableData
> = {};
250 refs
.set(value
, clone
as O
);
251 for (const key
of Object.keys(value
)) {
252 clone
[typeof formatKey
=== 'function' ? formatKey(key
) : key
] = deepClone(
261 export const cloneObject
= <T
>(object
: T
): T
=> {
262 return deepClone(object
as CloneableData
) as T
;
265 export const hasOwnProp
= (object
: unknown
, property
: PropertyKey
): boolean => {
266 return isObject(object
) && Object.hasOwn(object
as object
, property
);
269 export const isCFEnvironment
= (): boolean => {
270 return !isNullOrUndefined(env
.VCAP_APPLICATION
);
273 export const isIterable
= <T
>(obj
: T
): boolean => {
274 return !isNullOrUndefined(obj
) ? typeof obj
[Symbol
.iterator
as keyof T
] === 'function' : false;
277 const isString
= (value
: unknown
): boolean => {
278 return typeof value
=== 'string';
281 export const isEmptyString
= (value
: unknown
): boolean => {
282 return isNullOrUndefined(value
) || (isString(value
) && (value
as string).trim().length
=== 0);
285 export const isNotEmptyString
= (value
: unknown
): boolean => {
286 return isString(value
) && (value
as string).trim().length
> 0;
289 export const isUndefined
= (value
: unknown
): boolean => {
290 return value
=== undefined;
293 export const isNullOrUndefined
= (value
: unknown
): boolean => {
294 // eslint-disable-next-line eqeqeq, no-eq-null
295 return value
== null;
298 export const isEmptyArray
= (object
: unknown
): boolean => {
299 return Array.isArray(object
) && object
.length
=== 0;
302 export const isNotEmptyArray
= (object
: unknown
): boolean => {
303 return Array.isArray(object
) && object
.length
> 0;
306 export const isEmptyObject
= (obj
: object
): boolean => {
307 if (obj
?.constructor
!== Object) {
310 // Iterates over the keys of an object, if
311 // any exist, return false.
312 for (const _
in obj
) {
318 export const insertAt
= (str
: string, subStr
: string, pos
: number): string =>
319 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`;
322 * Computes the retry delay in milliseconds using an exponential backoff algorithm.
324 * @param retryNumber - the number of retries that have already been attempted
325 * @param delayFactor - the base delay factor in milliseconds
326 * @returns delay in milliseconds
328 export const exponentialDelay
= (retryNumber
= 0, delayFactor
= 100): number => {
329 const delay
= Math.pow(2, retryNumber
) * delayFactor
;
330 const randomSum
= delay
* 0.2 * secureRandom(); // 0-20% of the delay
331 return delay
+ randomSum
;
334 const isPromisePending
= (promise
: Promise
<unknown
>): boolean => {
335 return inspect(promise
).includes('pending');
338 export const promiseWithTimeout
= async <T
>(
342 timeoutCallback
: () => void = () => {
343 /* This is intentional */
346 // Creates a timeout promise that rejects in timeout milliseconds
347 const timeoutPromise
= new Promise
<never>((_
, reject
) => {
349 if (isPromisePending(promise
)) {
351 // FIXME: The original promise shall be canceled
353 reject(timeoutError
);
357 // Returns a race between timeout promise and the passed promise
358 return Promise
.race
<T
>([promise
, timeoutPromise
]);
362 * Generates a cryptographically secure random number in the [0,1[ range
364 * @returns A number in the [0,1[ range
366 export const secureRandom
= (): number => {
367 return webcrypto
.getRandomValues(new Uint32Array(1))[0] / 0x100000000;
370 export const JSONStringifyWithMapSupport
= (
371 obj
: Record
<string, unknown
> | Record
<string, unknown
>[] | Map
<unknown
, unknown
>,
374 return JSON
.stringify(
376 (_
, value
: Record
<string, unknown
>) => {
377 if (value
instanceof Map
) {
390 * Converts websocket error code to human readable string message
392 * @param code - websocket error code
393 * @returns human readable string message
395 export const getWebSocketCloseEventStatusString
= (code
: number): string => {
396 if (code
>= 0 && code
<= 999) {
398 } else if (code
>= 1016) {
400 return '(For WebSocket standard)';
401 } else if (code
<= 2999) {
402 return '(For WebSocket extensions)';
403 } else if (code
<= 3999) {
404 return '(For libraries and frameworks)';
405 } else if (code
<= 4999) {
406 return '(For applications)';
411 WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
],
414 return WebSocketCloseEventStatusString
[code
as keyof
typeof WebSocketCloseEventStatusString
];
419 export const isArraySorted
= <T
>(array
: T
[], compareFn
: (a
: T
, b
: T
) => number): boolean => {
420 for (let index
= 0; index
< array
.length
- 1; ++index
) {
421 if (compareFn(array
[index
], array
[index
+ 1]) > 0) {
428 // eslint-disable-next-line @typescript-eslint/no-explicit-any
429 export const once
= <T
, A
extends any[], R
>(
430 fn
: (...args
: A
) => R
,
432 ): ((...args
: A
) => R
) => {
434 return (...args
: A
) => {
436 result
= fn
.apply
<T
, A
, R
>(context
, args
);
437 (fn
as unknown
as undefined) = (context
as unknown
as undefined) = undefined;
443 export const min
= (...args
: number[]): number =>
444 args
.reduce((minimum
, num
) => (minimum
< num
? minimum
: num
), Infinity);
446 export const max
= (...args
: number[]): number =>
447 args
.reduce((maximum
, num
) => (maximum
> num
? maximum
: num
), -Infinity);