@lowentry/utils 1.25.3 → 2.0.3
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.
- package/LICENSE +23 -0
- package/README.md +0 -2
- package/dist/src/LeTypes.d.ts +73 -0
- package/dist/src/LeTypes.d.ts.map +1 -0
- package/dist/src/LeTypes.js +235 -0
- package/dist/src/LeTypes.js.map +1 -0
- package/dist/src/LeUtils.d.ts +745 -0
- package/dist/src/LeUtils.d.ts.map +1 -0
- package/dist/src/LeUtils.js +3168 -0
- package/dist/src/LeUtils.js.map +1 -0
- package/dist/src/classes/EventEmitter.d.ts +51 -0
- package/dist/src/classes/EventEmitter.d.ts.map +1 -0
- package/dist/src/classes/EventEmitter.js +94 -0
- package/dist/src/classes/EventEmitter.js.map +1 -0
- package/dist/src/classes/LinkedList.d.ts +35 -0
- package/dist/src/classes/LinkedList.d.ts.map +1 -0
- package/dist/src/classes/LinkedList.js +99 -0
- package/dist/src/classes/LinkedList.js.map +1 -0
- package/dist/src/classes/SerializableMap.d.ts +14 -0
- package/dist/src/classes/SerializableMap.d.ts.map +1 -0
- package/dist/src/classes/SerializableMap.js +27 -0
- package/dist/src/classes/SerializableMap.js.map +1 -0
- package/dist/src/classes/TreeSet.d.ts +123 -0
- package/dist/src/classes/TreeSet.d.ts.map +1 -0
- package/dist/src/classes/TreeSet.js +210 -0
- package/dist/src/classes/TreeSet.js.map +1 -0
- package/dist/src/index.d.ts +148 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +13 -0
- package/dist/src/index.js.map +1 -0
- package/dist/tests/CustomClasses.test.d.ts +2 -0
- package/dist/tests/CustomClasses.test.d.ts.map +1 -0
- package/dist/tests/CustomClasses.test.js +138 -0
- package/dist/tests/CustomClasses.test.js.map +1 -0
- package/dist/tests/LeClasses.EventEmitter.test.d.ts +2 -0
- package/dist/tests/LeClasses.EventEmitter.test.d.ts.map +1 -0
- package/dist/tests/LeClasses.EventEmitter.test.js +39 -0
- package/dist/tests/LeClasses.EventEmitter.test.js.map +1 -0
- package/dist/tests/LeClasses.LinkedList.test.d.ts +2 -0
- package/dist/tests/LeClasses.LinkedList.test.d.ts.map +1 -0
- package/dist/tests/LeClasses.LinkedList.test.js +36 -0
- package/dist/tests/LeClasses.LinkedList.test.js.map +1 -0
- package/dist/tests/LeClasses.SerializableMap.test.d.ts +2 -0
- package/dist/tests/LeClasses.SerializableMap.test.d.ts.map +1 -0
- package/dist/tests/LeClasses.SerializableMap.test.js +25 -0
- package/dist/tests/LeClasses.SerializableMap.test.js.map +1 -0
- package/dist/tests/LeClasses.TreeSet.test.d.ts +2 -0
- package/dist/tests/LeClasses.TreeSet.test.d.ts.map +1 -0
- package/dist/tests/LeClasses.TreeSet.test.js +28 -0
- package/dist/tests/LeClasses.TreeSet.test.js.map +1 -0
- package/dist/tests/LeTypes.ARRAY.test.d.ts +2 -0
- package/dist/tests/LeTypes.ARRAY.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.ARRAY.test.js +103 -0
- package/dist/tests/LeTypes.ARRAY.test.js.map +1 -0
- package/dist/tests/LeTypes.BOOL.test.d.ts +2 -0
- package/dist/tests/LeTypes.BOOL.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.BOOL.test.js +138 -0
- package/dist/tests/LeTypes.BOOL.test.js.map +1 -0
- package/dist/tests/LeTypes.FLOAT.test.d.ts +2 -0
- package/dist/tests/LeTypes.FLOAT.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.FLOAT.test.js +120 -0
- package/dist/tests/LeTypes.FLOAT.test.js.map +1 -0
- package/dist/tests/LeTypes.FLOAT_ANY.test.d.ts +2 -0
- package/dist/tests/LeTypes.FLOAT_ANY.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.FLOAT_ANY.test.js +47 -0
- package/dist/tests/LeTypes.FLOAT_ANY.test.js.map +1 -0
- package/dist/tests/LeTypes.INT.test.d.ts +2 -0
- package/dist/tests/LeTypes.INT.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.INT.test.js +80 -0
- package/dist/tests/LeTypes.INT.test.js.map +1 -0
- package/dist/tests/LeTypes.INT_ANY.test.d.ts +2 -0
- package/dist/tests/LeTypes.INT_ANY.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.INT_ANY.test.js +37 -0
- package/dist/tests/LeTypes.INT_ANY.test.js.map +1 -0
- package/dist/tests/LeTypes.ISSET.test.d.ts +2 -0
- package/dist/tests/LeTypes.ISSET.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.ISSET.test.js +113 -0
- package/dist/tests/LeTypes.ISSET.test.js.map +1 -0
- package/dist/tests/LeTypes.IS_ARRAY.test.d.ts +2 -0
- package/dist/tests/LeTypes.IS_ARRAY.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.IS_ARRAY.test.js +81 -0
- package/dist/tests/LeTypes.IS_ARRAY.test.js.map +1 -0
- package/dist/tests/LeTypes.IS_OBJECT.test.d.ts +2 -0
- package/dist/tests/LeTypes.IS_OBJECT.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.IS_OBJECT.test.js +98 -0
- package/dist/tests/LeTypes.IS_OBJECT.test.js.map +1 -0
- package/dist/tests/LeTypes.LAX.test.d.ts +2 -0
- package/dist/tests/LeTypes.LAX.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.LAX.test.js +89 -0
- package/dist/tests/LeTypes.LAX.test.js.map +1 -0
- package/dist/tests/LeTypes.OBJECT.test.d.ts +2 -0
- package/dist/tests/LeTypes.OBJECT.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.OBJECT.test.js +81 -0
- package/dist/tests/LeTypes.OBJECT.test.js.map +1 -0
- package/dist/tests/LeTypes.STRING.test.d.ts +2 -0
- package/dist/tests/LeTypes.STRING.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.STRING.test.js +204 -0
- package/dist/tests/LeTypes.STRING.test.js.map +1 -0
- package/dist/tests/LeTypes.STRING_ANY.test.d.ts +2 -0
- package/dist/tests/LeTypes.STRING_ANY.test.d.ts.map +1 -0
- package/dist/tests/LeTypes.STRING_ANY.test.js +58 -0
- package/dist/tests/LeTypes.STRING_ANY.test.js.map +1 -0
- package/dist/tests/LeUtils.clone.test.d.ts +2 -0
- package/dist/tests/LeUtils.clone.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.clone.test.js +180 -0
- package/dist/tests/LeUtils.clone.test.js.map +1 -0
- package/dist/tests/LeUtils.collections.test.d.ts +2 -0
- package/dist/tests/LeUtils.collections.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.collections.test.js +149 -0
- package/dist/tests/LeUtils.collections.test.js.map +1 -0
- package/dist/tests/LeUtils.comparison.test.d.ts +2 -0
- package/dist/tests/LeUtils.comparison.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.comparison.test.js +125 -0
- package/dist/tests/LeUtils.comparison.test.js.map +1 -0
- package/dist/tests/LeUtils.contains.test.d.ts +2 -0
- package/dist/tests/LeUtils.contains.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.contains.test.js +52 -0
- package/dist/tests/LeUtils.contains.test.js.map +1 -0
- package/dist/tests/LeUtils.each.test.d.ts +2 -0
- package/dist/tests/LeUtils.each.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.each.test.js +267 -0
- package/dist/tests/LeUtils.each.test.js.map +1 -0
- package/dist/tests/LeUtils.eachAsync.test.d.ts +2 -0
- package/dist/tests/LeUtils.eachAsync.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.eachAsync.test.js +80 -0
- package/dist/tests/LeUtils.eachAsync.test.js.map +1 -0
- package/dist/tests/LeUtils.encoding.test.d.ts +2 -0
- package/dist/tests/LeUtils.encoding.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.encoding.test.js +45 -0
- package/dist/tests/LeUtils.encoding.test.js.map +1 -0
- package/dist/tests/LeUtils.equals.test.d.ts +2 -0
- package/dist/tests/LeUtils.equals.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.equals.test.js +328 -0
- package/dist/tests/LeUtils.equals.test.js.map +1 -0
- package/dist/tests/LeUtils.flatten.test.d.ts +2 -0
- package/dist/tests/LeUtils.flatten.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.flatten.test.js +30 -0
- package/dist/tests/LeUtils.flatten.test.js.map +1 -0
- package/dist/tests/LeUtils.misc.test.d.ts +2 -0
- package/dist/tests/LeUtils.misc.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.misc.test.js +50 -0
- package/dist/tests/LeUtils.misc.test.js.map +1 -0
- package/dist/tests/LeUtils.misc2.test.d.ts +2 -0
- package/dist/tests/LeUtils.misc2.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.misc2.test.js +58 -0
- package/dist/tests/LeUtils.misc2.test.js.map +1 -0
- package/dist/tests/LeUtils.newlyAdded.test.d.ts +2 -0
- package/dist/tests/LeUtils.newlyAdded.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.newlyAdded.test.js +121 -0
- package/dist/tests/LeUtils.newlyAdded.test.js.map +1 -0
- package/dist/tests/LeUtils.strings.test.d.ts +2 -0
- package/dist/tests/LeUtils.strings.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.strings.test.js +125 -0
- package/dist/tests/LeUtils.strings.test.js.map +1 -0
- package/dist/tests/LeUtils.timing.test.d.ts +2 -0
- package/dist/tests/LeUtils.timing.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.timing.test.js +72 -0
- package/dist/tests/LeUtils.timing.test.js.map +1 -0
- package/dist/tests/LeUtils.transactional.test.d.ts +2 -0
- package/dist/tests/LeUtils.transactional.test.d.ts.map +1 -0
- package/dist/tests/LeUtils.transactional.test.js +39 -0
- package/dist/tests/LeUtils.transactional.test.js.map +1 -0
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +6 -0
- package/dist/tests/setup.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +21 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +52 -53
- package/api-extractor.json +0 -43
- package/index.d.ts +0 -454
- package/index.js +0 -4160
- package/index.js.map +0 -1
- package/src/LeTypes.js +0 -254
- package/src/LeUtils.js +0 -3611
- package/src/classes/EventEmitter.js +0 -124
- package/src/classes/LinkedList.js +0 -145
- package/src/classes/SerializableMap.js +0 -17
- package/src/classes/TreeSet.js +0 -235
- package/src/index.js +0 -6
- package/tsconfig.d.ts +0 -1
- package/tsconfig.json +0 -39
|
@@ -0,0 +1,3168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LeUtils - Core utility functions
|
|
3
|
+
*/
|
|
4
|
+
import { ISSET, IS_OBJECT, IS_ARRAY, ARRAY, STRING, INT_LAX, FLOAT_LAX, INT_LAX_ANY, FLOAT_LAX_ANY } from './LeTypes';
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to safely get property value without 'as'
|
|
7
|
+
* Returns undefined if property doesn't exist (this is normal, no warning)
|
|
8
|
+
* This is a defensive helper - it's expected that obj might not be an object or prop might not exist
|
|
9
|
+
*/
|
|
10
|
+
function safeGetProperty(obj, prop) {
|
|
11
|
+
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) {
|
|
12
|
+
// Non-object - return undefined (defensive, no warning)
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (!(prop in obj)) {
|
|
16
|
+
// Property doesn't exist - this is normal, no warning
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const desc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
20
|
+
if (desc === undefined) {
|
|
21
|
+
// Property might be on prototype - use direct access
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
|
23
|
+
return obj[prop];
|
|
24
|
+
}
|
|
25
|
+
return desc.value;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Type predicate: check if value has numeric length property
|
|
29
|
+
*/
|
|
30
|
+
function hasNumericLength(val) {
|
|
31
|
+
if ((typeof val !== 'object' && typeof val !== 'function') || val === null) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (!('length' in val)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const lengthValue = safeGetProperty(val, 'length');
|
|
38
|
+
if (typeof lengthValue !== 'number') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Type predicate: check if value is DataView-like
|
|
45
|
+
*/
|
|
46
|
+
function isDataViewLike(val) {
|
|
47
|
+
if ((typeof val !== 'object' && typeof val !== 'function') || val === null) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (!('byteLength' in val) || !('getUint8' in val)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const byteLengthValue = safeGetProperty(val, 'byteLength');
|
|
54
|
+
const getUint8Value = safeGetProperty(val, 'getUint8');
|
|
55
|
+
if (typeof byteLengthValue !== 'number' || typeof getUint8Value !== 'function') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Type predicate: check if value is RegExp
|
|
62
|
+
*/
|
|
63
|
+
function isRegExp(val) {
|
|
64
|
+
return val instanceof RegExp;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Type predicate: check if value is Record-like
|
|
68
|
+
*/
|
|
69
|
+
function isRecordLike(val) {
|
|
70
|
+
return (typeof val === 'object' || typeof val === 'function') && val !== null && !Array.isArray(val) && !(val instanceof Map) && !(val instanceof Set);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Type predicate: check if value is array-like (unknown[])
|
|
74
|
+
*/
|
|
75
|
+
function isUnknownArray(val) {
|
|
76
|
+
return Array.isArray(val);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Type predicate: check if value is Map-like
|
|
80
|
+
*/
|
|
81
|
+
function isMapLike(val) {
|
|
82
|
+
return val instanceof Map;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Type predicate: check if value is Set-like
|
|
86
|
+
*/
|
|
87
|
+
function isSetLike(val) {
|
|
88
|
+
return val instanceof Set;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Type predicate: check if value is ArrayBufferView
|
|
92
|
+
*/
|
|
93
|
+
function isArrayBufferView(val) {
|
|
94
|
+
return ArrayBuffer.isView(val);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Type predicate: check if value has constructor property
|
|
98
|
+
*/
|
|
99
|
+
function hasConstructor(val) {
|
|
100
|
+
if ((typeof val !== 'object' && typeof val !== 'function') || val === null) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const constructorValue = safeGetProperty(val, 'constructor');
|
|
104
|
+
return typeof constructorValue === 'function';
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Type predicate: check if value is Iterable
|
|
108
|
+
*/
|
|
109
|
+
function isIterable(val) {
|
|
110
|
+
if ((typeof val !== 'object' && typeof val !== 'function') || val === null) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
// Check for Symbol.iterator using 'in' operator and direct property access
|
|
114
|
+
if (!(Symbol.iterator in val)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const desc = Object.getOwnPropertyDescriptor(val, Symbol.iterator);
|
|
118
|
+
if (desc === undefined) {
|
|
119
|
+
// Property might be on prototype - use direct access
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
|
121
|
+
return typeof val[Symbol.iterator] === 'function';
|
|
122
|
+
}
|
|
123
|
+
return typeof desc.value === 'function';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Type predicate: check if value has forEach method
|
|
127
|
+
*/
|
|
128
|
+
function hasForEach(val) {
|
|
129
|
+
if (typeof val !== 'object' || val === null) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const forEach = safeGetProperty(val, 'forEach');
|
|
133
|
+
return typeof forEach === 'function';
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Type predicate: check if value is object (not null, not array, not Map, not Set)
|
|
137
|
+
*/
|
|
138
|
+
function isPlainObject(val) {
|
|
139
|
+
if (typeof val !== 'object' || val === null) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const constructor = safeGetProperty(val, 'constructor');
|
|
143
|
+
return constructor === Object;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Type predicate: check if value is a tuple pair [unknown, unknown]
|
|
147
|
+
*/
|
|
148
|
+
function isPair(val) {
|
|
149
|
+
if (!Array.isArray(val)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return val.length === 2;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Type predicate: check if value is a Promise
|
|
156
|
+
*/
|
|
157
|
+
function isPromise(val) {
|
|
158
|
+
if (typeof val !== 'object' || val === null) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const then = safeGetProperty(val, 'then');
|
|
162
|
+
return typeof then === 'function';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Type predicate: check if value is TransactionalValue
|
|
166
|
+
*/
|
|
167
|
+
function isTransactionalValue(val) {
|
|
168
|
+
if (typeof val !== 'object' || val === null) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (!('value' in val)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (!('changes' in val)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const changes = safeGetProperty(val, 'changes');
|
|
178
|
+
if (!Array.isArray(changes)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Type predicate: check if Performance has timing property (legacy API)
|
|
185
|
+
*/
|
|
186
|
+
function hasPerformanceTiming(perf) {
|
|
187
|
+
if (typeof perf !== 'object' || perf === null) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Type predicate: check if globalThis has setTimeout
|
|
194
|
+
*/
|
|
195
|
+
function hasSetTimeout(global) {
|
|
196
|
+
if (typeof global !== 'object' || global === null) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const setTimeout = safeGetProperty(global, 'setTimeout');
|
|
200
|
+
return typeof setTimeout === 'function';
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Type predicate: check if globalThis has setInterval
|
|
204
|
+
*/
|
|
205
|
+
function hasSetInterval(global) {
|
|
206
|
+
if (typeof global !== 'object' || global === null) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const setInterval = safeGetProperty(global, 'setInterval');
|
|
210
|
+
return typeof setInterval === 'function';
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Type predicate: check if value is a number array (for gradients)
|
|
214
|
+
*/
|
|
215
|
+
function isNumberArray(val) {
|
|
216
|
+
if (!Array.isArray(val)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return val.every((item) => typeof item === 'number');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Type predicate: check if Navigator has vendor property
|
|
223
|
+
*/
|
|
224
|
+
function hasNavigatorVendor(nav) {
|
|
225
|
+
if (typeof nav !== 'object' || nav === null) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Type predicate: check if globalThis has opera property (legacy)
|
|
232
|
+
*/
|
|
233
|
+
function hasOpera(global) {
|
|
234
|
+
if (typeof global !== 'object' || global === null) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Type predicate: check if Navigator has languages
|
|
241
|
+
*/
|
|
242
|
+
function hasNavigatorLanguages(nav) {
|
|
243
|
+
if (typeof nav !== 'object' || nav === null) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Type predicate: check if globalThis has Intl
|
|
250
|
+
*/
|
|
251
|
+
function hasIntl(global) {
|
|
252
|
+
if (typeof global !== 'object' || global === null) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Type predicate: check if object has id property
|
|
259
|
+
*/
|
|
260
|
+
function hasId(obj) {
|
|
261
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const id = safeGetProperty(obj, 'id');
|
|
265
|
+
return typeof id === 'string';
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Type predicate: check if value is Worker with sendMessage
|
|
269
|
+
*/
|
|
270
|
+
function hasSendMessage(worker) {
|
|
271
|
+
if (typeof worker !== 'object' || worker === null) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const sendMessage = safeGetProperty(worker, 'sendMessage');
|
|
275
|
+
return typeof sendMessage === 'function';
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Type predicate: check if error has message property
|
|
279
|
+
*/
|
|
280
|
+
function hasErrorMessage(err) {
|
|
281
|
+
if (typeof err !== 'object' || err === null) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const message = safeGetProperty(err, 'message');
|
|
285
|
+
return typeof message === 'string';
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Type predicate: check if version has major, minor, patch
|
|
289
|
+
*/
|
|
290
|
+
function isVersionObject(version) {
|
|
291
|
+
if (!IS_OBJECT(version)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const major = safeGetProperty(version, 'major');
|
|
295
|
+
const minor = safeGetProperty(version, 'minor');
|
|
296
|
+
const patch = safeGetProperty(version, 'patch');
|
|
297
|
+
return major !== undefined && minor !== undefined && patch !== undefined;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Private helper to convert a number to bytes (Uint8ClampedArray).
|
|
301
|
+
*/
|
|
302
|
+
function numberToBytes(num) {
|
|
303
|
+
const size = (num === 0) ? 0 : Math.ceil((Math.floor(Math.log2(num)) + 1) / 8);
|
|
304
|
+
const bytes = new Uint8ClampedArray(size);
|
|
305
|
+
let x = num;
|
|
306
|
+
for (let i = (size - 1); i >= 0; i--) {
|
|
307
|
+
const rightByte = x & 0xff;
|
|
308
|
+
bytes[i] = rightByte;
|
|
309
|
+
x = Math.floor(x / 0x100);
|
|
310
|
+
}
|
|
311
|
+
return bytes;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Deep equality comparison.
|
|
315
|
+
* Compares objects, arrays, Maps, Sets, primitives, and handles special cases.
|
|
316
|
+
*
|
|
317
|
+
* @param a - First value to compare
|
|
318
|
+
* @param b - Second value to compare
|
|
319
|
+
* @returns true if values are equivalent
|
|
320
|
+
*/
|
|
321
|
+
export function equals(a, b) {
|
|
322
|
+
const seen = new WeakSet();
|
|
323
|
+
function recursiveEquals(valA, valB) {
|
|
324
|
+
if (valA === valB) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
if (valA && valB && typeof valA === 'object' && typeof valB === 'object') {
|
|
328
|
+
if (seen.has(valA)) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
seen.add(valA);
|
|
332
|
+
// Type guard: check constructor equality
|
|
333
|
+
const constructorAValue = safeGetProperty(valA, 'constructor');
|
|
334
|
+
const constructorBValue = safeGetProperty(valB, 'constructor');
|
|
335
|
+
if (constructorAValue !== constructorBValue) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
if (Array.isArray(valA)) {
|
|
339
|
+
// Type guard: verify valB is also an array
|
|
340
|
+
if (!Array.isArray(valB)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const length = valA.length;
|
|
344
|
+
if (length !== valB.length) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
for (let i = length; i-- !== 0;) {
|
|
348
|
+
if (!recursiveEquals(valA[i], valB[i])) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if ((valA instanceof Map) && (valB instanceof Map)) {
|
|
355
|
+
if (valA.size !== valB.size) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
for (const [keyA, valueA] of valA.entries()) {
|
|
359
|
+
let found = false;
|
|
360
|
+
let valueB;
|
|
361
|
+
if (valB.has(keyA)) {
|
|
362
|
+
found = true;
|
|
363
|
+
valueB = valB.get(keyA);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
for (const [keyB, vb] of valB.entries()) {
|
|
367
|
+
if (recursiveEquals(keyA, keyB)) {
|
|
368
|
+
found = true;
|
|
369
|
+
valueB = vb;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!found || !recursiveEquals(valueA, valueB)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if ((valA instanceof Set) && (valB instanceof Set)) {
|
|
381
|
+
if (valA.size !== valB.size) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
for (const itemA of valA) {
|
|
385
|
+
if (valB.has(itemA)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
let found = false;
|
|
389
|
+
for (const itemB of valB) {
|
|
390
|
+
if (recursiveEquals(itemA, itemB)) {
|
|
391
|
+
found = true;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (!found) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (ArrayBuffer.isView(valA) && ArrayBuffer.isView(valB)) {
|
|
402
|
+
if (hasNumericLength(valA) && hasNumericLength(valB)) {
|
|
403
|
+
const lengthA = valA.length;
|
|
404
|
+
const lengthB = valB.length;
|
|
405
|
+
if (lengthA !== lengthB) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any -- TypedArray index access
|
|
409
|
+
const a = valA;
|
|
410
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any -- TypedArray index access
|
|
411
|
+
const b = valB;
|
|
412
|
+
for (let i = lengthA; i-- !== 0;) {
|
|
413
|
+
if (a[i] !== b[i]) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
if (isDataViewLike(valA) && isDataViewLike(valB)) {
|
|
420
|
+
const byteLengthA = valA.byteLength;
|
|
421
|
+
const byteLengthB = valB.byteLength;
|
|
422
|
+
if (byteLengthA !== byteLengthB) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
for (let i = byteLengthA; i-- !== 0;) {
|
|
426
|
+
if (valA.getUint8(i) !== valB.getUint8(i)) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
if (isRegExp(valA)) {
|
|
435
|
+
if (!isRegExp(valB)) {
|
|
436
|
+
// Type mismatch - this is normal comparison behavior, not an error
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
return valA.source === valB.source && valA.flags === valB.flags;
|
|
440
|
+
}
|
|
441
|
+
// Type guard: check for valueOf method
|
|
442
|
+
const valueOfA = safeGetProperty(valA, 'valueOf');
|
|
443
|
+
if (typeof valueOfA === 'function' && valueOfA !== Object.prototype.valueOf) {
|
|
444
|
+
try {
|
|
445
|
+
const valueOfB = safeGetProperty(valB, 'valueOf');
|
|
446
|
+
if (typeof valueOfB === 'function' && valueOfB !== Object.prototype.valueOf) {
|
|
447
|
+
const resultA = Function.prototype.call.call(valueOfA, valA);
|
|
448
|
+
const resultB = Function.prototype.call.call(valueOfB, valB);
|
|
449
|
+
return resultA === resultB;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
// Error calling valueOf - this indicates the method exists but threw an error
|
|
454
|
+
// This is unexpected and indicates invalid data or implementation
|
|
455
|
+
console.error('LeUtils.equals: error calling valueOf - invalid data or implementation', e);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Type guard: check for toString method
|
|
459
|
+
const toStringA = safeGetProperty(valA, 'toString');
|
|
460
|
+
if (typeof toStringA === 'function' && toStringA !== Object.prototype.toString) {
|
|
461
|
+
try {
|
|
462
|
+
const toStringB = safeGetProperty(valB, 'toString');
|
|
463
|
+
if (typeof toStringB === 'function' && toStringB !== Object.prototype.toString) {
|
|
464
|
+
const resultA = Function.prototype.call.call(toStringA, valA);
|
|
465
|
+
const resultB = Function.prototype.call.call(toStringB, valB);
|
|
466
|
+
return resultA === resultB;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
// Error calling toString - this indicates the method exists but threw an error
|
|
471
|
+
// This is unexpected and indicates invalid data or implementation
|
|
472
|
+
console.error('LeUtils.equals: error calling toString - invalid data or implementation', e);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const constructorA = safeGetProperty(valA, 'constructor');
|
|
476
|
+
const constructorB = safeGetProperty(valB, 'constructor');
|
|
477
|
+
if (constructorA !== constructorB) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
if (!isRecordLike(valA) || !isRecordLike(valB)) {
|
|
481
|
+
// Type mismatch - this is normal comparison behavior, not an error
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const keys = Object.keys(valA);
|
|
485
|
+
const length = keys.length;
|
|
486
|
+
if (length !== Object.keys(valB).length) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
for (let i = length; i-- !== 0;) {
|
|
490
|
+
if (!Object.prototype.hasOwnProperty.call(valB, keys[i])) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (let i = length; i-- !== 0;) {
|
|
495
|
+
const key = keys[i];
|
|
496
|
+
const typeofA = safeGetProperty(valA, '$$typeof');
|
|
497
|
+
if ((key === '_owner') && typeofA) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const aVal = valA[key];
|
|
501
|
+
const bVal = valB[key];
|
|
502
|
+
if (!recursiveEquals(aVal, bVal)) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
// true if both are NaN, false otherwise
|
|
509
|
+
return ((valA !== valA) && (valB !== valB));
|
|
510
|
+
}
|
|
511
|
+
return recursiveEquals(a, b);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Deep clones a value.
|
|
515
|
+
*
|
|
516
|
+
* @param value - Value to clone
|
|
517
|
+
* @returns Deep cloned value
|
|
518
|
+
*/
|
|
519
|
+
export function clone(value) {
|
|
520
|
+
const seen = new WeakMap();
|
|
521
|
+
function recursiveClone(val) {
|
|
522
|
+
if (val === null || typeof val !== 'object') {
|
|
523
|
+
return val;
|
|
524
|
+
}
|
|
525
|
+
if (typeof val === 'function') {
|
|
526
|
+
return val;
|
|
527
|
+
}
|
|
528
|
+
if (seen.has(val)) {
|
|
529
|
+
return seen.get(val);
|
|
530
|
+
}
|
|
531
|
+
let result;
|
|
532
|
+
if (isUnknownArray(val)) {
|
|
533
|
+
const resultArray = [];
|
|
534
|
+
result = resultArray;
|
|
535
|
+
seen.set(val, result);
|
|
536
|
+
for (let i = 0; i < val.length; i++) {
|
|
537
|
+
resultArray.push(recursiveClone(val[i]));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else if (val instanceof Date) {
|
|
541
|
+
result = new Date(val.getTime());
|
|
542
|
+
seen.set(val, result);
|
|
543
|
+
}
|
|
544
|
+
else if (isRegExp(val)) {
|
|
545
|
+
result = new RegExp(val.source, val.flags);
|
|
546
|
+
seen.set(val, result);
|
|
547
|
+
}
|
|
548
|
+
else if (isMapLike(val)) {
|
|
549
|
+
const resultMap = new Map();
|
|
550
|
+
result = resultMap;
|
|
551
|
+
seen.set(val, result);
|
|
552
|
+
for (const [k, v] of val.entries()) {
|
|
553
|
+
resultMap.set(recursiveClone(k), recursiveClone(v));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else if (isSetLike(val)) {
|
|
557
|
+
const resultSet = new Set();
|
|
558
|
+
result = resultSet;
|
|
559
|
+
seen.set(val, result);
|
|
560
|
+
for (const v of val.values()) {
|
|
561
|
+
resultSet.add(recursiveClone(v));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else if (isArrayBufferView(val)) {
|
|
565
|
+
// TypedArray clone
|
|
566
|
+
if (!hasConstructor(val)) {
|
|
567
|
+
console.error('LeUtils.clone: ArrayBufferView value does not have constructor property');
|
|
568
|
+
return value;
|
|
569
|
+
}
|
|
570
|
+
const Constructor = val.constructor;
|
|
571
|
+
// Create a new buffer to ensure independent memory
|
|
572
|
+
const buffer = val.buffer.slice(val.byteOffset, val.byteOffset + val.byteLength);
|
|
573
|
+
result = new Constructor(buffer);
|
|
574
|
+
seen.set(val, result);
|
|
575
|
+
}
|
|
576
|
+
else if (isRecordLike(val)) {
|
|
577
|
+
// Generic object
|
|
578
|
+
const resultObj = Object.create(Object.getPrototypeOf(val));
|
|
579
|
+
result = resultObj;
|
|
580
|
+
seen.set(val, result);
|
|
581
|
+
for (const key of Object.keys(val)) {
|
|
582
|
+
resultObj[key] = recursiveClone(val[key]);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
// Unknown object type - this indicates an unsupported type being cloned
|
|
587
|
+
console.error('LeUtils.clone: unsupported object type, cannot clone properly', val);
|
|
588
|
+
return value;
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
const cloned = recursiveClone(value);
|
|
593
|
+
if (cloned === undefined && value !== undefined) {
|
|
594
|
+
// Clone should only return undefined if input is undefined - otherwise it's a bug
|
|
595
|
+
console.error('LeUtils.clone: clone returned undefined for non-undefined input - cloning failed');
|
|
596
|
+
return value;
|
|
597
|
+
}
|
|
598
|
+
// Type guard: verify cloned value matches expected type T
|
|
599
|
+
// Since we're cloning, the structure should match, but TypeScript can't verify this
|
|
600
|
+
// We use a type predicate approach: if value is T, then cloned should also be T
|
|
601
|
+
// This is safe because clone preserves structure
|
|
602
|
+
// The only way to satisfy TypeScript here is through a type assertion, but we've validated the clone succeeded
|
|
603
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
604
|
+
return cloned;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Checks if the given elements can be iterated over using each().
|
|
608
|
+
*
|
|
609
|
+
* @param elements - Value to check
|
|
610
|
+
* @returns true if can iterate
|
|
611
|
+
*/
|
|
612
|
+
export function supportsEach(elements) {
|
|
613
|
+
if ((elements === null) || (typeof elements === 'undefined') || (typeof elements === 'string')) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
return !!((Array.isArray(elements))
|
|
617
|
+
|| isPlainObject(elements)
|
|
618
|
+
|| isIterable(elements)
|
|
619
|
+
|| hasForEach(elements)
|
|
620
|
+
|| ((typeof elements === 'object') || (typeof elements === 'function')));
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Returns an iterator that yields [value, key/index] for each element.
|
|
624
|
+
*
|
|
625
|
+
* @param elements - Collection to iterate
|
|
626
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty check
|
|
627
|
+
* @yields [value, key/index]
|
|
628
|
+
*/
|
|
629
|
+
export function* eachIterator(elements, optionalSkipHasOwnPropertyCheck = false) {
|
|
630
|
+
if ((elements === null) || (typeof elements === 'undefined')) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (Array.isArray(elements)) {
|
|
634
|
+
for (let i = 0; i < elements.length; i++) {
|
|
635
|
+
yield [elements[i], i];
|
|
636
|
+
}
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (elements instanceof Map) {
|
|
640
|
+
for (const [i, value] of elements) {
|
|
641
|
+
yield [value, i];
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (elements instanceof Set) {
|
|
646
|
+
for (const value of elements) {
|
|
647
|
+
yield [value, value];
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (isPlainObject(elements)) {
|
|
652
|
+
for (const i in elements) {
|
|
653
|
+
if ((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, i)) {
|
|
654
|
+
yield [elements[i], i];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (typeof elements !== 'string') {
|
|
660
|
+
if (isIterable(elements)) {
|
|
661
|
+
let i = 0;
|
|
662
|
+
for (const value of elements) {
|
|
663
|
+
yield [value, i];
|
|
664
|
+
i++;
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (hasForEach(elements)) {
|
|
669
|
+
const buffer = [];
|
|
670
|
+
elements.forEach((value, i) => {
|
|
671
|
+
buffer.push([value, i]);
|
|
672
|
+
});
|
|
673
|
+
for (const entry of buffer) {
|
|
674
|
+
yield entry;
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if ((typeof elements === 'object') && (elements !== null)) {
|
|
680
|
+
// Last resort: try to iterate over any object properties
|
|
681
|
+
if (isRecordLike(elements)) {
|
|
682
|
+
for (const i in elements) {
|
|
683
|
+
if ((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, i)) {
|
|
684
|
+
yield [elements[i], i];
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Reached end of function without yielding - invalid/unsupported type
|
|
691
|
+
// This is unexpected - user passed a type that can't be iterated
|
|
692
|
+
console.error('LeUtils.eachIterator: executed on invalid/unsupported type', typeof elements, elements);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Loops through each element and calls callback.
|
|
696
|
+
* Callback can return false to break iteration.
|
|
697
|
+
*
|
|
698
|
+
* @param elements - Collection to iterate
|
|
699
|
+
* @param callback - Function called for each element
|
|
700
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty check
|
|
701
|
+
* @returns Original elements
|
|
702
|
+
*/
|
|
703
|
+
export function each(elements, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
704
|
+
if (typeof callback !== 'function') {
|
|
705
|
+
throw new TypeError('The given callback is not a function');
|
|
706
|
+
}
|
|
707
|
+
for (const [value, key] of eachIterator(elements, optionalSkipHasOwnPropertyCheck)) {
|
|
708
|
+
if (callback.call(value, value, key) === false) {
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return elements;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Like each(), except that it expects an async callback.
|
|
716
|
+
*
|
|
717
|
+
* @param elements - Collection to iterate
|
|
718
|
+
* @param asyncCallback - Async function called for each element
|
|
719
|
+
* @param parallelCount - Number of parallel executions (default 1)
|
|
720
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty check
|
|
721
|
+
* @returns Promise resolving to original elements
|
|
722
|
+
*/
|
|
723
|
+
export async function eachAsync(elements, asyncCallback, parallelCount = 1, optionalSkipHasOwnPropertyCheck = false) {
|
|
724
|
+
if ((elements === null) || (typeof elements === 'undefined')) {
|
|
725
|
+
return elements;
|
|
726
|
+
}
|
|
727
|
+
if (typeof asyncCallback !== 'function') {
|
|
728
|
+
throw new TypeError('The given callback is not a function');
|
|
729
|
+
}
|
|
730
|
+
const pCount = INT_LAX(parallelCount);
|
|
731
|
+
if (pCount > 1) {
|
|
732
|
+
const runningPromises = new Set();
|
|
733
|
+
let doBreak = false;
|
|
734
|
+
for (const [value, key] of eachIterator(elements, optionalSkipHasOwnPropertyCheck)) {
|
|
735
|
+
if (doBreak)
|
|
736
|
+
break;
|
|
737
|
+
while (runningPromises.size >= pCount) {
|
|
738
|
+
await Promise.race(runningPromises);
|
|
739
|
+
if (doBreak)
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
const promise = (async () => {
|
|
743
|
+
if ((await asyncCallback.call(value, value, key)) === false) {
|
|
744
|
+
doBreak = true;
|
|
745
|
+
}
|
|
746
|
+
})();
|
|
747
|
+
runningPromises.add(promise);
|
|
748
|
+
promise.finally(() => {
|
|
749
|
+
runningPromises.delete(promise);
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
await Promise.all(runningPromises);
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
for (const [value, key] of eachIterator(elements, optionalSkipHasOwnPropertyCheck)) {
|
|
756
|
+
if ((await asyncCallback.call(value, value, key)) === false) {
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return elements;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Returns empty collection matching type of input, plus add function.
|
|
765
|
+
*
|
|
766
|
+
* @param elements - Source collection to match type
|
|
767
|
+
* @returns [success, emptyCollection, addFunction]
|
|
768
|
+
*/
|
|
769
|
+
export function getEmptySimplifiedCollection(elements) {
|
|
770
|
+
if ((elements === null) || (typeof elements === 'undefined')) {
|
|
771
|
+
return [false, [], (_value, _index) => { }];
|
|
772
|
+
}
|
|
773
|
+
let collection = null;
|
|
774
|
+
let add = null;
|
|
775
|
+
if (Array.isArray(elements)) {
|
|
776
|
+
const arrayCollection = [];
|
|
777
|
+
collection = arrayCollection;
|
|
778
|
+
add = (value, _index) => {
|
|
779
|
+
arrayCollection.push(value);
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
else if (isPlainObject(elements)) {
|
|
783
|
+
const objectCollection = {};
|
|
784
|
+
collection = objectCollection;
|
|
785
|
+
add = (value, index) => {
|
|
786
|
+
const stringIndex = typeof index === 'string' ? index : String(index);
|
|
787
|
+
objectCollection[stringIndex] = value;
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
else if (elements instanceof Map) {
|
|
791
|
+
const mapCollection = new Map();
|
|
792
|
+
collection = mapCollection;
|
|
793
|
+
add = (value, index) => {
|
|
794
|
+
mapCollection.set(index, value);
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
else if ((typeof elements !== 'string') && (elements !== null) && (isIterable(elements) || hasForEach(elements))) {
|
|
798
|
+
const arrayCollection = [];
|
|
799
|
+
collection = arrayCollection;
|
|
800
|
+
add = (value, _index) => {
|
|
801
|
+
arrayCollection.push(value);
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
else if ((typeof elements === 'object' || typeof elements === 'function') && (elements !== null)) {
|
|
805
|
+
const objectCollection = {};
|
|
806
|
+
collection = objectCollection;
|
|
807
|
+
add = (value, index) => {
|
|
808
|
+
const stringIndex = typeof index === 'string' ? index : String(index);
|
|
809
|
+
objectCollection[stringIndex] = value;
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
// Invalid/unsupported type - this is unexpected
|
|
814
|
+
console.error('LeUtils.getEmptySimplifiedCollection: executed on invalid/unsupported type', typeof elements, elements);
|
|
815
|
+
return [false, [], (_value, _index) => { }];
|
|
816
|
+
}
|
|
817
|
+
return [true, collection, add];
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Filters collection, returning only elements matching predicate.
|
|
821
|
+
*
|
|
822
|
+
* @param elements - Collection to filter
|
|
823
|
+
* @param callback - Predicate function (truthy = keep)
|
|
824
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
825
|
+
* @returns Filtered collection (same type as input)
|
|
826
|
+
*/
|
|
827
|
+
export function filter(elements, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
828
|
+
if (callback && (typeof callback !== 'function')) {
|
|
829
|
+
throw new TypeError('The given callback is not a function');
|
|
830
|
+
}
|
|
831
|
+
const [success, collection, add] = getEmptySimplifiedCollection(elements);
|
|
832
|
+
if (!success) {
|
|
833
|
+
return elements;
|
|
834
|
+
}
|
|
835
|
+
each(elements, (value, index) => {
|
|
836
|
+
if (!callback) {
|
|
837
|
+
if (value) {
|
|
838
|
+
add(value, index);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else if (callback.call(value, value, index)) {
|
|
842
|
+
add(value, index);
|
|
843
|
+
}
|
|
844
|
+
}, optionalSkipHasOwnPropertyCheck);
|
|
845
|
+
return collection;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Maps collection, transforming each element.
|
|
849
|
+
*
|
|
850
|
+
* @param elements - Collection to map
|
|
851
|
+
* @param callback - Transform function
|
|
852
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
853
|
+
* @returns Mapped collection (same type as input)
|
|
854
|
+
*/
|
|
855
|
+
export function map(elements, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
856
|
+
if (callback && (typeof callback !== 'function')) {
|
|
857
|
+
throw new TypeError('The given callback is not a function');
|
|
858
|
+
}
|
|
859
|
+
const [success, collection, add] = getEmptySimplifiedCollection(elements);
|
|
860
|
+
if (!success) {
|
|
861
|
+
return elements;
|
|
862
|
+
}
|
|
863
|
+
each(elements, (value, index) => {
|
|
864
|
+
if (!callback) {
|
|
865
|
+
add(value, index);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
add(callback.call(value, value, index), index);
|
|
869
|
+
}
|
|
870
|
+
}, optionalSkipHasOwnPropertyCheck);
|
|
871
|
+
return collection;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Maps collection to array.
|
|
875
|
+
*
|
|
876
|
+
* @param elements - Collection to map
|
|
877
|
+
* @param callback - Transform function
|
|
878
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
879
|
+
* @returns Array of transformed values
|
|
880
|
+
*/
|
|
881
|
+
export function mapToArray(elements, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
882
|
+
if (callback && (typeof callback !== 'function')) {
|
|
883
|
+
throw new TypeError('The given callback is not a function');
|
|
884
|
+
}
|
|
885
|
+
const result = [];
|
|
886
|
+
each(elements, (value, index) => {
|
|
887
|
+
if (!callback) {
|
|
888
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
889
|
+
result.push(value);
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
result.push(callback.call(value, value, index));
|
|
893
|
+
}
|
|
894
|
+
}, optionalSkipHasOwnPropertyCheck);
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Sorts keys by comparing values with comparator.
|
|
899
|
+
*
|
|
900
|
+
* @param elements - Collection to get keys from
|
|
901
|
+
* @param comparator - Comparison function
|
|
902
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
903
|
+
* @returns Sorted array of keys
|
|
904
|
+
*/
|
|
905
|
+
export function sortKeys(elements, comparator, optionalSkipHasOwnPropertyCheck = false) {
|
|
906
|
+
if (typeof comparator !== 'function') {
|
|
907
|
+
throw new TypeError('The given comparator is not a function');
|
|
908
|
+
}
|
|
909
|
+
const keys = [];
|
|
910
|
+
each(elements, (_value, index) => {
|
|
911
|
+
keys.push(index);
|
|
912
|
+
}, optionalSkipHasOwnPropertyCheck);
|
|
913
|
+
keys.sort((a, b) => comparator(getValueAtIndex(elements, a, optionalSkipHasOwnPropertyCheck), getValueAtIndex(elements, b, optionalSkipHasOwnPropertyCheck)));
|
|
914
|
+
return keys;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Gets value at index/key from collection.
|
|
918
|
+
*
|
|
919
|
+
* @param elements - Collection
|
|
920
|
+
* @param index - Key or index
|
|
921
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
922
|
+
* @returns Value at index, or undefined
|
|
923
|
+
*/
|
|
924
|
+
export function getValueAtIndex(elements, index, optionalSkipHasOwnPropertyCheck = false) {
|
|
925
|
+
if ((elements === null) || (typeof elements === 'undefined')) {
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
if (Array.isArray(elements)) {
|
|
929
|
+
const numIndex = typeof index === 'number' ? index : Number(index);
|
|
930
|
+
if (Number.isNaN(numIndex)) {
|
|
931
|
+
console.error('LeUtils.getValueAtIndex: invalid array index (not a number)', index);
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
return elements[numIndex];
|
|
935
|
+
}
|
|
936
|
+
if (elements instanceof Map) {
|
|
937
|
+
return elements.get(index);
|
|
938
|
+
}
|
|
939
|
+
if (elements instanceof Set) {
|
|
940
|
+
return index;
|
|
941
|
+
}
|
|
942
|
+
if (typeof elements !== 'string') {
|
|
943
|
+
if (ArrayBuffer.isView(elements) && !(elements instanceof DataView)) {
|
|
944
|
+
// TypedArray - access by numeric index
|
|
945
|
+
if (hasNumericLength(elements)) {
|
|
946
|
+
const numIndex = typeof index === 'number' ? index : Number(index);
|
|
947
|
+
if (Number.isNaN(numIndex)) {
|
|
948
|
+
console.error('LeUtils.getValueAtIndex: invalid TypedArray index (not a number)', index);
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
return elements[numIndex];
|
|
952
|
+
}
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
if (isIterable(elements)) {
|
|
956
|
+
let i = 0;
|
|
957
|
+
for (const value of elements) {
|
|
958
|
+
if (i === index) {
|
|
959
|
+
return value;
|
|
960
|
+
}
|
|
961
|
+
i++;
|
|
962
|
+
}
|
|
963
|
+
return undefined;
|
|
964
|
+
}
|
|
965
|
+
if (hasForEach(elements)) {
|
|
966
|
+
let result = undefined;
|
|
967
|
+
let shouldContinue = true;
|
|
968
|
+
elements.forEach((value, i) => {
|
|
969
|
+
if (shouldContinue) {
|
|
970
|
+
if (i === index) {
|
|
971
|
+
result = value;
|
|
972
|
+
shouldContinue = false;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if ((typeof elements === 'object') || (typeof elements === 'function')) {
|
|
980
|
+
// Type guard: verify elements is Record-like for property access
|
|
981
|
+
if (isRecordLike(elements)) {
|
|
982
|
+
const propertyKey = typeof index === 'string' || typeof index === 'number' || typeof index === 'symbol' ? index : String(index);
|
|
983
|
+
if ((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, propertyKey)) {
|
|
984
|
+
const stringKey = typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
985
|
+
return elements[stringKey];
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Flattens nested arrays to single level (like Array.flat(Infinity)).
|
|
994
|
+
*
|
|
995
|
+
* @param array - Array to flatten
|
|
996
|
+
* @returns Flattened array
|
|
997
|
+
*/
|
|
998
|
+
export function flattenArray(array) {
|
|
999
|
+
if (!Array.isArray(array)) {
|
|
1000
|
+
return [array];
|
|
1001
|
+
}
|
|
1002
|
+
const result = [];
|
|
1003
|
+
const flattenRecursive = (arr) => {
|
|
1004
|
+
if (!Array.isArray(arr)) {
|
|
1005
|
+
result.push(arr);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
arr.forEach((entry) => {
|
|
1009
|
+
flattenRecursive(entry);
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
array.forEach((entry) => {
|
|
1013
|
+
flattenRecursive(entry);
|
|
1014
|
+
});
|
|
1015
|
+
return result;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Flattens any collection (arrays, Sets, Maps, objects) to single array.
|
|
1019
|
+
*
|
|
1020
|
+
* @param elements - Collection to flatten
|
|
1021
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
1022
|
+
* @returns Flattened array
|
|
1023
|
+
*/
|
|
1024
|
+
export function flattenToArray(elements, optionalSkipHasOwnPropertyCheck = false) {
|
|
1025
|
+
const result = [];
|
|
1026
|
+
const flattenRecursive = (value) => {
|
|
1027
|
+
if (Array.isArray(value)) {
|
|
1028
|
+
value.forEach((entry) => {
|
|
1029
|
+
flattenRecursive(entry);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
else if (supportsEach(value)) {
|
|
1033
|
+
each(value, (v) => {
|
|
1034
|
+
flattenRecursive(v);
|
|
1035
|
+
}, optionalSkipHasOwnPropertyCheck);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
result.push(value);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
flattenRecursive(elements);
|
|
1042
|
+
return result;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Maps and sorts collection to array.
|
|
1046
|
+
*
|
|
1047
|
+
* @param elements - Collection to process
|
|
1048
|
+
* @param comparator - Sort comparison function
|
|
1049
|
+
* @param callback - Optional transform
|
|
1050
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
1051
|
+
* @returns Sorted array
|
|
1052
|
+
*/
|
|
1053
|
+
export function mapToArraySorted(elements, comparator, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
1054
|
+
if (callback && (typeof callback !== 'function')) {
|
|
1055
|
+
throw new TypeError('The given callback is not a function');
|
|
1056
|
+
}
|
|
1057
|
+
const keys = sortKeys(elements, comparator, optionalSkipHasOwnPropertyCheck);
|
|
1058
|
+
const result = [];
|
|
1059
|
+
for (const key of keys) {
|
|
1060
|
+
const value = getValueAtIndex(elements, key, optionalSkipHasOwnPropertyCheck);
|
|
1061
|
+
if (!callback) {
|
|
1062
|
+
result.push(value);
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
result.push(callback.call(value, value, key));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return result;
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Generic comparison function for sorting.
|
|
1072
|
+
*
|
|
1073
|
+
* @param a - First value
|
|
1074
|
+
* @param b - Second value
|
|
1075
|
+
* @returns -1 if a < b, 1 if a > b, 0 if equal
|
|
1076
|
+
*/
|
|
1077
|
+
export function compare(a, b) {
|
|
1078
|
+
// compare() accepts any types and uses JavaScript's native comparison operators
|
|
1079
|
+
// We can't avoid type coercion here as the function is designed to work with any types
|
|
1080
|
+
// Use Function.prototype.call to compare without type assertions
|
|
1081
|
+
const compareResult = (valueA, valueB) => {
|
|
1082
|
+
// Use JavaScript's native comparison (handles type coercion automatically)
|
|
1083
|
+
// @ts-ignore - Intentional: compare() is designed to work with any types using native JS comparison
|
|
1084
|
+
if (valueA < valueB) {
|
|
1085
|
+
return -1;
|
|
1086
|
+
}
|
|
1087
|
+
// @ts-ignore - Intentional: compare() is designed to work with any types using native JS comparison
|
|
1088
|
+
if (valueA > valueB) {
|
|
1089
|
+
return 1;
|
|
1090
|
+
}
|
|
1091
|
+
return 0;
|
|
1092
|
+
};
|
|
1093
|
+
return compareResult(a, b);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Compares two numbers (for sorting).
|
|
1097
|
+
*
|
|
1098
|
+
* @param a - First number
|
|
1099
|
+
* @param b - Second number
|
|
1100
|
+
* @returns Difference (a - b)
|
|
1101
|
+
*/
|
|
1102
|
+
export function compareNumbers(a, b) {
|
|
1103
|
+
return a - b;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Compares two numeric strings (like version numbers).
|
|
1107
|
+
* Splits on '.', compares each part numerically considering length.
|
|
1108
|
+
*
|
|
1109
|
+
* @param a - First numeric string
|
|
1110
|
+
* @param b - Second numeric string
|
|
1111
|
+
* @returns Comparison result
|
|
1112
|
+
*/
|
|
1113
|
+
export function compareNumericStrings(a, b) {
|
|
1114
|
+
const aParts = STRING(a).split('.');
|
|
1115
|
+
const bParts = STRING(b).split('.');
|
|
1116
|
+
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
|
1117
|
+
const aPart = aParts[i].trim();
|
|
1118
|
+
const bPart = bParts[i].trim();
|
|
1119
|
+
if (aPart.length !== bPart.length) {
|
|
1120
|
+
return (aPart.length < bPart.length) ? -1 : 1;
|
|
1121
|
+
}
|
|
1122
|
+
if (aPart !== bPart) {
|
|
1123
|
+
return (aPart < bPart) ? -1 : 1;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (aParts.length !== bParts.length) {
|
|
1127
|
+
return (aParts.length < bParts.length) ? -1 : 1;
|
|
1128
|
+
}
|
|
1129
|
+
return 0;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Natural string comparison (numbers in strings compared numerically).
|
|
1133
|
+
* "file5.txt" sorts before "file10.txt".
|
|
1134
|
+
*
|
|
1135
|
+
* @param a - First string
|
|
1136
|
+
* @param b - Second string
|
|
1137
|
+
* @returns Comparison result
|
|
1138
|
+
*/
|
|
1139
|
+
export function compareNaturalStrings(a, b) {
|
|
1140
|
+
const re = /(\d+|\D+)/g; // Split into runs of digits or non-digits
|
|
1141
|
+
const aTokens = a.match(re) ?? [];
|
|
1142
|
+
const bTokens = b.match(re) ?? [];
|
|
1143
|
+
const len = Math.min(aTokens.length, bTokens.length);
|
|
1144
|
+
for (let i = 0; i < len; i++) {
|
|
1145
|
+
const x = aTokens[i];
|
|
1146
|
+
const y = bTokens[i];
|
|
1147
|
+
if (x === y) {
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
// If both are numbers, compare numerically
|
|
1151
|
+
const nx = parseInt(x, 10);
|
|
1152
|
+
const ny = parseInt(y, 10);
|
|
1153
|
+
if (!isNaN(nx) && !isNaN(ny)) {
|
|
1154
|
+
return nx - ny;
|
|
1155
|
+
}
|
|
1156
|
+
// Otherwise compare lexically
|
|
1157
|
+
return x < y ? -1 : 1;
|
|
1158
|
+
}
|
|
1159
|
+
return aTokens.length - bTokens.length;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Checks if object is empty (no own properties).
|
|
1163
|
+
*
|
|
1164
|
+
* @param obj - Object to check
|
|
1165
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
1166
|
+
* @returns true if empty
|
|
1167
|
+
*/
|
|
1168
|
+
export function isEmptyObject(obj, optionalSkipHasOwnPropertyCheck = false) {
|
|
1169
|
+
if (obj === null || typeof obj !== 'object') {
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
// Type guard: obj is already verified as object, can iterate
|
|
1173
|
+
if (isRecordLike(obj)) {
|
|
1174
|
+
for (const field in obj) {
|
|
1175
|
+
if ((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field)) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Counts own properties in object.
|
|
1184
|
+
*
|
|
1185
|
+
* @param obj - Object to count
|
|
1186
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
1187
|
+
* @returns Number of own properties
|
|
1188
|
+
*/
|
|
1189
|
+
export function getObjectFieldsCount(obj, optionalSkipHasOwnPropertyCheck = false) {
|
|
1190
|
+
if (obj === null || typeof obj !== 'object') {
|
|
1191
|
+
return 0;
|
|
1192
|
+
}
|
|
1193
|
+
let count = 0;
|
|
1194
|
+
// Type guard: obj is already verified as object
|
|
1195
|
+
if (isRecordLike(obj)) {
|
|
1196
|
+
for (const field in obj) {
|
|
1197
|
+
if ((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(obj, field)) {
|
|
1198
|
+
count++;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return count;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Capitalizes first character of string.
|
|
1206
|
+
*
|
|
1207
|
+
* @param string - String to capitalize
|
|
1208
|
+
* @returns Capitalized string
|
|
1209
|
+
*/
|
|
1210
|
+
export function capitalize(string) {
|
|
1211
|
+
const str = STRING(string).trim();
|
|
1212
|
+
if (str.length <= 0) {
|
|
1213
|
+
return str;
|
|
1214
|
+
}
|
|
1215
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Checks if string ends with any of the given suffixes.
|
|
1219
|
+
*
|
|
1220
|
+
* @param string - String to check
|
|
1221
|
+
* @param endingCharsStringOrArray - String of chars or array of suffixes
|
|
1222
|
+
* @returns true if ends with any
|
|
1223
|
+
*/
|
|
1224
|
+
export function endsWithAny(string, endingCharsStringOrArray) {
|
|
1225
|
+
const str = STRING(string);
|
|
1226
|
+
let endingCharsArray;
|
|
1227
|
+
if (Array.isArray(endingCharsStringOrArray)) {
|
|
1228
|
+
endingCharsArray = endingCharsStringOrArray;
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
endingCharsArray = STRING(endingCharsStringOrArray).split('');
|
|
1232
|
+
}
|
|
1233
|
+
let result = false;
|
|
1234
|
+
each(endingCharsArray, (chars) => {
|
|
1235
|
+
if (str.endsWith(STRING(chars))) {
|
|
1236
|
+
result = true;
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
return result;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Checks if string starts with any of the given prefixes.
|
|
1244
|
+
*
|
|
1245
|
+
* @param string - String to check
|
|
1246
|
+
* @param startingCharsStringOrArray - String of chars or array of prefixes
|
|
1247
|
+
* @returns true if starts with any
|
|
1248
|
+
*/
|
|
1249
|
+
export function startsWithAny(string, startingCharsStringOrArray) {
|
|
1250
|
+
const str = STRING(string);
|
|
1251
|
+
let startingCharsArray;
|
|
1252
|
+
if (Array.isArray(startingCharsStringOrArray)) {
|
|
1253
|
+
startingCharsArray = startingCharsStringOrArray;
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
startingCharsArray = STRING(startingCharsStringOrArray).split('');
|
|
1257
|
+
}
|
|
1258
|
+
let result = false;
|
|
1259
|
+
each(startingCharsArray, (chars) => {
|
|
1260
|
+
if (str.startsWith(STRING(chars))) {
|
|
1261
|
+
result = true;
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
return result;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Trims specific characters from end of string.
|
|
1269
|
+
*
|
|
1270
|
+
* @param string - String to trim
|
|
1271
|
+
* @param trimCharsStringOrArray - Characters to remove
|
|
1272
|
+
* @returns Trimmed string
|
|
1273
|
+
*/
|
|
1274
|
+
export function trimEnd(string, trimCharsStringOrArray) {
|
|
1275
|
+
let str = STRING(string);
|
|
1276
|
+
let endingCharsArray;
|
|
1277
|
+
if (Array.isArray(trimCharsStringOrArray)) {
|
|
1278
|
+
endingCharsArray = trimCharsStringOrArray;
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
endingCharsArray = STRING(trimCharsStringOrArray).split('');
|
|
1282
|
+
}
|
|
1283
|
+
let run = true;
|
|
1284
|
+
const trimChars = (chars) => {
|
|
1285
|
+
const charsStr = STRING(chars);
|
|
1286
|
+
if (str.endsWith(charsStr)) {
|
|
1287
|
+
str = str.substring(0, str.length - charsStr.length);
|
|
1288
|
+
run = true;
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
while (run) {
|
|
1292
|
+
run = false;
|
|
1293
|
+
each(endingCharsArray, trimChars);
|
|
1294
|
+
}
|
|
1295
|
+
return str;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Trims specific characters from start of string.
|
|
1299
|
+
*
|
|
1300
|
+
* @param string - String to trim
|
|
1301
|
+
* @param trimCharsStringOrArray - Characters to remove
|
|
1302
|
+
* @returns Trimmed string
|
|
1303
|
+
*/
|
|
1304
|
+
export function trimStart(string, trimCharsStringOrArray) {
|
|
1305
|
+
let str = STRING(string);
|
|
1306
|
+
let startingCharsArray;
|
|
1307
|
+
if (Array.isArray(trimCharsStringOrArray)) {
|
|
1308
|
+
startingCharsArray = trimCharsStringOrArray;
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
startingCharsArray = STRING(trimCharsStringOrArray).split('');
|
|
1312
|
+
}
|
|
1313
|
+
let run = true;
|
|
1314
|
+
const trimChars = (chars) => {
|
|
1315
|
+
const charsStr = STRING(chars);
|
|
1316
|
+
if (str.startsWith(charsStr)) {
|
|
1317
|
+
str = str.substring(charsStr.length);
|
|
1318
|
+
run = true;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
while (run) {
|
|
1322
|
+
run = false;
|
|
1323
|
+
each(startingCharsArray, trimChars);
|
|
1324
|
+
}
|
|
1325
|
+
return str;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Trims specific characters from both ends of string.
|
|
1329
|
+
*
|
|
1330
|
+
* @param string - String to trim
|
|
1331
|
+
* @param trimCharsStringOrArray - Characters to remove
|
|
1332
|
+
* @returns Trimmed string
|
|
1333
|
+
*/
|
|
1334
|
+
export function trim(string, trimCharsStringOrArray) {
|
|
1335
|
+
return trimStart(trimEnd(string, trimCharsStringOrArray), trimCharsStringOrArray);
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Checks if collection contains value (string comparison).
|
|
1339
|
+
*
|
|
1340
|
+
* @param array - Collection to search
|
|
1341
|
+
* @param value - Value to find
|
|
1342
|
+
* @returns true if found
|
|
1343
|
+
*/
|
|
1344
|
+
export function contains(array, value) {
|
|
1345
|
+
if (!array) {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
let result = false;
|
|
1349
|
+
const valueStr = STRING(value);
|
|
1350
|
+
each(array, (val) => {
|
|
1351
|
+
if (STRING(val) === valueStr) {
|
|
1352
|
+
result = true;
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
return result;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Checks if collection contains value (case-insensitive).
|
|
1360
|
+
*
|
|
1361
|
+
* @param array - Collection to search
|
|
1362
|
+
* @param value - Value to find
|
|
1363
|
+
* @returns true if found
|
|
1364
|
+
*/
|
|
1365
|
+
export function containsCaseInsensitive(array, value) {
|
|
1366
|
+
if (!array) {
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
let result = false;
|
|
1370
|
+
const valueStr = STRING(value).toLowerCase();
|
|
1371
|
+
each(array, (val) => {
|
|
1372
|
+
if (STRING(val).toLowerCase() === valueStr) {
|
|
1373
|
+
result = true;
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
return result;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Checks if collection contains all given values.
|
|
1381
|
+
*
|
|
1382
|
+
* @param array - Collection to search
|
|
1383
|
+
* @param values - Values to find
|
|
1384
|
+
* @returns true if all found
|
|
1385
|
+
*/
|
|
1386
|
+
export function containsAll(array, values) {
|
|
1387
|
+
if (!array) {
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
let result = true;
|
|
1391
|
+
each(values, (value) => {
|
|
1392
|
+
if (!contains(array, value)) {
|
|
1393
|
+
result = false;
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
return result;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Checks if collection contains any of given values.
|
|
1401
|
+
*
|
|
1402
|
+
* @param array - Collection to search
|
|
1403
|
+
* @param values - Values to find
|
|
1404
|
+
* @returns true if any found
|
|
1405
|
+
*/
|
|
1406
|
+
export function containsAny(array, values) {
|
|
1407
|
+
if (!array) {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
let result = false;
|
|
1411
|
+
each(values, (value) => {
|
|
1412
|
+
if (contains(array, value)) {
|
|
1413
|
+
result = true;
|
|
1414
|
+
return false;
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
return result;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Checks if collection contains none of given values.
|
|
1421
|
+
*
|
|
1422
|
+
* @param array - Collection to search
|
|
1423
|
+
* @param values - Values to check
|
|
1424
|
+
* @returns true if none found
|
|
1425
|
+
*/
|
|
1426
|
+
export function containsNone(array, values) {
|
|
1427
|
+
if (!array) {
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
let result = true;
|
|
1431
|
+
each(values, (value) => {
|
|
1432
|
+
if (contains(array, value)) {
|
|
1433
|
+
result = false;
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
return result;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Finds first element matching predicate, returns {index, value}.
|
|
1441
|
+
*
|
|
1442
|
+
* @param elements - Collection to search
|
|
1443
|
+
* @param callback - Predicate function
|
|
1444
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
1445
|
+
* @returns {index, value} or null
|
|
1446
|
+
*/
|
|
1447
|
+
export function findIndexValue(elements, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
1448
|
+
if (typeof callback !== 'function') {
|
|
1449
|
+
throw new TypeError('The given callback is not a function');
|
|
1450
|
+
}
|
|
1451
|
+
let result = null;
|
|
1452
|
+
each(elements, (value, index) => {
|
|
1453
|
+
// Type guard: verify elements is Record-like for callback context
|
|
1454
|
+
if (isRecordLike(elements)) {
|
|
1455
|
+
const propertyKey = typeof index === 'string' || typeof index === 'number' || typeof index === 'symbol' ? index : String(index);
|
|
1456
|
+
const stringKey = typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
1457
|
+
const elementValue = elements[stringKey];
|
|
1458
|
+
if (callback.call(elementValue, elementValue, index)) {
|
|
1459
|
+
result = { index, value };
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
// For non-record-like elements, use value directly
|
|
1465
|
+
if (callback.call(value, value, index)) {
|
|
1466
|
+
result = { index, value };
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}, optionalSkipHasOwnPropertyCheck);
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Finds first element matching predicate, returns index.
|
|
1475
|
+
*
|
|
1476
|
+
* @param elements - Collection to search
|
|
1477
|
+
* @param callback - Predicate function
|
|
1478
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
1479
|
+
* @returns Index/key or null
|
|
1480
|
+
*/
|
|
1481
|
+
export function findIndex(elements, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
1482
|
+
return findIndexValue(elements, callback, optionalSkipHasOwnPropertyCheck)?.index ?? null;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Finds first element matching predicate, returns value.
|
|
1486
|
+
*
|
|
1487
|
+
* @param elements - Collection to search
|
|
1488
|
+
* @param callback - Predicate function
|
|
1489
|
+
* @param optionalSkipHasOwnPropertyCheck - Skip hasOwnProperty
|
|
1490
|
+
* @returns Value or null
|
|
1491
|
+
*/
|
|
1492
|
+
export function find(elements, callback, optionalSkipHasOwnPropertyCheck = false) {
|
|
1493
|
+
return findIndexValue(elements, callback, optionalSkipHasOwnPropertyCheck)?.value ?? null;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Generates a base64 string (with +/ replaced by -_) that is guaranteed to be unique.
|
|
1497
|
+
*
|
|
1498
|
+
* @returns Unique ID string
|
|
1499
|
+
*/
|
|
1500
|
+
export const uniqueId = (() => {
|
|
1501
|
+
let previousUniqueIdsTime = null;
|
|
1502
|
+
const previousUniqueIds = new Map();
|
|
1503
|
+
const generateUniqueId = () => {
|
|
1504
|
+
let now = null;
|
|
1505
|
+
try {
|
|
1506
|
+
// @ts-ignore -- performance might not exist or might not have legacy timing API
|
|
1507
|
+
// Type guard: check if performance has timing property (legacy API)
|
|
1508
|
+
let timingNavStart = 0;
|
|
1509
|
+
if (hasPerformanceTiming(performance) && 'timing' in performance && performance.timing) {
|
|
1510
|
+
const timing = performance.timing;
|
|
1511
|
+
if ('navigationStart' in timing && typeof timing.navigationStart === 'number') {
|
|
1512
|
+
timingNavStart = timing.navigationStart;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const timeOrigin = hasPerformanceTiming(performance) && 'timeOrigin' in performance && typeof performance.timeOrigin === 'number' ? performance.timeOrigin : 0;
|
|
1516
|
+
const nowFunc = hasPerformanceTiming(performance) && 'now' in performance && typeof performance.now === 'function' ? performance.now : (() => 0);
|
|
1517
|
+
now = timeOrigin || timingNavStart || 0;
|
|
1518
|
+
now = now + nowFunc();
|
|
1519
|
+
}
|
|
1520
|
+
catch {
|
|
1521
|
+
// Ignore error
|
|
1522
|
+
}
|
|
1523
|
+
now = now || Date.now();
|
|
1524
|
+
now = Math.round(now);
|
|
1525
|
+
const nowBytes = numberToBytes(now);
|
|
1526
|
+
let uuid = null;
|
|
1527
|
+
try {
|
|
1528
|
+
if (typeof crypto?.randomUUID === 'function') {
|
|
1529
|
+
uuid = base64ToBytes(hexToBase64(crypto.randomUUID()));
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
// Ignore error
|
|
1534
|
+
}
|
|
1535
|
+
if (!uuid) {
|
|
1536
|
+
const bytesChunkA = numberToBytes(Number((Math.random() + ' ').substring(2, 12).padEnd(10, '0')));
|
|
1537
|
+
const bytesChunkB = numberToBytes(Number((Math.random() + ' ').substring(2, 12).padEnd(10, '0')));
|
|
1538
|
+
const bytesChunkC = numberToBytes(Number((Math.random() + ' ').substring(2, 12).padEnd(10, '0')));
|
|
1539
|
+
const bytesChunkD = numberToBytes(Number((Math.random() + ' ').substring(2, 12).padEnd(10, '0')));
|
|
1540
|
+
uuid = new Uint8Array(bytesChunkA.length + bytesChunkB.length + bytesChunkC.length + bytesChunkD.length);
|
|
1541
|
+
uuid.set(bytesChunkA, 0);
|
|
1542
|
+
uuid.set(bytesChunkB, bytesChunkA.length);
|
|
1543
|
+
uuid.set(bytesChunkC, bytesChunkA.length + bytesChunkB.length);
|
|
1544
|
+
uuid.set(bytesChunkD, bytesChunkA.length + bytesChunkB.length + bytesChunkC.length);
|
|
1545
|
+
}
|
|
1546
|
+
const bytes = new Uint8Array(nowBytes.length + uuid.length);
|
|
1547
|
+
bytes.set(nowBytes, 0);
|
|
1548
|
+
bytes.set(uuid, nowBytes.length);
|
|
1549
|
+
const finalUuid = bytesToBase64(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
|
|
1550
|
+
return {
|
|
1551
|
+
time: now,
|
|
1552
|
+
id: finalUuid,
|
|
1553
|
+
};
|
|
1554
|
+
};
|
|
1555
|
+
return () => {
|
|
1556
|
+
while (true) {
|
|
1557
|
+
const result = generateUniqueId();
|
|
1558
|
+
if (previousUniqueIdsTime !== result.time) {
|
|
1559
|
+
previousUniqueIdsTime = result.time;
|
|
1560
|
+
previousUniqueIds.clear();
|
|
1561
|
+
previousUniqueIds.set(result.id, true);
|
|
1562
|
+
return result.id;
|
|
1563
|
+
}
|
|
1564
|
+
else if (previousUniqueIds.get(result.id) !== true) {
|
|
1565
|
+
previousUniqueIds.set(result.id, true);
|
|
1566
|
+
return result.id;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
})();
|
|
1571
|
+
/**
|
|
1572
|
+
* Generates a base64 string (with +/ replaced by -_) of the current time.
|
|
1573
|
+
*
|
|
1574
|
+
* @param now - Optional time to use
|
|
1575
|
+
* @returns Timestamp string
|
|
1576
|
+
*/
|
|
1577
|
+
export const timestamp = (() => {
|
|
1578
|
+
return (now = null) => {
|
|
1579
|
+
let time;
|
|
1580
|
+
if (ISSET(now)) {
|
|
1581
|
+
time = FLOAT_LAX(now);
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
let performanceNow = null;
|
|
1585
|
+
try {
|
|
1586
|
+
// @ts-ignore -- performance might not exist or might not have legacy timing API
|
|
1587
|
+
// Type guard: check if performance has timing property (legacy API)
|
|
1588
|
+
let timingNavStart = 0;
|
|
1589
|
+
if (performance && hasPerformanceTiming(performance) && 'timing' in performance) {
|
|
1590
|
+
const timing = safeGetProperty(performance, 'timing');
|
|
1591
|
+
if (timing && typeof timing === 'object' && timing !== null) {
|
|
1592
|
+
const navStart = safeGetProperty(timing, 'navigationStart');
|
|
1593
|
+
if (typeof navStart === 'number') {
|
|
1594
|
+
timingNavStart = navStart;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const timeOrigin = performance && hasPerformanceTiming(performance) && 'timeOrigin' in performance ? safeGetProperty(performance, 'timeOrigin') : undefined;
|
|
1599
|
+
const nowFunc = performance && hasPerformanceTiming(performance) && 'now' in performance ? safeGetProperty(performance, 'now') : undefined;
|
|
1600
|
+
const timeOriginVal = typeof timeOrigin === 'number' ? timeOrigin : 0;
|
|
1601
|
+
const nowFuncVal = typeof nowFunc === 'function' ? nowFunc() : 0;
|
|
1602
|
+
performanceNow = timeOriginVal || timingNavStart || 0;
|
|
1603
|
+
performanceNow = performanceNow + nowFuncVal;
|
|
1604
|
+
}
|
|
1605
|
+
catch {
|
|
1606
|
+
// Ignore error
|
|
1607
|
+
}
|
|
1608
|
+
time = performanceNow || Date.now();
|
|
1609
|
+
}
|
|
1610
|
+
time = Math.round(time);
|
|
1611
|
+
const nowBytes = numberToBytes(time);
|
|
1612
|
+
return bytesToBase64(nowBytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
|
|
1613
|
+
};
|
|
1614
|
+
})();
|
|
1615
|
+
/**
|
|
1616
|
+
* Checks if value is a generator function.
|
|
1617
|
+
*
|
|
1618
|
+
* @param value - Value to check
|
|
1619
|
+
* @returns true if generator, false otherwise
|
|
1620
|
+
*/
|
|
1621
|
+
export const isGeneratorFunction = (() => {
|
|
1622
|
+
const GeneratorFunction = (function* () { }).constructor;
|
|
1623
|
+
const AsyncGeneratorFunction = (async function* () { }).constructor;
|
|
1624
|
+
return (func) => {
|
|
1625
|
+
if (typeof func !== 'function') {
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
// Type guard: verify func has constructor property
|
|
1629
|
+
if (!hasConstructor(func)) {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
const constructor = func.constructor;
|
|
1633
|
+
return (constructor === GeneratorFunction || constructor === AsyncGeneratorFunction);
|
|
1634
|
+
};
|
|
1635
|
+
})();
|
|
1636
|
+
/**
|
|
1637
|
+
* Environment-safe btoa.
|
|
1638
|
+
*
|
|
1639
|
+
* @param str - String to encode
|
|
1640
|
+
* @returns Base64 string
|
|
1641
|
+
*/
|
|
1642
|
+
export function btoa(str) {
|
|
1643
|
+
if (typeof globalThis.btoa === 'function') {
|
|
1644
|
+
return globalThis.btoa(str);
|
|
1645
|
+
}
|
|
1646
|
+
// Node.js fallback
|
|
1647
|
+
// @ts-ignore -- Buffer might not be defined in non-Node environments
|
|
1648
|
+
if (typeof globalThis.Buffer?.from === 'function') {
|
|
1649
|
+
// @ts-ignore -- Buffer might not be defined in non-Node environments
|
|
1650
|
+
return globalThis.Buffer.from(str, 'binary').toString('base64');
|
|
1651
|
+
}
|
|
1652
|
+
throw new Error('LeUtils.btoa: No btoa implementation found');
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Environment-safe atob.
|
|
1656
|
+
*
|
|
1657
|
+
* @param str - Base64 string to decode
|
|
1658
|
+
* @returns Decoded string
|
|
1659
|
+
*/
|
|
1660
|
+
export function atob(str) {
|
|
1661
|
+
if (typeof globalThis.atob === 'function') {
|
|
1662
|
+
return globalThis.atob(str);
|
|
1663
|
+
}
|
|
1664
|
+
// Node.js fallback
|
|
1665
|
+
// @ts-ignore -- Buffer might not be defined in non-Node environments
|
|
1666
|
+
if (typeof globalThis.Buffer?.from === 'function') {
|
|
1667
|
+
// @ts-ignore -- Buffer might not be defined in non-Node environments
|
|
1668
|
+
return globalThis.Buffer.from(str, 'base64').toString('binary');
|
|
1669
|
+
}
|
|
1670
|
+
throw new Error('LeUtils.atob: No atob implementation found');
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Encodes a UTF-8 string into a base64 string.
|
|
1674
|
+
*
|
|
1675
|
+
* @param string - UTF-8 string
|
|
1676
|
+
* @returns Base64 string
|
|
1677
|
+
*/
|
|
1678
|
+
export function utf8ToBase64(string) {
|
|
1679
|
+
return btoa(encodeURIComponent(string).replace(/%([0-9A-F]{2})/g, (_match, p1) => String.fromCharCode(parseInt(p1, 16))));
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Decodes a base64 string back into a UTF-8 string.
|
|
1683
|
+
*
|
|
1684
|
+
* @param base64string - Base64 string
|
|
1685
|
+
* @returns Decoded UTF-8 string
|
|
1686
|
+
*/
|
|
1687
|
+
export function base64ToUtf8(base64string) {
|
|
1688
|
+
return decodeURIComponent(atob(base64string.trim()).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Converts a base64 string into a hex string.
|
|
1692
|
+
*
|
|
1693
|
+
* @param base64string - Base64 string
|
|
1694
|
+
* @returns Hex string
|
|
1695
|
+
*/
|
|
1696
|
+
export function base64ToHex(base64string) {
|
|
1697
|
+
return atob(base64string.trim()).split('').map((c) => ('0' + c.charCodeAt(0).toString(16)).slice(-2)).join('');
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Converts a hex string into a base64 string.
|
|
1701
|
+
*
|
|
1702
|
+
* @param hexstring - Hex string
|
|
1703
|
+
* @returns Base64 string
|
|
1704
|
+
*/
|
|
1705
|
+
export function hexToBase64(hexstring) {
|
|
1706
|
+
const hexResult = hexstring.replace(/[^0-9A-F]/gi, '').match(/\w{2}/g)?.map((a) => String.fromCharCode(parseInt(a, 16)))?.join('');
|
|
1707
|
+
if (!hexResult) {
|
|
1708
|
+
throw new Error('Invalid hex string: "' + hexstring + '"');
|
|
1709
|
+
}
|
|
1710
|
+
return btoa(hexResult);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Converts a base64 string into bytes (Uint8Array).
|
|
1714
|
+
*
|
|
1715
|
+
* @param base64string - Base64 string
|
|
1716
|
+
* @returns Bytes
|
|
1717
|
+
*/
|
|
1718
|
+
export function base64ToBytes(base64string) {
|
|
1719
|
+
const binary = atob(base64string.trim());
|
|
1720
|
+
const len = binary.length;
|
|
1721
|
+
const data = new Uint8Array(len);
|
|
1722
|
+
for (let i = 0; i < len; i++) {
|
|
1723
|
+
data[i] = binary.charCodeAt(i);
|
|
1724
|
+
}
|
|
1725
|
+
return data;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Converts bytes into a base64 string.
|
|
1729
|
+
*
|
|
1730
|
+
* @param arraybuffer - Bytes source
|
|
1731
|
+
* @returns Base64 string
|
|
1732
|
+
*/
|
|
1733
|
+
export function bytesToBase64(arraybuffer) {
|
|
1734
|
+
const bytes = new Uint8Array(arraybuffer);
|
|
1735
|
+
const len = bytes.byteLength;
|
|
1736
|
+
let binary = '';
|
|
1737
|
+
for (let i = 0; i < len; i++) {
|
|
1738
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1739
|
+
}
|
|
1740
|
+
return btoa(binary);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Executes callback after ms. Delta time in seconds passed to callback.
|
|
1744
|
+
*
|
|
1745
|
+
* @param callback - Function to call
|
|
1746
|
+
* @param ms - Milliseconds to wait
|
|
1747
|
+
* @returns Handler with remove() method
|
|
1748
|
+
*/
|
|
1749
|
+
export function setTimeout(callback, ms) {
|
|
1750
|
+
if (!globalThis?.setTimeout || !globalThis?.clearTimeout) {
|
|
1751
|
+
console.warn('LeUtils.setTimeout: Not supported.');
|
|
1752
|
+
return { remove: () => { } };
|
|
1753
|
+
}
|
|
1754
|
+
const msVal = FLOAT_LAX(ms);
|
|
1755
|
+
let lastTime = globalThis?.performance?.now?.() ?? 0;
|
|
1756
|
+
// Type guard: verify globalThis has setTimeout
|
|
1757
|
+
if (!hasSetTimeout(globalThis)) {
|
|
1758
|
+
console.error('LeUtils.setTimeout: globalThis.setTimeout is not available');
|
|
1759
|
+
return { remove: () => { } };
|
|
1760
|
+
}
|
|
1761
|
+
// @ts-ignore - handler type varies between Node.js (Timeout) and browser (number), using ReturnType below
|
|
1762
|
+
let handler = globalThis.setTimeout(() => {
|
|
1763
|
+
const currentTime = globalThis?.performance?.now?.() ?? 0;
|
|
1764
|
+
try {
|
|
1765
|
+
callback((currentTime - lastTime) / 1000);
|
|
1766
|
+
}
|
|
1767
|
+
catch (e) {
|
|
1768
|
+
console.error(e);
|
|
1769
|
+
}
|
|
1770
|
+
lastTime = currentTime;
|
|
1771
|
+
}, msVal);
|
|
1772
|
+
return {
|
|
1773
|
+
remove: () => {
|
|
1774
|
+
if (handler !== null) {
|
|
1775
|
+
globalThis.clearTimeout(handler);
|
|
1776
|
+
handler = null;
|
|
1777
|
+
}
|
|
1778
|
+
},
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Executes callback every intervalMs. Delta time in seconds passed to callback.
|
|
1783
|
+
*
|
|
1784
|
+
* @param callback - Function to call
|
|
1785
|
+
* @param intervalMs - Interval in milliseconds
|
|
1786
|
+
* @param fireImmediately - Whether to call immediately
|
|
1787
|
+
* @returns Handler with remove() method
|
|
1788
|
+
*/
|
|
1789
|
+
export function setInterval(callback, intervalMs = 1000, fireImmediately = false) {
|
|
1790
|
+
const msVal = FLOAT_LAX_ANY(intervalMs, 1000);
|
|
1791
|
+
if (fireImmediately) {
|
|
1792
|
+
try {
|
|
1793
|
+
callback(0);
|
|
1794
|
+
}
|
|
1795
|
+
catch (e) {
|
|
1796
|
+
console.error(e);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (!globalThis?.setInterval || !globalThis?.clearInterval) {
|
|
1800
|
+
console.warn('LeUtils.setInterval: Not supported.');
|
|
1801
|
+
return { remove: () => { } };
|
|
1802
|
+
}
|
|
1803
|
+
let lastTime = globalThis?.performance?.now?.() ?? 0;
|
|
1804
|
+
// Type guard: verify globalThis has setInterval
|
|
1805
|
+
if (!hasSetInterval(globalThis)) {
|
|
1806
|
+
console.error('LeUtils.setInterval: globalThis.setInterval is not available');
|
|
1807
|
+
return { remove: () => { } };
|
|
1808
|
+
}
|
|
1809
|
+
// @ts-ignore - handler type varies between Node.js (Timeout) and browser (number), using ReturnType below
|
|
1810
|
+
let handler = globalThis.setInterval(() => {
|
|
1811
|
+
const currentTime = globalThis?.performance?.now?.() ?? 0;
|
|
1812
|
+
try {
|
|
1813
|
+
callback((currentTime - lastTime) / 1000);
|
|
1814
|
+
}
|
|
1815
|
+
catch (e) {
|
|
1816
|
+
console.error(e);
|
|
1817
|
+
}
|
|
1818
|
+
lastTime = currentTime;
|
|
1819
|
+
}, msVal);
|
|
1820
|
+
return {
|
|
1821
|
+
remove: () => {
|
|
1822
|
+
if (handler !== null) {
|
|
1823
|
+
globalThis.clearInterval(handler);
|
|
1824
|
+
handler = null;
|
|
1825
|
+
}
|
|
1826
|
+
},
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Executes callback after frames. Delta time in seconds passed to callback.
|
|
1831
|
+
*
|
|
1832
|
+
* @param callback - Function to call
|
|
1833
|
+
* @param frames - Number of frames to wait
|
|
1834
|
+
* @returns Handler with remove() method
|
|
1835
|
+
*/
|
|
1836
|
+
export function setAnimationFrameTimeout(callback, frames = 1) {
|
|
1837
|
+
if (!globalThis?.requestAnimationFrame || !globalThis?.cancelAnimationFrame) {
|
|
1838
|
+
console.warn('LeUtils.setAnimationFrameTimeout: Not supported.');
|
|
1839
|
+
return { remove: () => { } };
|
|
1840
|
+
}
|
|
1841
|
+
let framesLeft = INT_LAX_ANY(frames, 1);
|
|
1842
|
+
let run = true;
|
|
1843
|
+
let requestAnimationFrameId = null;
|
|
1844
|
+
let lastTime = globalThis?.performance?.now?.() ?? 0;
|
|
1845
|
+
const tick = () => {
|
|
1846
|
+
if (run) {
|
|
1847
|
+
if (framesLeft <= 0) {
|
|
1848
|
+
run = false;
|
|
1849
|
+
requestAnimationFrameId = null;
|
|
1850
|
+
const currentTime = globalThis?.performance?.now?.() ?? 0;
|
|
1851
|
+
try {
|
|
1852
|
+
callback((currentTime - lastTime) / 1000);
|
|
1853
|
+
}
|
|
1854
|
+
catch (e) {
|
|
1855
|
+
console.error(e);
|
|
1856
|
+
}
|
|
1857
|
+
lastTime = currentTime;
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
framesLeft--;
|
|
1861
|
+
requestAnimationFrameId = globalThis.requestAnimationFrame(tick);
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
tick();
|
|
1865
|
+
return {
|
|
1866
|
+
remove: () => {
|
|
1867
|
+
run = false;
|
|
1868
|
+
if (requestAnimationFrameId !== null) {
|
|
1869
|
+
globalThis.cancelAnimationFrame(requestAnimationFrameId);
|
|
1870
|
+
requestAnimationFrameId = null;
|
|
1871
|
+
}
|
|
1872
|
+
},
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Executes callback every intervalFrames. Delta time in seconds passed to callback.
|
|
1877
|
+
*
|
|
1878
|
+
* @param callback - Function to call
|
|
1879
|
+
* @param intervalFrames - Interval in frames
|
|
1880
|
+
* @param fireImmediately - Whether to call immediately
|
|
1881
|
+
* @returns Handler with remove() method
|
|
1882
|
+
*/
|
|
1883
|
+
export function setAnimationFrameInterval(callback, intervalFrames = 1, fireImmediately = false) {
|
|
1884
|
+
const iFrames = INT_LAX_ANY(intervalFrames, 1);
|
|
1885
|
+
if (fireImmediately) {
|
|
1886
|
+
try {
|
|
1887
|
+
callback(0);
|
|
1888
|
+
}
|
|
1889
|
+
catch (e) {
|
|
1890
|
+
console.error(e);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (!globalThis?.requestAnimationFrame || !globalThis?.cancelAnimationFrame) {
|
|
1894
|
+
console.warn('LeUtils.setAnimationFrameInterval: Not supported.');
|
|
1895
|
+
return { remove: () => { } };
|
|
1896
|
+
}
|
|
1897
|
+
let run = true;
|
|
1898
|
+
let requestAnimationFrameId = null;
|
|
1899
|
+
let lastTimestamp = 0;
|
|
1900
|
+
let totalTime = 0;
|
|
1901
|
+
let frames = iFrames;
|
|
1902
|
+
const tick = (timestampVal) => {
|
|
1903
|
+
if (run) {
|
|
1904
|
+
if (lastTimestamp === 0) {
|
|
1905
|
+
lastTimestamp = timestampVal;
|
|
1906
|
+
}
|
|
1907
|
+
totalTime += (timestampVal - lastTimestamp);
|
|
1908
|
+
lastTimestamp = timestampVal;
|
|
1909
|
+
frames--;
|
|
1910
|
+
if (frames <= 0) {
|
|
1911
|
+
try {
|
|
1912
|
+
callback(totalTime / 1000);
|
|
1913
|
+
}
|
|
1914
|
+
catch (e) {
|
|
1915
|
+
console.error(e);
|
|
1916
|
+
}
|
|
1917
|
+
totalTime = 0;
|
|
1918
|
+
frames = iFrames;
|
|
1919
|
+
}
|
|
1920
|
+
if (run) {
|
|
1921
|
+
requestAnimationFrameId = globalThis.requestAnimationFrame(tick);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
globalThis.requestAnimationFrame(tick);
|
|
1926
|
+
return {
|
|
1927
|
+
remove: () => {
|
|
1928
|
+
run = false;
|
|
1929
|
+
if (requestAnimationFrameId !== null) {
|
|
1930
|
+
globalThis.cancelAnimationFrame(requestAnimationFrameId);
|
|
1931
|
+
requestAnimationFrameId = null;
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Returns a promise resolved after ms.
|
|
1938
|
+
*
|
|
1939
|
+
* @param ms - Milliseconds to wait
|
|
1940
|
+
* @returns Promise resolving to delta time
|
|
1941
|
+
*/
|
|
1942
|
+
export function promiseTimeout(ms) {
|
|
1943
|
+
const msVal = FLOAT_LAX(ms);
|
|
1944
|
+
if (msVal <= 0) {
|
|
1945
|
+
return Promise.resolve(0);
|
|
1946
|
+
}
|
|
1947
|
+
return new Promise((resolve) => setTimeout(resolve, msVal));
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Returns a promise resolved after frames.
|
|
1951
|
+
*
|
|
1952
|
+
* @param frames - Number of frames to wait
|
|
1953
|
+
* @returns Promise resolving to delta time
|
|
1954
|
+
*/
|
|
1955
|
+
export function promiseAnimationFrameTimeout(frames) {
|
|
1956
|
+
const iFrames = INT_LAX(frames);
|
|
1957
|
+
if (iFrames <= 0) {
|
|
1958
|
+
return Promise.resolve(0);
|
|
1959
|
+
}
|
|
1960
|
+
return new Promise((resolve) => setAnimationFrameTimeout(resolve, iFrames));
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Fetch with retry and abort functionality.
|
|
1964
|
+
*
|
|
1965
|
+
* @param url - URL to fetch
|
|
1966
|
+
* @param options - Fetch options including retries and delay
|
|
1967
|
+
* @returns Object with then(), catch(), finally(), remove(), isRemoved()
|
|
1968
|
+
*/
|
|
1969
|
+
export function fetch(url, options) {
|
|
1970
|
+
let currentRetries = 0;
|
|
1971
|
+
// Type guard: check if options has retries property
|
|
1972
|
+
const retriesValue = isRecordLike(options) && 'retries' in options ? options.retries : undefined;
|
|
1973
|
+
const retries = INT_LAX(retriesValue);
|
|
1974
|
+
let controllerAborted = false;
|
|
1975
|
+
let controller = null;
|
|
1976
|
+
if (globalThis?.AbortController) {
|
|
1977
|
+
controller = new AbortController();
|
|
1978
|
+
}
|
|
1979
|
+
let promise = (async () => {
|
|
1980
|
+
const attemptFetch = async () => {
|
|
1981
|
+
if (controllerAborted || controller?.signal?.aborted) {
|
|
1982
|
+
throw new Error('Aborted');
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
const response = await globalThis.fetch(url, {
|
|
1986
|
+
signal: controller?.signal,
|
|
1987
|
+
...(isRecordLike(options) ? options : {}),
|
|
1988
|
+
// @ts-ignore -- removing custom options from standard fetch options
|
|
1989
|
+
retries: undefined,
|
|
1990
|
+
// @ts-ignore -- removing custom options from standard fetch options
|
|
1991
|
+
delay: undefined,
|
|
1992
|
+
});
|
|
1993
|
+
if (!response.ok) {
|
|
1994
|
+
throw new Error('Network request failed: ' + response.status + ' ' + response.statusText);
|
|
1995
|
+
}
|
|
1996
|
+
return response;
|
|
1997
|
+
}
|
|
1998
|
+
catch (error) {
|
|
1999
|
+
if (controllerAborted || controller?.signal?.aborted) {
|
|
2000
|
+
throw new Error('Aborted');
|
|
2001
|
+
}
|
|
2002
|
+
if (currentRetries >= retries) {
|
|
2003
|
+
throw error;
|
|
2004
|
+
}
|
|
2005
|
+
currentRetries++;
|
|
2006
|
+
// Type guard: check if delay is a function or a value
|
|
2007
|
+
const delayValue = isRecordLike(options) && 'delay' in options ? options.delay : undefined;
|
|
2008
|
+
const delay = typeof delayValue === 'function' ? INT_LAX_ANY(delayValue(currentRetries), 500) : INT_LAX_ANY(delayValue, 500);
|
|
2009
|
+
await promiseTimeout(delay);
|
|
2010
|
+
return await attemptFetch();
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
return await attemptFetch();
|
|
2014
|
+
})();
|
|
2015
|
+
const result = {
|
|
2016
|
+
then: () => result,
|
|
2017
|
+
catch: () => result,
|
|
2018
|
+
finally: () => result,
|
|
2019
|
+
remove: () => undefined,
|
|
2020
|
+
isRemoved: () => false,
|
|
2021
|
+
};
|
|
2022
|
+
result.then = (...args) => {
|
|
2023
|
+
// Type guard: verify promise is a Promise before chaining
|
|
2024
|
+
if (isPromise(promise)) {
|
|
2025
|
+
// Type guard: promise.then() returns a Promise, which we know is Promise<Response> in this context
|
|
2026
|
+
// We've already verified promise is a Promise, so the result of .then() is also a Promise
|
|
2027
|
+
// The generic Promise chain maintains the Response type in this context
|
|
2028
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
2029
|
+
const thenResult = promise.then(...args);
|
|
2030
|
+
promise = thenResult;
|
|
2031
|
+
}
|
|
2032
|
+
return result;
|
|
2033
|
+
};
|
|
2034
|
+
result.catch = (...args) => {
|
|
2035
|
+
// Type guard: verify promise is a Promise before chaining
|
|
2036
|
+
if (isPromise(promise)) {
|
|
2037
|
+
// Type guard: promise.catch() returns a Promise, which we know is Promise<Response> in this context
|
|
2038
|
+
// We've already verified promise is a Promise, so the result of .catch() is also a Promise
|
|
2039
|
+
// The generic Promise chain maintains the Response type in this context
|
|
2040
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
2041
|
+
const catchResult = promise.catch(...args);
|
|
2042
|
+
promise = catchResult;
|
|
2043
|
+
}
|
|
2044
|
+
return result;
|
|
2045
|
+
};
|
|
2046
|
+
result.finally = (...args) => {
|
|
2047
|
+
// Type guard: verify promise is a Promise before chaining
|
|
2048
|
+
if (isPromise(promise)) {
|
|
2049
|
+
// Type guard: promise.finally() returns a Promise, which we know is Promise<Response> in this context
|
|
2050
|
+
// We've already verified promise is a Promise, so the result of .finally() is also a Promise
|
|
2051
|
+
// The generic Promise chain maintains the Response type in this context
|
|
2052
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
2053
|
+
const finallyResult = promise.finally(...args);
|
|
2054
|
+
promise = finallyResult;
|
|
2055
|
+
}
|
|
2056
|
+
return result;
|
|
2057
|
+
};
|
|
2058
|
+
result.remove = (...args) => {
|
|
2059
|
+
controllerAborted = true;
|
|
2060
|
+
if (controller) {
|
|
2061
|
+
controller.abort(...args);
|
|
2062
|
+
}
|
|
2063
|
+
return result;
|
|
2064
|
+
};
|
|
2065
|
+
result.isRemoved = () => (controllerAborted || !!controller?.signal?.aborted);
|
|
2066
|
+
return result;
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Cached version of fetch.
|
|
2070
|
+
*
|
|
2071
|
+
* @param url - URL to fetch
|
|
2072
|
+
* @param options - Fetch options
|
|
2073
|
+
* @param responseFunction - Optional function to process response before caching
|
|
2074
|
+
* @returns Promise resolving to data
|
|
2075
|
+
*/
|
|
2076
|
+
export const cachedFetch = (() => {
|
|
2077
|
+
const cache = new Map();
|
|
2078
|
+
return async (url, options, responseFunction) => {
|
|
2079
|
+
if (cache.has(url)) {
|
|
2080
|
+
const result = cache.get(url);
|
|
2081
|
+
// Type guard: check result structure
|
|
2082
|
+
if (isRecordLike(result)) {
|
|
2083
|
+
if ('data' in result && result.data !== undefined) {
|
|
2084
|
+
return result.data;
|
|
2085
|
+
}
|
|
2086
|
+
if ('promise' in result && isPromise(result.promise)) {
|
|
2087
|
+
return await result.promise;
|
|
2088
|
+
}
|
|
2089
|
+
if ('error' in result && result.error !== undefined) {
|
|
2090
|
+
throw result.error;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
// Type guard: fetch returns Promise<Response>
|
|
2096
|
+
// Type guard: verify options is RequestInit-like for fetch
|
|
2097
|
+
// RequestInit is compatible with Record<string, unknown>, so we can use it directly
|
|
2098
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
2099
|
+
const fetchOptions = isRecordLike(options) ? options : undefined;
|
|
2100
|
+
const fetchPromise = globalThis.fetch(url, fetchOptions);
|
|
2101
|
+
const promise = fetchPromise
|
|
2102
|
+
.then(async (response) => {
|
|
2103
|
+
const data = responseFunction ? (await responseFunction(response)) : response;
|
|
2104
|
+
// Type guard: check if options has verify function
|
|
2105
|
+
if (isRecordLike(options) && 'verify' in options) {
|
|
2106
|
+
const verify = options.verify;
|
|
2107
|
+
if (typeof verify === 'function') {
|
|
2108
|
+
await verify(data, response);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
return data;
|
|
2112
|
+
})
|
|
2113
|
+
.then((data) => {
|
|
2114
|
+
cache.set(url, { data });
|
|
2115
|
+
return data;
|
|
2116
|
+
})
|
|
2117
|
+
.catch((error) => {
|
|
2118
|
+
cache.set(url, { error });
|
|
2119
|
+
throw error;
|
|
2120
|
+
});
|
|
2121
|
+
if (!cache.has(url)) {
|
|
2122
|
+
cache.set(url, { promise });
|
|
2123
|
+
}
|
|
2124
|
+
return await promise;
|
|
2125
|
+
};
|
|
2126
|
+
})();
|
|
2127
|
+
/**
|
|
2128
|
+
* Creates a new transactional value.
|
|
2129
|
+
*
|
|
2130
|
+
* @param value - Initial value
|
|
2131
|
+
* @returns Transactional value object
|
|
2132
|
+
*/
|
|
2133
|
+
export function createTransactionalValue(value) {
|
|
2134
|
+
return {
|
|
2135
|
+
value: (typeof value === 'undefined') ? null : value,
|
|
2136
|
+
changes: [],
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Validates a transactional value structure.
|
|
2141
|
+
*
|
|
2142
|
+
* @param transactionalValue - Value to check
|
|
2143
|
+
* @returns true if valid
|
|
2144
|
+
*/
|
|
2145
|
+
export function isTransactionalValueValid(transactionalValue) {
|
|
2146
|
+
return ((typeof transactionalValue === 'object') &&
|
|
2147
|
+
(transactionalValue !== null) &&
|
|
2148
|
+
isTransactionalValue(transactionalValue));
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Private helper to ensure valid transactional value.
|
|
2152
|
+
*/
|
|
2153
|
+
function checkTransactionalValue(transactionalValue) {
|
|
2154
|
+
if (!isTransactionalValueValid(transactionalValue)) {
|
|
2155
|
+
throw new Error('Invalid TransactionalValue provided');
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Private helper to find a change by ID.
|
|
2160
|
+
*/
|
|
2161
|
+
function findTransactionalValueChange(transactionalValue, changeId) {
|
|
2162
|
+
for (let i = 0; i < transactionalValue.changes.length; i++) {
|
|
2163
|
+
if (transactionalValue.changes[i].id === changeId) {
|
|
2164
|
+
return { change: transactionalValue.changes[i], index: i };
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Converts transactional value to string representation of its state.
|
|
2171
|
+
*
|
|
2172
|
+
* @param transactionalValue - Transactional value
|
|
2173
|
+
* @returns State string
|
|
2174
|
+
*/
|
|
2175
|
+
export function transactionalValueToString(transactionalValue) {
|
|
2176
|
+
if (!isTransactionalValueValid(transactionalValue)) {
|
|
2177
|
+
return STRING(transactionalValue);
|
|
2178
|
+
}
|
|
2179
|
+
if (transactionalValue.changes.length <= 0) {
|
|
2180
|
+
return STRING(transactionalValue.value);
|
|
2181
|
+
}
|
|
2182
|
+
let valuesString = STRING(transactionalValue.value);
|
|
2183
|
+
for (let i = 0; i < transactionalValue.changes.length; i++) {
|
|
2184
|
+
valuesString += ' -> ' + STRING(transactionalValue.changes[i].value);
|
|
2185
|
+
}
|
|
2186
|
+
return STRING(transactionalValue.changes[transactionalValue.changes.length - 1].value) + ' (' + valuesString + ')';
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Sets and commits a value, clearing all uncommitted changes.
|
|
2190
|
+
*
|
|
2191
|
+
* @param transactionalValue - Transactional value
|
|
2192
|
+
* @param value - New value
|
|
2193
|
+
*/
|
|
2194
|
+
export function transactionSetAndCommit(transactionalValue, value) {
|
|
2195
|
+
checkTransactionalValue(transactionalValue);
|
|
2196
|
+
transactionalValue.value = (typeof value === 'undefined') ? null : value;
|
|
2197
|
+
transactionalValue.changes = [];
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Adds an uncommitted change. Returns change ID.
|
|
2201
|
+
*
|
|
2202
|
+
* @param transactionalValue - Transactional value
|
|
2203
|
+
* @param value - New value
|
|
2204
|
+
* @returns Change ID
|
|
2205
|
+
*/
|
|
2206
|
+
export function transactionSetWithoutCommitting(transactionalValue, value) {
|
|
2207
|
+
checkTransactionalValue(transactionalValue);
|
|
2208
|
+
const id = uniqueId();
|
|
2209
|
+
transactionalValue.changes.push({ id, value: (typeof value === 'undefined') ? null : value });
|
|
2210
|
+
return id;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Commits a specific change by ID.
|
|
2214
|
+
*
|
|
2215
|
+
* @param transactionalValue - Transactional value
|
|
2216
|
+
* @param changeId - Change ID
|
|
2217
|
+
* @returns true if committed
|
|
2218
|
+
*/
|
|
2219
|
+
export function transactionCommitChange(transactionalValue, changeId) {
|
|
2220
|
+
checkTransactionalValue(transactionalValue);
|
|
2221
|
+
const result = findTransactionalValueChange(transactionalValue, changeId);
|
|
2222
|
+
if (result === null) {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
transactionalValue.value = result.change.value;
|
|
2226
|
+
transactionalValue.changes.splice(0, result.index + 1);
|
|
2227
|
+
return true;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Cancels a specific change by ID.
|
|
2231
|
+
*
|
|
2232
|
+
* @param transactionalValue - Transactional value
|
|
2233
|
+
* @param changeId - Change ID
|
|
2234
|
+
* @returns true if cancelled
|
|
2235
|
+
*/
|
|
2236
|
+
export function transactionCancelChange(transactionalValue, changeId) {
|
|
2237
|
+
checkTransactionalValue(transactionalValue);
|
|
2238
|
+
const result = findTransactionalValueChange(transactionalValue, changeId);
|
|
2239
|
+
if (result === null) {
|
|
2240
|
+
return false;
|
|
2241
|
+
}
|
|
2242
|
+
transactionalValue.changes.splice(result.index, 1);
|
|
2243
|
+
return true;
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Checks if a change is still relevant.
|
|
2247
|
+
*
|
|
2248
|
+
* @param transactionalValue - Transactional value
|
|
2249
|
+
* @param changeId - Change ID
|
|
2250
|
+
* @returns true if relevant
|
|
2251
|
+
*/
|
|
2252
|
+
export function transactionIsChangeRelevant(transactionalValue, changeId) {
|
|
2253
|
+
checkTransactionalValue(transactionalValue);
|
|
2254
|
+
return (findTransactionalValueChange(transactionalValue, changeId) !== null);
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Gets current committed value.
|
|
2258
|
+
*
|
|
2259
|
+
* @param transactionalValue - Transactional value
|
|
2260
|
+
* @returns Committed value
|
|
2261
|
+
*/
|
|
2262
|
+
export function transactionGetCommittedValue(transactionalValue) {
|
|
2263
|
+
checkTransactionalValue(transactionalValue);
|
|
2264
|
+
return transactionalValue.value;
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Gets current value (including most recent uncommitted change).
|
|
2268
|
+
*
|
|
2269
|
+
* @param transactionalValue - Transactional value
|
|
2270
|
+
* @returns Current value
|
|
2271
|
+
*/
|
|
2272
|
+
export function transactionGetValue(transactionalValue) {
|
|
2273
|
+
checkTransactionalValue(transactionalValue);
|
|
2274
|
+
if (transactionalValue.changes.length <= 0) {
|
|
2275
|
+
return transactionalValue.value;
|
|
2276
|
+
}
|
|
2277
|
+
return transactionalValue.changes[transactionalValue.changes.length - 1].value;
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Performs a deep equality comparison between two collections, sorting on keys first.
|
|
2281
|
+
*
|
|
2282
|
+
* @param elementsA - First collection
|
|
2283
|
+
* @param elementsB - Second collection
|
|
2284
|
+
* @param ignoreKeys - Keys to ignore
|
|
2285
|
+
* @returns true if equivalent
|
|
2286
|
+
*/
|
|
2287
|
+
export function equalsMapLike(elementsA, elementsB, ignoreKeys = []) {
|
|
2288
|
+
const sortKeyValueArrays = (pairA, pairB) => {
|
|
2289
|
+
// Type guard: verify both are pairs
|
|
2290
|
+
if (isPair(pairA) && isPair(pairB)) {
|
|
2291
|
+
return compare(pairA[0], pairB[0]);
|
|
2292
|
+
}
|
|
2293
|
+
return 0;
|
|
2294
|
+
};
|
|
2295
|
+
// Type guard: verify mapToArray returns array of pairs
|
|
2296
|
+
const aArray = mapToArray(elementsA, (value, key) => [key, value]);
|
|
2297
|
+
const bArray = mapToArray(elementsB, (value, key) => [key, value]);
|
|
2298
|
+
const aPairs = (Array.isArray(aArray) ? aArray.filter((item) => isPair(item)) : []).sort(sortKeyValueArrays);
|
|
2299
|
+
const bPairs = (Array.isArray(bArray) ? bArray.filter((item) => isPair(item)) : []).sort(sortKeyValueArrays);
|
|
2300
|
+
const ignore = (typeof ignoreKeys === 'string') ? ARRAY(ignoreKeys) : mapToArray(ignoreKeys);
|
|
2301
|
+
let i = 0, j = 0;
|
|
2302
|
+
while (i < aPairs.length && j < bPairs.length) {
|
|
2303
|
+
const [ka, va] = aPairs[i];
|
|
2304
|
+
const [kb, vb] = bPairs[j];
|
|
2305
|
+
if (ignore.includes(ka)) {
|
|
2306
|
+
i++;
|
|
2307
|
+
if (ignore.includes(kb))
|
|
2308
|
+
j++;
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
if (ignore.includes(kb)) {
|
|
2312
|
+
j++;
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
if (!equals(ka, kb) || !equals(va, vb))
|
|
2316
|
+
return false;
|
|
2317
|
+
i++;
|
|
2318
|
+
j++;
|
|
2319
|
+
}
|
|
2320
|
+
while (i < aPairs.length && ignore.includes(aPairs[i][0]))
|
|
2321
|
+
i++;
|
|
2322
|
+
if (i < aPairs.length)
|
|
2323
|
+
return false;
|
|
2324
|
+
while (j < bPairs.length && ignore.includes(bPairs[j][0]))
|
|
2325
|
+
j++;
|
|
2326
|
+
return j >= bPairs.length;
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Executes callback when document is ready.
|
|
2330
|
+
*
|
|
2331
|
+
* @param callback - Function to call
|
|
2332
|
+
* @returns Handler with remove() method
|
|
2333
|
+
*/
|
|
2334
|
+
export function onDomReady(callback) {
|
|
2335
|
+
// @ts-ignore -- document might not be defined in non-browser environments
|
|
2336
|
+
if (!globalThis?.document?.addEventListener) {
|
|
2337
|
+
console.warn('LeUtils.onDomReady: No document found.');
|
|
2338
|
+
return { remove: () => { } };
|
|
2339
|
+
}
|
|
2340
|
+
// @ts-ignore -- document might not be defined in non-browser environments
|
|
2341
|
+
if (globalThis.document.readyState === 'interactive' || globalThis.document.readyState === 'complete') {
|
|
2342
|
+
return setTimeout(callback, 0);
|
|
2343
|
+
}
|
|
2344
|
+
let listening = true;
|
|
2345
|
+
const wrapper = () => {
|
|
2346
|
+
if (listening) {
|
|
2347
|
+
listening = false;
|
|
2348
|
+
// @ts-ignore -- document might not be defined in non-browser environments
|
|
2349
|
+
globalThis.document.removeEventListener('DOMContentLoaded', wrapper);
|
|
2350
|
+
callback();
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
// @ts-ignore -- document might not be defined in non-browser environments
|
|
2354
|
+
globalThis.document.addEventListener('DOMContentLoaded', wrapper);
|
|
2355
|
+
return {
|
|
2356
|
+
remove: () => {
|
|
2357
|
+
if (listening) {
|
|
2358
|
+
listening = false;
|
|
2359
|
+
// @ts-ignore -- document might not be defined in non-browser environments
|
|
2360
|
+
globalThis.document.removeEventListener('DOMContentLoaded', wrapper);
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Parses version string into object with comparison helpers.
|
|
2367
|
+
*
|
|
2368
|
+
* @param version - Version string or object
|
|
2369
|
+
* @returns Version object with comparison methods
|
|
2370
|
+
*/
|
|
2371
|
+
export function parseVersionString(version) {
|
|
2372
|
+
// Type guard: verify version has major, minor, patch
|
|
2373
|
+
if (isVersionObject(version)) {
|
|
2374
|
+
const major = INT_LAX(version.major);
|
|
2375
|
+
const minor = INT_LAX(version.minor);
|
|
2376
|
+
const patch = INT_LAX(version.patch);
|
|
2377
|
+
return {
|
|
2378
|
+
major,
|
|
2379
|
+
minor,
|
|
2380
|
+
patch,
|
|
2381
|
+
toString: () => `${major}.${minor}.${patch}`,
|
|
2382
|
+
equals: (other) => {
|
|
2383
|
+
if (!isVersionObject(other)) {
|
|
2384
|
+
return false;
|
|
2385
|
+
}
|
|
2386
|
+
return major === INT_LAX(other.major) && minor === INT_LAX(other.minor) && patch === INT_LAX(other.patch);
|
|
2387
|
+
},
|
|
2388
|
+
largerThan: (other) => {
|
|
2389
|
+
if (!isVersionObject(other)) {
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
2392
|
+
const otherMajor = INT_LAX(other.major);
|
|
2393
|
+
const otherMinor = INT_LAX(other.minor);
|
|
2394
|
+
const otherPatch = INT_LAX(other.patch);
|
|
2395
|
+
return (major > otherMajor) || (major === otherMajor && minor > otherMinor) || (major === otherMajor && minor === otherMinor && patch > otherPatch);
|
|
2396
|
+
},
|
|
2397
|
+
largerThanOrEquals: (other) => {
|
|
2398
|
+
if (!isVersionObject(other)) {
|
|
2399
|
+
return false;
|
|
2400
|
+
}
|
|
2401
|
+
const otherMajor = INT_LAX(other.major);
|
|
2402
|
+
const otherMinor = INT_LAX(other.minor);
|
|
2403
|
+
const otherPatch = INT_LAX(other.patch);
|
|
2404
|
+
return (major > otherMajor) || (major === otherMajor && minor > otherMinor) || (major === otherMajor && minor === otherMinor && patch >= otherPatch);
|
|
2405
|
+
},
|
|
2406
|
+
smallerThan: (other) => {
|
|
2407
|
+
if (!isVersionObject(other)) {
|
|
2408
|
+
return false;
|
|
2409
|
+
}
|
|
2410
|
+
const otherMajor = INT_LAX(other.major);
|
|
2411
|
+
const otherMinor = INT_LAX(other.minor);
|
|
2412
|
+
const otherPatch = INT_LAX(other.patch);
|
|
2413
|
+
return !((major > otherMajor) || (major === otherMajor && minor > otherMinor) || (major === otherMajor && minor === otherMinor && patch >= otherPatch));
|
|
2414
|
+
},
|
|
2415
|
+
smallerThanOrEquals: (other) => {
|
|
2416
|
+
if (!isVersionObject(other)) {
|
|
2417
|
+
return false;
|
|
2418
|
+
}
|
|
2419
|
+
const otherMajor = INT_LAX(other.major);
|
|
2420
|
+
const otherMinor = INT_LAX(other.minor);
|
|
2421
|
+
const otherPatch = INT_LAX(other.patch);
|
|
2422
|
+
return !((major > otherMajor) || (major === otherMajor && minor > otherMinor) || (major === otherMajor && minor === otherMinor && patch > otherPatch));
|
|
2423
|
+
},
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
const v = STRING(version).trim();
|
|
2427
|
+
const parts = v.split(' ')[0].split('-')[0].split('.');
|
|
2428
|
+
const major = INT_LAX(parts[0]);
|
|
2429
|
+
const minor = INT_LAX(parts[1]);
|
|
2430
|
+
const patch = INT_LAX(parts[2]);
|
|
2431
|
+
const obj = {
|
|
2432
|
+
major,
|
|
2433
|
+
minor,
|
|
2434
|
+
patch,
|
|
2435
|
+
toString: () => `${major}.${minor}.${patch}`,
|
|
2436
|
+
equals: (other) => {
|
|
2437
|
+
const o = parseVersionString(other);
|
|
2438
|
+
return major === o.major && minor === o.minor && patch === o.patch;
|
|
2439
|
+
},
|
|
2440
|
+
largerThan: (other) => {
|
|
2441
|
+
const o = parseVersionString(other);
|
|
2442
|
+
if (major > o.major)
|
|
2443
|
+
return true;
|
|
2444
|
+
if (major < o.major)
|
|
2445
|
+
return false;
|
|
2446
|
+
if (minor > o.minor)
|
|
2447
|
+
return true;
|
|
2448
|
+
if (minor < o.minor)
|
|
2449
|
+
return false;
|
|
2450
|
+
return patch > o.patch;
|
|
2451
|
+
},
|
|
2452
|
+
largerThanOrEquals: (other) => {
|
|
2453
|
+
const o = parseVersionString(other);
|
|
2454
|
+
if (major > o.major)
|
|
2455
|
+
return true;
|
|
2456
|
+
if (major < o.major)
|
|
2457
|
+
return false;
|
|
2458
|
+
if (minor > o.minor)
|
|
2459
|
+
return true;
|
|
2460
|
+
if (minor < o.minor)
|
|
2461
|
+
return false;
|
|
2462
|
+
return patch >= o.patch;
|
|
2463
|
+
},
|
|
2464
|
+
smallerThan: (other) => {
|
|
2465
|
+
return !obj.largerThanOrEquals(other);
|
|
2466
|
+
},
|
|
2467
|
+
smallerThanOrEquals: (other) => {
|
|
2468
|
+
return !obj.largerThan(other);
|
|
2469
|
+
},
|
|
2470
|
+
};
|
|
2471
|
+
return obj;
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Increases numeric string by 1 without limit.
|
|
2475
|
+
*
|
|
2476
|
+
* @param str - Numeric string
|
|
2477
|
+
* @returns Increased string
|
|
2478
|
+
*/
|
|
2479
|
+
export function increaseNumericStringByOne(str) {
|
|
2480
|
+
const s = (typeof str !== 'string') ? String(str) : str;
|
|
2481
|
+
if (!s || /[^0-9]/.test(s))
|
|
2482
|
+
return '1';
|
|
2483
|
+
const chars = s.split('');
|
|
2484
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
2485
|
+
const n = parseInt(chars[i], 10);
|
|
2486
|
+
if (n < 9) {
|
|
2487
|
+
chars[i] = String(n + 1);
|
|
2488
|
+
return chars.join('');
|
|
2489
|
+
}
|
|
2490
|
+
chars[i] = '0';
|
|
2491
|
+
}
|
|
2492
|
+
return '1' + chars.join('');
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Checks if collection contains all given values (case-insensitive).
|
|
2496
|
+
*
|
|
2497
|
+
* @param array - Collection to search
|
|
2498
|
+
* @param values - Values to find
|
|
2499
|
+
* @returns true if all found
|
|
2500
|
+
*/
|
|
2501
|
+
export function containsAllCaseInsensitive(array, values) {
|
|
2502
|
+
if (!array)
|
|
2503
|
+
return false;
|
|
2504
|
+
let result = true;
|
|
2505
|
+
each(values, (v) => {
|
|
2506
|
+
if (!containsCaseInsensitive(array, v)) {
|
|
2507
|
+
result = false;
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
return result;
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Checks if collection contains any of given values (case-insensitive).
|
|
2515
|
+
*
|
|
2516
|
+
* @param array - Collection to search
|
|
2517
|
+
* @param values - Values to find
|
|
2518
|
+
* @returns true if any found
|
|
2519
|
+
*/
|
|
2520
|
+
export function containsAnyCaseInsensitive(array, values) {
|
|
2521
|
+
if (!array)
|
|
2522
|
+
return false;
|
|
2523
|
+
let result = false;
|
|
2524
|
+
each(values, (v) => {
|
|
2525
|
+
if (containsCaseInsensitive(array, v)) {
|
|
2526
|
+
result = true;
|
|
2527
|
+
return false;
|
|
2528
|
+
}
|
|
2529
|
+
});
|
|
2530
|
+
return result;
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Checks if collection contains none of given values (case-insensitive).
|
|
2534
|
+
*
|
|
2535
|
+
* @param array - Collection to search
|
|
2536
|
+
* @param values - Values to find
|
|
2537
|
+
* @returns true if none found
|
|
2538
|
+
*/
|
|
2539
|
+
export function containsNoneCaseInsensitive(array, values) {
|
|
2540
|
+
if (!array)
|
|
2541
|
+
return true;
|
|
2542
|
+
let result = true;
|
|
2543
|
+
each(values, (v) => {
|
|
2544
|
+
if (containsCaseInsensitive(array, v)) {
|
|
2545
|
+
result = false;
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
return result;
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Returns string with start/end trimmed and ensures valid sentence ending.
|
|
2553
|
+
*
|
|
2554
|
+
* @param sentence - String to purge
|
|
2555
|
+
* @returns Purged sentence
|
|
2556
|
+
*/
|
|
2557
|
+
export function purgeSentence(sentence) {
|
|
2558
|
+
let s = trimEnd(STRING(sentence).trim(), '.: \r\n\t');
|
|
2559
|
+
s += (endsWithAny(s, '!?;') ? '' : '.');
|
|
2560
|
+
return s;
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Obtains error message from any input.
|
|
2564
|
+
*
|
|
2565
|
+
* @param error - Error source
|
|
2566
|
+
* @returns Error message string
|
|
2567
|
+
*/
|
|
2568
|
+
export function purgeErrorMessage(error) {
|
|
2569
|
+
let err = error;
|
|
2570
|
+
// Type guard: check if error has message property
|
|
2571
|
+
if (hasErrorMessage(err)) {
|
|
2572
|
+
err = err.message;
|
|
2573
|
+
}
|
|
2574
|
+
if (typeof err === 'string') {
|
|
2575
|
+
const errorParts = err.split('threw an error:');
|
|
2576
|
+
err = errorParts[errorParts.length - 1];
|
|
2577
|
+
}
|
|
2578
|
+
else {
|
|
2579
|
+
try {
|
|
2580
|
+
err = JSON.stringify(err);
|
|
2581
|
+
}
|
|
2582
|
+
catch {
|
|
2583
|
+
err = 'An unknown error occurred';
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
return STRING(err).trim();
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Generates permutations of names (e.g. foobar, fooBar, FooBar, foo-bar, foo_bar).
|
|
2590
|
+
*
|
|
2591
|
+
* @param names - Parts to combine
|
|
2592
|
+
* @returns Array of permutations
|
|
2593
|
+
*/
|
|
2594
|
+
export function generateNamePermutations(...names) {
|
|
2595
|
+
const n = flattenToArray(names)
|
|
2596
|
+
.map((name) => STRING(name).trim().toLowerCase())
|
|
2597
|
+
.filter((name) => (name.length > 0));
|
|
2598
|
+
const results = [];
|
|
2599
|
+
if (n.length > 0) {
|
|
2600
|
+
results.push(n.join('')); //foobar
|
|
2601
|
+
results.push(n.map(capitalize).join('')); //FooBar
|
|
2602
|
+
}
|
|
2603
|
+
if (n.length > 1) {
|
|
2604
|
+
results.push([n[0]].concat(n.slice(1).map(capitalize)).join('')); //fooBar
|
|
2605
|
+
results.push(n.join('-')); //foo-bar
|
|
2606
|
+
results.push(n.join('_')); //foo_bar
|
|
2607
|
+
}
|
|
2608
|
+
return results;
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Compares two strings generated by LeUtils.timestamp(). Primarily used for sorting.
|
|
2612
|
+
*/
|
|
2613
|
+
export function compareTimestampStrings(a, b) {
|
|
2614
|
+
try {
|
|
2615
|
+
const ta = base64ToHex(STRING(a).replaceAll('-', '+').replaceAll('_', '/'));
|
|
2616
|
+
const tb = base64ToHex(STRING(b).replaceAll('-', '+').replaceAll('_', '/'));
|
|
2617
|
+
return compare(ta, tb);
|
|
2618
|
+
}
|
|
2619
|
+
catch {
|
|
2620
|
+
return 0;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Returns true if the user is on a smartphone device (mobile).
|
|
2625
|
+
*/
|
|
2626
|
+
export function platformIsMobile() {
|
|
2627
|
+
// Type guard: check navigator and globalThis for vendor/opera properties
|
|
2628
|
+
let userAgent = globalThis?.navigator?.userAgent || '';
|
|
2629
|
+
if (!userAgent && globalThis?.navigator && hasNavigatorVendor(globalThis.navigator)) {
|
|
2630
|
+
userAgent = globalThis.navigator.vendor || '';
|
|
2631
|
+
}
|
|
2632
|
+
if (!userAgent && hasOpera(globalThis)) {
|
|
2633
|
+
const operaValue = safeGetProperty(globalThis, 'opera');
|
|
2634
|
+
userAgent = typeof operaValue === 'string' ? operaValue : '';
|
|
2635
|
+
}
|
|
2636
|
+
const a = STRING(userAgent || '');
|
|
2637
|
+
return !!(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i
|
|
2638
|
+
.test(a) ||
|
|
2639
|
+
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br([ev])w|bumb|bw-([nu])|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do([cp])o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly([-_])|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-([mpt])|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c([- _agpst])|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac([ \-/])|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja([tv])a|jbro|jemu|jigs|kddi|keji|kgt([ /])|klon|kpt |kwc-|kyo([ck])|le(no|xi)|lg( g|\/([klu])|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t([- ov])|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30([02])|n50([025])|n7(0([01])|10)|ne(([cm])-|on|tf|wf|wg|wt)|nok([6i])|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan([adt])|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c([-01])|47|mc|nd|ri)|sgh-|shar|sie([-m])|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel([im])|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c([- ])|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i
|
|
2640
|
+
.test(a));
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Returns true if the user has a cursor (mouse).
|
|
2644
|
+
*/
|
|
2645
|
+
export function platformHasCursor() {
|
|
2646
|
+
return !platformIsMobile() && !globalThis?.matchMedia?.('(any-hover: none)')?.matches;
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Returns string with email purged (lowercased, trimmed).
|
|
2650
|
+
*/
|
|
2651
|
+
export function purgeEmail(email) {
|
|
2652
|
+
const e = STRING(email).trim().toLowerCase().replace(/\s/g, '');
|
|
2653
|
+
if (!e.includes('@') || !e.includes('.')) {
|
|
2654
|
+
return '';
|
|
2655
|
+
}
|
|
2656
|
+
return e;
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Returns true if the focus is effectively clear, meaning that the user is not typing in an input field.
|
|
2660
|
+
*/
|
|
2661
|
+
export const isFocusClear = (() => {
|
|
2662
|
+
const inputTypes = ['text', 'search', 'email', 'number', 'password', 'tel', 'time', 'url', 'week', 'month', 'date', 'datetime-local'];
|
|
2663
|
+
return () => !((globalThis?.document?.activeElement?.tagName?.toLowerCase() === 'input') && inputTypes.includes(globalThis?.document?.activeElement?.getAttribute('type')?.toLowerCase() ?? ''));
|
|
2664
|
+
})();
|
|
2665
|
+
/**
|
|
2666
|
+
* Returns the user's locale. Returns 'en-US' if it can't be determined.
|
|
2667
|
+
*/
|
|
2668
|
+
export const getUserLocale = (() => {
|
|
2669
|
+
let userLocale = null;
|
|
2670
|
+
return () => {
|
|
2671
|
+
if (userLocale === null) {
|
|
2672
|
+
userLocale = (() => {
|
|
2673
|
+
// Type guard: check if navigator has languages
|
|
2674
|
+
let locales = [];
|
|
2675
|
+
if (globalThis?.navigator && hasNavigatorLanguages(globalThis.navigator)) {
|
|
2676
|
+
const navLanguages = safeGetProperty(globalThis.navigator, 'languages');
|
|
2677
|
+
if (Array.isArray(navLanguages)) {
|
|
2678
|
+
locales = navLanguages;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (!IS_ARRAY(locales) || (locales.length <= 0)) {
|
|
2682
|
+
return 'en-US';
|
|
2683
|
+
}
|
|
2684
|
+
locales = locales.filter((locale) => ((typeof locale === 'string') && locale.includes('-') && (locale.toLowerCase() !== 'en-us')));
|
|
2685
|
+
if (locales.length <= 0) {
|
|
2686
|
+
return 'en-US';
|
|
2687
|
+
}
|
|
2688
|
+
const localesNoEnglish = locales.filter((locale) => STRING(locale).toLowerCase().startsWith('en-'));
|
|
2689
|
+
if (localesNoEnglish.length <= 0) {
|
|
2690
|
+
return STRING(locales[0]);
|
|
2691
|
+
}
|
|
2692
|
+
return STRING(localesNoEnglish[0]);
|
|
2693
|
+
})();
|
|
2694
|
+
}
|
|
2695
|
+
return userLocale;
|
|
2696
|
+
};
|
|
2697
|
+
})();
|
|
2698
|
+
/**
|
|
2699
|
+
* Returns the user's locale date format. Always returns YYYY MM DD, with the character in between depending on the user's locale. Returns 'YYYY/MM/DD' if the user's locale can't be determined.
|
|
2700
|
+
*/
|
|
2701
|
+
export const getUserLocaleDateFormat = (() => {
|
|
2702
|
+
let userLocaleDateFormat = null;
|
|
2703
|
+
return () => {
|
|
2704
|
+
if (userLocaleDateFormat === null) {
|
|
2705
|
+
userLocaleDateFormat = (() => {
|
|
2706
|
+
let char = '/';
|
|
2707
|
+
// Type guard: check if globalThis has Intl
|
|
2708
|
+
if (hasIntl(globalThis) && globalThis.Intl && 'DateTimeFormat' in globalThis.Intl) {
|
|
2709
|
+
const DateTimeFormat = safeGetProperty(globalThis.Intl, 'DateTimeFormat');
|
|
2710
|
+
if (typeof DateTimeFormat === 'function') {
|
|
2711
|
+
// Type guard: DateTimeFormat constructor accepts locale and returns object with format method
|
|
2712
|
+
// We've already verified it's a function, now construct it
|
|
2713
|
+
// Use Function.prototype.apply to avoid type assertion
|
|
2714
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
2715
|
+
const dtfInstance = Function.prototype.apply.call(DateTimeFormat, null, [getUserLocale()]);
|
|
2716
|
+
if ('format' in dtfInstance && typeof dtfInstance.format === 'function') {
|
|
2717
|
+
const formattedDate = dtfInstance.format();
|
|
2718
|
+
if (formattedDate.includes('-')) {
|
|
2719
|
+
char = '-';
|
|
2720
|
+
}
|
|
2721
|
+
else if (formattedDate.includes('. ')) {
|
|
2722
|
+
char = '.';
|
|
2723
|
+
}
|
|
2724
|
+
else if (formattedDate.includes('.')) {
|
|
2725
|
+
char = '.';
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return 'YYYY' + char + 'MM' + char + 'DD';
|
|
2731
|
+
})();
|
|
2732
|
+
}
|
|
2733
|
+
return userLocaleDateFormat;
|
|
2734
|
+
};
|
|
2735
|
+
})();
|
|
2736
|
+
/**
|
|
2737
|
+
* Returns an empty 1x1 transparent image data URL.
|
|
2738
|
+
*/
|
|
2739
|
+
export function getEmptyImageSrc() {
|
|
2740
|
+
return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Calculates and returns the percentage of the part and total ((part / total) * 100).
|
|
2744
|
+
*/
|
|
2745
|
+
export function getPercentage(part, total) {
|
|
2746
|
+
const p = FLOAT_LAX(part);
|
|
2747
|
+
const t = FLOAT_LAX(total);
|
|
2748
|
+
if (t === 0) {
|
|
2749
|
+
return 100;
|
|
2750
|
+
}
|
|
2751
|
+
return Math.max(0, Math.min(100, ((p / t) * 100)));
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Returns the pixels of the given Image object.
|
|
2755
|
+
*/
|
|
2756
|
+
export async function getImagePixels(image) {
|
|
2757
|
+
if (!globalThis?.document?.createElement || !globalThis?.document?.body?.appendChild) {
|
|
2758
|
+
console.warn('LeUtils.getImagePixels: Document is not available, returning empty pixels.');
|
|
2759
|
+
return new Uint8ClampedArray();
|
|
2760
|
+
}
|
|
2761
|
+
const canvas = globalThis.document.createElement('canvas');
|
|
2762
|
+
globalThis.document.body.appendChild(canvas);
|
|
2763
|
+
try {
|
|
2764
|
+
const ctx = canvas.getContext('2d');
|
|
2765
|
+
const width = Math.floor(image.width);
|
|
2766
|
+
const height = Math.floor(image.height);
|
|
2767
|
+
if (!ctx || (width <= 0) || (height <= 0)) {
|
|
2768
|
+
return new Uint8ClampedArray();
|
|
2769
|
+
}
|
|
2770
|
+
canvas.width = width;
|
|
2771
|
+
canvas.height = height;
|
|
2772
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
2773
|
+
return ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
2774
|
+
}
|
|
2775
|
+
finally {
|
|
2776
|
+
canvas?.parentNode?.removeChild(canvas);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Returns the data URL (mimetype "image/png") of a colored version of the given Image object.
|
|
2781
|
+
*/
|
|
2782
|
+
export async function getColoredImage(image, color) {
|
|
2783
|
+
if (!globalThis?.document?.createElement || !globalThis?.document?.body?.appendChild) {
|
|
2784
|
+
console.warn('LeUtils.getColoredImage: Document is not available, returning empty image src.');
|
|
2785
|
+
return getEmptyImageSrc();
|
|
2786
|
+
}
|
|
2787
|
+
const canvas = globalThis.document.createElement('canvas');
|
|
2788
|
+
globalThis.document.body.appendChild(canvas);
|
|
2789
|
+
try {
|
|
2790
|
+
const ctx = canvas.getContext('2d');
|
|
2791
|
+
const width = Math.floor(image.width);
|
|
2792
|
+
const height = Math.floor(image.height);
|
|
2793
|
+
if (!ctx || (width <= 0) || (height <= 0)) {
|
|
2794
|
+
return getEmptyImageSrc();
|
|
2795
|
+
}
|
|
2796
|
+
canvas.width = width;
|
|
2797
|
+
canvas.height = height;
|
|
2798
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
2799
|
+
ctx.globalCompositeOperation = 'source-in';
|
|
2800
|
+
ctx.fillStyle = color;
|
|
2801
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
2802
|
+
return canvas.toDataURL('image/png');
|
|
2803
|
+
}
|
|
2804
|
+
finally {
|
|
2805
|
+
canvas?.parentNode?.removeChild(canvas);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Returns the hex color of the given RGB(A).
|
|
2810
|
+
*/
|
|
2811
|
+
export function rgbToHex(rgb) {
|
|
2812
|
+
return '#' + rgb.map((x) => {
|
|
2813
|
+
const hex = x.toString(16);
|
|
2814
|
+
return ((hex.length === 1) ? '0' + hex : hex);
|
|
2815
|
+
}).join('');
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Returns the RGB(A) of the given hex color.
|
|
2819
|
+
*/
|
|
2820
|
+
export function hexToRgb(hexstring) {
|
|
2821
|
+
const initialHexstring = hexstring;
|
|
2822
|
+
const h = STRING(hexstring).replace(/[^0-9A-F]/gi, '');
|
|
2823
|
+
const hasAlpha = ((h.length === 4) || (h.length === 8));
|
|
2824
|
+
let finalH = h;
|
|
2825
|
+
while (finalH.length < 6) {
|
|
2826
|
+
finalH = finalH.replace(/(.)/g, '$1$1');
|
|
2827
|
+
}
|
|
2828
|
+
const result = finalH.match(/\w{2}/g)?.map((a) => parseInt(a, 16));
|
|
2829
|
+
if (!result || (result.length < 3)) {
|
|
2830
|
+
throw new Error('Invalid hex color: "' + finalH + '" (was given "' + initialHexstring + '")');
|
|
2831
|
+
}
|
|
2832
|
+
return [
|
|
2833
|
+
result[0],
|
|
2834
|
+
result[1],
|
|
2835
|
+
result[2],
|
|
2836
|
+
...(hasAlpha ? [result[3]] : []),
|
|
2837
|
+
];
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Returns the HSL(A) of the given RGB(A).
|
|
2841
|
+
*/
|
|
2842
|
+
export function rgbToHsl(rgb) {
|
|
2843
|
+
const r = rgb[0] / 255;
|
|
2844
|
+
const g = rgb[1] / 255;
|
|
2845
|
+
const b = rgb[2] / 255;
|
|
2846
|
+
const max = Math.max(r, g, b);
|
|
2847
|
+
const min = Math.min(r, g, b);
|
|
2848
|
+
let h = 0;
|
|
2849
|
+
let s = 0;
|
|
2850
|
+
const l = (max + min) / 2;
|
|
2851
|
+
if (max !== min) {
|
|
2852
|
+
const d = max - min;
|
|
2853
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
2854
|
+
switch (max) {
|
|
2855
|
+
case r:
|
|
2856
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
2857
|
+
break;
|
|
2858
|
+
case g:
|
|
2859
|
+
h = (b - r) / d + 2;
|
|
2860
|
+
break;
|
|
2861
|
+
case b:
|
|
2862
|
+
h = (r - g) / d + 4;
|
|
2863
|
+
break;
|
|
2864
|
+
}
|
|
2865
|
+
h /= 6;
|
|
2866
|
+
}
|
|
2867
|
+
return [h, s, l, ...((rgb.length >= 4) ? [rgb[3] / 255] : [])];
|
|
2868
|
+
}
|
|
2869
|
+
/**
|
|
2870
|
+
* Returns the RGB(A) of the given HSL(A).
|
|
2871
|
+
*/
|
|
2872
|
+
export const hslToRgb = (() => {
|
|
2873
|
+
const hue2rgb = (p, q, t) => {
|
|
2874
|
+
if (t < 0)
|
|
2875
|
+
t += 1;
|
|
2876
|
+
if (t > 1)
|
|
2877
|
+
t -= 1;
|
|
2878
|
+
if (t < 1 / 6)
|
|
2879
|
+
return p + (q - p) * 6 * t;
|
|
2880
|
+
if (t < 1 / 2)
|
|
2881
|
+
return q;
|
|
2882
|
+
if (t < 2 / 3)
|
|
2883
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
2884
|
+
return p;
|
|
2885
|
+
};
|
|
2886
|
+
return (hsl) => {
|
|
2887
|
+
const h = hsl[0];
|
|
2888
|
+
const s = hsl[1];
|
|
2889
|
+
const l = hsl[2];
|
|
2890
|
+
let r = 1;
|
|
2891
|
+
let g = 1;
|
|
2892
|
+
let b = 1;
|
|
2893
|
+
if (s !== 0) {
|
|
2894
|
+
const q = (l < 0.5) ? (l * (1 + s)) : (l + s - (l * s));
|
|
2895
|
+
const p = (2 * l) - q;
|
|
2896
|
+
r = hue2rgb(p, q, h + (1 / 3));
|
|
2897
|
+
g = hue2rgb(p, q, h);
|
|
2898
|
+
b = hue2rgb(p, q, h - (1 / 3));
|
|
2899
|
+
}
|
|
2900
|
+
return [r * 255, g * 255, b * 255, ...((hsl.length >= 4) ? [hsl[3] * 255] : [])].map((c) => Math.max(0, Math.min(255, Math.round(c))));
|
|
2901
|
+
};
|
|
2902
|
+
})();
|
|
2903
|
+
/**
|
|
2904
|
+
* Returns the LAB(A) of the given RGB(A).
|
|
2905
|
+
*/
|
|
2906
|
+
export function rgbToLab(rgb) {
|
|
2907
|
+
let r = rgb[0] / 255;
|
|
2908
|
+
let g = rgb[1] / 255;
|
|
2909
|
+
let b = rgb[2] / 255;
|
|
2910
|
+
r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
|
|
2911
|
+
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
|
|
2912
|
+
b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
|
|
2913
|
+
let x = ((r * 0.4124) + (g * 0.3576) + (b * 0.1805)) / 0.95047;
|
|
2914
|
+
let y = ((r * 0.2126) + (g * 0.7152) + (b * 0.0722));
|
|
2915
|
+
let z = ((r * 0.0193) + (g * 0.1192) + (b * 0.9505)) / 1.08883;
|
|
2916
|
+
x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);
|
|
2917
|
+
y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);
|
|
2918
|
+
z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);
|
|
2919
|
+
return [(116 * y) - 16, 500 * (x - y), 200 * (y - z), ...((rgb.length >= 4) ? [rgb[3] / 255] : [])];
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Returns the difference (calculated with DeltaE) of the LAB values of the given RGB values.
|
|
2923
|
+
*/
|
|
2924
|
+
export function getDifferenceBetweenRgb(rgbA, rgbB) {
|
|
2925
|
+
const labA = rgbToLab(rgbA);
|
|
2926
|
+
const labB = rgbToLab(rgbB);
|
|
2927
|
+
return getDifferenceBetweenLab(labA, labB);
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Returns the difference (calculated with DeltaE) of the given LAB values.
|
|
2931
|
+
*/
|
|
2932
|
+
export function getDifferenceBetweenLab(labA, labB) {
|
|
2933
|
+
const deltaL = labA[0] - labB[0];
|
|
2934
|
+
const deltaA = labA[1] - labB[1];
|
|
2935
|
+
const deltaB = labA[2] - labB[2];
|
|
2936
|
+
const c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
|
|
2937
|
+
const c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
|
|
2938
|
+
const deltaC = c1 - c2;
|
|
2939
|
+
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
|
|
2940
|
+
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
|
|
2941
|
+
const sc = 1.0 + 0.045 * c1;
|
|
2942
|
+
const sh = 1.0 + 0.015 * c1;
|
|
2943
|
+
const deltaLKlsl = deltaL / (1.0);
|
|
2944
|
+
const deltaCkcsc = deltaC / (sc);
|
|
2945
|
+
const deltaHkhsh = deltaH / (sh);
|
|
2946
|
+
const i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
|
|
2947
|
+
return (i < 0) ? 0 : Math.sqrt(i);
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Returns the RGB(A) between the two given RGB(A) values, based on the given percentage (0-100).
|
|
2951
|
+
*/
|
|
2952
|
+
export function getRgbBetween(startRgb, endRgb, percentage) {
|
|
2953
|
+
const p = FLOAT_LAX(percentage);
|
|
2954
|
+
const partEnd = Math.max(0, Math.min(1, (p / 100.0)));
|
|
2955
|
+
const partStart = (1 - partEnd);
|
|
2956
|
+
const length = Math.min(startRgb.length, endRgb.length);
|
|
2957
|
+
const result = [];
|
|
2958
|
+
for (let i = 0; i < length; i++) {
|
|
2959
|
+
result.push(Math.max(0, Math.min(255, Math.round((startRgb[i] * partStart) + (endRgb[i] * partEnd)))));
|
|
2960
|
+
}
|
|
2961
|
+
return result;
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Returns the RGB(A) of the given RGB(A) values, based on the given percentage (0-100).
|
|
2965
|
+
*/
|
|
2966
|
+
export function getRgbOfGradient(gradient, percentage) {
|
|
2967
|
+
const p = Math.max(0, Math.min(100, FLOAT_LAX(percentage)));
|
|
2968
|
+
let closest = null;
|
|
2969
|
+
each(gradient, (_color, percentStr) => {
|
|
2970
|
+
const percent = INT_LAX(percentStr);
|
|
2971
|
+
if (closest === null) {
|
|
2972
|
+
closest = [percent, Math.abs(p - percent)];
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
const difference = Math.abs(p - percent);
|
|
2976
|
+
if (difference < closest[1]) {
|
|
2977
|
+
closest = [percent, difference];
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
if (closest === null) {
|
|
2982
|
+
return [0, 0, 0];
|
|
2983
|
+
}
|
|
2984
|
+
const closestPercent = closest[0];
|
|
2985
|
+
const HIGHER = 99999;
|
|
2986
|
+
const LOWER = -99999;
|
|
2987
|
+
let higher = HIGHER;
|
|
2988
|
+
let lower = LOWER;
|
|
2989
|
+
each(gradient, (_color, percentStr) => {
|
|
2990
|
+
const percent = INT_LAX(percentStr);
|
|
2991
|
+
if (percent < closestPercent) {
|
|
2992
|
+
if (percent > lower) {
|
|
2993
|
+
lower = percent;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
if (percent > closestPercent) {
|
|
2997
|
+
if (percent < higher) {
|
|
2998
|
+
higher = percent;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
if (((higher === HIGHER) && (lower === LOWER)) || (higher === lower)) {
|
|
3003
|
+
// Type guard: verify gradient entry is number array
|
|
3004
|
+
const gradientValue = gradient[closestPercent];
|
|
3005
|
+
if (isNumberArray(gradientValue)) {
|
|
3006
|
+
return gradientValue;
|
|
3007
|
+
}
|
|
3008
|
+
console.error('LeUtils.getRgbFromGradient: gradient entry is not a number array', gradientValue);
|
|
3009
|
+
return [0, 0, 0];
|
|
3010
|
+
}
|
|
3011
|
+
else if ((higher !== HIGHER) && (lower !== LOWER)) {
|
|
3012
|
+
const higherDifference = Math.abs(higher - p);
|
|
3013
|
+
const lowerDifference = Math.abs(p - lower);
|
|
3014
|
+
if (higherDifference > lowerDifference) {
|
|
3015
|
+
higher = closestPercent;
|
|
3016
|
+
}
|
|
3017
|
+
else {
|
|
3018
|
+
lower = closestPercent;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
else if (lower === LOWER) {
|
|
3022
|
+
lower = closestPercent;
|
|
3023
|
+
}
|
|
3024
|
+
else {
|
|
3025
|
+
higher = closestPercent;
|
|
3026
|
+
}
|
|
3027
|
+
if (lower > higher) {
|
|
3028
|
+
const tmp = higher;
|
|
3029
|
+
higher = lower;
|
|
3030
|
+
lower = tmp;
|
|
3031
|
+
}
|
|
3032
|
+
const total = (higher - lower);
|
|
3033
|
+
const part = (p - lower);
|
|
3034
|
+
// Type guard: verify gradient entries are number arrays
|
|
3035
|
+
const lowerGradient = gradient[lower];
|
|
3036
|
+
const higherGradient = gradient[higher];
|
|
3037
|
+
if (!isNumberArray(lowerGradient) || !isNumberArray(higherGradient)) {
|
|
3038
|
+
console.error('LeUtils.getRgbFromGradient: gradient entries are not number arrays', lowerGradient, higherGradient);
|
|
3039
|
+
return [0, 0, 0];
|
|
3040
|
+
}
|
|
3041
|
+
return getRgbBetween(lowerGradient, higherGradient, ((part / total) * 100));
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Download file from URL or data.
|
|
3045
|
+
*/
|
|
3046
|
+
export function downloadFile(name, data) {
|
|
3047
|
+
if (!globalThis.document?.createElement || !globalThis.URL?.createObjectURL)
|
|
3048
|
+
return;
|
|
3049
|
+
const a = globalThis.document.createElement('a');
|
|
3050
|
+
a.style.display = 'none';
|
|
3051
|
+
const url = (typeof data === 'string') ? data : globalThis.URL.createObjectURL(data);
|
|
3052
|
+
a.href = url;
|
|
3053
|
+
a.download = name;
|
|
3054
|
+
globalThis.document.body.appendChild(a);
|
|
3055
|
+
a.click();
|
|
3056
|
+
setTimeout(() => {
|
|
3057
|
+
globalThis.document.body.removeChild(a);
|
|
3058
|
+
if (typeof data !== 'string')
|
|
3059
|
+
globalThis.URL.revokeObjectURL(url);
|
|
3060
|
+
}, 0);
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* LocalStorage Get.
|
|
3064
|
+
*/
|
|
3065
|
+
export function localStorageGet(key) {
|
|
3066
|
+
try {
|
|
3067
|
+
return globalThis.localStorage?.getItem(key) ?? null;
|
|
3068
|
+
}
|
|
3069
|
+
catch {
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* LocalStorage Set.
|
|
3075
|
+
*/
|
|
3076
|
+
export function localStorageSet(key, value) {
|
|
3077
|
+
try {
|
|
3078
|
+
globalThis.localStorage?.setItem(key, value);
|
|
3079
|
+
}
|
|
3080
|
+
catch {
|
|
3081
|
+
// Ignore error
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* LocalStorage Remove.
|
|
3086
|
+
*/
|
|
3087
|
+
export function localStorageRemove(key) {
|
|
3088
|
+
try {
|
|
3089
|
+
globalThis.localStorage?.removeItem(key);
|
|
3090
|
+
}
|
|
3091
|
+
catch {
|
|
3092
|
+
// Ignore error
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Checks if given host is private.
|
|
3097
|
+
*/
|
|
3098
|
+
export function isGivenHostPrivate(host) {
|
|
3099
|
+
const h = STRING(host).trim().toLowerCase();
|
|
3100
|
+
return h === 'localhost' || h === '127.0.0.1' || h.startsWith('192.168.') || h.startsWith('10.') || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(h);
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Checks if current host is private.
|
|
3104
|
+
*/
|
|
3105
|
+
export function isCurrentHostPrivate() {
|
|
3106
|
+
return isGivenHostPrivate(globalThis?.location?.hostname);
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* Create worker thread.
|
|
3110
|
+
*/
|
|
3111
|
+
export function createWorkerThread(name) {
|
|
3112
|
+
if (!globalThis?.Worker) {
|
|
3113
|
+
console.warn('LeUtils.createWorkerThread: Workers are not supported, returning a dummy.');
|
|
3114
|
+
return {
|
|
3115
|
+
worker: null,
|
|
3116
|
+
sendMessage: (_data, _options) => Promise.reject('Workers are not supported in this environment'),
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
const worker = new globalThis.Worker('/workers/' + name + '.worker.js');
|
|
3120
|
+
const listeners = new Map();
|
|
3121
|
+
const sendMessage = (data, options) => {
|
|
3122
|
+
return new Promise((resolve, reject) => {
|
|
3123
|
+
const id = uniqueId();
|
|
3124
|
+
listeners.set(id, resolve);
|
|
3125
|
+
globalThis.setTimeout(() => {
|
|
3126
|
+
listeners.delete(id);
|
|
3127
|
+
reject('timeout');
|
|
3128
|
+
}, options?.timeout ?? 10000);
|
|
3129
|
+
// Type guard: verify data is Record-like for spreading
|
|
3130
|
+
const messageData = isRecordLike(data) ? data : { data };
|
|
3131
|
+
worker.postMessage({ id, ...messageData });
|
|
3132
|
+
});
|
|
3133
|
+
};
|
|
3134
|
+
worker.onerror = (error) => {
|
|
3135
|
+
console.error('Worker ' + name + ':', error);
|
|
3136
|
+
};
|
|
3137
|
+
worker.onmessage = (message) => {
|
|
3138
|
+
const data = message.data;
|
|
3139
|
+
// Type guard: verify data has id property
|
|
3140
|
+
if (hasId(data)) {
|
|
3141
|
+
const callback = listeners.get(data.id);
|
|
3142
|
+
if (callback) {
|
|
3143
|
+
listeners.delete(data.id);
|
|
3144
|
+
callback(data);
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
return { worker, sendMessage };
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* Send message to worker.
|
|
3152
|
+
*/
|
|
3153
|
+
export const sendWorkerMessage = (() => {
|
|
3154
|
+
const workers = new Map();
|
|
3155
|
+
return (workerName, data, options) => {
|
|
3156
|
+
if (!workers.has(workerName)) {
|
|
3157
|
+
workers.set(workerName, createWorkerThread(workerName));
|
|
3158
|
+
}
|
|
3159
|
+
// Type guard: verify worker has sendMessage method
|
|
3160
|
+
const worker = workers.get(workerName);
|
|
3161
|
+
if (worker && hasSendMessage(worker)) {
|
|
3162
|
+
return worker.sendMessage(data, options);
|
|
3163
|
+
}
|
|
3164
|
+
console.error('LeUtils.createWorkerThread: worker does not have sendMessage method', worker);
|
|
3165
|
+
return Promise.reject(new Error('Worker does not have sendMessage method'));
|
|
3166
|
+
};
|
|
3167
|
+
})();
|
|
3168
|
+
//# sourceMappingURL=LeUtils.js.map
|