1 import crypto from
'node:crypto';
2 import util from
'node:util';
4 import clone from
'just-clone';
6 import { Constants
} from
'./Constants';
7 import { WebSocketCloseEventStatusString
} from
'../types';
10 private constructor() {
11 // This is intentional
14 public static logPrefix
= (prefixString
= ''): string => {
15 return `${new Date().toLocaleString()}${prefixString}`;
18 public static generateUUID(): string {
19 return crypto
.randomUUID();
22 public static validateUUID(uuid
: string): boolean {
23 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(
28 public static async sleep(milliSeconds
: number): Promise
<NodeJS
.Timeout
> {
29 return new Promise((resolve
) => setTimeout(resolve
as () => void, milliSeconds
));
32 public static formatDurationMilliSeconds(duration
: number): string {
33 duration
= Utils
.convertToInt(duration
);
34 const hours
= Math.floor(duration
/ (3600 * 1000));
35 const minutes
= Math.floor((duration
/ 1000 - hours
* 3600) / 60);
36 const seconds
= duration
/ 1000 - hours
* 3600 - minutes
* 60;
37 let hoursStr
= hours
.toString();
38 let minutesStr
= minutes
.toString();
39 let secondsStr
= seconds
.toString();
42 hoursStr
= `0${hours.toString()}`;
45 minutesStr
= `0${minutes.toString()}`;
48 secondsStr
= `0${seconds.toString()}`;
50 return `${hoursStr}:${minutesStr}:${secondsStr.substring(0, 6)}`;
53 public static formatDurationSeconds(duration
: number): string {
54 return Utils
.formatDurationMilliSeconds(duration
* 1000);
57 public static convertToDate(
58 value
: Date | string | number | null | undefined
59 ): Date | null | undefined {
60 if (Utils
.isNullOrUndefined(value
)) {
61 return value
as null | undefined;
63 if (value
instanceof Date) {
66 if (Utils
.isString(value
) || typeof value
=== 'number') {
67 return new Date(value
);
72 public static convertToInt(value
: unknown
): number {
76 let changedValue
: number = value
as number;
77 if (Number.isSafeInteger(value
)) {
78 return value
as number;
80 if (typeof value
=== 'number') {
81 return Math.trunc(value
);
83 if (Utils
.isString(value
)) {
84 changedValue
= parseInt(value
as string);
86 if (isNaN(changedValue
)) {
87 throw new Error(`Cannot convert to integer: ${value.toString()}`);
92 public static convertToFloat(value
: unknown
): number {
96 let changedValue
: number = value
as number;
97 if (Utils
.isString(value
)) {
98 changedValue
= parseFloat(value
as string);
100 if (isNaN(changedValue
)) {
101 throw new Error(`Cannot convert to float: ${value.toString()}`);
106 public static convertToBoolean(value
: unknown
): boolean {
110 if (typeof value
=== 'boolean') {
113 Utils
.isString(value
) &&
114 ((value
as string).toLowerCase() === 'true' || value
=== '1')
117 } else if (typeof value
=== 'number' && value
=== 1) {
124 public static getRandomFloat(max
= Number.MAX_VALUE
, min
= 0): number {
126 throw new RangeError('Invalid interval');
128 if (max
- min
=== Infinity) {
129 throw new RangeError('Invalid interval');
131 return (crypto
.randomBytes(4).readUInt32LE() / 0xffffffff) * (max
- min
) + min
;
134 public static getRandomInteger(max
= Constants
.MAX_RANDOM_INTEGER
, min
= 0): number {
135 max
= Math.floor(max
);
136 if (!Utils
.isNullOrUndefined(min
) && min
!== 0) {
137 min
= Math.ceil(min
);
138 return Math.floor(crypto
.randomInt(min
, max
+ 1));
140 return Math.floor(crypto
.randomInt(max
+ 1));
144 * Rounds the given number to the given scale.
145 * The rounding is done using the "round half away from zero" method.
147 * @param numberValue - The number to round.
148 * @param scale - The scale to round to.
149 * @returns The rounded number.
151 public static roundTo(numberValue
: number, scale
: number): number {
152 const roundPower
= Math.pow(10, scale
);
153 return Math.round(numberValue
* roundPower
* (1 + Number.EPSILON
)) / roundPower
;
156 public static getRandomFloatRounded(max
= Number.MAX_VALUE
, min
= 0, scale
= 2): number {
158 return Utils
.roundTo(Utils
.getRandomFloat(max
, min
), scale
);
160 return Utils
.roundTo(Utils
.getRandomFloat(max
), scale
);
163 public static getRandomFloatFluctuatedRounded(
165 fluctuationPercent
: number,
168 if (fluctuationPercent
< 0 || fluctuationPercent
> 100) {
169 throw new RangeError(
170 `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent}`
173 if (fluctuationPercent
=== 0) {
174 return Utils
.roundTo(staticValue
, scale
);
176 const fluctuationRatio
= fluctuationPercent
/ 100;
177 return Utils
.getRandomFloatRounded(
178 staticValue
* (1 + fluctuationRatio
),
179 staticValue
* (1 - fluctuationRatio
),
184 public static isObject(item
: unknown
): boolean {
186 Utils
.isNullOrUndefined(item
) === false &&
187 typeof item
=== 'object' &&
188 Array.isArray(item
) === false
192 public static cloneObject
<T
extends object
>(object
: T
): T
{
193 return clone
<T
>(object
);
196 public static hasOwnProp(object
: unknown
, property
: PropertyKey
): boolean {
197 return Utils
.isObject(object
) && Object.hasOwn(object
as object
, property
);
200 public static isCFEnvironment(): boolean {
201 return !Utils
.isNullOrUndefined(process
.env
.VCAP_APPLICATION
);
204 public static isIterable
<T
>(obj
: T
): boolean {
205 return !Utils
.isNullOrUndefined(obj
) ? typeof obj
[Symbol
.iterator
] === 'function' : false;
208 public static isString(value
: unknown
): boolean {
209 return typeof value
=== 'string';
212 public static isEmptyString(value
: unknown
): boolean {
214 Utils
.isNullOrUndefined(value
) ||
215 (Utils
.isString(value
) && (value
as string).trim().length
=== 0)
219 public static isNotEmptyString(value
: unknown
): boolean {
220 return Utils
.isString(value
) && (value
as string).trim().length
> 0;
223 public static isUndefined(value
: unknown
): boolean {
224 return value
=== undefined;
227 public static isNullOrUndefined(value
: unknown
): boolean {
228 // eslint-disable-next-line eqeqeq, no-eq-null
229 return value
== null;
232 public static isEmptyArray(object
: unknown
): boolean {
233 return Array.isArray(object
) && object
.length
=== 0;
236 public static isNotEmptyArray(object
: unknown
): boolean {
237 return Array.isArray(object
) && object
.length
> 0;
240 public static isEmptyObject(obj
: object
): boolean {
241 if (obj
?.constructor
!== Object) {
244 // Iterates over the keys of an object, if
245 // any exist, return false.
246 for (const _
in obj
) {
252 public static insertAt
= (str
: string, subStr
: string, pos
: number): string =>
253 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`;
256 * Computes the retry delay in milliseconds using an exponential backoff algorithm.
258 * @param retryNumber - the number of retries that have already been attempted
259 * @returns delay in milliseconds
261 public static exponentialDelay(retryNumber
= 0, maxDelayRatio
= 0.2): number {
262 const delay
= Math.pow(2, retryNumber
) * 100;
263 const randomSum
= delay
* maxDelayRatio
* Utils
.secureRandom(); // 0-20% of the delay
264 return delay
+ randomSum
;
267 public static isPromisePending(promise
: Promise
<unknown
>): boolean {
268 return util
.inspect(promise
).includes('pending');
271 public static async promiseWithTimeout
<T
>(
275 timeoutCallback
: () => void = () => {
276 /* This is intentional */
279 // Create a timeout promise that rejects in timeout milliseconds
280 const timeoutPromise
= new Promise
<never>((_
, reject
) => {
282 if (Utils
.isPromisePending(promise
)) {
284 // FIXME: The original promise shall be canceled
286 reject(timeoutError
);
290 // Returns a race between timeout promise and the passed promise
291 return Promise
.race
<T
>([promise
, timeoutPromise
]);
295 * Generates a cryptographically secure random number in the [0,1[ range
299 public static secureRandom(): number {
300 return crypto
.randomBytes(4).readUInt32LE() / 0x100000000;
303 public static JSONStringifyWithMapSupport(
304 obj
: Record
<string, unknown
> | Record
<string, unknown
>[] | Map
<unknown
, unknown
>,
307 return JSON
.stringify(
309 (key
, value
: Record
<string, unknown
>) => {
310 if (value
instanceof Map
) {
323 * Converts websocket error code to human readable string message
325 * @param code - websocket error code
326 * @returns human readable string message
328 public static getWebSocketCloseEventStatusString(code
: number): string {
329 if (code
>= 0 && code
<= 999) {
331 } else if (code
>= 1016) {
333 return '(For WebSocket standard)';
334 } else if (code
<= 2999) {
335 return '(For WebSocket extensions)';
336 } else if (code
<= 3999) {
337 return '(For libraries and frameworks)';
338 } else if (code
<= 4999) {
339 return '(For applications)';
342 if (!Utils
.isUndefined(WebSocketCloseEventStatusString
[code
])) {
343 return WebSocketCloseEventStatusString
[code
] as string;