@muspellheim/shared 0.6.1 → 0.7.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/LICENSE.txt +1 -1
- package/README.md +11 -13
- package/dist/shared.d.ts +423 -0
- package/dist/shared.js +535 -0
- package/dist/shared.umd.cjs +1 -0
- package/package.json +27 -23
- package/.prettierignore +0 -3
- package/.prettierrc +0 -5
- package/deno.json +0 -15
- package/deno.mk +0 -68
- package/eslint.config.js +0 -23
- package/lib/assert.js +0 -15
- package/lib/browser/components.js +0 -165
- package/lib/browser/index.js +0 -3
- package/lib/color.js +0 -137
- package/lib/configurable-responses.js +0 -69
- package/lib/feature-toggle.js +0 -9
- package/lib/health.js +0 -510
- package/lib/index.js +0 -23
- package/lib/lang.js +0 -100
- package/lib/logging.js +0 -599
- package/lib/long-polling-client.js +0 -186
- package/lib/message-client.js +0 -68
- package/lib/messages.js +0 -68
- package/lib/metrics.js +0 -120
- package/lib/node/actuator-controller.js +0 -102
- package/lib/node/configuration-properties.js +0 -291
- package/lib/node/handler.js +0 -25
- package/lib/node/index.js +0 -9
- package/lib/node/logging.js +0 -60
- package/lib/node/long-polling.js +0 -83
- package/lib/node/sse-emitter.js +0 -104
- package/lib/node/static-files-controller.js +0 -15
- package/lib/output-tracker.js +0 -89
- package/lib/service-locator.js +0 -44
- package/lib/sse-client.js +0 -163
- package/lib/stop-watch.js +0 -54
- package/lib/store.js +0 -129
- package/lib/time.js +0 -445
- package/lib/util.js +0 -380
- package/lib/validation.js +0 -290
- package/lib/vector.js +0 -194
- package/lib/vitest/equality-testers.js +0 -19
- package/lib/vitest/index.js +0 -1
- package/lib/web-socket-client.js +0 -262
- package/tsconfig.json +0 -13
package/lib/util.js
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Contains several miscellaneous utility classes.
|
|
5
|
-
*
|
|
6
|
-
* Ported from
|
|
7
|
-
* [Java Util](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/package-summary.html).
|
|
8
|
-
*
|
|
9
|
-
* @module
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { Clock } from './time.js';
|
|
13
|
-
|
|
14
|
-
// TODO deep equals
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Creates a deep copy of a value.
|
|
18
|
-
*
|
|
19
|
-
* @param {*} value The value to copy.
|
|
20
|
-
* @returns {*} The deep copy of the value.
|
|
21
|
-
*/
|
|
22
|
-
export function deepCopy(value) {
|
|
23
|
-
return JSON.parse(JSON.stringify(value));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Merges two objects deeply.
|
|
28
|
-
*
|
|
29
|
-
* @param {*} target The object to merge into.
|
|
30
|
-
* @param {*} source The object to merge.
|
|
31
|
-
* @returns {*} The merged object.
|
|
32
|
-
*/
|
|
33
|
-
export function deepMerge(target, source) {
|
|
34
|
-
if (source === undefined) {
|
|
35
|
-
return target;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof source !== 'object' || source === null) {
|
|
39
|
-
return source;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (Array.isArray(target) && Array.isArray(source)) {
|
|
43
|
-
for (const item of source) {
|
|
44
|
-
const element = deepMerge(undefined, item);
|
|
45
|
-
target.push(element);
|
|
46
|
-
}
|
|
47
|
-
return target;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
for (const key in source) {
|
|
51
|
-
if (typeof target !== 'object' || target === null) {
|
|
52
|
-
target = {};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
target[key] = deepMerge(target[key], source[key]);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return target;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* An instance of `Random` is used to generate random numbers.
|
|
63
|
-
*/
|
|
64
|
-
export class Random {
|
|
65
|
-
static create() {
|
|
66
|
-
return new Random();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** @hideconstructor */
|
|
70
|
-
constructor() {}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Returns a random boolean value.
|
|
74
|
-
*
|
|
75
|
-
* @param {number} [probabilityOfUndefined=0.0] The probability of returning
|
|
76
|
-
* `undefined`.
|
|
77
|
-
* @return {boolean|undefined} A random boolean between `origin` (inclusive)
|
|
78
|
-
* and `bound` (exclusive) or undefined.
|
|
79
|
-
*/
|
|
80
|
-
nextBoolean(probabilityOfUndefined = 0.0) {
|
|
81
|
-
return this.#randomOptional(
|
|
82
|
-
() => Math.random() < 0.5,
|
|
83
|
-
probabilityOfUndefined,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Returns a random integer between `origin` (inclusive) and `bound`
|
|
89
|
-
* (exclusive).
|
|
90
|
-
*
|
|
91
|
-
* @param {number} [origin=0] The least value that can be returned.
|
|
92
|
-
* @param {number} [bound=1] The upper bound (exclusive) for the returned
|
|
93
|
-
* value.
|
|
94
|
-
* @param {number} [probabilityOfUndefined=0.0] The probability of returning
|
|
95
|
-
* `undefined`.
|
|
96
|
-
* @return {number|undefined} A random integer between `origin` (inclusive)
|
|
97
|
-
* and `bound` (exclusive) or undefined.
|
|
98
|
-
*/
|
|
99
|
-
nextInt(origin = 0, bound = 1, probabilityOfUndefined = 0.0) {
|
|
100
|
-
return this.#randomOptional(
|
|
101
|
-
() => Math.floor(this.nextFloat(origin, bound)),
|
|
102
|
-
probabilityOfUndefined,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Returns a random float between `origin` (inclusive) and `bound`
|
|
108
|
-
* (exclusive).
|
|
109
|
-
*
|
|
110
|
-
* @param {number} [origin=0.0] The least value that can be returned.
|
|
111
|
-
* @param {number} [bound=1.0] The upper bound (exclusive) for the returned
|
|
112
|
-
* value.
|
|
113
|
-
* @param {number} [probabilityOfUndefined=0.0] The probability of returning
|
|
114
|
-
* `undefined`.
|
|
115
|
-
* @return {number|undefined} A random float between `origin` (inclusive) and
|
|
116
|
-
* `bound` (exclusive) or undefined.
|
|
117
|
-
*/
|
|
118
|
-
nextFloat(origin = 0.0, bound = 1.0, probabilityOfUndefined = 0.0) {
|
|
119
|
-
return this.#randomOptional(
|
|
120
|
-
() => Math.random() * (bound - origin) + origin,
|
|
121
|
-
probabilityOfUndefined,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Returns a random timestamp with optional random offset.
|
|
127
|
-
*
|
|
128
|
-
* @param {number} [maxMillis=0] The maximum offset in milliseconds.
|
|
129
|
-
* @param {number} [probabilityOfUndefined=0.0] The probability of returning
|
|
130
|
-
* `undefined`.
|
|
131
|
-
* @return {Date|undefined} A random timestamp or `undefined`.
|
|
132
|
-
*/
|
|
133
|
-
nextDate(maxMillis = 0, probabilityOfUndefined = 0.0) {
|
|
134
|
-
return this.#randomOptional(() => {
|
|
135
|
-
const now = new Date();
|
|
136
|
-
let t = now.getTime();
|
|
137
|
-
const r = Math.random();
|
|
138
|
-
t += r * maxMillis;
|
|
139
|
-
return new Date(t);
|
|
140
|
-
}, probabilityOfUndefined);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Returns a random value from an array.
|
|
145
|
-
*
|
|
146
|
-
* @param {Array} [values=[]] The array of values.
|
|
147
|
-
* @param {number} [probabilityOfUndefined=0.0] The probability of returning
|
|
148
|
-
* `undefined`.
|
|
149
|
-
* @return {*|undefined} A random value from the array or `undefined`.
|
|
150
|
-
*/
|
|
151
|
-
nextValue(values = [], probabilityOfUndefined = 0.0) {
|
|
152
|
-
return this.#randomOptional(() => {
|
|
153
|
-
const index = new Random().nextInt(0, values.length - 1);
|
|
154
|
-
return values[index];
|
|
155
|
-
}, probabilityOfUndefined);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
#randomOptional(randomFactory, probabilityOfUndefined) {
|
|
159
|
-
const r = Math.random();
|
|
160
|
-
return r < probabilityOfUndefined ? undefined : randomFactory();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const TASK_CREATED = 'created';
|
|
165
|
-
const TASK_SCHEDULED = 'scheduled';
|
|
166
|
-
const TASK_EXECUTED = 'executed';
|
|
167
|
-
const TASK_CANCELLED = 'cancelled';
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* A task that can be scheduled by a {@link Timer}.
|
|
171
|
-
*/
|
|
172
|
-
export class TimerTask {
|
|
173
|
-
_state = TASK_CREATED;
|
|
174
|
-
_nextExecutionTime = 0;
|
|
175
|
-
_period = 0;
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Runs the task.
|
|
179
|
-
*
|
|
180
|
-
* @abstract
|
|
181
|
-
*/
|
|
182
|
-
run() {
|
|
183
|
-
throw new Error('Method not implemented.');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Cancels the task.
|
|
188
|
-
*
|
|
189
|
-
* @return {boolean} `true` if this task was scheduled for one-time execution
|
|
190
|
-
* and has not yet run, or this task was scheduled for repeated execution.
|
|
191
|
-
* Return `false` if the task was scheduled for one-time execution and has
|
|
192
|
-
* already run, or if the task was never scheduled, or if the task was
|
|
193
|
-
* already cancelled.
|
|
194
|
-
*/
|
|
195
|
-
cancel() {
|
|
196
|
-
const result = this._state === TASK_SCHEDULED;
|
|
197
|
-
this._state = TASK_CANCELLED;
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Returns scheduled execution time of the most recent actual execution of
|
|
203
|
-
* this task.
|
|
204
|
-
*
|
|
205
|
-
* Example usage:
|
|
206
|
-
*
|
|
207
|
-
* ```javascript
|
|
208
|
-
* run() {
|
|
209
|
-
* if (Date.now() - scheduledExecutionTime() >= MAX_TARDINESS) {
|
|
210
|
-
* return; // Too late; skip this execution.
|
|
211
|
-
* }
|
|
212
|
-
* // Perform the task
|
|
213
|
-
* }
|
|
214
|
-
*
|
|
215
|
-
* ```
|
|
216
|
-
*
|
|
217
|
-
* @return {number} The time in milliseconds since the epoch, undefined if
|
|
218
|
-
* the task has not yet run for the first time.
|
|
219
|
-
*/
|
|
220
|
-
scheduledExecutionTime() {
|
|
221
|
-
return this._period < 0
|
|
222
|
-
? this._nextExecutionTime + this._period
|
|
223
|
-
: this._nextExecutionTime - this._period;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* A timer that schedules and cancels tasks.
|
|
229
|
-
*
|
|
230
|
-
* Tasks may be scheduled for one-time execution or for repeated execution at
|
|
231
|
-
* regular intervals.
|
|
232
|
-
*/
|
|
233
|
-
export class Timer extends EventTarget {
|
|
234
|
-
/**
|
|
235
|
-
* Returns a new `Timer`.
|
|
236
|
-
*/
|
|
237
|
-
static create() {
|
|
238
|
-
return new Timer(Clock.system(), globalThis);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Returns a new `Timer` for testing without side effects.
|
|
243
|
-
*/
|
|
244
|
-
static createNull({ clock = Clock.fixed() } = {}) {
|
|
245
|
-
return new Timer(clock, new TimeoutStub());
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** @type {Clock} */
|
|
249
|
-
#clock;
|
|
250
|
-
|
|
251
|
-
/** @type {globalThis} */
|
|
252
|
-
#global;
|
|
253
|
-
|
|
254
|
-
/** @type {TimerTask[]} */
|
|
255
|
-
_queue;
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Returns a new `Timer`.
|
|
259
|
-
*
|
|
260
|
-
* @param {Clock=} clock The clock to use.
|
|
261
|
-
* @param {globalThis} global The global object.
|
|
262
|
-
*/
|
|
263
|
-
constructor(clock = Clock.system(), global = globalThis) {
|
|
264
|
-
super();
|
|
265
|
-
this.#clock = clock;
|
|
266
|
-
this.#global = global;
|
|
267
|
-
this._queue = [];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Schedules a task for repeated execution at regular intervals.
|
|
272
|
-
*
|
|
273
|
-
* @param {TimerTask} task The task to execute.
|
|
274
|
-
* @param {number|Date} delayOrTime The delay before the first execution, in
|
|
275
|
-
* milliseconds or the time of the first execution.
|
|
276
|
-
* @param {number} [period=0] The interval between executions, in
|
|
277
|
-
* milliseconds; 0 means single execution.
|
|
278
|
-
*/
|
|
279
|
-
schedule(task, delayOrTime, period = 0) {
|
|
280
|
-
this.#doSchedule(task, delayOrTime, -period);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Schedule a task for repeated fixed-rate execution.
|
|
285
|
-
*
|
|
286
|
-
* @param {TimerTask} task The task to execute.
|
|
287
|
-
* @param {number|Date} delayOrTime The delay before the first execution, in
|
|
288
|
-
* milliseconds or the time of the first.
|
|
289
|
-
* @param {number} period The interval between executions, in milliseconds.
|
|
290
|
-
*/
|
|
291
|
-
scheduleAtFixedRate(task, delayOrTime, period) {
|
|
292
|
-
this.#doSchedule(task, delayOrTime, period);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Cancels all scheduled tasks.
|
|
297
|
-
*/
|
|
298
|
-
cancel() {
|
|
299
|
-
for (const task of this._queue) {
|
|
300
|
-
task.cancel();
|
|
301
|
-
}
|
|
302
|
-
this._queue = [];
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Removes all cancelled tasks from the task queue.
|
|
307
|
-
*
|
|
308
|
-
* @return {number} The number of tasks removed from the task queue.
|
|
309
|
-
*/
|
|
310
|
-
purge() {
|
|
311
|
-
let result = 0;
|
|
312
|
-
for (let i = 0; i < this._queue.length; i++) {
|
|
313
|
-
if (this._queue[i]._state === TASK_CANCELLED) {
|
|
314
|
-
this._queue.splice(i, 1);
|
|
315
|
-
i--;
|
|
316
|
-
result++;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return result;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Simulates the execution of a task.
|
|
324
|
-
*
|
|
325
|
-
* @param {object} options The simulation options.
|
|
326
|
-
* @param {number} [options.ticks=1000] The number of milliseconds to advance
|
|
327
|
-
* the clock.
|
|
328
|
-
*/
|
|
329
|
-
simulateTaskExecution({ ticks = 1000 } = {}) {
|
|
330
|
-
this.#clock.add(ticks);
|
|
331
|
-
this.#runMainLoop();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
#doSchedule(task, delayOrTime, period) {
|
|
335
|
-
if (delayOrTime instanceof Date) {
|
|
336
|
-
task._nextExecutionTime = delayOrTime.getTime();
|
|
337
|
-
} else {
|
|
338
|
-
task._nextExecutionTime = this.#clock.millis() + delayOrTime;
|
|
339
|
-
}
|
|
340
|
-
task._period = period;
|
|
341
|
-
task._state = TASK_SCHEDULED;
|
|
342
|
-
this._queue.push(task);
|
|
343
|
-
this._queue.sort((a, b) => b._nextExecutionTime - a._nextExecutionTime);
|
|
344
|
-
if (this._queue[0] === task) {
|
|
345
|
-
this.#runMainLoop();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
#runMainLoop() {
|
|
350
|
-
if (this._queue.length === 0) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const task = this._queue[0];
|
|
355
|
-
if (task._state === TASK_CANCELLED) {
|
|
356
|
-
this._queue.shift();
|
|
357
|
-
return this.#runMainLoop();
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const now = this.#clock.millis();
|
|
361
|
-
const executionTime = task._nextExecutionTime;
|
|
362
|
-
const taskFired = executionTime <= now;
|
|
363
|
-
if (taskFired) {
|
|
364
|
-
if (task._period === 0) {
|
|
365
|
-
this._queue.shift();
|
|
366
|
-
task._state = TASK_EXECUTED;
|
|
367
|
-
} else {
|
|
368
|
-
task._nextExecutionTime =
|
|
369
|
-
task._period < 0 ? now - task._period : executionTime + task._period;
|
|
370
|
-
}
|
|
371
|
-
task.run();
|
|
372
|
-
} else {
|
|
373
|
-
this.#global.setTimeout(() => this.#runMainLoop(), executionTime - now);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
class TimeoutStub {
|
|
379
|
-
setTimeout() {}
|
|
380
|
-
}
|
package/lib/validation.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
|
|
2
|
-
|
|
3
|
-
// TODO Use JSON schema to validate like Java Bean Validation?
|
|
4
|
-
|
|
5
|
-
export class ValidationError extends Error {
|
|
6
|
-
constructor(message) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = 'ValidationError';
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** @return {never} */
|
|
13
|
-
export function ensureUnreachable(message = 'Unreachable code executed.') {
|
|
14
|
-
throw new Error(message);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function ensureThat(
|
|
18
|
-
value,
|
|
19
|
-
predicate,
|
|
20
|
-
message = 'Expected predicate is not true.',
|
|
21
|
-
) {
|
|
22
|
-
const condition = predicate(value);
|
|
23
|
-
if (!condition) {
|
|
24
|
-
throw new ValidationError(message);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function ensureAnything(value, { name = 'value' } = {}) {
|
|
31
|
-
if (value == null) {
|
|
32
|
-
throw new ValidationError(`The ${name} is required, but it was ${value}.`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function ensureNonEmpty(value, { name = 'value' } = {}) {
|
|
39
|
-
const valueType = getType(value);
|
|
40
|
-
if (
|
|
41
|
-
(valueType === String && value.length === 0) ||
|
|
42
|
-
(valueType === Array && value.length === 0) ||
|
|
43
|
-
(valueType === Object && Object.keys(value).length === 0)
|
|
44
|
-
) {
|
|
45
|
-
throw new ValidationError(
|
|
46
|
-
`The ${name} must not be empty, but it was ${JSON.stringify(value)}.`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/*
|
|
54
|
-
* type: undefined | null | Boolean | Number | BigInt | String | Symbol | Function | Object | Array | Enum | constructor | Record<string, type>
|
|
55
|
-
* expectedType: type | [ type ]
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
export function ensureType(value, expectedType, { name = 'value' } = {}) {
|
|
59
|
-
const result = checkType(value, expectedType, { name });
|
|
60
|
-
if (result.error) {
|
|
61
|
-
throw new ValidationError(result.error);
|
|
62
|
-
}
|
|
63
|
-
return result.value;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function ensureItemType(array, expectedType, { name = 'value' } = {}) {
|
|
67
|
-
const result = checkType(array, Array, { name });
|
|
68
|
-
if (result.error) {
|
|
69
|
-
throw new ValidationError(result.error);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
array.forEach((item, index) => {
|
|
73
|
-
const result = checkType(item, expectedType, {
|
|
74
|
-
name: `${name}.${index}`,
|
|
75
|
-
});
|
|
76
|
-
if (result.error) {
|
|
77
|
-
throw new ValidationError(result.error);
|
|
78
|
-
}
|
|
79
|
-
array[index] = result.value;
|
|
80
|
-
});
|
|
81
|
-
return array;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function ensureArguments(args, expectedTypes = [], names = []) {
|
|
85
|
-
ensureThat(
|
|
86
|
-
expectedTypes,
|
|
87
|
-
Array.isArray,
|
|
88
|
-
'The expectedTypes must be an array.',
|
|
89
|
-
);
|
|
90
|
-
ensureThat(names, Array.isArray, 'The names must be an array.');
|
|
91
|
-
if (args.length > expectedTypes.length) {
|
|
92
|
-
throw new ValidationError(
|
|
93
|
-
`Too many arguments: expected ${expectedTypes.length}, but got ${args.length}.`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
expectedTypes.forEach((expectedType, index) => {
|
|
97
|
-
const name = names[index] ? names[index] : `argument #${index + 1}`;
|
|
98
|
-
ensureType(args[index], expectedType, { name });
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function checkType(value, expectedType, { name = 'value' } = {}) {
|
|
103
|
-
const valueType = getType(value);
|
|
104
|
-
|
|
105
|
-
// Check built-in types
|
|
106
|
-
if (
|
|
107
|
-
expectedType === undefined ||
|
|
108
|
-
expectedType === null ||
|
|
109
|
-
expectedType === Boolean ||
|
|
110
|
-
expectedType === Number ||
|
|
111
|
-
expectedType === BigInt ||
|
|
112
|
-
expectedType === String ||
|
|
113
|
-
expectedType === Symbol ||
|
|
114
|
-
expectedType === Function ||
|
|
115
|
-
expectedType === Object ||
|
|
116
|
-
expectedType === Array
|
|
117
|
-
) {
|
|
118
|
-
if (valueType === expectedType) {
|
|
119
|
-
return { value };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
error: `The ${name} must be ${describe(expectedType, {
|
|
124
|
-
articles: true,
|
|
125
|
-
})}, but it was ${describe(valueType, { articles: true })}.`,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Check enum types
|
|
130
|
-
if (Object.getPrototypeOf(expectedType).name === 'Enum') {
|
|
131
|
-
try {
|
|
132
|
-
return { value: expectedType.valueOf(String(value).toUpperCase()) };
|
|
133
|
-
} catch {
|
|
134
|
-
return {
|
|
135
|
-
error: `The ${name} must be ${describe(expectedType, {
|
|
136
|
-
articles: true,
|
|
137
|
-
})}, but it was ${describe(valueType, { articles: true })}.`,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Check constructor types
|
|
143
|
-
if (typeof expectedType === 'function') {
|
|
144
|
-
if (value instanceof expectedType) {
|
|
145
|
-
return { value };
|
|
146
|
-
} else {
|
|
147
|
-
const convertedValue = new expectedType(value);
|
|
148
|
-
if (String(convertedValue).toLowerCase().startsWith('invalid')) {
|
|
149
|
-
let error = `The ${name} must be a valid ${describe(
|
|
150
|
-
expectedType,
|
|
151
|
-
)}, but it was ${describe(valueType, { articles: true })}`;
|
|
152
|
-
if (valueType != null) {
|
|
153
|
-
error += `: ${JSON.stringify(value)}`;
|
|
154
|
-
}
|
|
155
|
-
error += '.';
|
|
156
|
-
return { error };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { value: convertedValue };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Check one of multiple types
|
|
164
|
-
if (Array.isArray(expectedType)) {
|
|
165
|
-
for (const type of expectedType) {
|
|
166
|
-
const result = checkType(value, type, { name });
|
|
167
|
-
if (!result.error) {
|
|
168
|
-
return { value };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
error: `The ${name} must be ${describe(expectedType, {
|
|
174
|
-
articles: true,
|
|
175
|
-
})}, but it was ${describe(valueType, { articles: true })}.`,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (typeof expectedType === 'object') {
|
|
180
|
-
// Check struct types
|
|
181
|
-
const result = checkType(value, Object, { name });
|
|
182
|
-
if (result.error) {
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
for (const key in expectedType) {
|
|
187
|
-
const result = checkType(value[key], expectedType[key], {
|
|
188
|
-
name: `${name}.${key}`,
|
|
189
|
-
});
|
|
190
|
-
if (result.error) {
|
|
191
|
-
return result;
|
|
192
|
-
}
|
|
193
|
-
value[key] = result.value;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return { value };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
ensureUnreachable();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function getType(value) {
|
|
203
|
-
if (value === null) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
if (Array.isArray(value)) {
|
|
207
|
-
return Array;
|
|
208
|
-
}
|
|
209
|
-
if (Number.isNaN(value)) {
|
|
210
|
-
return NaN;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
switch (typeof value) {
|
|
214
|
-
case 'undefined':
|
|
215
|
-
return undefined;
|
|
216
|
-
case 'boolean':
|
|
217
|
-
return Boolean;
|
|
218
|
-
case 'number':
|
|
219
|
-
return Number;
|
|
220
|
-
case 'bigint':
|
|
221
|
-
return BigInt;
|
|
222
|
-
case 'string':
|
|
223
|
-
return String;
|
|
224
|
-
case 'symbol':
|
|
225
|
-
return Symbol;
|
|
226
|
-
case 'function':
|
|
227
|
-
return Function;
|
|
228
|
-
case 'object':
|
|
229
|
-
return Object;
|
|
230
|
-
default:
|
|
231
|
-
ensureUnreachable(`Unknown typeof value: ${typeof value}.`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function describe(type, { articles = false } = {}) {
|
|
236
|
-
if (Array.isArray(type)) {
|
|
237
|
-
const types = type.map((t) => describe(t, { articles }));
|
|
238
|
-
if (types.length <= 2) {
|
|
239
|
-
return types.join(' or ');
|
|
240
|
-
} else {
|
|
241
|
-
const allButLast = types.slice(0, -1);
|
|
242
|
-
const last = types.at(-1);
|
|
243
|
-
return allButLast.join(', ') + ', or ' + last;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (Number.isNaN(type)) {
|
|
248
|
-
return 'NaN';
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
let name;
|
|
252
|
-
switch (type) {
|
|
253
|
-
case null:
|
|
254
|
-
return 'null';
|
|
255
|
-
case undefined:
|
|
256
|
-
return 'undefined';
|
|
257
|
-
case Array:
|
|
258
|
-
name = 'array';
|
|
259
|
-
break;
|
|
260
|
-
case Boolean:
|
|
261
|
-
name = 'boolean';
|
|
262
|
-
break;
|
|
263
|
-
case Number:
|
|
264
|
-
name = 'number';
|
|
265
|
-
break;
|
|
266
|
-
case BigInt:
|
|
267
|
-
name = 'bigint';
|
|
268
|
-
break;
|
|
269
|
-
case String:
|
|
270
|
-
name = 'string';
|
|
271
|
-
break;
|
|
272
|
-
case Symbol:
|
|
273
|
-
name = 'symbol';
|
|
274
|
-
break;
|
|
275
|
-
case Function:
|
|
276
|
-
name = 'function';
|
|
277
|
-
break;
|
|
278
|
-
case Object:
|
|
279
|
-
name = 'object';
|
|
280
|
-
break;
|
|
281
|
-
default:
|
|
282
|
-
name = type.name;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (articles) {
|
|
287
|
-
name = 'aeiou'.includes(name[0].toLowerCase()) ? `an ${name}` : `a ${name}`;
|
|
288
|
-
}
|
|
289
|
-
return name;
|
|
290
|
-
}
|