@byloth/core 2.0.0-rc.7 → 2.0.0-rc.9

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.7",
3
+ "version": "2.0.0-rc.9",
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,10 +47,10 @@
47
47
  },
48
48
  "types": "./src/index.ts",
49
49
  "devDependencies": {
50
- "@byloth/eslint-config-typescript": "^3.0.1",
51
- "@types/node": "^22.9.0",
52
- "husky": "^9.1.6",
53
- "typescript": "^5.6.3",
50
+ "@byloth/eslint-config-typescript": "^3.0.3",
51
+ "@types/node": "^22.10.1",
52
+ "husky": "^9.1.7",
53
+ "typescript": "^5.7.2",
54
54
  "vite": "^5.4.11"
55
55
  },
56
56
  "scripts": {
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "2.0.0-rc.7";
1
+ export const VERSION = "2.0.0-rc.9";
2
2
 
3
3
  export type { Constructor, Interval, Timeout } from "./core/types.js";
4
4
 
@@ -7,6 +7,7 @@ export { isBrowser, isNode, isWebWorker } from "./helpers.js";
7
7
  export {
8
8
  AggregatedIterator,
9
9
  AggregatedAsyncIterator,
10
+ CallableObject,
10
11
  Clock,
11
12
  Countdown,
12
13
  DeferredPromise,
@@ -18,6 +19,7 @@ export {
18
19
  GameLoop,
19
20
  JSONStorage,
20
21
  KeyException,
22
+ LongRunningTask,
21
23
  NotImplementedException,
22
24
  NetworkException,
23
25
  PermissionException,
@@ -29,6 +31,7 @@ export {
29
31
  SmartIterator,
30
32
  SmartAsyncIterator,
31
33
  SmartPromise,
34
+ SwitchableCallback,
32
35
  Thenable,
33
36
  TimeoutException,
34
37
  TimedPromise,
@@ -40,6 +43,7 @@ export {
40
43
  export type {
41
44
  AsyncGeneratorFunction,
42
45
  AsyncIteratorLike,
46
+ Callback,
43
47
  FulfilledHandler,
44
48
  GeneratorFunction,
45
49
  Iteratee,
@@ -50,12 +54,14 @@ export type {
50
54
  KeyedIteratee,
51
55
  KeyedReducer,
52
56
  KeyedTypeGuardIteratee,
57
+ LongRunningTaskOptions,
53
58
  MaybeAsyncKeyedIteratee,
54
59
  MaybeAsyncKeyedReducer,
55
60
  MaybeAsyncKeyedTypeGuardIteratee,
56
- MaybeAsyncReducer,
61
+ MaybeAsyncGeneratorFunction,
57
62
  MaybeAsyncIteratee,
58
63
  MaybeAsyncIteratorLike,
64
+ MaybeAsyncReducer,
59
65
  MaybeAsyncTypeGuardIteratee,
60
66
  MaybePromise,
61
67
  PromiseExecutor,
@@ -63,7 +69,6 @@ export type {
63
69
  PromiseResolver,
64
70
  Reducer,
65
71
  RejectedHandler,
66
- Subscriber,
67
72
  TypeGuardIteratee
68
73
 
69
74
  } from "./models/types.js";
@@ -87,6 +92,7 @@ export {
87
92
  shuffle,
88
93
  sum,
89
94
  unique,
95
+ yieldToEventLoop,
90
96
  zip
91
97
 
92
98
  } from "./utils/index.js";
@@ -0,0 +1,23 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import type { Callback } from "./types.js";
4
+
5
+ export const SmartFunction = (Function as unknown) as
6
+ new<T extends Callback<any[], any> = () => void>(...args: string[]) =>
7
+ (...args: Parameters<T>) => ReturnType<T>;
8
+
9
+ export default abstract class CallableObject<T extends Callback<any[], any> = () => void>
10
+ extends SmartFunction<T>
11
+ {
12
+ public constructor()
13
+ {
14
+ super(`return this.invoke(...arguments);`);
15
+
16
+ const self = this.bind(this);
17
+ Object.setPrototypeOf(this, self);
18
+
19
+ return self as this;
20
+ }
21
+
22
+ public abstract invoke(...args: Parameters<T>): ReturnType<T>;
23
+ }
@@ -0,0 +1,5 @@
1
+ import CallableObject, { SmartFunction } from "./callable-object.js";
2
+ import Publisher from "./publisher.js";
3
+ import SwitchableCallback from "./switchable-callback.js";
4
+
5
+ export { CallableObject, Publisher, SmartFunction, SwitchableCallback };
@@ -0,0 +1,45 @@
1
+ import { ReferenceException } from "../exceptions/index.js";
2
+
3
+ import type { Callback } from "./types.js";
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export default class Publisher<T extends { [K in keyof T]: Callback<any[], any> } = Record<string, Callback>>
7
+ {
8
+ protected _subscribers: Map<keyof T, Callback<unknown[], unknown>[]>;
9
+
10
+ public constructor()
11
+ {
12
+ this._subscribers = new Map();
13
+ }
14
+
15
+ public subscribe<K extends keyof T>(event: K, subscriber: T[K]): () => void
16
+ {
17
+ if (!(this._subscribers.has(event))) { this._subscribers.set(event, []); }
18
+
19
+ const subscribers = this._subscribers.get(event)!;
20
+ subscribers.push(subscriber);
21
+
22
+ return () =>
23
+ {
24
+ const index = subscribers.indexOf(subscriber);
25
+ if (index < 0)
26
+ {
27
+ throw new ReferenceException("Unable to unsubscribe the required subscriber. " +
28
+ "The subscription was already unsubscribed.");
29
+ }
30
+
31
+ subscribers.splice(index, 1);
32
+ };
33
+ }
34
+
35
+ public publish<K extends keyof T>(event: K, ...args: Parameters<T[K]>): ReturnType<T[K]>[]
36
+ {
37
+ const subscribers = this._subscribers.get(event);
38
+ if (!(subscribers)) { return []; }
39
+
40
+ return subscribers.slice()
41
+ .map((subscriber) => subscriber(...args)) as ReturnType<T[K]>[];
42
+ }
43
+
44
+ public readonly [Symbol.toStringTag]: string = "Publisher";
45
+ }
@@ -0,0 +1,108 @@
1
+ import { KeyException, NotImplementedException, RuntimeException } from "../exceptions/index.js";
2
+
3
+ import CallableObject from "./callable-object.js";
4
+ import type { Callback } from "./types.js";
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ export default class SwitchableCallback<T extends Callback<any[], any> = Callback> extends CallableObject<T>
8
+ {
9
+ protected _callback: T;
10
+ protected _callbacks: Map<string, T>;
11
+
12
+ protected _isEnabled: boolean;
13
+ public get isEnabled(): boolean { return this._isEnabled; }
14
+
15
+ protected _key: string;
16
+ public get key(): string { return this._key; }
17
+
18
+ public readonly invoke: (...args: Parameters<T>) => ReturnType<T>;
19
+
20
+ public constructor()
21
+ {
22
+ const _default = () =>
23
+ {
24
+ throw new NotImplementedException(
25
+ "The `SwitchableCallback` has no callback defined yet. " +
26
+ "Did you forget to call the `register` method?"
27
+ );
28
+ };
29
+
30
+ super();
31
+
32
+ this._callback = ((_default) as unknown) as T;
33
+ this._callbacks = new Map<string, T>();
34
+
35
+ this._isEnabled = false;
36
+ this._key = "";
37
+
38
+ this.invoke = (...args: Parameters<T>): ReturnType<T> => this._callback(...args);
39
+ }
40
+
41
+ public enable(): void
42
+ {
43
+ if (!(this._key))
44
+ {
45
+ throw new KeyException(
46
+ "The `SwitchableCallback` has no callback defined yet. " +
47
+ "Did you forget to call the `register` method?"
48
+ );
49
+ }
50
+ if (this._isEnabled)
51
+ {
52
+ throw new RuntimeException("The `SwitchableCallback` is already enabled.");
53
+ }
54
+
55
+ this._callback = this._callbacks.get(this._key)!;
56
+ this._isEnabled = true;
57
+ }
58
+ public disable(): void
59
+ {
60
+ if (!(this._isEnabled))
61
+ {
62
+ throw new RuntimeException("The `SwitchableCallback` is already disabled.");
63
+ }
64
+
65
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
66
+ this._callback = (() => { }) as T;
67
+ this._isEnabled = false;
68
+ }
69
+
70
+ public register(key: string, callback: T): void
71
+ {
72
+ if (this._callbacks.size === 0)
73
+ {
74
+ this._key = key;
75
+ this._callback = callback;
76
+ }
77
+ else if (this._callbacks.has(key))
78
+ {
79
+ throw new KeyException(`The key '${key}' has already been used for another callback.`);
80
+ }
81
+
82
+ this._callbacks.set(key, callback);
83
+ }
84
+ public unregister(key: string): void
85
+ {
86
+ if (!(this._callbacks.has(key)))
87
+ {
88
+ throw new KeyException(`The key '${key}' doesn't yet have any associated callback.`);
89
+ }
90
+
91
+ this._callbacks.delete(key);
92
+ }
93
+
94
+ public switch(key: string): void
95
+ {
96
+ if (!(this._callbacks.has(key)))
97
+ {
98
+ throw new KeyException(`The key '${key}' doesn't yet have any associated callback.`);
99
+ }
100
+
101
+ this._key = key;
102
+
103
+ if (this._isEnabled)
104
+ {
105
+ this._callback = this._callbacks.get(key)!;
106
+ }
107
+ }
108
+ }
@@ -0,0 +1 @@
1
+ export type Callback<A extends unknown[] = [], R = void> = (...args: A) => R;
@@ -5,6 +5,7 @@ export {
5
5
 
6
6
  } from "./aggregators/index.js";
7
7
 
8
+ export { CallableObject, Publisher, SmartFunction, SwitchableCallback } from "./callbacks/index.js";
8
9
  export {
9
10
  Exception,
10
11
  FatalErrorException,
@@ -28,10 +29,8 @@ import GameLoop from "./game-loop.js";
28
29
 
29
30
  export { SmartIterator, SmartAsyncIterator } from "./iterators/index.js";
30
31
  export { JSONStorage } from "./json/index.js";
31
- export { DeferredPromise, SmartPromise, Thenable, TimedPromise } from "./promises/index.js";
32
-
33
- import Publisher from "./publisher.js";
32
+ export { DeferredPromise, LongRunningTask, SmartPromise, Thenable, TimedPromise } from "./promises/index.js";
34
33
 
35
34
  export { Clock, Countdown } from "./timers/index.js";
36
35
 
37
- export { GameLoop, Publisher };
36
+ export { GameLoop };
@@ -1,7 +1,5 @@
1
1
  export type JSONArray = JSONValue[];
2
2
 
3
- // @ts-expect-error - This is a circular reference to itself.
4
- export type JSONObject = Record<string, JSONValue>;
5
-
6
- // @ts-expect-error - This is a circular reference to itself.
3
+ // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
4
+ export interface JSONObject { [key: string]: JSONValue }
7
5
  export type JSONValue = boolean | number | string | null | JSONObject | JSONArray;
@@ -1,6 +1,7 @@
1
1
  import DeferredPromise from "./deferred-promise.js";
2
+ import LongRunningTask from "./long-running-task.js";
2
3
  import SmartPromise from "./smart-promise.js";
3
4
  import Thenable from "./thenable.js";
4
5
  import TimedPromise from "./timed-promise.js";
5
6
 
6
- export { DeferredPromise, SmartPromise, Thenable, TimedPromise };
7
+ export { DeferredPromise, LongRunningTask, SmartPromise, Thenable, TimedPromise };
@@ -0,0 +1,294 @@
1
+ import { yieldToEventLoop } from "../../utils/async.js";
2
+
3
+ import Publisher from "../callbacks/publisher.js";
4
+ import { RuntimeException } from "../exceptions/index.js";
5
+
6
+ import type { MaybeAsyncGeneratorFunction } from "../iterators/types.js";
7
+ import type { FulfilledHandler, PromiseExecutor, RejectedHandler } from "./types.js";
8
+
9
+ export interface LongRunningTaskOptions
10
+ {
11
+ ignoreErrors?: boolean;
12
+ stepIncrement?: number;
13
+ totalSteps?: number | null;
14
+ trackProgress?: boolean;
15
+ }
16
+
17
+ export default class LongRunningTask<T = void> implements Promise<T>
18
+ {
19
+ private static get _DefaultOptions(): Required<LongRunningTaskOptions>
20
+ {
21
+ return {
22
+ ignoreErrors: false,
23
+ stepIncrement: 1,
24
+ totalSteps: null,
25
+ trackProgress: false
26
+ };
27
+ }
28
+
29
+ protected _startTime: number;
30
+ protected _estimatedTime: number;
31
+ protected _endTime?: number;
32
+
33
+ protected _currentStep: number;
34
+ protected _percentage: number;
35
+
36
+ protected _isRunning: boolean;
37
+ protected _hasCompleted: boolean;
38
+ protected _hasFailed: boolean;
39
+
40
+ protected _promise: Promise<T>;
41
+ protected _publisher?: Publisher;
42
+
43
+ public constructor(executor: MaybeAsyncGeneratorFunction<undefined, T>, options?: LongRunningTaskOptions);
44
+ public constructor(executor: MaybeAsyncGeneratorFunction<number, T>, options?: LongRunningTaskOptions);
45
+ public constructor(executor: MaybeAsyncGeneratorFunction<number | undefined, T>, options?: LongRunningTaskOptions)
46
+ {
47
+ this._startTime = 0;
48
+ this._estimatedTime = 0;
49
+
50
+ this._currentStep = 0;
51
+ this._percentage = 0;
52
+
53
+ this._isRunning = true;
54
+ this._hasCompleted = false;
55
+ this._hasFailed = false;
56
+
57
+ const _onFulfilled = (result: T): T =>
58
+ {
59
+ this._estimatedTime = 0;
60
+ this._endTime = Date.now();
61
+
62
+ this._percentage = 100;
63
+
64
+ this._isRunning = false;
65
+ this._hasCompleted = true;
66
+
67
+ return result;
68
+ };
69
+ const _onRejected = (reason: unknown): never =>
70
+ {
71
+ this._endTime = Date.now();
72
+
73
+ this._isRunning = false;
74
+ this._hasFailed = true;
75
+
76
+ throw reason;
77
+ };
78
+
79
+ let _executor: PromiseExecutor<T>;
80
+
81
+ options = { ...LongRunningTask._DefaultOptions, ...options };
82
+ if ((options.trackProgress))
83
+ {
84
+ let _trackProgress: (steps: number | undefined) => void;
85
+
86
+ this._publisher = new Publisher();
87
+
88
+ if (options.totalSteps)
89
+ {
90
+ if (options.stepIncrement)
91
+ {
92
+ _trackProgress = (steps: number | undefined) =>
93
+ {
94
+ if (steps) { this._currentStep += steps; }
95
+ else { this._currentStep += options.stepIncrement!; }
96
+
97
+ this._percentage = (this._currentStep / options.totalSteps!) * 100;
98
+
99
+ const elapsedTime = Date.now() - this._startTime;
100
+ const remainingSteps = options.totalSteps! - this._currentStep;
101
+ this._estimatedTime = (elapsedTime / this._currentStep) * remainingSteps;
102
+ };
103
+ }
104
+ else
105
+ {
106
+ _trackProgress = (steps: number | undefined) =>
107
+ {
108
+ if (steps)
109
+ {
110
+ this._currentStep += steps;
111
+ this._percentage = (this._currentStep / options.totalSteps!) * 100;
112
+
113
+ const elapsedTime = Date.now() - this._startTime;
114
+ const remainingSteps = options.totalSteps! - this._currentStep;
115
+ this._estimatedTime = (elapsedTime / this._currentStep) * remainingSteps;
116
+ }
117
+ };
118
+ }
119
+ }
120
+ else if (options.stepIncrement)
121
+ {
122
+ _trackProgress = (steps: number | undefined) =>
123
+ {
124
+ if (steps) { this._currentStep += steps; }
125
+ else { this._currentStep += options.stepIncrement!; }
126
+ };
127
+ }
128
+ else
129
+ {
130
+ _trackProgress = (steps: number | undefined) =>
131
+ {
132
+ if (steps) { this._currentStep += steps; }
133
+ };
134
+ }
135
+
136
+ if (options.ignoreErrors)
137
+ {
138
+ _executor = async (resolve) =>
139
+ {
140
+ const generator = executor();
141
+ this._startTime = Date.now();
142
+
143
+ while (true)
144
+ {
145
+ try
146
+ {
147
+ const { done, value } = await generator.next();
148
+
149
+ if (done) { return resolve(value); }
150
+ else { _trackProgress(value); }
151
+ }
152
+
153
+ // eslint-disable-next-line no-console
154
+ catch (error) { console.error(error); }
155
+
156
+ this._publisher!.publish("progress");
157
+
158
+ await yieldToEventLoop();
159
+ }
160
+ };
161
+ }
162
+ else
163
+ {
164
+ _executor = async (resolve, reject) =>
165
+ {
166
+ try
167
+ {
168
+ const generator = executor();
169
+ this._startTime = Date.now();
170
+
171
+ while (true)
172
+ {
173
+ const { done, value } = await generator.next();
174
+ if (done) { return resolve(value); }
175
+ else { _trackProgress(value); }
176
+
177
+ this._publisher!.publish("progress");
178
+
179
+ await yieldToEventLoop();
180
+ }
181
+ }
182
+ catch (error) { reject(error); }
183
+ };
184
+ }
185
+ }
186
+ else if (options.ignoreErrors)
187
+ {
188
+ _executor = async (resolve) =>
189
+ {
190
+ const generator = executor();
191
+ this._startTime = Date.now();
192
+
193
+ while (true)
194
+ {
195
+ try
196
+ {
197
+ const { done, value } = await generator.next();
198
+ if (done) { return resolve(value); }
199
+ }
200
+
201
+ // eslint-disable-next-line no-console
202
+ catch (error) { console.error(error); }
203
+
204
+ await yieldToEventLoop();
205
+ }
206
+ };
207
+ }
208
+ else
209
+ {
210
+ _executor = async (resolve, reject) =>
211
+ {
212
+ try
213
+ {
214
+ const generator = executor();
215
+ this._startTime = Date.now();
216
+
217
+ while (true)
218
+ {
219
+ const { done, value } = await generator.next();
220
+ if (done) { return resolve(value); }
221
+
222
+ await yieldToEventLoop();
223
+ }
224
+ }
225
+ catch (error) { reject(error); }
226
+ };
227
+ }
228
+
229
+ this._promise = new Promise(_executor)
230
+ .then(_onFulfilled, _onRejected);
231
+ }
232
+
233
+ public get startTime(): number { return this._startTime; }
234
+ public get elapsedTime(): number
235
+ {
236
+ if (this._isRunning) { return Date.now() - this._startTime; }
237
+
238
+ return this._endTime! - this._startTime;
239
+ }
240
+ public get estimatedTime(): number { return this._estimatedTime; }
241
+ public get endTime(): number
242
+ {
243
+ if (this._isRunning)
244
+ {
245
+ throw new RuntimeException("The task is still running and has no end time yet.");
246
+ }
247
+
248
+ return this._endTime!;
249
+ }
250
+
251
+ public get currentStep(): number { return this._currentStep; }
252
+ public get percentage(): number { return this._percentage; }
253
+
254
+ public get isRunning(): boolean { return this._isRunning; }
255
+ public get hasCompleted(): boolean { return this._hasCompleted; }
256
+ public get hasFailed(): boolean { return this._hasFailed; }
257
+
258
+ public then(onFulfilled?: null): Promise<T>;
259
+ public then<F = T>(onFulfilled: FulfilledHandler<T, F>, onRejected?: null): Promise<F>;
260
+ public then<F = T, R = never>(onFulfilled: FulfilledHandler<T, F>, onRejected: RejectedHandler<unknown, R>)
261
+ : Promise<F | R>;
262
+ public then<F = T, R = never>(
263
+ onFulfilled?: FulfilledHandler<T, F> | null,
264
+ onRejected?: RejectedHandler<unknown, R> | null): Promise<F | R>
265
+ {
266
+ return this._promise.then(onFulfilled, onRejected);
267
+ }
268
+
269
+ public catch(onRejected?: null): Promise<T>;
270
+ public catch<R = never>(onRejected: RejectedHandler<unknown, R>): Promise<T | R>;
271
+ public catch<R = never>(onRejected?: RejectedHandler<unknown, R> | null): Promise<T | R>
272
+ {
273
+ return this._promise.catch(onRejected);
274
+ }
275
+ public finally(onFinally?: (() => void) | null): Promise<T>
276
+ {
277
+ return this._promise.finally(onFinally);
278
+ }
279
+
280
+ public onProgress(callback: () => void): () => void
281
+ {
282
+ if (!(this._publisher))
283
+ {
284
+ throw new RuntimeException(
285
+ "You cannot subscribe to progress events without enabling progress tracking. " +
286
+ "Did you forget to set the `trackProgress` option to `true` when creating the task?"
287
+ );
288
+ }
289
+
290
+ return this._publisher.subscribe("progress", callback);
291
+ }
292
+
293
+ public readonly [Symbol.toStringTag]: string = "LongRunningTask";
294
+ }
@@ -1,3 +1,5 @@
1
+ export type { LongRunningTaskOptions } from "./long-running-task.js";
2
+
1
3
  export type MaybePromise<T> = T | PromiseLike<T>;
2
4
 
3
5
  export type FulfilledHandler<T = void, R = T> = (value: T) => MaybePromise<R>;
@@ -1,21 +1,19 @@
1
1
  import { TimeUnit } from "../../utils/date.js";
2
2
  import { RangeException, RuntimeException } from "../exceptions/index.js";
3
3
 
4
+ import Publisher from "../callbacks/publisher.js";
4
5
  import GameLoop from "../game-loop.js";
5
- import Publisher from "../publisher.js";
6
6
 
7
- interface ClockEvents
7
+ interface ClockEventMap
8
8
  {
9
- /* eslint-disable @typescript-eslint/no-invalid-void-type */
10
-
11
- start: [[], void];
12
- stop: [[], void];
13
- tick: [[number], void];
9
+ start: () => void;
10
+ stop: () => void;
11
+ tick: (elapsedTime: number) => void;
14
12
  }
15
13
 
16
14
  export default class Clock extends GameLoop
17
15
  {
18
- protected _publisher: Publisher<ClockEvents>;
16
+ protected _publisher: Publisher<ClockEventMap>;
19
17
 
20
18
  public constructor(msIfNotBrowser: number = TimeUnit.Second)
21
19
  {
@@ -3,24 +3,21 @@ import { TimeUnit } from "../../utils/date.js";
3
3
  import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js";
4
4
  import { DeferredPromise, SmartPromise } from "../promises/index.js";
5
5
 
6
+ import Publisher from "../callbacks/publisher.js";
6
7
  import GameLoop from "../game-loop.js";
7
- import Publisher from "../publisher.js";
8
8
 
9
- interface CountdownEvents
9
+ interface CountdownEventMap
10
10
  {
11
- /* eslint-disable @typescript-eslint/no-invalid-void-type */
12
-
13
- start: [[], void];
14
- stop: [[unknown], void];
15
- tick: [[number], void];
16
- expire: [[], void];
11
+ start: () => void;
12
+ stop: (reason: unknown) => void;
13
+ tick: (remainingTime: number) => void;
14
+ expire: () => void;
17
15
  }
18
16
 
19
17
  export default class Countdown extends GameLoop
20
18
  {
21
19
  protected _deferrer?: DeferredPromise<void>;
22
-
23
- protected _publisher: Publisher<CountdownEvents>;
20
+ protected _publisher: Publisher<CountdownEventMap>;
24
21
 
25
22
  protected _duration: number;
26
23
  public get duration(): number