@muspellheim/shared 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -4
  2. package/package.json +9 -5
  3. package/dist/index.cjs +0 -4455
package/dist/index.cjs DELETED
@@ -1,4455 +0,0 @@
1
- 'use strict';
2
-
3
- var process = require('node:process');
4
- var fsPromises = require('node:fs/promises');
5
- var path = require('node:path');
6
-
7
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
8
-
9
- /**
10
- * Assert that an object is not `null`.
11
- *
12
- * @param {*} object The object to check.
13
- * @param {string|Function} message The message to throw or a function that
14
- * returns the message.
15
- */
16
- function assertNotNull(object, message) {
17
- if (object == null) {
18
- message = typeof message === 'function' ? message() : message;
19
- throw new ReferenceError(message);
20
- }
21
- }
22
-
23
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
24
-
25
- const FACTOR = 0.7;
26
-
27
- /**
28
- * The Color class represents a color in the RGB color space.
29
- */
30
- class Color {
31
- #value;
32
-
33
- /**
34
- * Creates a color instance from RGB values.
35
- *
36
- * @param {number} red The red component or the RGB value.
37
- * @param {number} [green] The green component.
38
- * @param {number} [blue] The blue component.
39
- */
40
- constructor(red, green, blue) {
41
- if (green === undefined && blue === undefined) {
42
- if (typeof red === 'string') {
43
- this.#value = parseInt(red, 16);
44
- return;
45
- }
46
-
47
- this.#value = Number(red);
48
- return;
49
- }
50
-
51
- this.#value = ((red & 0xff) << 16) | ((green & 0xff) << 8) |
52
- ((blue & 0xff) << 0);
53
- }
54
-
55
- /**
56
- * The RGB value of the color.
57
- *
58
- * @type {number}
59
- */
60
- get rgb() {
61
- return this.#value;
62
- }
63
-
64
- /**
65
- * The red component of the color.
66
- *
67
- * @type {number}
68
- */
69
- get red() {
70
- return (this.rgb >> 16) & 0xff;
71
- }
72
-
73
- /**
74
- * The green component of the color.
75
- *
76
- * @type {number}
77
- */
78
- get green() {
79
- return (this.rgb >> 8) & 0xff;
80
- }
81
-
82
- /**
83
- * The blue component of the color.
84
- *
85
- * @type {number}
86
- */
87
- get blue() {
88
- return (this.rgb >> 0) & 0xff;
89
- }
90
-
91
- /**
92
- * Creates a new color that is brighter than this color.
93
- *
94
- * @param {number} [factor] The optional factor to brighten the color.
95
- * @return {Color} The brighter color.
96
- */
97
- brighter(factor = FACTOR) {
98
- if (Number.isNaN(this.rgb)) {
99
- return new Color();
100
- }
101
-
102
- let red = this.red;
103
- let green = this.green;
104
- let blue = this.blue;
105
-
106
- const inverse = Math.floor(1 / (1 - factor));
107
- if (red === 0 && green === 0 && blue === 0) {
108
- return new Color(inverse, inverse, inverse);
109
- }
110
-
111
- if (red > 0 && red < inverse) red = inverse;
112
- if (green > 0 && green < inverse) green = inverse;
113
- if (blue > 0 && blue < inverse) blue = inverse;
114
-
115
- return new Color(
116
- Math.min(Math.floor(red / FACTOR), 255),
117
- Math.min(Math.floor(green / FACTOR), 255),
118
- Math.min(Math.floor(blue / FACTOR), 255),
119
- );
120
- }
121
-
122
- /**
123
- * Creates a new color that is darker than this color.
124
- *
125
- * @param {number} [factor] The optional factor to darken the color.
126
- * @return {Color} The darker color.
127
- */
128
- darker(factor = FACTOR) {
129
- if (Number.isNaN(this.rgb)) {
130
- return new Color();
131
- }
132
-
133
- return new Color(
134
- Math.max(Math.floor(this.red * factor), 0),
135
- Math.max(Math.floor(this.green * factor), 0),
136
- Math.max(Math.floor(this.blue * factor), 0),
137
- );
138
- }
139
-
140
- /**
141
- * Returns the RGB value of the color.
142
- *
143
- * @return {number} The RGB value of the color.
144
- */
145
- valueOf() {
146
- return this.rgb;
147
- }
148
-
149
- /**
150
- * Returns the hexadecimal representation of the color.
151
- *
152
- * @return {string} The hexadecimal representation of the color.
153
- */
154
- toString() {
155
- if (Number.isNaN(this.rgb)) {
156
- return 'Invalid Color';
157
- }
158
-
159
- return this.rgb.toString(16).padStart(6, '0');
160
- }
161
- }
162
-
163
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
164
-
165
- /**
166
- * Handle returning a pre-configured responses.
167
- *
168
- * This is one of the nullability patterns from James Shore's article on
169
- * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#configurable-responses).
170
- *
171
- * Example usage for stubbing `fetch` function:
172
- *
173
- * ```javascript
174
- * function createFetchStub(responses) {
175
- * const configurableResponses = ConfigurableResponses.create(responses);
176
- * return async function () {
177
- * const response = configurableResponses.next();
178
- * return {
179
- * status: response.status,
180
- * json: async () => response.body,
181
- * };
182
- * };
183
- * }
184
- * ```
185
- */
186
- class ConfigurableResponses {
187
- /**
188
- * Creates a configurable responses instance from a single response or an
189
- * array of responses with an optional response name.
190
- *
191
- * @param {*|Array} responses A single response or an array of responses.
192
- * @param {string} [name] An optional name for the responses.
193
- */
194
- static create(responses, name) {
195
- return new ConfigurableResponses(responses, name);
196
- }
197
-
198
- #description;
199
- #responses;
200
-
201
- /**
202
- * Creates a configurable responses instance from a single response or an
203
- * array of responses with an optional response name.
204
- *
205
- * @param {*|Array} responses A single response or an array of responses.
206
- * @param {string} [name] An optional name for the responses.
207
- */
208
- constructor(/** @type {*|Array} */ responses, /** @type {?string} */ name) {
209
- this.#description = name == null ? '' : ` in ${name}`;
210
- this.#responses = Array.isArray(responses) ? [...responses] : responses;
211
- }
212
-
213
- /**
214
- * Returns the next response.
215
- *
216
- * If there are no more responses, an error is thrown. If a single response is
217
- * configured, it is always returned.
218
- *
219
- * @return {*} The next response.
220
- */
221
- next() {
222
- const response = Array.isArray(this.#responses)
223
- ? this.#responses.shift()
224
- : this.#responses;
225
- if (response === undefined) {
226
- throw new Error(`No more responses configured${this.#description}.`);
227
- }
228
-
229
- return response;
230
- }
231
- }
232
-
233
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
234
-
235
- // TODO Use JSON schema to validate like Java Bean Validation?
236
-
237
- class ValidationError extends Error {
238
- constructor(message) {
239
- super(message);
240
- this.name = 'ValidationError';
241
- }
242
- }
243
-
244
- /** @return {never} */
245
- function ensureUnreachable(message = 'Unreachable code executed.') {
246
- throw new Error(message);
247
- }
248
-
249
- function ensureThat(
250
- value,
251
- predicate,
252
- message = 'Expected predicate is not true.',
253
- ) {
254
- const condition = predicate(value);
255
- if (!condition) {
256
- throw new ValidationError(message);
257
- }
258
-
259
- return value;
260
- }
261
-
262
- function ensureAnything(value, { name = 'value' } = {}) {
263
- if (value == null) {
264
- throw new ValidationError(`The ${name} is required, but it was ${value}.`);
265
- }
266
-
267
- return value;
268
- }
269
-
270
- function ensureNonEmpty(value, { name = 'value' } = {}) {
271
- const valueType = getType(value);
272
- if (
273
- (valueType === String && value.length === 0) ||
274
- (valueType === Array && value.length === 0) ||
275
- (valueType === Object && Object.keys(value).length === 0)
276
- ) {
277
- throw new ValidationError(
278
- `The ${name} must not be empty, but it was ${JSON.stringify(value)}.`,
279
- );
280
- }
281
-
282
- return value;
283
- }
284
-
285
- /*
286
- * type: undefined | null | Boolean | Number | BigInt | String | Symbol | Function | Object | Array | Enum | constructor | Record<string, type>
287
- * expectedType: type | [ type ]
288
- */
289
-
290
- function ensureType(value, expectedType, { name = 'value' } = {}) {
291
- const result = checkType(value, expectedType, { name });
292
- if (result.error) {
293
- throw new ValidationError(result.error);
294
- }
295
- return result.value;
296
- }
297
-
298
- function ensureItemType(array, expectedType, { name = 'value' } = {}) {
299
- const result = checkType(array, Array, { name });
300
- if (result.error) {
301
- throw new ValidationError(result.error);
302
- }
303
-
304
- array.forEach((item, index) => {
305
- const result = checkType(item, expectedType, {
306
- name: `${name}.${index}`,
307
- });
308
- if (result.error) {
309
- throw new ValidationError(result.error);
310
- }
311
- array[index] = result.value;
312
- });
313
- return array;
314
- }
315
-
316
- function ensureArguments(args, expectedTypes = [], names = []) {
317
- ensureThat(
318
- expectedTypes,
319
- Array.isArray,
320
- 'The expectedTypes must be an array.',
321
- );
322
- ensureThat(names, Array.isArray, 'The names must be an array.');
323
- if (args.length > expectedTypes.length) {
324
- throw new ValidationError(
325
- `Too many arguments: expected ${expectedTypes.length}, but got ${args.length}.`,
326
- );
327
- }
328
- expectedTypes.forEach((expectedType, index) => {
329
- const name = names[index] ? names[index] : `argument #${index + 1}`;
330
- ensureType(args[index], expectedType, { name });
331
- });
332
- }
333
-
334
- /** @return {{value: ?*, error: ?string}}} */
335
- function checkType(value, expectedType, { name = 'value' } = {}) {
336
- const valueType = getType(value);
337
-
338
- // Check built-in types
339
- if (
340
- expectedType === undefined ||
341
- expectedType === null ||
342
- expectedType === Boolean ||
343
- expectedType === Number ||
344
- expectedType === BigInt ||
345
- expectedType === String ||
346
- expectedType === Symbol ||
347
- expectedType === Function ||
348
- expectedType === Object ||
349
- expectedType === Array
350
- ) {
351
- if (valueType === expectedType) {
352
- return { value };
353
- }
354
-
355
- return {
356
- error: `The ${name} must be ${
357
- describe(expectedType, {
358
- articles: true,
359
- })
360
- }, but it was ${describe(valueType, { articles: true })}.`,
361
- };
362
- }
363
-
364
- // Check enum types
365
- if (Object.getPrototypeOf(expectedType).name === 'Enum') {
366
- try {
367
- return { value: expectedType.valueOf(String(value).toUpperCase()) };
368
- } catch {
369
- return {
370
- error: `The ${name} must be ${
371
- describe(expectedType, {
372
- articles: true,
373
- })
374
- }, but it was ${describe(valueType, { articles: true })}.`,
375
- };
376
- }
377
- }
378
-
379
- // Check constructor types
380
- if (typeof expectedType === 'function') {
381
- if (value instanceof expectedType) {
382
- return { value };
383
- } else {
384
- const convertedValue = new expectedType(value);
385
- if (String(convertedValue).toLowerCase().startsWith('invalid')) {
386
- let error = `The ${name} must be a valid ${
387
- describe(
388
- expectedType,
389
- )
390
- }, but it was ${describe(valueType, { articles: true })}`;
391
- if (valueType != null) {
392
- error += `: ${JSON.stringify(value, { articles: true })}`;
393
- }
394
- error += '.';
395
- return { error };
396
- }
397
-
398
- return { value: convertedValue };
399
- }
400
- }
401
-
402
- // Check one of multiple types
403
- if (Array.isArray(expectedType)) {
404
- for (const type of expectedType) {
405
- const result = checkType(value, type, { name });
406
- if (!result.error) {
407
- return { value };
408
- }
409
- }
410
-
411
- return {
412
- error: `The ${name} must be ${
413
- describe(expectedType, {
414
- articles: true,
415
- })
416
- }, but it was ${describe(valueType, { articles: true })}.`,
417
- };
418
- }
419
-
420
- if (typeof expectedType === 'object') {
421
- // Check struct types
422
- const result = checkType(value, Object, { name });
423
- if (result.error) {
424
- return result;
425
- }
426
-
427
- for (const key in expectedType) {
428
- const result = checkType(value[key], expectedType[key], {
429
- name: `${name}.${key}`,
430
- });
431
- if (result.error) {
432
- return result;
433
- }
434
- value[key] = result.value;
435
- }
436
-
437
- return { value };
438
- }
439
-
440
- ensureUnreachable();
441
- }
442
-
443
- function getType(value) {
444
- if (value === null) {
445
- return null;
446
- }
447
- if (Array.isArray(value)) {
448
- return Array;
449
- }
450
- if (Number.isNaN(value)) {
451
- return NaN;
452
- }
453
-
454
- switch (typeof value) {
455
- case 'undefined':
456
- return undefined;
457
- case 'boolean':
458
- return Boolean;
459
- case 'number':
460
- return Number;
461
- case 'bigint':
462
- return BigInt;
463
- case 'string':
464
- return String;
465
- case 'symbol':
466
- return Symbol;
467
- case 'function':
468
- return Function;
469
- case 'object':
470
- return Object;
471
- default:
472
- ensureUnreachable(`Unknown typeof value: ${typeof value}.`);
473
- }
474
- }
475
-
476
- function describe(type, { articles = false } = {}) {
477
- if (Array.isArray(type)) {
478
- const types = type.map((t) => describe(t, { articles }));
479
- if (types.length <= 2) {
480
- return types.join(' or ');
481
- } else {
482
- const allButLast = types.slice(0, -1);
483
- const last = types.at(-1);
484
- return allButLast.join(', ') + ', or ' + last;
485
- }
486
- }
487
-
488
- if (Number.isNaN(type)) {
489
- return 'NaN';
490
- }
491
-
492
- let name;
493
- switch (type) {
494
- case null:
495
- return 'null';
496
- case undefined:
497
- return 'undefined';
498
- case Array:
499
- name = 'array';
500
- break;
501
- case Boolean:
502
- name = 'boolean';
503
- break;
504
- case Number:
505
- name = 'number';
506
- break;
507
- case BigInt:
508
- name = 'bigint';
509
- break;
510
- case String:
511
- name = 'string';
512
- break;
513
- case Symbol:
514
- name = 'symbol';
515
- break;
516
- case Function:
517
- name = 'function';
518
- break;
519
- case Object:
520
- name = 'object';
521
- break;
522
- default:
523
- name = type.name;
524
- break;
525
- }
526
-
527
- if (articles) {
528
- name = 'aeiou'.includes(name[0].toLowerCase()) ? `an ${name}` : `a ${name}`;
529
- }
530
- return name;
531
- }
532
-
533
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
534
-
535
-
536
- /**
537
- * This is a base class for creating enum objects.
538
- *
539
- * Example:
540
- *
541
- * ```js
542
- * class YesNo extends Enum {
543
- * static YES = new YesNo('YES', 0);
544
- * static NO = new YesNo('NO', 1);
545
- * }
546
- * ```
547
- *
548
- * @template [T=Enum] - the type of the enum object
549
- */
550
- class Enum {
551
- /**
552
- * Returns all enum constants.
553
- *
554
- * @return {T[]} All enum constants.
555
- */
556
- static values() {
557
- return Object.values(this);
558
- }
559
-
560
- /**
561
- * Returns an enum constant by its name.
562
- *
563
- * @param {string} name The name of the enum constant.
564
- * @return {T} The enum constant.
565
- */
566
- static valueOf(name) {
567
- const value = this.values().find((v) => v.name === name);
568
- if (value == null) {
569
- throw new Error(`No enum constant ${this.name}.${name} exists.`);
570
- }
571
-
572
- return value;
573
- }
574
-
575
- /**
576
- * Creates an enum object.
577
- *
578
- * @param {number} ordinal The ordinal of the enum constant.
579
- * @param {string} name The name of the enum constant.
580
- */
581
- constructor(name, ordinal) {
582
- ensureArguments(arguments, [String, Number]);
583
- this.name = name;
584
- this.ordinal = ordinal;
585
- }
586
-
587
- /**
588
- * Returns the name of the enum constant.
589
- *
590
- * @return {string} The name of the enum constant.
591
- */
592
- toString() {
593
- return this.name;
594
- }
595
-
596
- /**
597
- * Returns the ordinal of the enum constant.
598
- *
599
- * @return {number} The ordinal of the enum constant.
600
- */
601
- valueOf() {
602
- return this.ordinal;
603
- }
604
-
605
- /**
606
- * Returns the name of the enum constant.
607
- *
608
- * @return {string} The name of the enum constant.
609
- */
610
- toJSON() {
611
- return this.name;
612
- }
613
- }
614
-
615
- /**
616
- * Temporarily cease execution for the specified duration.
617
- *
618
- * @param {number} millis The duration to sleep in milliseconds.
619
- * @return {Promise<void>} A promise that resolves after the specified
620
- * duration.
621
- */
622
- async function sleep(millis) {
623
- await new Promise((resolve) => setTimeout(resolve, millis));
624
- }
625
-
626
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
627
-
628
- class FeatureToggle {
629
- /*
630
- static isFoobarEnabled() {
631
- return true;
632
- }
633
- */
634
- }
635
-
636
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
637
-
638
-
639
- /**
640
- * Express state of a component.
641
- */
642
- class Status {
643
- /**
644
- * Indicates the component is in an unknown state.
645
- *
646
- * @type {Status}
647
- */
648
- static UNKNOWN = new Status('UNKNOWN');
649
-
650
- /**
651
- * Indicates the component is functioning as expected
652
- *
653
- * @type {Status}
654
- */
655
- static UP = new Status('UP');
656
-
657
- /**
658
- * Indicates the component has suffered an unexpected failure.
659
- *
660
- * @type {Status}
661
- */
662
- static DOWN = new Status('DOWN');
663
-
664
- /**
665
- * Indicates the component has been taken out of service and should not be used.
666
- *
667
- * @type {Status}
668
- */
669
- static OUT_OF_SERVICE = new Status('OUT_OF_SERVICE');
670
-
671
- /**
672
- * Creates a new status.
673
- *
674
- * @param {string} code The status code.
675
- */
676
- constructor(code) {
677
- assertNotNull(code, 'Code must not be null.');
678
- this.code = code;
679
- }
680
-
681
- /**
682
- * Returns a string representation of the status.
683
- *
684
- * @return {string} The status code.
685
- */
686
- toString() {
687
- return this.code;
688
- }
689
-
690
- /**
691
- * Returns the value of the status.
692
- *
693
- * @return {string} The status code.
694
- */
695
- valueOf() {
696
- return this.code;
697
- }
698
-
699
- /**
700
- * Returns the status code.
701
- *
702
- * @return {string} The status code.
703
- */
704
- toJSON() {
705
- return this.code;
706
- }
707
- }
708
-
709
- /**
710
- * Carry information about the health of a component.
711
- */
712
- class Health {
713
- /**
714
- * Creates a new health object with status {@link Status.UNKNOWN}.
715
- *
716
- * @param {object} options The health options.
717
- * @param {Record<string, *>} [options.details] The details of the health.
718
- */
719
- static unknown({ details } = {}) {
720
- return Health.status({ status: Status.UNKNOWN, details });
721
- }
722
-
723
- /**
724
- * Creates a new health object with status {@link Status.UP}.
725
- *
726
- * @param {object} options The health options.
727
- * @param {Record<string, *>} [options.details] The details of the health.
728
- */
729
- static up({ details } = {}) {
730
- return Health.status({ status: Status.UP, details });
731
- }
732
-
733
- /**
734
- * Creates a new health object with status {@link Status.DOWN}.
735
- *
736
- * @param {object} options The health options.
737
- * @param {Record<string, *>} [options.details] The details of the health.
738
- * @param {Error} [options.error] The error of the health.
739
- */
740
- static down({ details, error } = {}) {
741
- return Health.status({ status: Status.DOWN, details, error });
742
- }
743
-
744
- /**
745
- * Creates a new health object with status {@link Status.OUT_OF_SERVICE}.
746
- *
747
- * @param {object} options The health options.
748
- * @param {Record<string, *>} [options.details] The details of the health.
749
- */
750
- static outOfService({ details } = {}) {
751
- return Health.status({ status: Status.OUT_OF_SERVICE, details });
752
- }
753
-
754
- /**
755
- * Creates a new health object.
756
- *
757
- * @param {object} options The health options.
758
- * @param {Status} options.status The status of the health.
759
- * @param {Record<string, *>} [options.details] The details of the health.
760
- * @param {Error} [options.error] The error of the health.
761
- */
762
- static status({ status = Status.UNKNOWN, details, error } = {}) {
763
- if (error) {
764
- details = { ...details, error: `${error.name}: ${error.message}` };
765
- }
766
- return new Health(status, details);
767
- }
768
-
769
- /**
770
- * The status of the health.
771
- *
772
- * @type {Status}
773
- */
774
- status;
775
-
776
- /**
777
- * The details of the health.
778
- *
779
- * @type {?Record<string, *>}
780
- */
781
- details;
782
-
783
- /**
784
- * Creates a new health object.
785
- *
786
- * @param {Status} status The status of the health.
787
- * @param {Record<string, *>} details The details of the health.
788
- */
789
- constructor(
790
- /** @type {Status} */ status,
791
- /** @type {?Record<string, *>} */ details,
792
- ) {
793
- assertNotNull(status, 'Status must not be null.');
794
- // TODO assertNotNull(details, 'Details must not be null.');
795
-
796
- this.status = status;
797
- this.details = details;
798
- }
799
- }
800
-
801
- /**
802
- * A {@link Health} that is composed of other {@link Health} instances.
803
- */
804
- class CompositeHealth {
805
- /**
806
- * The status of the component.
807
- *
808
- * @type {Status}
809
- */
810
- status;
811
-
812
- /**
813
- * The components of the health.
814
- *
815
- * @type {?Record<string, Health|CompositeHealth>}
816
- */
817
- components;
818
-
819
- /**
820
- * Creates a new composite health object.
821
- *
822
- * @param {Status} status The combined status of the components.
823
- * @param {Record<string, Health|CompositeHealth>} [components] The components.
824
- */
825
- constructor(
826
- /** @type {Status} */ status,
827
- /** @type {?Record<string, Health|CompositeHealth>} */ components,
828
- ) {
829
- assertNotNull(status, 'Status must not be null.');
830
-
831
- this.status = status;
832
- this.components = components;
833
- }
834
- }
835
-
836
- /**
837
- * Strategy interface used to contribute {@link Health} to the results returned
838
- * from the {@link HealthEndpoint}.
839
- *
840
- * @typedef {object} HealthIndicator
841
- * @property {function(): Health} health Returns the health of the component.
842
- */
843
-
844
- /**
845
- * A named {@link HealthIndicator}.
846
- *
847
- * @typedef {object} NamedContributor
848
- * @property {string} name The name of the contributor.
849
- * @property {HealthIndicator} contributor The contributor.
850
- */
851
-
852
- /**
853
- * A registry of {@link HealthIndicator} instances.
854
- */
855
- class HealthContributorRegistry {
856
- static #instance = new HealthContributorRegistry();
857
-
858
- /**
859
- * Returns the default registry.
860
- *
861
- * @return {HealthContributorRegistry} The default registry.
862
- */
863
- static getDefault() {
864
- return HealthContributorRegistry.#instance;
865
- }
866
-
867
- #contributors;
868
-
869
- /**
870
- * Creates a new registry.
871
- *
872
- * @param {Map<string, HealthIndicator>} [contributors] The initial
873
- * contributors.
874
- */
875
- constructor(contributors) {
876
- this.#contributors = contributors ?? new Map();
877
- }
878
-
879
- /**
880
- * Registers a contributor.
881
- *
882
- * @param {string} name The name of the contributor.
883
- * @param {HealthIndicator} contributor The contributor.
884
- */
885
- registerContributor(name, contributor) {
886
- this.#contributors.set(name, contributor);
887
- }
888
-
889
- /**
890
- * Unregisters a contributor.
891
- *
892
- * @param {string} name The name of the contributor.
893
- */
894
- unregisterContributor(name) {
895
- this.#contributors.delete(name);
896
- }
897
-
898
- /**
899
- * Returns a contributor by name.
900
- *
901
- * @param {string} name The name of the contributor.
902
- * @return {HealthIndicator} The contributorm or `undefined` if not found.
903
- */
904
- getContributor(name) {
905
- return this.#contributors.get(name);
906
- }
907
-
908
- /**
909
- * Returns an iterator over the named contributors.
910
- *
911
- * @return {IterableIterator<NamedContributor>} The iterator.
912
- */
913
- *[Symbol.iterator]() {
914
- for (const [name, contributor] of this.#contributors) {
915
- yield { name, contributor };
916
- }
917
- }
918
- }
919
-
920
- /**
921
- * Strategy interface used to aggregate multiple {@link Status} instances into a
922
- * single one.
923
- */
924
- class StatusAggregator {
925
- /**
926
- * Returns the default status aggregator.
927
- *
928
- * @return {StatusAggregator} The default status aggregator.
929
- */
930
- static getDefault() {
931
- return SimpleStatusAggregator.INSTANCE;
932
- }
933
-
934
- /**
935
- * Returns the aggregate status of the given statuses.
936
- *
937
- * @param {Status[]} statuses The statuses to aggregate.
938
- * @return {Status} The aggregate status.
939
- * @abstract
940
- */
941
- getAggregateStatus(_statuses) {
942
- throw new Error('Method not implemented.');
943
- }
944
- }
945
-
946
- /**
947
- * A simple {@link StatusAggregator} that uses a predefined order to determine
948
- * the aggregate status.
949
- *
950
- * @extends StatusAggregator
951
- */
952
- class SimpleStatusAggregator extends StatusAggregator {
953
- static #DEFAULT_ORDER = [
954
- Status.DOWN,
955
- Status.OUT_OF_SERVICE,
956
- Status.UP,
957
- Status.UNKNOWN,
958
- ];
959
-
960
- static INSTANCE = new SimpleStatusAggregator();
961
-
962
- #order;
963
-
964
- /**
965
- * Creates a new aggregator.
966
- *
967
- * @param {Status[]} order The order of the statuses.
968
- */
969
- constructor(order = SimpleStatusAggregator.#DEFAULT_ORDER) {
970
- super();
971
- this.#order = order;
972
- }
973
-
974
- /** @override */
975
- getAggregateStatus(statuses) {
976
- if (statuses.length === 0) {
977
- return Status.UNKNOWN;
978
- }
979
-
980
- statuses.sort((a, b) => this.#order.indexOf(a) - this.#order.indexOf(b));
981
- return statuses[0];
982
- }
983
- }
984
-
985
- /**
986
- * Strategy interface used to map {@link Status} instances to HTTP status codes.
987
- */
988
- class HttpCodeStatusMapper {
989
- /**
990
- * Returns the default HTTP code status mapper.
991
- *
992
- * @return {HttpCodeStatusMapper} The default HTTP code status mapper.
993
- */
994
- static getDefault() {
995
- return SimpleHttpCodeStatusMapper.INSTANCE;
996
- }
997
-
998
- /**
999
- * Returns the HTTP status code for the given status.
1000
- *
1001
- * @param {Status} status The status.
1002
- * @return {number} The HTTP status code.
1003
- * @abstract
1004
- */
1005
- getStatusCode(_status) {
1006
- throw new Error('Method not implemented.');
1007
- }
1008
- }
1009
-
1010
- /**
1011
- * A simple {@link HttpCodeStatusMapper} that uses a predefined mapping to
1012
- * determine the HTTP status code.
1013
- *
1014
- * @extends HttpCodeStatusMapper
1015
- */
1016
- class SimpleHttpCodeStatusMapper extends HttpCodeStatusMapper {
1017
- static #DEFAULT_MAPPING = new Map([
1018
- [Status.DOWN, 503],
1019
- [Status.OUT_OF_SERVICE, 503],
1020
- ]);
1021
-
1022
- static INSTANCE = new SimpleHttpCodeStatusMapper();
1023
-
1024
- #mappings;
1025
-
1026
- constructor(mappings = SimpleHttpCodeStatusMapper.#DEFAULT_MAPPING) {
1027
- super();
1028
- this.#mappings = mappings;
1029
- }
1030
-
1031
- /** @override */
1032
- getStatusCode(/** @type {Status} */ status) {
1033
- return this.#mappings.get(status) ?? 200;
1034
- }
1035
- }
1036
-
1037
- /**
1038
- * A logical grouping of health contributors that can be exposed by the
1039
- * {@link HealthEndpoint}.
1040
- *
1041
- * @typedef {object} HealthEndpointGroup
1042
- * @property {StatusAggregator} statusAggregator The status aggregator.
1043
- * @property {HttpCodeStatusMapper} httpCodeStatusMapper The HTTP code status
1044
- * mapper.
1045
- */
1046
-
1047
- /**
1048
- * A collection of groups for use with a health endpoint.
1049
- *
1050
- * @typedef {object} HealthEndpointGroups
1051
- * @property {HealthEndpointGroup} primary The primary group.
1052
- */
1053
-
1054
- /**
1055
- * Returned by an operation to provide addtional, web-specific information such
1056
- * as the HTTP status code.
1057
- *
1058
- * @typedef {object} EndpointResponse
1059
- * @property {number} status The HTTP status code.
1060
- * @property {Health | CompositeHealth} body The response body.
1061
- */
1062
-
1063
- /**
1064
- * A health endpoint that provides information about the health of the
1065
- * application.
1066
- */
1067
- class HealthEndpoint {
1068
- static ID = 'health';
1069
-
1070
- static #INSTANCE = new HealthEndpoint(
1071
- HealthContributorRegistry.getDefault(),
1072
- {
1073
- primary: {
1074
- statusAggregator: StatusAggregator.getDefault(),
1075
- httpCodeStatusMapper: HttpCodeStatusMapper.getDefault(),
1076
- },
1077
- },
1078
- );
1079
-
1080
- /**
1081
- * Returns the default health endpoint.
1082
- *
1083
- * @return {HealthEndpoint} The default health endpoint.
1084
- */
1085
- static getDefault() {
1086
- return HealthEndpoint.#INSTANCE;
1087
- }
1088
-
1089
- #registry;
1090
- #groups;
1091
-
1092
- /**
1093
- * Creates a new health endpoint.
1094
- *
1095
- * @param {HealthContributorRegistry} registry The health contributor
1096
- * registry.
1097
- * @param {HealthEndpointGroups} groups The health groups.
1098
- */
1099
- constructor(/** @type {HealthContributorRegistry} */ registry, groups) {
1100
- assertNotNull(registry, 'Registry must not be null.');
1101
- assertNotNull(groups, 'Groups must not be null.');
1102
- this.#registry = registry;
1103
- this.#groups = groups;
1104
- }
1105
-
1106
- /**
1107
- * Returns the health of the application.
1108
- *
1109
- * @return {EndpointResponse} The health response.
1110
- */
1111
- health() {
1112
- const result = this.#getHealth();
1113
- const health = result.health;
1114
- const status = result.group.httpCodeStatusMapper.getStatusCode(
1115
- health.status,
1116
- );
1117
- return { status, body: health };
1118
- }
1119
-
1120
- #getHealth() {
1121
- const statuses = [];
1122
- const components = {};
1123
- for (const { name, contributor } of this.#registry) {
1124
- components[name] = contributor.health();
1125
- statuses.push(components[name].status);
1126
- }
1127
-
1128
- let health;
1129
- if (statuses.length > 0) {
1130
- const status = this.#groups.primary.statusAggregator.getAggregateStatus(
1131
- statuses,
1132
- );
1133
- health = new CompositeHealth(status, components);
1134
- } else {
1135
- health = Health.up();
1136
- }
1137
- return { health, group: this.#groups.primary };
1138
- }
1139
- }
1140
-
1141
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
1142
-
1143
- /**
1144
- * Tracks output events.
1145
- *
1146
- * This is one of the nullability patterns from James Shore's article on
1147
- * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#output-tracking).
1148
- *
1149
- * Example implementation of an event store:
1150
- *
1151
- * ```javascript
1152
- * async record(event) {
1153
- * // ...
1154
- * this.dispatchEvent(new CustomEvent(EVENT_RECORDED_EVENT, { detail: event }));
1155
- * }
1156
- *
1157
- * trackEventsRecorded() {
1158
- * return new OutputTracker(this, EVENT_RECORDED_EVENT);
1159
- * }
1160
- * ```
1161
- *
1162
- * Example usage:
1163
- *
1164
- * ```javascript
1165
- * const eventsRecorded = eventStore.trackEventsRecorded();
1166
- * // ...
1167
- * const data = eventsRecorded.data(); // [event1, event2, ...]
1168
- * ```
1169
- */
1170
- class OutputTracker {
1171
- /**
1172
- * Creates a tracker for a specific event of an event target.
1173
- *
1174
- * @param {EventTarget} eventTarget The event target to track.
1175
- * @param {string} event The event name to track.
1176
- */
1177
- static create(eventTarget, event) {
1178
- return new OutputTracker(eventTarget, event);
1179
- }
1180
-
1181
- #eventTarget;
1182
- #event;
1183
- #tracker;
1184
- #data = [];
1185
-
1186
- /**
1187
- * Creates a tracker for a specific event of an event target.
1188
- *
1189
- * @param {EventTarget} eventTarget The event target to track.
1190
- * @param {string} event The event name to track.
1191
- */
1192
- constructor(
1193
- /** @type {EventTarget} */ eventTarget,
1194
- /** @type {string} */ event,
1195
- ) {
1196
- this.#eventTarget = eventTarget;
1197
- this.#event = event;
1198
- this.#tracker = (event) => this.#data.push(event.detail);
1199
-
1200
- this.#eventTarget.addEventListener(this.#event, this.#tracker);
1201
- }
1202
-
1203
- /**
1204
- * Returns the tracked data.
1205
- *
1206
- * @return {Array} The tracked data.
1207
- */
1208
- get data() {
1209
- return this.#data;
1210
- }
1211
-
1212
- /**
1213
- * Clears the tracked data and returns the cleared data.
1214
- *
1215
- * @return {Array} The cleared data.
1216
- */
1217
- clear() {
1218
- const result = [...this.#data];
1219
- this.#data.length = 0;
1220
- return result;
1221
- }
1222
-
1223
- /**
1224
- * Stops tracking.
1225
- */
1226
- stop() {
1227
- this.#eventTarget.removeEventListener(this.#event, this.#tracker);
1228
- }
1229
- }
1230
-
1231
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
1232
-
1233
-
1234
- const MESSAGE_LOGGED_EVENT = 'message-logged';
1235
-
1236
- /**
1237
- * Define a set of standard logging levels that can be used to control logging
1238
- * output.
1239
- */
1240
- class Level {
1241
- static #levels = [];
1242
-
1243
- /**
1244
- * `OFF` is a special level that can be used to turn off logging.
1245
- *
1246
- * @type {Level}
1247
- */
1248
- static OFF = new Level('OFF', Number.MAX_SAFE_INTEGER);
1249
-
1250
- /**
1251
- * `ERROR` is a message level indicating a serious failure.
1252
- *
1253
- * @type {Level}
1254
- */
1255
- static ERROR = new Level('ERROR', 1000);
1256
-
1257
- /**
1258
- * `WARNING` is a message level indicating a potential problem.
1259
- *
1260
- * @type {Level}
1261
- */
1262
- static WARNING = new Level('WARNING', 900);
1263
-
1264
- /**
1265
- * `INFO` is a message level for informational messages.
1266
- *
1267
- * @type {Level}
1268
- */
1269
- static INFO = new Level('INFO', 800);
1270
-
1271
- /**
1272
- * `DEBUG` is a message level providing tracing information.
1273
- *
1274
- * @type {Level}
1275
- */
1276
- static DEBUG = new Level('DEBUG', 700);
1277
-
1278
- /**
1279
- * `TRACE` is a message level providing fine-grained tracing information.
1280
- *
1281
- * @type {Level}
1282
- */
1283
- static TRACE = new Level('TRACE', 600);
1284
-
1285
- /**
1286
- * `ALL` indicates that all messages should be logged.
1287
- *
1288
- * @type {Level}
1289
- */
1290
- static ALL = new Level('ALL', Number.MIN_SAFE_INTEGER);
1291
-
1292
- /**
1293
- * Parses a level string or number into a Level.
1294
- *
1295
- * For example:
1296
- * - "ERROR"
1297
- * - "1000"
1298
- *
1299
- * @param {string|number} name The name or value of the level.
1300
- * @return The parsed value.
1301
- */
1302
- static parse(name) {
1303
- const level = Level.#levels.find(
1304
- (level) => level.name === String(name) || level.value === Number(name),
1305
- );
1306
- if (level == null) {
1307
- throw new Error(`Bad log level "${name}".`);
1308
- }
1309
-
1310
- return level;
1311
- }
1312
-
1313
- /**
1314
- * The name of the level.
1315
- *
1316
- * @type {string}
1317
- */
1318
- name;
1319
-
1320
- /**
1321
- * The value of the level.
1322
- *
1323
- * @type {number}
1324
- */
1325
- value;
1326
-
1327
- /**
1328
- * Initializes a new level and registers it.
1329
- *
1330
- * @param {string} name The name of the level.
1331
- * @param {number} value The value of the level.
1332
- */
1333
- constructor(name, value) {
1334
- this.name = name;
1335
- this.value = value;
1336
- Level.#levels.push(this);
1337
- }
1338
-
1339
- /**
1340
- * Returns a string representation of the level.
1341
- *
1342
- * @return {string} The name of the level.
1343
- */
1344
- toString() {
1345
- return this.name;
1346
- }
1347
-
1348
- /**
1349
- * Returns the value of the level.
1350
- *
1351
- * @return {number} The value of the level.
1352
- */
1353
- valueOf() {
1354
- return this.value;
1355
- }
1356
-
1357
- /**
1358
- * Returns the name of the level.
1359
- *
1360
- * @return {string} The name of the level.
1361
- */
1362
- toJSON() {
1363
- return this.name;
1364
- }
1365
- }
1366
-
1367
- /**
1368
- * A `Logger` object is used to log messages for a specific system or
1369
- * application component.
1370
- */
1371
- class Logger extends EventTarget {
1372
- /**
1373
- * Finds or creates a logger with the given name.
1374
- *
1375
- * @param {string} name The name of the logger.
1376
- * @return {Logger} The logger.
1377
- */
1378
- static getLogger(name) {
1379
- const manager = LogManager.getLogManager();
1380
- return manager.demandLogger(name);
1381
- }
1382
-
1383
- /**
1384
- * Creates a new logger without any handlers.
1385
- *
1386
- * @param {Object} options The options for the logger.
1387
- * @param {Level} options.level The level of the logger.
1388
- * @return {Logger} The logger.
1389
- */
1390
- static getAnonymousLogger() {
1391
- const manager = LogManager.getLogManager();
1392
- const logger = new Logger(null);
1393
- logger.parent = manager.getLogger('');
1394
- return logger;
1395
- }
1396
-
1397
- /**
1398
- * The parent logger.
1399
- *
1400
- * The root logger has not a parent.
1401
- *
1402
- * @type {?Logger}
1403
- */
1404
- parent;
1405
-
1406
- /**
1407
- * The level of the logger.
1408
- *
1409
- * If the level is not set, it will use the level of the parent logger.
1410
- *
1411
- * @type {?Level}
1412
- */
1413
- level;
1414
-
1415
- /**
1416
- * @type {Handler[]}
1417
- */
1418
- #handlers = [];
1419
-
1420
- #name;
1421
-
1422
- /**
1423
- * Initializes a new logger with the given name.
1424
- *
1425
- * @param {string} name The name of the logger.
1426
- * @private
1427
- */
1428
- constructor(name) {
1429
- super();
1430
- this.#name = name;
1431
- }
1432
-
1433
- /**
1434
- * The name of the logger.
1435
- *
1436
- * @type {string}
1437
- */
1438
- get name() {
1439
- return this.#name;
1440
- }
1441
-
1442
- /**
1443
- * Logs a message with the `ERROR` level.
1444
- *
1445
- * @param {...*} message The message to log.
1446
- */
1447
- error(...message) {
1448
- this.log(Level.ERROR, ...message);
1449
- }
1450
-
1451
- /**
1452
- * Logs a message with the `WARNING` level.
1453
- *
1454
- * @param {...*} message The message to log.
1455
- */
1456
- warning(...message) {
1457
- this.log(Level.WARNING, ...message);
1458
- }
1459
-
1460
- /**
1461
- * Logs a message with the `INFO` level.
1462
- *
1463
- * @param {...*} message The message to log.
1464
- */
1465
- info(...message) {
1466
- this.log(Level.INFO, ...message);
1467
- }
1468
-
1469
- /**
1470
- * Logs a message with the `DEBUG` level.
1471
- *
1472
- * @param {...*} message The message to log.
1473
- */
1474
- debug(...message) {
1475
- this.log(Level.DEBUG, ...message);
1476
- }
1477
-
1478
- /**
1479
- * Logs a message with the `TRACE` level.
1480
- *
1481
- * @param {...*} message The message to log.
1482
- */
1483
-
1484
- trace(...message) {
1485
- this.log(Level.TRACE, ...message);
1486
- }
1487
- /**
1488
- * Logs a message.
1489
- *
1490
- * @param {Level} level The level of the message.
1491
- * @param {...*} message The message to log.
1492
- */
1493
- log(level, ...message) {
1494
- if (!this.isLoggable(level)) {
1495
- return;
1496
- }
1497
-
1498
- const record = new LogRecord(level, ...message);
1499
- record.loggerName = this.name;
1500
- this.#handlers.forEach((handler) => handler.publish(record));
1501
- let logger = this.parent;
1502
- while (logger != null) {
1503
- logger.#handlers.forEach((handler) => handler.publish(record));
1504
- logger = logger.parent;
1505
- }
1506
- this.dispatchEvent(
1507
- new CustomEvent(MESSAGE_LOGGED_EVENT, { detail: record }),
1508
- );
1509
- }
1510
-
1511
- /**
1512
- * Returns an output tracker for messages logged by this logger.
1513
- *
1514
- * @return {OutputTracker} The output tracker.
1515
- */
1516
- trackMessagesLogged() {
1517
- return new OutputTracker(this, MESSAGE_LOGGED_EVENT);
1518
- }
1519
-
1520
- /**
1521
- * Checks if a message of the given level would actually be logged by this
1522
- * logger.
1523
- *
1524
- * @param {Level} level The level to check.
1525
- * @return {boolean} `true` if the message would be logged.
1526
- */
1527
- isLoggable(level) {
1528
- return this.level != null
1529
- ? level >= this.level
1530
- : this.parent.isLoggable(level);
1531
- }
1532
-
1533
- /**
1534
- * Adds a log handler to receive logging messages.
1535
- *
1536
- * @param {Handler} handler The handler to add.
1537
- */
1538
- addHandler(handler) {
1539
- this.#handlers.push(handler);
1540
- }
1541
-
1542
- /**
1543
- * Removes a log handler.
1544
- *
1545
- * @param {Handler} handler The handler to remove.
1546
- */
1547
- removeHandler(handler) {
1548
- this.#handlers = this.#handlers.filter((h) => h !== handler);
1549
- }
1550
-
1551
- /**
1552
- * Returns the handlers of the logger.
1553
- *
1554
- * @return {Handler[]} The handlers of the logger.
1555
- */
1556
- getHandlers() {
1557
- return Array.from(this.#handlers);
1558
- }
1559
- }
1560
-
1561
- /**
1562
- * A `LogRecord` object is used to pass logging requests between the logging
1563
- * framework and individual log handlers.
1564
- */
1565
- class LogRecord {
1566
- static #globalSequenceNumber = 1;
1567
-
1568
- /**
1569
- * The timestamp when the log record was created.
1570
- *
1571
- * @type {Date}
1572
- */
1573
- date;
1574
-
1575
- /**
1576
- * The sequence number of the log record.
1577
- *
1578
- * @type {number}
1579
- */
1580
- sequenceNumber;
1581
-
1582
- /**
1583
- * The log level.
1584
- *
1585
- * @type {Level}
1586
- */
1587
- level;
1588
-
1589
- /**
1590
- * The log message.
1591
- *
1592
- * @type {Array}
1593
- */
1594
- message;
1595
-
1596
- /**
1597
- * The name of the logger.
1598
- *
1599
- * @type {string|undefined}
1600
- */
1601
- loggerName;
1602
-
1603
- /**
1604
- * Initializes a new log record.
1605
- *
1606
- * @param {Level} level The level of the log record.
1607
- * @param {...*} message The message to log.
1608
- */
1609
- constructor(level, ...message) {
1610
- this.date = new Date();
1611
- this.sequenceNumber = LogRecord.#globalSequenceNumber++;
1612
- this.level = level;
1613
- this.message = message;
1614
- }
1615
-
1616
- /**
1617
- * Returns the timestamp of the log record in milliseconds.
1618
- *
1619
- * @type {number}
1620
- * @readonly
1621
- */
1622
- get millis() {
1623
- return this.date.getTime();
1624
- }
1625
- }
1626
-
1627
- /**
1628
- * A `Handler` object takes log messages from a Logger and exports them.
1629
- */
1630
- class Handler {
1631
- /**
1632
- * The log level which messages will be logged by this `Handler`.
1633
- *
1634
- * @type {Level}
1635
- */
1636
- level = Level.ALL;
1637
-
1638
- /**
1639
- * The formatter used to format log records.
1640
- *
1641
- * @type {Formatter}
1642
- */
1643
- formatter;
1644
-
1645
- /**
1646
- * Publishes a `LogRecord`.
1647
- *
1648
- * @param {LogRecord} record The log record to publish.
1649
- * @abstract
1650
- */
1651
- async publish() {
1652
- await Promise.reject('Not implemented');
1653
- }
1654
-
1655
- /**
1656
- * Checks if this handler would actually log a given `LogRecord`.
1657
- *
1658
- * @param {Level} level The level to check.
1659
- * @return {boolean} `true` if the message would be logged.
1660
- */
1661
- isLoggable(level) {
1662
- return level >= this.level;
1663
- }
1664
- }
1665
-
1666
- /**
1667
- * A `Handler` that writes log messages to the console.
1668
- *
1669
- * @extends Handler
1670
- */
1671
- class ConsoleHandler extends Handler {
1672
- /** @override */
1673
- async publish(/** @type {LogRecord} */ record) {
1674
- if (!this.isLoggable(record.level)) {
1675
- return;
1676
- }
1677
-
1678
- const message = this.formatter.format(record);
1679
- switch (record.level) {
1680
- case Level.ERROR:
1681
- console.error(message);
1682
- break;
1683
- case Level.WARNING:
1684
- console.warn(message);
1685
- break;
1686
- case Level.INFO:
1687
- console.info(message);
1688
- break;
1689
- case Level.DEBUG:
1690
- console.debug(message);
1691
- break;
1692
- case Level.TRACE:
1693
- console.trace(message);
1694
- break;
1695
- }
1696
-
1697
- await Promise.resolve();
1698
- }
1699
- }
1700
-
1701
- /**
1702
- * A `Formatter` provides support for formatting log records.
1703
- */
1704
- class Formatter {
1705
- /**
1706
- * Formats the given log record and return the formatted string.
1707
- *
1708
- * @param {LogRecord} record The log record to format.
1709
- * @return {string} The formatted log record.
1710
- * @abstract
1711
- */
1712
- format() {
1713
- throw new Error('Not implemented');
1714
- }
1715
- }
1716
-
1717
- /**
1718
- * Print a brief summary of the `LogRecord` in a human readable format.
1719
- *
1720
- * @implements {Formatter}
1721
- */
1722
- class SimpleFormatter extends Formatter {
1723
- /** @override */
1724
- format(/** @type {LogRecord} */ record) {
1725
- let s = record.date.toISOString();
1726
- if (record.loggerName) {
1727
- s += ' [' + record.loggerName + ']';
1728
- }
1729
- s += ' ' + record.level.toString();
1730
- s += ' - ' +
1731
- record.message
1732
- .map((m) => (typeof m === 'object' ? JSON.stringify(m) : m))
1733
- .join(' ');
1734
- return s;
1735
- }
1736
- }
1737
-
1738
- /**
1739
- * Format a `LogRecord` into a JSON object.
1740
- *
1741
- * The JSON object has the following properties:
1742
- * - `date`: string
1743
- * - `millis`: number
1744
- * - `sequence`: number
1745
- * - `logger`: string (optional)
1746
- * - `level`: string
1747
- * - `message`: string
1748
- *
1749
- * @implements {Formatter}
1750
- */
1751
- class JsonFormatter extends Formatter {
1752
- /** @override */
1753
- format(/** @type {LogRecord} */ record) {
1754
- const data = {
1755
- date: record.date.toISOString(),
1756
- millis: record.millis,
1757
- sequence: record.sequenceNumber,
1758
- logger: record.loggerName,
1759
- level: record.level.toString(),
1760
- message: record.message
1761
- .map((m) => (typeof m === 'object' ? JSON.stringify(m) : m))
1762
- .join(' '),
1763
- };
1764
- return JSON.stringify(data);
1765
- }
1766
- }
1767
-
1768
- class LogManager {
1769
- /** @type {LogManager} */ static #logManager;
1770
-
1771
- /** @type {Map<string, Logger>} */ #namedLoggers = new Map();
1772
- /** @type {Logger} */ #rootLogger;
1773
-
1774
- static getLogManager() {
1775
- if (!LogManager.#logManager) {
1776
- LogManager.#logManager = new LogManager();
1777
- }
1778
-
1779
- return LogManager.#logManager;
1780
- }
1781
-
1782
- constructor() {
1783
- this.#rootLogger = this.#createRootLogger();
1784
- }
1785
-
1786
- demandLogger(/** @type {string} */ name) {
1787
- let logger = this.getLogger(name);
1788
- if (logger == null) {
1789
- logger = this.#createLogger(name);
1790
- }
1791
- return logger;
1792
- }
1793
-
1794
- addLogger(/** @type {Logger} */ logger) {
1795
- this.#namedLoggers.set(logger.name, logger);
1796
- }
1797
-
1798
- getLogger(/** @type {string} */ name) {
1799
- return this.#namedLoggers.get(name);
1800
- }
1801
-
1802
- #createRootLogger() {
1803
- const logger = new Logger('');
1804
- logger.level = Level.INFO;
1805
- const handler = new ConsoleHandler();
1806
- handler.formatter = new SimpleFormatter();
1807
- logger.addHandler(handler);
1808
- this.addLogger(logger);
1809
- return logger;
1810
- }
1811
-
1812
- #createLogger(/** @type {string} */ name) {
1813
- const logger = new Logger(name);
1814
- logger.parent = this.#rootLogger;
1815
- this.addLogger(logger);
1816
- return logger;
1817
- }
1818
- }
1819
-
1820
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
1821
-
1822
- /**
1823
- * @import { LongPollingClient } from './long-polling-client.js'
1824
- * @import { SseClient } from './sse-client.js'
1825
- * @import { WebSocketClient } from './web-socket-client.js'
1826
- */
1827
-
1828
- /**
1829
- * An interface for a streaming message client.
1830
- *
1831
- * Emits the following events:
1832
- *
1833
- * - open, {@link Event}
1834
- * - message, {@link MessageEvent}
1835
- * - error, {@link Event}
1836
- *
1837
- * It is used for wrappers around {@link EventSource} and {@link WebSocket},
1838
- * also for long polling.
1839
- *
1840
- * @interface
1841
- * @see SseClient
1842
- * @see WebSocketClient
1843
- * @see LongPollingClient
1844
- */
1845
- class MessageClient extends EventTarget {
1846
- /**
1847
- * Returns whether the client is connected.
1848
- *
1849
- * @type {boolean}
1850
- * @readonly
1851
- */
1852
- get isConnected() {
1853
- throw new Error('Not implemented.');
1854
- }
1855
-
1856
- /**
1857
- * Returns the server URL.
1858
- *
1859
- * @type {string}
1860
- * @readonly
1861
- */
1862
- get url() {
1863
- throw new Error('Not implemented.');
1864
- }
1865
-
1866
- /**
1867
- * Connects to the server.
1868
- *
1869
- * @param {URL | string} url The server URL to connect to.
1870
- */
1871
- async connect(_url) {
1872
- await Promise.reject('Not implemented.');
1873
- }
1874
-
1875
- /**
1876
- * Sends a message to the server.
1877
- *
1878
- * This is an optional method for streams with bidirectional communication.
1879
- *
1880
- * @param {string} message The message to send.
1881
- * @param {string} type The optional message type.
1882
- */
1883
- async send(_message, _type) {
1884
- await Promise.reject('Not implemented.');
1885
- }
1886
-
1887
- /**
1888
- * Closes the connection.
1889
- */
1890
- async close() {
1891
- await Promise.reject('Not implemented.');
1892
- }
1893
- }
1894
-
1895
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
1896
-
1897
-
1898
- const REQUEST_SENT_EVENT = 'request-sent';
1899
-
1900
- /**
1901
- * A client handling long polling a HTTP request.
1902
- *
1903
- * @implements {MessageClient}
1904
- */
1905
- class LongPollingClient extends MessageClient {
1906
- /**
1907
- * Creates a long polling client.
1908
- *
1909
- * @param {object} options
1910
- * @param {number} [options.wait=90000] The wait interval for a response.
1911
- * @param {number} [options.retry=1000] The retry interval after an error.
1912
- * @return {LongPollingClient} A new long polling client.
1913
- */
1914
- static create({ wait = 90000, retry = 1000 } = {}) {
1915
- return new LongPollingClient(
1916
- wait,
1917
- retry,
1918
- globalThis.fetch.bind(globalThis),
1919
- );
1920
- }
1921
-
1922
- /**
1923
- * Creates a nulled long polling client.
1924
- *
1925
- * @param {object} options
1926
- * @return {LongPollingClient} A new nulled long polling client.
1927
- */
1928
- static createNull(
1929
- {
1930
- fetchResponse = {
1931
- status: 304,
1932
- statusText: 'Not Modified',
1933
- headers: undefined,
1934
- body: null,
1935
- },
1936
- } = {},
1937
- ) {
1938
- return new LongPollingClient(90000, 0, createFetchStub(fetchResponse));
1939
- }
1940
-
1941
- #wait;
1942
- #retry;
1943
- #fetch;
1944
- #connected;
1945
- #aboutController;
1946
- #url;
1947
- #tag;
1948
-
1949
- /**
1950
- * The constructor is for internal use. Use the factory methods instead.
1951
- *
1952
- * @see LongPollingClient.create
1953
- * @see LongPollingClient.createNull
1954
- */
1955
- constructor(
1956
- /** @type {number} */ wait,
1957
- /** @type {number} */ retry,
1958
- /** @type {fetch} */ fetchFunc,
1959
- ) {
1960
- super();
1961
- this.#wait = wait;
1962
- this.#retry = retry;
1963
- this.#fetch = fetchFunc;
1964
- this.#connected = false;
1965
- this.#aboutController = new AbortController();
1966
- }
1967
-
1968
- get isConnected() {
1969
- return this.#connected;
1970
- }
1971
-
1972
- get url() {
1973
- return this.#url;
1974
- }
1975
-
1976
- async connect(url) {
1977
- if (this.isConnected) {
1978
- throw new Error('Already connected.');
1979
- }
1980
-
1981
- this.#url = url;
1982
- this.#startPolling();
1983
- this.dispatchEvent(new Event('open'));
1984
- await Promise.resolve();
1985
- }
1986
-
1987
- /**
1988
- * Returns a tracker for requests sent.
1989
- *
1990
- * @return {OutputTracker} A new output tracker.
1991
- */
1992
- trackRequestSent() {
1993
- return OutputTracker.create(this, REQUEST_SENT_EVENT);
1994
- }
1995
-
1996
- async close() {
1997
- this.#aboutController.abort();
1998
- this.#connected = false;
1999
- await Promise.resolve();
2000
- }
2001
-
2002
- async #startPolling() {
2003
- this.#connected = true;
2004
- while (this.isConnected) {
2005
- try {
2006
- const headers = { Prefer: `wait=${this.#wait / 1000}` };
2007
- if (this.#tag) {
2008
- headers['If-None-Match'] = this.#tag;
2009
- }
2010
- this.dispatchEvent(
2011
- new CustomEvent(REQUEST_SENT_EVENT, { detail: { headers } }),
2012
- );
2013
- const response = await this.#fetch(this.#url, {
2014
- headers,
2015
- signal: this.#aboutController.signal,
2016
- });
2017
- await this.#handleResponse(response);
2018
- } catch (error) {
2019
- if (error.name === 'AbortError') {
2020
- break;
2021
- } else {
2022
- await this.#handleError(error);
2023
- }
2024
- }
2025
- }
2026
- }
2027
-
2028
- async #handleResponse(/** @type {Response} */ response) {
2029
- if (response.status === 304) {
2030
- return;
2031
- }
2032
-
2033
- if (!response.ok) {
2034
- throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
2035
- }
2036
-
2037
- this.#tag = response.headers.get('ETag');
2038
- const message = await response.text();
2039
- this.dispatchEvent(new MessageEvent('message', { data: message }));
2040
- }
2041
-
2042
- async #handleError(error) {
2043
- console.error(error);
2044
- this.dispatchEvent(new Event('error'));
2045
- await sleep(this.#retry);
2046
- }
2047
- }
2048
-
2049
- function createFetchStub(response) {
2050
- const responses = ConfigurableResponses.create(response);
2051
- return async (_url, options) => {
2052
- await sleep(0);
2053
- return new Promise((resolve, reject) => {
2054
- options?.signal?.addEventListener('abort', () => reject());
2055
- const res = responses.next();
2056
- resolve(
2057
- new Response(res.body, {
2058
- status: res.status,
2059
- statusText: res.statusText,
2060
- headers: res.headers,
2061
- }),
2062
- );
2063
- });
2064
- };
2065
- }
2066
-
2067
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2068
-
2069
-
2070
- // TODO Create gauges (can increment and decrement)
2071
- // TODO Create timers (total time, average time and count)
2072
- // TODO Publish metrics to a server via HTTP
2073
- // TODO Provide metrics for Prometheus
2074
-
2075
- class MeterRegistry {
2076
- static create() {
2077
- return new MeterRegistry();
2078
- }
2079
-
2080
- #meters = [];
2081
-
2082
- get meters() {
2083
- return this.#meters;
2084
- }
2085
-
2086
- counter(name, tags) {
2087
- const id = MeterId.create({ name, tags, type: MeterType.COUNTER });
2088
- /** @type {Counter} */ let meter = this.#meters.find((meter) =>
2089
- meter.id.equals(id)
2090
- );
2091
- if (!meter) {
2092
- meter = new Counter(id);
2093
- this.#meters.push(meter);
2094
- }
2095
-
2096
- // TODO validate found meter is a counter
2097
- return meter;
2098
- }
2099
- }
2100
-
2101
- class Meter {
2102
- #id;
2103
-
2104
- constructor(/** @type {MeterId} */ id) {
2105
- // TODO validate parameters are not null
2106
- this.#id = id;
2107
- }
2108
-
2109
- get id() {
2110
- return this.#id;
2111
- }
2112
- }
2113
-
2114
- class Counter extends Meter {
2115
- #count = 0;
2116
-
2117
- constructor(/** @type {MeterId} */ id) {
2118
- super(id);
2119
- // TODO validate type is counter
2120
- }
2121
-
2122
- count() {
2123
- return this.#count;
2124
- }
2125
-
2126
- increment(amount = 1) {
2127
- this.#count += amount;
2128
- }
2129
- }
2130
-
2131
- class MeterId {
2132
- static create({ name, tags = [], type }) {
2133
- return new MeterId(name, tags, type);
2134
- }
2135
-
2136
- #name;
2137
- #tags;
2138
- #type;
2139
-
2140
- constructor(
2141
- /** @type {string} */ name,
2142
- /** @type {string[]} */ tags,
2143
- /** @type {MeterType} */ type,
2144
- ) {
2145
- // TODO validate parameters are not null
2146
- this.#name = name;
2147
- this.#tags = Array.from(tags).sort();
2148
- this.#type = type;
2149
- }
2150
-
2151
- get name() {
2152
- return this.#name;
2153
- }
2154
-
2155
- get tags() {
2156
- return this.#tags;
2157
- }
2158
-
2159
- get type() {
2160
- return this.#type;
2161
- }
2162
-
2163
- equals(other) {
2164
- return (
2165
- this.name === other.name &&
2166
- this.tags.length === other.tags.length &&
2167
- this.tags.every((tag, index) => tag === other.tags[index])
2168
- );
2169
- }
2170
- }
2171
-
2172
- class MeterType extends Enum {
2173
- static COUNTER = new MeterType('COUNTER', 0);
2174
- static GAUGE = new MeterType('GAUGE', 1);
2175
- static TIMER = new MeterType('TIMER', 2);
2176
- }
2177
-
2178
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2179
-
2180
- /**
2181
- * A central place to register and resolve services.
2182
- */
2183
- class ServiceLocator {
2184
- static #instance = new ServiceLocator();
2185
-
2186
- /**
2187
- * Gets the default service locator.
2188
- *
2189
- * @return {ServiceLocator} The default service locator.
2190
- */
2191
- static getDefault() {
2192
- return ServiceLocator.#instance;
2193
- }
2194
-
2195
- #services = new Map();
2196
-
2197
- /**
2198
- * Registers a service with name.
2199
- *
2200
- * @param {string} name The name of the service.
2201
- * @param {object|Function} service The service object or constructor.
2202
- */
2203
- register(name, service) {
2204
- this.#services.set(name, service);
2205
- }
2206
-
2207
- /**
2208
- * Resolves a service by name.
2209
- *
2210
- * @param {string} name The name of the service.
2211
- * @return {object} The service object.
2212
- */
2213
- resolve(name) {
2214
- const service = this.#services.get(name);
2215
- if (service == null) {
2216
- throw new Error(`Service not found: ${name}.`);
2217
- }
2218
-
2219
- return typeof service === 'function' ? service() : service;
2220
- }
2221
- }
2222
-
2223
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2224
-
2225
-
2226
- /**
2227
- * A client for the server-sent events protocol.
2228
- *
2229
- * @implements {MessageClient}
2230
- */
2231
- class SseClient extends MessageClient {
2232
- /**
2233
- * Creates a SSE client.
2234
- *
2235
- * @return {SseClient} A new SSE client.
2236
- */
2237
- static create() {
2238
- return new SseClient(EventSource);
2239
- }
2240
-
2241
- /**
2242
- * Creates a nulled SSE client.
2243
- *
2244
- * @return {SseClient} A new SSE client.
2245
- */
2246
- static createNull() {
2247
- return new SseClient(EventSourceStub);
2248
- }
2249
-
2250
- #eventSourceConstructor;
2251
- /** @type {EventSource} */ #eventSource;
2252
-
2253
- /**
2254
- * The constructor is for internal use. Use the factory methods instead.
2255
- *
2256
- * @see SseClient.create
2257
- * @see SseClient.createNull
2258
- */
2259
- constructor(/** @type {function(new:EventSource)} */ eventSourceConstructor) {
2260
- super();
2261
- this.#eventSourceConstructor = eventSourceConstructor;
2262
- }
2263
-
2264
- get isConnected() {
2265
- return this.#eventSource?.readyState === this.#eventSourceConstructor.OPEN;
2266
- }
2267
-
2268
- get url() {
2269
- return this.#eventSource?.url;
2270
- }
2271
-
2272
- /**
2273
- * Connects to the server.
2274
- *
2275
- * @param {URL | string} url The server URL to connect to.
2276
- * @param {string} [eventName=message] The optional event type to listen to.
2277
- */
2278
-
2279
- async connect(url, eventName = 'message') {
2280
- await new Promise((resolve, reject) => {
2281
- if (this.isConnected) {
2282
- reject(new Error('Already connected.'));
2283
- return;
2284
- }
2285
-
2286
- try {
2287
- this.#eventSource = new this.#eventSourceConstructor(url);
2288
- this.#eventSource.addEventListener('open', (e) => {
2289
- this.#handleOpen(e);
2290
- resolve();
2291
- });
2292
- this.#eventSource.addEventListener(
2293
- eventName,
2294
- (e) => this.#handleMessage(e),
2295
- );
2296
- this.#eventSource.addEventListener(
2297
- 'error',
2298
- (e) => this.#handleError(e),
2299
- );
2300
- } catch (error) {
2301
- reject(error);
2302
- }
2303
- });
2304
- }
2305
-
2306
- async close() {
2307
- await new Promise((resolve, reject) => {
2308
- if (!this.isConnected) {
2309
- resolve();
2310
- return;
2311
- }
2312
-
2313
- try {
2314
- this.#eventSource.close();
2315
- resolve();
2316
- } catch (error) {
2317
- reject(error);
2318
- }
2319
- });
2320
- }
2321
-
2322
- /**
2323
- * Simulates a message event from the server.
2324
- *
2325
- * @param {string} message The message to receive.
2326
- * @param {string} [eventName=message] The optional event type.
2327
- * @param {string} [lastEventId] The optional last event ID.
2328
- */
2329
- simulateMessage(message, eventName = 'message', lastEventId = undefined) {
2330
- this.#handleMessage(
2331
- new MessageEvent(eventName, { data: message, lastEventId }),
2332
- );
2333
- }
2334
-
2335
- /**
2336
- * Simulates an error event.
2337
- */
2338
- simulateError() {
2339
- this.#handleError(new Event('error'));
2340
- }
2341
-
2342
- #handleOpen(event) {
2343
- this.dispatchEvent(new event.constructor(event.type, event));
2344
- }
2345
-
2346
- #handleMessage(event) {
2347
- this.dispatchEvent(new event.constructor(event.type, event));
2348
- }
2349
-
2350
- #handleError(event) {
2351
- this.dispatchEvent(new event.constructor(event.type, event));
2352
- }
2353
- }
2354
-
2355
- class EventSourceStub extends EventTarget {
2356
- // The constants have to be defined here because JSDOM is missing EventSource.
2357
- static CONNECTING = 0;
2358
- static OPEN = 1;
2359
- static CLOSED = 2;
2360
-
2361
- constructor(url) {
2362
- super();
2363
- this.url = url;
2364
- setTimeout(() => {
2365
- this.readyState = EventSourceStub.OPEN;
2366
- this.dispatchEvent(new Event('open'));
2367
- }, 0);
2368
- }
2369
-
2370
- close() {
2371
- this.readyState = EventSourceStub.CLOSED;
2372
- }
2373
- }
2374
-
2375
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2376
-
2377
- /**
2378
- * An API for time and durations.
2379
- *
2380
- * Portated from
2381
- * [Java Time](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/package-summary.html).
2382
- *
2383
- * @module
2384
- */
2385
-
2386
- /**
2387
- * A clock provides access to the current timestamp.
2388
- */
2389
- class Clock {
2390
- /**
2391
- * Creates a clock using system clock.
2392
- *
2393
- * @return {Clock} A clock that uses system clock.
2394
- */
2395
- static system() {
2396
- return new Clock();
2397
- }
2398
-
2399
- /**
2400
- * Creates a clock using a fixed date.
2401
- *
2402
- * @param {Date} [fixed='2024-02-21T19:16:00Z'] The fixed date of the clock.
2403
- * @return {Clock} A clock that returns alaways a fixed date.
2404
- * @see Clock#add
2405
- */
2406
- static fixed(fixedDate = new Date('2024-02-21T19:16:00Z')) {
2407
- return new Clock(fixedDate);
2408
- }
2409
-
2410
- #date;
2411
-
2412
- /** @hideconstructor */
2413
- constructor(/** @type {Date} */ date) {
2414
- this.#date = date;
2415
- }
2416
-
2417
- /**
2418
- * Returns the current timestamp of the clock.
2419
- *
2420
- * @return {Date} The current timestamp.
2421
- */
2422
- date() {
2423
- return this.#date ? new Date(this.#date) : new Date();
2424
- }
2425
-
2426
- /**
2427
- * Returns the current timestamp of the clock in milliseconds.
2428
- *
2429
- * @return {number} The current timestamp in milliseconds.
2430
- */
2431
- millis() {
2432
- return this.date().getTime();
2433
- }
2434
-
2435
- /**
2436
- * Adds a duration to the current timestamp of the clock.
2437
- *
2438
- * @param {Duration|string|number} offsetDuration The duration or number of
2439
- * millis to add.
2440
- */
2441
- add(offsetDuration) {
2442
- const current = this.date();
2443
- this.#date = new Date(
2444
- current.getTime() + new Duration(offsetDuration).millis,
2445
- );
2446
- }
2447
- }
2448
-
2449
- /**
2450
- * A duration is a time-based amount of time, such as '34.5 seconds'.
2451
- */
2452
- class Duration {
2453
- /**
2454
- * Creates a duration with zero value.
2455
- *
2456
- * @return {Duration} A zero duration.
2457
- */
2458
- static zero() {
2459
- return new Duration();
2460
- }
2461
-
2462
- /**
2463
- * Creates a duration from a ISO 8601 string like `[-]P[dD]T[hH][mM][s[.f]S]`.
2464
- *
2465
- * @param {string} isoString The ISO 8601 string to parse.
2466
- * @return {Duration} The parsed duration.
2467
- */
2468
- static parse(isoString) {
2469
- const match = isoString.match(
2470
- /^(-)?P(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+\.?\d*)S)?$/,
2471
- );
2472
- if (match == null) {
2473
- return new Duration(NaN);
2474
- }
2475
-
2476
- const sign = match[1] === '-' ? -1 : 1;
2477
- const days = Number(match[2] || 0);
2478
- const hours = Number(match[3] || 0);
2479
- const minutes = Number(match[4] || 0);
2480
- const seconds = Number(match[5] || 0);
2481
- const millis = Number(match[6] || 0);
2482
- return new Duration(
2483
- sign *
2484
- (days * 86400000 +
2485
- hours * 3600000 +
2486
- minutes * 60000 +
2487
- seconds * 1000 +
2488
- millis),
2489
- );
2490
- }
2491
-
2492
- /**
2493
- * Obtains a Duration representing the duration between two temporal objects.
2494
- *
2495
- * @param {Date|number} startInclusive The start date or millis, inclusive.
2496
- * @param {Date|number} endExclusive The end date or millis, exclusive.
2497
- * @return {Duration} The duration between the two dates.
2498
- */
2499
- static between(startInclusive, endExclusive) {
2500
- return new Duration(endExclusive - startInclusive);
2501
- }
2502
-
2503
- /**
2504
- * The total length of the duration in milliseconds.
2505
- *
2506
- * @type {number}
2507
- */
2508
- millis;
2509
-
2510
- /**
2511
- * Creates a duration.
2512
- *
2513
- * The duration is zero if no value is provided.
2514
- *
2515
- * @param {number|string|Duration} [value] The duration in millis, an ISO 8601
2516
- * string or another duration.
2517
- */
2518
- constructor(value) {
2519
- if (value === null || arguments.length === 0) {
2520
- this.millis = 0;
2521
- } else if (typeof value === 'string') {
2522
- this.millis = Duration.parse(value).millis;
2523
- } else if (typeof value === 'number') {
2524
- if (Number.isFinite(value)) {
2525
- this.millis = Math.trunc(value);
2526
- } else {
2527
- this.millis = NaN;
2528
- }
2529
- } else if (value instanceof Duration) {
2530
- this.millis = value.millis;
2531
- } else {
2532
- this.millis = NaN;
2533
- }
2534
- }
2535
-
2536
- /**
2537
- * Gets the number of days in the duration.
2538
- *
2539
- * @type {number}
2540
- * @readonly
2541
- */
2542
- get days() {
2543
- return Math.trunc(this.millis / 86400000);
2544
- }
2545
-
2546
- /**
2547
- * Extracts the number of days in the duration.
2548
- *
2549
- * @type {number}
2550
- * @readonly
2551
- */
2552
- get daysPart() {
2553
- const value = this.millis / 86400000;
2554
- return this.isNegative() ? Math.ceil(value) : Math.floor(value);
2555
- }
2556
-
2557
- /**
2558
- * Gets the number of hours in the duration.
2559
- *
2560
- * @type {number}
2561
- * @readonly
2562
- */
2563
- get hours() {
2564
- return Math.trunc(this.millis / 3600000);
2565
- }
2566
-
2567
- /**
2568
- * Extracts the number of hours in the duration.
2569
- *
2570
- * @type {number}
2571
- * @readonly
2572
- */
2573
- get hoursPart() {
2574
- const value = (this.millis - this.daysPart * 86400000) / 3600000;
2575
- return this.isNegative() ? Math.ceil(value) : Math.floor(value);
2576
- }
2577
-
2578
- /**
2579
- * Gets the number of minutes in the duration.
2580
- *
2581
- * @type {number}
2582
- * @readonly
2583
- */
2584
- get minutes() {
2585
- return Math.trunc(this.millis / 60000);
2586
- }
2587
-
2588
- /**
2589
- * Extracts the number of minutes in the duration.
2590
- *
2591
- * @type {number}
2592
- * @readonly
2593
- */
2594
- get minutesPart() {
2595
- const value =
2596
- (this.millis - this.daysPart * 86400000 - this.hoursPart * 3600000) /
2597
- 60000;
2598
- return this.isNegative() ? Math.ceil(value) : Math.floor(value);
2599
- }
2600
-
2601
- /**
2602
- * Gets the number of seconds in the duration.
2603
- *
2604
- * @type {number}
2605
- * @readonly
2606
- */
2607
- get seconds() {
2608
- return Math.trunc(this.millis / 1000);
2609
- }
2610
-
2611
- /**
2612
- * Extracts the number of seconds in the duration.
2613
- *
2614
- * @type {number}
2615
- * @readonly
2616
- */
2617
- get secondsPart() {
2618
- const value = (this.millis -
2619
- this.daysPart * 86400000 -
2620
- this.hoursPart * 3600000 -
2621
- this.minutesPart * 60000) /
2622
- 1000;
2623
- return this.isNegative() ? Math.ceil(value) : Math.floor(value);
2624
- }
2625
-
2626
- /**
2627
- * Gets the number of milliseconds in the duration.
2628
- *
2629
- * @type {number}
2630
- * @readonly
2631
- */
2632
- get millisPart() {
2633
- const value = this.millis -
2634
- this.daysPart * 86400000 -
2635
- this.hoursPart * 3600000 -
2636
- this.minutesPart * 60000 -
2637
- this.secondsPart * 1000;
2638
- return this.isNegative() ? Math.ceil(value) : Math.floor(value);
2639
- }
2640
-
2641
- /**
2642
- * Checks if the duration is zero.
2643
- *
2644
- * @type {boolean}
2645
- */
2646
- isZero() {
2647
- return this.millis === 0;
2648
- }
2649
-
2650
- /**
2651
- * Checks if the duration is negative.
2652
- *
2653
- * @type {boolean}
2654
- */
2655
- isNegative() {
2656
- return this.millis < 0;
2657
- }
2658
-
2659
- /**
2660
- * Checks if the duration is positive.
2661
- *
2662
- * @type {boolean}
2663
- */
2664
- isPositive() {
2665
- return this.millis > 0;
2666
- }
2667
-
2668
- /**
2669
- * Returns a copy of this duration with a positive length.
2670
- *
2671
- * @return {Duration} The absolute value of the duration.
2672
- */
2673
- absolutized() {
2674
- return new Duration(Math.abs(this.millis));
2675
- }
2676
-
2677
- /**
2678
- * Returns a copy of this duration with length negated.
2679
- *
2680
- * @return {Duration} The negated value of the duration.
2681
- */
2682
- negated() {
2683
- return new Duration(-this.millis);
2684
- }
2685
-
2686
- /**
2687
- * Returns a copy of this duration with the specified duration added.
2688
- *
2689
- * @param {Duration|string|number} duration The duration to add or number of
2690
- * millis.
2691
- * @return {Duration} The new duration.
2692
- */
2693
- plus(duration) {
2694
- return new Duration(this.millis + new Duration(duration).millis);
2695
- }
2696
-
2697
- /**
2698
- * Returns a copy of this duration with the specified duration subtracted.
2699
- *
2700
- * @param {Duration|string|number} duration The duration to subtract or number
2701
- * of millis.
2702
- * @return {Duration} The new duration.
2703
- */
2704
- minus(duration) {
2705
- return new Duration(this.millis - new Duration(duration));
2706
- }
2707
-
2708
- /**
2709
- * Returns a copy of this duration multiplied by the scalar.
2710
- *
2711
- * @param {number} multiplicand The value to multiply the duration by.
2712
- * @return {Duration} The new duration.
2713
- */
2714
- multipliedBy(multiplicand) {
2715
- return new Duration(this.millis * multiplicand);
2716
- }
2717
-
2718
- /**
2719
- * Returns a copy of this duration divided by the specified value.
2720
- *
2721
- * @param {number} divisor The value to divide the duration by.
2722
- * @return {Duration} The new duration.
2723
- */
2724
- dividedBy(divisor) {
2725
- return new Duration(this.millis / divisor);
2726
- }
2727
-
2728
- /**
2729
- * Returns a string representation of this duration using ISO 8601, such as
2730
- * `PT8H6M12.345S`.
2731
- *
2732
- * @return {string} The ISO 8601 string representation of the duration.
2733
- */
2734
- toISOString() {
2735
- if (this.isZero()) {
2736
- return 'PT0S';
2737
- }
2738
-
2739
- const value = this.absolutized();
2740
-
2741
- let period = 'PT';
2742
- const days = value.daysPart;
2743
- const hours = value.hoursPart;
2744
- if (days > 0 || hours > 0) {
2745
- period += `${days * 24 + hours}H`;
2746
- }
2747
- const minutes = value.minutesPart;
2748
- if (minutes > 0) {
2749
- period += `${minutes}M`;
2750
- }
2751
- const seconds = value.secondsPart;
2752
- const millis = value.millisPart;
2753
- if (seconds > 0 || millis > 0) {
2754
- period += `${seconds + millis / 1000}S`;
2755
- }
2756
- if (this.isNegative()) {
2757
- period = `-${period}`;
2758
- }
2759
- return period;
2760
- }
2761
-
2762
- /**
2763
- * Returns a parsable string representation of this duration.
2764
- *
2765
- * @return {string} The string representation of this duration.
2766
- */
2767
- toJSON() {
2768
- return this.toISOString();
2769
- }
2770
-
2771
- /**
2772
- * Returns a string representation of this duration, such as `08:06:12`.
2773
- *
2774
- * @param {object} options The options to create the string.
2775
- * @param {string} [options.style='medium'] The style of the string (`short`,
2776
- * `medium`, `long`).
2777
- * @return {string} The string representation of the duration.
2778
- */
2779
- toString({ style = 'medium' } = {}) {
2780
- if (Number.isNaN(this.valueOf())) {
2781
- return 'Invalid Duration';
2782
- }
2783
-
2784
- const value = this.absolutized();
2785
- const hours = String(Math.floor(value.hours)).padStart(2, '0');
2786
- const minutes = String(value.minutesPart).padStart(2, '0');
2787
- const seconds = String(value.secondsPart).padStart(2, '0');
2788
- let result = `${hours}:${minutes}`;
2789
- if (style === 'medium' || style === 'long') {
2790
- result += `:${seconds}`;
2791
- }
2792
- if (style === 'long') {
2793
- result += `.${String(value.millisPart).padStart(3, '0')}`;
2794
- }
2795
- if (this.isNegative()) {
2796
- result = `-${result}`;
2797
- }
2798
- return result;
2799
- }
2800
-
2801
- /**
2802
- * Returns the value of the duration in milliseconds.
2803
- *
2804
- * @return {number} The value of the duration in milliseconds.
2805
- */
2806
- valueOf() {
2807
- return this.millis;
2808
- }
2809
-
2810
- [Symbol.toPrimitive](hint) {
2811
- if (hint === 'number') {
2812
- return this.valueOf();
2813
- } else {
2814
- return this.toString();
2815
- }
2816
- }
2817
- }
2818
-
2819
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2820
-
2821
-
2822
- /**
2823
- * A simple stop watch.
2824
- */
2825
- class StopWatch {
2826
- #clock;
2827
- #startTime;
2828
- #stopTime;
2829
-
2830
- /**
2831
- * Creates a new stop watch.
2832
- *
2833
- * @param {Clock} [clock=Clock.system()] The clock to use for time
2834
- * measurement.
2835
- */
2836
- constructor(clock = Clock.system()) {
2837
- this.#clock = clock;
2838
- }
2839
-
2840
- /**
2841
- * Starts an unnamed task.
2842
- */
2843
- start() {
2844
- this.#startTime = this.#clock.millis();
2845
- }
2846
-
2847
- /**
2848
- * Stops the current task.
2849
- */
2850
- stop() {
2851
- this.#stopTime = this.#clock.millis();
2852
- }
2853
-
2854
- /**
2855
- * Gets the total time in milliseconds.
2856
- *
2857
- * @return {number} The total time in milliseconds.
2858
- */
2859
- getTotalTimeMillis() {
2860
- return this.#stopTime - this.#startTime;
2861
- }
2862
-
2863
- /**
2864
- * Gets the total time in seconds.
2865
- *
2866
- * @return {number} The total time in seconds.
2867
- */
2868
- getTotalTimeSeconds() {
2869
- return this.getTotalTimeMillis() / 1000;
2870
- }
2871
- }
2872
-
2873
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2874
-
2875
- /**
2876
- * Simple global state management with store and reducer.
2877
- *
2878
- * This implementation is compatible with [Redux](https://redux.js.org). It is
2879
- * intended to replace it with Redux if necessary, for example if the
2880
- * application grows.
2881
- *
2882
- * @module
2883
- */
2884
-
2885
- /**
2886
- * A reducer is a function that changes the state of the application based on an
2887
- * action.
2888
- *
2889
- * @callback ReducerType
2890
- * @param {StateType} state The current state of the application.
2891
- * @param {ActionType} action The action to handle.
2892
- * @return {StateType} The next state of the application or the initial state
2893
- * if the state parameter is `undefined`.
2894
- */
2895
-
2896
- /**
2897
- * The application state can be any object.
2898
- *
2899
- * @typedef {object} StateType
2900
- */
2901
-
2902
- /**
2903
- * An action describe an command or an event that changes the state of the
2904
- * application.
2905
- *
2906
- * An action can have any properties, but it should have a `type` property.
2907
- *
2908
- * @typedef {object} ActionType
2909
- * @property {string} type A string that identifies the action.
2910
- */
2911
-
2912
- /**
2913
- * A listener is a function that is called when the state of the store changes.
2914
- *
2915
- * @callback ListenerType
2916
- */
2917
-
2918
- /**
2919
- * An unsubscriber is a function that removes a listener from the store.
2920
- *
2921
- * @callback UnsubscriberType
2922
- */
2923
-
2924
- /**
2925
- * Creates a new store with the given reducer and optional preloaded state.
2926
- *
2927
- * @param {ReducerType} reducer The reducer function.
2928
- * @param {StateType} [preloadedState] The optional initial state of the store.
2929
- * @return {Store} The new store.
2930
- */
2931
- function createStore(reducer, preloadedState) {
2932
- const initialState = preloadedState || reducer(undefined, { type: '@@INIT' });
2933
- return new Store(reducer, initialState);
2934
- }
2935
-
2936
- /**
2937
- * A simple store compatible with [Redux](https://redux.js.org/api/store).
2938
- */
2939
- class Store {
2940
- #reducer;
2941
- #state;
2942
- #listeners = [];
2943
-
2944
- /**
2945
- * Creates a new store with the given reducer and initial state.
2946
- *
2947
- * @param {ReducerType} reducer
2948
- * @param {StateType} initialState
2949
- */
2950
- constructor(reducer, initialState) {
2951
- this.#reducer = reducer;
2952
- this.#state = initialState;
2953
- }
2954
-
2955
- /**
2956
- * Returns the current state of the store.
2957
- *
2958
- * @return {StateType} The current state of the store.
2959
- */
2960
- getState() {
2961
- return this.#state;
2962
- }
2963
-
2964
- /**
2965
- * Updates the state of the store by dispatching an action to the reducer.
2966
- *
2967
- * @param {ActionType} action The action to dispatch.
2968
- */
2969
- dispatch(action) {
2970
- const oldState = this.#state;
2971
- this.#state = this.#reducer(this.#state, action);
2972
- if (oldState !== this.#state) {
2973
- this.#emitChange();
2974
- }
2975
- }
2976
-
2977
- /**
2978
- * Subscribes a listener to store changes.
2979
- *
2980
- * @param {ListenerType} listener The listener to subscribe.
2981
- * @return {UnsubscriberType} A function that unsubscribes the listener.
2982
- */
2983
- subscribe(listener) {
2984
- this.#listeners.push(listener);
2985
- return () => this.#unsubscribe(listener);
2986
- }
2987
-
2988
- #emitChange() {
2989
- this.#listeners.forEach((listener) => {
2990
- // Unsubscribe replace listeners array with a new array, so we must double
2991
- // check if listener is still subscribed.
2992
- if (this.#listeners.includes(listener)) {
2993
- listener();
2994
- }
2995
- });
2996
- }
2997
-
2998
- #unsubscribe(listener) {
2999
- this.#listeners = this.#listeners.filter((l) => l !== listener);
3000
- }
3001
- }
3002
-
3003
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
3004
-
3005
-
3006
- // TODO check if import from time.js is needed
3007
- // TODO deep copy
3008
- // TODO deep equals
3009
-
3010
- function deepMerge(source, target) {
3011
- if (target === undefined) {
3012
- return source;
3013
- }
3014
-
3015
- if (typeof target !== 'object' || target === null) {
3016
- return target;
3017
- }
3018
-
3019
- if (Array.isArray(source) && Array.isArray(target)) {
3020
- for (const item of target) {
3021
- const element = deepMerge(undefined, item);
3022
- source.push(element);
3023
- }
3024
- return source;
3025
- }
3026
-
3027
- for (const key in target) {
3028
- if (typeof source !== 'object' || source === null) {
3029
- source = {};
3030
- }
3031
-
3032
- source[key] = deepMerge(source[key], target[key]);
3033
- }
3034
-
3035
- return source;
3036
- }
3037
-
3038
- /**
3039
- * An instance of `Random` is used to generate random numbers.
3040
- */
3041
- class Random {
3042
- static create() {
3043
- return new Random();
3044
- }
3045
-
3046
- /** @hideconstructor */
3047
- constructor() {}
3048
-
3049
- /**
3050
- * Returns a random boolean value.
3051
- *
3052
- * @param {number} [probabilityOfUndefined=0.0] The probability of returning
3053
- * `undefined`.
3054
- * @return {boolean|undefined} A random boolean between `origin` (inclusive)
3055
- * and `bound` (exclusive) or undefined.
3056
- */
3057
- nextBoolean(probabilityOfUndefined = 0.0) {
3058
- return this.#randomOptional(
3059
- () => Math.random() < 0.5,
3060
- probabilityOfUndefined,
3061
- );
3062
- }
3063
-
3064
- /**
3065
- * Returns a random integer between `origin` (inclusive) and `bound`
3066
- * (exclusive).
3067
- *
3068
- * @param {number} [origin=0] The least value that can be returned.
3069
- * @param {number} [bound=1] The upper bound (exclusive) for the returned
3070
- * value.
3071
- * @param {number} [probabilityOfUndefined=0.0] The probability of returning
3072
- * `undefined`.
3073
- * @return {number|undefined} A random integer between `origin` (inclusive)
3074
- * and `bound` (exclusive) or undefined.
3075
- */
3076
- nextInt(origin = 0, bound = 1, probabilityOfUndefined = 0.0) {
3077
- return this.#randomOptional(
3078
- () => Math.floor(this.nextFloat(origin, bound)),
3079
- probabilityOfUndefined,
3080
- );
3081
- }
3082
-
3083
- /**
3084
- * Returns a random float between `origin` (inclusive) and `bound`
3085
- * (exclusive).
3086
- *
3087
- * @param {number} [origin=0.0] The least value that can be returned.
3088
- * @param {number} [bound=1.0] The upper bound (exclusive) for the returned
3089
- * value.
3090
- * @param {number} [probabilityOfUndefined=0.0] The probability of returning
3091
- * `undefined`.
3092
- * @return {number|undefined} A random float between `origin` (inclusive) and
3093
- * `bound` (exclusive) or undefined.
3094
- */
3095
- nextFloat(origin = 0.0, bound = 1.0, probabilityOfUndefined = 0.0) {
3096
- return this.#randomOptional(
3097
- () => Math.random() * (bound - origin) + origin,
3098
- probabilityOfUndefined,
3099
- );
3100
- }
3101
-
3102
- /**
3103
- * Returns a random timestamp with optional random offset.
3104
- *
3105
- * @param {number} [maxMillis=0] The maximum offset in milliseconds.
3106
- * @param {number} [probabilityOfUndefined=0.0] The probability of returning
3107
- * `undefined`.
3108
- * @return {Date|undefined} A random timestamp or `undefined`.
3109
- */
3110
- nextDate(maxMillis = 0, probabilityOfUndefined = 0.0) {
3111
- return this.#randomOptional(() => {
3112
- const now = new Date();
3113
- let t = now.getTime();
3114
- const r = Math.random();
3115
- t += r * maxMillis;
3116
- return new Date(t);
3117
- }, probabilityOfUndefined);
3118
- }
3119
-
3120
- /**
3121
- * Returns a random value from an array.
3122
- *
3123
- * @param {Array} [values=[]] The array of values.
3124
- * @param {number} [probabilityOfUndefined=0.0] The probability of returning
3125
- * `undefined`.
3126
- * @return {*|undefined} A random value from the array or `undefined`.
3127
- */
3128
- nextValue(values = [], probabilityOfUndefined = 0.0) {
3129
- return this.#randomOptional(() => {
3130
- const index = new Random().nextInt(0, values.length - 1);
3131
- return values[index];
3132
- }, probabilityOfUndefined);
3133
- }
3134
-
3135
- #randomOptional(randomFactory, probabilityOfUndefined) {
3136
- const r = Math.random();
3137
- return r < probabilityOfUndefined ? undefined : randomFactory();
3138
- }
3139
- }
3140
-
3141
- const TASK_CREATED = 'created';
3142
- const TASK_SCHEDULED = 'scheduled';
3143
- const TASK_EXECUTED = 'executed';
3144
- const TASK_CANCELLED = 'cancelled';
3145
-
3146
- /**
3147
- * A task that can be scheduled by a {@link Timer}.
3148
- */
3149
- class TimerTask {
3150
- _state = TASK_CREATED;
3151
- _nextExecutionTime = 0;
3152
- _period = 0;
3153
-
3154
- /**
3155
- * Runs the task.
3156
- *
3157
- * @abstract
3158
- */
3159
- run() {
3160
- throw new Error('Method not implemented.');
3161
- }
3162
-
3163
- /**
3164
- * Cancels the task.
3165
- *
3166
- * @return {boolean} `true` if this task was scheduled for one-time execution
3167
- * and has not yet run, or this task was scheduled for repeated execution.
3168
- * Return `false` if the task was scheduled for one-time execution and has
3169
- * already run, or if the task was never scheduled, or if the task was
3170
- * already cancelled.
3171
- */
3172
- cancel() {
3173
- const result = this._state === TASK_SCHEDULED;
3174
- this._state = TASK_CANCELLED;
3175
- return result;
3176
- }
3177
-
3178
- /**
3179
- * Returns scheduled execution time of the most recent actual execution of
3180
- * this task.
3181
- *
3182
- * Example usage:
3183
- *
3184
- * ```javascript
3185
- * run() {
3186
- * if (Date.now() - scheduledExecutionTime() >= MAX_TARDINESS) {
3187
- * return; // Too late; skip this execution.
3188
- * }
3189
- * // Perform the task
3190
- * }
3191
- *
3192
- * ```
3193
- *
3194
- * @return {number} The time in milliseconds since the epoch, undefined if
3195
- * the task has not yet run for the first time.
3196
- */
3197
- scheduledExecutionTime() {
3198
- return this._period < 0
3199
- ? this._nextExecutionTime + this._period
3200
- : this._nextExecutionTime - this._period;
3201
- }
3202
- }
3203
-
3204
- /**
3205
- * A timer that schedules and cancels tasks.
3206
- *
3207
- * Tasks may be scheduled for one-time execution or for repeated execution at
3208
- * regular intervals.
3209
- */
3210
- class Timer extends EventTarget {
3211
- /**
3212
- * Returns a new `Timer`.
3213
- */
3214
- static create() {
3215
- return new Timer(Clock.system(), globalThis);
3216
- }
3217
-
3218
- /**
3219
- * Returns a new `Timer` for testing without side effects.
3220
- */
3221
- static createNull({ clock = Clock.fixed() } = {}) {
3222
- return new Timer(clock, new TimeoutStub(clock));
3223
- }
3224
-
3225
- #clock;
3226
- #global;
3227
- _queue;
3228
-
3229
- /**
3230
- * Returns a new `Timer`.
3231
- */
3232
- constructor(
3233
- /** @type {Clock} */ clock = Clock.system(),
3234
- /** @type {globalThis} */ global = globalThis,
3235
- ) {
3236
- super();
3237
- this.#clock = clock;
3238
- this.#global = global;
3239
- this._queue = [];
3240
- }
3241
-
3242
- /**
3243
- * Schedules a task for repeated execution at regular intervals.
3244
- *
3245
- * @param {TimerTask} task The task to execute.
3246
- * @param {number|Date} delayOrTime The delay before the first execution, in
3247
- * milliseconds or the time of the first execution.
3248
- * @param {number} [period=0] The interval between executions, in
3249
- * milliseconds; 0 means single execution.
3250
- */
3251
- schedule(task, delayOrTime, period = 0) {
3252
- this.#doSchedule(task, delayOrTime, -period);
3253
- }
3254
-
3255
- /**
3256
- * Schedule a task for repeated fixed-rate execution.
3257
- *
3258
- * @param {TimerTask} task The task to execute.
3259
- * @param {number|Date} delayOrTime The delay before the first execution, in
3260
- * milliseconds or the time of the first.
3261
- * @param {number} period The interval between executions, in milliseconds.
3262
- */
3263
- scheduleAtFixedRate(task, delayOrTime, period) {
3264
- this.#doSchedule(task, delayOrTime, period);
3265
- }
3266
-
3267
- /**
3268
- * Cancels all scheduled tasks.
3269
- */
3270
- cancel() {
3271
- for (const task of this._queue) {
3272
- task.cancel();
3273
- }
3274
- this._queue = [];
3275
- }
3276
-
3277
- /**
3278
- * Removes all cancelled tasks from the task queue.
3279
- *
3280
- * @return {number} The number of tasks removed from the task queue.
3281
- */
3282
- purge() {
3283
- let result = 0;
3284
- for (let i = 0; i < this._queue.length; i++) {
3285
- if (this._queue[i]._state === TASK_CANCELLED) {
3286
- this._queue.splice(i, 1);
3287
- i--;
3288
- result++;
3289
- }
3290
- }
3291
- return result;
3292
- }
3293
-
3294
- /**
3295
- * Simulates the execution of a task.
3296
- *
3297
- * @param {object} options The simulation options.
3298
- * @param {number} [options.ticks=1000] The number of milliseconds to advance
3299
- * the clock.
3300
- */
3301
- simulateTaskExecution({ ticks = 1000 } = {}) {
3302
- this.#clock.add(ticks);
3303
- this.#runMainLoop();
3304
- }
3305
-
3306
- #doSchedule(task, delayOrTime, period) {
3307
- if (delayOrTime instanceof Date) {
3308
- task._nextExecutionTime = delayOrTime.getTime();
3309
- } else {
3310
- task._nextExecutionTime = this.#clock.millis() + delayOrTime;
3311
- }
3312
- task._period = period;
3313
- task._state = TASK_SCHEDULED;
3314
- this._queue.push(task);
3315
- this._queue.sort((a, b) => b._nextExecutionTime - a._nextExecutionTime);
3316
- if (this._queue[0] === task) {
3317
- this.#runMainLoop();
3318
- }
3319
- }
3320
-
3321
- #runMainLoop() {
3322
- if (this._queue.length === 0) {
3323
- return;
3324
- }
3325
-
3326
- /** @type {TimerTask} */ const task = this._queue[0];
3327
- if (task._state === TASK_CANCELLED) {
3328
- this._queue.shift();
3329
- return this.#runMainLoop();
3330
- }
3331
-
3332
- const now = this.#clock.millis();
3333
- const executionTime = task._nextExecutionTime;
3334
- const taskFired = executionTime <= now;
3335
- if (taskFired) {
3336
- if (task._period === 0) {
3337
- this._queue.shift();
3338
- task._state = TASK_EXECUTED;
3339
- } else {
3340
- task._nextExecutionTime = task._period < 0
3341
- ? now - task._period
3342
- : executionTime + task._period;
3343
- }
3344
- task.run();
3345
- } else {
3346
- this.#global.setTimeout(() => this.#runMainLoop(), executionTime - now);
3347
- }
3348
- }
3349
- }
3350
-
3351
- class TimeoutStub {
3352
- setTimeout() {}
3353
- }
3354
-
3355
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
3356
-
3357
- /**
3358
- * Calculate with vectors in a two-dimensional space.
3359
- *
3360
- * @module
3361
- */
3362
-
3363
- /**
3364
- * A vector in a two-dimensional space.
3365
- */
3366
- class Vector2D {
3367
- /**
3368
- * Creates a vector from 2 points.
3369
- *
3370
- * @param {Vector2D} a The first point.
3371
- * @param {Vector2D} b The second point.
3372
- * @return {Vector2D} The vector from a to b.
3373
- */
3374
- static fromPoints(a, b) {
3375
- return new Vector2D(b.x - a.x, b.y - a.y);
3376
- }
3377
-
3378
- /**
3379
- * Creates a new vector.
3380
- *
3381
- * Examples:
3382
- *
3383
- * ```java
3384
- * new Vector2D(1, 2)
3385
- * new Vector2D([1, 2])
3386
- * new Vector2D({ x: 1, y: 2 })
3387
- * ```
3388
- *
3389
- * @param {number|Array<number>|Vector2D} [x=0] The x coordinate or an array
3390
- * or another vector.
3391
- * @param {number} [y=0] The y coordinate or undefined if x is an array or
3392
- * another vector.
3393
- */
3394
- constructor(x = 0, y = 0) {
3395
- if (Array.isArray(x)) {
3396
- this.x = Number(x[0]);
3397
- this.y = Number(x[1]);
3398
- } else if (typeof x === 'object' && 'x' in x && 'y' in x) {
3399
- this.x = Number(x.x);
3400
- this.y = Number(x.y);
3401
- } else {
3402
- this.x = Number(x);
3403
- this.y = Number(y);
3404
- }
3405
- }
3406
-
3407
- /**
3408
- * Returns the length of the vector.
3409
- *
3410
- * @return {number} The length of the vector.
3411
- */
3412
- length() {
3413
- return Math.hypot(this.x, this.y);
3414
- }
3415
-
3416
- /**
3417
- * Adds another vector to this vector and return the new vector.
3418
- *
3419
- * @param {Vector2D} v The vector to add.
3420
- * @return {Vector2D} The new vector.
3421
- */
3422
- add(v) {
3423
- v = new Vector2D(v);
3424
- return new Vector2D(this.x + v.x, this.y + v.y);
3425
- }
3426
-
3427
- /**
3428
- * Subtracts another vector from this vector and return the new vector.
3429
- *
3430
- * @param {Vector2D} v The vector to subtract.
3431
- * @return {Vector2D} The new vector.
3432
- */
3433
- subtract(v) {
3434
- v = new Vector2D(v);
3435
- return new Vector2D(this.x - v.x, this.y - v.y);
3436
- }
3437
-
3438
- /**
3439
- * Multiplies the vector with a scalar and returns the new vector.
3440
- *
3441
- * @param {number} scalar The scalar to multiply with.
3442
- * @return {Vector2D} The new vector.
3443
- */
3444
- scale(scalar) {
3445
- return new Vector2D(this.x * scalar, this.y * scalar);
3446
- }
3447
-
3448
- /**
3449
- * Multiplies the vector with another vector and returns the scalar.
3450
- *
3451
- * @param {Vector2D} v The vector to multiply with.
3452
- * @return {number} The scalar.
3453
- */
3454
- dot(v) {
3455
- v = new Vector2D(v);
3456
- return this.x * v.x + this.y * v.y;
3457
- }
3458
-
3459
- /**
3460
- * Returns the distance between this vector and another vector.
3461
- *
3462
- * @param {Vector2D} v The other vector.
3463
- * @return {number} The distance.
3464
- */
3465
- distance(v) {
3466
- v = new Vector2D(v);
3467
- return Vector2D.fromPoints(this, v).length();
3468
- }
3469
-
3470
- /**
3471
- * Returns the rotated vector by the given angle in radians.
3472
- *
3473
- * @param {number} angle The angle in radians.
3474
- * @return {Vector2D} The rotated vector.
3475
- */
3476
- rotate(angle) {
3477
- const cos = Math.cos(angle);
3478
- const sin = Math.sin(angle);
3479
- return new Vector2D(
3480
- this.x * cos - this.y * sin,
3481
- this.x * sin + this.y * cos,
3482
- );
3483
- }
3484
-
3485
- /**
3486
- * Returns the unit vector of this vector.
3487
- *
3488
- * @return {Vector2D} The unit vector.
3489
- */
3490
- normalize() {
3491
- return this.scale(1 / this.length());
3492
- }
3493
- }
3494
-
3495
- /**
3496
- * A line in a two-dimensional space.
3497
- */
3498
- class Line2D {
3499
- /**
3500
- * Creates a line from 2 points.
3501
- *
3502
- * @param {Vector2D} a The first point.
3503
- * @param {Vector2D} b The second point.
3504
- * @return {Line2D} The line from a to b.
3505
- */
3506
- static fromPoints(a, b) {
3507
- return new Line2D(a, Vector2D.fromPoints(a, b));
3508
- }
3509
-
3510
- /**
3511
- * Creates a new line.
3512
- *
3513
- * @param {Vector2D} point A point on the line.
3514
- * @param {Vector2D} direction The direction of the line.
3515
- */
3516
- constructor(point, direction) {
3517
- this.point = new Vector2D(point);
3518
- this.direction = new Vector2D(direction);
3519
- }
3520
-
3521
- /**
3522
- * Returns the perpendicular of a point on this line.
3523
- *
3524
- * @param {Vector2D} point A point.
3525
- * @return {{foot: number, scalar: number}} The `foot` and the `scalar`.
3526
- */
3527
- perpendicular(point) {
3528
- // dissolve after r: (line.position + r * line.direction - point) * line.direction = 0
3529
- const a = this.point.subtract(point);
3530
- const b = a.dot(this.direction);
3531
- const c = this.direction.dot(this.direction);
3532
- const r = b !== 0 ? -b / c : 0;
3533
-
3534
- // solve with r: line.position + r * line.direction = foot
3535
- const foot = this.point.add(this.direction.scale(r));
3536
-
3537
- let scalar;
3538
- if (this.direction.x !== 0.0) {
3539
- scalar = (foot.x - this.point.x) / this.direction.x;
3540
- } else if (this.direction.y !== 0.0) {
3541
- scalar = (foot.y - this.point.y) / this.direction.y;
3542
- } else {
3543
- scalar = Number.NaN;
3544
- }
3545
-
3546
- return { foot, scalar };
3547
- }
3548
- }
3549
-
3550
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
3551
-
3552
-
3553
- const HEARTBEAT_TYPE = 'heartbeat';
3554
-
3555
- const MESSAGE_SENT_EVENT = 'message-sent';
3556
-
3557
- /**
3558
- * A client for the WebSocket protocol.
3559
- *
3560
- * Emits the following events:
3561
- *
3562
- * - open, {@link Event}
3563
- * - message, {@link MessageEvent}
3564
- * - error, {@link Event}
3565
- * - close, {@link CloseEvent}
3566
- *
3567
- * @implements {MessageClient}
3568
- */
3569
- class WebSocketClient extends MessageClient {
3570
- // TODO Recover connection with timeout after an error event.
3571
-
3572
- /**
3573
- * Creates a WebSocket client.
3574
- *
3575
- * @param {object} options
3576
- * @param {number} [options.heartbeat=30000] The heartbeat interval i
3577
- * milliseconds. A value <= 0 disables the heartbeat.
3578
- * @return {WebSocketClient} A new WebSocket client.
3579
- */
3580
- static create({ heartbeat = 30000 } = {}) {
3581
- return new WebSocketClient(heartbeat, Timer.create(), WebSocket);
3582
- }
3583
-
3584
- /**
3585
- * Creates a nulled WebSocket client.
3586
- *
3587
- * @param {object} options
3588
- * @param {number} [options.heartbeat=-1] The heartbeat interval in
3589
- * milliseconds. A value <= 0 disables the heartbeat.
3590
- * @return {WebSocketClient} A new nulled WebSocket client.
3591
- */
3592
- static createNull({ heartbeat = -1 } = {}) {
3593
- return new WebSocketClient(heartbeat, Timer.createNull(), WebSocketStub);
3594
- }
3595
-
3596
- #heartbeat;
3597
- #timer;
3598
- #webSocketConstructor;
3599
- /** @type {WebSocket} */ #webSocket;
3600
-
3601
- /**
3602
- * The constructor is for internal use. Use the factory methods instead.
3603
- *
3604
- * @see WebSocketClient.create
3605
- * @see WebSocketClient.createNull
3606
- */
3607
- constructor(
3608
- /** @type {number} */ heartbeat,
3609
- /** @type {Timer} */ timer,
3610
- /** @type {function(new:WebSocket)} */ webSocketConstructor,
3611
- ) {
3612
- super();
3613
- this.#heartbeat = heartbeat;
3614
- this.#timer = timer;
3615
- this.#webSocketConstructor = webSocketConstructor;
3616
- }
3617
-
3618
- get isConnected() {
3619
- return this.#webSocket?.readyState === WebSocket.OPEN;
3620
- }
3621
-
3622
- get url() {
3623
- return this.#webSocket?.url;
3624
- }
3625
-
3626
- async connect(/** @type {string | URL} */ url) {
3627
- await new Promise((resolve, reject) => {
3628
- if (this.isConnected) {
3629
- reject(new Error('Already connected.'));
3630
- return;
3631
- }
3632
-
3633
- try {
3634
- this.#webSocket = new this.#webSocketConstructor(url);
3635
- this.#webSocket.addEventListener('open', (e) => {
3636
- this.#handleOpen(e);
3637
- resolve();
3638
- });
3639
- this.#webSocket.addEventListener(
3640
- 'message',
3641
- (e) => this.#handleMessage(e),
3642
- );
3643
- this.#webSocket.addEventListener('close', (e) => this.#handleClose(e));
3644
- this.#webSocket.addEventListener('error', (e) => this.#handleError(e));
3645
- } catch (error) {
3646
- reject(error);
3647
- }
3648
- });
3649
- }
3650
-
3651
- /**
3652
- * Sends a message to the server.
3653
- *
3654
- * @param {string} message The message to send.
3655
- */
3656
- send(message) {
3657
- this.#webSocket.send(message);
3658
- this.dispatchEvent(
3659
- new CustomEvent(MESSAGE_SENT_EVENT, { detail: message }),
3660
- );
3661
- }
3662
-
3663
- /**
3664
- * Returns a tracker for messages sent.
3665
- *
3666
- * @return {OutputTracker} A new output tracker.
3667
- */
3668
- trackMessageSent() {
3669
- return OutputTracker.create(this, MESSAGE_SENT_EVENT);
3670
- }
3671
-
3672
- /**
3673
- * Closes the connection.
3674
- *
3675
- * If a code is provided, also a reason should be provided.
3676
- *
3677
- * @param {number} code An optional code.
3678
- * @param {string} reason An optional reason.
3679
- */
3680
- async close(code, reason) {
3681
- await new Promise((resolve) => {
3682
- if (!this.isConnected) {
3683
- resolve();
3684
- return;
3685
- }
3686
-
3687
- this.#webSocket.addEventListener('close', () => resolve());
3688
- this.#webSocket.close(code, reason);
3689
- });
3690
- }
3691
-
3692
- /**
3693
- * Simulates a message event from the server.
3694
- *
3695
- * @param {string} message The message to receive.
3696
- */
3697
- simulateMessage(message) {
3698
- this.#handleMessage(new MessageEvent('message', { data: message }));
3699
- }
3700
-
3701
- /**
3702
- * Simulates a heartbeat.
3703
- */
3704
- simulateHeartbeat() {
3705
- this.#timer.simulateTaskExecution({ ticks: this.#heartbeat });
3706
- }
3707
-
3708
- /**
3709
- * Simulates a close event.
3710
- *
3711
- * @param {number} code An optional code.
3712
- * @param {string} reason An optional reason.
3713
- */
3714
- simulateClose(code, reason) {
3715
- this.#handleClose(new CloseEvent('close', { code, reason }));
3716
- }
3717
-
3718
- /**
3719
- * Simulates an error event.
3720
- */
3721
- simulateError() {
3722
- this.#handleError(new Event('error'));
3723
- }
3724
-
3725
- #handleOpen(event) {
3726
- this.dispatchEvent(new event.constructor(event.type, event));
3727
- this.#startHeartbeat();
3728
- }
3729
-
3730
- #handleMessage(event) {
3731
- this.dispatchEvent(new event.constructor(event.type, event));
3732
- }
3733
-
3734
- #handleClose(event) {
3735
- this.#stopHeartbeat();
3736
- this.dispatchEvent(new event.constructor(event.type, event));
3737
- }
3738
-
3739
- #handleError(event) {
3740
- this.dispatchEvent(new event.constructor(event.type, event));
3741
- }
3742
-
3743
- #startHeartbeat() {
3744
- if (this.#heartbeat <= 0) {
3745
- return;
3746
- }
3747
-
3748
- this.#timer.scheduleAtFixedRate(
3749
- new HeartbeatTask(this),
3750
- this.#heartbeat,
3751
- this.#heartbeat,
3752
- );
3753
- }
3754
-
3755
- #stopHeartbeat() {
3756
- this.#timer.cancel();
3757
- }
3758
- }
3759
-
3760
- class HeartbeatTask extends TimerTask {
3761
- #client;
3762
-
3763
- constructor(/** @type {WebSocketClient} */ client) {
3764
- super();
3765
- this.#client = client;
3766
- }
3767
-
3768
- run() {
3769
- this.#client.send(HEARTBEAT_TYPE);
3770
- }
3771
- }
3772
-
3773
- class WebSocketStub extends EventTarget {
3774
- readyState = WebSocket.CONNECTING;
3775
-
3776
- constructor(url) {
3777
- super();
3778
- this.url = url;
3779
- setTimeout(() => {
3780
- this.readyState = WebSocket.OPEN;
3781
- this.dispatchEvent(new Event('open'));
3782
- }, 0);
3783
- }
3784
-
3785
- send() {}
3786
-
3787
- close() {
3788
- this.readyState = WebSocket.CLOSED;
3789
- this.dispatchEvent(new Event('close'));
3790
- }
3791
- }
3792
-
3793
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
3794
-
3795
- /**
3796
- * @import * as express from 'express'
3797
- */
3798
-
3799
- function runSafe(/** @type {express.RequestHandler} */ handler) {
3800
- // TODO runSafe is obsolete with with Express 5
3801
- return async (request, response, next) => {
3802
- try {
3803
- await handler(request, response);
3804
- } catch (error) {
3805
- next(error);
3806
- }
3807
- };
3808
- }
3809
-
3810
- function reply(
3811
- /** @type {express.Response} */ response,
3812
- { status = 200, headers = { 'Content-Type': 'text/plain' }, body = '' } = {},
3813
- ) {
3814
- response.status(status).header(headers).send(body);
3815
- }
3816
-
3817
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
3818
-
3819
-
3820
- class ActuatorController {
3821
- #services;
3822
- #healthContributorRegistry;
3823
-
3824
- constructor(
3825
- services, // FIXME Services is not defined in library
3826
- /** @type {HealthContributorRegistry} */ healthContributorRegistry,
3827
- /** @type {express.Express} */ app,
3828
- ) {
3829
- this.#services = services;
3830
- this.#healthContributorRegistry = healthContributorRegistry;
3831
-
3832
- app.get('/actuator', this.#getActuator.bind(this));
3833
- app.get('/actuator/info', this.#getActuatorInfo.bind(this));
3834
- app.get('/actuator/metrics', this.#getActuatorMetrics.bind(this));
3835
- app.get('/actuator/health', this.#getActuatorHealth.bind(this));
3836
- app.get(
3837
- '/actuator/prometheus',
3838
- runSafe(this.#getMetrics.bind(this)),
3839
- );
3840
- }
3841
-
3842
- #getActuator(
3843
- /** @type {express.Request} */ request,
3844
- /** @type {express.Response} */ response,
3845
- ) {
3846
- let requestedUrl = request.protocol + '://' + request.get('host') +
3847
- request.originalUrl;
3848
- if (!requestedUrl.endsWith('/')) {
3849
- requestedUrl += '/';
3850
- }
3851
- response.status(200).json({
3852
- _links: {
3853
- self: { href: requestedUrl },
3854
- info: { href: requestedUrl + 'info' },
3855
- metrics: { href: requestedUrl + 'metrics' },
3856
- health: { href: requestedUrl + 'health' },
3857
- prometheus: { href: requestedUrl + 'prometheus' },
3858
- },
3859
- });
3860
- }
3861
-
3862
- #getActuatorInfo(
3863
- /** @type {express.Request} */ _request,
3864
- /** @type {express.Response} */ response,
3865
- ) {
3866
- const info = {};
3867
- info[process.env.npm_package_name] = {
3868
- version: process.env.npm_package_version,
3869
- };
3870
- response.status(200).json(info);
3871
- }
3872
-
3873
- #getActuatorMetrics(
3874
- /** @type {express.Request} */ _request,
3875
- /** @type {express.Response} */ response,
3876
- ) {
3877
- response.status(200).json({
3878
- cpu: process.cpuUsage(),
3879
- mem: process.memoryUsage(),
3880
- uptime: process.uptime(),
3881
- });
3882
- }
3883
-
3884
- #getActuatorHealth(
3885
- /** @type {express.Request} */ _request,
3886
- /** @type {express.Response} */ response,
3887
- ) {
3888
- const health = this.#healthContributorRegistry.health();
3889
- const status = health.status === 'UP' ? 200 : 503;
3890
- response.status(status).json(health);
3891
- }
3892
-
3893
- async #getMetrics(
3894
- /** @type {express.Request} */ _request,
3895
- /** @type {express.Response} */ response,
3896
- ) {
3897
- // TODO count warnings and errors
3898
- // TODO create class MeterRegistry
3899
-
3900
- const metrics = await this.#services.getMetrics();
3901
- const timestamp = new Date().getTime();
3902
- let body =
3903
- `# TYPE talks_count gauge\ntalks_count ${metrics.talksCount} ${timestamp}\n\n`;
3904
- body +=
3905
- `# TYPE presenters_count gauge\npresenters_count ${metrics.presentersCount} ${timestamp}\n\n`;
3906
- body +=
3907
- `# TYPE comments_count gauge\ncomments_count ${metrics.commentsCount} ${timestamp}\n\n`;
3908
- reply(response, { body });
3909
- }
3910
- }
3911
-
3912
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
3913
-
3914
-
3915
- // TODO How to handle optional values? Cast to which type?
3916
- // TODO Use JSON schema to validate the configuration?
3917
-
3918
- /**
3919
- * Provide the configuration of an application.
3920
- *
3921
- * The configuration is read from a JSON file `application.json` from the
3922
- * working directory.
3923
- *
3924
- * Example:
3925
- *
3926
- * ```javascript
3927
- * const configuration = ConfigurationProperties.create();
3928
- * const config = await configuration.get();
3929
- * ```
3930
- *
3931
- * With default values:
3932
- *
3933
- * ```javascript
3934
- * const configuration = ConfigurationProperties.create({
3935
- * defaults: {
3936
- * port: 8080,
3937
- * database: { host: 'localhost', port: 5432 },
3938
- * },
3939
- * });
3940
- * const config = await configuration.get();
3941
- * ```
3942
- */
3943
- class ConfigurationProperties {
3944
- /**
3945
- * Creates an instance of the application configuration.
3946
- *
3947
- * @param {object} options The configuration options.
3948
- * @param {object} [options.defaults={}] The default configuration.
3949
- * @param {string} [options.prefix=""] The prefix of the properties to get.
3950
- * @param {string} [options.name='application.json'] The name of the
3951
- * configuration file.
3952
- * @param {string[]} [options.location=['.', 'config']] The locations where to
3953
- * search for the configuration file.
3954
- * @return {ConfigurationProperties} The new instance.
3955
- */
3956
- static create({
3957
- defaults = {},
3958
- prefix = '',
3959
- name = 'application.json',
3960
- location = ['.', 'config'],
3961
- } = {}) {
3962
- return new ConfigurationProperties(
3963
- defaults,
3964
- prefix,
3965
- name,
3966
- location,
3967
- fsPromises,
3968
- );
3969
- }
3970
-
3971
- /**
3972
- * Creates a nullable of the application configuration.
3973
- *
3974
- * @param {object} options The configuration options.
3975
- * @param {object} [options.defaults={}] The default configuration.
3976
- * @param {string} [options.prefix=""] The prefix of the properties to get.
3977
- * @param {string} [options.name='application.json'] The name of the
3978
- * configuration file.
3979
- * @param {string[]} [options.location=['.', 'config']] The locations where to
3980
- * search for the configuration file.
3981
- * @param {object} [options.files={}] The files and file content that are
3982
- * available.
3983
- */
3984
- static createNull({
3985
- defaults = {},
3986
- prefix = '',
3987
- name = 'application.json',
3988
- location = ['.', 'config'],
3989
- files = {},
3990
- } = {}) {
3991
- return new ConfigurationProperties(
3992
- defaults,
3993
- prefix,
3994
- name,
3995
- location,
3996
- new FsStub(files),
3997
- );
3998
- }
3999
-
4000
- #defaults;
4001
- #prefix;
4002
- #name;
4003
- #locations;
4004
- #fs;
4005
-
4006
- /**
4007
- * The constructor is for internal use. Use the factory methods instead.
4008
- *
4009
- * @see ConfigurationProperties.create
4010
- * @see ConfigurationProperties.createNull
4011
- */
4012
- constructor(
4013
- /** @type {object} */ defaults,
4014
- /** @type {string} */ prefix,
4015
- /** @type {string} */ name,
4016
- /** @type {string[]} */ locations,
4017
- /** @type {fsPromises} */ fs,
4018
- ) {
4019
- this.#defaults = defaults;
4020
- this.#prefix = prefix;
4021
- this.#name = name;
4022
- this.#locations = locations;
4023
- this.#fs = fs;
4024
- }
4025
-
4026
- /**
4027
- * Loads the configuration from the file.
4028
- *
4029
- * @return {Promise<object>} The configuration object.
4030
- */
4031
- async get() {
4032
- let config = await this.#loadFile();
4033
- // FIXME copy defaults before merging
4034
- config = deepMerge(this.#defaults, config);
4035
- this.#applyEnvironmentVariables(config);
4036
- // TODO apply command line arguments
4037
- return this.#getSubset(config, this.#prefix);
4038
- }
4039
-
4040
- async #loadFile() {
4041
- let config = {};
4042
- for (const location of this.#locations) {
4043
- try {
4044
- const filePath = path.join(location, this.#name);
4045
- const content = await this.#fs.readFile(filePath, 'utf-8');
4046
- config = JSON.parse(content);
4047
- break;
4048
- } catch (err) {
4049
- if (err.code === 'ENOENT') {
4050
- // ignore file not found
4051
- continue;
4052
- }
4053
-
4054
- throw err;
4055
- }
4056
- }
4057
- return config;
4058
- }
4059
-
4060
- #applyEnvironmentVariables(config, path) {
4061
- // handle object
4062
- // handle array
4063
- // handle string
4064
- // handle number
4065
- // handle boolean (true, false)
4066
- // handle null (empty env var set the value to null)
4067
- // if env var is undefined, keep the default value
4068
- for (const key in config) {
4069
- if (typeof config[key] === 'boolean') {
4070
- const value = this.#getEnv(key, path);
4071
- if (value === null) {
4072
- config[key] = null;
4073
- } else if (value) {
4074
- config[key] = value.toLowerCase() === 'true';
4075
- }
4076
- } else if (typeof config[key] === 'number') {
4077
- const value = this.#getEnv(key, path);
4078
- if (value === null) {
4079
- config[key] = null;
4080
- } else if (value) {
4081
- config[key] = Number(value);
4082
- }
4083
- } else if (typeof config[key] === 'string') {
4084
- const value = this.#getEnv(key, path);
4085
- if (value === null) {
4086
- config[key] = null;
4087
- } else if (value) {
4088
- config[key] = String(value);
4089
- }
4090
- } else if (config[key] === null) {
4091
- const value = this.#getEnv(key, path);
4092
- if (value === null) {
4093
- config[key] = null;
4094
- } else if (value) {
4095
- config[key] = value;
4096
- }
4097
- } else if (typeof config[key] === 'object') {
4098
- const value = this.#getEnv(key, path);
4099
- if (value === null) {
4100
- config[key] = null;
4101
- } else if (Array.isArray(config[key]) && value) {
4102
- config[key] = value.split(',');
4103
- } else {
4104
- this.#applyEnvironmentVariables(config[key], key);
4105
- }
4106
- } else {
4107
- throw new Error(`Unsupported type: ${typeof config[key]}`);
4108
- }
4109
- }
4110
- }
4111
-
4112
- #getEnv(key, path = '') {
4113
- let envKey = key;
4114
- if (path) {
4115
- envKey = `${path}_${envKey}`;
4116
- }
4117
- envKey = envKey.toUpperCase();
4118
- const value = process.env[envKey];
4119
- if (value === '') {
4120
- return null;
4121
- }
4122
- return value;
4123
- }
4124
-
4125
- #getSubset(config, prefix) {
4126
- if (prefix === '') {
4127
- return config;
4128
- }
4129
-
4130
- const [key, ...rest] = prefix.split('.');
4131
- if (rest.length === 0) {
4132
- return config[key];
4133
- }
4134
-
4135
- return this.#getSubset(config[key], rest.join('.'));
4136
- }
4137
- }
4138
-
4139
- class FsStub {
4140
- #files;
4141
-
4142
- constructor(files) {
4143
- this.#files = files;
4144
- }
4145
-
4146
- readFile(path) {
4147
- const fileContent = this.#files[path];
4148
- if (fileContent == null) {
4149
- const err = new Error(`File not found: ${path}`);
4150
- err.code = 'ENOENT';
4151
- throw err;
4152
- }
4153
-
4154
- if (typeof fileContent === 'string') {
4155
- return fileContent;
4156
- }
4157
-
4158
- return JSON.stringify(fileContent);
4159
- }
4160
- }
4161
-
4162
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
4163
-
4164
-
4165
- /**
4166
- * A `Handler` that writes log messages to a file.
4167
- *
4168
- * @extends {Handler}
4169
- */
4170
- class FileHandler extends Handler {
4171
- #filename;
4172
- #limit;
4173
-
4174
- /**
4175
- * Initialize a new `FileHandler`.
4176
- *
4177
- * @param {string} filename The name of the file to write log messages to.
4178
- * @param {number} [limit=0] The maximum size of the file in bytes before it
4179
- * is rotated.
4180
- */
4181
- constructor(filename, limit = 0) {
4182
- super();
4183
- this.#filename = filename;
4184
- this.#limit = limit < 0 ? 0 : limit;
4185
- }
4186
-
4187
- /** @override */
4188
- async publish(/** @type {LogRecord} */ record) {
4189
- if (!this.isLoggable(record.level)) {
4190
- return;
4191
- }
4192
-
4193
- const message = this.formatter.format(record);
4194
- if (this.#limit > 0) {
4195
- try {
4196
- const stats = await fsPromises.stat(this.#filename);
4197
- const fileSize = stats.size;
4198
- const newSize = fileSize + message.length;
4199
- if (newSize > this.#limit) {
4200
- await fsPromises.rm(this.#filename);
4201
- }
4202
- } catch (error) {
4203
- // ignore error if file does not exist
4204
- if (error.code !== 'ENOENT') {
4205
- console.error(error);
4206
- }
4207
- }
4208
- }
4209
- await fsPromises.appendFile(this.#filename, message + '\n');
4210
- }
4211
- }
4212
-
4213
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
4214
-
4215
-
4216
- class LongPolling {
4217
- #version = 0;
4218
- #waiting = [];
4219
- #getData;
4220
-
4221
- constructor(/** @type {function(): Promise<*>} */ getData) {
4222
- this.#getData = getData;
4223
- }
4224
-
4225
- async poll(
4226
- /** @type {express.Request} */ request,
4227
- /** @type {express.Response} */ response,
4228
- ) {
4229
- if (this.#isCurrentVersion(request)) {
4230
- const responseData = await this.#tryLongPolling(request);
4231
- reply(response, responseData);
4232
- } else {
4233
- const responseData = await this.#getResponse();
4234
- reply(response, responseData);
4235
- }
4236
- }
4237
-
4238
- async send() {
4239
- this.#version++;
4240
- const response = await this.#getResponse();
4241
- this.#waiting.forEach((resolve) => resolve(response));
4242
- this.#waiting = [];
4243
- }
4244
-
4245
- #isCurrentVersion(/** @type {express.Request} */ request) {
4246
- const tag = /"(.*)"/.exec(request.get('If-None-Match'));
4247
- return tag && tag[1] === String(this.#version);
4248
- }
4249
-
4250
- #tryLongPolling(/** @type {express.Request} */ request) {
4251
- const time = this.#getPollingTime(request);
4252
- if (time == null) {
4253
- return { status: 304 };
4254
- }
4255
-
4256
- return this.#waitForChange(time);
4257
- }
4258
-
4259
- #getPollingTime(/** @type {express.Request} */ request) {
4260
- const wait = /\bwait=(\d+)/.exec(request.get('Prefer'));
4261
- return wait != null ? Number(wait[1]) : null;
4262
- }
4263
-
4264
- #waitForChange(/** @type {number} */ time) {
4265
- return new Promise((resolve) => {
4266
- this.#waiting.push(resolve);
4267
- setTimeout(() => {
4268
- if (this.#waiting.includes(resolve)) {
4269
- this.#waiting = this.#waiting.filter((r) => r !== resolve);
4270
- resolve({ status: 304 });
4271
- }
4272
- }, time * 1000);
4273
- });
4274
- }
4275
-
4276
- async #getResponse() {
4277
- const data = await this.#getData();
4278
- const body = JSON.stringify(data);
4279
- return {
4280
- headers: {
4281
- 'Content-Type': 'application/json',
4282
- ETag: `"${this.#version}"`,
4283
- 'Cache-Control': 'no-store',
4284
- },
4285
- body,
4286
- };
4287
- }
4288
- }
4289
-
4290
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
4291
-
4292
- /**
4293
- * @import http from 'node:http'
4294
- */
4295
-
4296
- /**
4297
- * An object for sending
4298
- * [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
4299
- */
4300
- class SseEmitter {
4301
- /** @type {?number} */ #timeout;
4302
- /** @type {http.ServerResponse|undefined} */ #response;
4303
-
4304
- /**
4305
- * Creates a new SSE emitter with an optional timeout.
4306
- *
4307
- * @param {number} [timeout] The timeout in milliseconds after which the
4308
- * connection will be closed.
4309
- */
4310
- constructor(timeout) {
4311
- this.#timeout = timeout;
4312
- }
4313
-
4314
- /**
4315
- * The timeout in milliseconds after which the connection will be closed or
4316
- * undefined if no timeout is set.
4317
- *
4318
- * @type {number|undefined}
4319
- */
4320
- get timeout() {
4321
- return this.#timeout;
4322
- }
4323
-
4324
- /**
4325
- * Sets and extends the response object for sending Server-Sent Events.
4326
- *
4327
- * @param {http.ServerResponse} outputMessage The response object to use.
4328
- */
4329
- extendResponse(outputMessage) {
4330
- // TODO check HTTP version, is it HTTP/2 when using EventSource?
4331
- outputMessage.statusCode = 200;
4332
- this.#response = outputMessage
4333
- .setHeader('Content-Type', 'text/event-stream')
4334
- .setHeader('Cache-Control', 'no-cache')
4335
- .setHeader('Keep-Alive', 'timeout=60')
4336
- .setHeader('Connection', 'keep-alive');
4337
-
4338
- if (this.timeout != null) {
4339
- const timeoutId = setTimeout(() => this.#close(), this.timeout);
4340
- this.#response.addListener('close', () => clearTimeout(timeoutId));
4341
- }
4342
- }
4343
-
4344
- /**
4345
- * Sends a SSE event.
4346
- *
4347
- * @param {object} event The event to send.
4348
- * @param {string} [event.id] Add a SSE "id" line.
4349
- * @param {string} [event.name] Add a SSE "event" line.
4350
- * @param {number} [event.reconnectTime] Add a SSE "retry" line.
4351
- * @param {string} [event.comment] Add a SSE "comment" line.
4352
- * @param {string|object} [event.data] Add a SSE "data" line.
4353
- */
4354
- send({ id, name, reconnectTime, comment, data } = {}) {
4355
- if (comment != null) {
4356
- this.#response.write(`: ${comment}\n`);
4357
- }
4358
-
4359
- if (name != null) {
4360
- this.#response.write(`event: ${name}\n`);
4361
- }
4362
-
4363
- if (data != null) {
4364
- if (typeof data === 'object') {
4365
- data = JSON.stringify(data);
4366
- } else {
4367
- data = String(data).replaceAll('\n', '\ndata: ');
4368
- }
4369
- this.#response.write(`data: ${data}\n`);
4370
- }
4371
-
4372
- if (id != null) {
4373
- this.#response.write(`id: ${id}\n`);
4374
- }
4375
-
4376
- if (reconnectTime != null) {
4377
- this.#response.write(`retry: ${reconnectTime}\n`);
4378
- }
4379
-
4380
- this.#response.write('\n');
4381
- }
4382
-
4383
- /**
4384
- * Simulates a timeout.
4385
- */
4386
- simulateTimeout() {
4387
- this.#close();
4388
- }
4389
-
4390
- #close() {
4391
- this.#response.end();
4392
- }
4393
- }
4394
-
4395
- exports.ActuatorController = ActuatorController;
4396
- exports.Clock = Clock;
4397
- exports.Color = Color;
4398
- exports.CompositeHealth = CompositeHealth;
4399
- exports.ConfigurableResponses = ConfigurableResponses;
4400
- exports.ConfigurationProperties = ConfigurationProperties;
4401
- exports.ConsoleHandler = ConsoleHandler;
4402
- exports.Counter = Counter;
4403
- exports.Duration = Duration;
4404
- exports.Enum = Enum;
4405
- exports.FeatureToggle = FeatureToggle;
4406
- exports.FileHandler = FileHandler;
4407
- exports.Formatter = Formatter;
4408
- exports.HEARTBEAT_TYPE = HEARTBEAT_TYPE;
4409
- exports.Handler = Handler;
4410
- exports.Health = Health;
4411
- exports.HealthContributorRegistry = HealthContributorRegistry;
4412
- exports.HealthEndpoint = HealthEndpoint;
4413
- exports.HttpCodeStatusMapper = HttpCodeStatusMapper;
4414
- exports.JsonFormatter = JsonFormatter;
4415
- exports.Level = Level;
4416
- exports.Line2D = Line2D;
4417
- exports.LogRecord = LogRecord;
4418
- exports.Logger = Logger;
4419
- exports.LongPolling = LongPolling;
4420
- exports.LongPollingClient = LongPollingClient;
4421
- exports.MessageClient = MessageClient;
4422
- exports.Meter = Meter;
4423
- exports.MeterId = MeterId;
4424
- exports.MeterRegistry = MeterRegistry;
4425
- exports.MeterType = MeterType;
4426
- exports.OutputTracker = OutputTracker;
4427
- exports.Random = Random;
4428
- exports.ServiceLocator = ServiceLocator;
4429
- exports.SimpleFormatter = SimpleFormatter;
4430
- exports.SimpleHttpCodeStatusMapper = SimpleHttpCodeStatusMapper;
4431
- exports.SimpleStatusAggregator = SimpleStatusAggregator;
4432
- exports.SseClient = SseClient;
4433
- exports.SseEmitter = SseEmitter;
4434
- exports.Status = Status;
4435
- exports.StatusAggregator = StatusAggregator;
4436
- exports.StopWatch = StopWatch;
4437
- exports.Store = Store;
4438
- exports.Timer = Timer;
4439
- exports.TimerTask = TimerTask;
4440
- exports.ValidationError = ValidationError;
4441
- exports.Vector2D = Vector2D;
4442
- exports.WebSocketClient = WebSocketClient;
4443
- exports.assertNotNull = assertNotNull;
4444
- exports.createStore = createStore;
4445
- exports.deepMerge = deepMerge;
4446
- exports.ensureAnything = ensureAnything;
4447
- exports.ensureArguments = ensureArguments;
4448
- exports.ensureItemType = ensureItemType;
4449
- exports.ensureNonEmpty = ensureNonEmpty;
4450
- exports.ensureThat = ensureThat;
4451
- exports.ensureType = ensureType;
4452
- exports.ensureUnreachable = ensureUnreachable;
4453
- exports.reply = reply;
4454
- exports.runSafe = runSafe;
4455
- exports.sleep = sleep;