@zelgadis87/utils-core 5.2.12 → 5.3.1
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/.rollup/index.cjs +3448 -0
- package/.rollup/index.cjs.map +1 -0
- package/.rollup/index.d.ts +1409 -0
- package/.rollup/index.mjs +3268 -0
- package/.rollup/index.mjs.map +1 -0
- package/.rollup/tsconfig.tsbuildinfo +1 -0
- package/CHANGELOG.md +17 -0
- package/package.json +6 -8
- package/src/lazy/Cached.ts +216 -0
- package/src/lazy/Lazy.ts +4 -0
- package/src/lazy/LazyAsync.ts +4 -0
- package/src/lazy/_index.ts +1 -0
- package/src/utils/arrays/indexBy.ts +17 -17
- package/src/utils/arrays.ts +3 -1
- package/dist/Logger.d.ts +0 -22
- package/dist/Optional.d.ts +0 -108
- package/dist/async/CancelableDeferred.d.ts +0 -17
- package/dist/async/Deferred.d.ts +0 -32
- package/dist/async/RateThrottler.d.ts +0 -13
- package/dist/async/Semaphore.d.ts +0 -17
- package/dist/async/_index.d.ts +0 -3
- package/dist/index.d.ts +0 -8
- package/dist/lazy/Lazy.d.ts +0 -22
- package/dist/lazy/LazyAsync.d.ts +0 -24
- package/dist/lazy/LazyDictionary.d.ts +0 -9
- package/dist/lazy/_index.d.ts +0 -3
- package/dist/sorting/ComparisonChain.d.ts +0 -57
- package/dist/sorting/Sorter.d.ts +0 -121
- package/dist/sorting/_index.d.ts +0 -2
- package/dist/sorting/types.d.ts +0 -5
- package/dist/time/RandomTimeDuration.d.ts +0 -9
- package/dist/time/TimeBase.d.ts +0 -49
- package/dist/time/TimeDuration.d.ts +0 -71
- package/dist/time/TimeFrequency.d.ts +0 -10
- package/dist/time/TimeInstant.d.ts +0 -174
- package/dist/time/TimeInstantBuilder.d.ts +0 -72
- package/dist/time/TimeRange.d.ts +0 -11
- package/dist/time/TimeUnit.d.ts +0 -15
- package/dist/time/_index.d.ts +0 -8
- package/dist/time/constants.d.ts +0 -14
- package/dist/time/types.d.ts +0 -29
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/upgrade/DataUpgrader.d.ts +0 -22
- package/dist/upgrade/_index.d.ts +0 -2
- package/dist/upgrade/errors.d.ts +0 -12
- package/dist/upgrade/getTransitionsPath.d.ts +0 -3
- package/dist/upgrade/types.d.ts +0 -31
- package/dist/utils/_index.d.ts +0 -15
- package/dist/utils/arrays/groupBy.d.ts +0 -9
- package/dist/utils/arrays/indexBy.d.ts +0 -7
- package/dist/utils/arrays/statistics.d.ts +0 -8
- package/dist/utils/arrays/uniqBy.d.ts +0 -4
- package/dist/utils/arrays.d.ts +0 -71
- package/dist/utils/booleans.d.ts +0 -2
- package/dist/utils/css.d.ts +0 -14
- package/dist/utils/empties.d.ts +0 -18
- package/dist/utils/errors/withTryCatch.d.ts +0 -4
- package/dist/utils/errors.d.ts +0 -20
- package/dist/utils/functions/_index.d.ts +0 -3
- package/dist/utils/functions/constant.d.ts +0 -11
- package/dist/utils/functions/iff.d.ts +0 -8
- package/dist/utils/functions/predicateBuilder.d.ts +0 -7
- package/dist/utils/functions.d.ts +0 -41
- package/dist/utils/json.d.ts +0 -11
- package/dist/utils/nulls.d.ts +0 -9
- package/dist/utils/numbers/round.d.ts +0 -8
- package/dist/utils/numbers.d.ts +0 -35
- package/dist/utils/operations.d.ts +0 -28
- package/dist/utils/primitives.d.ts +0 -3
- package/dist/utils/promises.d.ts +0 -10
- package/dist/utils/random.d.ts +0 -3
- package/dist/utils/records/entries.d.ts +0 -7
- package/dist/utils/records.d.ts +0 -63
- package/dist/utils/strings/StringParts.d.ts +0 -38
- package/dist/utils/strings.d.ts +0 -20
- package/esbuild/index.cjs +0 -3380
- package/esbuild/index.cjs.map +0 -7
- package/esbuild/index.mjs +0 -3177
- package/esbuild/index.mjs.map +0 -7
|
@@ -0,0 +1,3268 @@
|
|
|
1
|
+
class DeferredCanceledError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("Execution canceled");
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
class Deferred {
|
|
7
|
+
_pending;
|
|
8
|
+
_internals;
|
|
9
|
+
get pending() { return this._pending; }
|
|
10
|
+
constructor() {
|
|
11
|
+
this._pending = true;
|
|
12
|
+
this._internals = this.createInternals();
|
|
13
|
+
}
|
|
14
|
+
resolve(val) {
|
|
15
|
+
if (!this._pending)
|
|
16
|
+
throw new Error('Illegal state exception');
|
|
17
|
+
this.resolveIfPending(val);
|
|
18
|
+
}
|
|
19
|
+
reject(reason) {
|
|
20
|
+
if (!this._pending)
|
|
21
|
+
throw new Error('Illegal state exception');
|
|
22
|
+
this.rejectIfPending(reason);
|
|
23
|
+
}
|
|
24
|
+
cancel() {
|
|
25
|
+
if (!this._pending)
|
|
26
|
+
throw new Error('Illegal state exception');
|
|
27
|
+
this.cancelIfPending();
|
|
28
|
+
}
|
|
29
|
+
resolveIfPending(val) {
|
|
30
|
+
if (!this._pending)
|
|
31
|
+
return;
|
|
32
|
+
this._pending = false;
|
|
33
|
+
this._internals.resolve(val);
|
|
34
|
+
}
|
|
35
|
+
rejectIfPending(reason) {
|
|
36
|
+
if (!this._pending)
|
|
37
|
+
return;
|
|
38
|
+
this._pending = false;
|
|
39
|
+
this._internals.reject(reason);
|
|
40
|
+
}
|
|
41
|
+
cancelIfPending() {
|
|
42
|
+
if (!this._pending)
|
|
43
|
+
return;
|
|
44
|
+
this.reject(new DeferredCanceledError());
|
|
45
|
+
}
|
|
46
|
+
then(onFulfilled, onRejected) {
|
|
47
|
+
return this._internals.promise.then(onFulfilled, onRejected);
|
|
48
|
+
}
|
|
49
|
+
catch(onRejected) {
|
|
50
|
+
return this._internals.promise.catch(onRejected);
|
|
51
|
+
}
|
|
52
|
+
finally(onfinally) {
|
|
53
|
+
return this._internals.promise.finally(onfinally);
|
|
54
|
+
}
|
|
55
|
+
asPromise() {
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
asCancelablePromise() {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
createInternals() {
|
|
62
|
+
let resolveSelf, rejectSelf;
|
|
63
|
+
const promise = new Promise((resolve, reject) => {
|
|
64
|
+
resolveSelf = resolve;
|
|
65
|
+
rejectSelf = reject;
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
promise,
|
|
69
|
+
resolve: resolveSelf,
|
|
70
|
+
reject: rejectSelf,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
get [Symbol.toStringTag]() {
|
|
74
|
+
return '[object Deferred]';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class RateThrottler {
|
|
79
|
+
_frequency;
|
|
80
|
+
_availableSlots;
|
|
81
|
+
_waitingRequests;
|
|
82
|
+
constructor(_frequency) {
|
|
83
|
+
this._frequency = _frequency;
|
|
84
|
+
const initialOffset = this.cooldown.divideBy(_frequency.times);
|
|
85
|
+
this._availableSlots = new Array(_frequency.times).fill(0).map((_, i) => TimeInstant.now().addMs(i * initialOffset.ms));
|
|
86
|
+
this._waitingRequests = [];
|
|
87
|
+
}
|
|
88
|
+
get cooldown() {
|
|
89
|
+
return this._frequency.period;
|
|
90
|
+
}
|
|
91
|
+
async execute(fn) {
|
|
92
|
+
const wrappedFn = async () => {
|
|
93
|
+
const slot = this._availableSlots.shift();
|
|
94
|
+
try {
|
|
95
|
+
return slot.promise().then(() => fn());
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
this._availableSlots.push(TimeInstant.now().addDuration(this.cooldown));
|
|
99
|
+
if (this._waitingRequests.length > 0)
|
|
100
|
+
this._waitingRequests.shift().resolve();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
if (this._availableSlots.length > 0) {
|
|
104
|
+
return wrappedFn();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Semaphore implementation using promises.
|
|
108
|
+
const waitingRequest = new Deferred();
|
|
109
|
+
this._waitingRequests.push(waitingRequest);
|
|
110
|
+
return waitingRequest.then(() => wrappedFn());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function throttle(fn, frequence) {
|
|
115
|
+
const semaphore = new RateThrottler(frequence);
|
|
116
|
+
return (...t) => {
|
|
117
|
+
return semaphore.execute(async () => fn(...t));
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class Semaphore {
|
|
122
|
+
_availableSlots;
|
|
123
|
+
_queuedRequests;
|
|
124
|
+
_inProgress = 0;
|
|
125
|
+
constructor(_availableSlots = 1) {
|
|
126
|
+
this._availableSlots = _availableSlots;
|
|
127
|
+
this._queuedRequests = [];
|
|
128
|
+
}
|
|
129
|
+
async _awaitSlot() {
|
|
130
|
+
if (this._availableSlots > 0) {
|
|
131
|
+
this._availableSlots -= 1;
|
|
132
|
+
this._inProgress += 1;
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Semaphore implementation using promises.
|
|
137
|
+
const deferred = new Deferred();
|
|
138
|
+
this._queuedRequests.push(deferred);
|
|
139
|
+
return deferred.asPromise();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
_releaseSlot() {
|
|
143
|
+
const waitingSlotToResolve = this._queuedRequests.shift();
|
|
144
|
+
if (waitingSlotToResolve) {
|
|
145
|
+
// Let the next request start.
|
|
146
|
+
waitingSlotToResolve.resolve();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Free a slot.
|
|
150
|
+
this._availableSlots += 1;
|
|
151
|
+
this._inProgress -= 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async execute(fn, cooldown = TimeDuration.ZERO) {
|
|
155
|
+
return this._awaitSlot().then(fn).finally(() => { void cooldown.delay(() => this._releaseSlot()); });
|
|
156
|
+
}
|
|
157
|
+
get availableSlots() {
|
|
158
|
+
return this._availableSlots;
|
|
159
|
+
}
|
|
160
|
+
get queueSize() {
|
|
161
|
+
return this._queuedRequests.length;
|
|
162
|
+
}
|
|
163
|
+
get inProgressSize() {
|
|
164
|
+
return this._inProgress;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function ensureDefined(v, name = 'value') {
|
|
169
|
+
if (isDefined(v))
|
|
170
|
+
return v;
|
|
171
|
+
throw new Error('Expected ' + name + ' to be defined, got: ' + v);
|
|
172
|
+
}
|
|
173
|
+
function isDefined(x) {
|
|
174
|
+
return x !== null && x !== undefined;
|
|
175
|
+
}
|
|
176
|
+
function isNullOrUndefined(x) {
|
|
177
|
+
return x === null || x === undefined;
|
|
178
|
+
}
|
|
179
|
+
function ifDefined(source, callback) {
|
|
180
|
+
if (isDefined(source))
|
|
181
|
+
callback(source);
|
|
182
|
+
}
|
|
183
|
+
function mapDefined(source, mapper) {
|
|
184
|
+
if (isDefined(source))
|
|
185
|
+
return mapper(source);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function ifNullOrUndefined(source, callback) {
|
|
189
|
+
if (isNullOrUndefined(source))
|
|
190
|
+
callback();
|
|
191
|
+
}
|
|
192
|
+
function throwIfNullOrUndefined(source, errorProducer = () => new Error(`Unexpected ${source} value`)) {
|
|
193
|
+
return ifNullOrUndefined(source, () => { throw errorProducer(); });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
class Optional {
|
|
197
|
+
_present;
|
|
198
|
+
_value;
|
|
199
|
+
constructor(t) {
|
|
200
|
+
const defined = isDefined(t);
|
|
201
|
+
this._value = defined ? t : undefined;
|
|
202
|
+
this._present = defined;
|
|
203
|
+
}
|
|
204
|
+
getRawValue() {
|
|
205
|
+
return this._value;
|
|
206
|
+
}
|
|
207
|
+
get() {
|
|
208
|
+
return this.getOrElseThrow(() => new ErrorGetEmptyOptional());
|
|
209
|
+
}
|
|
210
|
+
getOrElseThrow(errorProducer) {
|
|
211
|
+
if (this.isEmpty())
|
|
212
|
+
throw errorProducer();
|
|
213
|
+
return this._value;
|
|
214
|
+
}
|
|
215
|
+
set(t) {
|
|
216
|
+
if (isNullOrUndefined(t))
|
|
217
|
+
throw new ErrorSetEmptyOptional();
|
|
218
|
+
this._value = t;
|
|
219
|
+
this._present = true;
|
|
220
|
+
}
|
|
221
|
+
setNullable(t) {
|
|
222
|
+
if (isDefined(t)) {
|
|
223
|
+
return this.set(t);
|
|
224
|
+
}
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
clear() {
|
|
228
|
+
this._value = undefined;
|
|
229
|
+
this._present = false;
|
|
230
|
+
}
|
|
231
|
+
isEmpty() {
|
|
232
|
+
return !this._present;
|
|
233
|
+
}
|
|
234
|
+
isPresent() {
|
|
235
|
+
return this._present;
|
|
236
|
+
}
|
|
237
|
+
ifEmpty(callback) {
|
|
238
|
+
if (this.isEmpty())
|
|
239
|
+
return callback();
|
|
240
|
+
}
|
|
241
|
+
ifPresent(callback) {
|
|
242
|
+
if (this.isPresent())
|
|
243
|
+
return callback(this.get());
|
|
244
|
+
}
|
|
245
|
+
ifPresentThenClear(callback) {
|
|
246
|
+
if (this.isPresent()) {
|
|
247
|
+
callback(this.get());
|
|
248
|
+
this.clear();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
apply(callbackIfPresent, callbackIfEmpty) {
|
|
252
|
+
if (this.isEmpty()) {
|
|
253
|
+
return callbackIfEmpty();
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
return callbackIfPresent(this.get());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
orElseReturn(newValue) {
|
|
260
|
+
if (this.isPresent()) {
|
|
261
|
+
return this.get();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
return newValue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
orElse = this.orElseReturn.bind(this);
|
|
268
|
+
orElseProduce(newValueProducer) {
|
|
269
|
+
if (this.isPresent()) {
|
|
270
|
+
return this.get();
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
return newValueProducer();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
orElseGet = this.orElseProduce.bind(this);
|
|
277
|
+
orElseReturnAndApply(newValue) {
|
|
278
|
+
if (this.isPresent()) {
|
|
279
|
+
return this.get();
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
this.set(newValue);
|
|
283
|
+
return newValue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
orElseProduceAndApply(newValueProducer) {
|
|
287
|
+
if (this.isPresent()) {
|
|
288
|
+
return this.get();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const newValue = newValueProducer();
|
|
292
|
+
this.set(newValue);
|
|
293
|
+
return newValue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
orElseReturnNullableAndApply(newValue) {
|
|
297
|
+
if (this.isPresent()) {
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.setNullable(newValue);
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
orElseProduceNullableAndApply(newValueProducer) {
|
|
306
|
+
if (this.isPresent()) {
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const newValue = newValueProducer();
|
|
311
|
+
this.setNullable(newValue);
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
orElseReturnNullable(newValue) {
|
|
316
|
+
if (this.isEmpty())
|
|
317
|
+
return Optional.ofNullable(newValue);
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
orElseNullable = this.orElseReturnNullable.bind(this);
|
|
321
|
+
orElseProduceNullable(newValueProducer) {
|
|
322
|
+
if (this.isEmpty()) {
|
|
323
|
+
const newValue = newValueProducer();
|
|
324
|
+
return Optional.ofNullable(newValue);
|
|
325
|
+
}
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
orElseGetNullable = this.orElseProduceNullable.bind(this);
|
|
329
|
+
orElseThrow(errorProducer) {
|
|
330
|
+
if (this.isEmpty())
|
|
331
|
+
throw errorProducer();
|
|
332
|
+
return this;
|
|
333
|
+
}
|
|
334
|
+
throwIfPresent(errorProducer) {
|
|
335
|
+
if (this.isEmpty())
|
|
336
|
+
return this;
|
|
337
|
+
throw errorProducer(this.get());
|
|
338
|
+
}
|
|
339
|
+
mapTo(mapper) {
|
|
340
|
+
return this.flatMapTo(t => Optional.ofNullable(mapper(t)));
|
|
341
|
+
}
|
|
342
|
+
flatMapTo(mapper) {
|
|
343
|
+
return this.isPresent() ? mapper(this.get()) : Optional.empty();
|
|
344
|
+
}
|
|
345
|
+
filter(predicate) {
|
|
346
|
+
if (this.isEmpty())
|
|
347
|
+
return this;
|
|
348
|
+
if (predicate(this.get()))
|
|
349
|
+
return this;
|
|
350
|
+
return Optional.empty();
|
|
351
|
+
}
|
|
352
|
+
static empty() {
|
|
353
|
+
return new Optional(undefined);
|
|
354
|
+
}
|
|
355
|
+
static of(t) {
|
|
356
|
+
if (isNullOrUndefined(t))
|
|
357
|
+
throw new ErrorCannotInstantiatePresentOptionalWithEmptyValue();
|
|
358
|
+
return new Optional(t);
|
|
359
|
+
}
|
|
360
|
+
static ofNullable(t) {
|
|
361
|
+
return new Optional(t);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
class ErrorGetEmptyOptional extends Error {
|
|
365
|
+
constructor() {
|
|
366
|
+
super("Cannot retrieve a value from an empty Optional.");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
class ErrorSetEmptyOptional extends Error {
|
|
370
|
+
constructor() {
|
|
371
|
+
super("Cannot set a null or undefined value.");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
class ErrorCannotInstantiatePresentOptionalWithEmptyValue extends Error {
|
|
375
|
+
constructor() {
|
|
376
|
+
super("Cannot initialize a PresentOptional with a null or undefined value.");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function groupByString(arr, field) {
|
|
381
|
+
return groupByStringWith(arr, t => t[field]);
|
|
382
|
+
}
|
|
383
|
+
function groupByNumber(arr, field) {
|
|
384
|
+
return groupByNumberWith(arr, t => t[field]);
|
|
385
|
+
}
|
|
386
|
+
function groupByBoolean(arr, field) {
|
|
387
|
+
return groupByBooleanWith(arr, t => t[field]);
|
|
388
|
+
}
|
|
389
|
+
function groupBySymbol(arr, field) {
|
|
390
|
+
return groupBySymbolWith(arr, t => t[field]);
|
|
391
|
+
}
|
|
392
|
+
function groupByStringWith(arr, getter) {
|
|
393
|
+
return doGroupByWith(arr, getter);
|
|
394
|
+
}
|
|
395
|
+
function groupByNumberWith(arr, getter) {
|
|
396
|
+
return doGroupByWith(arr, getter);
|
|
397
|
+
}
|
|
398
|
+
function groupByBooleanWith(arr, getter) {
|
|
399
|
+
return doGroupByWith(arr, item => getter(item) ? "true" : "false");
|
|
400
|
+
}
|
|
401
|
+
function groupBySymbolWith(arr, getter) {
|
|
402
|
+
return doGroupByWith(arr, getter);
|
|
403
|
+
}
|
|
404
|
+
function doGroupByWith(arr, getter) {
|
|
405
|
+
return [...arr].reduce((dict, cur) => {
|
|
406
|
+
const key = getter(cur);
|
|
407
|
+
if (key !== null && key !== undefined) {
|
|
408
|
+
const arr = dict[key] ?? [];
|
|
409
|
+
arr.push(cur);
|
|
410
|
+
dict[key] = arr;
|
|
411
|
+
}
|
|
412
|
+
return dict;
|
|
413
|
+
}, {});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function indexByString(arr, field, valueMapper = v => v) {
|
|
417
|
+
return indexByStringWith(arr, t => t[field], valueMapper);
|
|
418
|
+
}
|
|
419
|
+
function indexByNumber(arr, field, valueMapper = v => v) {
|
|
420
|
+
return indexByNumberWith(arr, t => t[field], valueMapper);
|
|
421
|
+
}
|
|
422
|
+
function indexBySymbol(arr, field, valueMapper = v => v) {
|
|
423
|
+
return indexBySymbolWith(arr, t => t[field], valueMapper);
|
|
424
|
+
}
|
|
425
|
+
function indexByStringWith(arr, keyGetter, valueMapper = v => v) {
|
|
426
|
+
return doIndexByWith(arr, keyGetter, valueMapper);
|
|
427
|
+
}
|
|
428
|
+
function indexByNumberWith(arr, keyGetter, valueMapper = v => v) {
|
|
429
|
+
return doIndexByWith(arr, keyGetter, valueMapper);
|
|
430
|
+
}
|
|
431
|
+
function indexBySymbolWith(arr, keyGetter, valueMapper = v => v) {
|
|
432
|
+
return doIndexByWith(arr, keyGetter, valueMapper);
|
|
433
|
+
}
|
|
434
|
+
function doIndexByWith(arr, keyGetter, valueMapper = v => v) {
|
|
435
|
+
return arr.reduce((dict, cur) => {
|
|
436
|
+
const key = keyGetter(cur);
|
|
437
|
+
if (key !== null && key !== undefined) {
|
|
438
|
+
if (dict[key]) {
|
|
439
|
+
throw new Error(`Multiple values indexed by same key "${String(key)}".`);
|
|
440
|
+
}
|
|
441
|
+
dict[key] = valueMapper(cur);
|
|
442
|
+
}
|
|
443
|
+
return dict;
|
|
444
|
+
}, {});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function average(arr) {
|
|
448
|
+
const f = 1 / arr.length;
|
|
449
|
+
return arr.reduce((tot, cur) => tot + (cur * f), 0);
|
|
450
|
+
}
|
|
451
|
+
function sum(arr) {
|
|
452
|
+
return arr.reduce((tot, cur) => tot + cur, 0);
|
|
453
|
+
}
|
|
454
|
+
function sumBy(arr, getter) {
|
|
455
|
+
return sum(arr.map(getter));
|
|
456
|
+
}
|
|
457
|
+
function min(arr) {
|
|
458
|
+
if (arr.length === 0)
|
|
459
|
+
throw new Error('Cannot calculate value on empty array');
|
|
460
|
+
return arr.reduce((min, cur) => cur < min ? cur : min);
|
|
461
|
+
}
|
|
462
|
+
function minBy(arr, getter) {
|
|
463
|
+
return min(arr.map(getter));
|
|
464
|
+
}
|
|
465
|
+
function max(arr) {
|
|
466
|
+
if (arr.length === 0)
|
|
467
|
+
throw new Error('Cannot calculate value on empty array');
|
|
468
|
+
return arr.reduce((max, cur) => cur > max ? cur : max);
|
|
469
|
+
}
|
|
470
|
+
function maxBy(arr, getter) {
|
|
471
|
+
return max(arr.map(getter));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function constant(v) { return () => v; }
|
|
475
|
+
function identity(t) { return t; }
|
|
476
|
+
const constantNull = constant(null);
|
|
477
|
+
const constantTrue = constant(true);
|
|
478
|
+
const constantFalse = constant(false);
|
|
479
|
+
const constantUndefined = constant(void 0);
|
|
480
|
+
const alwaysTrue = constantTrue;
|
|
481
|
+
const alwaysFalse = constantFalse;
|
|
482
|
+
const constantZero = constant(0);
|
|
483
|
+
const constantOne = constant(1);
|
|
484
|
+
|
|
485
|
+
function iff(firstPredicate, valueIfTrue) {
|
|
486
|
+
if (firstPredicate === true || (firstPredicate instanceof Function && firstPredicate() === true)) {
|
|
487
|
+
const ret = {
|
|
488
|
+
elseIf: () => ret,
|
|
489
|
+
otherwise: () => valueIfTrue
|
|
490
|
+
};
|
|
491
|
+
return ret;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
const ret = {
|
|
495
|
+
elseIf: (elseIf, valueIfElseIfTrue) => iff(elseIf, valueIfElseIfTrue),
|
|
496
|
+
otherwise: (valueIfElse) => valueIfElse
|
|
497
|
+
};
|
|
498
|
+
return ret;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
class PredicateBuilder {
|
|
503
|
+
_currentPredicate = constantTrue;
|
|
504
|
+
and(predicate) {
|
|
505
|
+
const curPredicate = this._currentPredicate.bind(undefined);
|
|
506
|
+
const newPredicate = (t) => curPredicate(t) && predicate(t);
|
|
507
|
+
this._currentPredicate = newPredicate;
|
|
508
|
+
return this;
|
|
509
|
+
}
|
|
510
|
+
or(predicate) {
|
|
511
|
+
const newPredicate = (t) => this._currentPredicate(t) || predicate(t);
|
|
512
|
+
this._currentPredicate = newPredicate;
|
|
513
|
+
return this;
|
|
514
|
+
}
|
|
515
|
+
toPredicate() {
|
|
516
|
+
return this._currentPredicate;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function isFunction(t) {
|
|
521
|
+
return typeof t === 'function';
|
|
522
|
+
}
|
|
523
|
+
function noop() { } // eslint-disable-line @typescript-eslint/no-empty-function
|
|
524
|
+
function filterWithTypePredicate(filter) {
|
|
525
|
+
return (t) => filter(t);
|
|
526
|
+
}
|
|
527
|
+
function not(predicate) {
|
|
528
|
+
return (t) => !predicate(t);
|
|
529
|
+
}
|
|
530
|
+
function and(...predicates) {
|
|
531
|
+
if (predicates.length === 0)
|
|
532
|
+
return constantTrue;
|
|
533
|
+
else if (predicates.length === 1)
|
|
534
|
+
return predicates[0];
|
|
535
|
+
return (t) => predicates.reduce((prev, cur) => prev ? cur(t) : false, true);
|
|
536
|
+
}
|
|
537
|
+
function or(...predicates) {
|
|
538
|
+
if (predicates.length === 0)
|
|
539
|
+
return constantTrue;
|
|
540
|
+
else if (predicates.length === 1)
|
|
541
|
+
return predicates[0];
|
|
542
|
+
return (t) => predicates.reduce((prev, cur) => prev ? true : cur(t), false);
|
|
543
|
+
}
|
|
544
|
+
function xor(a, b) {
|
|
545
|
+
return (t) => a(t) !== b(t);
|
|
546
|
+
}
|
|
547
|
+
function pipedInvoke(...fns) { return pipedInvokeFromArray(fns); }
|
|
548
|
+
function pipedInvokeFromArray(fns) {
|
|
549
|
+
if (fns.length === 0) {
|
|
550
|
+
return identity;
|
|
551
|
+
}
|
|
552
|
+
if (fns.length === 1) {
|
|
553
|
+
return fns[0];
|
|
554
|
+
}
|
|
555
|
+
return function piped(input) {
|
|
556
|
+
return fns.reduce((prev, fn) => fn(prev), input);
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function uniq(arr) {
|
|
561
|
+
return uniqBy(arr, identity);
|
|
562
|
+
}
|
|
563
|
+
function uniqBy(arr, getter) {
|
|
564
|
+
return arr.reduce((dict, cur) => {
|
|
565
|
+
const key = getter(cur);
|
|
566
|
+
if (dict.keys.includes(key)) {
|
|
567
|
+
return dict;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
return {
|
|
571
|
+
keys: [...dict.keys, key],
|
|
572
|
+
values: [...dict.values, cur]
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}, { keys: [], values: [] }).values;
|
|
576
|
+
}
|
|
577
|
+
function uniqByKey(arr, key) {
|
|
578
|
+
return uniqBy(arr, item => item[key]);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function ensureArray(t) {
|
|
582
|
+
if (isNullOrUndefined(t))
|
|
583
|
+
return [];
|
|
584
|
+
return t instanceof Array ? t : [t];
|
|
585
|
+
}
|
|
586
|
+
function ensureReadableArray(t) {
|
|
587
|
+
if (isNullOrUndefined(t))
|
|
588
|
+
return [];
|
|
589
|
+
return t instanceof Array ? t : [t];
|
|
590
|
+
}
|
|
591
|
+
function isArray(t) {
|
|
592
|
+
return t instanceof Array;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Generate a new copy of an array with:
|
|
596
|
+
* - the first instance of an item matching the predicate being replaced with the given argument
|
|
597
|
+
* - the given argument added as last item of the array if the predicate did not produce a match
|
|
598
|
+
* @param arr - the original array of items
|
|
599
|
+
* @param item - the item to insert or update
|
|
600
|
+
* @param predicate - a function that returns true iff an item is equal to the given argument
|
|
601
|
+
* @returns a new copy of the array with the item updated or added in last place
|
|
602
|
+
*/
|
|
603
|
+
function upsert(arr, item, isEqual) {
|
|
604
|
+
const index = arr.findIndex(a => isEqual(a, item));
|
|
605
|
+
if (index === -1) {
|
|
606
|
+
return [...arr, item];
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
return [
|
|
610
|
+
...arr.slice(0, index),
|
|
611
|
+
item,
|
|
612
|
+
...arr.slice(index + 1)
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function range(start, end) {
|
|
617
|
+
if (end < start)
|
|
618
|
+
throw new Error();
|
|
619
|
+
let length = (end - start) + 1;
|
|
620
|
+
return new Array(length).fill(1).map((_, i) => start + i);
|
|
621
|
+
}
|
|
622
|
+
function fill(length, value) {
|
|
623
|
+
return new Array(length).fill(value);
|
|
624
|
+
}
|
|
625
|
+
function extendArray(arr, props) {
|
|
626
|
+
return arr.map((t) => ({
|
|
627
|
+
...t,
|
|
628
|
+
...props
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
function extendArrayWith(arr, propsFn) {
|
|
632
|
+
return arr.map((t) => ({
|
|
633
|
+
...t,
|
|
634
|
+
...propsFn(t)
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
function reverse$1(arr) {
|
|
638
|
+
return [...arr].reverse();
|
|
639
|
+
}
|
|
640
|
+
function first$1(arr, defaultValue = null) {
|
|
641
|
+
return (arr.length ? arr[0] : defaultValue);
|
|
642
|
+
}
|
|
643
|
+
function last$1(arr, defaultValue = null) {
|
|
644
|
+
return (arr.length ? arr[arr.length - 1] : defaultValue);
|
|
645
|
+
}
|
|
646
|
+
const head = first$1;
|
|
647
|
+
function tail(arr) {
|
|
648
|
+
return (arr.length ? arr.slice(1) : []);
|
|
649
|
+
}
|
|
650
|
+
function sortedArray(arr, sortFn) {
|
|
651
|
+
return [...arr].sort(sortFn);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Custom implementation of includes which allows checking for arbitrary items inside the array.
|
|
655
|
+
*/
|
|
656
|
+
function arrayIncludes(arr, item, fromIndex) {
|
|
657
|
+
return arr.includes(item, fromIndex);
|
|
658
|
+
}
|
|
659
|
+
/** @deprecated[2025.11.08]: Use {@link arrayIncludes} instead. */
|
|
660
|
+
const includes = arrayIncludes;
|
|
661
|
+
function mapTruthys(arr, mapper) {
|
|
662
|
+
return arr.map(mapper).filter(value => value !== undefined);
|
|
663
|
+
}
|
|
664
|
+
function flatMapTruthys(arr, mapper) {
|
|
665
|
+
return arr.flatMap(mapper).filter(value => value !== undefined);
|
|
666
|
+
}
|
|
667
|
+
function filterMap(array, filterFn, mapFn) {
|
|
668
|
+
return array.filter(filterFn).map(mapFn);
|
|
669
|
+
}
|
|
670
|
+
function filterMapReduce(array, filterFn, mapFn, reduceFn, initialValue) {
|
|
671
|
+
return array.filter(filterFn).map(mapFn).reduce(reduceFn, initialValue);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Partitions the given array in two groups, in the first one there will be any and only values of the array that match the given predicate, in the second one there will be any and only values that don't.
|
|
675
|
+
* @param arr the array of items
|
|
676
|
+
* @param predicate the predicate to use to partition the array
|
|
677
|
+
* @returns a tuple, where the first array contains items matching the predicate, and the second array contains items not matching the predicate.
|
|
678
|
+
*/
|
|
679
|
+
function partition(arr, predicate) {
|
|
680
|
+
return arr.reduce((partition, item) => {
|
|
681
|
+
partition[(predicate(item) ? 0 : 1)].push(item);
|
|
682
|
+
return partition;
|
|
683
|
+
}, [[], []]);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Maps the first truthy value in the array using the provided mapping function.
|
|
687
|
+
* @param arr - The array of items.
|
|
688
|
+
* @param mapFn - The mapping function.
|
|
689
|
+
* @returns The first truthy value returned by the mapping function, or null if no truthy value is found.
|
|
690
|
+
*/
|
|
691
|
+
function mapFirstTruthy(arr, mapFn) {
|
|
692
|
+
for (let i = 0; i < arr.length; i++) {
|
|
693
|
+
const result = mapFn(arr[i]);
|
|
694
|
+
if (result)
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
function listToDict(arr, mapFn) {
|
|
700
|
+
return arr.reduce((dict, cur) => {
|
|
701
|
+
const [key, value] = mapFn(cur);
|
|
702
|
+
return { ...dict, [key]: value };
|
|
703
|
+
}, {});
|
|
704
|
+
}
|
|
705
|
+
function shallowArrayEquals(a, b) {
|
|
706
|
+
if (a === b)
|
|
707
|
+
return true;
|
|
708
|
+
if (a.length !== b.length)
|
|
709
|
+
return false;
|
|
710
|
+
for (let i = 0; i < a.length; i++) {
|
|
711
|
+
if (a[i] !== b[i])
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
function findInArray(arr, predicate) {
|
|
717
|
+
return Optional.ofNullable(arr.find(predicate));
|
|
718
|
+
}
|
|
719
|
+
function findIndexInArray(arr, predicate) {
|
|
720
|
+
const idx = arr.findIndex(predicate);
|
|
721
|
+
return idx === -1 ? Optional.empty() : Optional.of(idx);
|
|
722
|
+
}
|
|
723
|
+
function zip(ts, rs) {
|
|
724
|
+
if (ts.length !== rs.length)
|
|
725
|
+
throw new Error(`Arrays must have the same length. Got ${ts.length} and ${rs.length}`);
|
|
726
|
+
return ts.map((t, i) => [t, rs[i]]);
|
|
727
|
+
}
|
|
728
|
+
function unzip(arr) {
|
|
729
|
+
return arr.reduce(([ts, rs], [t, r]) => {
|
|
730
|
+
return [[...ts, t], [...rs, r]];
|
|
731
|
+
}, [[], []]);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Gets the element at the specified index in the array, wrapped in an Optional.
|
|
735
|
+
* Returns an empty Optional if the index is out of bounds (negative or >= array length).
|
|
736
|
+
* @param arr - The array to get the element from
|
|
737
|
+
* @param index - The index of the element to retrieve
|
|
738
|
+
* @returns An Optional containing the element at the index, or empty if index is out of bounds
|
|
739
|
+
*/
|
|
740
|
+
function arrayGet(arr, index) {
|
|
741
|
+
if (index < 0 || index >= arr.length)
|
|
742
|
+
return Optional.empty();
|
|
743
|
+
return Optional.of(arr[index]);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function isTrue(x) {
|
|
747
|
+
return x === true;
|
|
748
|
+
}
|
|
749
|
+
function isFalse(x) {
|
|
750
|
+
return x === false;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const newLine = '\n', tabulation = '\t', colon = ':', semiColon = ';', space = ' ', openBracket = '{', closeBracket = '}';
|
|
754
|
+
function cssDeclarationRulesDictionaryToCss(syleDeclarationRulesForSelectorsProduceable, indent = 0) {
|
|
755
|
+
const syleDeclarationRulesForSelectors = produceableToValue(syleDeclarationRulesForSelectorsProduceable);
|
|
756
|
+
return Object.entries(syleDeclarationRulesForSelectors).map(([selector, styleDeclarationRules]) => {
|
|
757
|
+
const cssRules = cssSelectorDeclarationRulesDictionaryToCss(styleDeclarationRules, indent + 1);
|
|
758
|
+
if (!cssRules.length)
|
|
759
|
+
return null;
|
|
760
|
+
return repeat(tabulation, indent) + selector + space + openBracket + newLine + cssRules.join(newLine) + newLine + closeBracket;
|
|
761
|
+
}).filter(Boolean).join(newLine + newLine);
|
|
762
|
+
}
|
|
763
|
+
/* exported for test purposes only */
|
|
764
|
+
function cssSelectorDeclarationRulesDictionaryToCss(styleDeclarationRules, indent = 0) {
|
|
765
|
+
return Object.entries(styleDeclarationRules).map(([key, value]) => {
|
|
766
|
+
if (typeof value === 'string') {
|
|
767
|
+
if (key.startsWith('--')) {
|
|
768
|
+
return repeat(tabulation, indent) + key + colon + space + value + semiColon;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
return repeat(tabulation, indent) + pascalCaseToKebabCase(key) + colon + space + value + semiColon;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
return repeat(tabulation, indent) + key + space + openBracket + newLine + cssSelectorDeclarationRulesDictionaryToCss(value, indent + 1).join(newLine) + newLine + repeat(tabulation, indent) + closeBracket;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
function transformCssDictionary(dict, transformer) {
|
|
780
|
+
const recurse = (dict) => {
|
|
781
|
+
const newDict = {};
|
|
782
|
+
for (const [key, value] of Object.entries(dict)) {
|
|
783
|
+
if (typeof value === 'string') {
|
|
784
|
+
const newValue = transformer(key, value);
|
|
785
|
+
if (isDefined(newValue)) {
|
|
786
|
+
newDict[key] = newValue;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
newDict[key] = transformCssDictionary(value, transformer);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return newDict;
|
|
794
|
+
};
|
|
795
|
+
return recurse(dict);
|
|
796
|
+
}
|
|
797
|
+
function pascalCaseToKebabCase(s) {
|
|
798
|
+
return s.split(/([A-Z][a-z]*)/).filter(Boolean).map(n => n.toLowerCase()).join('-');
|
|
799
|
+
}
|
|
800
|
+
function produceableToValue(t) {
|
|
801
|
+
return isFunction(t) ? t() : t;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function withTryCatch(fn, errMapFn = identity) {
|
|
805
|
+
try {
|
|
806
|
+
return [fn(), void 0];
|
|
807
|
+
}
|
|
808
|
+
catch (e) {
|
|
809
|
+
return [void 0, errMapFn(asError(e))];
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function withTryCatchAsync(fn, errMapFn = identity) {
|
|
813
|
+
try {
|
|
814
|
+
return [await fn(), void 0];
|
|
815
|
+
}
|
|
816
|
+
catch (e) {
|
|
817
|
+
return [void 0, errMapFn(asError(e))];
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function asError(e) {
|
|
822
|
+
if (e === undefined || e === null)
|
|
823
|
+
return new Error('Void message');
|
|
824
|
+
if (isError(e))
|
|
825
|
+
return e;
|
|
826
|
+
if (typeof e === 'string')
|
|
827
|
+
return new Error(e);
|
|
828
|
+
if (e instanceof String)
|
|
829
|
+
return new Error(e.toString());
|
|
830
|
+
return new Error(JSON.stringify(e));
|
|
831
|
+
}
|
|
832
|
+
function isError(e) {
|
|
833
|
+
return e instanceof Error;
|
|
834
|
+
}
|
|
835
|
+
function getMessageFromError(error) {
|
|
836
|
+
return `${error.name}: ${error.message}${getCauseMessageFromError(error)}`;
|
|
837
|
+
}
|
|
838
|
+
function getStackFromError(error) {
|
|
839
|
+
const stack = error.stack && error.stack.includes(error.message) ? error.stack : error.message + ' ' + (error.stack ?? '');
|
|
840
|
+
return `${error.name}: ${stack}${getCauseStackFromError(error)}`;
|
|
841
|
+
}
|
|
842
|
+
function getCauseMessageFromError(error) {
|
|
843
|
+
if (isNullOrUndefined(error.cause))
|
|
844
|
+
return '';
|
|
845
|
+
return `\ncaused by: ${getMessageFromError(asError(error.cause))}`;
|
|
846
|
+
}
|
|
847
|
+
function getCauseStackFromError(error) {
|
|
848
|
+
if (isNullOrUndefined(error.cause))
|
|
849
|
+
return '';
|
|
850
|
+
return `\ncaused by: ${getStackFromError(asError(error.cause))}`;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Throws an error if a switch discriminated union type does not cover all the expected cases.
|
|
854
|
+
* Expected usage: `
|
|
855
|
+
* switch ( x.type ) {
|
|
856
|
+
* case "x": return doX();
|
|
857
|
+
* case "y": return doY();
|
|
858
|
+
* ...
|
|
859
|
+
* default: throw new NonExhaustiveSwitchError( x satisfies never );
|
|
860
|
+
* }`
|
|
861
|
+
*/
|
|
862
|
+
class NonExhaustiveSwitchError extends Error {
|
|
863
|
+
constructor(value) {
|
|
864
|
+
super("Expected switch to be exhaustive, got: " + value);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function tryToParseJson(jsonContent) {
|
|
869
|
+
return withTryCatch(() => parseJson(jsonContent));
|
|
870
|
+
}
|
|
871
|
+
function parseJson(jsonContent) {
|
|
872
|
+
return JSON.parse(jsonContent);
|
|
873
|
+
}
|
|
874
|
+
function stringifyJson(jsonSerializable, minify = false) {
|
|
875
|
+
return JSON.stringify(jsonSerializable, undefined, minify ? undefined : 2);
|
|
876
|
+
}
|
|
877
|
+
function jsonCloneDeep(a) {
|
|
878
|
+
// Handle the 3 simple types, and null or undefined
|
|
879
|
+
if (null === a || "object" !== typeof a)
|
|
880
|
+
return a;
|
|
881
|
+
if (a instanceof Date) {
|
|
882
|
+
// Handle Date
|
|
883
|
+
return new Date(a.getTime());
|
|
884
|
+
}
|
|
885
|
+
else if (a instanceof Array) {
|
|
886
|
+
// Handle Array
|
|
887
|
+
const copy = [];
|
|
888
|
+
for (let i = 0, len = a.length; i < len; i++) {
|
|
889
|
+
copy[i] = jsonCloneDeep(a[i]);
|
|
890
|
+
}
|
|
891
|
+
return copy;
|
|
892
|
+
}
|
|
893
|
+
else if (a instanceof Object) {
|
|
894
|
+
// Handle Object
|
|
895
|
+
const copy = {};
|
|
896
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
897
|
+
for (let attr in a) {
|
|
898
|
+
if (a.hasOwnProperty(attr))
|
|
899
|
+
copy[attr] = jsonCloneDeep(a[attr]);
|
|
900
|
+
}
|
|
901
|
+
return copy;
|
|
902
|
+
}
|
|
903
|
+
throw new Error("Unable to copy obj! Its type isn't supported.");
|
|
904
|
+
}
|
|
905
|
+
function omitFromJsonObject(o, ...keys) {
|
|
906
|
+
return keys.reduce((obj, key) => {
|
|
907
|
+
delete obj[key];
|
|
908
|
+
return obj;
|
|
909
|
+
}, { ...o });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const roundModes = {
|
|
913
|
+
'toNearest': Math.round,
|
|
914
|
+
'toLower': Math.floor,
|
|
915
|
+
'toUpper': Math.ceil,
|
|
916
|
+
'towardsZero': n => n > 0 ? Math.floor(n) : Math.ceil(n),
|
|
917
|
+
'awayFromZero': n => n > 0 ? Math.ceil(n) : Math.floor(n),
|
|
918
|
+
};
|
|
919
|
+
function round(n, precision = 0, mode = 'toNearest') {
|
|
920
|
+
const base = 10;
|
|
921
|
+
const power = Math.pow(base, precision);
|
|
922
|
+
return roundModes[mode](n * power) / power;
|
|
923
|
+
}
|
|
924
|
+
const roundToNearest = (n, precision = 0) => round(n, precision, 'toNearest');
|
|
925
|
+
const roundToLower = (n, precision = 0) => round(n, precision, 'toLower');
|
|
926
|
+
const roundToUpper = (n, precision = 0) => round(n, precision, 'toUpper');
|
|
927
|
+
const roundTowardsZero = (n, precision = 0) => round(n, precision, 'towardsZero');
|
|
928
|
+
const roundAwayFromZero = (n, precision = 0) => round(n, precision, 'awayFromZero');
|
|
929
|
+
|
|
930
|
+
function ensurePositiveNumber(v, name = 'value') {
|
|
931
|
+
if (v !== undefined && v !== null && v > 0)
|
|
932
|
+
return v;
|
|
933
|
+
throw new Error(`Expected ${name} to be positive, got: ${v}`);
|
|
934
|
+
}
|
|
935
|
+
function ensureNonNegativeNumber(v, name = 'value') {
|
|
936
|
+
if (v !== undefined && v !== null && v >= 0)
|
|
937
|
+
return v;
|
|
938
|
+
throw new Error(`Expected ${name} to be non-negative, got: ${v}`);
|
|
939
|
+
}
|
|
940
|
+
function ensureNonPositiveNumber(v, name = 'value') {
|
|
941
|
+
if (v !== undefined && v !== null && v <= 0)
|
|
942
|
+
return v;
|
|
943
|
+
throw new Error(`Expected ${name} to be non-positive, got: ${v}`);
|
|
944
|
+
}
|
|
945
|
+
function ensureNegativeNumber(v, name = 'value') {
|
|
946
|
+
if (v !== undefined && v !== null && v < 0)
|
|
947
|
+
return v;
|
|
948
|
+
throw new Error(`Expected ${name} to be negative, got: ${v}`);
|
|
949
|
+
}
|
|
950
|
+
function incrementBy(n) {
|
|
951
|
+
return (v) => v + n;
|
|
952
|
+
}
|
|
953
|
+
function multiplyBy(n) {
|
|
954
|
+
return (v) => v * n;
|
|
955
|
+
}
|
|
956
|
+
function divideBy(n) {
|
|
957
|
+
return (v) => v / n;
|
|
958
|
+
}
|
|
959
|
+
const increment = incrementBy(1);
|
|
960
|
+
const decrement = incrementBy(-1);
|
|
961
|
+
const decrementBy = (n) => incrementBy(-n);
|
|
962
|
+
function isNumber(v) {
|
|
963
|
+
return typeof v === "number";
|
|
964
|
+
}
|
|
965
|
+
function isPositiveNumber(v) {
|
|
966
|
+
return v > 0;
|
|
967
|
+
}
|
|
968
|
+
function isNegativeNumber(v) {
|
|
969
|
+
return v < 0;
|
|
970
|
+
}
|
|
971
|
+
function isZero(v) {
|
|
972
|
+
return v === 0;
|
|
973
|
+
}
|
|
974
|
+
function clamp(n, min, max) {
|
|
975
|
+
if (min === null || max === null || n === null || isNaN(min) || isNaN(max) || isNaN(n) || min > max)
|
|
976
|
+
throw new Error();
|
|
977
|
+
if (n > max) {
|
|
978
|
+
return max;
|
|
979
|
+
}
|
|
980
|
+
else if (n < min) {
|
|
981
|
+
return min;
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
return n;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function clampInt0_100(n) {
|
|
988
|
+
return clamp(Math.round(n), 0, 100);
|
|
989
|
+
}
|
|
990
|
+
function tryToParseNumber(numberStr) {
|
|
991
|
+
return withTryCatch(() => {
|
|
992
|
+
const type = typeof ensureDefined(numberStr);
|
|
993
|
+
if (type !== "string")
|
|
994
|
+
throw new Error('Invalid number given: ' + numberStr);
|
|
995
|
+
if (numberStr.trim().length === 0)
|
|
996
|
+
throw new Error('Invalid number given: ' + numberStr);
|
|
997
|
+
const num = Number(numberStr);
|
|
998
|
+
if (isNaN(num))
|
|
999
|
+
throw new Error('Invalid number given: ' + numberStr);
|
|
1000
|
+
return num;
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const Operation = {
|
|
1005
|
+
ok: (data) => {
|
|
1006
|
+
return { success: true, data };
|
|
1007
|
+
},
|
|
1008
|
+
ko: (error) => {
|
|
1009
|
+
return { success: false, error };
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
function asPromise(promisable) {
|
|
1014
|
+
return Promise.resolve(promisable);
|
|
1015
|
+
}
|
|
1016
|
+
function promiseSequence(...fns) {
|
|
1017
|
+
const fn = async function () {
|
|
1018
|
+
const results = [];
|
|
1019
|
+
for (let i = 0; i < fns.length; i++) {
|
|
1020
|
+
results[i] = await fns[i]();
|
|
1021
|
+
}
|
|
1022
|
+
return results;
|
|
1023
|
+
};
|
|
1024
|
+
return fn;
|
|
1025
|
+
}
|
|
1026
|
+
function delayPromise(duration) {
|
|
1027
|
+
return (result) => duration.promise().then(() => result);
|
|
1028
|
+
}
|
|
1029
|
+
class TimeoutError extends Error {
|
|
1030
|
+
}
|
|
1031
|
+
async function awaitAtMost(promise, duration) {
|
|
1032
|
+
return Promise.race([
|
|
1033
|
+
promise.then(value => ({ status: 'ok', value })),
|
|
1034
|
+
duration.promise().then(() => ({ status: 'timeout' }))
|
|
1035
|
+
]).then(({ status, value }) => {
|
|
1036
|
+
if (status === 'ok')
|
|
1037
|
+
return value;
|
|
1038
|
+
throw new TimeoutError("Time out reached while waiting for a promise to resolve.");
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
const NEVER = new Promise(_resolve => { });
|
|
1042
|
+
|
|
1043
|
+
function randomNumberInInterval(min, max) {
|
|
1044
|
+
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
1045
|
+
}
|
|
1046
|
+
const randomId = (length) => {
|
|
1047
|
+
return Math.random().toString(36).substring(2, length + 2);
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
function dictToEntries(obj) {
|
|
1051
|
+
return Object.entries(obj);
|
|
1052
|
+
}
|
|
1053
|
+
function entriesToDict(entries) {
|
|
1054
|
+
return Object.fromEntries(entries);
|
|
1055
|
+
}
|
|
1056
|
+
function entriesToEntries(dict, mapper) {
|
|
1057
|
+
return entriesToDict(dictToEntries(dict).map((entry) => mapper(entry)));
|
|
1058
|
+
}
|
|
1059
|
+
/** @deprecated[2025.08.01]: Compatibility layer. */
|
|
1060
|
+
const mapEntries = entriesToEntries;
|
|
1061
|
+
function entriesToList(dict, mapper) {
|
|
1062
|
+
return dictToEntries(dict).map((entry) => mapper(entry));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function dictToList(obj) {
|
|
1066
|
+
return Object.keys(obj).map(key => obj[key]);
|
|
1067
|
+
}
|
|
1068
|
+
function pick(o, keys) {
|
|
1069
|
+
return keys.reduce((obj, key) => {
|
|
1070
|
+
obj[key] = o[key];
|
|
1071
|
+
return obj;
|
|
1072
|
+
}, {});
|
|
1073
|
+
}
|
|
1074
|
+
function shallowRecordEquals(a, b) {
|
|
1075
|
+
if (a === b)
|
|
1076
|
+
return true;
|
|
1077
|
+
const aKeys = Object.keys(a), bKeys = Object.keys(b);
|
|
1078
|
+
if (aKeys.length !== bKeys.length)
|
|
1079
|
+
return false;
|
|
1080
|
+
for (const key of aKeys) {
|
|
1081
|
+
if (a[key] !== b[key])
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
class StringParts {
|
|
1088
|
+
_parts;
|
|
1089
|
+
constructor(...parts) {
|
|
1090
|
+
this._parts = parts;
|
|
1091
|
+
}
|
|
1092
|
+
toUpperCase() {
|
|
1093
|
+
return new StringParts(...this.parts.map(part => part.toUpperCase()));
|
|
1094
|
+
}
|
|
1095
|
+
toLowerCase() {
|
|
1096
|
+
return new StringParts(...this.parts.map(part => part.toLowerCase()));
|
|
1097
|
+
}
|
|
1098
|
+
capitalizeFirst() {
|
|
1099
|
+
if (!this.length)
|
|
1100
|
+
return this;
|
|
1101
|
+
return new StringParts(capitalizeWord(this.first), ...this.tail.map(part => part.toLowerCase()));
|
|
1102
|
+
}
|
|
1103
|
+
capitalizeEach() {
|
|
1104
|
+
return new StringParts(...this.parts.map(part => capitalizeWord(part)));
|
|
1105
|
+
}
|
|
1106
|
+
get parts() {
|
|
1107
|
+
return this._parts;
|
|
1108
|
+
}
|
|
1109
|
+
get tail() {
|
|
1110
|
+
const [_first, ...tail] = this.parts;
|
|
1111
|
+
return tail;
|
|
1112
|
+
}
|
|
1113
|
+
get first() {
|
|
1114
|
+
return this._parts[0] ?? null;
|
|
1115
|
+
}
|
|
1116
|
+
get last() {
|
|
1117
|
+
return this._parts[this.length - 1] ?? null;
|
|
1118
|
+
}
|
|
1119
|
+
get length() {
|
|
1120
|
+
return this._parts.length;
|
|
1121
|
+
}
|
|
1122
|
+
trim() {
|
|
1123
|
+
return new StringParts(...this.parts.map(part => part.trim()));
|
|
1124
|
+
}
|
|
1125
|
+
toSnakeCase() {
|
|
1126
|
+
return this.toLowerCase().join('_');
|
|
1127
|
+
}
|
|
1128
|
+
toCamelCase() {
|
|
1129
|
+
if (!this.length)
|
|
1130
|
+
return '';
|
|
1131
|
+
return [this.first.toLowerCase(), ...this.tail.map(capitalizeWord)].join('');
|
|
1132
|
+
}
|
|
1133
|
+
toKebabCase() {
|
|
1134
|
+
return this.toLowerCase().join('-');
|
|
1135
|
+
}
|
|
1136
|
+
toPascalCase() {
|
|
1137
|
+
return this.capitalizeEach().join('');
|
|
1138
|
+
}
|
|
1139
|
+
toHumanCase() {
|
|
1140
|
+
return this.capitalizeFirst().join(' ');
|
|
1141
|
+
}
|
|
1142
|
+
join(separator) {
|
|
1143
|
+
return this.parts.join(separator);
|
|
1144
|
+
}
|
|
1145
|
+
mergeWith({ parts: otherParts }) {
|
|
1146
|
+
return new StringParts(...this.parts, ...otherParts);
|
|
1147
|
+
}
|
|
1148
|
+
slice(start, end) {
|
|
1149
|
+
return new StringParts(...this.parts.slice(start, end));
|
|
1150
|
+
}
|
|
1151
|
+
splice(start, deleteCount, ...items) {
|
|
1152
|
+
return new StringParts(...this.parts.splice(start, deleteCount, ...items));
|
|
1153
|
+
}
|
|
1154
|
+
push(part) {
|
|
1155
|
+
return new StringParts(...this.parts, part);
|
|
1156
|
+
}
|
|
1157
|
+
shift(part) {
|
|
1158
|
+
return new StringParts(part, ...this.parts);
|
|
1159
|
+
}
|
|
1160
|
+
reverse() {
|
|
1161
|
+
return new StringParts(...this.parts.reverse());
|
|
1162
|
+
}
|
|
1163
|
+
static fromString = (s, separator = /\s+/g) => {
|
|
1164
|
+
if (s === null || separator === null)
|
|
1165
|
+
throw new Error('Invalid arguments');
|
|
1166
|
+
return new StringParts(...s.split(separator));
|
|
1167
|
+
};
|
|
1168
|
+
static fromParts = (...parts) => {
|
|
1169
|
+
return new StringParts(...parts);
|
|
1170
|
+
};
|
|
1171
|
+
/**
|
|
1172
|
+
* Tries to convert a word in PascalCase to an array of words. Will not work if there are spaces or special characters. Does not cope well on uppercase abbreviations (eg, "CSS").
|
|
1173
|
+
* @param s a string in PascalCase.
|
|
1174
|
+
* @returns a StringParts where each sub-word in PascalCase is now a part.
|
|
1175
|
+
*/
|
|
1176
|
+
static tryToParsePascalCaseWord(s) {
|
|
1177
|
+
return new StringParts(...s.split(/([A-Z][a-z]*)/).filter(Boolean).map(n => n.toLowerCase()));
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function hashCode(str) {
|
|
1182
|
+
let hash = 0;
|
|
1183
|
+
for (let i = 0; i < str.length; ++i)
|
|
1184
|
+
hash = Math.imul(31, hash) + str.charCodeAt(i);
|
|
1185
|
+
return hash | 0;
|
|
1186
|
+
}
|
|
1187
|
+
function repeat(char, times) {
|
|
1188
|
+
return times > 0 ? range(0, times - 1).map(_ => char).join('') : '';
|
|
1189
|
+
}
|
|
1190
|
+
function capitalizeWord(word) {
|
|
1191
|
+
return word[0].toUpperCase() + word.substring(1).toLowerCase();
|
|
1192
|
+
}
|
|
1193
|
+
function splitWords(text, regEx = /\s+/) {
|
|
1194
|
+
return text.split(regEx).filter(t => t.length > 0);
|
|
1195
|
+
}
|
|
1196
|
+
function stringToNumber(s) {
|
|
1197
|
+
if (isNullOrUndefined(s))
|
|
1198
|
+
return null;
|
|
1199
|
+
return Number(s);
|
|
1200
|
+
}
|
|
1201
|
+
function pad(str, n, char, where = 'left') {
|
|
1202
|
+
const length = ensureDefined(str).length;
|
|
1203
|
+
if (length >= ensureDefined(n))
|
|
1204
|
+
return str;
|
|
1205
|
+
if (ensureDefined(char).length !== 1)
|
|
1206
|
+
throw new Error('Illegal pad character');
|
|
1207
|
+
const padding = repeat(char, n - length);
|
|
1208
|
+
return (where === 'left' ? padding : '') + str + (where === 'right' ? padding : '');
|
|
1209
|
+
}
|
|
1210
|
+
function padLeft(str, n, char) {
|
|
1211
|
+
return pad(str, n, char, 'left');
|
|
1212
|
+
}
|
|
1213
|
+
function padRight(str, n, char) {
|
|
1214
|
+
return pad(str, n, char, 'right');
|
|
1215
|
+
}
|
|
1216
|
+
function ellipsis(str, maxLength) {
|
|
1217
|
+
if (maxLength < 4)
|
|
1218
|
+
throw new Error('Invalid argument maxLength');
|
|
1219
|
+
if (str.length <= maxLength) {
|
|
1220
|
+
return str;
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
return str.substring(0, maxLength - 3) + '...';
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function pluralize(n, singular, plural) {
|
|
1227
|
+
if (!singular || !singular.length)
|
|
1228
|
+
throw new Error();
|
|
1229
|
+
if (n === 1)
|
|
1230
|
+
return singular;
|
|
1231
|
+
plural = plural ?? singular + 's';
|
|
1232
|
+
const firstUppercase = singular.charAt(0) === singular.charAt(0).toUpperCase();
|
|
1233
|
+
if (firstUppercase) {
|
|
1234
|
+
const PLURAL = plural.toUpperCase();
|
|
1235
|
+
const isAllUppercase = plural === PLURAL;
|
|
1236
|
+
plural = isAllUppercase ? PLURAL : plural.charAt(0).toUpperCase() + plural.slice(1).toLowerCase();
|
|
1237
|
+
}
|
|
1238
|
+
return plural;
|
|
1239
|
+
}
|
|
1240
|
+
function isString(source) {
|
|
1241
|
+
return isDefined(source) && typeof source === "string";
|
|
1242
|
+
}
|
|
1243
|
+
function isEmpty(source) {
|
|
1244
|
+
return source.trim().length === 0;
|
|
1245
|
+
}
|
|
1246
|
+
function isNullOrUndefinedOrEmpty(source) {
|
|
1247
|
+
return isNullOrUndefined(source) || isEmpty(source);
|
|
1248
|
+
}
|
|
1249
|
+
function wrapWithString(str, start, end = start) {
|
|
1250
|
+
if (isNullOrUndefinedOrEmpty(str))
|
|
1251
|
+
throw new Error(`Expected string to be non-empty, got: "${str}"`);
|
|
1252
|
+
if (isNullOrUndefinedOrEmpty(start))
|
|
1253
|
+
throw new Error(`Expected start delimiter to be non-empty, got: "${start}"`);
|
|
1254
|
+
if (isNullOrUndefinedOrEmpty(end))
|
|
1255
|
+
throw new Error(`Expected end delimiter to be non-empty, got: "${end}"`);
|
|
1256
|
+
return start + str + end;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Class that stores a value that is computationally/memory intensive to initialize.
|
|
1261
|
+
* The initialization of the value is done once and only if required (lazy initialization).
|
|
1262
|
+
* @author gtomberli
|
|
1263
|
+
*/
|
|
1264
|
+
class Lazy {
|
|
1265
|
+
_value = undefined;
|
|
1266
|
+
_initialized = false;
|
|
1267
|
+
_generator;
|
|
1268
|
+
constructor(generator) {
|
|
1269
|
+
this._generator = ensureDefined(generator);
|
|
1270
|
+
}
|
|
1271
|
+
get value() {
|
|
1272
|
+
return this._value;
|
|
1273
|
+
}
|
|
1274
|
+
getOrCreate() {
|
|
1275
|
+
if (!this._initialized) {
|
|
1276
|
+
this._value = this._generator();
|
|
1277
|
+
this._initialized = true;
|
|
1278
|
+
}
|
|
1279
|
+
return this._value;
|
|
1280
|
+
}
|
|
1281
|
+
getOrThrow(errorMessage) {
|
|
1282
|
+
if (!this._initialized)
|
|
1283
|
+
throw new Error(errorMessage);
|
|
1284
|
+
return this._value;
|
|
1285
|
+
}
|
|
1286
|
+
or(t) {
|
|
1287
|
+
if (!this._initialized) {
|
|
1288
|
+
return t;
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
return this._value;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
isPresent() {
|
|
1295
|
+
return this._initialized;
|
|
1296
|
+
}
|
|
1297
|
+
isEmpty() {
|
|
1298
|
+
return !this._initialized;
|
|
1299
|
+
}
|
|
1300
|
+
ifPresent(fn) {
|
|
1301
|
+
if (this.isPresent()) {
|
|
1302
|
+
fn(this._value);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
ifEmpty(fn) {
|
|
1306
|
+
if (this.isEmpty()) {
|
|
1307
|
+
fn();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
apply(fnPresent, fnEmpty) {
|
|
1311
|
+
if (this.isPresent()) {
|
|
1312
|
+
fnPresent(this._value);
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
fnEmpty();
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
empty() {
|
|
1319
|
+
this._initialized = false;
|
|
1320
|
+
}
|
|
1321
|
+
static of(generator) {
|
|
1322
|
+
return new Lazy(generator);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
class LazyAsync {
|
|
1327
|
+
_promise = undefined;
|
|
1328
|
+
_pending = false;
|
|
1329
|
+
_resolvedValue = undefined;
|
|
1330
|
+
_error = undefined;
|
|
1331
|
+
_generator;
|
|
1332
|
+
constructor(generator) {
|
|
1333
|
+
this._generator = ensureDefined(generator);
|
|
1334
|
+
}
|
|
1335
|
+
getOrCreate() {
|
|
1336
|
+
if (this.isEmpty()) {
|
|
1337
|
+
this._pending = true;
|
|
1338
|
+
this._promise = this._generator();
|
|
1339
|
+
this._promise.then(value => {
|
|
1340
|
+
this._pending = false;
|
|
1341
|
+
this._resolvedValue = value;
|
|
1342
|
+
}, err => {
|
|
1343
|
+
this._pending = false;
|
|
1344
|
+
this._error = err;
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
return this._promise;
|
|
1348
|
+
}
|
|
1349
|
+
isPresent() {
|
|
1350
|
+
return this._promise !== undefined;
|
|
1351
|
+
}
|
|
1352
|
+
isEmpty() {
|
|
1353
|
+
return this._promise === undefined;
|
|
1354
|
+
}
|
|
1355
|
+
isPending() {
|
|
1356
|
+
return this.isPresent() && this._pending === true;
|
|
1357
|
+
}
|
|
1358
|
+
isReady() {
|
|
1359
|
+
return this.isPresent() && this._pending === false;
|
|
1360
|
+
}
|
|
1361
|
+
isResolved() {
|
|
1362
|
+
return this.isReady() && this._error === undefined;
|
|
1363
|
+
}
|
|
1364
|
+
isError() {
|
|
1365
|
+
return this.isReady() && this._error !== undefined;
|
|
1366
|
+
}
|
|
1367
|
+
ifPresent(fn) {
|
|
1368
|
+
if (this.isPresent()) {
|
|
1369
|
+
fn(this._promise);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
ifEmpty(fn) {
|
|
1373
|
+
if (this.isEmpty()) {
|
|
1374
|
+
fn();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
ifPending(fn) {
|
|
1378
|
+
if (this.isPending()) {
|
|
1379
|
+
fn(this._promise);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
ifReady(fn) {
|
|
1383
|
+
if (this.isReady()) {
|
|
1384
|
+
fn(this._promise);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
ifResolved(fn) {
|
|
1388
|
+
if (this.isResolved()) {
|
|
1389
|
+
fn(this._resolvedValue);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
ifError(fn) {
|
|
1393
|
+
if (this.isError()) {
|
|
1394
|
+
fn(this._error);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
getOrThrow(errorMessage) {
|
|
1398
|
+
if (!this._promise)
|
|
1399
|
+
throw new Error(errorMessage);
|
|
1400
|
+
return this._promise;
|
|
1401
|
+
}
|
|
1402
|
+
empty() {
|
|
1403
|
+
this._promise = undefined;
|
|
1404
|
+
}
|
|
1405
|
+
static of(generator) {
|
|
1406
|
+
return new LazyAsync(generator);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
class LazyDictionary {
|
|
1411
|
+
_generator;
|
|
1412
|
+
_dictionary = {};
|
|
1413
|
+
constructor(_generator) {
|
|
1414
|
+
this._generator = _generator;
|
|
1415
|
+
}
|
|
1416
|
+
static of(generator) {
|
|
1417
|
+
return new LazyDictionary(generator);
|
|
1418
|
+
}
|
|
1419
|
+
getOrCreate(key) {
|
|
1420
|
+
if (key in this._dictionary) {
|
|
1421
|
+
return this._dictionary[key];
|
|
1422
|
+
}
|
|
1423
|
+
const value = this._generator(key);
|
|
1424
|
+
this._dictionary[key] = value;
|
|
1425
|
+
return value;
|
|
1426
|
+
}
|
|
1427
|
+
getOrThrow(key, errorMessage) {
|
|
1428
|
+
if (key in this._dictionary) {
|
|
1429
|
+
return this._dictionary[key];
|
|
1430
|
+
}
|
|
1431
|
+
throw new Error(errorMessage);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
class TimeUnit {
|
|
1436
|
+
multiplier;
|
|
1437
|
+
constructor(multiplier) {
|
|
1438
|
+
this.multiplier = multiplier;
|
|
1439
|
+
}
|
|
1440
|
+
toUnit(value, unit) {
|
|
1441
|
+
return value * (this.multiplier / unit.multiplier);
|
|
1442
|
+
}
|
|
1443
|
+
toMs(value) {
|
|
1444
|
+
return this.toUnit(value, TimeUnit.MILLISECONDS);
|
|
1445
|
+
}
|
|
1446
|
+
toSeconds(value) {
|
|
1447
|
+
return this.toUnit(value, TimeUnit.SECONDS);
|
|
1448
|
+
}
|
|
1449
|
+
toMinutes(value) {
|
|
1450
|
+
return this.toUnit(value, TimeUnit.MINUTES);
|
|
1451
|
+
}
|
|
1452
|
+
toHours(value) {
|
|
1453
|
+
return this.toUnit(value, TimeUnit.HOURS);
|
|
1454
|
+
}
|
|
1455
|
+
toDays(value) {
|
|
1456
|
+
return this.toUnit(value, TimeUnit.DAYS);
|
|
1457
|
+
}
|
|
1458
|
+
static MILLISECONDS = new TimeUnit(1);
|
|
1459
|
+
static SECONDS = new TimeUnit(1000 * TimeUnit.MILLISECONDS.multiplier);
|
|
1460
|
+
static MINUTES = new TimeUnit(60 * TimeUnit.SECONDS.multiplier);
|
|
1461
|
+
static HOURS = new TimeUnit(60 * TimeUnit.MINUTES.multiplier);
|
|
1462
|
+
static DAYS = new TimeUnit(24 * TimeUnit.HOURS.multiplier);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
class TimeBase {
|
|
1466
|
+
_ms;
|
|
1467
|
+
constructor(value, unit) {
|
|
1468
|
+
this._ms = unit.toMs(value);
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Total number of milliseconds, rounded down.
|
|
1472
|
+
*/
|
|
1473
|
+
get ms() {
|
|
1474
|
+
return Math.floor(this._ms / TimeUnit.MILLISECONDS.multiplier);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Total number of seconds, rounded down.
|
|
1478
|
+
*/
|
|
1479
|
+
get seconds() {
|
|
1480
|
+
return Math.floor(this._ms / TimeUnit.SECONDS.multiplier);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Total number of minutes, rounded down.
|
|
1484
|
+
*/
|
|
1485
|
+
get minutes() {
|
|
1486
|
+
return Math.floor(this._ms / TimeUnit.MINUTES.multiplier);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Total number of hours, rounded down.
|
|
1490
|
+
*/
|
|
1491
|
+
get hours() {
|
|
1492
|
+
return Math.floor(this._ms / TimeUnit.HOURS.multiplier);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Total number of days, rounded down.
|
|
1496
|
+
*/
|
|
1497
|
+
get days() {
|
|
1498
|
+
return Math.floor(this._ms / TimeUnit.DAYS.multiplier);
|
|
1499
|
+
}
|
|
1500
|
+
addMs(milliseconds) {
|
|
1501
|
+
return this.addUnits(milliseconds, TimeUnit.MILLISECONDS);
|
|
1502
|
+
}
|
|
1503
|
+
addSeconds(seconds) {
|
|
1504
|
+
return this.addUnits(seconds, TimeUnit.SECONDS);
|
|
1505
|
+
}
|
|
1506
|
+
addMinutes(minutes) {
|
|
1507
|
+
return this.addUnits(minutes, TimeUnit.MINUTES);
|
|
1508
|
+
}
|
|
1509
|
+
addHours(hours) {
|
|
1510
|
+
return this.addUnits(hours, TimeUnit.HOURS);
|
|
1511
|
+
}
|
|
1512
|
+
addDays(days) {
|
|
1513
|
+
return this.addUnits(days, TimeUnit.DAYS);
|
|
1514
|
+
}
|
|
1515
|
+
removeDays(days) {
|
|
1516
|
+
return this.removeUnits(days, TimeUnit.DAYS);
|
|
1517
|
+
}
|
|
1518
|
+
addUnits(n, unit) {
|
|
1519
|
+
return this.create(this._ms + unit.toMs(n), TimeUnit.MILLISECONDS);
|
|
1520
|
+
}
|
|
1521
|
+
removeMs(milliseconds) {
|
|
1522
|
+
return this.addUnits(-milliseconds, TimeUnit.MILLISECONDS);
|
|
1523
|
+
}
|
|
1524
|
+
removeSeconds(seconds) {
|
|
1525
|
+
return this.addUnits(-seconds, TimeUnit.SECONDS);
|
|
1526
|
+
}
|
|
1527
|
+
removeMinutes(minutes) {
|
|
1528
|
+
return this.addUnits(-minutes, TimeUnit.MINUTES);
|
|
1529
|
+
}
|
|
1530
|
+
removeHours(hours) {
|
|
1531
|
+
return this.addUnits(-hours, TimeUnit.HOURS);
|
|
1532
|
+
}
|
|
1533
|
+
removeUnits(n, unit) {
|
|
1534
|
+
return this.addUnits(-n, unit);
|
|
1535
|
+
}
|
|
1536
|
+
getUnit(unit) {
|
|
1537
|
+
return this._ms / unit.multiplier;
|
|
1538
|
+
}
|
|
1539
|
+
toUnits() {
|
|
1540
|
+
return {
|
|
1541
|
+
milliseconds: this.ms % TimeUnit.SECONDS.multiplier,
|
|
1542
|
+
seconds: Math.floor(this.seconds % 60),
|
|
1543
|
+
minutes: Math.floor(this.minutes % 60),
|
|
1544
|
+
hours: Math.floor(this.hours % 24),
|
|
1545
|
+
days: Math.floor(this.days),
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
static unitsToMs(units) {
|
|
1549
|
+
if (!units)
|
|
1550
|
+
throw new Error('Invalid units given');
|
|
1551
|
+
let ms = 0;
|
|
1552
|
+
ms += (units.milliseconds ?? 0) * TimeUnit.MILLISECONDS.multiplier;
|
|
1553
|
+
ms += (units.seconds ?? 0) * TimeUnit.SECONDS.multiplier;
|
|
1554
|
+
ms += (units.minutes ?? 0) * TimeUnit.MINUTES.multiplier;
|
|
1555
|
+
ms += (units.hours ?? 0) * TimeUnit.HOURS.multiplier;
|
|
1556
|
+
ms += (units.days ?? 0) * TimeUnit.DAYS.multiplier;
|
|
1557
|
+
return ms;
|
|
1558
|
+
}
|
|
1559
|
+
toJSON() {
|
|
1560
|
+
return this._ms;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
class TimeDuration extends TimeBase {
|
|
1565
|
+
constructor(value, unit) {
|
|
1566
|
+
super(value, unit);
|
|
1567
|
+
if (value < 0)
|
|
1568
|
+
throw new Error('Duration cannot be less than 0');
|
|
1569
|
+
}
|
|
1570
|
+
create(value, unit) {
|
|
1571
|
+
return new TimeDuration(value, unit);
|
|
1572
|
+
}
|
|
1573
|
+
addDuration(duration) {
|
|
1574
|
+
return this.addUnits(duration.ms, TimeUnit.MILLISECONDS);
|
|
1575
|
+
}
|
|
1576
|
+
removeDuration(duration) {
|
|
1577
|
+
return this.removeUnits(duration.ms, TimeUnit.MILLISECONDS);
|
|
1578
|
+
}
|
|
1579
|
+
removeUnits(n, unit) {
|
|
1580
|
+
const nn = Math.min(n, this.getUnit(unit)); // Avoid going negative.
|
|
1581
|
+
return super.removeUnits(nn, unit);
|
|
1582
|
+
}
|
|
1583
|
+
multiplyBy(times) {
|
|
1584
|
+
ensureNonNegativeNumber(times, "times");
|
|
1585
|
+
return TimeDuration.ms(this.ms * times);
|
|
1586
|
+
}
|
|
1587
|
+
divideBy(times) {
|
|
1588
|
+
ensurePositiveNumber(times, "times");
|
|
1589
|
+
return TimeDuration.ms(this.ms / times);
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Returns the current duration in a human readable format, prioritizing the most significant unit of time and excluding the rest.
|
|
1593
|
+
* @todo[2023-06] Should allow more customization options
|
|
1594
|
+
* @todo[2023-06] By default should show the secondary unit only when actually needed (eg, 1h 20m & 3h, instead of 1h 20m & 3h 28m)
|
|
1595
|
+
* @todo[2023-06] Should not allow negative durations, as this is a duration and not an instant.
|
|
1596
|
+
* @returns the duration, with only the most significant units displayed as a human readable string, eg: 1d 20h, 10m 30s.
|
|
1597
|
+
*/
|
|
1598
|
+
get formatted() {
|
|
1599
|
+
const format = (x, u1, y, u2) => `${x}${u1} ${pad(y.toString(), 2, '0')}${u2}`;
|
|
1600
|
+
const units = this.toUnits();
|
|
1601
|
+
if (units.days >= 1)
|
|
1602
|
+
return format(units.days, 'd', units.hours, 'h');
|
|
1603
|
+
else if (units.hours >= 1)
|
|
1604
|
+
return format(units.hours, 'h', units.minutes, 'm');
|
|
1605
|
+
else if (units.minutes >= 1)
|
|
1606
|
+
return format(units.minutes, 'm', units.seconds, 's');
|
|
1607
|
+
else if (units.seconds >= 0)
|
|
1608
|
+
return units.seconds.toString() + 's';
|
|
1609
|
+
else if (units.seconds > -60)
|
|
1610
|
+
return 'A few moments ago';
|
|
1611
|
+
else
|
|
1612
|
+
return 'Some time ago';
|
|
1613
|
+
}
|
|
1614
|
+
interval(cb) {
|
|
1615
|
+
return setInterval(cb, this.ms);
|
|
1616
|
+
}
|
|
1617
|
+
timeout(cb) {
|
|
1618
|
+
return setTimeout(cb, this.ms);
|
|
1619
|
+
}
|
|
1620
|
+
cancelablePromise() {
|
|
1621
|
+
const deferred = new Deferred;
|
|
1622
|
+
if (this.ms > 0) {
|
|
1623
|
+
this.timeout(() => deferred.resolve());
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
deferred.resolve();
|
|
1627
|
+
}
|
|
1628
|
+
return deferred.asCancelablePromise();
|
|
1629
|
+
}
|
|
1630
|
+
promise() {
|
|
1631
|
+
if (this.isEmpty())
|
|
1632
|
+
return Promise.resolve();
|
|
1633
|
+
const deferred = new Deferred;
|
|
1634
|
+
this.timeout(() => deferred.resolve());
|
|
1635
|
+
return deferred.asPromise();
|
|
1636
|
+
}
|
|
1637
|
+
delay(cb) {
|
|
1638
|
+
void this.promise().then(() => cb());
|
|
1639
|
+
}
|
|
1640
|
+
cancelableDelay(cb) {
|
|
1641
|
+
const deferred = this.cancelablePromise();
|
|
1642
|
+
void deferred.then(() => {
|
|
1643
|
+
cb();
|
|
1644
|
+
}, err => {
|
|
1645
|
+
if (err instanceof DeferredCanceledError)
|
|
1646
|
+
return; // Do nothing on cancelation
|
|
1647
|
+
throw err;
|
|
1648
|
+
});
|
|
1649
|
+
return { cancel: () => deferred.cancel() };
|
|
1650
|
+
}
|
|
1651
|
+
debounce(fn) {
|
|
1652
|
+
let handle = null;
|
|
1653
|
+
return (t) => {
|
|
1654
|
+
if (handle)
|
|
1655
|
+
clearTimeout(handle);
|
|
1656
|
+
handle = this.timeout(() => {
|
|
1657
|
+
handle = null;
|
|
1658
|
+
fn(t);
|
|
1659
|
+
});
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
toDigitalClock() {
|
|
1663
|
+
const units = this.toUnits();
|
|
1664
|
+
// data.hours += data.days * 24; // roll over days as hours.
|
|
1665
|
+
return pad(units.hours.toString(), 2, '0') + ':' + pad(units.minutes.toString(), 2, '0') + ':' + pad(units.seconds.toString(), 2, '0');
|
|
1666
|
+
}
|
|
1667
|
+
get asSeconds() {
|
|
1668
|
+
if (this.ms < 0)
|
|
1669
|
+
return "0";
|
|
1670
|
+
if (this.ms < 1000)
|
|
1671
|
+
return (this.ms / 1000).toFixed(1);
|
|
1672
|
+
return Math.floor(this.seconds).toString();
|
|
1673
|
+
}
|
|
1674
|
+
fromNow() {
|
|
1675
|
+
return TimeInstant.now().addDuration(this);
|
|
1676
|
+
}
|
|
1677
|
+
differenceFrom(other) {
|
|
1678
|
+
return new TimeDuration(Math.abs(this.ms - other.ms), TimeUnit.MILLISECONDS);
|
|
1679
|
+
}
|
|
1680
|
+
isGreaterThan(other) {
|
|
1681
|
+
return this.ms > other.ms;
|
|
1682
|
+
}
|
|
1683
|
+
isLessThan(other) {
|
|
1684
|
+
return this.ms < other.ms;
|
|
1685
|
+
}
|
|
1686
|
+
compareTo(other) {
|
|
1687
|
+
if (this.ms === other.ms) {
|
|
1688
|
+
return 0;
|
|
1689
|
+
}
|
|
1690
|
+
else if (this.isLessThan(other)) {
|
|
1691
|
+
return -1;
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
return 1;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
atLeast(other) {
|
|
1698
|
+
if (!this.isLessThan(other)) {
|
|
1699
|
+
return this;
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
return other;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
atMost(other) {
|
|
1706
|
+
if (!this.isGreaterThan(other)) {
|
|
1707
|
+
return this;
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
return other;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
isEmpty() {
|
|
1714
|
+
return this.ms <= 0;
|
|
1715
|
+
}
|
|
1716
|
+
isNotEmpty() {
|
|
1717
|
+
return this.ms > 0;
|
|
1718
|
+
}
|
|
1719
|
+
toUnits() {
|
|
1720
|
+
return super.toUnits();
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* This method is used to provide a better DX when inspecting a TimeInstant object in DevTools.
|
|
1724
|
+
*/
|
|
1725
|
+
get _duration() {
|
|
1726
|
+
return this.formatted;
|
|
1727
|
+
}
|
|
1728
|
+
toString() {
|
|
1729
|
+
return `${this.constructor.name}[${this.formatted}]`;
|
|
1730
|
+
}
|
|
1731
|
+
static parseHumanTime(humanTime) {
|
|
1732
|
+
const match = humanTime.trim().match(/^(?:([0-9]+)d|)\s*(?:([0-9]+)h|)\s*(?:([0-9]+)m|)\s*(?:([0-9]+)s|)$/);
|
|
1733
|
+
if (match) {
|
|
1734
|
+
const [_, days, hours, minutes, seconds] = match;
|
|
1735
|
+
return new TimeDuration(TimeBase.unitsToMs({ days: Number(days ?? 0), hours: Number(hours ?? 0), minutes: Number(minutes ?? 0), seconds: Number(seconds ?? 0) }), TimeUnit.MILLISECONDS);
|
|
1736
|
+
}
|
|
1737
|
+
throw new Error('Failed to parse time: ' + humanTime);
|
|
1738
|
+
}
|
|
1739
|
+
static compare(a, b) {
|
|
1740
|
+
return a.compareTo(b);
|
|
1741
|
+
}
|
|
1742
|
+
static ms = (value) => new TimeDuration(value, TimeUnit.MILLISECONDS);
|
|
1743
|
+
static seconds = (value) => new TimeDuration(value, TimeUnit.SECONDS);
|
|
1744
|
+
static minutes = (value) => new TimeDuration(value, TimeUnit.MINUTES);
|
|
1745
|
+
static hours = (value) => new TimeDuration(value, TimeUnit.HOURS);
|
|
1746
|
+
static days = (value) => new TimeDuration(value, TimeUnit.DAYS);
|
|
1747
|
+
static shamefulUnref = (ms) => {
|
|
1748
|
+
if (ms instanceof TimeDuration) {
|
|
1749
|
+
return ms.ms;
|
|
1750
|
+
}
|
|
1751
|
+
return ms;
|
|
1752
|
+
};
|
|
1753
|
+
static shamefulRef = (ms) => {
|
|
1754
|
+
if (ms instanceof TimeDuration) {
|
|
1755
|
+
return ms;
|
|
1756
|
+
}
|
|
1757
|
+
if (ms < 0) {
|
|
1758
|
+
ms = 0;
|
|
1759
|
+
}
|
|
1760
|
+
return TimeDuration.ms(ms);
|
|
1761
|
+
};
|
|
1762
|
+
static unref = (ms) => {
|
|
1763
|
+
return TimeDuration.shamefulUnref(ms);
|
|
1764
|
+
};
|
|
1765
|
+
static ref = (ms) => {
|
|
1766
|
+
return TimeDuration.shamefulRef(ms);
|
|
1767
|
+
};
|
|
1768
|
+
static fromMs(ms) {
|
|
1769
|
+
return new TimeDuration(ms, TimeUnit.MILLISECONDS);
|
|
1770
|
+
}
|
|
1771
|
+
static distanceFromNow(instant) {
|
|
1772
|
+
return TimeDuration.distance(TimeInstant.now(), instant);
|
|
1773
|
+
}
|
|
1774
|
+
static distance(instant1, instant2) {
|
|
1775
|
+
return instant1.distanceFrom(instant2);
|
|
1776
|
+
}
|
|
1777
|
+
static greatest(duration1, duration2) {
|
|
1778
|
+
return duration1.isGreaterThan(duration2) ? duration1 : duration2;
|
|
1779
|
+
}
|
|
1780
|
+
static lowest(duration1, duration2) {
|
|
1781
|
+
return duration1.isLessThan(duration2) ? duration1 : duration2;
|
|
1782
|
+
}
|
|
1783
|
+
static ZERO = new TimeDuration(0, TimeUnit.MILLISECONDS);
|
|
1784
|
+
static fromJSON(ms) {
|
|
1785
|
+
return TimeDuration.ms(ms);
|
|
1786
|
+
}
|
|
1787
|
+
static fromUnits(parameters) {
|
|
1788
|
+
return TimeDuration.ms(TimeBase.unitsToMs(parameters));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
function isAllowedTimeDuration(t) {
|
|
1792
|
+
return (typeof t === "number" && t > 0) || t instanceof TimeDuration;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const monthNames = {
|
|
1796
|
+
january: 1,
|
|
1797
|
+
february: 2,
|
|
1798
|
+
march: 3,
|
|
1799
|
+
april: 4,
|
|
1800
|
+
may: 5,
|
|
1801
|
+
june: 6,
|
|
1802
|
+
july: 7,
|
|
1803
|
+
august: 8,
|
|
1804
|
+
september: 9,
|
|
1805
|
+
october: 10,
|
|
1806
|
+
november: 11,
|
|
1807
|
+
december: 12,
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
const isRelativeNumber = (x) => x !== undefined && x.length > 0 && (x[0] === '+' || x[0] === '-');
|
|
1811
|
+
const defaultTimeInstantCreationParameters = {
|
|
1812
|
+
year: { relative: 0, relativeTo: 'now' },
|
|
1813
|
+
month: { relative: 0, relativeTo: 'now' },
|
|
1814
|
+
date: { relative: 0, relativeTo: 'now' },
|
|
1815
|
+
hours: { relative: 0, relativeTo: 'now' },
|
|
1816
|
+
minutes: { relative: 0, relativeTo: 'now' },
|
|
1817
|
+
seconds: { relative: 0, relativeTo: 'now' },
|
|
1818
|
+
milliseconds: { relative: 0, relativeTo: 'now' },
|
|
1819
|
+
};
|
|
1820
|
+
const timeInstantCreationRelativeAliases = {
|
|
1821
|
+
current: 0,
|
|
1822
|
+
last: -1,
|
|
1823
|
+
next: 1,
|
|
1824
|
+
};
|
|
1825
|
+
const timeInstantResolveValue = (getFromDate, referenceDate, x) => {
|
|
1826
|
+
if (x === undefined) {
|
|
1827
|
+
return getFromDate(referenceDate);
|
|
1828
|
+
}
|
|
1829
|
+
else if (typeof x === 'number') {
|
|
1830
|
+
return x;
|
|
1831
|
+
}
|
|
1832
|
+
else if (typeof x === 'string') {
|
|
1833
|
+
if (isRelativeNumber(x)) {
|
|
1834
|
+
return getFromDate(referenceDate) + parseInt(x);
|
|
1835
|
+
}
|
|
1836
|
+
else if (x in timeInstantCreationRelativeAliases) {
|
|
1837
|
+
return getFromDate(referenceDate) + timeInstantCreationRelativeAliases[x];
|
|
1838
|
+
}
|
|
1839
|
+
else if (x in monthNames) {
|
|
1840
|
+
return monthNames[x];
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
throw new Error('Uparseable string detected: ' + x);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
else if ("relative" in x) {
|
|
1847
|
+
const { relative, relativeTo } = x;
|
|
1848
|
+
if (relativeTo === undefined || relativeTo === 'now') {
|
|
1849
|
+
return getFromDate(referenceDate) + relative;
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
return getFromDate(relativeTo.toDate()) + relative;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
else if ("absolute" in x) {
|
|
1856
|
+
return x.absolute;
|
|
1857
|
+
}
|
|
1858
|
+
else {
|
|
1859
|
+
throw new Error('Uparseable value detected: ' + x);
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
const getFromDate = {
|
|
1863
|
+
year: d => d.getFullYear(),
|
|
1864
|
+
month: d => d.getMonth() + 1,
|
|
1865
|
+
date: d => d.getDate(),
|
|
1866
|
+
hours: d => d.getHours(),
|
|
1867
|
+
minutes: d => d.getMinutes(),
|
|
1868
|
+
seconds: d => d.getSeconds(),
|
|
1869
|
+
milliseconds: d => d.getMilliseconds(),
|
|
1870
|
+
};
|
|
1871
|
+
const toReferenceDate = (x) => {
|
|
1872
|
+
ensureDefined(x);
|
|
1873
|
+
if (isTimeInstant(x))
|
|
1874
|
+
return x.toDate();
|
|
1875
|
+
return x;
|
|
1876
|
+
};
|
|
1877
|
+
function createTimeInstantFromParameters(aParameters, aReferenceDate = TimeInstant.now()) {
|
|
1878
|
+
const { year, month, date, hours, minutes, seconds, milliseconds } = { ...defaultTimeInstantCreationParameters, ...aParameters };
|
|
1879
|
+
const referenceDate = toReferenceDate(aReferenceDate);
|
|
1880
|
+
const timestamp = Date.UTC(timeInstantResolveValue(getFromDate.year, referenceDate, year), timeInstantResolveValue(getFromDate.month, referenceDate, month) - 1, // Convert from 1-based to 0-based for Date.UTC()
|
|
1881
|
+
timeInstantResolveValue(getFromDate.date, referenceDate, date), timeInstantResolveValue(getFromDate.hours, referenceDate, hours), timeInstantResolveValue(getFromDate.minutes, referenceDate, minutes), timeInstantResolveValue(getFromDate.seconds, referenceDate, seconds), timeInstantResolveValue(getFromDate.milliseconds, referenceDate, milliseconds));
|
|
1882
|
+
if (isNaN(timestamp))
|
|
1883
|
+
throw new Error(`Unparseable date: ${JSON.stringify(aParameters)} }`);
|
|
1884
|
+
return TimeInstant.fromUnixTimestamp(timestamp);
|
|
1885
|
+
}
|
|
1886
|
+
function timeInstantBuilder() {
|
|
1887
|
+
let referenceDate = TimeInstant.now().toDate();
|
|
1888
|
+
const value = { ...defaultTimeInstantCreationParameters };
|
|
1889
|
+
const ret = {
|
|
1890
|
+
year: (x) => { value.year = x; return ret; },
|
|
1891
|
+
month: (x) => { value.month = x; return ret; },
|
|
1892
|
+
date: (x) => { value.date = x; return ret; },
|
|
1893
|
+
hours: (x) => { value.hours = x; return ret; },
|
|
1894
|
+
minutes: (x) => { value.minutes = x; return ret; },
|
|
1895
|
+
seconds: (x) => { value.seconds = x; return ret; },
|
|
1896
|
+
milliseconds: (x) => { value.milliseconds = x; return ret; },
|
|
1897
|
+
values: (x) => { Object.keys(value).forEach(key => value[key] = x[key] ?? value[key]); return ret; },
|
|
1898
|
+
relativeTo: (x) => { referenceDate = toReferenceDate(x); return ret; },
|
|
1899
|
+
build: () => createTimeInstantFromParameters(value, referenceDate),
|
|
1900
|
+
};
|
|
1901
|
+
return ret;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
class TimeInstant extends TimeBase {
|
|
1905
|
+
constructor(number, unit) {
|
|
1906
|
+
super(number, unit);
|
|
1907
|
+
}
|
|
1908
|
+
create(value, unit) {
|
|
1909
|
+
return new TimeInstant(value, unit);
|
|
1910
|
+
}
|
|
1911
|
+
addDuration(duration) {
|
|
1912
|
+
return TimeInstant.fromUnixTimestamp(this.ms + duration.ms);
|
|
1913
|
+
}
|
|
1914
|
+
removeDuration(duration) {
|
|
1915
|
+
return TimeInstant.fromUnixTimestamp(this.ms - duration.ms);
|
|
1916
|
+
}
|
|
1917
|
+
distanceFrom(instant) {
|
|
1918
|
+
return TimeDuration.fromMs(Math.abs(this.ms - instant.ms));
|
|
1919
|
+
}
|
|
1920
|
+
distanceFromNow() {
|
|
1921
|
+
return TimeDuration.fromMs(Math.abs(this.ms - Date.now()));
|
|
1922
|
+
}
|
|
1923
|
+
distanceFromUnixTimestamp(timestamp) {
|
|
1924
|
+
return TimeDuration.ms(this.ms - timestamp);
|
|
1925
|
+
}
|
|
1926
|
+
timeLeftFrom(instant) {
|
|
1927
|
+
const distance = this.ms - instant.ms;
|
|
1928
|
+
return TimeDuration.fromMs(Math.max(distance, 0));
|
|
1929
|
+
}
|
|
1930
|
+
timeLeftFromNow() {
|
|
1931
|
+
const distance = this.ms - Date.now();
|
|
1932
|
+
return TimeDuration.fromMs(Math.max(distance, 0));
|
|
1933
|
+
}
|
|
1934
|
+
distanceFromStartOfDay() {
|
|
1935
|
+
// Calculate milliseconds since start of day directly without creating intermediate TimeInstant
|
|
1936
|
+
const params = this.toParameters();
|
|
1937
|
+
const msInDay = params.hours * 3600000 + params.minutes * 60000 + params.seconds * 1000 + params.milliseconds;
|
|
1938
|
+
return TimeDuration.fromMs(msInDay);
|
|
1939
|
+
}
|
|
1940
|
+
atStartOfDay() {
|
|
1941
|
+
return this.atTime({ hours: 0, minutes: 0, seconds: 0, milliseconds: 0 });
|
|
1942
|
+
}
|
|
1943
|
+
distanceFromEndOfDay() {
|
|
1944
|
+
// Calculate milliseconds until end of day directly
|
|
1945
|
+
const params = this.toParameters();
|
|
1946
|
+
const msInDay = params.hours * 3600000 + params.minutes * 60000 + params.seconds * 1000 + params.milliseconds;
|
|
1947
|
+
const msUntilEndOfDay = 86399999 - msInDay; // 23:59:59.999 in ms
|
|
1948
|
+
return TimeDuration.fromMs(msUntilEndOfDay);
|
|
1949
|
+
}
|
|
1950
|
+
atEndOfDay() {
|
|
1951
|
+
return this.atTime({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 });
|
|
1952
|
+
}
|
|
1953
|
+
promise() {
|
|
1954
|
+
return this.timeLeftFromNow().promise();
|
|
1955
|
+
}
|
|
1956
|
+
cancelablePromise() {
|
|
1957
|
+
return this.timeLeftFromNow().cancelablePromise();
|
|
1958
|
+
}
|
|
1959
|
+
delay(cb) {
|
|
1960
|
+
return this.timeLeftFromNow().delay(cb);
|
|
1961
|
+
}
|
|
1962
|
+
cancelableDelay(cb) {
|
|
1963
|
+
return this.timeLeftFromNow().cancelableDelay(cb);
|
|
1964
|
+
}
|
|
1965
|
+
ensureInTheFuture() {
|
|
1966
|
+
if (!this.isInTheFuture())
|
|
1967
|
+
throw new Error('Instant is not in the future');
|
|
1968
|
+
}
|
|
1969
|
+
ensureInThePast() {
|
|
1970
|
+
if (!this.isInThePast())
|
|
1971
|
+
throw new Error('Instant is not in the past');
|
|
1972
|
+
}
|
|
1973
|
+
isToday() {
|
|
1974
|
+
// Calculate days directly from timestamps without creating TimeInstant object
|
|
1975
|
+
return Math.floor(this.ms / 86400000) === Math.floor(Date.now() / 86400000);
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Formats this instant using the given pattern. The pattern can contain the following tokens:
|
|
1979
|
+
*
|
|
1980
|
+
* Note: Implementation inspired by the small-date library (https://github.com/robinweser/small-date).
|
|
1981
|
+
*
|
|
1982
|
+
* | Token | Description | Example |
|
|
1983
|
+
* |:------|:--------------------------------|:------------------------------|
|
|
1984
|
+
* | D | Weekday, 1 letter | W |
|
|
1985
|
+
* | DD | Weekday, 3 letters | Wed |
|
|
1986
|
+
* | DDD | Weekday, long | Wednesday |
|
|
1987
|
+
* | d | Day of the month, no padding | 3 |
|
|
1988
|
+
* | dd | Day of the month, padded to 2 | 03 |
|
|
1989
|
+
* | M | Month, numeric | 3 |
|
|
1990
|
+
* | MM | Month, 2 digits | 03 |
|
|
1991
|
+
* | MMM | Month, 3 letters | Mar |
|
|
1992
|
+
* | MMMM | Month, long | March |
|
|
1993
|
+
* | y | Year, numeric | 2021 |
|
|
1994
|
+
* | yy | Year, 2 digits | 21 |
|
|
1995
|
+
* | yyyy | Year, numeric | 2021 |
|
|
1996
|
+
* | h | Hours, no padding | 6 |
|
|
1997
|
+
* | hh | Hours, padded to 2 | 06 |
|
|
1998
|
+
* | H | Hours in 24-format, no padding | 18 |
|
|
1999
|
+
* | HH | Hours in 24-format, padded to 2 | 18 |
|
|
2000
|
+
* | m | Minutes, no padding | 7 |
|
|
2001
|
+
* | m | Minutes, padded to 2 | 07 |
|
|
2002
|
+
* | s | Seconds, no padding | 8 |
|
|
2003
|
+
* | ss | Seconds, padded to 2 | 08 |
|
|
2004
|
+
* | S | Milliseconds, no padding | 9 |
|
|
2005
|
+
* | SS | Milliseconds, padded to 2 | 09 |
|
|
2006
|
+
* | SSS | Milliseconds, padded to 3 | 009 |
|
|
2007
|
+
* | G | Era, narrow | A |
|
|
2008
|
+
* | GG | Era, short | AD |
|
|
2009
|
+
* | GGG | Era, long | Anno Domino |
|
|
2010
|
+
* | Z | Time zone, short | GMT+1 |
|
|
2011
|
+
* | ZZ | Time short, long C | entral European Standard Time |
|
|
2012
|
+
* | P | Period of the day, narrow | in the morning |
|
|
2013
|
+
* | PP | Period of the day, short | in the morning |
|
|
2014
|
+
* | PPP | Period of the day, long | in the morning |
|
|
2015
|
+
* | a | Meridiem | pm |
|
|
2016
|
+
* @param pattern The pattern to use. Refer to the token table above for details.
|
|
2017
|
+
* @param config An optional locale and timeZone definition to use during the format.
|
|
2018
|
+
* @returns a string, formatted using the given pattern, at the given timeZone with the given locale.
|
|
2019
|
+
*/
|
|
2020
|
+
format(pattern, config = {}) {
|
|
2021
|
+
return formatTimeInstant(this, pattern, config);
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Parses a date string using the given pattern and creates a TimeInstant.
|
|
2025
|
+
* This method is the inverse of format() - parsing a formatted string should recreate the original instant.
|
|
2026
|
+
*
|
|
2027
|
+
* For partial patterns (e.g., time-only or date-only), the missing components are taken from the base instant.
|
|
2028
|
+
* For example, parsing "14:30" with base set to yesterday at midnight will result in yesterday at 14:30.
|
|
2029
|
+
*
|
|
2030
|
+
* Note: Currently performs basic validation (e.g., month 1-12, day 1-31) but does not validate
|
|
2031
|
+
* calendar-specific constraints (e.g., February 30th, April 31st). Invalid dates may be
|
|
2032
|
+
* normalized by the underlying Date constructor.
|
|
2033
|
+
*
|
|
2034
|
+
* @param dateString The date string to parse
|
|
2035
|
+
* @param pattern The pattern used to parse the string (same tokens as format())
|
|
2036
|
+
* @param base The base TimeInstant to use for partial patterns (defaults to now)
|
|
2037
|
+
* @param config An optional locale and timeZone definition to use during parsing
|
|
2038
|
+
* @returns A TimeInstant parsed from the string
|
|
2039
|
+
* @throws Error if the string doesn't match the pattern or contains invalid basic values
|
|
2040
|
+
* @todo Add calendar-aware validation to reject dates like February 30th, April 31st
|
|
2041
|
+
*/
|
|
2042
|
+
static fromString(dateString, pattern, base = TimeInstant.now(), config = {}) {
|
|
2043
|
+
return parseTimeInstant(dateString, pattern, base, config);
|
|
2044
|
+
}
|
|
2045
|
+
static parse(dateString, pattern, config = {}) {
|
|
2046
|
+
return parseTimeInstantComponents(dateString, pattern, config);
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* @returns this instant, in ISO 8601 format (eg, 2024-11-01T15:49:22.024Z). The format is meant to always be realiable.
|
|
2050
|
+
*/
|
|
2051
|
+
asIso8601() {
|
|
2052
|
+
return this.format('yyyy-MM-dd"T"HH:mm:ss.SSS"Z"', { timeZone: 'UTC' });
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* @returns this instant, in a human readable format, containing both the date and the time. The format COULD change in the future, do NOT use this method for consistent outputs.
|
|
2056
|
+
*/
|
|
2057
|
+
asHumanTimestamp() {
|
|
2058
|
+
return this.format('yyyy-MM-dd HH:mm:ss');
|
|
2059
|
+
}
|
|
2060
|
+
toUnixTimestamp() {
|
|
2061
|
+
// Syntactic sugar for this.ms;
|
|
2062
|
+
return this.ms;
|
|
2063
|
+
}
|
|
2064
|
+
toDate() {
|
|
2065
|
+
return new Date(this.ms);
|
|
2066
|
+
}
|
|
2067
|
+
toParameters() {
|
|
2068
|
+
const d = this.toDate();
|
|
2069
|
+
return {
|
|
2070
|
+
year: d.getUTCFullYear(),
|
|
2071
|
+
month: d.getUTCMonth() + 1,
|
|
2072
|
+
date: d.getUTCDate(),
|
|
2073
|
+
hours: d.getUTCHours(),
|
|
2074
|
+
minutes: d.getUTCMinutes(),
|
|
2075
|
+
seconds: d.getUTCSeconds(),
|
|
2076
|
+
milliseconds: d.getUTCMilliseconds(),
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* @returns true if the instant is in the past, false otherwise
|
|
2081
|
+
*/
|
|
2082
|
+
isInThePast() {
|
|
2083
|
+
return this.ms < Date.now();
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* @returns true if the instant is in the future, false otherwise
|
|
2087
|
+
*/
|
|
2088
|
+
isInTheFuture() {
|
|
2089
|
+
return this.ms > Date.now();
|
|
2090
|
+
}
|
|
2091
|
+
isAfter(other) {
|
|
2092
|
+
return this.ms > other.ms;
|
|
2093
|
+
}
|
|
2094
|
+
isBefore(other) {
|
|
2095
|
+
return this.ms < other.ms;
|
|
2096
|
+
}
|
|
2097
|
+
compareTo(other) {
|
|
2098
|
+
if (this.ms === other.ms) {
|
|
2099
|
+
return 0;
|
|
2100
|
+
}
|
|
2101
|
+
else if (this.isBefore(other)) {
|
|
2102
|
+
return -1;
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
return 1;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
atTime(parameters) {
|
|
2109
|
+
return TimeInstant.fromParameters(parameters, this);
|
|
2110
|
+
}
|
|
2111
|
+
atDate(parameters) {
|
|
2112
|
+
return TimeInstant.fromParameters(parameters, this);
|
|
2113
|
+
}
|
|
2114
|
+
at(parameters) {
|
|
2115
|
+
return TimeInstant.fromParameters(parameters, this);
|
|
2116
|
+
}
|
|
2117
|
+
isBetween(start, end) {
|
|
2118
|
+
return this.ms >= start.ms && this.ms <= end.ms;
|
|
2119
|
+
}
|
|
2120
|
+
static fromDate(date) {
|
|
2121
|
+
return this.fromUnixTimestamp(date.getTime());
|
|
2122
|
+
}
|
|
2123
|
+
static fromUnixTimestamp(unixTimestamp) {
|
|
2124
|
+
return new TimeInstant(unixTimestamp, TimeUnit.MILLISECONDS);
|
|
2125
|
+
}
|
|
2126
|
+
static fromUnixSeconds(unixSeconds) {
|
|
2127
|
+
return new TimeInstant(unixSeconds, TimeUnit.SECONDS);
|
|
2128
|
+
}
|
|
2129
|
+
static fromParameters(parameters, referenceDate) {
|
|
2130
|
+
return createTimeInstantFromParameters(parameters, referenceDate);
|
|
2131
|
+
}
|
|
2132
|
+
static highest(instant1, instant2) {
|
|
2133
|
+
return instant1.isAfter(instant2) ? instant1 : instant2;
|
|
2134
|
+
}
|
|
2135
|
+
static lowest(instant1, instant2) {
|
|
2136
|
+
return instant1.isBefore(instant2) ? instant1 : instant2;
|
|
2137
|
+
}
|
|
2138
|
+
static builder() {
|
|
2139
|
+
return timeInstantBuilder();
|
|
2140
|
+
}
|
|
2141
|
+
static fromIso8601(str) {
|
|
2142
|
+
// Regex to capture: YYYY-MM-DDTHH:mm:ss.sssZ or YYYY-MM-DDTHH:mm:ss.sss±HH:mm
|
|
2143
|
+
const iso8601Regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})(Z|[+-]\d{2}:\d{2})?$/;
|
|
2144
|
+
const match = str.match(iso8601Regex);
|
|
2145
|
+
if (!match) {
|
|
2146
|
+
throw new Error('Invalid ISO 8601 date format: ' + str);
|
|
2147
|
+
}
|
|
2148
|
+
const [, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, millisecondStr] = match;
|
|
2149
|
+
// Parse strings to numbers
|
|
2150
|
+
const year = parseInt(yearStr, 10);
|
|
2151
|
+
const month = parseInt(monthStr, 10);
|
|
2152
|
+
const date = parseInt(dayStr, 10);
|
|
2153
|
+
const hours = parseInt(hourStr, 10);
|
|
2154
|
+
const minutes = parseInt(minuteStr, 10);
|
|
2155
|
+
const seconds = parseInt(secondStr, 10);
|
|
2156
|
+
const milliseconds = parseInt(millisecondStr, 10);
|
|
2157
|
+
// Validate each component using standalone validation functions
|
|
2158
|
+
if (!isValidYear(year))
|
|
2159
|
+
throw new Error('Invalid year in: ' + str);
|
|
2160
|
+
if (!isValidMonth(month))
|
|
2161
|
+
throw new Error('Invalid month in: ' + str);
|
|
2162
|
+
if (!isValidDayOfMonth(date))
|
|
2163
|
+
throw new Error('Invalid day in: ' + str);
|
|
2164
|
+
if (!isValidHour(hours))
|
|
2165
|
+
throw new Error('Invalid hour in: ' + str);
|
|
2166
|
+
if (!isValidMinute(minutes))
|
|
2167
|
+
throw new Error('Invalid minute in: ' + str);
|
|
2168
|
+
if (!isValidSecond(seconds))
|
|
2169
|
+
throw new Error('Invalid second in: ' + str);
|
|
2170
|
+
if (!isValidMillisecond(milliseconds))
|
|
2171
|
+
throw new Error('Invalid millisecond in: ' + str);
|
|
2172
|
+
return TimeInstant.fromParameters({
|
|
2173
|
+
year,
|
|
2174
|
+
month,
|
|
2175
|
+
date,
|
|
2176
|
+
hours,
|
|
2177
|
+
minutes,
|
|
2178
|
+
seconds,
|
|
2179
|
+
milliseconds,
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* @deprecated [2025.10.19]: Use fromIso8601 instead.
|
|
2184
|
+
*/
|
|
2185
|
+
static tryFromIso8601 = this.fromIso8601;
|
|
2186
|
+
static now() {
|
|
2187
|
+
return TimeInstant.fromUnixTimestamp(Date.now());
|
|
2188
|
+
}
|
|
2189
|
+
static compare(a, b) {
|
|
2190
|
+
return a.compareTo(b);
|
|
2191
|
+
}
|
|
2192
|
+
roundedToStartOf(unit) {
|
|
2193
|
+
return TimeInstant.fromUnixTimestamp(this.ms - (this.ms % unit.multiplier));
|
|
2194
|
+
}
|
|
2195
|
+
roundedToEndOf(unit) {
|
|
2196
|
+
return TimeInstant.fromUnixTimestamp(this.ms - (this.ms % unit.multiplier) + unit.toMs(1) - 1);
|
|
2197
|
+
}
|
|
2198
|
+
roundedToNext(unit, factor = 1) {
|
|
2199
|
+
const unitMs = unit.toMs(factor);
|
|
2200
|
+
const timestamp = Math.ceil(this.ms / unitMs) * unitMs;
|
|
2201
|
+
return TimeInstant.fromUnixTimestamp(timestamp);
|
|
2202
|
+
}
|
|
2203
|
+
roundedToPrevious(unit, factor = 1) {
|
|
2204
|
+
const unitMs = unit.toMs(factor);
|
|
2205
|
+
const timestamp = Math.floor(this.ms / unitMs) * unitMs;
|
|
2206
|
+
return TimeInstant.fromUnixTimestamp(timestamp);
|
|
2207
|
+
}
|
|
2208
|
+
roundedTo(unit, factor = 1) {
|
|
2209
|
+
const unitMs = unit.toMs(factor);
|
|
2210
|
+
const timestamp = Math.round(this.ms / unitMs) * unitMs;
|
|
2211
|
+
return TimeInstant.fromUnixTimestamp(timestamp);
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Returns the week number represented by this instant, according to the ISO 8601 standard.
|
|
2215
|
+
* Please note that the instant and the week number could be of two different years, eg the friday 1st january is actually part of week 52 of the previous year.
|
|
2216
|
+
*/
|
|
2217
|
+
get weekNumber() {
|
|
2218
|
+
/**
|
|
2219
|
+
* According to the ISO 8601 Standard, week number 1 is defined as the week containing the 4th of january (or, equivalently, the week that contains the first Thursday of the year).
|
|
2220
|
+
* As such, we basically have to count how many Thursdays there has been in this year.
|
|
2221
|
+
* Please note that the thursdayOfThisWeek could be in the previous year.
|
|
2222
|
+
*/
|
|
2223
|
+
const date = this.toDate();
|
|
2224
|
+
const oneDay = 1000 * 60 * 60 * 24;
|
|
2225
|
+
// Get thursday of this week
|
|
2226
|
+
const dayOfWeek = date.getUTCDay() || 7; // Sunday = 7 in ISO
|
|
2227
|
+
const thursdayMs = this.ms + (4 - dayOfWeek) * oneDay;
|
|
2228
|
+
const thursdayDate = new Date(thursdayMs);
|
|
2229
|
+
const thursdayYear = thursdayDate.getUTCFullYear();
|
|
2230
|
+
// First of January at noon UTC
|
|
2231
|
+
const firstOfJanMs = Date.UTC(thursdayYear, 0, 1, 12, 0, 0);
|
|
2232
|
+
const dayOfTheYear = Math.round((thursdayMs - firstOfJanMs) / oneDay);
|
|
2233
|
+
const weekNumber = Math.floor(dayOfTheYear / 7) + 1;
|
|
2234
|
+
return { weekNumber: weekNumber, year: thursdayYear };
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* This method is used to provide a better DX when inspecting a TimeInstant object in DevTools.
|
|
2238
|
+
*/
|
|
2239
|
+
get _time() {
|
|
2240
|
+
return this.asHumanTimestamp();
|
|
2241
|
+
}
|
|
2242
|
+
toString() {
|
|
2243
|
+
return `${this.constructor.name}[${this.asHumanTimestamp()}]`;
|
|
2244
|
+
}
|
|
2245
|
+
static ZERO = TimeInstant.fromUnixTimestamp(0);
|
|
2246
|
+
static fromJSON(ms) {
|
|
2247
|
+
return TimeInstant.fromUnixTimestamp(ms);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
function isTimeInstant(x) {
|
|
2251
|
+
return x !== null && x !== undefined && x instanceof TimeInstant;
|
|
2252
|
+
}
|
|
2253
|
+
// Utility functions for date formatting and parsing
|
|
2254
|
+
// Standalone validation functions
|
|
2255
|
+
function isValidYear(num) {
|
|
2256
|
+
return num >= 0 && num <= 9999;
|
|
2257
|
+
}
|
|
2258
|
+
function isValidMonth(num) {
|
|
2259
|
+
return num >= 1 && num <= 12;
|
|
2260
|
+
}
|
|
2261
|
+
// TODO: Add calendar-aware validation to check month-specific day limits
|
|
2262
|
+
// (e.g., reject February 30th, April 31st, February 29th in non-leap years)
|
|
2263
|
+
function isValidDayOfMonth(num) {
|
|
2264
|
+
return num >= 1 && num <= 31;
|
|
2265
|
+
}
|
|
2266
|
+
function isValidHour(num) {
|
|
2267
|
+
return num >= 0 && num <= 23;
|
|
2268
|
+
}
|
|
2269
|
+
function isValidMinute(num) {
|
|
2270
|
+
return num >= 0 && num <= 59;
|
|
2271
|
+
}
|
|
2272
|
+
function isValidSecond(num) {
|
|
2273
|
+
return num >= 0 && num <= 59;
|
|
2274
|
+
}
|
|
2275
|
+
function isValidMillisecond(num) {
|
|
2276
|
+
return num >= 0 && num <= 999;
|
|
2277
|
+
}
|
|
2278
|
+
// Month names cache by locale to avoid regenerating on every call
|
|
2279
|
+
const monthNamesCache = new Map();
|
|
2280
|
+
function getMonthNames(locale = 'en') {
|
|
2281
|
+
// Check cache first
|
|
2282
|
+
const cached = monthNamesCache.get(locale);
|
|
2283
|
+
if (cached) {
|
|
2284
|
+
return cached;
|
|
2285
|
+
}
|
|
2286
|
+
// Use Intl.DateTimeFormat to generate month names for any locale
|
|
2287
|
+
const shortFormatter = new Intl.DateTimeFormat(locale, { month: 'short' });
|
|
2288
|
+
const longFormatter = new Intl.DateTimeFormat(locale, { month: 'long' });
|
|
2289
|
+
const short = [];
|
|
2290
|
+
const long = [];
|
|
2291
|
+
// Generate names for all 12 months (0-11 in Date constructor)
|
|
2292
|
+
for (let month = 0; month < 12; month++) {
|
|
2293
|
+
// Use January 1st of year 2000 as a reference date (arbitrary non-leap year)
|
|
2294
|
+
const date = new Date(2000, month, 1);
|
|
2295
|
+
short.push(normalizeMonthName(shortFormatter.format(date)));
|
|
2296
|
+
long.push(normalizeMonthName(longFormatter.format(date)));
|
|
2297
|
+
}
|
|
2298
|
+
const result = { short, long };
|
|
2299
|
+
monthNamesCache.set(locale, result);
|
|
2300
|
+
return result;
|
|
2301
|
+
}
|
|
2302
|
+
// Normalize month names by removing accents, periods, and other special characters
|
|
2303
|
+
function normalizeMonthName(name) {
|
|
2304
|
+
return name
|
|
2305
|
+
// Remove periods and other punctuation
|
|
2306
|
+
.replace(/[.,;:!?]/g, '')
|
|
2307
|
+
// Normalize Unicode (decompose accented characters)
|
|
2308
|
+
.normalize('NFD')
|
|
2309
|
+
// Remove diacritical marks (accents)
|
|
2310
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
2311
|
+
// Trim whitespace
|
|
2312
|
+
.trim();
|
|
2313
|
+
}
|
|
2314
|
+
const PATTERN_REGEX = /(M|y|d|D|h|H|m|s|S|G|Z|P|a)+/g;
|
|
2315
|
+
const ESCAPE_REGEX = /\\"|"((?:\\"|[^"])*)"/g;
|
|
2316
|
+
// Formatter configuration mapping: token pattern -> configuration with optional extraction rules
|
|
2317
|
+
const formatterConfigs = {
|
|
2318
|
+
// Year
|
|
2319
|
+
'y': { options: { year: 'numeric' } },
|
|
2320
|
+
'yy': { options: { year: '2-digit' } },
|
|
2321
|
+
'yyyy': { options: { year: 'numeric' } },
|
|
2322
|
+
// Month
|
|
2323
|
+
'M': { options: { month: 'numeric' } },
|
|
2324
|
+
'MM': { options: { month: '2-digit' } },
|
|
2325
|
+
'MMM': { options: { month: 'short' } },
|
|
2326
|
+
'MMMM': { options: { month: 'long' } },
|
|
2327
|
+
// Day
|
|
2328
|
+
'd': { options: { day: 'numeric' } },
|
|
2329
|
+
'dd': { options: { day: '2-digit' } },
|
|
2330
|
+
// Hours (24-hour) - extract and pad manually due to Intl quirk
|
|
2331
|
+
'H': { options: { hour: 'numeric', hour12: false }, extract: { partType: 'hour' } },
|
|
2332
|
+
'HH': { options: { hour: '2-digit', hour12: false }, extract: { partType: 'hour', transform: (v) => v.padStart(2, '0') } },
|
|
2333
|
+
// Hours (12-hour) - extract and pad manually due to Intl quirk
|
|
2334
|
+
'h': { options: { hour: 'numeric', hour12: true }, extract: { partType: 'hour' } },
|
|
2335
|
+
'hh': { options: { hour: '2-digit', hour12: true }, extract: { partType: 'hour', transform: (v) => v.padStart(2, '0') } },
|
|
2336
|
+
// Minutes - extract and pad manually due to Intl quirk
|
|
2337
|
+
'm': { options: { minute: 'numeric' }, extract: { partType: 'minute' } },
|
|
2338
|
+
'mm': { options: { minute: '2-digit' }, extract: { partType: 'minute', transform: (v) => v.padStart(2, '0') } },
|
|
2339
|
+
// Seconds - extract and pad manually due to Intl quirk
|
|
2340
|
+
's': { options: { second: 'numeric' }, extract: { partType: 'second' } },
|
|
2341
|
+
'ss': { options: { second: '2-digit' }, extract: { partType: 'second', transform: (v) => v.padStart(2, '0') } },
|
|
2342
|
+
// Milliseconds - extract manually
|
|
2343
|
+
'S': { options: { fractionalSecondDigits: 1 }, extract: { partType: 'fractionalSecond' } },
|
|
2344
|
+
'SS': { options: { fractionalSecondDigits: 2 }, extract: { partType: 'fractionalSecond' } },
|
|
2345
|
+
'SSS': { options: { fractionalSecondDigits: 3 }, extract: { partType: 'fractionalSecond' } },
|
|
2346
|
+
// Weekday
|
|
2347
|
+
'D': { options: { weekday: 'narrow' } },
|
|
2348
|
+
'DD': { options: { weekday: 'short' } },
|
|
2349
|
+
'DDD': { options: { weekday: 'long' } },
|
|
2350
|
+
// Era
|
|
2351
|
+
'G': { options: { era: 'narrow' } },
|
|
2352
|
+
'GG': { options: { era: 'short' } },
|
|
2353
|
+
'GGG': { options: { era: 'long' } },
|
|
2354
|
+
// Timezone
|
|
2355
|
+
'Z': { options: { timeZoneName: 'short' } },
|
|
2356
|
+
'ZZ': { options: { timeZoneName: 'long' } },
|
|
2357
|
+
// Day period
|
|
2358
|
+
'P': { options: { dayPeriod: 'narrow' } },
|
|
2359
|
+
'PP': { options: { dayPeriod: 'short' } },
|
|
2360
|
+
'PPP': { options: { dayPeriod: 'long' } },
|
|
2361
|
+
// Meridiem - extract dayPeriod and lowercase it
|
|
2362
|
+
'a': { options: { hour: 'numeric', hour12: true }, extract: { partType: 'dayPeriod', transform: (v) => v.toLowerCase() } },
|
|
2363
|
+
};
|
|
2364
|
+
// Format a token using Intl.DateTimeFormat
|
|
2365
|
+
function formatType(type, length, date, locale = 'en', timeZone = 'UTC') {
|
|
2366
|
+
const tokenKey = type.repeat(length);
|
|
2367
|
+
const config = formatterConfigs[tokenKey];
|
|
2368
|
+
if (!config) {
|
|
2369
|
+
return undefined;
|
|
2370
|
+
}
|
|
2371
|
+
const formatter = new Intl.DateTimeFormat(locale, { ...config.options, timeZone });
|
|
2372
|
+
// If extraction metadata is specified, use it
|
|
2373
|
+
if (config.extract) {
|
|
2374
|
+
const part = formatter.formatToParts(date).find(p => p.type === config.extract.partType);
|
|
2375
|
+
const value = part?.value;
|
|
2376
|
+
if (!value) {
|
|
2377
|
+
return undefined;
|
|
2378
|
+
}
|
|
2379
|
+
// Apply transformation if specified
|
|
2380
|
+
return config.extract.transform ? config.extract.transform(value) : value;
|
|
2381
|
+
}
|
|
2382
|
+
// Otherwise, format directly
|
|
2383
|
+
return formatter.format(date);
|
|
2384
|
+
}
|
|
2385
|
+
function formatTimeInstant(instant, pattern, config = {}) {
|
|
2386
|
+
const date = instant.toDate();
|
|
2387
|
+
const locale = config.locale || 'en';
|
|
2388
|
+
const timeZone = config.timeZone || 'UTC';
|
|
2389
|
+
return pattern
|
|
2390
|
+
.split(ESCAPE_REGEX)
|
|
2391
|
+
.filter((sub) => sub !== undefined)
|
|
2392
|
+
.map((sub, index) => {
|
|
2393
|
+
// keep escaped strings as is
|
|
2394
|
+
if (index % 2 !== 0) {
|
|
2395
|
+
return sub;
|
|
2396
|
+
}
|
|
2397
|
+
return sub.replace(PATTERN_REGEX, (match) => {
|
|
2398
|
+
const type = match.charAt(0);
|
|
2399
|
+
const length = match.length;
|
|
2400
|
+
return formatType(type, length, date, locale, timeZone) || match;
|
|
2401
|
+
});
|
|
2402
|
+
})
|
|
2403
|
+
.join('');
|
|
2404
|
+
}
|
|
2405
|
+
function parseTimeInstant(dateString, pattern, base, config = {}) {
|
|
2406
|
+
// Parse the components from the date string
|
|
2407
|
+
const parsed = parseTimeInstantComponents(dateString, pattern, config);
|
|
2408
|
+
// Fill in missing values from the base instant
|
|
2409
|
+
const params = {
|
|
2410
|
+
...base.toParameters(),
|
|
2411
|
+
...parsed
|
|
2412
|
+
};
|
|
2413
|
+
// Create and return the TimeInstant with complete parameters
|
|
2414
|
+
return TimeInstant.fromParameters(params);
|
|
2415
|
+
}
|
|
2416
|
+
function parseTimeInstantComponents(dateString, pattern, config = {}) {
|
|
2417
|
+
// Create a regex pattern from the format pattern
|
|
2418
|
+
let regexPattern = pattern;
|
|
2419
|
+
const tokens = [];
|
|
2420
|
+
let position = 0;
|
|
2421
|
+
// Split by escape regex first, like the original implementation
|
|
2422
|
+
const parts = pattern.split(ESCAPE_REGEX).filter((sub) => sub !== undefined);
|
|
2423
|
+
// Rebuild pattern and track tokens
|
|
2424
|
+
regexPattern = parts.map((sub, index) => {
|
|
2425
|
+
// Skip escaped strings (odd indices after split)
|
|
2426
|
+
if (index % 2 !== 0) {
|
|
2427
|
+
return sub.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape special regex chars
|
|
2428
|
+
}
|
|
2429
|
+
return sub.replace(PATTERN_REGEX, (match) => {
|
|
2430
|
+
const type = match.charAt(0);
|
|
2431
|
+
tokens.push({ type, length: match.length, position: position++ });
|
|
2432
|
+
// Create appropriate regex for each token type
|
|
2433
|
+
switch (type) {
|
|
2434
|
+
case 'y':
|
|
2435
|
+
return match.length === 2 ? '(\\d{2})' : '(\\d{4})';
|
|
2436
|
+
case 'M':
|
|
2437
|
+
if (match.length === 1)
|
|
2438
|
+
return '(\\d{1,2})';
|
|
2439
|
+
if (match.length === 2)
|
|
2440
|
+
return '(\\d{2})';
|
|
2441
|
+
if (match.length === 3)
|
|
2442
|
+
return '([A-Za-z.]{1,7})';
|
|
2443
|
+
return '([A-Za-z]+)';
|
|
2444
|
+
case 'd':
|
|
2445
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
2446
|
+
case 'H':
|
|
2447
|
+
case 'h':
|
|
2448
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
2449
|
+
case 'm':
|
|
2450
|
+
case 's':
|
|
2451
|
+
return match.length === 1 ? '(\\d{1,2})' : '(\\d{2})';
|
|
2452
|
+
case 'S':
|
|
2453
|
+
return `(\\d{${match.length}})`;
|
|
2454
|
+
case 'a':
|
|
2455
|
+
return '([aApP][mM])';
|
|
2456
|
+
case 'D':
|
|
2457
|
+
if (match.length === 1)
|
|
2458
|
+
return '([A-Za-z])';
|
|
2459
|
+
if (match.length === 2)
|
|
2460
|
+
return '([A-Za-z]{3})';
|
|
2461
|
+
return '([A-Za-z]+)';
|
|
2462
|
+
case 'G':
|
|
2463
|
+
if (match.length === 1)
|
|
2464
|
+
return '([A-Za-z])';
|
|
2465
|
+
if (match.length === 2)
|
|
2466
|
+
return '([A-Za-z]{2})';
|
|
2467
|
+
return '([A-Za-z\\s]+)';
|
|
2468
|
+
case 'Z':
|
|
2469
|
+
return match.length === 1 ? '([A-Za-z0-9+\\-:]+)' : '([A-Za-z\\s]+)';
|
|
2470
|
+
case 'P':
|
|
2471
|
+
return '([A-Za-z\\s]+)';
|
|
2472
|
+
default:
|
|
2473
|
+
return match;
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
}).join('');
|
|
2477
|
+
const regex = new RegExp('^' + regexPattern + '$');
|
|
2478
|
+
const matches = dateString.match(regex);
|
|
2479
|
+
if (!matches) {
|
|
2480
|
+
throw new Error(`Date string "${dateString}" does not match pattern "${pattern}"`);
|
|
2481
|
+
}
|
|
2482
|
+
// Extract only the values that were actually parsed (no defaults)
|
|
2483
|
+
const result = {};
|
|
2484
|
+
const locale = config.locale || 'en';
|
|
2485
|
+
let isPM = false;
|
|
2486
|
+
let is12Hour = false;
|
|
2487
|
+
let hourValue;
|
|
2488
|
+
tokens.forEach((token, index) => {
|
|
2489
|
+
const value = matches[index + 1];
|
|
2490
|
+
switch (token.type) {
|
|
2491
|
+
case 'y':
|
|
2492
|
+
if (token.length === 2) {
|
|
2493
|
+
const shortYear = parseInt(value, 10);
|
|
2494
|
+
result.year = shortYear < 50 ? 2000 + shortYear : 1900 + shortYear;
|
|
2495
|
+
}
|
|
2496
|
+
else {
|
|
2497
|
+
result.year = parseInt(value, 10);
|
|
2498
|
+
}
|
|
2499
|
+
break;
|
|
2500
|
+
case 'M':
|
|
2501
|
+
switch (token.length) {
|
|
2502
|
+
case 1:
|
|
2503
|
+
case 2:
|
|
2504
|
+
result.month = parseInt(value, 10);
|
|
2505
|
+
break;
|
|
2506
|
+
case 3: {
|
|
2507
|
+
const normalizedValue = normalizeMonthName(value);
|
|
2508
|
+
const monthIndex = getMonthNames(locale).short.findIndex(name => name.toLowerCase() === normalizedValue.toLowerCase());
|
|
2509
|
+
if (monthIndex === -1)
|
|
2510
|
+
throw new Error(`Invalid short month name in date string: ${dateString}`);
|
|
2511
|
+
result.month = (monthIndex + 1);
|
|
2512
|
+
break;
|
|
2513
|
+
}
|
|
2514
|
+
case 4: {
|
|
2515
|
+
const normalizedValue = normalizeMonthName(value);
|
|
2516
|
+
const monthIndex = getMonthNames(locale).long.findIndex(name => name.toLowerCase() === normalizedValue.toLowerCase());
|
|
2517
|
+
if (monthIndex === -1)
|
|
2518
|
+
throw new Error(`Invalid full month name in date string: ${dateString}`);
|
|
2519
|
+
result.month = (monthIndex + 1);
|
|
2520
|
+
break;
|
|
2521
|
+
}
|
|
2522
|
+
default:
|
|
2523
|
+
throw new Error(`Invalid month pattern: ${token}`);
|
|
2524
|
+
}
|
|
2525
|
+
break;
|
|
2526
|
+
case 'd':
|
|
2527
|
+
result.date = parseInt(value, 10);
|
|
2528
|
+
break;
|
|
2529
|
+
case 'H':
|
|
2530
|
+
result.hours = parseInt(value, 10);
|
|
2531
|
+
break;
|
|
2532
|
+
case 'h':
|
|
2533
|
+
hourValue = parseInt(value, 10);
|
|
2534
|
+
is12Hour = true;
|
|
2535
|
+
break;
|
|
2536
|
+
case 'm':
|
|
2537
|
+
result.minutes = parseInt(value, 10);
|
|
2538
|
+
break;
|
|
2539
|
+
case 's':
|
|
2540
|
+
result.seconds = parseInt(value, 10);
|
|
2541
|
+
break;
|
|
2542
|
+
case 'S':
|
|
2543
|
+
let ms = parseInt(value, 10);
|
|
2544
|
+
// Normalize to milliseconds based on length
|
|
2545
|
+
if (token.length === 1)
|
|
2546
|
+
ms *= 100;
|
|
2547
|
+
else if (token.length === 2)
|
|
2548
|
+
ms *= 10;
|
|
2549
|
+
result.milliseconds = ms;
|
|
2550
|
+
break;
|
|
2551
|
+
case 'a':
|
|
2552
|
+
isPM = value.toLowerCase().includes('p');
|
|
2553
|
+
break;
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
// Handle 12-hour format conversion
|
|
2557
|
+
if (is12Hour && hourValue !== undefined) {
|
|
2558
|
+
if (isPM && hourValue < 12) {
|
|
2559
|
+
result.hours = (hourValue + 12);
|
|
2560
|
+
}
|
|
2561
|
+
else if (!isPM && hourValue === 12) {
|
|
2562
|
+
result.hours = 0;
|
|
2563
|
+
}
|
|
2564
|
+
else {
|
|
2565
|
+
result.hours = hourValue;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
// Validate each extracted component (only validate primitives)
|
|
2569
|
+
if (typeof result.year === 'number' && !isValidYear(result.year)) {
|
|
2570
|
+
throw new Error(`Invalid year in date string: ${dateString}`);
|
|
2571
|
+
}
|
|
2572
|
+
if (typeof result.month === 'number' && !isValidMonth(result.month)) {
|
|
2573
|
+
throw new Error(`Invalid month in date string: ${dateString}`);
|
|
2574
|
+
}
|
|
2575
|
+
if (typeof result.date === 'number' && !isValidDayOfMonth(result.date)) {
|
|
2576
|
+
throw new Error(`Invalid day in date string: ${dateString}`);
|
|
2577
|
+
}
|
|
2578
|
+
if (typeof result.hours === 'number' && !isValidHour(result.hours)) {
|
|
2579
|
+
throw new Error(`Invalid hour in date string: ${dateString}`);
|
|
2580
|
+
}
|
|
2581
|
+
if (typeof result.minutes === 'number' && !isValidMinute(result.minutes)) {
|
|
2582
|
+
throw new Error(`Invalid minute in date string: ${dateString}`);
|
|
2583
|
+
}
|
|
2584
|
+
if (typeof result.seconds === 'number' && !isValidSecond(result.seconds)) {
|
|
2585
|
+
throw new Error(`Invalid second in date string: ${dateString}`);
|
|
2586
|
+
}
|
|
2587
|
+
if (typeof result.milliseconds === 'number' && !isValidMillisecond(result.milliseconds)) {
|
|
2588
|
+
throw new Error(`Invalid millisecond in date string: ${dateString}`);
|
|
2589
|
+
}
|
|
2590
|
+
return result;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
const LEVELS = ["trace", "log", "debug", "info", "warn", "error"];
|
|
2594
|
+
const timestamp = () => TimeInstant.now().format('HH:mm:ss');
|
|
2595
|
+
class Logger {
|
|
2596
|
+
name;
|
|
2597
|
+
enabled = true;
|
|
2598
|
+
console;
|
|
2599
|
+
minLevel;
|
|
2600
|
+
constructor(name, args) {
|
|
2601
|
+
this.name = name;
|
|
2602
|
+
this.console = args?.console ?? globalThis.console;
|
|
2603
|
+
this.enabled = args?.enabled ?? true;
|
|
2604
|
+
this.minLevel = LEVELS.indexOf((args?.minLevel ?? "DEBUG").toLowerCase());
|
|
2605
|
+
}
|
|
2606
|
+
get trace() { return this.logWrap('trace'); }
|
|
2607
|
+
get log() { return this.logWrap('log'); }
|
|
2608
|
+
get debug() { return this.logWrap('debug'); }
|
|
2609
|
+
get info() { return this.logWrap('info'); }
|
|
2610
|
+
get warn() { return this.logWrap('warn'); }
|
|
2611
|
+
get error() { return this.logWrap('error'); }
|
|
2612
|
+
logWrap(methodName) {
|
|
2613
|
+
if (!this.enabled || !this.console || LEVELS.indexOf(methodName) < this.minLevel)
|
|
2614
|
+
return noop;
|
|
2615
|
+
return this.console[methodName].bind(this.console, timestamp(), padRight(methodName.toUpperCase(), 5, ' '), this.name);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
const logger = new Logger('Cached');
|
|
2620
|
+
/**
|
|
2621
|
+
* A cached value with stale-while-revalidate pattern.
|
|
2622
|
+
* On initialization, the class starts calculating the async result (isPending() === true).
|
|
2623
|
+
* When complete, the class will store the generated value (isReady() === true).
|
|
2624
|
+
* After maxStale duration, the value becomes stale and will refresh in background on next access (isStale() === true).
|
|
2625
|
+
* After maxAge has passed, the cached value is no longer used (isExpired() === true).
|
|
2626
|
+
*/
|
|
2627
|
+
class Cached {
|
|
2628
|
+
/** Current promise for the value generation. */
|
|
2629
|
+
_promise = undefined;
|
|
2630
|
+
/** Flag indicating if a generation is currently in progress. */
|
|
2631
|
+
_pending = false;
|
|
2632
|
+
/** The last successfully generated value. */
|
|
2633
|
+
_resolvedValue = undefined;
|
|
2634
|
+
/** Function that generates the cached value. */
|
|
2635
|
+
_generator;
|
|
2636
|
+
/** Timestamp of the last successful value generation. */
|
|
2637
|
+
_generatedAt = TimeInstant.ZERO;
|
|
2638
|
+
/** The duration for which value should be kept cached. After maxAge from the last generated value, this object is considered empty. */
|
|
2639
|
+
_maxAge;
|
|
2640
|
+
/** The duration for which the value should be served from cache, even if stale. Has no effect if greater or equal to maxAge. */
|
|
2641
|
+
_maxStale;
|
|
2642
|
+
/** Callback invoked when new data becomes available. */
|
|
2643
|
+
_onNewDataAvailable = noop;
|
|
2644
|
+
/** Callback invoked when generation fails. */
|
|
2645
|
+
_onError = err => logger.error('Cache generation failed:', err);
|
|
2646
|
+
constructor(generator, maxAge, maxStale) {
|
|
2647
|
+
this._generator = ensureDefined(generator);
|
|
2648
|
+
this._maxAge = ensureDefined(maxAge);
|
|
2649
|
+
this._maxStale = ensureDefined(TimeDuration.lowest(maxAge, maxStale));
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Gets the cached value.
|
|
2653
|
+
* Returns fresh data if available, stale data while revalidating, or triggers generation if empty/expired.
|
|
2654
|
+
* @returns Promise resolving to the cached value.
|
|
2655
|
+
*/
|
|
2656
|
+
async get() {
|
|
2657
|
+
if (this.isPending()) {
|
|
2658
|
+
return this._promise;
|
|
2659
|
+
}
|
|
2660
|
+
else if (this.isEmpty() || this.isExpired()) {
|
|
2661
|
+
this._promise = this.generate();
|
|
2662
|
+
return this._promise;
|
|
2663
|
+
}
|
|
2664
|
+
else {
|
|
2665
|
+
if (this.isStale()) {
|
|
2666
|
+
// Start re-generating the contents, while serving stale data.
|
|
2667
|
+
void this.generate();
|
|
2668
|
+
}
|
|
2669
|
+
return this._resolvedValue;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Generates a new value using the generator function.
|
|
2674
|
+
* Sets up promise handlers for success and error cases.
|
|
2675
|
+
* @returns The generation promise.
|
|
2676
|
+
*/
|
|
2677
|
+
generate() {
|
|
2678
|
+
if (this._pending)
|
|
2679
|
+
return this._promise;
|
|
2680
|
+
this._pending = true;
|
|
2681
|
+
const deferred = new Deferred();
|
|
2682
|
+
this._generator().then(value => {
|
|
2683
|
+
this._pending = false;
|
|
2684
|
+
this._resolvedValue = value;
|
|
2685
|
+
this._generatedAt = TimeInstant.now();
|
|
2686
|
+
deferred.resolve(value);
|
|
2687
|
+
withTryCatch(() => this._onNewDataAvailable(value));
|
|
2688
|
+
}, error => {
|
|
2689
|
+
const err = asError(error);
|
|
2690
|
+
this._pending = false;
|
|
2691
|
+
deferred.reject(err);
|
|
2692
|
+
withTryCatch(() => this._onError(err));
|
|
2693
|
+
});
|
|
2694
|
+
return deferred.asPromise();
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Checks if value has been requested at least once.
|
|
2698
|
+
* @returns True if a promise exists.
|
|
2699
|
+
*/
|
|
2700
|
+
isPresent() {
|
|
2701
|
+
return this._promise !== undefined;
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Checks value has never been requested.
|
|
2705
|
+
* @returns True if empty.
|
|
2706
|
+
*/
|
|
2707
|
+
isEmpty() {
|
|
2708
|
+
return this._promise === undefined;
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Checks if value generation is currently in progress.
|
|
2712
|
+
* @returns True if generation is pending.
|
|
2713
|
+
*/
|
|
2714
|
+
isPending() {
|
|
2715
|
+
return this.isPresent() && this._pending === true;
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Checks if value is stored and fresh.
|
|
2719
|
+
* @returns True if stored and fresh.
|
|
2720
|
+
*/
|
|
2721
|
+
isReady() {
|
|
2722
|
+
return this.isPresent() && this.getAge().isLessThan(this._maxAge);
|
|
2723
|
+
}
|
|
2724
|
+
// protected isResolved() {
|
|
2725
|
+
// return this.isReady() && this._error === undefined;
|
|
2726
|
+
// }
|
|
2727
|
+
// protected isError() {
|
|
2728
|
+
// return this.isReady() && this._error !== undefined;
|
|
2729
|
+
// }
|
|
2730
|
+
/**
|
|
2731
|
+
* Checks if cached value is stale (older than maxStale duration).
|
|
2732
|
+
* @returns True if value is stale.
|
|
2733
|
+
*/
|
|
2734
|
+
isStale() {
|
|
2735
|
+
return this.isPresent() && this.getAge().isGreaterThan(this._maxStale);
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Checks if cached value is expired (older than maxAge duration).
|
|
2739
|
+
* @returns True if value is expired.
|
|
2740
|
+
*/
|
|
2741
|
+
isExpired() {
|
|
2742
|
+
return this.isPresent() && this.getAge().isGreaterThan(this._maxAge);
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Gets the timestamp when the value was last successfully generated.
|
|
2746
|
+
* @returns The last update timestamp.
|
|
2747
|
+
*/
|
|
2748
|
+
getLastUpdated() {
|
|
2749
|
+
return this._generatedAt;
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Gets the age of the cached value.
|
|
2753
|
+
* @returns The duration since the value was last generated.
|
|
2754
|
+
*/
|
|
2755
|
+
getAge() {
|
|
2756
|
+
return this._generatedAt.distanceFromNow();
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Invalidates the cache by resetting the generation timestamp.
|
|
2760
|
+
* Next access will trigger fresh generation.
|
|
2761
|
+
*/
|
|
2762
|
+
invalidate() {
|
|
2763
|
+
this._generatedAt = TimeInstant.ZERO;
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Sets a callback to be invoked when new data becomes available.
|
|
2767
|
+
* @param callback Function to call with the new value.
|
|
2768
|
+
*/
|
|
2769
|
+
onNewDataAvailable(callback) {
|
|
2770
|
+
this._onNewDataAvailable = ensureDefined(callback);
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Sets a callback to be invoked when generation fails.
|
|
2774
|
+
* @param callback Function to call with the error.
|
|
2775
|
+
*/
|
|
2776
|
+
onError(callback) {
|
|
2777
|
+
this._onError = ensureDefined(callback);
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Creates a new Cached instance.
|
|
2781
|
+
* @param generator Function that generates the value to cache.
|
|
2782
|
+
* @param maxAge Maximum duration for which cached value is considered valid.
|
|
2783
|
+
* @param maxStale Maximum duration for which stale value is served while revalidating. Defaults to maxAge.
|
|
2784
|
+
* @returns A new Cached instance.
|
|
2785
|
+
*/
|
|
2786
|
+
static of(generator, maxAge, maxStale = maxAge) {
|
|
2787
|
+
const cached = new Cached(generator, maxAge, maxStale);
|
|
2788
|
+
void cached.get();
|
|
2789
|
+
return cached;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
const defaultCompareValuesOptions = {
|
|
2794
|
+
nullsFirst: false,
|
|
2795
|
+
};
|
|
2796
|
+
const defaultCompareFunctionOptions = {
|
|
2797
|
+
...defaultCompareValuesOptions,
|
|
2798
|
+
direction: 'ASC',
|
|
2799
|
+
};
|
|
2800
|
+
const naturalOrderComparison = (a, b) => a < b ? -1 : 1;
|
|
2801
|
+
function compareValues(a, b, cmp, { nullsFirst }) {
|
|
2802
|
+
if (a === b)
|
|
2803
|
+
return 0;
|
|
2804
|
+
const nullA = isNullOrUndefined(a);
|
|
2805
|
+
const nullB = isNullOrUndefined(b);
|
|
2806
|
+
if (nullA && nullB)
|
|
2807
|
+
return 0;
|
|
2808
|
+
if (nullA !== nullB)
|
|
2809
|
+
return (nullA === nullsFirst) ? -1 : 1;
|
|
2810
|
+
// A & B are defined and are different.
|
|
2811
|
+
return cmp(a, b);
|
|
2812
|
+
}
|
|
2813
|
+
function compareFunction(fn, cmp, { nullsFirst, direction }) {
|
|
2814
|
+
return (a, b) => applyDirection(compareValues(fn(a), fn(b), cmp, { nullsFirst }), direction);
|
|
2815
|
+
}
|
|
2816
|
+
function applyDirection(res, direction) {
|
|
2817
|
+
return res * (direction === 'ASC' ? 1 : -1);
|
|
2818
|
+
}
|
|
2819
|
+
function combine(f, g) {
|
|
2820
|
+
return (t) => g(f(t));
|
|
2821
|
+
}
|
|
2822
|
+
function reverse(fn) {
|
|
2823
|
+
return (a, b) => fn(a, b) * -1;
|
|
2824
|
+
}
|
|
2825
|
+
const chain = (fns, cmpFn) => {
|
|
2826
|
+
return doCreateWithFunctions([...fns, cmpFn]);
|
|
2827
|
+
};
|
|
2828
|
+
const transformAndChain = (fns, transform, cmpFn) => {
|
|
2829
|
+
const fn = (a, b) => cmpFn(transform(a), transform(b));
|
|
2830
|
+
return chain(fns, fn);
|
|
2831
|
+
};
|
|
2832
|
+
const compareStrings = (fns, transform, options = {}) => {
|
|
2833
|
+
const { nullsFirst, direction, ignoreCase } = { ...defaultStringComparisonOptions, ...options };
|
|
2834
|
+
if (ignoreCase)
|
|
2835
|
+
transform = combine(transform, t => t.toLowerCase());
|
|
2836
|
+
return chain(fns, compareFunction(transform, naturalOrderComparison, { nullsFirst, direction }));
|
|
2837
|
+
};
|
|
2838
|
+
const compareNumbers = (fns, transform, options = {}) => {
|
|
2839
|
+
const { nullsFirst, direction } = { ...defaultNumberComparisonOptions, ...options };
|
|
2840
|
+
return chain(fns, compareFunction(transform, naturalOrderComparison, { nullsFirst, direction }));
|
|
2841
|
+
};
|
|
2842
|
+
const compareDates = (fns, transform, options = {}) => {
|
|
2843
|
+
const { nullsFirst, direction } = { ...defaultDateComparisonOptions, ...options };
|
|
2844
|
+
return chain(fns, compareFunction(transform, naturalOrderComparison, { nullsFirst, direction }));
|
|
2845
|
+
};
|
|
2846
|
+
const compareTimeInstants = (fns, transform, options = {}) => {
|
|
2847
|
+
const { nullsFirst, direction } = { ...defaultTimeInstantComparisonOptions, ...options };
|
|
2848
|
+
return chain(fns, compareFunction(transform, (a, b) => a.isBefore(b) ? -1 : 1, { nullsFirst, direction }));
|
|
2849
|
+
};
|
|
2850
|
+
const compareTimeDurations = (fns, transform, options = {}) => {
|
|
2851
|
+
const { nullsFirst, direction } = { ...defaultTimeDurationComparisonOptions, ...options };
|
|
2852
|
+
return chain(fns, compareFunction(transform, (a, b) => a.isLessThan(b) ? -1 : 1, { nullsFirst, direction }));
|
|
2853
|
+
};
|
|
2854
|
+
const compareBooleans = (fns, transform, options) => {
|
|
2855
|
+
const { nullsFirst, truesFirst } = { ...defaultBooleanComparisonOptions, ...options };
|
|
2856
|
+
return chain(fns, compareFunction(transform, naturalOrderComparison, { nullsFirst, direction: truesFirst ? 'DESC' : 'ASC' }));
|
|
2857
|
+
};
|
|
2858
|
+
const prioritizeSet = (fns, transform, set, reversed = false) => {
|
|
2859
|
+
return compareBooleans(fns, (t) => {
|
|
2860
|
+
const r = transform(t);
|
|
2861
|
+
return isDefined(r) && set.includes(r);
|
|
2862
|
+
}, { truesFirst: !reversed, nullsFirst: false });
|
|
2863
|
+
};
|
|
2864
|
+
const prioritizeArray = (fns, transform, arr, reversed = false) => {
|
|
2865
|
+
return compareNumbers(fns, (t) => {
|
|
2866
|
+
const r = transform(t);
|
|
2867
|
+
if (!isDefined(r))
|
|
2868
|
+
return Number.MAX_VALUE;
|
|
2869
|
+
const indexOf = arr.indexOf(r);
|
|
2870
|
+
return indexOf === -1 ? Number.MAX_VALUE : indexOf;
|
|
2871
|
+
}, { direction: reversed ? 'DESC' : 'ASC', nullsFirst: false });
|
|
2872
|
+
};
|
|
2873
|
+
const next = (fns, transform) => {
|
|
2874
|
+
const using = (transformToX) => {
|
|
2875
|
+
return next(fns, combine(transform, transformToX));
|
|
2876
|
+
};
|
|
2877
|
+
const retAsChainable = {
|
|
2878
|
+
chain(chain) {
|
|
2879
|
+
return transformAndChain(fns, transform, chain.compare);
|
|
2880
|
+
},
|
|
2881
|
+
chainFunction(fn) {
|
|
2882
|
+
return transformAndChain(fns, transform, fn);
|
|
2883
|
+
},
|
|
2884
|
+
};
|
|
2885
|
+
const retAsUsing = {
|
|
2886
|
+
using,
|
|
2887
|
+
};
|
|
2888
|
+
const retAsPrioritizable = {
|
|
2889
|
+
prioritizing(arr, _options = {}) {
|
|
2890
|
+
return prioritizeArray(fns, transform, arr);
|
|
2891
|
+
},
|
|
2892
|
+
deprioritizing(arr, _options = {}) {
|
|
2893
|
+
return prioritizeArray(fns, transform, arr, true);
|
|
2894
|
+
},
|
|
2895
|
+
prioritizingWithEqualWeight(arr, _options = {}) {
|
|
2896
|
+
return prioritizeSet(fns, transform, arr);
|
|
2897
|
+
},
|
|
2898
|
+
deprioritizingWithEqualWeight(arr, _options = {}) {
|
|
2899
|
+
return prioritizeSet(fns, transform, arr, true);
|
|
2900
|
+
},
|
|
2901
|
+
};
|
|
2902
|
+
const retForRecords = {
|
|
2903
|
+
...retAsUsing,
|
|
2904
|
+
usingStrings: field => using(t => t[field]),
|
|
2905
|
+
usingNumbers: field => using(t => t[field]),
|
|
2906
|
+
usingBooleans: field => using(t => t[field]),
|
|
2907
|
+
usingDates: field => using(t => t[field]),
|
|
2908
|
+
usingTimeInstant: field => using(t => t[field]),
|
|
2909
|
+
usingTimeDuration: field => using(t => t[field]),
|
|
2910
|
+
};
|
|
2911
|
+
const retForNumbers = {
|
|
2912
|
+
...retAsUsing,
|
|
2913
|
+
inAscendingOrder(opts = {}) {
|
|
2914
|
+
return compareNumbers(fns, transform, { direction: 'ASC', ...opts });
|
|
2915
|
+
},
|
|
2916
|
+
inDescendingOrder(opts = {}) {
|
|
2917
|
+
return compareNumbers(fns, transform, { direction: 'DESC', ...opts });
|
|
2918
|
+
},
|
|
2919
|
+
};
|
|
2920
|
+
const retForStringsV1 = {
|
|
2921
|
+
...retAsUsing,
|
|
2922
|
+
inLexographicalOrder(opts = {}) {
|
|
2923
|
+
return compareStrings(fns, transform, { direction: 'ASC', ignoreCase: false, ...opts });
|
|
2924
|
+
},
|
|
2925
|
+
inLexographicalOrderIgnoringCase(opts = {}) {
|
|
2926
|
+
return compareStrings(fns, transform, { direction: 'ASC', ignoreCase: true, ...opts });
|
|
2927
|
+
},
|
|
2928
|
+
inReverseLexographicalOrder(opts = {}) {
|
|
2929
|
+
return compareStrings(fns, transform, { direction: 'DESC', ignoreCase: false, ...opts });
|
|
2930
|
+
},
|
|
2931
|
+
inReverseLexographicalOrderIgnoringCase(opts = {}) {
|
|
2932
|
+
return compareStrings(fns, transform, { direction: 'DESC', ignoreCase: true, ...opts });
|
|
2933
|
+
},
|
|
2934
|
+
};
|
|
2935
|
+
const retForStrings = {
|
|
2936
|
+
...retAsUsing,
|
|
2937
|
+
...retForStringsV1,
|
|
2938
|
+
inLexicographicalOrder(opts = {}) {
|
|
2939
|
+
return compareStrings(fns, transform, { direction: 'ASC', ignoreCase: false, ...opts });
|
|
2940
|
+
},
|
|
2941
|
+
inLexicographicalOrderIgnoringCase(opts = {}) {
|
|
2942
|
+
return compareStrings(fns, transform, { direction: 'ASC', ignoreCase: true, ...opts });
|
|
2943
|
+
},
|
|
2944
|
+
inReverseLexicographicalOrder(opts = {}) {
|
|
2945
|
+
return compareStrings(fns, transform, { direction: 'DESC', ignoreCase: false, ...opts });
|
|
2946
|
+
},
|
|
2947
|
+
inReverseLexicographicalOrderIgnoringCase(opts = {}) {
|
|
2948
|
+
return compareStrings(fns, transform, { direction: 'DESC', ignoreCase: true, ...opts });
|
|
2949
|
+
},
|
|
2950
|
+
};
|
|
2951
|
+
const retForBooleans = {
|
|
2952
|
+
...retAsUsing,
|
|
2953
|
+
truesFirst(opts = {}) {
|
|
2954
|
+
return compareBooleans(fns, transform, { truesFirst: true, ...opts });
|
|
2955
|
+
},
|
|
2956
|
+
falsesFirst(opts = {}) {
|
|
2957
|
+
return compareBooleans(fns, transform, { truesFirst: false, ...opts });
|
|
2958
|
+
},
|
|
2959
|
+
};
|
|
2960
|
+
const retForDates = {
|
|
2961
|
+
...retAsUsing,
|
|
2962
|
+
oldestDateFirst(opts = {}) {
|
|
2963
|
+
return compareDates(fns, transform, { direction: 'ASC', ...opts });
|
|
2964
|
+
},
|
|
2965
|
+
newestDateFirst(opts = {}) {
|
|
2966
|
+
return compareDates(fns, transform, { direction: 'DESC', ...opts });
|
|
2967
|
+
},
|
|
2968
|
+
};
|
|
2969
|
+
const retForInstants = {
|
|
2970
|
+
...retAsUsing,
|
|
2971
|
+
oldestFirst(opts = {}) {
|
|
2972
|
+
return compareTimeInstants(fns, transform, { direction: 'ASC', ...opts });
|
|
2973
|
+
},
|
|
2974
|
+
newestFirst(opts = {}) {
|
|
2975
|
+
return compareTimeInstants(fns, transform, { direction: 'DESC', ...opts });
|
|
2976
|
+
},
|
|
2977
|
+
};
|
|
2978
|
+
const retForDurations = {
|
|
2979
|
+
...retAsUsing,
|
|
2980
|
+
longestFirst(opts = {}) {
|
|
2981
|
+
return compareTimeDurations(fns, transform, { direction: 'DESC', ...opts });
|
|
2982
|
+
},
|
|
2983
|
+
shortestFirst(opts = {}) {
|
|
2984
|
+
return compareTimeDurations(fns, transform, { direction: 'ASC', ...opts });
|
|
2985
|
+
},
|
|
2986
|
+
};
|
|
2987
|
+
return {
|
|
2988
|
+
...retAsPrioritizable,
|
|
2989
|
+
...retAsChainable,
|
|
2990
|
+
...retForRecords,
|
|
2991
|
+
...retForNumbers,
|
|
2992
|
+
...retForStrings,
|
|
2993
|
+
...retForBooleans,
|
|
2994
|
+
...retForDates,
|
|
2995
|
+
...retForInstants,
|
|
2996
|
+
...retForDurations,
|
|
2997
|
+
};
|
|
2998
|
+
};
|
|
2999
|
+
const createComparisonFunction = (fns) => {
|
|
3000
|
+
return (a, b) => {
|
|
3001
|
+
let cmp = 0, i = 0;
|
|
3002
|
+
while (i < fns.length && cmp === 0) {
|
|
3003
|
+
cmp = fns[i++](a, b);
|
|
3004
|
+
}
|
|
3005
|
+
return cmp;
|
|
3006
|
+
};
|
|
3007
|
+
};
|
|
3008
|
+
const compare = (fns, a, b) => {
|
|
3009
|
+
return createComparisonFunction(fns)(a, b);
|
|
3010
|
+
};
|
|
3011
|
+
const sort = (fns, arr) => {
|
|
3012
|
+
const comparisonFn = createComparisonFunction(fns);
|
|
3013
|
+
return [...arr].sort(comparisonFn);
|
|
3014
|
+
};
|
|
3015
|
+
const top = (fns, arr, n) => {
|
|
3016
|
+
return sort(fns, arr).slice(0, Math.max(0, n));
|
|
3017
|
+
};
|
|
3018
|
+
const bottom = (fns, arr, n) => {
|
|
3019
|
+
return sort(fns, arr).slice(-Math.max(0, n)).reverse();
|
|
3020
|
+
};
|
|
3021
|
+
const first = (fns, arr) => {
|
|
3022
|
+
return arr.length ? top(fns, arr, 1)[0] : null;
|
|
3023
|
+
};
|
|
3024
|
+
const last = (fns, arr) => {
|
|
3025
|
+
return arr.length ? bottom(fns, arr, 1)[0] : null;
|
|
3026
|
+
};
|
|
3027
|
+
function doCreateEmpty() {
|
|
3028
|
+
// If we have no comparison functions, return an object that only has chain methods.
|
|
3029
|
+
const base = next([], x => x);
|
|
3030
|
+
const ret = {
|
|
3031
|
+
...base,
|
|
3032
|
+
usingFunction(fn) {
|
|
3033
|
+
return doCreateWithFunctions([fn]);
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
return ret;
|
|
3037
|
+
}
|
|
3038
|
+
function doCreateWithFunctions(fns) {
|
|
3039
|
+
// If we have some comparison functions, return an object that has chain methods and ending methods.
|
|
3040
|
+
return {
|
|
3041
|
+
get then() {
|
|
3042
|
+
return next(fns, identity);
|
|
3043
|
+
},
|
|
3044
|
+
reversed() {
|
|
3045
|
+
return doCreateWithFunctions([...fns.map(reverse)]);
|
|
3046
|
+
},
|
|
3047
|
+
compare: (a, b) => {
|
|
3048
|
+
return compare(fns, a, b);
|
|
3049
|
+
},
|
|
3050
|
+
sort: (arr) => {
|
|
3051
|
+
return sort(fns, arr);
|
|
3052
|
+
},
|
|
3053
|
+
top: (arr, n) => {
|
|
3054
|
+
return top(fns, arr, n);
|
|
3055
|
+
},
|
|
3056
|
+
bottom: (arr, n) => {
|
|
3057
|
+
return bottom(fns, arr, n);
|
|
3058
|
+
},
|
|
3059
|
+
first: (arr) => {
|
|
3060
|
+
return first(fns, arr);
|
|
3061
|
+
},
|
|
3062
|
+
last: (arr) => {
|
|
3063
|
+
return last(fns, arr);
|
|
3064
|
+
},
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
const defaultStringComparisonOptions = { ...defaultCompareFunctionOptions, ignoreCase: false };
|
|
3068
|
+
const defaultNumberComparisonOptions = { ...defaultCompareFunctionOptions };
|
|
3069
|
+
const defaultDateComparisonOptions = { ...defaultCompareFunctionOptions };
|
|
3070
|
+
const defaultTimeInstantComparisonOptions = { ...defaultCompareFunctionOptions };
|
|
3071
|
+
const defaultTimeDurationComparisonOptions = { ...defaultCompareFunctionOptions };
|
|
3072
|
+
const defaultBooleanComparisonOptions = { ...defaultCompareValuesOptions };
|
|
3073
|
+
const Sorter = {
|
|
3074
|
+
createFor: (_template) => doCreateEmpty(),
|
|
3075
|
+
sort: (arr, builder) => builder(doCreateEmpty()).sort(arr),
|
|
3076
|
+
top: (arr, n, builder) => builder(doCreateEmpty()).top(arr, n),
|
|
3077
|
+
bottom: (arr, n, builder) => builder(doCreateEmpty()).bottom(arr, n),
|
|
3078
|
+
first: (arr, builder) => builder(doCreateEmpty()).first(arr),
|
|
3079
|
+
last: (arr, builder) => builder(doCreateEmpty()).last(arr),
|
|
3080
|
+
};
|
|
3081
|
+
|
|
3082
|
+
function randomize(unit) {
|
|
3083
|
+
return (a, b) => {
|
|
3084
|
+
return TimeDuration.fromMs(randomNumberInInterval(unit.toMs(a), unit.toMs(b)));
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
class RandomTimeDuration {
|
|
3088
|
+
constructor() { }
|
|
3089
|
+
static ms = randomize(TimeUnit.MILLISECONDS);
|
|
3090
|
+
static seconds = randomize(TimeUnit.SECONDS);
|
|
3091
|
+
static minutes = randomize(TimeUnit.MINUTES);
|
|
3092
|
+
static hours = randomize(TimeUnit.HOURS);
|
|
3093
|
+
static days = randomize(TimeUnit.DAYS);
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
class TimeFrequency {
|
|
3097
|
+
times;
|
|
3098
|
+
period;
|
|
3099
|
+
constructor(times, period) {
|
|
3100
|
+
this.times = times;
|
|
3101
|
+
this.period = period;
|
|
3102
|
+
if (isNaN(times) || times <= 0)
|
|
3103
|
+
throw new Error();
|
|
3104
|
+
if (period.isEmpty())
|
|
3105
|
+
throw new Error();
|
|
3106
|
+
}
|
|
3107
|
+
get frequency() {
|
|
3108
|
+
return this.period.divideBy(this.times);
|
|
3109
|
+
}
|
|
3110
|
+
static onceEvery(period) {
|
|
3111
|
+
return new TimeFrequency(1, period);
|
|
3112
|
+
}
|
|
3113
|
+
static twiceEvery(period) {
|
|
3114
|
+
return new TimeFrequency(2, period);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
class TimeRange {
|
|
3119
|
+
start;
|
|
3120
|
+
duration;
|
|
3121
|
+
end;
|
|
3122
|
+
constructor(start, duration) {
|
|
3123
|
+
this.start = start;
|
|
3124
|
+
this.duration = duration;
|
|
3125
|
+
this.end = start.addDuration(duration);
|
|
3126
|
+
}
|
|
3127
|
+
static fromInstants(a, b) {
|
|
3128
|
+
const lowest = TimeInstant.lowest(a, b);
|
|
3129
|
+
const highest = TimeInstant.highest(a, b);
|
|
3130
|
+
return new TimeRange(lowest, lowest.distanceFrom(highest));
|
|
3131
|
+
}
|
|
3132
|
+
static fromInstantWithDuration(a, d) {
|
|
3133
|
+
return new TimeRange(a, d);
|
|
3134
|
+
}
|
|
3135
|
+
static fromNowWithDuration(d) {
|
|
3136
|
+
return new TimeRange(TimeInstant.now(), d);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
class UnavailableUpgradeError extends Error {
|
|
3141
|
+
data;
|
|
3142
|
+
from;
|
|
3143
|
+
to;
|
|
3144
|
+
constructor(data, from, to) {
|
|
3145
|
+
super(`No transition found to upgrade data from version ${from} to version ${to}.`);
|
|
3146
|
+
this.data = data;
|
|
3147
|
+
this.from = from;
|
|
3148
|
+
this.to = to;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
class EmptyUpgradeError extends Error {
|
|
3152
|
+
data;
|
|
3153
|
+
from;
|
|
3154
|
+
to;
|
|
3155
|
+
constructor(data, from, to) {
|
|
3156
|
+
super(`Transition from version ${from} to version ${to} resulted in empty data`);
|
|
3157
|
+
this.data = data;
|
|
3158
|
+
this.from = from;
|
|
3159
|
+
this.to = to;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
const DEBUG_ENABLED = false;
|
|
3164
|
+
function getTransitionsPath(matrix, from, to) {
|
|
3165
|
+
return internalGetTransitionsPath(matrix, from, to, 0, DEBUG_ENABLED);
|
|
3166
|
+
}
|
|
3167
|
+
function internalGetTransitionsPath(matrix, from, to, depth, debug) {
|
|
3168
|
+
const transitionsTo = matrix[to];
|
|
3169
|
+
if (!transitionsTo) {
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
// Check if there is a transition that goes directly from from to to.
|
|
3173
|
+
const exactTransition = transitionsTo[from];
|
|
3174
|
+
if (exactTransition) {
|
|
3175
|
+
return [exactTransition];
|
|
3176
|
+
}
|
|
3177
|
+
// Otherwise, recursively search for the min path.
|
|
3178
|
+
const keys = Object.keys(transitionsTo);
|
|
3179
|
+
return keys.reduce((curBestPath, transitionKey) => {
|
|
3180
|
+
const transition = transitionsTo[transitionKey];
|
|
3181
|
+
const path = internalGetTransitionsPath(matrix, from, transition.from, depth + 1, debug);
|
|
3182
|
+
if (path === null) {
|
|
3183
|
+
debugLog(debug, depth + 1, from, transition.from);
|
|
3184
|
+
return curBestPath;
|
|
3185
|
+
}
|
|
3186
|
+
else {
|
|
3187
|
+
if (curBestPath === null || curBestPath.length > path.length + 1) {
|
|
3188
|
+
debugLog(debug, depth + 1, from, transition.from, 'New best path found: ' + printTransitions(path));
|
|
3189
|
+
return [...path, transition];
|
|
3190
|
+
}
|
|
3191
|
+
else {
|
|
3192
|
+
return curBestPath;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
}, null);
|
|
3196
|
+
}
|
|
3197
|
+
function debugLog(enabled, depth, from, to, message) {
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
function printTransitions(transitions) {
|
|
3201
|
+
return transitions.reduce((ret, cur) => {
|
|
3202
|
+
return ret + " -> " + cur.to;
|
|
3203
|
+
}, "" + transitions[0].from);
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
const VERSION_FIELD = "$version";
|
|
3207
|
+
class DataUpgrader {
|
|
3208
|
+
latestVersion;
|
|
3209
|
+
transitions = {};
|
|
3210
|
+
constructor(latestVersion) {
|
|
3211
|
+
this.latestVersion = latestVersion;
|
|
3212
|
+
}
|
|
3213
|
+
addTransition(from, to, apply) {
|
|
3214
|
+
if (from === undefined || from < 0)
|
|
3215
|
+
throw new Error(`Invalid argument from, non-negative number expected, got: ${from}`);
|
|
3216
|
+
if (to === undefined || to <= 0)
|
|
3217
|
+
throw new Error(`Invalid argument to, positive number expected, got: ${to}`);
|
|
3218
|
+
if (to <= from)
|
|
3219
|
+
throw new Error(`Invalid argument to, expected number greater than ${from}, got: ${to}`);
|
|
3220
|
+
if (to > this.latestVersion)
|
|
3221
|
+
throw new Error(`Invalid argument to, expected number lower than ${this.latestVersion}, got: ${to}`);
|
|
3222
|
+
if (this.transitions[to]?.[from])
|
|
3223
|
+
throw new Error(`Invalid transition arguments, duplicated transition found from ${from} to ${to}`);
|
|
3224
|
+
const toMatrix = this.transitions[to] ?? {};
|
|
3225
|
+
toMatrix[from] = { from, to, apply };
|
|
3226
|
+
this.transitions[to] = toMatrix;
|
|
3227
|
+
return this;
|
|
3228
|
+
}
|
|
3229
|
+
async upgrade(data) {
|
|
3230
|
+
if (!data || typeof data !== "object")
|
|
3231
|
+
throw new Error(`Invalid argument data, object expected, got: ${typeof data}.`);
|
|
3232
|
+
if (data.$version === null || data.$version === undefined || typeof data.$version !== "number")
|
|
3233
|
+
throw new Error(`Invalid argument data, version metadata required, got: ${data.$version}`);
|
|
3234
|
+
if (data.$version > this.latestVersion)
|
|
3235
|
+
throw new Error(`Invalid argument data, version metadata is in the future: ${data.$version}, current: ${this.latestVersion}`);
|
|
3236
|
+
if (this.isLatestVersion(data))
|
|
3237
|
+
return data;
|
|
3238
|
+
const from = data.$version;
|
|
3239
|
+
const to = this.latestVersion;
|
|
3240
|
+
const path = getTransitionsPath(this.transitions, from, to);
|
|
3241
|
+
if (path === null)
|
|
3242
|
+
throw new UnavailableUpgradeError(data, from, to);
|
|
3243
|
+
let current = jsonCloneDeep(data);
|
|
3244
|
+
for (const transition of path) {
|
|
3245
|
+
const next = await transition.apply(current);
|
|
3246
|
+
if (next === null || next === undefined)
|
|
3247
|
+
throw new EmptyUpgradeError(current, from, to);
|
|
3248
|
+
current = next;
|
|
3249
|
+
}
|
|
3250
|
+
return current;
|
|
3251
|
+
}
|
|
3252
|
+
isLatestVersion(data) {
|
|
3253
|
+
return data.$version === this.latestVersion;
|
|
3254
|
+
}
|
|
3255
|
+
static create(latest) {
|
|
3256
|
+
return new DataUpgrader(latest);
|
|
3257
|
+
}
|
|
3258
|
+
static Errors = {
|
|
3259
|
+
EmptyUpgrade: EmptyUpgradeError,
|
|
3260
|
+
UnavailableUpgrade: UnavailableUpgradeError
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
function isUpgradable(obj) {
|
|
3264
|
+
return isDefined(obj) && typeof obj === "object" && VERSION_FIELD in obj && isNumber(obj.$version) && isPositiveNumber(obj.$version);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
export { Cached, DataUpgrader, Deferred, DeferredCanceledError, ErrorCannotInstantiatePresentOptionalWithEmptyValue, ErrorGetEmptyOptional, ErrorSetEmptyOptional, Lazy, LazyAsync, LazyDictionary, Logger, NEVER, NonExhaustiveSwitchError, Operation, Optional, PredicateBuilder, RandomTimeDuration, RateThrottler, Semaphore, Sorter, StringParts, TimeDuration, TimeFrequency, TimeInstant, TimeRange, TimeUnit, TimeoutError, alwaysFalse, alwaysTrue, and, arrayGet, arrayIncludes, asError, asPromise, average, awaitAtMost, capitalizeWord, clamp, clampInt0_100, constant, constantFalse, constantNull, constantOne, constantTrue, constantUndefined, constantZero, cssDeclarationRulesDictionaryToCss, decrement, decrementBy, delayPromise, dictToEntries, dictToList, divideBy, ellipsis, ensureArray, ensureDefined, ensureNegativeNumber, ensureNonNegativeNumber, ensureNonPositiveNumber, ensurePositiveNumber, ensureReadableArray, entriesToDict, entriesToEntries, entriesToList, extendArray, extendArrayWith, fill, filterMap, filterMapReduce, filterWithTypePredicate, findInArray, findIndexInArray, first$1 as first, flatMapTruthys, getCauseMessageFromError, getCauseStackFromError, getMessageFromError, getStackFromError, groupByBoolean, groupByBooleanWith, groupByNumber, groupByNumberWith, groupByString, groupByStringWith, groupBySymbol, groupBySymbolWith, hashCode, head, identity, ifDefined, ifNullOrUndefined, iff, includes, increment, incrementBy, indexByNumber, indexByNumberWith, indexByString, indexByStringWith, indexBySymbol, indexBySymbolWith, isAllowedTimeDuration, isArray, isDefined, isEmpty, isError, isFalse, isFunction, isNegativeNumber, isNullOrUndefined, isNullOrUndefinedOrEmpty, isNumber, isPositiveNumber, isString, isTimeInstant, isTrue, isUpgradable, isZero, jsonCloneDeep, last$1 as last, listToDict, mapDefined, mapEntries, mapFirstTruthy, mapTruthys, max, maxBy, min, minBy, multiplyBy, noop, not, omitFromJsonObject, or, pad, padLeft, padRight, parseJson, partition, pick, pipedInvoke, pipedInvokeFromArray, pluralize, promiseSequence, randomId, randomNumberInInterval, range, repeat, reverse$1 as reverse, round, roundAwayFromZero, roundToLower, roundToNearest, roundToUpper, roundTowardsZero, shallowArrayEquals, shallowRecordEquals, sortedArray, splitWords, stringToNumber, stringifyJson, sum, sumBy, tail, throttle, throwIfNullOrUndefined, transformCssDictionary, tryToParseJson, tryToParseNumber, uniq, uniqBy, uniqByKey, unzip, upsert, withTryCatch, withTryCatchAsync, wrapWithString, xor, zip };
|
|
3268
|
+
//# sourceMappingURL=index.mjs.map
|