@byloth/core 2.0.0-rc.4 → 2.0.0-rc.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byloth/core",
3
- "version": "2.0.0-rc.4",
3
+ "version": "2.0.0-rc.6",
4
4
  "description": "An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧",
5
5
  "keywords": [
6
6
  "Core",
@@ -47,11 +47,11 @@
47
47
  },
48
48
  "types": "./src/index.ts",
49
49
  "devDependencies": {
50
- "@byloth/eslint-config-typescript": "^3.0.0",
51
- "@types/node": "^20.16.11",
50
+ "@byloth/eslint-config-typescript": "^3.0.1",
51
+ "@types/node": "^22.9.0",
52
52
  "husky": "^9.1.6",
53
53
  "typescript": "^5.6.3",
54
- "vite": "^5.4.8"
54
+ "vite": "^5.4.11"
55
55
  },
56
56
  "scripts": {
57
57
  "dev": "vite",
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "2.0.0-rc.4";
1
+ export const VERSION = "2.0.0-rc.6";
2
2
 
3
3
  export type { Constructor, Interval, Timeout } from "./core/types.js";
4
4
 
@@ -29,6 +29,7 @@ export {
29
29
  SmartIterator,
30
30
  SmartAsyncIterator,
31
31
  SmartPromise,
32
+ Thenable,
32
33
  TimeoutException,
33
34
  TimedPromise,
34
35
  TypeException,
@@ -190,12 +190,7 @@ export default class AggregatedAsyncIterator<K extends PropertyKey, T>
190
190
  for await (const [key, element] of elements)
191
191
  {
192
192
  const index = indexes.get(key) ?? 0;
193
- if (index >= limit)
194
- {
195
- if (indexes.values().every((value) => value >= limit)) { break; }
196
-
197
- continue;
198
- }
193
+ if (index >= limit) { continue; }
199
194
 
200
195
  yield [key, element];
201
196
 
@@ -178,12 +178,7 @@ export default class AggregatedIterator<K extends PropertyKey, T>
178
178
  for (const [key, element] of elements)
179
179
  {
180
180
  const index = indexes.get(key) ?? 0;
181
- if (index >= limit)
182
- {
183
- if (indexes.values().every((value) => value >= limit)) { break; }
184
-
185
- continue;
186
- }
181
+ if (index >= limit) { continue; }
187
182
 
188
183
  yield [key, element];
189
184
 
@@ -1,11 +1,11 @@
1
+ import type { Interval } from "../core/types.js";
1
2
  import { isBrowser } from "../helpers.js";
2
- import { TimeUnit } from "../utils/date.js";
3
3
 
4
4
  import { FatalErrorException, RuntimeException } from "./exceptions/index.js";
5
5
 
6
6
  export default class GameLoop
7
7
  {
8
- protected _handle?: number;
8
+ protected _handle?: number | Interval;
9
9
 
10
10
  protected _startTime: number;
11
11
  public get startTime(): number
@@ -27,7 +27,7 @@ export default class GameLoop
27
27
  protected _start: () => void;
28
28
  protected _stop: () => void;
29
29
 
30
- public constructor(callback: FrameRequestCallback, fpsIfNotBrowser = 30)
30
+ public constructor(callback: FrameRequestCallback, msIfNotBrowser = 40)
31
31
  {
32
32
  this._startTime = 0;
33
33
  this._isRunning = false;
@@ -41,24 +41,22 @@ export default class GameLoop
41
41
  this._handle = window.requestAnimationFrame(this._start);
42
42
  };
43
43
 
44
- this._stop = () => window.cancelAnimationFrame(this._handle!);
44
+ this._stop = () => window.cancelAnimationFrame(this._handle as number);
45
45
  }
46
46
  else
47
47
  {
48
48
  // eslint-disable-next-line no-console
49
49
  console.warn(
50
50
  "Not a browser environment detected. " +
51
- `Using setInterval@${fpsIfNotBrowser}fps instead of requestAnimationFrame...`
51
+ `Using setInterval@${msIfNotBrowser}ms instead of requestAnimationFrame...`
52
52
  );
53
53
 
54
54
  this._start = () =>
55
55
  {
56
- const delay = (TimeUnit.Second / fpsIfNotBrowser);
57
-
58
- this._handle = (setInterval(() => callback(this.elapsedTime), delay) as unknown) as number;
56
+ this._handle = setInterval(() => callback(this.elapsedTime), msIfNotBrowser);
59
57
  };
60
58
 
61
- this._stop = () => clearInterval(this._handle!);
59
+ this._stop = () => clearInterval(this._handle as Interval);
62
60
  }
63
61
  }
64
62
 
@@ -28,7 +28,7 @@ import GameLoop from "./game-loop.js";
28
28
 
29
29
  export { SmartIterator, SmartAsyncIterator } from "./iterators/index.js";
30
30
  export { JSONStorage } from "./json/index.js";
31
- export { DeferredPromise, SmartPromise, TimedPromise } from "./promises/index.js";
31
+ export { DeferredPromise, SmartPromise, Thenable, TimedPromise } from "./promises/index.js";
32
32
 
33
33
  import Publisher from "./publisher.js";
34
34
 
@@ -1,5 +1,6 @@
1
1
  import DeferredPromise from "./deferred-promise.js";
2
2
  import SmartPromise from "./smart-promise.js";
3
+ import Thenable from "./thenable.js";
3
4
  import TimedPromise from "./timed-promise.js";
4
5
 
5
- export { DeferredPromise, SmartPromise, TimedPromise };
6
+ export { DeferredPromise, SmartPromise, Thenable, TimedPromise };
@@ -0,0 +1,97 @@
1
+ export default class Thenable<T> implements Promise<T>
2
+ {
3
+ protected _onFulfilled: (result: T) => T;
4
+ protected _resolve(result: T): T
5
+ {
6
+ return this._onFulfilled(result);
7
+ }
8
+
9
+ public constructor()
10
+ {
11
+ this._onFulfilled = (result: T) => result;
12
+ }
13
+
14
+ public then(onFulfilled?: null): Thenable<T>;
15
+ public then<F = T>(onFulfilled: (result: T) => F, onRejected?: null): Thenable<F>;
16
+ public then<F = T, R = never>(onFulfilled: (result: T) => F, onRejected: (reason: unknown) => R)
17
+ : Thenable<F | R>;
18
+ public then<F = T, R = never>(onFulfilled?: ((result: T) => F) | null, onRejected?: ((reason: unknown) => R) | null)
19
+ : Thenable<F | R>
20
+ {
21
+ if (onRejected)
22
+ {
23
+ const _previousOnFulfilled = this._onFulfilled;
24
+ this._onFulfilled = (result: T) =>
25
+ {
26
+ try
27
+ {
28
+ result = _previousOnFulfilled(result);
29
+
30
+ return (onFulfilled!(result) as unknown) as T;
31
+ }
32
+ catch (error)
33
+ {
34
+ return (onRejected(error) as unknown) as T;
35
+ }
36
+ };
37
+ }
38
+ else if (onFulfilled)
39
+ {
40
+ const _previousOnFulfilled = this._onFulfilled;
41
+ this._onFulfilled = (result: T) =>
42
+ {
43
+ result = _previousOnFulfilled(result);
44
+
45
+ return (onFulfilled(result) as unknown) as T;
46
+ };
47
+ }
48
+
49
+ return (this as unknown) as Thenable<F | R>;
50
+ }
51
+
52
+ public catch(onRejected?: null): Thenable<T>;
53
+ public catch<R = never>(onRejected: (reason: unknown) => R): Thenable<T | R>;
54
+ public catch<R = never>(onRejected?: ((reason: unknown) => R) | null): Thenable<T | R>
55
+ {
56
+ if (onRejected)
57
+ {
58
+ const _previousOnFulfilled = this._onFulfilled;
59
+ this._onFulfilled = (result) =>
60
+ {
61
+ try
62
+ {
63
+ return _previousOnFulfilled(result);
64
+ }
65
+ catch (error)
66
+ {
67
+ return (onRejected(error) as unknown) as T;
68
+ }
69
+ };
70
+ }
71
+
72
+ return this as Thenable<T | R>;
73
+ }
74
+
75
+ public finally(onFinally?: (() => void) | null): Thenable<T>
76
+ {
77
+ if (onFinally)
78
+ {
79
+ const _previousOnFulfilled = this._onFulfilled;
80
+ this._onFulfilled = (result) =>
81
+ {
82
+ try
83
+ {
84
+ return _previousOnFulfilled(result);
85
+ }
86
+ finally
87
+ {
88
+ onFinally();
89
+ }
90
+ };
91
+ }
92
+
93
+ return this;
94
+ }
95
+
96
+ public readonly [Symbol.toStringTag]: string = "Thenable";
97
+ }
@@ -6,13 +6,17 @@ import Publisher from "../publisher.js";
6
6
 
7
7
  export default class Clock extends GameLoop
8
8
  {
9
- protected _publisher: Publisher<[number]>;
9
+ protected _starter: Publisher;
10
+ protected _stopper: Publisher;
11
+ protected _ticker: Publisher<[number]>;
10
12
 
11
- public constructor(fpsIfNotBrowser: number = TimeUnit.Second)
13
+ public constructor(msIfNotBrowser: number = TimeUnit.Second)
12
14
  {
13
- super((elapsedTime) => this._publisher.publish(elapsedTime), fpsIfNotBrowser);
15
+ super((elapsedTime) => this._ticker.publish(elapsedTime), msIfNotBrowser);
14
16
 
15
- this._publisher = new Publisher();
17
+ this._starter = new Publisher();
18
+ this._stopper = new Publisher();
19
+ this._ticker = new Publisher();
16
20
  }
17
21
 
18
22
  public start(elapsedTime = 0): void
@@ -20,6 +24,8 @@ export default class Clock extends GameLoop
20
24
  if (this._isRunning) { throw new RuntimeException("The clock has already been started."); }
21
25
 
22
26
  super.start(elapsedTime);
27
+
28
+ this._starter.publish();
23
29
  }
24
30
 
25
31
  public stop(): void
@@ -27,16 +33,27 @@ export default class Clock extends GameLoop
27
33
  if (!(this._isRunning)) { throw new RuntimeException("The clock hadn't yet started."); }
28
34
 
29
35
  super.stop();
36
+
37
+ this._stopper.publish();
38
+ }
39
+
40
+ public onStart(callback: () => void): () => void
41
+ {
42
+ return this._starter.subscribe(callback);
43
+ }
44
+ public onStop(callback: () => void): () => void
45
+ {
46
+ return this._stopper.subscribe(callback);
30
47
  }
31
48
 
32
49
  public onTick(callback: (elapsedTime: number) => void, tickStep = 0): () => void
33
50
  {
34
51
  if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
35
- if (tickStep === 0) { return this._publisher.subscribe(callback); }
52
+ if (tickStep === 0) { return this._ticker.subscribe(callback); }
36
53
 
37
54
  let lastTick = 0;
38
55
 
39
- return this._publisher.subscribe((elapsedTime: number) =>
56
+ return this._ticker.subscribe((elapsedTime: number) =>
40
57
  {
41
58
  if ((elapsedTime - lastTick) < tickStep) { return; }
42
59
 
@@ -9,7 +9,11 @@ import Publisher from "../publisher.js";
9
9
  export default class Countdown extends GameLoop
10
10
  {
11
11
  protected _deferrer?: DeferredPromise<void>;
12
- protected _publisher: Publisher<[number]>;
12
+
13
+ protected _expirer: Publisher;
14
+ protected _starter: Publisher;
15
+ protected _stopper: Publisher<[unknown]>;
16
+ protected _ticker: Publisher<[number]>;
13
17
 
14
18
  protected _duration: number;
15
19
  public get duration(): number
@@ -22,22 +26,44 @@ export default class Countdown extends GameLoop
22
26
  return this._duration - this.elapsedTime;
23
27
  }
24
28
 
25
- public constructor(duration: number, fpsIfNotBrowser: number = TimeUnit.Second)
29
+ public constructor(duration: number, msIfNotBrowser: number = TimeUnit.Second)
26
30
  {
27
31
  const callback = () =>
28
32
  {
29
33
  const remainingTime = this.remainingTime;
30
- this._publisher.publish(remainingTime);
34
+ this._ticker.publish(remainingTime);
35
+
36
+ if (remainingTime <= 0)
37
+ {
38
+ this._deferrerStop();
31
39
 
32
- if (remainingTime <= 0) { this.stop(); }
40
+ this._expirer.publish();
41
+ }
33
42
  };
34
43
 
35
- super(callback, fpsIfNotBrowser);
44
+ super(callback, msIfNotBrowser);
45
+
46
+ this._expirer = new Publisher();
47
+ this._starter = new Publisher();
48
+ this._stopper = new Publisher();
49
+ this._ticker = new Publisher();
36
50
 
37
- this._publisher = new Publisher();
38
51
  this._duration = duration;
39
52
  }
40
53
 
54
+ protected _deferrerStop(reason?: unknown): void
55
+ {
56
+ if (!(this._isRunning)) { throw new RuntimeException("The countdown hadn't yet started."); }
57
+ if (!(this._deferrer)) { throw new FatalErrorException(); }
58
+
59
+ super.stop();
60
+
61
+ if (reason !== undefined) { this._deferrer.reject(reason); }
62
+ else { this._deferrer.resolve(); }
63
+
64
+ this._deferrer = undefined;
65
+ }
66
+
41
67
  public start(remainingTime: number = this.duration): SmartPromise<void>
42
68
  {
43
69
  if (this._isRunning) { throw new RuntimeException("The countdown has already been started."); }
@@ -46,29 +72,39 @@ export default class Countdown extends GameLoop
46
72
  this._deferrer = new DeferredPromise();
47
73
  super.start(this.duration - remainingTime);
48
74
 
75
+ this._starter.publish();
76
+
49
77
  return this._deferrer;
50
78
  }
51
79
  public stop(reason?: unknown): void
52
80
  {
53
- if (!(this._isRunning)) { throw new RuntimeException("The countdown hadn't yet started."); }
54
- if (!(this._deferrer)) { throw new FatalErrorException(); }
81
+ this._deferrerStop(reason);
55
82
 
56
- super.stop();
83
+ this._stopper.publish(reason);
84
+ }
57
85
 
58
- if (reason !== undefined) { this._deferrer.reject(reason); }
59
- else { this._deferrer.resolve(); }
86
+ public onExpire(callback: () => void): () => void
87
+ {
88
+ return this._expirer.subscribe(callback);
89
+ }
60
90
 
61
- this._deferrer = undefined;
91
+ public onStart(callback: () => void): () => void
92
+ {
93
+ return this._starter.subscribe(callback);
94
+ }
95
+ public onStop(callback: (reason?: unknown) => void): () => void
96
+ {
97
+ return this._stopper.subscribe(callback);
62
98
  }
63
99
 
64
100
  public onTick(callback: (remainingTime: number) => void, tickStep = 0): () => void
65
101
  {
66
102
  if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
67
- if (tickStep === 0) { return this._publisher.subscribe(callback); }
103
+ if (tickStep === 0) { return this._ticker.subscribe(callback); }
68
104
 
69
105
  let lastTick = 0;
70
106
 
71
- return this._publisher.subscribe((remainingTime: number) =>
107
+ return this._ticker.subscribe((remainingTime: number) =>
72
108
  {
73
109
  if ((lastTick - remainingTime) < tickStep) { return; }
74
110