@byloth/core 2.1.2 → 2.1.4

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.1.2",
3
+ "version": "2.1.4",
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",
@@ -50,15 +50,15 @@
50
50
  "types": "src/index.ts",
51
51
  "devDependencies": {
52
52
  "@byloth/eslint-config-typescript": "^3.1.0",
53
- "@eslint/compat": "^1.2.9",
54
- "@types/node": "^22.15.21",
55
- "@vitest/coverage-v8": "^3.1.4",
56
- "eslint": "^9.27.0",
53
+ "@eslint/compat": "^1.3.0",
54
+ "@types/node": "^22.15.31",
55
+ "@vitest/coverage-v8": "^3.2.3",
56
+ "eslint": "^9.28.0",
57
57
  "husky": "^9.1.7",
58
58
  "jsdom": "^26.1.0",
59
59
  "typescript": "^5.8.3",
60
60
  "vite": "^6.3.5",
61
- "vitest": "^3.1.4"
61
+ "vitest": "^3.2.3"
62
62
  },
63
63
  "scripts": {
64
64
  "dev": "vite",
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "2.1.2";
1
+ export const VERSION = "2.1.4";
2
2
 
3
3
  export type { Constructor, Interval, Timeout, ValueOf } from "./core/types.js";
4
4
 
@@ -52,7 +52,7 @@ export default class AggregatedAsyncIterator<K extends PropertyKey, T>
52
52
  /**
53
53
  * The internal {@link SmartAsyncIterator} object that holds the elements to aggregate.
54
54
  */
55
- protected _elements: SmartAsyncIterator<[K, T]>;
55
+ protected readonly _elements: SmartAsyncIterator<[K, T]>;
56
56
 
57
57
  /**
58
58
  * Initializes a new instance of the {@link AggregatedAsyncIterator} class.
@@ -43,7 +43,7 @@ export default class AggregatedIterator<K extends PropertyKey, T>
43
43
  /**
44
44
  * The internal {@link SmartIterator} object that holds the elements to aggregate.
45
45
  */
46
- protected _elements: SmartIterator<[K, T]>;
46
+ protected readonly _elements: SmartIterator<[K, T]>;
47
47
 
48
48
  /**
49
49
  * Initializes a new instance of the {@link AggregatedIterator} class.
@@ -45,7 +45,7 @@ export default class ReducedIterator<K extends PropertyKey, T>
45
45
  /**
46
46
  * The internal {@link SmartIterator} object that holds the reduced elements.
47
47
  */
48
- protected _elements: SmartIterator<[K, T]>;
48
+ protected readonly _elements: SmartIterator<[K, T]>;
49
49
 
50
50
  /**
51
51
  * Initializes a new instance of the {@link ReducedIterator} class.
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
-
3
1
  import type { Callback } from "./types.js";
4
2
 
5
3
  const SmartFunction = (Function as unknown) as new<A extends unknown[] = [], R = void>(...args: string[])
@@ -34,6 +32,7 @@ const SmartFunction = (Function as unknown) as new<A extends unknown[] = [], R =
34
32
  * The type signature of the callback function.
35
33
  * It must be a function. Default is `(...args: any[]) => any`.
36
34
  */
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
36
  export default abstract class CallableObject<T extends Callback<any[], any> = () => void>
38
37
  extends SmartFunction<Parameters<T>, ReturnType<T>>
39
38
  {
@@ -1,6 +1,6 @@
1
1
  import { ReferenceException } from "../exceptions/index.js";
2
2
 
3
- import type { Callback, CallbackMap } from "./types.js";
3
+ import type { Callback, CallbackMap, WithWildcard } from "./types.js";
4
4
 
5
5
  /**
6
6
  * A class implementing the
@@ -39,8 +39,10 @@ import type { Callback, CallbackMap } from "./types.js";
39
39
  * A map containing the names of the emittable events and the
40
40
  * related callback signatures that can be subscribed to them.
41
41
  * Default is `Record<string, (...args: unknown[]) => unknown>`.
42
+ *
43
+ * @template E An utility type that extends the `T` map with a wildcard event.
42
44
  */
43
- export default class Publisher<T extends CallbackMap<T> = CallbackMap>
45
+ export default class Publisher<T extends CallbackMap<T> = CallbackMap, W extends WithWildcard<T> = WithWildcard<T>>
44
46
  {
45
47
  /**
46
48
  * A map containing all the subscribers for each event.
@@ -48,7 +50,7 @@ export default class Publisher<T extends CallbackMap<T> = CallbackMap>
48
50
  * The keys are the names of the events they are subscribed to.
49
51
  * The values are the arrays of the subscribers themselves.
50
52
  */
51
- protected _subscribers: Map<string, Callback<unknown[], unknown>[]>;
53
+ protected readonly _subscribers: Map<string, Callback<unknown[], unknown>[]>;
52
54
 
53
55
  /**
54
56
  * Initializes a new instance of the {@link Publisher} class.
@@ -87,9 +89,59 @@ export default class Publisher<T extends CallbackMap<T> = CallbackMap>
87
89
  */
88
90
  public clear(): void
89
91
  {
92
+ // @ts-expect-error It's an internal event, not part of the public API.
93
+ this.publish("__internals__:clear");
94
+
90
95
  this._subscribers.clear();
91
96
  }
92
97
 
98
+ /**
99
+ * Creates a new scoped instance of the {@link Publisher} class,
100
+ * which can be used to publish and subscribe events within a specific context.
101
+ *
102
+ * It can receive all events published to the parent publisher while also allowing
103
+ * the scoped publisher to handle its own events independently.
104
+ * In fact, events published to the scoped publisher won't be propagated back to the parent publisher.
105
+ *
106
+ * ---
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const publisher = new Publisher();
111
+ * const context = publisher.createScope();
112
+ *
113
+ * publisher.subscribe("player:death", () => { console.log(`Player has died.`); });
114
+ * context.subscribe("player:spawn", () => { console.log(`Player has spawned.`); });
115
+ *
116
+ * publisher.publish("player:spawn"); // Player has spawned.
117
+ * context.publish("player:death"); // * no output *
118
+ * ```
119
+ *
120
+ * ---
121
+ *
122
+ * @template U
123
+ * A map containing the names of the emittable events and the
124
+ * related callback signatures that can be subscribed to them.
125
+ * Default is `T`.
126
+ *
127
+ * @template X An utility type that extends the `U` map with a wildcard event.
128
+ */
129
+ public createScope<U extends T = T, X extends WithWildcard<U> = WithWildcard<U>>(): Publisher<U, X>
130
+ {
131
+ const scope = new Publisher<U, X>();
132
+
133
+ const propagator = (event: (keyof T) & string, ...args: Parameters<T[keyof T]>): void =>
134
+ {
135
+ scope.publish(event, ...args);
136
+ };
137
+
138
+ // @ts-expect-error It's an internal event, not part of the public API.
139
+ this.subscribe("__internals__:clear", () => scope.clear());
140
+ this.subscribe("*", propagator as W["*"]);
141
+
142
+ return scope;
143
+ }
144
+
93
145
  /**
94
146
  * Publishes an event to all the subscribers.
95
147
  *
@@ -115,11 +167,26 @@ export default class Publisher<T extends CallbackMap<T> = CallbackMap>
115
167
  */
116
168
  public publish<K extends keyof T>(event: K & string, ...args: Parameters<T[K]>): ReturnType<T[K]>[]
117
169
  {
118
- const subscribers = this._subscribers.get(event);
119
- if (!(subscribers)) { return []; }
170
+ let results: ReturnType<T[K]>[];
171
+ let subscribers = this._subscribers.get(event);
172
+ if (subscribers)
173
+ {
174
+ results = subscribers.slice()
175
+ .map((subscriber) => subscriber(...args)) as ReturnType<T[K]>[];
176
+ }
177
+ else { results = []; }
178
+
179
+ if (!(event.startsWith("__")))
180
+ {
181
+ subscribers = this._subscribers.get("*");
182
+ if (subscribers)
183
+ {
184
+ subscribers.slice()
185
+ .forEach((subscriber) => subscriber(event, ...args));
186
+ }
187
+ }
120
188
 
121
- return subscribers.slice()
122
- .map((subscriber) => subscriber(...args)) as ReturnType<T[K]>[];
189
+ return results;
123
190
  }
124
191
 
125
192
  /**
@@ -146,13 +213,13 @@ export default class Publisher<T extends CallbackMap<T> = CallbackMap>
146
213
  *
147
214
  * @returns A function that can be used to unsubscribe the subscriber from the event.
148
215
  */
149
- public subscribe<K extends keyof T>(event: K & string, subscriber: T[K]): () => void
216
+ public subscribe<K extends keyof W>(event: K & string, subscriber: W[K]): () => void
150
217
  {
151
- if (!(this._subscribers.has(event))) { this._subscribers.set(event, []); }
152
-
153
- const subscribers = this._subscribers.get(event)!;
218
+ const subscribers = this._subscribers.get(event) ?? [];
154
219
  subscribers.push(subscriber);
155
220
 
221
+ this._subscribers.set(event, subscribers);
222
+
156
223
  return () =>
157
224
  {
158
225
  const index = subscribers.indexOf(subscriber);
@@ -186,10 +253,14 @@ export default class Publisher<T extends CallbackMap<T> = CallbackMap>
186
253
  * @param event The name of the event to unsubscribe from.
187
254
  * @param subscriber The subscriber to remove from the event.
188
255
  */
189
- public unsubscribe<K extends keyof T>(event: K & string, subscriber: T[K]): void
256
+ public unsubscribe<K extends keyof W>(event: K & string, subscriber: W[K]): void
190
257
  {
191
258
  const subscribers = this._subscribers.get(event);
192
- if (!(subscribers)) { return; }
259
+ if (!(subscribers))
260
+ {
261
+ throw new ReferenceException("Unable to unsubscribe the required subscriber. " +
262
+ "The subscription was already unsubscribed or was never subscribed.");
263
+ }
193
264
 
194
265
  const index = subscribers.indexOf(subscriber);
195
266
  if (index < 0)
@@ -199,6 +270,7 @@ export default class Publisher<T extends CallbackMap<T> = CallbackMap>
199
270
  }
200
271
 
201
272
  subscribers.splice(index, 1);
273
+ if (subscribers.length === 0) { this._subscribers.delete(event); }
202
274
  }
203
275
 
204
276
  public readonly [Symbol.toStringTag]: string = "Publisher";
@@ -43,7 +43,7 @@ export default class SwitchableCallback<T extends Callback<any[], any> = Callbac
43
43
  * The keys are the names of the implementations they were registered with.
44
44
  * The values are the implementations themselves.
45
45
  */
46
- protected _callbacks: Map<string, T>;
46
+ protected readonly _callbacks: Map<string, T>;
47
47
 
48
48
  /**
49
49
  * A flag indicating whether the callback is enabled or not.
@@ -157,3 +157,16 @@ export interface Subscribable<T extends CallbackMap<T> = CallbackMap>
157
157
  */
158
158
  unsubscribe<K extends keyof T>(event: K & string, subscriber: T[K]): void;
159
159
  }
160
+
161
+ /**
162
+ * An utility type that may be used to wrap a {@link CallbackMap} and extend it with a wildcard event.
163
+ * The resulting type will be the same as the original map, but with an additional `"*"` key.
164
+ *
165
+ * It's natively used by the {@link Publisher} class to allow subscribers to listen to all events.
166
+ *
167
+ * ---
168
+ *
169
+ * @template T A `CallbackMap` compatible interface that defines the map of callbacks.
170
+ *
171
+ */
172
+ export type WithWildcard<T extends CallbackMap<T>> = T & { "*"?: (type: string, ...args: unknown[]) => void };
@@ -1,4 +1,5 @@
1
1
  import Publisher from "../callbacks/publisher.js";
2
+ import type { WithWildcard } from "../callbacks/types.js";
2
3
 
3
4
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
5
  import type SetView from "./set-view.js";
@@ -172,7 +173,9 @@ export default class MapView<K, V> extends Map<K, V>
172
173
  *
173
174
  * @returns A function that can be used to unsubscribe the callback from the event.
174
175
  */
175
- public subscribe<T extends keyof MapViewEventsMap<K, V>>(event: T, callback: MapViewEventsMap<K, V>[T]): () => void
176
+ public subscribe<T extends keyof WithWildcard<MapViewEventsMap<K, V>>>(
177
+ event: T & string, callback: WithWildcard<MapViewEventsMap<K, V>>[T]
178
+ ): () => void
176
179
  {
177
180
  return this._publisher.subscribe(event, callback);
178
181
  }
@@ -201,7 +204,8 @@ export default class MapView<K, V> extends Map<K, V>
201
204
  * @param event The name of the event to unsubscribe from.
202
205
  * @param callback The callback to remove from the event.
203
206
  */
204
- public unsubscribe<T extends keyof MapViewEventsMap<K, V>>(event: T, callback: MapViewEventsMap<K, V>[T]): void
207
+ public unsubscribe<T extends keyof WithWildcard<MapViewEventsMap<K, V>>>(
208
+ event: T & string, callback: WithWildcard<MapViewEventsMap<K, V>>[T]): void
205
209
  {
206
210
  this._publisher.unsubscribe(event, callback);
207
211
  }
@@ -1,4 +1,5 @@
1
1
  import Publisher from "../callbacks/publisher.js";
2
+ import type { WithWildcard } from "../callbacks/types.js";
2
3
 
3
4
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
5
  import type MapView from "./map-view.js";
@@ -166,7 +167,9 @@ export default class SetView<T> extends Set<T>
166
167
  *
167
168
  * @returns A function that can be used to unsubscribe the callback from the event.
168
169
  */
169
- public subscribe<K extends keyof SetViewEventsMap<T>>(event: K, callback: SetViewEventsMap<T>[K]): () => void
170
+ public subscribe<K extends keyof WithWildcard<SetViewEventsMap<T>>>(
171
+ event: K & string, callback: WithWildcard<SetViewEventsMap<T>>[K]
172
+ ): () => void
170
173
  {
171
174
  return this._publisher.subscribe(event, callback);
172
175
  }
@@ -195,7 +198,9 @@ export default class SetView<T> extends Set<T>
195
198
  * @param event The name of the event to unsubscribe from.
196
199
  * @param callback The callback to remove from the event.
197
200
  */
198
- public unsubscribe<K extends keyof SetViewEventsMap<T>>(event: K, callback: SetViewEventsMap<T>[K]): void
201
+ public unsubscribe<K extends keyof WithWildcard<SetViewEventsMap<T>>>(
202
+ event: K & string, callback: WithWildcard<SetViewEventsMap<T>>[K]
203
+ ): void
199
204
  {
200
205
  this._publisher.unsubscribe(event, callback);
201
206
  }
@@ -1,5 +1,6 @@
1
1
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2
2
  import type MapView from "./map-view.js";
3
+
3
4
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
5
  import type SetView from "./set-view.js";
5
6
 
@@ -51,7 +51,7 @@ export default class SmartAsyncIterator<T, R = void, N = undefined> implements A
51
51
  /**
52
52
  * The native {@link AsyncIterator} object that is being wrapped by this instance.
53
53
  */
54
- protected _iterator: AsyncIterator<T, R, N>;
54
+ protected readonly _iterator: AsyncIterator<T, R, N>;
55
55
 
56
56
  /**
57
57
  * Initializes a new instance of the {@link SmartAsyncIterator} class.
@@ -42,7 +42,7 @@ export default class SmartIterator<T, R = void, N = undefined> implements Iterat
42
42
  /**
43
43
  * The native {@link Iterator} object that is being wrapped by this instance.
44
44
  */
45
- protected _iterator: Iterator<T, R, N>;
45
+ protected readonly _iterator: Iterator<T, R, N>;
46
46
 
47
47
  /**
48
48
  * Initializes a new instance of the {@link SmartIterator} class.
@@ -29,17 +29,17 @@ export default class JSONStorage
29
29
  * If `true`, the persistent storage is preferred. If `false`, the volatile storage is preferred.
30
30
  * Default is `true`.
31
31
  */
32
- protected _preferPersistence: boolean;
32
+ protected readonly _preferPersistence: boolean;
33
33
 
34
34
  /**
35
35
  * A reference to the volatile {@link sessionStorage} storage.
36
36
  */
37
- protected _volatile: Storage;
37
+ protected readonly _volatile: Storage;
38
38
 
39
39
  /**
40
40
  * A reference to the persistent {@link localStorage} storage.
41
41
  */
42
- protected _persistent: Storage;
42
+ protected readonly _persistent: Storage;
43
43
 
44
44
  /**
45
45
  * Initializes a new instance of the {@link JSONStorage} class.
@@ -40,7 +40,7 @@ export default class DeferredPromise<T = void, F = T, R = never> extends SmartPr
40
40
  * This protected property is the only one that can be modified directly by the derived classes.
41
41
  * If you're looking for the public and readonly property, use the {@link DeferredPromise.resolve} getter instead.
42
42
  */
43
- protected _resolve: PromiseResolver<T>;
43
+ protected readonly _resolve: PromiseResolver<T>;
44
44
 
45
45
  /**
46
46
  * The exposed function that allows to reject the promise.
@@ -53,13 +53,18 @@ export default class DeferredPromise<T = void, F = T, R = never> extends SmartPr
53
53
  * This protected property is the only one that can be modified directly by the derived classes.
54
54
  * If you're looking for the public and readonly property, use the {@link DeferredPromise.reject} getter instead.
55
55
  */
56
- protected _reject: PromiseRejecter;
56
+ protected readonly _reject: PromiseRejecter;
57
57
 
58
58
  /**
59
59
  * The exposed function that allows to reject the promise.
60
60
  */
61
61
  public get reject(): PromiseRejecter { return this._reject; }
62
62
 
63
+ /**
64
+ * The native {@link Promise} object wrapped by this instance.
65
+ */
66
+ declare protected readonly _promise: Promise<F | R>;
67
+
63
68
  /**
64
69
  * Initializes a new instance of the {@link DeferredPromise} class.
65
70
  *
@@ -110,7 +110,7 @@ export default class SmartPromise<T = void> implements Promise<T>
110
110
  /**
111
111
  * The native {@link Promise} object wrapped by this instance.
112
112
  */
113
- protected _promise: Promise<T>;
113
+ protected readonly _promise: Promise<T>;
114
114
 
115
115
  /**
116
116
  * Initializes a new instance of the {@link SmartPromise} class.
@@ -2,18 +2,14 @@ import { TimeUnit } from "../../utils/date.js";
2
2
 
3
3
  import Publisher from "../callbacks/publisher.js";
4
4
  import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js";
5
- import type { Callback } from "../types.js";
6
5
 
7
6
  import GameLoop from "./game-loop.js";
8
7
 
9
- interface ClockEventMap
8
+ interface ClockEventsMap
10
9
  {
11
10
  start: () => void;
12
11
  stop: () => void;
13
12
  tick: (elapsedTime: number) => void;
14
-
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- [key: string]: Callback<any[], any>;
17
13
  }
18
14
 
19
15
  /**
@@ -40,7 +36,7 @@ export default class Clock extends GameLoop
40
36
  /**
41
37
  * The {@link Publisher} object that will be used to publish the events of the clock.
42
38
  */
43
- protected override _publisher: Publisher<ClockEventMap>;
39
+ declare protected readonly _publisher: Publisher<ClockEventsMap>;
44
40
 
45
41
  /**
46
42
  * Initializes a new instance of the {@link Clock} class.
@@ -61,8 +57,6 @@ export default class Clock extends GameLoop
61
57
  public constructor(msIfNotBrowser: number = TimeUnit.Second)
62
58
  {
63
59
  super((elapsedTime) => this._publisher.publish("tick", elapsedTime), msIfNotBrowser);
64
-
65
- this._publisher = new Publisher();
66
60
  }
67
61
 
68
62
  /**
@@ -7,7 +7,7 @@ import type { Callback } from "../types.js";
7
7
 
8
8
  import GameLoop from "./game-loop.js";
9
9
 
10
- interface CountdownEventMap
10
+ interface CountdownEventsMap
11
11
  {
12
12
  start: () => void;
13
13
  stop: (reason: unknown) => void;
@@ -43,7 +43,7 @@ export default class Countdown extends GameLoop
43
43
  /**
44
44
  * The {@link Publisher} object that will be used to publish the events of the countdown.
45
45
  */
46
- protected override _publisher: Publisher<CountdownEventMap>;
46
+ declare protected readonly _publisher: Publisher<CountdownEventsMap>;
47
47
 
48
48
  /**
49
49
  * The total duration of the countdown in milliseconds.
@@ -114,7 +114,6 @@ export default class Countdown extends GameLoop
114
114
 
115
115
  super(callback, msIfNotBrowser);
116
116
 
117
- this._publisher = new Publisher();
118
117
  this._duration = duration;
119
118
  }
120
119
 
@@ -3,15 +3,11 @@ import { isBrowser } from "../../helpers.js";
3
3
 
4
4
  import Publisher from "../callbacks/publisher.js";
5
5
  import { FatalErrorException, RuntimeException } from "../exceptions/index.js";
6
- import type { Callback } from "../types.js";
7
6
 
8
- interface GameLoopEventMap
7
+ interface GameLoopEventsMap
9
8
  {
10
9
  start: () => void;
11
10
  stop: () => void;
12
-
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- [key: string]: Callback<any[], any>;
15
11
  }
16
12
 
17
13
  /**
@@ -98,7 +94,7 @@ export default class GameLoop
98
94
  /**
99
95
  * The {@link Publisher} object that will be used to publish the events of the game loop.
100
96
  */
101
- protected _publisher: Publisher<GameLoopEventMap>;
97
+ protected readonly _publisher: Publisher<GameLoopEventsMap>;
102
98
 
103
99
  /**
104
100
  * The internal method actually responsible for starting the game loop.
@@ -106,7 +102,7 @@ export default class GameLoop
106
102
  * Depending on the current environment, it could use the
107
103
  * {@link requestAnimationFrame} or the {@link setInterval} function.
108
104
  */
109
- protected _start: () => void;
105
+ protected readonly _start: () => void;
110
106
 
111
107
  /**
112
108
  * The internal method actually responsible for stopping the game loop.
@@ -114,7 +110,7 @@ export default class GameLoop
114
110
  * Depending on the current environment, it could use the
115
111
  * {@link cancelAnimationFrame} or the {@link clearInterval} function.
116
112
  */
117
- protected _stop: () => void;
113
+ protected readonly _stop: () => void;
118
114
 
119
115
  /**
120
116
  * Initializes a new instance of the {@link GameLoop} class.
package/src/utils/date.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/prefer-literal-enum-member */
1
2
 
2
3
  import { RangeException, SmartIterator } from "../models/index.js";
3
4
 
@@ -15,8 +16,6 @@ import { RangeException, SmartIterator } from "../models/index.js";
15
16
  */
16
17
  export enum TimeUnit
17
18
  {
18
- /* eslint-disable @typescript-eslint/prefer-literal-enum-member */
19
-
20
19
  /**
21
20
  * A millisecond: the base time unit.
22
21
  */