@ti-engine/core 1.6.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +383 -364
  2. package/LICENSE.md +321 -321
  3. package/README.md +597 -548
  4. package/bin/localization/labels.json +122 -122
  5. package/bin/settings.json +41 -41
  6. package/bin/start-instance.js +164 -156
  7. package/components/auditing.js +191 -191
  8. package/components/connection-observer.js +72 -72
  9. package/components/definitions.types.js +248 -248
  10. package/components/exchange/default/default-message-exchange.js +136 -136
  11. package/components/exchange/default/default-message-receiver.js +101 -101
  12. package/components/exchange/default/default-message-sender.js +100 -100
  13. package/components/exchange/message-dispatcher.js +168 -168
  14. package/components/exchange/message-exchange.js +449 -449
  15. package/components/exchange/message-handler.js +235 -234
  16. package/components/exchange/message-memory-cache.js +190 -190
  17. package/components/exchange/message-observer.js +126 -126
  18. package/components/exchange/message-receiver.js +181 -181
  19. package/components/exchange/message-sender.js +143 -143
  20. package/components/exchange/message-tracer.js +212 -212
  21. package/components/service-caller.js +370 -370
  22. package/components/service-consumer.js +131 -131
  23. package/components/service-executor.js +278 -278
  24. package/components/service-instance.js +316 -316
  25. package/components/service-provider.js +251 -251
  26. package/integrations/redis-integration.js +591 -591
  27. package/package.json +89 -90
  28. package/utils/cache.js +772 -772
  29. package/utils/config.js +103 -103
  30. package/utils/exceptions.js +368 -368
  31. package/utils/localization.js +298 -298
  32. package/utils/logger.js +82 -82
  33. package/utils/tools.js +632 -632
package/utils/tools.js CHANGED
@@ -1,633 +1,633 @@
1
- /*
2
- * The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
3
- * Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
4
- * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
- * You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7
- */
8
-
9
- const _ = require( "lodash" );
10
- const crypto = require( "node:crypto" );
11
-
12
- /**
13
- * Used to generate and return new UUID.
14
- *
15
- * @method
16
- * @returns {string}
17
- * @public
18
- */
19
- module.exports.getUUID = () => {
20
- return crypto.randomUUID( { disableEntropyCache: true } );
21
- };
22
-
23
- /**
24
- * Used to deep-freeze an object.
25
- *
26
- * @method
27
- * @param {Object} object
28
- * @param {WeakSet} [seen]
29
- * @returns {Object}
30
- * @public
31
- */
32
- module.exports.deepFreeze = ( object, seen = new WeakSet() ) => {
33
- if ( object === null || typeof object !== "object" || seen.has( object ) ) {
34
- return object;
35
- } else {
36
- seen.add( object );
37
- _.forOwn( object, ( value ) => {
38
- module.exports.deepFreeze( value, seen );
39
- } );
40
- return Object.freeze( object );
41
- }
42
- };
43
-
44
- /**
45
- * Used to create a custom Enum list.
46
- *
47
- * @method
48
- * @param {Object} seed
49
- * @returns {Object} This is a {@link TiEnum} object. Setting the proper reference here would unfortunately break IDE support.
50
- * @public
51
- */
52
- module.exports.enum = ( seed ) => {
53
- const enumObject = Object.create( null );
54
- const properties = Object.create( null );
55
- const reserved = new Set( [ "properties", "name", "description", "contains", "__proto__", "prototype", "constructor" ] );
56
-
57
- _.forOwn( seed, ( value, key ) => {
58
- if ( !reserved.has( key ) ) {
59
- if ( Array.isArray( value ) ) {
60
- enumObject[ key ] = value[ 0 ];
61
- properties[ value[ 0 ] ] = {
62
- value: value[ 0 ],
63
- name: value[ 1 ],
64
- description: value[ 2 ]
65
- };
66
- } else {
67
- enumObject[ key ] = value;
68
- properties[ value ] = {
69
- value: value,
70
- name: key
71
- };
72
- }
73
- }
74
- } );
75
- Object.values( properties ).forEach( Object.freeze );
76
- Object.freeze( properties );
77
-
78
- /**
79
- * Used to get the name of an {@link TiEnumValue} if such value exists.
80
- *
81
- * @method
82
- * @param {number|string} value
83
- * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a name defined.
84
- * @returns {string|undefined}
85
- * @public
86
- */
87
- const name = ( value, placeholder = undefined ) => {
88
- return ( properties[ value ] ) ? properties[ value ].name : placeholder;
89
- };
90
-
91
- /**
92
- * Used to get the description of an {@link TiEnumValue} if such value exists.
93
- *
94
- * @method
95
- * @param {number|string} value
96
- * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a description defined.
97
- * @returns {string|undefined}
98
- * @public
99
- */
100
- const description = ( value, placeholder = undefined ) => {
101
- if ( !properties[ value ] ) {
102
- return placeholder;
103
- } else {
104
- return ( properties[ value ].description !== undefined ) ? properties[ value ].description : placeholder;
105
- }
106
- };
107
-
108
- /**
109
- * Used to check if the provided value is contained in the provided {@link TiEnum} list.
110
- *
111
- * @method
112
- * @param {number|string} value
113
- * @returns {boolean}
114
- * @public
115
- */
116
- const contains = ( value ) => {
117
- return !!( properties[ value ] );
118
- };
119
-
120
- Object.defineProperties( enumObject, {
121
- contains: {
122
- enumerable: false,
123
- configurable: false,
124
- writable: false,
125
- value: contains
126
- },
127
- description: {
128
- enumerable: false,
129
- configurable: false,
130
- writable: false,
131
- value: description
132
- },
133
- name: {
134
- enumerable: false,
135
- configurable: false,
136
- writable: false,
137
- value: name
138
- },
139
- properties: {
140
- enumerable: false,
141
- configurable: false,
142
- writable: false,
143
- value: properties
144
- }
145
- } );
146
- Object.freeze( enumObject );
147
-
148
- return enumObject;
149
- };
150
-
151
- /**
152
- * Used to get the name of an {@link TiEnum} value if such exists.
153
- *
154
- * @method
155
- * @deprecated Use the 'name' property of the provided {@link TiEnum} instead.
156
- * @param {TiEnum} enumList
157
- * @param {number|string} enumValue
158
- * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a name defined.
159
- * @returns {string|undefined}
160
- * @public
161
- */
162
- module.exports.getEnumName = ( enumList, enumValue, placeholder = undefined ) => {
163
- return ( enumList.properties[ enumValue ] ) ? enumList.properties[ enumValue ].name : placeholder;
164
- };
165
-
166
- /**
167
- * Convert an Error to a JSON object.
168
- * <br/>
169
- * NOTE: If the value provided is not an error, then it will just be cloned.
170
- *
171
- * @method
172
- * @param {Error} value
173
- * @returns {Object}
174
- * @public
175
- */
176
- module.exports.errorToJSON = ( value ) => {
177
- let error = {};
178
-
179
- if ( value instanceof Error ) {
180
- Object.getOwnPropertyNames( value ).forEach( ( key ) => {
181
- error[ key ] = value[ key ];
182
- } );
183
- } else {
184
- error = _.cloneDeep( value );
185
- }
186
-
187
- return error;
188
- };
189
-
190
- /**
191
- * Used to parse a value and return its boolean representation (if possible).
192
- *
193
- * @param {*} value
194
- * @returns {boolean}
195
- * @public
196
- */
197
- module.exports.toBool = ( value ) => {
198
- let result = true;
199
- let regexp = /^false$|^0$|^no$/i;
200
-
201
- if ( !value || regexp.test( value ) || value === "N" || value === "0" || ( _.isObjectLike( value ) && _.size( value ) === 0 ) ) {
202
- result = false;
203
- }
204
-
205
- return result;
206
- };
207
-
208
- /**
209
- * Used to fetch only the unique values from the provided array.
210
- * <br/>
211
- * Note: For objects and arrays, uniqueness is determined by reference equality. Primitives are compared by their value.
212
- *
213
- * @method
214
- * @param {Array} array
215
- * @returns {Array}
216
- * @throws {TypeError} If the provided parameter is not an array
217
- * @public
218
- */
219
- module.exports.arrayUniques = ( array ) => {
220
- if ( !Array.isArray( array ) ) {
221
- throw new TypeError( "Expected an array" );
222
- }
223
- return [ ...new Set( array ) ];
224
- };
225
-
226
- /**
227
- * Will return a UTC date string in format YYYY-MM-DD from the provided date.
228
- *
229
- * @method
230
- * @param {Date} date
231
- * @returns {string}
232
- * @public
233
- */
234
- module.exports.getUTCDateString = ( date ) => {
235
- const year = date.getUTCFullYear();
236
- const month = String( date.getUTCMonth() + 1 ).padStart( 2, "0" );
237
- const day = String( date.getUTCDate() ).padStart( 2, "0" );
238
- return `${ year }-${ month }-${ day }`;
239
- };
240
-
241
- /**
242
- * Will return a UTC time string in format hh:mm:ss, or hh:mm:ss.MMM when useMilliseconds is true.
243
- *
244
- * @method
245
- * @param {Date} date
246
- * @param {boolean} [useMilliseconds=false]
247
- * @returns {string}
248
- * @public
249
- */
250
- module.exports.getUTCTimeString = ( date, useMilliseconds = false ) => {
251
- const hours = String( date.getUTCHours() ).padStart( 2, "0" );
252
- const minutes = String( date.getUTCMinutes() ).padStart( 2, "0" );
253
- const seconds = String( date.getUTCSeconds() ).padStart( 2, "0" );
254
- const milliseconds = useMilliseconds ? `${ String( date.getUTCMilliseconds() ).padStart( 3, "0" ) }` : "";
255
- return `${ hours }:${ minutes }:${ seconds }${ useMilliseconds ? `.${ milliseconds }` : "" }`;
256
- };
257
-
258
- /**
259
- * Used to remove any circular dependencies from JSON objects.
260
- * <br/>
261
- * NOTE: Original file from here - https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
262
- *
263
- * Make a deep copy of an object or array, assuring that there is at most one instance of each object or array in the
264
- * resulting structure. The duplicate references (which might be forming cycles) are replaced with an object of the
265
- * form of {"$ref": PATH} where the PATH is a JSONPath string that locates the first occurrence.
266
- *
267
- * So,
268
- *
269
- * var a = [];
270
- * a[0] = a;
271
- * return JSON.stringify(JSON.decycle(a));
272
- *
273
- * produces the string '[{"$ref":"$"}]'.
274
- *
275
- * If a replacer function is provided, then it will be called for each value. A replacer function receives a value
276
- * and returns a replacement value.
277
- *
278
- * JSONPath is used to locate the unique object. $ indicates the top level of the object or array. [NUMBER] or [STRING]
279
- * indicates a child element or property.
280
- *
281
- * @method
282
- * @param {Object} object
283
- * @param {function( Object ): Object} [replacer]
284
- * @returns {Object}
285
- * @public
286
- */
287
- module.exports.decycle = ( object, replacer ) => {
288
- "use strict";
289
-
290
- let objects = new WeakMap();
291
-
292
- // The derez function recurse through the object, producing the deep copy.
293
- return ( function derez( value, path ) {
294
- let oldPath; // The path of an earlier occurrence of value
295
- let newItem; // The new object or array
296
-
297
- // If a replacer function was provided, then call it to get a replacement value.
298
- if ( replacer !== undefined ) {
299
- value = replacer( value );
300
- }
301
-
302
- // typeof null === "object", so go on if this value is really an object but not
303
- // one of the weird builtin objects.
304
- if (
305
- typeof value === "object"
306
- && value !== null
307
- && !( value instanceof Boolean )
308
- && !( value instanceof Date )
309
- && !( value instanceof Number )
310
- && !( value instanceof RegExp )
311
- && !( value instanceof String )
312
- ) {
313
- // If the value is an object or array, look to see if we have already
314
- // encountered it. If so, return a {"$ref":PATH} object. This uses an
315
- // ES6 WeakMap.
316
- oldPath = objects.get( value );
317
- if ( oldPath !== undefined ) {
318
- return { $ref: oldPath };
319
- }
320
-
321
- // Otherwise, accumulate the unique value and its path.
322
- objects.set( value, path );
323
-
324
- // If it is an array, replicate the array.
325
- if ( Array.isArray( value ) ) {
326
- newItem = [];
327
- value.forEach( ( element, i ) => {
328
- newItem[ i ] = derez( element, path + "[" + i + "]" );
329
- } );
330
- } else {
331
- // If it is an object, replicate the object.
332
- newItem = {};
333
- Object.keys( value ).forEach( ( name ) => {
334
- newItem[ name ] = derez(
335
- value[ name ],
336
- path + "[" + JSON.stringify( name ) + "]"
337
- );
338
- } );
339
- }
340
- return newItem;
341
- }
342
- return value;
343
- }( object, "$" ) );
344
- };
345
-
346
- /**
347
- * Used to restore any circular dependencies from JSON objects after using the 'decycle' method.
348
- * <br/>
349
- * NOTE: Original file from here - https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
350
- *
351
- * Restore an object that was reduced by decycle. Members whose values are objects of the form of {$ref: PATH} are
352
- * replaced with references to the value found by the PATH. This will restore cycles. The object will be mutated.
353
- *
354
- * The eval function is used to locate the values described by a PATH. The root object is kept in a $ variable. A
355
- * regular expression is used to ensure that the PATH is extremely well-formed. The regexp contains nested quantifiers.
356
- * That has been known to have extremely bad performance problems on some browsers for very long strings. A PATH is
357
- * expected to be reasonably short. A PATH is allowed to belong to a very restricted subset of Goessner's JSONPath.
358
- *
359
- * So,
360
- *
361
- * var s = '[{"$ref":"$"}]';
362
- * return JSON.retrocycle(JSON.parse(s));
363
- *
364
- * produces an array containing a single element which is the array itself.
365
- *
366
- * @method
367
- * @param {Object} $
368
- * @returns {Object}
369
- * @public
370
- */
371
- module.exports.retrocycle = ( $ ) => {
372
- "use strict";
373
-
374
- // eslint-disable-next-line no-control-regex
375
- let px = /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\(?:[\\"/bfnrt]|u[0-9a-zA-Z]{4}))*")])*$/;
376
-
377
- // The rez function walks recursively through the object looking for $ref
378
- // properties. When it finds one that has a value that is a path, then it
379
- // replaces the $ref object with a reference to the value that is found by
380
- // the path.
381
- ( function rez( value ) {
382
- if ( value && typeof value === "object" ) {
383
- if ( Array.isArray( value ) ) {
384
- value.forEach( ( element, i ) => {
385
- if ( typeof element === "object" && element !== null ) {
386
- let path = element.$ref;
387
- if ( typeof path === "string" && px.test( path ) ) {
388
- value[ i ] = eval( path );
389
- } else {
390
- rez( element );
391
- }
392
- }
393
- } );
394
- } else {
395
- Object.keys( value ).forEach( ( name ) => {
396
- let item = value[ name ];
397
- if ( typeof item === "object" && item !== null ) {
398
- let path = item.$ref;
399
- if ( typeof path === "string" && px.test( path ) ) {
400
- value[ name ] = eval( path );
401
- } else {
402
- rez( item );
403
- }
404
- }
405
- } );
406
- }
407
- }
408
- }( $ ) );
409
- return $;
410
- };
411
-
412
- /**
413
- * Use this to stringify any JSON object for internal system purposes as it ensures no potential circular dependencies
414
- * will cause it to throw exception.
415
- *
416
- * @method
417
- * @param {Object} value
418
- * @returns {string|*}
419
- * @public
420
- */
421
- module.exports.stringifyJSON = ( value ) => {
422
- return _.isObjectLike( value ) ? JSON.stringify( _.toPlainObject( module.exports.decycle( value ) ) ) : value;
423
- };
424
-
425
- /**
426
- * Use this to verify if the provided string can be parsed as a JSON.
427
- *
428
- * @method
429
- * @param {string} string
430
- * @returns {boolean}
431
- * @public
432
- */
433
- module.exports.isJsonString = ( string ) => {
434
- try {
435
- JSON.parse( string );
436
- } catch {
437
- return false;
438
- }
439
- return true;
440
- };
441
-
442
- /**
443
- * Use this to parse any JSON string into a JSON object for internal system purposes as it ensures to restore any
444
- * circular dependencies obscured with 'stringifyJSON'.
445
- *
446
- * @method
447
- * @param {string} value
448
- * @returns {Object|string}
449
- * @public
450
- */
451
- module.exports.parseJSON = ( value ) => {
452
- try {
453
- let transformed = JSON.parse( value );
454
- return module.exports.retrocycle( transformed );
455
- } catch {
456
- return value;
457
- }
458
- };
459
-
460
- /**
461
- * Use this to decompose a JSON object into a sorted string. The values will be ordered alphabetically and combined with
462
- * their keys, where applicable, starting from the bottom and moving up. Null or undefined values will be ignored, and
463
- * their keys will not be included in the final string.
464
- *
465
- * @param {Object} input
466
- * @recursion
467
- * @returns {string|null}
468
- * @public
469
- */
470
- module.exports.decomposeJSON = ( input ) => {
471
- let decomposed;
472
-
473
- if ( !_.isNil( input ) ) {
474
- if ( _.isArray( input ) ) {
475
- decomposed = [];
476
- _.forEach( input, ( value ) => {
477
- let decomposedValue = module.exports.decomposeJSON( value );
478
- if ( decomposedValue !== undefined ) {
479
- decomposed.push( decomposedValue );
480
- }
481
- } );
482
- decomposed = decomposed.sort();
483
- decomposed = decomposed.join( ":" );
484
- } else if ( _.isPlainObject( input ) ) {
485
- decomposed = [];
486
- _.forOwn( input, ( value, key ) => {
487
- let decomposedValue = module.exports.decomposeJSON( value );
488
- if ( decomposedValue !== undefined ) {
489
- decomposed.push( _.toString( key ) + ":" + decomposedValue );
490
- }
491
- } );
492
- decomposed = decomposed.sort();
493
- decomposed = decomposed.join( ":" );
494
- } else {
495
- decomposed = _.toString( input );
496
- }
497
- }
498
-
499
- return decomposed;
500
- };
501
-
502
- /**
503
- * Constant-time string comparison. Mirrors the web-framework safe-compare idiom: coerces inputs to
504
- * utf8 buffers, short-circuits on length mismatch, and never throws on hostile/non-string input.
505
- *
506
- * @method
507
- * @param {*} a
508
- * @param {*} b
509
- * @returns {boolean} True only when both inputs coerce to equal-length, byte-identical strings.
510
- * @public
511
- */
512
- module.exports.constantTimeEquals = ( a, b ) => {
513
- try {
514
- const ba = Buffer.from( String( a || "" ), "utf8" );
515
- const bb = Buffer.from( String( b || "" ), "utf8" );
516
- return ( ba.length !== bb.length ) ? false : crypto.timingSafeEqual( ba, bb );
517
- } catch {
518
- return false;
519
- }
520
- };
521
-
522
- /**
523
- * Used to create retry policy for the execution of an operation.
524
- *
525
- * @class RetryPolicy
526
- * @public
527
- */
528
- class RetryPolicy {
529
-
530
- #maxAttempts;
531
- #onFailedAttempt;
532
- #onRetry;
533
-
534
- /**
535
- * @constructor
536
- * @param {number} maxAttempts The maximum number of attempts to execute the operation.
537
- * @throws {TypeError} maxAttempts must be a positive integer.
538
- */
539
- constructor( maxAttempts ) {
540
- if ( !Number.isInteger( maxAttempts ) || maxAttempts < 1 ) {
541
- throw new TypeError( "maxAttempts must be a positive integer" );
542
- }
543
- this.#maxAttempts = maxAttempts;
544
- }
545
-
546
- /* Public interface */
547
-
548
- /**
549
- * Used to start execution of the provided operation.
550
- *
551
- * @method
552
- * @param {Object} context The context in which the operation will be executed (i.e., this reference).
553
- * @param {function( ...* ): Promise<*>} operation Operation to be executed; must return a Promise.
554
- * @param {Array<*>} [params=[]] The arguments to be provided to the operation upon execution.
555
- * @returns {Promise}
556
- * @public
557
- */
558
- execute( context, operation, params = [] ) {
559
- return this.#retry( context, operation, params, 1, undefined );
560
- }
561
-
562
- /**
563
- * Used to register a method that will be automatically called on a failed execution attempt.
564
- *
565
- * @method
566
- * @param {function( Error )} action The execution error will be provided as an argument.
567
- * @public
568
- */
569
- onFailedAttempt( action ) {
570
- if ( typeof ( action ) === "function" ) {
571
- this.#onFailedAttempt = action;
572
- }
573
- }
574
-
575
- /**
576
- * Used to register a method that will be automatically called on each execution retry (after the initial one).
577
- *
578
- * @method
579
- * @param {function( number, (Error|undefined) )} action The current attempt and last error are provided.
580
- * @public
581
- */
582
- onRetry( action ) {
583
- if ( typeof ( action ) === "function" ) {
584
- this.#onRetry = action;
585
- }
586
- }
587
-
588
- /* Private interface */
589
-
590
- /**
591
- * Will retry the execution of operation up to max attempts.
592
- *
593
- * @method
594
- * @param {Object} context
595
- * @param {function( ...* ): Promise<*>} operation Operation to be executed; must return a Promise.
596
- * @param {Array<*>} params The arguments to be provided to the operation upon execution.
597
- * @param {number} attempt
598
- * @param {Error} error
599
- * @returns {Promise}
600
- * @private
601
- */
602
- #retry( context, operation, params, attempt, error ) {
603
- if ( attempt > this.#maxAttempts ) {
604
- return Promise.reject( error );
605
- } else {
606
- if ( attempt > 1 && this.#onRetry ) {
607
- try {
608
- this.#onRetry( attempt, error );
609
- } catch {
610
- // ignore observer errors...
611
- }
612
- }
613
- return Promise
614
- .resolve()
615
- .then( () => {
616
- return operation.apply( context, params );
617
- } )
618
- .catch( ( error ) => {
619
- if ( this.#onFailedAttempt ) {
620
- try {
621
- this.#onFailedAttempt( error );
622
- } catch {
623
- // ignore observer errors...
624
- }
625
- }
626
- return this.#retry( context, operation, params, ( attempt + 1 ), error );
627
- } );
628
- }
629
- }
630
-
631
- }
632
-
1
+ /*
2
+ * The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
3
+ * Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
4
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
+ * You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7
+ */
8
+
9
+ const _ = require( "lodash" );
10
+ const crypto = require( "node:crypto" );
11
+
12
+ /**
13
+ * Used to generate and return new UUID.
14
+ *
15
+ * @method
16
+ * @returns {string}
17
+ * @public
18
+ */
19
+ module.exports.getUUID = () => {
20
+ return crypto.randomUUID( { disableEntropyCache: true } );
21
+ };
22
+
23
+ /**
24
+ * Used to deep-freeze an object.
25
+ *
26
+ * @method
27
+ * @param {Object} object
28
+ * @param {WeakSet} [seen]
29
+ * @returns {Object}
30
+ * @public
31
+ */
32
+ module.exports.deepFreeze = ( object, seen = new WeakSet() ) => {
33
+ if ( object === null || typeof object !== "object" || seen.has( object ) ) {
34
+ return object;
35
+ } else {
36
+ seen.add( object );
37
+ _.forOwn( object, ( value ) => {
38
+ module.exports.deepFreeze( value, seen );
39
+ } );
40
+ return Object.freeze( object );
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Used to create a custom Enum list.
46
+ *
47
+ * @method
48
+ * @param {Object} seed
49
+ * @returns {Object} This is a {@link TiEnum} object. Setting the proper reference here would unfortunately break IDE support.
50
+ * @public
51
+ */
52
+ module.exports.enum = ( seed ) => {
53
+ const enumObject = Object.create( null );
54
+ const properties = Object.create( null );
55
+ const reserved = new Set( [ "properties", "name", "description", "contains", "__proto__", "prototype", "constructor" ] );
56
+
57
+ _.forOwn( seed, ( value, key ) => {
58
+ if ( !reserved.has( key ) ) {
59
+ if ( Array.isArray( value ) ) {
60
+ enumObject[ key ] = value[ 0 ];
61
+ properties[ value[ 0 ] ] = {
62
+ value: value[ 0 ],
63
+ name: value[ 1 ],
64
+ description: value[ 2 ]
65
+ };
66
+ } else {
67
+ enumObject[ key ] = value;
68
+ properties[ value ] = {
69
+ value: value,
70
+ name: key
71
+ };
72
+ }
73
+ }
74
+ } );
75
+ Object.values( properties ).forEach( Object.freeze );
76
+ Object.freeze( properties );
77
+
78
+ /**
79
+ * Used to get the name of an {@link TiEnumValue} if such value exists.
80
+ *
81
+ * @method
82
+ * @param {number|string} value
83
+ * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a name defined.
84
+ * @returns {string|undefined}
85
+ * @public
86
+ */
87
+ const name = ( value, placeholder = undefined ) => {
88
+ return ( properties[ value ] ) ? properties[ value ].name : placeholder;
89
+ };
90
+
91
+ /**
92
+ * Used to get the description of an {@link TiEnumValue} if such value exists.
93
+ *
94
+ * @method
95
+ * @param {number|string} value
96
+ * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a description defined.
97
+ * @returns {string|undefined}
98
+ * @public
99
+ */
100
+ const description = ( value, placeholder = undefined ) => {
101
+ if ( !properties[ value ] ) {
102
+ return placeholder;
103
+ } else {
104
+ return ( properties[ value ].description !== undefined ) ? properties[ value ].description : placeholder;
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Used to check if the provided value is contained in the provided {@link TiEnum} list.
110
+ *
111
+ * @method
112
+ * @param {number|string} value
113
+ * @returns {boolean}
114
+ * @public
115
+ */
116
+ const contains = ( value ) => {
117
+ return !!( properties[ value ] );
118
+ };
119
+
120
+ Object.defineProperties( enumObject, {
121
+ contains: {
122
+ enumerable: false,
123
+ configurable: false,
124
+ writable: false,
125
+ value: contains
126
+ },
127
+ description: {
128
+ enumerable: false,
129
+ configurable: false,
130
+ writable: false,
131
+ value: description
132
+ },
133
+ name: {
134
+ enumerable: false,
135
+ configurable: false,
136
+ writable: false,
137
+ value: name
138
+ },
139
+ properties: {
140
+ enumerable: false,
141
+ configurable: false,
142
+ writable: false,
143
+ value: properties
144
+ }
145
+ } );
146
+ Object.freeze( enumObject );
147
+
148
+ return enumObject;
149
+ };
150
+
151
+ /**
152
+ * Used to get the name of an {@link TiEnum} value if such exists.
153
+ *
154
+ * @method
155
+ * @deprecated Use the 'name' property of the provided {@link TiEnum} instead.
156
+ * @param {TiEnum} enumList
157
+ * @param {number|string} enumValue
158
+ * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a name defined.
159
+ * @returns {string|undefined}
160
+ * @public
161
+ */
162
+ module.exports.getEnumName = ( enumList, enumValue, placeholder = undefined ) => {
163
+ return ( enumList.properties[ enumValue ] ) ? enumList.properties[ enumValue ].name : placeholder;
164
+ };
165
+
166
+ /**
167
+ * Convert an Error to a JSON object.
168
+ * <br/>
169
+ * NOTE: If the value provided is not an error, then it will just be cloned.
170
+ *
171
+ * @method
172
+ * @param {Error} value
173
+ * @returns {Object}
174
+ * @public
175
+ */
176
+ module.exports.errorToJSON = ( value ) => {
177
+ let error = {};
178
+
179
+ if ( value instanceof Error ) {
180
+ Object.getOwnPropertyNames( value ).forEach( ( key ) => {
181
+ error[ key ] = value[ key ];
182
+ } );
183
+ } else {
184
+ error = _.cloneDeep( value );
185
+ }
186
+
187
+ return error;
188
+ };
189
+
190
+ /**
191
+ * Used to parse a value and return its boolean representation (if possible).
192
+ *
193
+ * @param {*} value
194
+ * @returns {boolean}
195
+ * @public
196
+ */
197
+ module.exports.toBool = ( value ) => {
198
+ let result = true;
199
+ let regexp = /^false$|^0$|^no$/i;
200
+
201
+ if ( !value || regexp.test( value ) || value === "N" || value === "0" || ( _.isObjectLike( value ) && _.size( value ) === 0 ) ) {
202
+ result = false;
203
+ }
204
+
205
+ return result;
206
+ };
207
+
208
+ /**
209
+ * Used to fetch only the unique values from the provided array.
210
+ * <br/>
211
+ * Note: For objects and arrays, uniqueness is determined by reference equality. Primitives are compared by their value.
212
+ *
213
+ * @method
214
+ * @param {Array} array
215
+ * @returns {Array}
216
+ * @throws {TypeError} If the provided parameter is not an array
217
+ * @public
218
+ */
219
+ module.exports.arrayUniques = ( array ) => {
220
+ if ( !Array.isArray( array ) ) {
221
+ throw new TypeError( "Expected an array" );
222
+ }
223
+ return [ ...new Set( array ) ];
224
+ };
225
+
226
+ /**
227
+ * Will return a UTC date string in format YYYY-MM-DD from the provided date.
228
+ *
229
+ * @method
230
+ * @param {Date} date
231
+ * @returns {string}
232
+ * @public
233
+ */
234
+ module.exports.getUTCDateString = ( date ) => {
235
+ const year = date.getUTCFullYear();
236
+ const month = String( date.getUTCMonth() + 1 ).padStart( 2, "0" );
237
+ const day = String( date.getUTCDate() ).padStart( 2, "0" );
238
+ return `${ year }-${ month }-${ day }`;
239
+ };
240
+
241
+ /**
242
+ * Will return a UTC time string in format hh:mm:ss, or hh:mm:ss.MMM when useMilliseconds is true.
243
+ *
244
+ * @method
245
+ * @param {Date} date
246
+ * @param {boolean} [useMilliseconds=false]
247
+ * @returns {string}
248
+ * @public
249
+ */
250
+ module.exports.getUTCTimeString = ( date, useMilliseconds = false ) => {
251
+ const hours = String( date.getUTCHours() ).padStart( 2, "0" );
252
+ const minutes = String( date.getUTCMinutes() ).padStart( 2, "0" );
253
+ const seconds = String( date.getUTCSeconds() ).padStart( 2, "0" );
254
+ const milliseconds = useMilliseconds ? `${ String( date.getUTCMilliseconds() ).padStart( 3, "0" ) }` : "";
255
+ return `${ hours }:${ minutes }:${ seconds }${ useMilliseconds ? `.${ milliseconds }` : "" }`;
256
+ };
257
+
258
+ /**
259
+ * Used to remove any circular dependencies from JSON objects.
260
+ * <br/>
261
+ * NOTE: Original file from here - https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
262
+ *
263
+ * Make a deep copy of an object or array, assuring that there is at most one instance of each object or array in the
264
+ * resulting structure. The duplicate references (which might be forming cycles) are replaced with an object of the
265
+ * form of {"$ref": PATH} where the PATH is a JSONPath string that locates the first occurrence.
266
+ *
267
+ * So,
268
+ *
269
+ * var a = [];
270
+ * a[0] = a;
271
+ * return JSON.stringify(JSON.decycle(a));
272
+ *
273
+ * produces the string '[{"$ref":"$"}]'.
274
+ *
275
+ * If a replacer function is provided, then it will be called for each value. A replacer function receives a value
276
+ * and returns a replacement value.
277
+ *
278
+ * JSONPath is used to locate the unique object. $ indicates the top level of the object or array. [NUMBER] or [STRING]
279
+ * indicates a child element or property.
280
+ *
281
+ * @method
282
+ * @param {Object} object
283
+ * @param {function( Object ): Object} [replacer]
284
+ * @returns {Object}
285
+ * @public
286
+ */
287
+ module.exports.decycle = ( object, replacer ) => {
288
+ "use strict";
289
+
290
+ let objects = new WeakMap();
291
+
292
+ // The derez function recurse through the object, producing the deep copy.
293
+ return ( function derez( value, path ) {
294
+ let oldPath; // The path of an earlier occurrence of value
295
+ let newItem; // The new object or array
296
+
297
+ // If a replacer function was provided, then call it to get a replacement value.
298
+ if ( replacer !== undefined ) {
299
+ value = replacer( value );
300
+ }
301
+
302
+ // typeof null === "object", so go on if this value is really an object but not
303
+ // one of the weird builtin objects.
304
+ if (
305
+ typeof value === "object"
306
+ && value !== null
307
+ && !( value instanceof Boolean )
308
+ && !( value instanceof Date )
309
+ && !( value instanceof Number )
310
+ && !( value instanceof RegExp )
311
+ && !( value instanceof String )
312
+ ) {
313
+ // If the value is an object or array, look to see if we have already
314
+ // encountered it. If so, return a {"$ref":PATH} object. This uses an
315
+ // ES6 WeakMap.
316
+ oldPath = objects.get( value );
317
+ if ( oldPath !== undefined ) {
318
+ return { $ref: oldPath };
319
+ }
320
+
321
+ // Otherwise, accumulate the unique value and its path.
322
+ objects.set( value, path );
323
+
324
+ // If it is an array, replicate the array.
325
+ if ( Array.isArray( value ) ) {
326
+ newItem = [];
327
+ value.forEach( ( element, i ) => {
328
+ newItem[ i ] = derez( element, path + "[" + i + "]" );
329
+ } );
330
+ } else {
331
+ // If it is an object, replicate the object.
332
+ newItem = {};
333
+ Object.keys( value ).forEach( ( name ) => {
334
+ newItem[ name ] = derez(
335
+ value[ name ],
336
+ path + "[" + JSON.stringify( name ) + "]"
337
+ );
338
+ } );
339
+ }
340
+ return newItem;
341
+ }
342
+ return value;
343
+ }( object, "$" ) );
344
+ };
345
+
346
+ /**
347
+ * Used to restore any circular dependencies from JSON objects after using the 'decycle' method.
348
+ * <br/>
349
+ * NOTE: Original file from here - https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
350
+ *
351
+ * Restore an object that was reduced by decycle. Members whose values are objects of the form of {$ref: PATH} are
352
+ * replaced with references to the value found by the PATH. This will restore cycles. The object will be mutated.
353
+ *
354
+ * The eval function is used to locate the values described by a PATH. The root object is kept in a $ variable. A
355
+ * regular expression is used to ensure that the PATH is extremely well-formed. The regexp contains nested quantifiers.
356
+ * That has been known to have extremely bad performance problems on some browsers for very long strings. A PATH is
357
+ * expected to be reasonably short. A PATH is allowed to belong to a very restricted subset of Goessner's JSONPath.
358
+ *
359
+ * So,
360
+ *
361
+ * var s = '[{"$ref":"$"}]';
362
+ * return JSON.retrocycle(JSON.parse(s));
363
+ *
364
+ * produces an array containing a single element which is the array itself.
365
+ *
366
+ * @method
367
+ * @param {Object} $
368
+ * @returns {Object}
369
+ * @public
370
+ */
371
+ module.exports.retrocycle = ( $ ) => {
372
+ "use strict";
373
+
374
+ // eslint-disable-next-line no-control-regex
375
+ let px = /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\(?:[\\"/bfnrt]|u[0-9a-zA-Z]{4}))*")])*$/;
376
+
377
+ // The rez function walks recursively through the object looking for $ref
378
+ // properties. When it finds one that has a value that is a path, then it
379
+ // replaces the $ref object with a reference to the value that is found by
380
+ // the path.
381
+ ( function rez( value ) {
382
+ if ( value && typeof value === "object" ) {
383
+ if ( Array.isArray( value ) ) {
384
+ value.forEach( ( element, i ) => {
385
+ if ( typeof element === "object" && element !== null ) {
386
+ let path = element.$ref;
387
+ if ( typeof path === "string" && px.test( path ) ) {
388
+ value[ i ] = eval( path );
389
+ } else {
390
+ rez( element );
391
+ }
392
+ }
393
+ } );
394
+ } else {
395
+ Object.keys( value ).forEach( ( name ) => {
396
+ let item = value[ name ];
397
+ if ( typeof item === "object" && item !== null ) {
398
+ let path = item.$ref;
399
+ if ( typeof path === "string" && px.test( path ) ) {
400
+ value[ name ] = eval( path );
401
+ } else {
402
+ rez( item );
403
+ }
404
+ }
405
+ } );
406
+ }
407
+ }
408
+ }( $ ) );
409
+ return $;
410
+ };
411
+
412
+ /**
413
+ * Use this to stringify any JSON object for internal system purposes as it ensures no potential circular dependencies
414
+ * will cause it to throw exception.
415
+ *
416
+ * @method
417
+ * @param {Object} value
418
+ * @returns {string|*}
419
+ * @public
420
+ */
421
+ module.exports.stringifyJSON = ( value ) => {
422
+ return _.isObjectLike( value ) ? JSON.stringify( _.toPlainObject( module.exports.decycle( value ) ) ) : value;
423
+ };
424
+
425
+ /**
426
+ * Use this to verify if the provided string can be parsed as a JSON.
427
+ *
428
+ * @method
429
+ * @param {string} string
430
+ * @returns {boolean}
431
+ * @public
432
+ */
433
+ module.exports.isJsonString = ( string ) => {
434
+ try {
435
+ JSON.parse( string );
436
+ } catch {
437
+ return false;
438
+ }
439
+ return true;
440
+ };
441
+
442
+ /**
443
+ * Use this to parse any JSON string into a JSON object for internal system purposes as it ensures to restore any
444
+ * circular dependencies obscured with 'stringifyJSON'.
445
+ *
446
+ * @method
447
+ * @param {string} value
448
+ * @returns {Object|string}
449
+ * @public
450
+ */
451
+ module.exports.parseJSON = ( value ) => {
452
+ try {
453
+ let transformed = JSON.parse( value );
454
+ return module.exports.retrocycle( transformed );
455
+ } catch {
456
+ return value;
457
+ }
458
+ };
459
+
460
+ /**
461
+ * Use this to decompose a JSON object into a sorted string. The values will be ordered alphabetically and combined with
462
+ * their keys, where applicable, starting from the bottom and moving up. Null or undefined values will be ignored, and
463
+ * their keys will not be included in the final string.
464
+ *
465
+ * @param {Object} input
466
+ * @recursion
467
+ * @returns {string|null}
468
+ * @public
469
+ */
470
+ module.exports.decomposeJSON = ( input ) => {
471
+ let decomposed;
472
+
473
+ if ( !_.isNil( input ) ) {
474
+ if ( _.isArray( input ) ) {
475
+ decomposed = [];
476
+ _.forEach( input, ( value ) => {
477
+ let decomposedValue = module.exports.decomposeJSON( value );
478
+ if ( decomposedValue !== undefined ) {
479
+ decomposed.push( decomposedValue );
480
+ }
481
+ } );
482
+ decomposed = decomposed.sort();
483
+ decomposed = decomposed.join( ":" );
484
+ } else if ( _.isPlainObject( input ) ) {
485
+ decomposed = [];
486
+ _.forOwn( input, ( value, key ) => {
487
+ let decomposedValue = module.exports.decomposeJSON( value );
488
+ if ( decomposedValue !== undefined ) {
489
+ decomposed.push( _.toString( key ) + ":" + decomposedValue );
490
+ }
491
+ } );
492
+ decomposed = decomposed.sort();
493
+ decomposed = decomposed.join( ":" );
494
+ } else {
495
+ decomposed = _.toString( input );
496
+ }
497
+ }
498
+
499
+ return decomposed;
500
+ };
501
+
502
+ /**
503
+ * Constant-time string comparison. Mirrors the web-framework safe-compare idiom: coerces inputs to
504
+ * utf8 buffers, short-circuits on length mismatch, and never throws on hostile/non-string input.
505
+ *
506
+ * @method
507
+ * @param {*} a
508
+ * @param {*} b
509
+ * @returns {boolean} True only when both inputs coerce to equal-length, byte-identical strings.
510
+ * @public
511
+ */
512
+ module.exports.constantTimeEquals = ( a, b ) => {
513
+ try {
514
+ const ba = Buffer.from( String( a || "" ), "utf8" );
515
+ const bb = Buffer.from( String( b || "" ), "utf8" );
516
+ return ( ba.length !== bb.length ) ? false : crypto.timingSafeEqual( ba, bb );
517
+ } catch {
518
+ return false;
519
+ }
520
+ };
521
+
522
+ /**
523
+ * Used to create retry policy for the execution of an operation.
524
+ *
525
+ * @class RetryPolicy
526
+ * @public
527
+ */
528
+ class RetryPolicy {
529
+
530
+ #maxAttempts;
531
+ #onFailedAttempt;
532
+ #onRetry;
533
+
534
+ /**
535
+ * @constructor
536
+ * @param {number} maxAttempts The maximum number of attempts to execute the operation.
537
+ * @throws {TypeError} maxAttempts must be a positive integer.
538
+ */
539
+ constructor( maxAttempts ) {
540
+ if ( !Number.isInteger( maxAttempts ) || maxAttempts < 1 ) {
541
+ throw new TypeError( "maxAttempts must be a positive integer" );
542
+ }
543
+ this.#maxAttempts = maxAttempts;
544
+ }
545
+
546
+ /* Public interface */
547
+
548
+ /**
549
+ * Used to start execution of the provided operation.
550
+ *
551
+ * @method
552
+ * @param {Object} context The context in which the operation will be executed (i.e., this reference).
553
+ * @param {function( ...* ): Promise<*>} operation Operation to be executed; must return a Promise.
554
+ * @param {Array<*>} [params=[]] The arguments to be provided to the operation upon execution.
555
+ * @returns {Promise}
556
+ * @public
557
+ */
558
+ execute( context, operation, params = [] ) {
559
+ return this.#retry( context, operation, params, 1, undefined );
560
+ }
561
+
562
+ /**
563
+ * Used to register a method that will be automatically called on a failed execution attempt.
564
+ *
565
+ * @method
566
+ * @param {function( Error )} action The execution error will be provided as an argument.
567
+ * @public
568
+ */
569
+ onFailedAttempt( action ) {
570
+ if ( typeof ( action ) === "function" ) {
571
+ this.#onFailedAttempt = action;
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Used to register a method that will be automatically called on each execution retry (after the initial one).
577
+ *
578
+ * @method
579
+ * @param {function( number, (Error|undefined) )} action The current attempt and last error are provided.
580
+ * @public
581
+ */
582
+ onRetry( action ) {
583
+ if ( typeof ( action ) === "function" ) {
584
+ this.#onRetry = action;
585
+ }
586
+ }
587
+
588
+ /* Private interface */
589
+
590
+ /**
591
+ * Will retry the execution of operation up to max attempts.
592
+ *
593
+ * @method
594
+ * @param {Object} context
595
+ * @param {function( ...* ): Promise<*>} operation Operation to be executed; must return a Promise.
596
+ * @param {Array<*>} params The arguments to be provided to the operation upon execution.
597
+ * @param {number} attempt
598
+ * @param {Error} error
599
+ * @returns {Promise}
600
+ * @private
601
+ */
602
+ #retry( context, operation, params, attempt, error ) {
603
+ if ( attempt > this.#maxAttempts ) {
604
+ return Promise.reject( error );
605
+ } else {
606
+ if ( attempt > 1 && this.#onRetry ) {
607
+ try {
608
+ this.#onRetry( attempt, error );
609
+ } catch {
610
+ // ignore observer errors...
611
+ }
612
+ }
613
+ return Promise
614
+ .resolve()
615
+ .then( () => {
616
+ return operation.apply( context, params );
617
+ } )
618
+ .catch( ( error ) => {
619
+ if ( this.#onFailedAttempt ) {
620
+ try {
621
+ this.#onFailedAttempt( error );
622
+ } catch {
623
+ // ignore observer errors...
624
+ }
625
+ }
626
+ return this.#retry( context, operation, params, ( attempt + 1 ), error );
627
+ } );
628
+ }
629
+ }
630
+
631
+ }
632
+
633
633
  module.exports.RetryPolicy = RetryPolicy;