@byloth/core 2.0.0-rc.9 → 2.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 (47) hide show
  1. package/dist/core.js +3371 -608
  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 +13 -10
  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 +765 -21
  10. package/src/models/aggregators/aggregated-iterator.ts +698 -22
  11. package/src/models/aggregators/reduced-iterator.ts +699 -10
  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 +139 -4
  16. package/src/models/callbacks/switchable-callback.ts +138 -4
  17. package/src/models/callbacks/types.ts +16 -0
  18. package/src/models/exceptions/core.ts +112 -3
  19. package/src/models/exceptions/index.ts +340 -13
  20. package/src/models/index.ts +4 -8
  21. package/src/models/iterators/smart-async-iterator.ts +687 -22
  22. package/src/models/iterators/smart-iterator.ts +631 -21
  23. package/src/models/iterators/types.ts +268 -9
  24. package/src/models/json/json-storage.ts +388 -110
  25. package/src/models/json/types.ts +10 -1
  26. package/src/models/promises/deferred-promise.ts +75 -5
  27. package/src/models/promises/index.ts +1 -3
  28. package/src/models/promises/smart-promise.ts +232 -4
  29. package/src/models/promises/timed-promise.ts +38 -1
  30. package/src/models/promises/types.ts +84 -2
  31. package/src/models/timers/clock.ts +91 -19
  32. package/src/models/timers/countdown.ts +152 -22
  33. package/src/models/timers/game-loop.ts +243 -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 +75 -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 +109 -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,55 @@
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
+ * ```ts
46
+ * const clock = new Clock();
47
+ * ```
48
+ *
49
+ * @param msIfNotBrowser
50
+ * The interval in milliseconds at which the clock will tick if the environment is not a browser.
51
+ * `TimeUnit.Second` by default.
52
+ */
18
53
  public constructor(msIfNotBrowser: number = TimeUnit.Second)
19
54
  {
20
55
  super((elapsedTime) => this._publisher.publish("tick", elapsedTime), msIfNotBrowser);
@@ -22,33 +57,70 @@ export default class Clock extends GameLoop
22
57
  this._publisher = new Publisher();
23
58
  }
24
59
 
25
- public start(elapsedTime = 0): void
60
+ /**
61
+ * Starts the execution of the clock.
62
+ *
63
+ * If the clock is already running, a {@link RuntimeException} will be thrown.
64
+ *
65
+ * ```ts
66
+ * clock.onStart(() => { [...] }); // This callback will be executed.
67
+ * clock.start();
68
+ * ```
69
+ *
70
+ * @param elapsedTime The elapsed time to set as default when the clock starts. Default is `0`.
71
+ */
72
+ public override start(elapsedTime = 0): void
26
73
  {
27
74
  if (this._isRunning) { throw new RuntimeException("The clock has already been started."); }
28
75
 
29
- super.start(elapsedTime);
76
+ this._startTime = performance.now() - elapsedTime;
77
+ this._start();
78
+ this._isRunning = true;
30
79
 
31
80
  this._publisher.publish("start");
32
81
  }
33
82
 
34
- public stop(): void
83
+ /**
84
+ * Stops the execution of the clock.
85
+ *
86
+ * If the clock hasn't yet started, a {@link RuntimeException} will be thrown.
87
+ *
88
+ * ```ts
89
+ * clock.onStop(() => { [...] }); // This callback will be executed.
90
+ * clock.stop();
91
+ * ```
92
+ */
93
+ public override stop(): void
35
94
  {
36
- if (!(this._isRunning)) { throw new RuntimeException("The clock hadn't yet started."); }
95
+ if (!(this._isRunning)) { throw new RuntimeException("The clock had already stopped or hadn't yet started."); }
96
+ if (!(this._handle)) { throw new FatalErrorException(); }
37
97
 
38
- super.stop();
98
+ this._stop();
99
+ this._handle = undefined;
100
+ this._isRunning = false;
39
101
 
40
102
  this._publisher.publish("stop");
41
103
  }
42
104
 
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
-
105
+ /**
106
+ * Subscribes to the `tick` event of the clock.
107
+ *
108
+ * ```ts
109
+ * clock.onTick((elapsedTime) => { [...] }); // This callback will be executed.
110
+ * clock.start();
111
+ * ```
112
+ *
113
+ * @param callback The callback that will be executed when the clock ticks.
114
+ * @param tickStep
115
+ * The minimum time in milliseconds that must pass from the previous execution of the callback to the next one.
116
+ *
117
+ * - If it's a positive number, the callback will be executed only if the
118
+ * time passed from the previous execution is greater than this number.
119
+ * - If it's `0`, the callback will be executed every tick without even checking for the time.
120
+ * - If it's a negative number, a {@link RangeException} will be thrown.
121
+ *
122
+ * @returns A function that can be used to unsubscribe from the event.
123
+ */
52
124
  public onTick(callback: (elapsedTime: number) => void, tickStep = 0): () => void
53
125
  {
54
126
  if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
@@ -65,5 +137,5 @@ export default class Clock extends GameLoop
65
137
  });
66
138
  }
67
139
 
68
- public readonly [Symbol.toStringTag]: string = "Clock";
140
+ public override readonly [Symbol.toStringTag]: string = "Clock";
69
141
  }
@@ -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,95 @@ 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>;
21
-
40
+ /**
41
+ * The {@link Publisher} object that will be used to publish the events of the countdown.
42
+ */
43
+ protected override _publisher: Publisher<CountdownEventMap>;
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
+ * ```ts
79
+ * const countdown = new Countdown(10_000);
80
+ * ```
81
+ *
82
+ * @param duration
83
+ * The total duration of the countdown in milliseconds.
84
+ *
85
+ * @param msIfNotBrowser
86
+ * The interval in milliseconds at which the countdown will tick if the environment is not a browser.
87
+ * `TimeUnit.Second` by default.
88
+ */
33
89
  public constructor(duration: number, msIfNotBrowser: number = TimeUnit.Second)
34
90
  {
35
91
  const callback = () =>
36
92
  {
37
93
  const remainingTime = this.remainingTime;
38
- this._publisher.publish("tick", remainingTime);
39
-
40
94
  if (remainingTime <= 0)
41
95
  {
42
96
  this._deferrerStop();
43
97
 
98
+ this._publisher.publish("tick", 0);
44
99
  this._publisher.publish("expire");
45
100
  }
101
+ else
102
+ {
103
+ this._publisher.publish("tick", remainingTime);
104
+ }
46
105
  };
47
106
 
48
107
  super(callback, msIfNotBrowser);
@@ -51,12 +110,24 @@ export default class Countdown extends GameLoop
51
110
  this._duration = duration;
52
111
  }
53
112
 
113
+ /**
114
+ * The internal method actually responsible for stopping the
115
+ * countdown and resolving or rejecting the {@link Countdown._deferrer} promise.
116
+ *
117
+ * @param reason
118
+ * The reason why the countdown has stopped.
119
+ *
120
+ * - If it's `undefined`, the promise will be resolved.
121
+ * - If it's a value, the promise will be rejected with that value.
122
+ */
54
123
  protected _deferrerStop(reason?: unknown): void
55
124
  {
56
125
  if (!(this._isRunning)) { throw new RuntimeException("The countdown hadn't yet started."); }
57
126
  if (!(this._deferrer)) { throw new FatalErrorException(); }
58
127
 
59
- super.stop();
128
+ this._stop();
129
+ this._handle = undefined;
130
+ this._isRunning = false;
60
131
 
61
132
  if (reason !== undefined) { this._deferrer.reject(reason); }
62
133
  else { this._deferrer.resolve(); }
@@ -64,9 +135,25 @@ export default class Countdown extends GameLoop
64
135
  this._deferrer = undefined;
65
136
  }
66
137
 
67
- public start(remainingTime: number = this.duration): SmartPromise<void>
138
+ /**
139
+ * Starts the execution of the countdown.
140
+ *
141
+ * If the countdown is already running, a {@link RuntimeException} will be thrown.
142
+ *
143
+ * ```ts
144
+ * countdown.onStart(() => { [...] }); // This callback will be executed.
145
+ * countdown.start();
146
+ * ```
147
+ *
148
+ * @param remainingTime
149
+ * The remaining time to set as default when the countdown starts.
150
+ * Default is the {@link Countdown.duration} itself.
151
+ *
152
+ * @returns A {@link SmartPromise} that will be resolved or rejected when the countdown expires or stops.
153
+ */
154
+ public override start(remainingTime: number = this.duration): SmartPromise<void>
68
155
  {
69
- if (this._isRunning) { throw new RuntimeException("The countdown has already been started."); }
156
+ if (this._isRunning) { throw new RuntimeException("The countdown had already stopped or hadn't yet started."); }
70
157
  if (this._deferrer) { throw new FatalErrorException(); }
71
158
 
72
159
  this._deferrer = new DeferredPromise();
@@ -76,33 +163,76 @@ export default class Countdown extends GameLoop
76
163
 
77
164
  return this._deferrer;
78
165
  }
79
- public stop(reason?: unknown): void
166
+
167
+ /**
168
+ * Stops the execution of the countdown.
169
+ *
170
+ * If the countdown hasn't yet started, a {@link RuntimeException} will be thrown.
171
+ *
172
+ * ```ts
173
+ * countdown.onStop(() => { [...] }); // This callback will be executed.
174
+ * countdown.stop();
175
+ * ```
176
+ *
177
+ * @param reason
178
+ * The reason why the countdown has stopped.
179
+ *
180
+ * - If it's `undefined`, the promise will be resolved.
181
+ * - If it's a value, the promise will be rejected with that value.
182
+ */
183
+ public override stop(reason?: unknown): void
80
184
  {
185
+ // TODO: Once solved Issues #6 & #10, make the `reason` parameter required.
186
+ // - https://github.com/Byloth/core/issues/6
187
+ // - https://github.com/Byloth/core/issues/10
188
+ //
81
189
  this._deferrerStop(reason);
82
190
 
83
191
  this._publisher.publish("stop", reason);
84
192
  }
85
193
 
194
+ /**
195
+ * Subscribes to the `expire` event of the countdown.
196
+ *
197
+ * ```ts
198
+ * countdown.onExpire(() => { [...] }); // This callback will be executed once the countdown has expired.
199
+ * countdown.start();
200
+ * ```
201
+ *
202
+ * @param callback The callback that will be executed when the countdown expires.
203
+ *
204
+ * @returns A function that can be used to unsubscribe from the event.
205
+ */
86
206
  public onExpire(callback: () => void): () => void
87
207
  {
88
208
  return this._publisher.subscribe("expire", callback);
89
209
  }
90
210
 
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
-
211
+ /**
212
+ * Subscribes to the `tick` event of the countdown.
213
+ *
214
+ * ```ts
215
+ * countdown.onTick((remainingTime) => { [...] }); // This callback will be executed.
216
+ * countdown.start();
217
+ * ```
218
+ *
219
+ * @param callback The callback that will be executed when the countdown ticks.
220
+ * @param tickStep
221
+ * The minimum time in milliseconds that must pass from the previous execution of the callback to the next one.
222
+ *
223
+ * - If it's a positive number, the callback will be executed only if the
224
+ * time passed from the previous execution is greater than this number.
225
+ * - If it's `0`, the callback will be executed every tick without even checking for the time.
226
+ * - If it's a negative number, a {@link RangeException} will be thrown.
227
+ *
228
+ * @returns A function that can be used to unsubscribe from the event.
229
+ */
100
230
  public onTick(callback: (remainingTime: number) => void, tickStep = 0): () => void
101
231
  {
102
232
  if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
103
233
  if (tickStep === 0) { return this._publisher.subscribe("tick", callback); }
104
234
 
105
- let lastTick = 0;
235
+ let lastTick = this.remainingTime;
106
236
 
107
237
  return this._publisher.subscribe("tick", (remainingTime: number) =>
108
238
  {
@@ -113,5 +243,5 @@ export default class Countdown extends GameLoop
113
243
  });
114
244
  }
115
245
 
116
- public readonly [Symbol.toStringTag]: string = "Countdown";
246
+ public override readonly [Symbol.toStringTag]: string = "Countdown";
117
247
  }
@@ -0,0 +1,243 @@
1
+ import type { Interval } from "../../core/types.js";
2
+ import { isBrowser } from "../../helpers.js";
3
+
4
+ import Publisher from "../callbacks/publisher.js";
5
+ import { FatalErrorException, RuntimeException } from "../exceptions/index.js";
6
+ import type { Callback } from "../types.js";
7
+
8
+ interface GameLoopEventMap
9
+ {
10
+ start: () => void;
11
+ stop: () => void;
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ [key: string]: Callback<any[], any>;
15
+ }
16
+
17
+ /**
18
+ * A class representing a {@link https://en.wikipedia.org/wiki/Video_game_programming#Game_structure|game loop} pattern
19
+ * that allows to run a function at a specific frame rate.
20
+ *
21
+ * In a browser environment, it uses the native {@link requestAnimationFrame}
22
+ * function to run the callback at the refresh rate of the screen.
23
+ * In a non-browser environment, however, it uses the {@link setInterval}
24
+ * function to run the callback at the specified fixed interval of time.
25
+ *
26
+ * Every time the callback is executed, it receives the
27
+ * elapsed time since the start of the game loop.
28
+ * It's also possible to subscribe to the `start` & `stop` events to receive notifications when they occur.
29
+ *
30
+ * ```ts
31
+ * const loop = new GameLoop((elapsedTime: number) =>
32
+ * {
33
+ * console.log(`The game loop has been running for ${elapsedTime}ms.`);
34
+ * });
35
+ *
36
+ * loop.onStart(() => { console.log("The game loop has started."); });
37
+ * loop.onStop(() => { console.log("The game loop has stopped."); });
38
+ *
39
+ * loop.start();
40
+ * ```
41
+ */
42
+ export default class GameLoop
43
+ {
44
+ /**
45
+ * The handle of the interval or the animation frame, depending on the environment.
46
+ * It's used to stop the game loop when the {@link GameLoop._stop} method is called.
47
+ */
48
+ protected _handle?: number | Interval;
49
+
50
+ /**
51
+ * The time when the game loop has started.
52
+ * In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp}
53
+ * of the start of the game loop, it's also used to calculate the elapsed time.
54
+ *
55
+ * This protected property is the only one that can be modified directly by the derived classes.
56
+ * If you're looking for the public and readonly property, use the {@link GameLoop.startTime} getter instead.
57
+ */
58
+ protected _startTime: number;
59
+
60
+ /**
61
+ * The time when the game loop has started.
62
+ * In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp}
63
+ * of the start of the game loop, it's also used to calculate the elapsed time.
64
+ */
65
+ public get startTime(): number
66
+ {
67
+ return this._startTime;
68
+ }
69
+
70
+ /**
71
+ * A flag indicating whether the game loop is currently running or not.
72
+ *
73
+ * This protected property is the only one that can be modified directly by the derived classes.
74
+ * If you're looking for the public and readonly property, use the {@link GameLoop.isRunning} getter instead.
75
+ */
76
+ protected _isRunning: boolean;
77
+
78
+ /**
79
+ * A flag indicating whether the game loop is currently running or not.
80
+ */
81
+ public get isRunning(): boolean
82
+ {
83
+ return this._isRunning;
84
+ }
85
+
86
+ /**
87
+ * The elapsed time since the start of the game loop.
88
+ * It's calculated as the difference between the current time and the {@link GameLoop.startTime}.
89
+ */
90
+ public get elapsedTime(): number
91
+ {
92
+ return performance.now() - this._startTime;
93
+ }
94
+
95
+ /**
96
+ * The {@link Publisher} object that will be used to publish the events of the game loop.
97
+ */
98
+ protected _publisher: Publisher<GameLoopEventMap>;
99
+
100
+ /**
101
+ * The internal method actually responsible for starting the game loop.
102
+ *
103
+ * Depending on the current environment, it could use the
104
+ * {@link requestAnimationFrame} or the {@link setInterval} function.
105
+ */
106
+ protected _start: () => void;
107
+
108
+ /**
109
+ * The internal method actually responsible for stopping the game loop.
110
+ *
111
+ * Depending on the current environment, it could use the
112
+ * {@link cancelAnimationFrame} or the {@link clearInterval} function.
113
+ */
114
+ protected _stop: () => void;
115
+
116
+ /**
117
+ * Initializes a new instance of the {@link GameLoop} class.
118
+ *
119
+ * ```ts
120
+ * const loop = new GameLoop((elapsedTime: number) => { [...] });
121
+ * ```
122
+ *
123
+ * @param callback The function that will be executed at each iteration of the game loop.
124
+ * @param msIfNotBrowser
125
+ * The interval in milliseconds that will be used if the current environment isn't a browser. Default is `40`.
126
+ */
127
+ public constructor(callback: FrameRequestCallback, msIfNotBrowser = 40)
128
+ {
129
+ this._startTime = 0;
130
+ this._isRunning = false;
131
+
132
+ if (isBrowser)
133
+ {
134
+ this._start = () =>
135
+ {
136
+ callback(this.elapsedTime);
137
+
138
+ this._handle = window.requestAnimationFrame(this._start);
139
+ };
140
+
141
+ this._stop = () => window.cancelAnimationFrame(this._handle as number);
142
+ }
143
+ else
144
+ {
145
+ // eslint-disable-next-line no-console
146
+ console.warn(
147
+ "Not a browser environment detected. " +
148
+ `Using setInterval@${msIfNotBrowser}ms instead of requestAnimationFrame...`
149
+ );
150
+
151
+ this._start = () =>
152
+ {
153
+ this._handle = setInterval(() => callback(this.elapsedTime), msIfNotBrowser);
154
+ };
155
+
156
+ this._stop = () => clearInterval(this._handle as Interval);
157
+ }
158
+
159
+ this._publisher = new Publisher();
160
+ }
161
+
162
+ /**
163
+ * Starts the execution of the game loop.
164
+ *
165
+ * If the game loop is already running, a {@link RuntimeException} will be thrown.
166
+ *
167
+ * ```ts
168
+ * loop.onStart(() => { [...] }); // This callback will be executed.
169
+ * loop.start();
170
+ * ```
171
+ *
172
+ * @param elapsedTime The elapsed time to set as default when the game loop starts. Default is `0`.
173
+ */
174
+ public start(elapsedTime = 0): void
175
+ {
176
+ if (this._isRunning) { throw new RuntimeException("The game loop has already been started."); }
177
+
178
+ this._startTime = performance.now() - elapsedTime;
179
+ this._start();
180
+ this._isRunning = true;
181
+
182
+ this._publisher.publish("start");
183
+ }
184
+
185
+ /**
186
+ * Stops the execution of the game loop.
187
+ *
188
+ * If the game loop hasn't yet started, a {@link RuntimeException} will be thrown.
189
+ *
190
+ * ```ts
191
+ * loop.onStop(() => { [...] }); // This callback will be executed.
192
+ * loop.stop();
193
+ * ```
194
+ */
195
+ public stop(): void
196
+ {
197
+ if (!(this._isRunning))
198
+ {
199
+ throw new RuntimeException("The game loop had already stopped or hadn't yet started.");
200
+ }
201
+ if (!(this._handle)) { throw new FatalErrorException(); }
202
+
203
+ this._stop();
204
+ this._handle = undefined;
205
+ this._isRunning = false;
206
+
207
+ this._publisher.publish("stop");
208
+ }
209
+
210
+ /**
211
+ * Subscribes to the `start` event of the game loop.
212
+ *
213
+ * ```ts
214
+ * loop.onStart(() => { console.log("The game loop has started."); });
215
+ * ```
216
+ *
217
+ * @param callback The function that will be executed when the game loop starts.
218
+ *
219
+ * @returns A function that can be used to unsubscribe from the event.
220
+ */
221
+ public onStart(callback: () => void): () => void
222
+ {
223
+ return this._publisher.subscribe("start", callback);
224
+ }
225
+
226
+ /**
227
+ * Subscribes to the `stop` event of the game loop.
228
+ *
229
+ * ```ts
230
+ * loop.onStop(() => { console.log("The game loop has stopped."); });
231
+ * ```
232
+ *
233
+ * @param callback The function that will be executed when the game loop stops.
234
+ *
235
+ * @returns A function that can be used to unsubscribe from the event.
236
+ */
237
+ public onStop(callback: () => void): () => void
238
+ {
239
+ return this._publisher.subscribe("stop", callback);
240
+ }
241
+
242
+ public readonly [Symbol.toStringTag]: string = "GameLoop";
243
+ }
@@ -1,4 +1,5 @@
1
1
  import Clock from "./clock.js";
2
2
  import Countdown from "./countdown.js";
3
+ import GameLoop from "./game-loop.js";
3
4
 
4
- export { Clock, Countdown };
5
+ export { Clock, Countdown, GameLoop };
@@ -1,9 +1,10 @@
1
1
  export type {
2
2
  KeyedIteratee,
3
+ AsyncKeyedIteratee,
3
4
  MaybeAsyncKeyedIteratee,
4
- KeyedTypeGuardIteratee,
5
- MaybeAsyncKeyedTypeGuardIteratee,
5
+ KeyedTypeGuardPredicate,
6
6
  KeyedReducer,
7
+ AsyncKeyedReducer,
7
8
  MaybeAsyncKeyedReducer
8
9
 
9
10
  } from "./aggregators/types.js";
@@ -13,10 +14,11 @@ export type {
13
14
  AsyncGeneratorFunction,
14
15
  MaybeAsyncGeneratorFunction,
15
16
  Iteratee,
17
+ AsyncIteratee,
16
18
  MaybeAsyncIteratee,
17
- TypeGuardIteratee,
18
- MaybeAsyncTypeGuardIteratee,
19
+ TypeGuardPredicate,
19
20
  Reducer,
21
+ AsyncReducer,
20
22
  MaybeAsyncReducer,
21
23
  IteratorLike,
22
24
  AsyncIteratorLike,
@@ -32,7 +34,6 @@ export type {
32
34
  } from "./json/types.js";
33
35
 
34
36
  export type {
35
- LongRunningTaskOptions,
36
37
  MaybePromise,
37
38
  FulfilledHandler,
38
39
  RejectedHandler,