@vpmedia/simplify 1.74.0 → 1.75.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/CHANGELOG.md +60 -0
- package/dist/const/http_status.d.ts +66 -0
- package/dist/const/http_status.d.ts.map +1 -0
- package/dist/const/http_status.js +133 -0
- package/dist/const/http_status.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/{src → dist}/index.js +3 -20
- package/dist/index.js.map +1 -0
- package/dist/logging/AbstractLogHandler.d.ts +17 -0
- package/dist/logging/AbstractLogHandler.d.ts.map +1 -0
- package/dist/logging/AbstractLogHandler.js +16 -0
- package/dist/logging/AbstractLogHandler.js.map +1 -0
- package/dist/logging/ConsoleLogHandler.d.ts +13 -0
- package/dist/logging/ConsoleLogHandler.d.ts.map +1 -0
- package/dist/logging/ConsoleLogHandler.js +41 -0
- package/dist/logging/ConsoleLogHandler.js.map +1 -0
- package/dist/logging/Logger.d.ts +19 -0
- package/dist/logging/Logger.d.ts.map +1 -0
- package/dist/logging/Logger.js +51 -0
- package/dist/logging/Logger.js.map +1 -0
- package/dist/logging/OpenTelemetryLogHandler.d.ts +15 -0
- package/dist/logging/OpenTelemetryLogHandler.d.ts.map +1 -0
- package/dist/logging/OpenTelemetryLogHandler.js +21 -0
- package/dist/logging/OpenTelemetryLogHandler.js.map +1 -0
- package/dist/logging/SentryLogHandler.d.ts +13 -0
- package/dist/logging/SentryLogHandler.d.ts.map +1 -0
- package/dist/logging/SentryLogHandler.js +36 -0
- package/dist/logging/SentryLogHandler.js.map +1 -0
- package/dist/logging/const.d.ts +14 -0
- package/dist/logging/const.d.ts.map +1 -0
- package/dist/logging/const.js +21 -0
- package/dist/logging/const.js.map +1 -0
- package/dist/logging/util.d.ts +14 -0
- package/dist/logging/util.d.ts.map +1 -0
- package/dist/logging/util.js +34 -0
- package/dist/logging/util.js.map +1 -0
- package/dist/pagelifecycle/const.d.ts +15 -0
- package/dist/pagelifecycle/const.d.ts.map +1 -0
- package/dist/pagelifecycle/const.js +27 -0
- package/dist/pagelifecycle/const.js.map +1 -0
- package/dist/pagelifecycle/typedef.d.ts +4 -0
- package/dist/pagelifecycle/typedef.d.ts.map +1 -0
- package/dist/pagelifecycle/typedef.js +2 -0
- package/dist/pagelifecycle/typedef.js.map +1 -0
- package/dist/pagelifecycle/util.d.ts +31 -0
- package/dist/pagelifecycle/util.d.ts.map +1 -0
- package/dist/pagelifecycle/util.js +117 -0
- package/dist/pagelifecycle/util.js.map +1 -0
- package/dist/typecheck/TypeCheckError.d.ts +11 -0
- package/dist/typecheck/TypeCheckError.d.ts.map +1 -0
- package/dist/typecheck/TypeCheckError.js +12 -0
- package/dist/typecheck/TypeCheckError.js.map +1 -0
- package/dist/typecheck/TypeChecker.d.ts +27 -0
- package/dist/typecheck/TypeChecker.d.ts.map +1 -0
- package/dist/typecheck/TypeChecker.js +69 -0
- package/dist/typecheck/TypeChecker.js.map +1 -0
- package/dist/typecheck/util.d.ts +17 -0
- package/dist/typecheck/util.d.ts.map +1 -0
- package/dist/typecheck/util.js +42 -0
- package/dist/typecheck/util.js.map +1 -0
- package/dist/util/async.d.ts +13 -0
- package/dist/util/async.d.ts.map +1 -0
- package/dist/util/async.js +39 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/error.d.ts +16 -0
- package/dist/util/error.d.ts.map +1 -0
- package/dist/util/error.js +27 -0
- package/dist/util/error.js.map +1 -0
- package/dist/util/event_emitter.d.ts +42 -0
- package/dist/util/event_emitter.d.ts.map +1 -0
- package/dist/util/event_emitter.js +127 -0
- package/dist/util/event_emitter.js.map +1 -0
- package/dist/util/fetch.d.ts +22 -0
- package/dist/util/fetch.d.ts.map +1 -0
- package/dist/util/fetch.js +75 -0
- package/dist/util/fetch.js.map +1 -0
- package/dist/util/number.d.ts +23 -0
- package/dist/util/number.d.ts.map +1 -0
- package/dist/util/number.js +52 -0
- package/dist/util/number.js.map +1 -0
- package/dist/util/object.d.ts +24 -0
- package/dist/util/object.d.ts.map +1 -0
- package/dist/util/object.js +83 -0
- package/dist/util/object.js.map +1 -0
- package/dist/util/query.d.ts +11 -0
- package/dist/util/query.d.ts.map +1 -0
- package/dist/util/query.js +24 -0
- package/dist/util/query.js.map +1 -0
- package/dist/util/state.d.ts +5 -0
- package/dist/util/state.d.ts.map +1 -0
- package/dist/util/state.js +19 -0
- package/dist/util/state.js.map +1 -0
- package/dist/util/string.d.ts +25 -0
- package/dist/util/string.d.ts.map +1 -0
- package/dist/util/string.js +59 -0
- package/dist/util/string.js.map +1 -0
- package/dist/util/uuid.d.ts +13 -0
- package/dist/util/uuid.d.ts.map +1 -0
- package/dist/util/uuid.js +27 -0
- package/dist/util/uuid.js.map +1 -0
- package/dist/util/validate.d.ts +106 -0
- package/dist/util/validate.d.ts.map +1 -0
- package/dist/util/validate.js +139 -0
- package/dist/util/validate.js.map +1 -0
- package/package.json +31 -16
- package/src/const/http_status.test.ts +7 -0
- package/src/const/{http_status.js → http_status.ts} +1 -1
- package/src/index.ts +51 -0
- package/src/logging/AbstractLogHandler.ts +31 -0
- package/src/logging/{ConsoleLogHandler.js → ConsoleLogHandler.ts} +15 -13
- package/src/logging/Logger.test.ts +69 -0
- package/src/logging/Logger.ts +77 -0
- package/src/logging/OpenTelemetryLogHandler.ts +40 -0
- package/src/logging/SentryLogHandler.ts +44 -0
- package/src/logging/{const.js → const.ts} +1 -1
- package/src/logging/util.test.ts +33 -0
- package/src/logging/util.ts +36 -0
- package/src/pagelifecycle/{const.js → const.ts} +2 -2
- package/src/pagelifecycle/typedef.ts +5 -0
- package/src/pagelifecycle/util.test.ts +99 -0
- package/src/pagelifecycle/{util.js → util.ts} +14 -27
- package/src/typecheck/{TypeCheckError.js → TypeCheckError.ts} +7 -3
- package/src/typecheck/TypeChecker.test.ts +70 -0
- package/src/typecheck/{TypeChecker.js → TypeChecker.ts} +10 -27
- package/src/typecheck/util.test.ts +36 -0
- package/src/typecheck/util.ts +50 -0
- package/src/util/async.test.ts +74 -0
- package/src/util/{async.js → async.ts} +3 -12
- package/src/util/error.test.ts +32 -0
- package/src/util/error.ts +37 -0
- package/src/util/event_emitter.test.ts +228 -0
- package/src/util/event_emitter.ts +147 -0
- package/src/util/fetch.test.ts +62 -0
- package/src/util/{fetch.js → fetch.ts} +40 -31
- package/src/util/number.test.ts +124 -0
- package/src/util/number.ts +58 -0
- package/src/util/object.test.ts +203 -0
- package/src/util/{object.js → object.ts} +14 -21
- package/src/util/query.test.ts +71 -0
- package/src/util/query.ts +35 -0
- package/src/util/state.test.ts +47 -0
- package/src/util/{state.js → state.ts} +3 -6
- package/src/util/string.test.ts +64 -0
- package/src/util/string.ts +65 -0
- package/src/util/uuid.test.ts +53 -0
- package/src/util/uuid.ts +31 -0
- package/src/util/validate.test.ts +309 -0
- package/src/util/validate.ts +230 -0
- package/.vscode/extensions.json +0 -6
- package/.vscode/settings.json +0 -27
- package/src/logging/AbstractLogHandler.js +0 -23
- package/src/logging/Logger.js +0 -115
- package/src/logging/OpenTelemetryLogHandler.js +0 -30
- package/src/logging/SentryLogHandler.js +0 -46
- package/src/logging/util.js +0 -41
- package/src/pagelifecycle/typedef.js +0 -9
- package/src/typecheck/util.js +0 -60
- package/src/util/error.js +0 -33
- package/src/util/event_emitter.js +0 -196
- package/src/util/number.js +0 -118
- package/src/util/query.js +0 -32
- package/src/util/string.js +0 -76
- package/src/util/uuid.js +0 -35
- package/src/util/validate.js +0 -247
- package/types/const/http_status.d.ts +0 -131
- package/types/const/http_status.d.ts.map +0 -1
- package/types/index.d.ts +0 -26
- package/types/index.d.ts.map +0 -1
- package/types/logging/AbstractLogHandler.d.ts +0 -20
- package/types/logging/AbstractLogHandler.d.ts.map +0 -1
- package/types/logging/ConsoleLogHandler.d.ts +0 -9
- package/types/logging/ConsoleLogHandler.d.ts.map +0 -1
- package/types/logging/Logger.d.ts +0 -66
- package/types/logging/Logger.d.ts.map +0 -1
- package/types/logging/OpenTelemetryLogHandler.d.ts +0 -11
- package/types/logging/OpenTelemetryLogHandler.d.ts.map +0 -1
- package/types/logging/SentryLogHandler.d.ts +0 -9
- package/types/logging/SentryLogHandler.d.ts.map +0 -1
- package/types/logging/const.d.ts +0 -14
- package/types/logging/const.d.ts.map +0 -1
- package/types/logging/util.d.ts +0 -4
- package/types/logging/util.d.ts.map +0 -1
- package/types/pagelifecycle/const.d.ts +0 -15
- package/types/pagelifecycle/const.d.ts.map +0 -1
- package/types/pagelifecycle/typedef.d.ts +0 -4
- package/types/pagelifecycle/typedef.d.ts.map +0 -1
- package/types/pagelifecycle/util.d.ts +0 -8
- package/types/pagelifecycle/util.d.ts.map +0 -1
- package/types/typecheck/TypeCheckError.d.ts +0 -13
- package/types/typecheck/TypeCheckError.d.ts.map +0 -1
- package/types/typecheck/TypeChecker.d.ts +0 -40
- package/types/typecheck/TypeChecker.d.ts.map +0 -1
- package/types/typecheck/util.d.ts +0 -4
- package/types/typecheck/util.d.ts.map +0 -1
- package/types/util/async.d.ts +0 -4
- package/types/util/async.d.ts.map +0 -1
- package/types/util/error.d.ts +0 -3
- package/types/util/error.d.ts.map +0 -1
- package/types/util/event_emitter.d.ts +0 -69
- package/types/util/event_emitter.d.ts.map +0 -1
- package/types/util/fetch.d.ts +0 -22
- package/types/util/fetch.d.ts.map +0 -1
- package/types/util/number.d.ts +0 -11
- package/types/util/number.d.ts.map +0 -1
- package/types/util/object.d.ts +0 -6
- package/types/util/object.d.ts.map +0 -1
- package/types/util/query.d.ts +0 -3
- package/types/util/query.d.ts.map +0 -1
- package/types/util/state.d.ts +0 -2
- package/types/util/state.d.ts.map +0 -1
- package/types/util/string.d.ts +0 -7
- package/types/util/string.d.ts.map +0 -1
- package/types/util/uuid.d.ts +0 -4
- package/types/util/uuid.d.ts.map +0 -1
- package/types/util/validate.d.ts +0 -45
- package/types/util/validate.d.ts.map +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { typeChecker } from './TypeChecker.js';
|
|
3
|
+
import { TypeCheckError } from './TypeCheckError.js';
|
|
4
|
+
|
|
5
|
+
const stringValidator = (value: unknown): value is string => typeof value === 'string';
|
|
6
|
+
|
|
7
|
+
describe('TypeChecker', () => {
|
|
8
|
+
it('should check value type correctly', () => {
|
|
9
|
+
const result = typeChecker.check('test', stringValidator);
|
|
10
|
+
expect(result).toBe('test');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should throw TypeCheckError for invalid type', () => {
|
|
14
|
+
expect(() => typeChecker.check(123, stringValidator)).toThrow(TypeCheckError);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should swallow errors when enabled', () => {
|
|
18
|
+
typeChecker.setSwallowErrors(true);
|
|
19
|
+
const result = typeChecker.check(123, stringValidator);
|
|
20
|
+
expect(result).toBe(123);
|
|
21
|
+
typeChecker.setSwallowErrors(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should check array type correctly', () => {
|
|
25
|
+
const result = typeChecker.checkArray(['a', 'b', 'c'], stringValidator);
|
|
26
|
+
expect(result).toEqual(['a', 'b', 'c']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should throw TypeCheckError for invalid array type', () => {
|
|
30
|
+
expect(() => typeChecker.checkArray([1, 2, 3], stringValidator)).toThrow(TypeCheckError);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should swallow array errors when enabled', () => {
|
|
34
|
+
typeChecker.setSwallowErrors(true);
|
|
35
|
+
const result = typeChecker.checkArray([1, 2, 3], stringValidator);
|
|
36
|
+
expect(result).toEqual([1, 2, 3]);
|
|
37
|
+
typeChecker.setSwallowErrors(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should check enum value correctly', () => {
|
|
41
|
+
const choices = ['option1', 'option2', 'option3'];
|
|
42
|
+
const result = typeChecker.checkEnum('option2', choices);
|
|
43
|
+
expect(result).toBe('option2');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should throw TypeCheckError for invalid enum value', () => {
|
|
47
|
+
const choices = ['option1', 'option2', 'option3'];
|
|
48
|
+
expect(() => typeChecker.checkEnum('invalid', choices)).toThrow(TypeCheckError);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should swallow enum errors when enabled', () => {
|
|
52
|
+
typeChecker.setSwallowErrors(true);
|
|
53
|
+
const choices = ['option1', 'option2', 'option3'];
|
|
54
|
+
const result = typeChecker.checkEnum('invalid', choices);
|
|
55
|
+
expect(result).toBe('invalid');
|
|
56
|
+
typeChecker.setSwallowErrors(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should handle number enum values', () => {
|
|
60
|
+
const choices = [1, 2, 3];
|
|
61
|
+
const result = typeChecker.checkEnum(2, choices);
|
|
62
|
+
expect(result).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle mixed string and number enum values', () => {
|
|
66
|
+
const choices = ['option1', 2, 'option3'];
|
|
67
|
+
const result = typeChecker.checkEnum(2, choices);
|
|
68
|
+
expect(result).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { TypeCheckError } from './TypeCheckError.js';
|
|
2
1
|
import { Logger } from '../logging/Logger.js';
|
|
3
|
-
import {
|
|
2
|
+
import { TypeCheckError } from './TypeCheckError.js';
|
|
3
|
+
import { typeCheck, typeCheckArray, typeCheckEnum, type EnumChoices } from './util.js';
|
|
4
4
|
|
|
5
5
|
const logger = new Logger('typechecker');
|
|
6
6
|
|
|
7
7
|
class TypeChecker {
|
|
8
|
-
|
|
9
|
-
static #instance;
|
|
8
|
+
static #instance: TypeChecker | undefined;
|
|
10
9
|
|
|
11
|
-
/** @type {boolean} */
|
|
12
10
|
#swallowErrors = false;
|
|
13
11
|
|
|
14
12
|
constructor() {
|
|
@@ -19,27 +17,21 @@ class TypeChecker {
|
|
|
19
17
|
|
|
20
18
|
/**
|
|
21
19
|
* Enable or disable swallowing of TypeCheckErrors.
|
|
22
|
-
* @param {boolean} value - Swallow errors flag.
|
|
23
20
|
*/
|
|
24
|
-
setSwallowErrors(value) {
|
|
21
|
+
setSwallowErrors(value: boolean): void {
|
|
25
22
|
this.#swallowErrors = Boolean(value);
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
26
|
* Type check a single value.
|
|
30
|
-
* @template T
|
|
31
|
-
* @param {unknown} value - The value to check.
|
|
32
|
-
* @param {(value: unknown) => value is T} validator - The validator to check with.
|
|
33
|
-
* @returns {T} - The type checked value.
|
|
34
27
|
*/
|
|
35
|
-
check(value, validator) {
|
|
28
|
+
check<T>(value: unknown, validator: (value: unknown) => value is T): T {
|
|
36
29
|
try {
|
|
37
30
|
return typeCheck(value, validator);
|
|
38
31
|
} catch (error) {
|
|
39
32
|
if (this.#swallowErrors && error instanceof TypeCheckError) {
|
|
40
33
|
logger.exception('check', error);
|
|
41
|
-
|
|
42
|
-
return value;
|
|
34
|
+
return value as T;
|
|
43
35
|
}
|
|
44
36
|
throw error;
|
|
45
37
|
}
|
|
@@ -47,32 +39,23 @@ class TypeChecker {
|
|
|
47
39
|
|
|
48
40
|
/**
|
|
49
41
|
* Type check an array of values.
|
|
50
|
-
* @template T
|
|
51
|
-
* @param {unknown[]} value - The value to check.
|
|
52
|
-
* @param {(value: unknown) => value is T} validator - The validator to check the array with.
|
|
53
|
-
* @returns {T[]} - The type checked value.
|
|
54
42
|
*/
|
|
55
|
-
checkArray(value, validator) {
|
|
43
|
+
checkArray<T>(value: unknown[], validator: (value: unknown) => value is T): T[] {
|
|
56
44
|
try {
|
|
57
45
|
return typeCheckArray(value, validator);
|
|
58
46
|
} catch (error) {
|
|
59
47
|
if (this.#swallowErrors && error instanceof TypeCheckError) {
|
|
60
48
|
logger.exception('checkArray', error);
|
|
61
|
-
|
|
62
|
-
return value;
|
|
49
|
+
return value as T[];
|
|
63
50
|
}
|
|
64
51
|
throw error;
|
|
65
52
|
}
|
|
66
53
|
}
|
|
67
54
|
|
|
68
55
|
/**
|
|
69
|
-
* Type check an
|
|
70
|
-
* @template T
|
|
71
|
-
* @param {string | number} value - The value to check.
|
|
72
|
-
* @param {(string | number)[] | Set<string | number> | Record<string | number, string | number>} choices - Enum list.
|
|
73
|
-
* @returns {string | number} - The type checked value.
|
|
56
|
+
* Type check an enum.
|
|
74
57
|
*/
|
|
75
|
-
checkEnum(value, choices) {
|
|
58
|
+
checkEnum(value: string | number, choices: EnumChoices): string | number {
|
|
76
59
|
try {
|
|
77
60
|
return typeCheckEnum(value, choices);
|
|
78
61
|
} catch (error) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isNumber, isPositiveInteger } from '../util/validate.js';
|
|
2
|
+
import { TypeCheckError } from './TypeCheckError.js';
|
|
3
|
+
import { typeCheck, typeCheckArray, typeCheckEnum } from './util.js';
|
|
4
|
+
|
|
5
|
+
describe('typecheck', () => {
|
|
6
|
+
test('typeCheck', () => {
|
|
7
|
+
expect(() => typeCheck(0.1, isNumber)).not.toThrowError(TypeCheckError);
|
|
8
|
+
expect(() => typeCheck(-0.1, isPositiveInteger)).toThrowError(TypeCheckError);
|
|
9
|
+
expect(() => typeCheck('string', isNumber)).toThrowError(TypeCheckError);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('typeCheckArray', () => {
|
|
13
|
+
expect(() => typeCheckArray([0.1], isNumber)).not.toThrowError(TypeCheckError);
|
|
14
|
+
expect(() => typeCheckArray(['string'], isNumber)).toThrowError(TypeCheckError);
|
|
15
|
+
// @ts-expect-error
|
|
16
|
+
expect(() => typeCheckArray(-0.1, isPositiveInteger)).toThrowError(TypeCheckError);
|
|
17
|
+
// @ts-expect-error
|
|
18
|
+
expect(() => typeCheckArray('string', isNumber)).toThrowError(TypeCheckError);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('typeCheckEnum', () => {
|
|
22
|
+
expect(() => typeCheckEnum('AA', ['AA'])).not.toThrowError(TypeCheckError);
|
|
23
|
+
expect(() => typeCheckEnum('AA', ['BB'])).toThrowError(TypeCheckError);
|
|
24
|
+
// @ts-expect-error
|
|
25
|
+
expect(() => typeCheckEnum(null, ['BB'])).toThrowError(TypeCheckError);
|
|
26
|
+
// @ts-expect-error
|
|
27
|
+
expect(() => typeCheckEnum(['AA'], null)).toThrowError(TypeCheckError);
|
|
28
|
+
try {
|
|
29
|
+
typeCheckEnum('AA', ['BB']);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
expect(error.message).toBe('Validation failed: isEnum - "AA" (string)');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getDisplayValue, getTypeFromValue } from '../util/string.js';
|
|
2
|
+
import { isArrayOf, isEnum } from '../util/validate.js';
|
|
3
|
+
import { TypeCheckError } from './TypeCheckError.js';
|
|
4
|
+
|
|
5
|
+
export type EnumChoices =
|
|
6
|
+
| ReadonlyArray<string | number>
|
|
7
|
+
| ReadonlySet<string | number>
|
|
8
|
+
| Readonly<Record<string | number, string | number>>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get error message for validator exceptions.
|
|
12
|
+
*/
|
|
13
|
+
const getErrorMessage = (validatorName: string, value: unknown): never => {
|
|
14
|
+
const displayValue = getDisplayValue(value);
|
|
15
|
+
const displayType = getTypeFromValue(value);
|
|
16
|
+
throw new TypeCheckError(`Validation failed: ${validatorName || '<anonymous>'} - ${displayValue} (${displayType})`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type check a value using a validator.
|
|
21
|
+
* @throws {TypeCheckError}
|
|
22
|
+
*/
|
|
23
|
+
export const typeCheck = <T>(value: unknown, validator: (value: unknown) => value is T): T => {
|
|
24
|
+
if (!validator(value)) {
|
|
25
|
+
getErrorMessage(validator.name, value);
|
|
26
|
+
}
|
|
27
|
+
return value as T;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type check an array of values using a validator.
|
|
32
|
+
* @throws {TypeCheckError}
|
|
33
|
+
*/
|
|
34
|
+
export const typeCheckArray = <T>(value: unknown[], validator: (value: unknown) => value is T): T[] => {
|
|
35
|
+
if (!isArrayOf(value, validator)) {
|
|
36
|
+
getErrorMessage(validator.name, value);
|
|
37
|
+
}
|
|
38
|
+
return value as T[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Type check an enum.
|
|
43
|
+
* @throws {TypeCheckError}
|
|
44
|
+
*/
|
|
45
|
+
export const typeCheckEnum = (value: string | number, choices: EnumChoices): string | number => {
|
|
46
|
+
if (!isEnum(value, choices)) {
|
|
47
|
+
getErrorMessage('isEnum', value);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { delayPromise, loadJSON, retryAsync } from './async.js';
|
|
2
|
+
|
|
3
|
+
describe('delayPromise', () => {
|
|
4
|
+
test('Returns a promise that resolves after specified delay', async () => {
|
|
5
|
+
const start = Date.now();
|
|
6
|
+
await delayPromise(10);
|
|
7
|
+
const end = Date.now();
|
|
8
|
+
|
|
9
|
+
expect(end - start).toBeGreaterThanOrEqual(9);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('delayPromise with zero delay', async () => {
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
await delayPromise(0);
|
|
15
|
+
const end = Date.now();
|
|
16
|
+
|
|
17
|
+
expect(end - start).toBeGreaterThanOrEqual(0);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('loadJSON', () => {
|
|
22
|
+
test('Load JSON data', async () => {
|
|
23
|
+
const data = await loadJSON('/test.json');
|
|
24
|
+
expect(data).toMatchObject({
|
|
25
|
+
method: 'GET',
|
|
26
|
+
success: true,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('retryAsync', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.useRealTimers();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns result when method succeeds immediately', async () => {
|
|
41
|
+
const method = vi.fn().mockResolvedValue('success');
|
|
42
|
+
const result = await retryAsync(method, 1, 100);
|
|
43
|
+
expect(result).toBe('success');
|
|
44
|
+
expect(method).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('retries once and then succeeds', async () => {
|
|
48
|
+
const method = vi.fn().mockRejectedValueOnce(new Error('fail')).mockResolvedValueOnce('success');
|
|
49
|
+
const promise = retryAsync(method, 1, 100);
|
|
50
|
+
const result = await promise;
|
|
51
|
+
expect(result).toBe('success');
|
|
52
|
+
expect(method).toHaveBeenCalledTimes(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws after exceeding retries', async () => {
|
|
56
|
+
const method = vi.fn().mockRejectedValue(new Error('fail'));
|
|
57
|
+
const promise = retryAsync(method, 1, 100);
|
|
58
|
+
await expect(promise).rejects.toThrow('fail');
|
|
59
|
+
expect(method).toHaveBeenCalledTimes(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not retry when numTries is 0', async () => {
|
|
63
|
+
const method = vi.fn().mockRejectedValue(new Error('fail'));
|
|
64
|
+
await expect(retryAsync(method, 0, 100)).rejects.toThrow('fail');
|
|
65
|
+
expect(method).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not wait when delayMs is 0', async () => {
|
|
69
|
+
const method = vi.fn().mockRejectedValueOnce(new Error('fail')).mockResolvedValueOnce('success');
|
|
70
|
+
const result = await retryAsync(method, 1, 0);
|
|
71
|
+
expect(result).toBe('success');
|
|
72
|
+
expect(method).toHaveBeenCalledTimes(2);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -2,23 +2,16 @@ import { getTypedError } from './error.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns a promise with delayed resolve.
|
|
5
|
-
* @param {number} delayMS - Promise resolve delay in milliseconds.
|
|
6
|
-
* @returns {Promise<void>} Delayed resolve promise.
|
|
7
5
|
*/
|
|
8
|
-
export const delayPromise = (delayMS) =>
|
|
6
|
+
export const delayPromise = (delayMS: number): Promise<void> =>
|
|
9
7
|
new Promise((resolve) => {
|
|
10
8
|
setTimeout(resolve, delayMS);
|
|
11
9
|
});
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Async method call retry helper.
|
|
15
|
-
* @template T
|
|
16
|
-
* @param {() => Promise<T>} method - Async function to call.
|
|
17
|
-
* @param {number} numTries - Max retries.
|
|
18
|
-
* @param {number} delayMs - Delay between attempts in ms.
|
|
19
|
-
* @returns {Promise<T>} Async function result.
|
|
20
13
|
*/
|
|
21
|
-
export const retryAsync = async (method
|
|
14
|
+
export const retryAsync = async <T>(method: () => Promise<T>, numTries = 1, delayMs = 100): Promise<T> => {
|
|
22
15
|
for (let attempt = 0; attempt <= numTries; attempt += 1) {
|
|
23
16
|
try {
|
|
24
17
|
// oxlint-disable-next-line no-await-in-loop
|
|
@@ -38,10 +31,8 @@ export const retryAsync = async (method, numTries = 1, delayMs = 100) => {
|
|
|
38
31
|
|
|
39
32
|
/**
|
|
40
33
|
* Load JSON file using a fetch GET request.
|
|
41
|
-
* @param {string} url - URL to load.
|
|
42
|
-
* @returns {Promise<unknown>} The parsed JSON data.
|
|
43
34
|
*/
|
|
44
|
-
export const loadJSON = async (url) => {
|
|
35
|
+
export const loadJSON = async (url: string): Promise<unknown> => {
|
|
45
36
|
const response = await fetch(url);
|
|
46
37
|
if (!response.ok) {
|
|
47
38
|
throw new DOMException(`Fetch error ${response.status}`, 'FetchError');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getErrorDetails, getTypedError } from './error.js';
|
|
2
|
+
|
|
3
|
+
describe('error', () => {
|
|
4
|
+
test('getErrorDetails', () => {
|
|
5
|
+
const error = new Error('Test error', { cause: 'Test cause' });
|
|
6
|
+
const errorDetails = getErrorDetails(error);
|
|
7
|
+
expect(errorDetails.type).toBe('Error');
|
|
8
|
+
expect(errorDetails.message).toBe('Test error');
|
|
9
|
+
expect(errorDetails.cause).toBe('Test cause');
|
|
10
|
+
expect(errorDetails['stack']).toBe(undefined);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('getErrorDetails with Error cause', () => {
|
|
14
|
+
const error = new SyntaxError('Test error', { cause: new TypeError('Cause error') });
|
|
15
|
+
const errorDetails = getErrorDetails(error);
|
|
16
|
+
expect(errorDetails.type).toBe('SyntaxError');
|
|
17
|
+
expect(errorDetails.message).toBe('Test error');
|
|
18
|
+
expect(errorDetails.cause instanceof Error).toBe(true);
|
|
19
|
+
if (errorDetails.cause instanceof Error) {
|
|
20
|
+
expect(errorDetails.cause.message).toBe('Cause error');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('getTypedError', () => {
|
|
25
|
+
expect(getTypedError(new Error('Error message')).message).toBe('Error message');
|
|
26
|
+
expect(getTypedError('Error message').message).toBe('Error message');
|
|
27
|
+
expect(getTypedError(1).message).toBe('1');
|
|
28
|
+
expect(getTypedError(true).message).toBe('true');
|
|
29
|
+
expect(getTypedError(null).message).toBe('null');
|
|
30
|
+
expect(getTypedError(undefined).message).toBe('undefined');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const DEFAULT_EXCLUDES = new Set(['stack']);
|
|
2
|
+
|
|
3
|
+
export interface ErrorDetails {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Retrieves detailed information from an error object.
|
|
13
|
+
*/
|
|
14
|
+
export const getErrorDetails = (error: Error, excludes?: string[] | null): ErrorDetails => {
|
|
15
|
+
const errorDetails: ErrorDetails = {
|
|
16
|
+
name: error.name,
|
|
17
|
+
type: error.constructor?.name ?? typeof error,
|
|
18
|
+
};
|
|
19
|
+
if (error.message) {
|
|
20
|
+
errorDetails.message = error.message;
|
|
21
|
+
}
|
|
22
|
+
if (error.cause) {
|
|
23
|
+
errorDetails.cause = error.cause;
|
|
24
|
+
}
|
|
25
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
26
|
+
if (!excludes?.includes(key) && !DEFAULT_EXCLUDES.has(key)) {
|
|
27
|
+
errorDetails[key] = (error as unknown as Record<string, unknown>)[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return errorDetails;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get typed error from an unknown type.
|
|
35
|
+
*/
|
|
36
|
+
export const getTypedError = (error: unknown): Error =>
|
|
37
|
+
error instanceof Error ? error : new Error(String(error));
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { EventEmitter } from './event_emitter.js';
|
|
3
|
+
|
|
4
|
+
describe('EventEmitter3 basics', () => {
|
|
5
|
+
it('can be instantiated', () => {
|
|
6
|
+
const e = new EventEmitter();
|
|
7
|
+
expect(e).toBeInstanceOf(EventEmitter);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('supports subclassing', () => {
|
|
11
|
+
class Beast extends EventEmitter {}
|
|
12
|
+
const beast = new Beast();
|
|
13
|
+
|
|
14
|
+
expect(beast).toBeInstanceOf(Beast);
|
|
15
|
+
expect(beast).toBeInstanceOf(EventEmitter);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('emit()', () => {
|
|
20
|
+
it('returns false when no listeners exist', () => {
|
|
21
|
+
const e = new EventEmitter();
|
|
22
|
+
expect(e.emit('foo')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns true when listeners exist', () => {
|
|
26
|
+
const e = new EventEmitter();
|
|
27
|
+
e.on('foo', () => {});
|
|
28
|
+
expect(e.emit('foo')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('invokes listeners with provided arguments', async () => {
|
|
32
|
+
const e = new EventEmitter();
|
|
33
|
+
|
|
34
|
+
await new Promise<void>((resolve) => {
|
|
35
|
+
e.on('foo', (a: number, b: number) => {
|
|
36
|
+
expect(a).toBe(1);
|
|
37
|
+
expect(b).toBe(2);
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
e.emit('foo', 1, 2);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('binds the correct context', async () => {
|
|
46
|
+
const e = new EventEmitter();
|
|
47
|
+
const ctx = { value: 42 };
|
|
48
|
+
|
|
49
|
+
await new Promise<void>((resolve) => {
|
|
50
|
+
e.on(
|
|
51
|
+
'foo',
|
|
52
|
+
function (this: typeof ctx) {
|
|
53
|
+
expect(this).toBe(ctx);
|
|
54
|
+
resolve();
|
|
55
|
+
},
|
|
56
|
+
ctx
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
e.emit('foo');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('supports many listeners for the same event', () => {
|
|
64
|
+
const e = new EventEmitter();
|
|
65
|
+
const calls: number[] = [];
|
|
66
|
+
|
|
67
|
+
e.on('foo', () => calls.push(1));
|
|
68
|
+
e.on('foo', () => calls.push(2));
|
|
69
|
+
|
|
70
|
+
e.emit('foo');
|
|
71
|
+
|
|
72
|
+
expect(calls).toEqual([1, 2]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('once()', () => {
|
|
77
|
+
it('fires the listener only once', () => {
|
|
78
|
+
const e = new EventEmitter();
|
|
79
|
+
let calls = 0;
|
|
80
|
+
|
|
81
|
+
e.once('foo', () => {
|
|
82
|
+
calls += 1;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
e.emit('foo');
|
|
86
|
+
e.emit('foo');
|
|
87
|
+
|
|
88
|
+
expect(calls).toBe(1);
|
|
89
|
+
expect(e.listenerCount('foo')).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('passes arguments correctly', async () => {
|
|
93
|
+
const e = new EventEmitter();
|
|
94
|
+
|
|
95
|
+
await new Promise<void>((resolve) => {
|
|
96
|
+
e.once('foo', (...args: unknown[]) => {
|
|
97
|
+
expect(args).toEqual([1, 2, 3]);
|
|
98
|
+
resolve();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
e.emit('foo', 1, 2, 3);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('listeners() and listenerCount()', () => {
|
|
107
|
+
it('returns an empty array when no listeners exist', () => {
|
|
108
|
+
const e = new EventEmitter();
|
|
109
|
+
expect(e.listeners('foo')).toEqual([]);
|
|
110
|
+
expect(e.listenerCount('foo')).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns only listener functions (not internals)', () => {
|
|
114
|
+
const e = new EventEmitter();
|
|
115
|
+
function fn() {}
|
|
116
|
+
|
|
117
|
+
e.on('foo', fn);
|
|
118
|
+
|
|
119
|
+
expect(e.listeners('foo')).toEqual([fn]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not expose internal listener storage', () => {
|
|
123
|
+
const e = new EventEmitter();
|
|
124
|
+
function fn() {}
|
|
125
|
+
|
|
126
|
+
e.on('foo', fn);
|
|
127
|
+
const listeners = e.listeners('foo');
|
|
128
|
+
listeners.push(() => {});
|
|
129
|
+
|
|
130
|
+
expect(e.listeners('foo')).toEqual([fn]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('off()', () => {
|
|
135
|
+
it('removes all listeners for an event when fn is omitted', () => {
|
|
136
|
+
const e = new EventEmitter();
|
|
137
|
+
|
|
138
|
+
e.on('foo', () => {});
|
|
139
|
+
e.on('foo', () => {});
|
|
140
|
+
|
|
141
|
+
e.off('foo');
|
|
142
|
+
|
|
143
|
+
expect(e.listenerCount('foo')).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('removes a specific listener', () => {
|
|
147
|
+
const e = new EventEmitter();
|
|
148
|
+
function fn() {}
|
|
149
|
+
|
|
150
|
+
e.on('foo', fn);
|
|
151
|
+
e.off('foo', fn);
|
|
152
|
+
|
|
153
|
+
expect(e.listeners('foo')).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('removes listeners matching both function and context', () => {
|
|
157
|
+
const e = new EventEmitter();
|
|
158
|
+
const ctx1 = {};
|
|
159
|
+
const ctx2 = {};
|
|
160
|
+
function fn() {}
|
|
161
|
+
|
|
162
|
+
e.on('foo', fn, ctx1);
|
|
163
|
+
e.on('foo', fn, ctx2);
|
|
164
|
+
|
|
165
|
+
e.off('foo', fn, ctx1);
|
|
166
|
+
|
|
167
|
+
expect(e.listenerCount('foo')).toBe(1);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('removeAllListeners()', () => {
|
|
172
|
+
it('removes listeners for a single event', () => {
|
|
173
|
+
const e = new EventEmitter();
|
|
174
|
+
|
|
175
|
+
e.on('foo', () => {});
|
|
176
|
+
e.on('bar', () => {});
|
|
177
|
+
|
|
178
|
+
e.removeAllListeners('foo');
|
|
179
|
+
|
|
180
|
+
expect(e.listenerCount('foo')).toBe(0);
|
|
181
|
+
expect(e.listenerCount('bar')).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('removes all listeners when no event is specified', () => {
|
|
185
|
+
const e = new EventEmitter();
|
|
186
|
+
|
|
187
|
+
e.on('foo', () => {});
|
|
188
|
+
e.on('bar', () => {});
|
|
189
|
+
|
|
190
|
+
e.removeAllListeners();
|
|
191
|
+
|
|
192
|
+
expect(e.eventNames()).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('eventNames()', () => {
|
|
197
|
+
it('returns an empty array when no events exist', () => {
|
|
198
|
+
const e = new EventEmitter();
|
|
199
|
+
expect(e.eventNames()).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns all registered event names', () => {
|
|
203
|
+
const e = new EventEmitter();
|
|
204
|
+
|
|
205
|
+
e.on('foo', () => {});
|
|
206
|
+
e.on('bar', () => {});
|
|
207
|
+
|
|
208
|
+
expect(e.eventNames()).toContain('foo');
|
|
209
|
+
expect(e.eventNames()).toContain('bar');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('supports symbol event names', () => {
|
|
213
|
+
const e = new EventEmitter();
|
|
214
|
+
const sym = Symbol('test');
|
|
215
|
+
|
|
216
|
+
e.on(sym, () => {});
|
|
217
|
+
expect(e.eventNames()).toContain(sym);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('events map is instance field', () => {
|
|
221
|
+
const e1 = new EventEmitter();
|
|
222
|
+
const e2 = new EventEmitter();
|
|
223
|
+
e1.on('event', () => {});
|
|
224
|
+
expect(e1.listenerCount('event')).toBe(1);
|
|
225
|
+
expect(e1.listenerCount('no-event')).toBe(0);
|
|
226
|
+
expect(e2.listenerCount('event')).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
});
|