@muspellheim/shared 0.4.0 → 0.6.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.
- package/.prettierignore +3 -0
- package/.prettierrc +5 -0
- package/README.md +9 -7
- package/deno.json +4 -1
- package/deno.mk +68 -0
- package/eslint.config.js +23 -0
- package/lib/assert.js +2 -2
- package/lib/browser/index.js +0 -2
- package/lib/color.js +20 -22
- package/lib/health.js +3 -7
- package/lib/index.js +2 -1
- package/lib/lang.js +3 -5
- package/lib/logging.js +11 -10
- package/lib/long-polling-client.js +21 -10
- package/lib/message-client.js +0 -6
- package/lib/messages.js +68 -0
- package/lib/metrics.js +1 -1
- package/lib/node/actuator-controller.js +7 -8
- package/lib/node/configuration-properties.js +123 -86
- package/lib/node/handler.js +4 -2
- package/lib/node/index.js +1 -2
- package/lib/node/logging.js +4 -1
- package/lib/node/long-polling.js +2 -0
- package/lib/node/static-files-controller.js +15 -0
- package/lib/sse-client.js +21 -10
- package/lib/time.js +13 -11
- package/lib/util.js +45 -26
- package/lib/validation.js +13 -22
- package/lib/vector.js +1 -1
- package/lib/vitest/equality-testers.js +19 -0
- package/lib/vitest/index.js +1 -0
- package/lib/web-socket-client.js +26 -9
- package/package.json +19 -9
- package/tsconfig.json +13 -0
- 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;
|