@byloth/core 2.0.0-rc.9 → 2.0.1

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 (47) hide show
  1. package/dist/core.js +4087 -621
  2. package/dist/core.js.map +1 -1
  3. package/dist/core.umd.cjs +2 -2
  4. package/dist/core.umd.cjs.map +1 -1
  5. package/package.json +17 -13
  6. package/src/core/types.ts +41 -0
  7. package/src/helpers.ts +11 -2
  8. package/src/index.ts +12 -9
  9. package/src/models/aggregators/aggregated-async-iterator.ts +920 -21
  10. package/src/models/aggregators/aggregated-iterator.ts +838 -22
  11. package/src/models/aggregators/reduced-iterator.ts +827 -11
  12. package/src/models/aggregators/types.ts +153 -10
  13. package/src/models/callbacks/callable-object.ts +42 -6
  14. package/src/models/callbacks/index.ts +2 -2
  15. package/src/models/callbacks/publisher.ts +160 -4
  16. package/src/models/callbacks/switchable-callback.ts +230 -23
  17. package/src/models/callbacks/types.ts +16 -0
  18. package/src/models/exceptions/core.ts +132 -3
  19. package/src/models/exceptions/index.ts +405 -13
  20. package/src/models/index.ts +4 -8
  21. package/src/models/iterators/smart-async-iterator.ts +827 -22
  22. package/src/models/iterators/smart-iterator.ts +755 -20
  23. package/src/models/iterators/types.ts +268 -9
  24. package/src/models/json/json-storage.ts +508 -110
  25. package/src/models/json/types.ts +10 -1
  26. package/src/models/promises/deferred-promise.ts +85 -5
  27. package/src/models/promises/index.ts +1 -3
  28. package/src/models/promises/smart-promise.ts +272 -4
  29. package/src/models/promises/timed-promise.ts +43 -1
  30. package/src/models/promises/types.ts +84 -2
  31. package/src/models/timers/clock.ts +109 -19
  32. package/src/models/timers/countdown.ts +176 -21
  33. package/src/models/timers/game-loop.ts +266 -0
  34. package/src/models/timers/index.ts +2 -1
  35. package/src/models/types.ts +6 -5
  36. package/src/utils/async.ts +43 -0
  37. package/src/utils/curve.ts +85 -0
  38. package/src/utils/date.ts +204 -10
  39. package/src/utils/dom.ts +16 -2
  40. package/src/utils/index.ts +3 -2
  41. package/src/utils/iterator.ts +200 -17
  42. package/src/utils/math.ts +55 -3
  43. package/src/utils/random.ts +139 -2
  44. package/src/utils/string.ts +11 -0
  45. package/src/models/game-loop.ts +0 -83
  46. package/src/models/promises/long-running-task.ts +0 -294
  47. package/src/models/promises/thenable.ts +0 -97
@@ -1,20 +1,60 @@
1
1
  import { TimeUnit } from "../../utils/date.js";
2
- import { RangeException, RuntimeException } from "../exceptions/index.js";
3
2
 
4
3
  import Publisher from "../callbacks/publisher.js";
5
- import GameLoop from "../game-loop.js";
4
+ import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js";
5
+ import type { Callback } from "../types.js";
6
+
7
+ import GameLoop from "./game-loop.js";
6
8
 
7
9
  interface ClockEventMap
8
10
  {
9
11
  start: () => void;
10
12
  stop: () => void;
11
13
  tick: (elapsedTime: number) => void;
14
+
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ [key: string]: Callback<any[], any>;
12
17
  }
13
18
 
19
+ /**
20
+ * A class representing a clock.
21
+ *
22
+ * It can be started, stopped and, when running, it ticks at a specific frame rate.
23
+ * It's possible to subscribe to these events to receive notifications when they occur.
24
+ *
25
+ * ```ts
26
+ * const clock = new Clock();
27
+ *
28
+ * clock.onStart(() => { console.log("The clock has started."); });
29
+ * clock.onTick((elapsedTime) => { console.log(`The clock has ticked at ${elapsedTime}ms.`); });
30
+ * clock.onStop(() => { console.log("The clock has stopped."); });
31
+ *
32
+ * clock.start();
33
+ * ```
34
+ */
14
35
  export default class Clock extends GameLoop
15
36
  {
16
- protected _publisher: Publisher<ClockEventMap>;
17
-
37
+ /**
38
+ * The {@link Publisher} object that will be used to publish the events of the clock.
39
+ */
40
+ protected override _publisher: Publisher<ClockEventMap>;
41
+
42
+ /**
43
+ * Initializes a new instance of the {@link Clock} class.
44
+ *
45
+ * ---
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const clock = new Clock();
50
+ * ```
51
+ *
52
+ * ---
53
+ *
54
+ * @param msIfNotBrowser
55
+ * The interval in milliseconds at which the clock will tick if the environment is not a browser.
56
+ * `TimeUnit.Second` by default.
57
+ */
18
58
  public constructor(msIfNotBrowser: number = TimeUnit.Second)
19
59
  {
20
60
  super((elapsedTime) => this._publisher.publish("tick", elapsedTime), msIfNotBrowser);
@@ -22,33 +62,83 @@ export default class Clock extends GameLoop
22
62
  this._publisher = new Publisher();
23
63
  }
24
64
 
25
- public start(elapsedTime = 0): void
65
+ /**
66
+ * Starts the execution of the clock.
67
+ *
68
+ * If the clock is already running, a {@link RuntimeException} will be thrown.
69
+ *
70
+ * ---
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * clock.onStart(() => { [...] }); // This callback will be executed.
75
+ * clock.start();
76
+ * ```
77
+ *
78
+ * ---
79
+ *
80
+ * @param elapsedTime The elapsed time to set as default when the clock starts. Default is `0`.
81
+ */
82
+ public override start(elapsedTime = 0): void
26
83
  {
27
84
  if (this._isRunning) { throw new RuntimeException("The clock has already been started."); }
28
85
 
29
- super.start(elapsedTime);
86
+ this._startTime = performance.now() - elapsedTime;
87
+ this._start();
88
+ this._isRunning = true;
30
89
 
31
90
  this._publisher.publish("start");
32
91
  }
33
92
 
34
- public stop(): void
93
+ /**
94
+ * Stops the execution of the clock.
95
+ *
96
+ * If the clock hasn't yet started, a {@link RuntimeException} will be thrown.
97
+ *
98
+ * ---
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * clock.onStop(() => { [...] }); // This callback will be executed.
103
+ * clock.stop();
104
+ * ```
105
+ */
106
+ public override stop(): void
35
107
  {
36
- if (!(this._isRunning)) { throw new RuntimeException("The clock hadn't yet started."); }
108
+ if (!(this._isRunning)) { throw new RuntimeException("The clock had already stopped or hadn't yet started."); }
109
+ if (!(this._handle)) { throw new FatalErrorException(); }
37
110
 
38
- super.stop();
111
+ this._stop();
112
+ this._handle = undefined;
113
+ this._isRunning = false;
39
114
 
40
115
  this._publisher.publish("stop");
41
116
  }
42
117
 
43
- public onStart(callback: () => void): () => void
44
- {
45
- return this._publisher.subscribe("start", callback);
46
- }
47
- public onStop(callback: () => void): () => void
48
- {
49
- return this._publisher.subscribe("stop", callback);
50
- }
51
-
118
+ /**
119
+ * Subscribes to the `tick` event of the clock.
120
+ *
121
+ * ---
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * clock.onTick((elapsedTime) => { [...] }); // This callback will be executed.
126
+ * clock.start();
127
+ * ```
128
+ *
129
+ * ---
130
+ *
131
+ * @param callback The callback that will be executed when the clock ticks.
132
+ * @param tickStep
133
+ * The minimum time in milliseconds that must pass from the previous execution of the callback to the next one.
134
+ *
135
+ * - If it's a positive number, the callback will be executed only if the
136
+ * time passed from the previous execution is greater than this number.
137
+ * - If it's `0`, the callback will be executed every tick without even checking for the time.
138
+ * - If it's a negative number, a {@link RangeException} will be thrown.
139
+ *
140
+ * @returns A function that can be used to unsubscribe from the event.
141
+ */
52
142
  public onTick(callback: (elapsedTime: number) => void, tickStep = 0): () => void
53
143
  {
54
144
  if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
@@ -65,5 +155,5 @@ export default class Clock extends GameLoop
65
155
  });
66
156
  }
67
157
 
68
- public readonly [Symbol.toStringTag]: string = "Clock";
158
+ public override readonly [Symbol.toStringTag]: string = "Clock";
69
159
  }
@@ -1,10 +1,11 @@
1
1
  import { TimeUnit } from "../../utils/date.js";
2
2
 
3
+ import Publisher from "../callbacks/publisher.js";
3
4
  import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js";
4
5
  import { DeferredPromise, SmartPromise } from "../promises/index.js";
6
+ import type { Callback } from "../types.js";
5
7
 
6
- import Publisher from "../callbacks/publisher.js";
7
- import GameLoop from "../game-loop.js";
8
+ import GameLoop from "./game-loop.js";
8
9
 
9
10
  interface CountdownEventMap
10
11
  {
@@ -12,37 +13,100 @@ interface CountdownEventMap
12
13
  stop: (reason: unknown) => void;
13
14
  tick: (remainingTime: number) => void;
14
15
  expire: () => void;
16
+
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ [key: string]: Callback<any[], any>;
15
19
  }
16
20
 
21
+ /**
22
+ * A class representing a countdown.
23
+ *
24
+ * It can be started, stopped, when running it ticks at a specific frame rate and it expires when the time's up.
25
+ * It's possible to subscribe to these events to receive notifications when they occur.
26
+ *
27
+ * ```ts
28
+ * const countdown = new Countdown(10_000);
29
+ *
30
+ * countdown.onStart(() => { console.log("The countdown has started."); });
31
+ * countdown.onTick((remainingTime) => { console.log(`The countdown has ${remainingTime}ms remaining.`); });
32
+ * countdown.onStop((reason) => { console.log(`The countdown has stopped because of ${reason}.`); });
33
+ * countdown.onExpire(() => { console.log("The countdown has expired."); });
34
+ *
35
+ * countdown.start();
36
+ * ```
37
+ */
17
38
  export default class Countdown extends GameLoop
18
39
  {
19
- protected _deferrer?: DeferredPromise<void>;
20
- protected _publisher: Publisher<CountdownEventMap>;
40
+ /**
41
+ * The {@link Publisher} object that will be used to publish the events of the countdown.
42
+ */
43
+ protected override _publisher: Publisher<CountdownEventMap>;
21
44
 
45
+ /**
46
+ * The total duration of the countdown in milliseconds.
47
+ *
48
+ * This protected property is the only one that can be modified directly by the derived classes.
49
+ * If you're looking for the public and readonly property, use the {@link Countdown.duration} getter instead.
50
+ */
22
51
  protected _duration: number;
52
+
53
+ /**
54
+ * The total duration of the countdown in milliseconds.
55
+ */
23
56
  public get duration(): number
24
57
  {
25
58
  return this._duration;
26
59
  }
27
60
 
61
+ /**
62
+ * The remaining time of the countdown in milliseconds.
63
+ * It's calculated as the difference between the total duration and the elapsed time.
64
+ */
28
65
  public get remainingTime(): number
29
66
  {
30
67
  return this._duration - this.elapsedTime;
31
68
  }
32
69
 
70
+ /**
71
+ * The {@link DeferredPromise} that will be resolved or rejected when the countdown expires or stops.
72
+ */
73
+ protected _deferrer?: DeferredPromise<void>;
74
+
75
+ /**
76
+ * Initializes a new instance of the {@link Countdown} class.
77
+ *
78
+ * ---
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const countdown = new Countdown(10_000);
83
+ * ```
84
+ *
85
+ * ---
86
+ *
87
+ * @param duration
88
+ * The total duration of the countdown in milliseconds.
89
+ *
90
+ * @param msIfNotBrowser
91
+ * The interval in milliseconds at which the countdown will tick if the environment is not a browser.
92
+ * `TimeUnit.Second` by default.
93
+ */
33
94
  public constructor(duration: number, msIfNotBrowser: number = TimeUnit.Second)
34
95
  {
35
96
  const callback = () =>
36
97
  {
37
98
  const remainingTime = this.remainingTime;
38
- this._publisher.publish("tick", remainingTime);
39
-
40
99
  if (remainingTime <= 0)
41
100
  {
42
101
  this._deferrerStop();
43
102
 
103
+ this._publisher.publish("tick", 0);
44
104
  this._publisher.publish("expire");
45
105
  }
106
+ else
107
+ {
108
+ this._publisher.publish("tick", remainingTime);
109
+ }
46
110
  };
47
111
 
48
112
  super(callback, msIfNotBrowser);
@@ -51,12 +115,24 @@ export default class Countdown extends GameLoop
51
115
  this._duration = duration;
52
116
  }
53
117
 
118
+ /**
119
+ * The internal method actually responsible for stopping the
120
+ * countdown and resolving or rejecting the {@link Countdown._deferrer} promise.
121
+ *
122
+ * @param reason
123
+ * The reason why the countdown has stopped.
124
+ *
125
+ * - If it's `undefined`, the promise will be resolved.
126
+ * - If it's a value, the promise will be rejected with that value.
127
+ */
54
128
  protected _deferrerStop(reason?: unknown): void
55
129
  {
56
130
  if (!(this._isRunning)) { throw new RuntimeException("The countdown hadn't yet started."); }
57
131
  if (!(this._deferrer)) { throw new FatalErrorException(); }
58
132
 
59
- super.stop();
133
+ this._stop();
134
+ this._handle = undefined;
135
+ this._isRunning = false;
60
136
 
61
137
  if (reason !== undefined) { this._deferrer.reject(reason); }
62
138
  else { this._deferrer.resolve(); }
@@ -64,9 +140,30 @@ export default class Countdown extends GameLoop
64
140
  this._deferrer = undefined;
65
141
  }
66
142
 
67
- public start(remainingTime: number = this.duration): SmartPromise<void>
143
+ /**
144
+ * Starts the execution of the countdown.
145
+ *
146
+ * If the countdown is already running, a {@link RuntimeException} will be thrown.
147
+ *
148
+ * ---
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * countdown.onStart(() => { [...] }); // This callback will be executed.
153
+ * countdown.start();
154
+ * ```
155
+ *
156
+ * ---
157
+ *
158
+ * @param remainingTime
159
+ * The remaining time to set as default when the countdown starts.
160
+ * Default is the {@link Countdown.duration} itself.
161
+ *
162
+ * @returns A {@link SmartPromise} that will be resolved or rejected when the countdown expires or stops.
163
+ */
164
+ public override start(remainingTime: number = this.duration): SmartPromise<void>
68
165
  {
69
- if (this._isRunning) { throw new RuntimeException("The countdown has already been started."); }
166
+ if (this._isRunning) { throw new RuntimeException("The countdown had already stopped or hadn't yet started."); }
70
167
  if (this._deferrer) { throw new FatalErrorException(); }
71
168
 
72
169
  this._deferrer = new DeferredPromise();
@@ -76,33 +173,91 @@ export default class Countdown extends GameLoop
76
173
 
77
174
  return this._deferrer;
78
175
  }
79
- public stop(reason?: unknown): void
176
+
177
+ /**
178
+ * Stops the execution of the countdown.
179
+ *
180
+ * If the countdown hasn't yet started, a {@link RuntimeException} will be thrown.
181
+ *
182
+ * ---
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * countdown.onStop(() => { [...] }); // This callback will be executed.
187
+ * countdown.stop();
188
+ * ```
189
+ *
190
+ * ---
191
+ *
192
+ * @param reason
193
+ * The reason why the countdown has stopped.
194
+ *
195
+ * - If it's `undefined`, the promise will be resolved.
196
+ * - If it's a value, the promise will be rejected with that value.
197
+ */
198
+ public override stop(reason?: unknown): void
80
199
  {
200
+ // TODO: Once solved Issues #6 & #10, make the `reason` parameter required.
201
+ // - https://github.com/Byloth/core/issues/6
202
+ // - https://github.com/Byloth/core/issues/10
203
+ //
81
204
  this._deferrerStop(reason);
82
205
 
83
206
  this._publisher.publish("stop", reason);
84
207
  }
85
208
 
209
+ /**
210
+ * Subscribes to the `expire` event of the countdown.
211
+ *
212
+ * ---
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * countdown.onExpire(() => { [...] }); // This callback will be executed once the countdown has expired.
217
+ * countdown.start();
218
+ * ```
219
+ *
220
+ * ---
221
+ *
222
+ * @param callback The callback that will be executed when the countdown expires.
223
+ *
224
+ * @returns A function that can be used to unsubscribe from the event.
225
+ */
86
226
  public onExpire(callback: () => void): () => void
87
227
  {
88
228
  return this._publisher.subscribe("expire", callback);
89
229
  }
90
230
 
91
- public onStart(callback: () => void): () => void
92
- {
93
- return this._publisher.subscribe("start", callback);
94
- }
95
- public onStop(callback: (reason?: unknown) => void): () => void
96
- {
97
- return this._publisher.subscribe("stop", callback);
98
- }
99
-
231
+ /**
232
+ * Subscribes to the `tick` event of the countdown.
233
+ *
234
+ * ---
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * countdown.onTick((remainingTime) => { [...] }); // This callback will be executed.
239
+ * countdown.start();
240
+ * ```
241
+ *
242
+ * ---
243
+ *
244
+ * @param callback The callback that will be executed when the countdown ticks.
245
+ * @param tickStep
246
+ * The minimum time in milliseconds that must pass from the previous execution of the callback to the next one.
247
+ *
248
+ * - If it's a positive number, the callback will be executed only if the
249
+ * time passed from the previous execution is greater than this number.
250
+ * - If it's `0`, the callback will be executed every tick without even checking for the time.
251
+ * - If it's a negative number, a {@link RangeException} will be thrown.
252
+ *
253
+ * @returns A function that can be used to unsubscribe from the event.
254
+ */
100
255
  public onTick(callback: (remainingTime: number) => void, tickStep = 0): () => void
101
256
  {
102
257
  if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
103
258
  if (tickStep === 0) { return this._publisher.subscribe("tick", callback); }
104
259
 
105
- let lastTick = 0;
260
+ let lastTick = this.remainingTime;
106
261
 
107
262
  return this._publisher.subscribe("tick", (remainingTime: number) =>
108
263
  {
@@ -113,5 +268,5 @@ export default class Countdown extends GameLoop
113
268
  });
114
269
  }
115
270
 
116
- public readonly [Symbol.toStringTag]: string = "Countdown";
271
+ public override readonly [Symbol.toStringTag]: string = "Countdown";
117
272
  }