sportident.js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.dependency-cruiser.js +233 -0
  2. package/.editorconfig +12 -0
  3. package/.eslintignore +6 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
  6. package/.github/workflows/npm.yml +17 -0
  7. package/LICENSE +21 -0
  8. package/README.md +31 -0
  9. package/babel.config.js +21 -0
  10. package/build-docs.sh +25 -0
  11. package/docs/index.md +9 -0
  12. package/docs/typedoc/index.md +11 -0
  13. package/eslint.base.js +232 -0
  14. package/install.sh +6 -0
  15. package/jest.config.ts +49 -0
  16. package/nx.json +39 -0
  17. package/package.json +51 -0
  18. package/src/SiCard/BaseSiCard.test.ts +187 -0
  19. package/src/SiCard/BaseSiCard.ts +101 -0
  20. package/src/SiCard/IRaceResultData.ts +16 -0
  21. package/src/SiCard/ISiCard.ts +23 -0
  22. package/src/SiCard/ISiCardExamples.ts +4 -0
  23. package/src/SiCard/index.ts +2 -0
  24. package/src/SiCard/raceResultTools.test.ts +260 -0
  25. package/src/SiCard/raceResultTools.ts +150 -0
  26. package/src/SiCard/types/FCard.test.ts +19 -0
  27. package/src/SiCard/types/FCard.ts +14 -0
  28. package/src/SiCard/types/ModernSiCard.test.ts +186 -0
  29. package/src/SiCard/types/ModernSiCard.ts +241 -0
  30. package/src/SiCard/types/PCard.test.ts +19 -0
  31. package/src/SiCard/types/PCard.ts +14 -0
  32. package/src/SiCard/types/SIAC.test.ts +84 -0
  33. package/src/SiCard/types/SIAC.ts +19 -0
  34. package/src/SiCard/types/SiCard10.test.ts +85 -0
  35. package/src/SiCard/types/SiCard10.ts +17 -0
  36. package/src/SiCard/types/SiCard11.test.ts +84 -0
  37. package/src/SiCard/types/SiCard11.ts +19 -0
  38. package/src/SiCard/types/SiCard5.test.ts +149 -0
  39. package/src/SiCard/types/SiCard5.ts +129 -0
  40. package/src/SiCard/types/SiCard6.test.ts +179 -0
  41. package/src/SiCard/types/SiCard6.ts +222 -0
  42. package/src/SiCard/types/SiCard8.test.ts +137 -0
  43. package/src/SiCard/types/SiCard8.ts +129 -0
  44. package/src/SiCard/types/SiCard9.test.ts +132 -0
  45. package/src/SiCard/types/SiCard9.ts +128 -0
  46. package/src/SiCard/types/TCard.test.ts +19 -0
  47. package/src/SiCard/types/TCard.ts +14 -0
  48. package/src/SiCard/types/index.test.ts +26 -0
  49. package/src/SiCard/types/index.ts +15 -0
  50. package/src/SiCard/types/modernSiCardExamples.ts +364 -0
  51. package/src/SiCard/types/siCard5Examples.ts +73 -0
  52. package/src/SiCard/types/siCard6Examples.ts +262 -0
  53. package/src/SiCard/types/siCard8Examples.ts +152 -0
  54. package/src/SiCard/types/siCard9Examples.ts +143 -0
  55. package/src/SiDevice/INavigatorWebSerial.ts +78 -0
  56. package/src/SiDevice/INavigatorWebUsb.ts +62 -0
  57. package/src/SiDevice/ISiDevice.ts +48 -0
  58. package/src/SiDevice/ISiDeviceDriver.ts +35 -0
  59. package/src/SiDevice/README.md +13 -0
  60. package/src/SiDevice/SiDevice.test.ts +354 -0
  61. package/src/SiDevice/SiDevice.ts +132 -0
  62. package/src/SiDevice/WebSerialSiDeviceDriver.ts +146 -0
  63. package/src/SiDevice/WebUsbSiDeviceDriver.ts +343 -0
  64. package/src/SiDevice/index.ts +3 -0
  65. package/src/SiDevice/testUtils/index.ts +2 -0
  66. package/src/SiDevice/testUtils/testISiDeviceDriver.ts +63 -0
  67. package/src/SiDevice/testUtils/testISiDeviceDriverWithAutodetection.ts +72 -0
  68. package/src/SiStation/BaseSiStation.test.ts +221 -0
  69. package/src/SiStation/BaseSiStation.ts +253 -0
  70. package/src/SiStation/CoupledSiStation.test.ts +41 -0
  71. package/src/SiStation/CoupledSiStation.ts +130 -0
  72. package/src/SiStation/ISiMainStation.ts +29 -0
  73. package/src/SiStation/ISiSendTask.ts +9 -0
  74. package/src/SiStation/ISiStation.ts +88 -0
  75. package/src/SiStation/ISiTargetMultiplexer.ts +51 -0
  76. package/src/SiStation/SiMainStation.test.ts +222 -0
  77. package/src/SiStation/SiMainStation.ts +133 -0
  78. package/src/SiStation/SiSendTask.ts +50 -0
  79. package/src/SiStation/SiTargetMultiplexer.targeting.test.ts +112 -0
  80. package/src/SiStation/SiTargetMultiplexer.test.ts +605 -0
  81. package/src/SiStation/SiTargetMultiplexer.ts +241 -0
  82. package/src/SiStation/index.ts +5 -0
  83. package/src/SiStation/siStationExamples.ts +103 -0
  84. package/src/constants.test.ts +17 -0
  85. package/src/constants.ts +92 -0
  86. package/src/fakes/FakeSiCard/BaseFakeSiCard.test.ts +11 -0
  87. package/src/fakes/FakeSiCard/BaseFakeSiCard.ts +10 -0
  88. package/src/fakes/FakeSiCard/IFakeSiCard.ts +6 -0
  89. package/src/fakes/FakeSiCard/index.ts +2 -0
  90. package/src/fakes/FakeSiCard/types/FakeModernSiCard.test.ts +62 -0
  91. package/src/fakes/FakeSiCard/types/FakeModernSiCard.ts +43 -0
  92. package/src/fakes/FakeSiCard/types/FakeSIAC.ts +17 -0
  93. package/src/fakes/FakeSiCard/types/FakeSiCard10.ts +17 -0
  94. package/src/fakes/FakeSiCard/types/FakeSiCard11.ts +17 -0
  95. package/src/fakes/FakeSiCard/types/FakeSiCard5.test.ts +42 -0
  96. package/src/fakes/FakeSiCard/types/FakeSiCard5.ts +40 -0
  97. package/src/fakes/FakeSiCard/types/FakeSiCard6.test.ts +62 -0
  98. package/src/fakes/FakeSiCard/types/FakeSiCard6.ts +44 -0
  99. package/src/fakes/FakeSiCard/types/FakeSiCard8.ts +16 -0
  100. package/src/fakes/FakeSiCard/types/FakeSiCard9.ts +16 -0
  101. package/src/fakes/FakeSiCard/types/index.ts +7 -0
  102. package/src/fakes/FakeSiDeviceDriver.ts +148 -0
  103. package/src/fakes/FakeSiMainStation.test.ts +141 -0
  104. package/src/fakes/FakeSiMainStation.ts +118 -0
  105. package/src/fakes/IFakeSiMainStation.ts +15 -0
  106. package/src/fakes/index.ts +2 -0
  107. package/src/index.ts +24 -0
  108. package/src/siProtocol.test.ts +509 -0
  109. package/src/siProtocol.ts +417 -0
  110. package/src/storage/SiArray.test.ts +103 -0
  111. package/src/storage/SiArray.ts +56 -0
  112. package/src/storage/SiBool.test.ts +81 -0
  113. package/src/storage/SiBool.ts +47 -0
  114. package/src/storage/SiDataType.test.ts +81 -0
  115. package/src/storage/SiDataType.ts +60 -0
  116. package/src/storage/SiDict.test.ts +115 -0
  117. package/src/storage/SiDict.ts +60 -0
  118. package/src/storage/SiEnum.test.ts +77 -0
  119. package/src/storage/SiEnum.ts +48 -0
  120. package/src/storage/SiFieldValue.test.ts +58 -0
  121. package/src/storage/SiFieldValue.ts +23 -0
  122. package/src/storage/SiInt.test.ts +80 -0
  123. package/src/storage/SiInt.ts +84 -0
  124. package/src/storage/SiModified.test.ts +135 -0
  125. package/src/storage/SiModified.ts +59 -0
  126. package/src/storage/SiStorage.test.ts +51 -0
  127. package/src/storage/SiStorage.ts +44 -0
  128. package/src/storage/index.test.ts +222 -0
  129. package/src/storage/index.ts +12 -0
  130. package/src/storage/interfaces.ts +41 -0
  131. package/src/storage/siStringEncoding.ts +1361 -0
  132. package/src/testUtils.test.ts +266 -0
  133. package/src/testUtils.ts +75 -0
  134. package/src/utils/NumberRange.test.ts +66 -0
  135. package/src/utils/NumberRange.ts +46 -0
  136. package/src/utils/NumberRangeRegistry.test.ts +49 -0
  137. package/src/utils/NumberRangeRegistry.ts +43 -0
  138. package/src/utils/bytes.test.ts +126 -0
  139. package/src/utils/bytes.ts +69 -0
  140. package/src/utils/errors.test.ts +29 -0
  141. package/src/utils/errors.ts +20 -0
  142. package/src/utils/events.test.ts +112 -0
  143. package/src/utils/events.ts +68 -0
  144. package/src/utils/general.test.ts +139 -0
  145. package/src/utils/general.ts +69 -0
  146. package/src/utils/index.ts +8 -0
  147. package/src/utils/mixins.test.ts +40 -0
  148. package/src/utils/mixins.ts +13 -0
  149. package/src/utils/typed.ts +3 -0
  150. package/tsconfig.base.json +22 -0
  151. package/tsconfig.json +15 -0
@@ -0,0 +1,69 @@
1
+ export const isByte = (byte: unknown): boolean => Number(byte) === byte && Math.floor(byte) === byte && byte <= 0xff && byte >= 0x00;
2
+
3
+ export const isByteArr = (arr: unknown[]): boolean => Array.isArray(arr) && !arr.some((e) => !isByte(e));
4
+
5
+ export const assertIsByteArr = (arr: unknown[]): void => {
6
+ if (!isByteArr(arr)) {
7
+ throw new Error(`${arr} is not a byte array`);
8
+ }
9
+ };
10
+
11
+ export const isArrOfLengths = (arr: unknown[], lengths: number[]): boolean => {
12
+ const actualLength = arr.length;
13
+ return lengths.some((length) => actualLength === length);
14
+ };
15
+
16
+ export const assertArrIsOfLengths = (arr: unknown[], lengths: number[]): void => {
17
+ if (!isArrOfLengths(arr, lengths)) {
18
+ throw new Error(`${arr} is not of lengths ${lengths}`);
19
+ }
20
+ };
21
+
22
+ export const arr2big = (arr: number[]): number => {
23
+ assertIsByteArr(arr);
24
+ let outnum = 0;
25
+ for (let i = 0; i < arr.length; i++) {
26
+ const byte = arr[i];
27
+ outnum += byte * Math.pow(0x100, arr.length - i - 1);
28
+ }
29
+ return outnum;
30
+ };
31
+
32
+ export const prettyHex = (input: string | (number | undefined)[], lineLength = 0): string => {
33
+ let iterable = input;
34
+ if (typeof input === 'string') {
35
+ iterable = [];
36
+ for (let strIndex = 0; strIndex < input.length; strIndex++) {
37
+ iterable.push(input.charCodeAt(strIndex));
38
+ }
39
+ }
40
+ const prettyBytes = [...(iterable as (number | undefined)[])].map((byte) => (byte !== undefined ? `00${byte.toString(16)}` : '??')).map((paddedStr) => paddedStr.slice(-2).toUpperCase());
41
+ if (lineLength === 0) {
42
+ return prettyBytes.join(' ');
43
+ }
44
+ const lines = [];
45
+ for (let lineIndex = 0; lineIndex < prettyBytes.length / lineLength; lineIndex++) {
46
+ const startIndex = lineIndex * lineLength;
47
+ const endIndex = (lineIndex + 1) * lineLength;
48
+ const line = prettyBytes.slice(startIndex, endIndex).join(' ');
49
+ lines.push(line);
50
+ }
51
+ return lines.join('\n');
52
+ };
53
+
54
+ export const unPrettyHex = (input: string): Array<number | undefined> => {
55
+ const hexString = input.replace(/\s/g, '');
56
+ if (hexString.length % 2 !== 0) {
57
+ throw new Error('Hex String length must be even');
58
+ }
59
+ const byteArray = [];
60
+ for (let byteIndex = 0; byteIndex < hexString.length / 2; byteIndex++) {
61
+ const hexByteString = hexString.substr(byteIndex * 2, 2);
62
+ const byteValue = hexByteString === '??' ? undefined : parseInt(hexByteString, 16);
63
+ if (byteValue !== undefined && (!Number.isInteger(byteValue) || byteValue < 0 || byteValue > 255)) {
64
+ throw new Error(`Invalid hex: ${hexByteString}`);
65
+ }
66
+ byteArray.push(byteValue);
67
+ }
68
+ return byteArray;
69
+ };
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import * as errorUtils from './errors';
3
+
4
+ describe('error utils', () => {
5
+ describe('getErrorOrThrow', () => {
6
+ test('returns error if it is a proper error', () => {
7
+ const err = new Error('test');
8
+ const unk = err as unknown;
9
+ expect(errorUtils.getErrorOrThrow(unk)).toEqual(err);
10
+ });
11
+
12
+ test('throws an error if it is not an error', () => {
13
+ expect(() => errorUtils.getErrorOrThrow('not an error!')).toThrow(Error);
14
+ });
15
+ });
16
+ test('without message', () => {
17
+ const testError = new errorUtils.SiError();
18
+ expect(testError.message).toEqual('');
19
+ });
20
+ test('NotImplementedError', () => {
21
+ const testError = new errorUtils.NotImplementedError('test');
22
+ expect(testError.message).toEqual('test');
23
+ });
24
+ test('notImplemented', () => {
25
+ expect(() => errorUtils.notImplemented()).toThrow(errorUtils.NotImplementedError);
26
+ expect(() => errorUtils.notImplemented('test')).toThrow(errorUtils.NotImplementedError);
27
+ expect(() => errorUtils.notImplemented('test')).toThrow('test');
28
+ });
29
+ });
@@ -0,0 +1,20 @@
1
+ export const getErrorOrThrow = (err: unknown): Error | SiError => {
2
+ if (!(err instanceof Error) && !(err instanceof SiError)) {
3
+ throw new Error('Thrown thing is not an error');
4
+ }
5
+ return err;
6
+ };
7
+
8
+ export class SiError {
9
+ // eslint-disable-next-line no-useless-constructor
10
+ constructor(
11
+ public message: string = '',
12
+ public stack = [] // eslint-disable-next-line no-empty-function
13
+ ) {}
14
+ }
15
+
16
+ export class NotImplementedError extends SiError {}
17
+
18
+ export const notImplemented = (message?: string): never => {
19
+ throw new NotImplementedError(message || 'Not implemented');
20
+ };
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import * as eventUtils from './events';
3
+ import * as mixinUtils from './mixins';
4
+ import * as testUtils from '../testUtils';
5
+
6
+ testUtils.useFakeTimers();
7
+
8
+ describe('event utils', () => {
9
+ class MyEvent extends eventUtils.Event<'myEvent'> {
10
+ constructor(public eventObject: Record<string, unknown>) {
11
+ super();
12
+ }
13
+ }
14
+
15
+ type MyEvents = { myEvent: MyEvent };
16
+
17
+ class MyEventTarget {}
18
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
19
+ interface MyEventTarget extends eventUtils.EventTarget<MyEvents> {}
20
+ mixinUtils.applyMixins(MyEventTarget, [eventUtils.EventTarget]);
21
+
22
+ test('works', () => {
23
+ const myEventTarget = new MyEventTarget();
24
+ const eventObject = {};
25
+
26
+ const callsToCallback: MyEvent[] = [];
27
+ const callback = (e: MyEvent) => {
28
+ callsToCallback.push(e);
29
+ };
30
+
31
+ myEventTarget.addEventListener('myEvent', callback);
32
+ expect(callsToCallback.length).toBe(0);
33
+
34
+ myEventTarget.dispatchEvent('myEvent', new MyEvent(eventObject));
35
+ expect(callsToCallback.length).toBe(1);
36
+ expect(callsToCallback[0].type).toBe('myEvent');
37
+ expect(callsToCallback[0].eventObject).toBe(eventObject);
38
+
39
+ myEventTarget.removeEventListener('myEvent', callback);
40
+ expect(callsToCallback.length).toBe(1);
41
+
42
+ myEventTarget.dispatchEvent('myEvent', new MyEvent(eventObject));
43
+ expect(callsToCallback.length).toBe(1);
44
+ });
45
+ test('works even if some listeners fail', () => {
46
+ const myEventTarget = new MyEventTarget();
47
+ const timeState = {
48
+ listener1Run: false,
49
+ listener2Run: false,
50
+ listener3Run: false
51
+ };
52
+ myEventTarget.addEventListener('myEvent', () => {
53
+ timeState.listener1Run = true;
54
+ });
55
+ myEventTarget.addEventListener('myEvent', () => {
56
+ timeState.listener2Run = true;
57
+ throw new Error('test');
58
+ });
59
+ myEventTarget.addEventListener('myEvent', () => {
60
+ timeState.listener3Run = true;
61
+ });
62
+
63
+ myEventTarget.dispatchEvent('myEvent', new MyEvent({}));
64
+ expect(timeState).toEqual({
65
+ listener1Run: true,
66
+ listener2Run: true,
67
+ listener3Run: true
68
+ });
69
+ });
70
+ test('can remove all listeners', () => {
71
+ const myEventTarget = new MyEventTarget();
72
+ const timeState = {
73
+ listener1Run: false,
74
+ listener2Run: false
75
+ };
76
+ myEventTarget.addEventListener('myEvent', () => {
77
+ timeState.listener1Run = true;
78
+ });
79
+ myEventTarget.addEventListener('myEvent', () => {
80
+ timeState.listener2Run = true;
81
+ });
82
+
83
+ myEventTarget.removeAllEventListeners();
84
+
85
+ myEventTarget.dispatchEvent('myEvent', new MyEvent({}));
86
+ expect(timeState).toEqual({
87
+ listener1Run: false,
88
+ listener2Run: false
89
+ });
90
+ });
91
+ test('remove inexistent event listener', () => {
92
+ const myEventTarget = new MyEventTarget();
93
+ const eventObject = {};
94
+
95
+ const callsToCallback: MyEvent[] = [];
96
+ const callback = (e: MyEvent) => {
97
+ callsToCallback.push(e);
98
+ };
99
+
100
+ myEventTarget.removeEventListener('myEvent', callback);
101
+ expect(callsToCallback.length).toBe(0);
102
+
103
+ myEventTarget.dispatchEvent('myEvent', new MyEvent(eventObject));
104
+ expect(callsToCallback.length).toBe(0);
105
+ });
106
+ test('dispatch inexistent event listener', () => {
107
+ const myEventTarget = new MyEventTarget();
108
+ const eventObject = {};
109
+
110
+ expect(() => myEventTarget.dispatchEvent('myEvent', new MyEvent(eventObject))).not.toThrow();
111
+ });
112
+ });
@@ -0,0 +1,68 @@
1
+ import * as errorUtils from './errors';
2
+
3
+ export interface IEvent<T extends string> {
4
+ type?: T;
5
+ target: unknown;
6
+ defaultPrevented: boolean;
7
+ }
8
+
9
+ export class Event<T extends string> implements IEvent<T> {
10
+ target: unknown;
11
+ defaultPrevented = false;
12
+
13
+ // eslint-disable-next-line no-useless-constructor
14
+ constructor(
15
+ public type?: T // eslint-disable-next-line no-empty-function
16
+ ) {}
17
+ }
18
+
19
+ export type EventCallback<T extends Event<string>> = (event: T) => void;
20
+
21
+ interface EventTypeDict {
22
+ [type: string]: Event<string>;
23
+ }
24
+
25
+ export interface IEventTarget<T extends EventTypeDict> {
26
+ addEventListener: <K extends keyof T>(type: K, callback: EventCallback<T[K]>) => void;
27
+ removeEventListener: <K extends keyof T>(type: K, callback: EventCallback<T[K]>) => void;
28
+ removeAllEventListeners: () => void;
29
+ dispatchEvent: <K extends keyof T>(type: K, event: T[K]) => void;
30
+ }
31
+
32
+ export class EventTarget<T extends EventTypeDict> implements IEventTarget<T> {
33
+ private eventRegistry?: { [type: string]: EventCallback<Event<string>>[] };
34
+
35
+ addEventListener<K extends keyof T>(type: K, callback: EventCallback<T[K]>): void {
36
+ const eventRegistry = this.eventRegistry || {};
37
+ const listeners = eventRegistry[type as string] || [];
38
+ eventRegistry[type as string] = [...listeners, callback as EventCallback<Event<string>>];
39
+ this.eventRegistry = eventRegistry;
40
+ }
41
+
42
+ removeEventListener<K extends keyof T>(type: K, callback: EventCallback<T[K]>): void {
43
+ const eventRegistry = this.eventRegistry || {};
44
+ const listeners = eventRegistry[type as string] || [];
45
+ eventRegistry[type as string] = listeners.filter((listener: EventCallback<T[K]>) => listener !== callback);
46
+ this.eventRegistry = eventRegistry;
47
+ }
48
+
49
+ removeAllEventListeners(): void {
50
+ this.eventRegistry = {};
51
+ }
52
+
53
+ dispatchEvent<K extends keyof T>(type: K, event: T[K]): boolean {
54
+ event.type = type as string;
55
+ const eventRegistry = this.eventRegistry || {};
56
+ const listeners = eventRegistry[type as string] || [];
57
+ listeners.forEach((listener: EventCallback<T[K]>) => {
58
+ try {
59
+ listener(event);
60
+ } catch (exc) {
61
+ const err = errorUtils.getErrorOrThrow(exc);
62
+ console.error(`Event Listener failed (${String(type)}): ${err.message}`);
63
+ console.info(err.stack);
64
+ }
65
+ });
66
+ return !event.defaultPrevented;
67
+ }
68
+ }
@@ -0,0 +1,139 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import _ from 'lodash';
3
+ import Immutable from 'immutable';
4
+ import * as testUtils from '../testUtils';
5
+ import * as generalUtils from './general';
6
+
7
+ testUtils.useFakeTimers();
8
+
9
+ describe('general utils', () => {
10
+ test('cached', () => {
11
+ const cache = {};
12
+ const numGettersCalled = { foo: 0, bar: 0 };
13
+ const getFoo = generalUtils.cached(cache, () => {
14
+ numGettersCalled.foo += 1;
15
+ return 'foo';
16
+ });
17
+ const getBar = generalUtils.cached(cache, () => {
18
+ numGettersCalled.bar += 1;
19
+ return 'bar';
20
+ });
21
+ expect(_.isFunction(getFoo)).toBe(true);
22
+ expect(_.isFunction(getBar)).toBe(true);
23
+ expect(numGettersCalled).toEqual({ foo: 0, bar: 0 });
24
+ expect(getFoo()).toBe('foo');
25
+ expect(numGettersCalled).toEqual({ foo: 1, bar: 0 });
26
+ expect(getFoo()).toBe('foo');
27
+ expect(numGettersCalled).toEqual({ foo: 1, bar: 0 });
28
+ expect(getBar()).toBe('bar');
29
+ expect(numGettersCalled).toEqual({ foo: 1, bar: 1 });
30
+ expect(getFoo()).toBe('foo');
31
+ expect(numGettersCalled).toEqual({ foo: 1, bar: 1 });
32
+ });
33
+ test('getLookup', () => {
34
+ expect(generalUtils.getLookup({})).toEqual({});
35
+ expect(generalUtils.getLookup({ a: '0' })).toEqual({ '0': 'a' });
36
+ expect(generalUtils.getLookup({ a: '0', b: '1' })).toEqual({ '0': 'a', '1': 'b' });
37
+ expect(generalUtils.getLookup({ a: '0', b: '1', c: '2' })).toEqual({
38
+ '0': 'a',
39
+ '1': 'b',
40
+ '2': 'c'
41
+ });
42
+ });
43
+ test('getLookup with function', () => {
44
+ interface ComplexValue {
45
+ val: string;
46
+ }
47
+ const getLookupKey = (value: ComplexValue) => value.val;
48
+ expect(generalUtils.getLookup({}, getLookupKey)).toEqual({});
49
+ expect(generalUtils.getLookup({ a: { val: '0' } }, getLookupKey)).toEqual({ '0': 'a' });
50
+ expect(generalUtils.getLookup({ a: { val: '0' }, b: { val: '1' } }, getLookupKey)).toEqual({
51
+ '0': 'a',
52
+ '1': 'b'
53
+ });
54
+ expect(generalUtils.getLookup({ a: { val: '0' }, b: { val: '1' }, c: { val: '2' } }, getLookupKey)).toEqual({ '0': 'a', '1': 'b', '2': 'c' });
55
+ });
56
+ test('getLookup sanitizes', () => {
57
+ expect(() => generalUtils.getLookup({ a: '0', b: '1', c: '1' })).toThrow();
58
+ });
59
+ test('getLookup is cached', () => {
60
+ const mapping: generalUtils.MappingWithLookup<string[]> = { a: ['0'], b: ['1', '2'] };
61
+ const lookup1 = generalUtils.getLookup(mapping, (value) => value[0]);
62
+ expect(mapping._lookup).not.toBe(undefined);
63
+ expect(mapping._lookup).toEqual(lookup1);
64
+ let numCallsToLookupKeyGetter = 0;
65
+ const lookup2 = generalUtils.getLookup(mapping, () => `${numCallsToLookupKeyGetter++}`);
66
+ expect(numCallsToLookupKeyGetter).toBe(0);
67
+ expect(lookup2).toEqual(lookup1);
68
+ });
69
+ test('waitFor 0', async () => {
70
+ let doneWaiting = false;
71
+ generalUtils.waitFor(0, 'now').then((result) => {
72
+ expect(result).toBe('now');
73
+ doneWaiting = true;
74
+ });
75
+ expect(doneWaiting).toBe(false);
76
+ await testUtils.advanceTimersByTime(0);
77
+ expect(doneWaiting).toBe(true);
78
+ });
79
+ test('waitFor 1', async () => {
80
+ let doneWaiting = false;
81
+ generalUtils.waitFor(1, 'later').then((result) => {
82
+ expect(result).toBe('later');
83
+ doneWaiting = true;
84
+ });
85
+ expect(doneWaiting).toBe(false);
86
+ await testUtils.advanceTimersByTime(0);
87
+ expect(doneWaiting).toBe(false);
88
+ await testUtils.advanceTimersByTime(1);
89
+ expect(doneWaiting).toBe(true);
90
+ });
91
+ test('binarySearch length 0', () => {
92
+ const list: number[] = [];
93
+ expect(generalUtils.binarySearch(list, -1)).toBe(0);
94
+ expect(generalUtils.binarySearch(list, 0)).toBe(0);
95
+ expect(generalUtils.binarySearch(list, 1)).toBe(0);
96
+ });
97
+ test('binarySearch length 1', () => {
98
+ const list = [3];
99
+ expect(generalUtils.binarySearch(list, -1)).toBe(0);
100
+ expect(generalUtils.binarySearch(list, 0)).toBe(0);
101
+ expect(generalUtils.binarySearch(list, 1)).toBe(0);
102
+ expect(generalUtils.binarySearch(list, 3)).toBe(0);
103
+ expect(generalUtils.binarySearch(list, 4)).toBe(1);
104
+ });
105
+ test('binarySearch length 3', () => {
106
+ const list = [1, 2, 4];
107
+ expect(generalUtils.binarySearch(list, -1)).toBe(0);
108
+ expect(generalUtils.binarySearch(list, 0)).toBe(0);
109
+ expect(generalUtils.binarySearch(list, 1)).toBe(0);
110
+ expect(generalUtils.binarySearch(list, 2)).toBe(1);
111
+ expect(generalUtils.binarySearch(list, 3)).toBe(2);
112
+ expect(generalUtils.binarySearch(list, 4)).toBe(2);
113
+ expect(generalUtils.binarySearch(list, 5)).toBe(3);
114
+ });
115
+ test('binarySearch duplicates', () => {
116
+ const listOdd = [1, 2, 2];
117
+ expect(generalUtils.binarySearch(listOdd, 1)).toBe(0);
118
+ expect(generalUtils.binarySearch(listOdd, 2)).toBe(1);
119
+ expect(generalUtils.binarySearch(listOdd, 3)).toBe(3);
120
+ const listEven = [1, 1, 2];
121
+ expect(generalUtils.binarySearch(listEven, 1)).toBe(0);
122
+ expect(generalUtils.binarySearch(listEven, 2)).toBe(2);
123
+ expect(generalUtils.binarySearch(listEven, 3)).toBe(3);
124
+ });
125
+ test('binarySearch immutable', () => {
126
+ const options: generalUtils.BinarySearchOptions<Immutable.List<number>, number> = {
127
+ getLength: (list) => list.size,
128
+ getItemAtIndex: (list, index) => list.get(index)
129
+ };
130
+ const list = Immutable.List([1, 2, 4]);
131
+ expect(generalUtils.binarySearch(list, -1, options)).toBe(0);
132
+ expect(generalUtils.binarySearch(list, 0, options)).toBe(0);
133
+ expect(generalUtils.binarySearch(list, 1, options)).toBe(0);
134
+ expect(generalUtils.binarySearch(list, 2, options)).toBe(1);
135
+ expect(generalUtils.binarySearch(list, 3, options)).toBe(2);
136
+ expect(generalUtils.binarySearch(list, 4, options)).toBe(2);
137
+ expect(generalUtils.binarySearch(list, 5, options)).toBe(3);
138
+ });
139
+ });
@@ -0,0 +1,69 @@
1
+ export type Cache<T> = { [id: string]: T };
2
+
3
+ export const cached = <T>(cache: Cache<T>, getThing: () => T): (() => T) => {
4
+ const getter = (): T => {
5
+ const getThingIdent = `${getThing.name}-${getThing.toString()}`;
6
+ const cachedThing = cache[getThingIdent];
7
+ if (cachedThing === undefined) {
8
+ const newThing = getThing();
9
+ cache[getThingIdent] = newThing;
10
+ return newThing;
11
+ }
12
+ return cachedThing;
13
+ };
14
+ return getter;
15
+ };
16
+
17
+ export type Lookup = { [id: string]: string };
18
+ export type MappingWithLookup<T> = { [id: string]: T | string | Lookup };
19
+
20
+ export const getLookup = <T>(mapping: MappingWithLookup<T>, getLookupKey?: (value: T) => string): Lookup => {
21
+ if (mapping._lookup) {
22
+ return mapping._lookup as Lookup;
23
+ }
24
+ const lookup: Lookup = {};
25
+ Object.keys(mapping)
26
+ .filter((mappingKey) => mappingKey.substr(0, 1) !== '_')
27
+ .forEach((mappingKey) => {
28
+ const mappingValue = mapping[mappingKey];
29
+ const lookupKey = getLookupKey ? getLookupKey(mappingValue as T) : (mappingValue as string);
30
+ if (lookupKey in lookup) {
31
+ throw new Error(`Duplicate lookup key: ${lookupKey}`);
32
+ }
33
+ lookup[lookupKey] = mappingKey;
34
+ });
35
+ mapping._lookup = lookup;
36
+ return lookup;
37
+ };
38
+
39
+ export const waitFor = <T>(milliseconds: number, value?: T): Promise<T | undefined> => {
40
+ const promise: Promise<T | undefined> = new Promise((resolve) => {
41
+ setTimeout(() => resolve(value), milliseconds);
42
+ });
43
+ return promise;
44
+ };
45
+
46
+ export interface BinarySearchOptions<L, T> {
47
+ getLength?: (list: L) => number;
48
+ getItemAtIndex?: (list: L, index: number) => T | undefined;
49
+ getNewRange?: (list: L, item: T, start: number, end: number) => [number, number];
50
+ }
51
+
52
+ export const binarySearch = <L, T>(list: L, item: T, options: BinarySearchOptions<L, T> = {}): number => {
53
+ const defaultGetLength = (list_: L): number => (list_ as unknown as T[]).length;
54
+ const getLength = options.getLength || defaultGetLength;
55
+ const defaultGetItemAtIndex = (list_: L, index: number): T => (list_ as unknown as T[])[index];
56
+ const getItemAtIndex = options.getItemAtIndex || defaultGetItemAtIndex;
57
+ const defaultGetNewRange = (list_: L, item_: T, start: number, end: number): [number, number] => {
58
+ const mid = Math.floor((start + end) / 2);
59
+ const midItem = getItemAtIndex(list_, mid)!;
60
+ return item_ <= midItem ? [start, mid] : [mid + 1, end];
61
+ };
62
+ const getNewRange = options.getNewRange || defaultGetNewRange;
63
+ let start = 0;
64
+ let end = getLength(list);
65
+ while (start < end) {
66
+ [start, end] = getNewRange(list, item, start, end);
67
+ }
68
+ return start;
69
+ };
@@ -0,0 +1,8 @@
1
+ export * from './bytes';
2
+ export * from './errors';
3
+ export * from './events';
4
+ export * from './general';
5
+ export * from './mixins';
6
+ export * from './NumberRange';
7
+ export * from './NumberRangeRegistry';
8
+ export * from './typed';
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import * as mixinUtils from './mixins';
3
+
4
+ describe('mixin utils', () => {
5
+ test('mixes in', () => {
6
+ class Disposable {
7
+ public isDisposed = false;
8
+
9
+ dispose() {
10
+ this.isDisposed = true;
11
+ }
12
+ }
13
+
14
+ class Activatable {
15
+ public isActive = false;
16
+
17
+ activate() {
18
+ this.isActive = true;
19
+ }
20
+ }
21
+
22
+ class MyObject {
23
+ isMine = true;
24
+ }
25
+
26
+ interface MyObject extends Disposable, Activatable {}
27
+ mixinUtils.applyMixins(MyObject, [Disposable, Activatable]);
28
+
29
+ const myObject = new MyObject();
30
+ expect(myObject.isMine).toBe(true);
31
+ expect(myObject.isDisposed).toBe(undefined);
32
+ expect(myObject.isActive).toBe(undefined);
33
+ expect(() => myObject.activate()).not.toThrow();
34
+ expect(myObject.isDisposed).toBe(undefined);
35
+ expect(myObject.isActive).toBe(true);
36
+ expect(() => myObject.dispose()).not.toThrow();
37
+ expect(myObject.isDisposed).toBe(true);
38
+ expect(myObject.isActive).toBe(true);
39
+ });
40
+ });
@@ -0,0 +1,13 @@
1
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
2
+ export const applyMixins = (derivedCtor: any, baseCtors: any[]): void => {
3
+ baseCtors.forEach((baseCtor) => {
4
+ Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
5
+ Object.defineProperty(
6
+ derivedCtor.prototype,
7
+ name,
8
+ // @ts-ignore
9
+ Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
10
+ );
11
+ });
12
+ });
13
+ };
@@ -0,0 +1,3 @@
1
+ export function typedKeys<T extends Record<string | number | symbol, unknown>>(dict: T): (keyof T)[] {
2
+ return Object.keys(dict) as (keyof T)[];
3
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["dom"],
4
+ "composite": true,
5
+ "sourceMap": true,
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "noImplicitAny": true,
9
+ "module": "commonjs",
10
+ "target": "es6",
11
+ "esModuleInterop": true,
12
+ "jsx": "react",
13
+ "allowJs": true,
14
+ "experimentalDecorators": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "noImplicitReturns": true,
17
+ "noUnusedLocals": true,
18
+ "strict": true,
19
+ "strictNullChecks": true,
20
+ "strictPropertyInitialization": true
21
+ }
22
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "files": [],
4
+ "references": [
5
+ {"path": "packages/sportident"},
6
+ {"path": "packages/sportident-example-app"},
7
+ {"path": "packages/sportident-node-usb"},
8
+ {"path": "packages/sportident-react"},
9
+ {"path": "packages/sportident-testbench-client"},
10
+ {"path": "packages/sportident-testbench-node"},
11
+ {"path": "packages/sportident-testbench-server"},
12
+ {"path": "packages/sportident-testbench-shell"},
13
+ {"path": "packages/sportident-webusb"}
14
+ ]
15
+ }