@simeonradivoev/gameflow-sdk 1.5.1 → 1.5.2

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/task-queue.ts ADDED
@@ -0,0 +1,307 @@
1
+
2
+ import EventEmitter from 'node:events';
3
+ import z from 'zod';
4
+ import { JobStatus } from './shared';
5
+
6
+ export class TaskQueue
7
+ {
8
+ private activeQueue: JobContext<IJob<any, string>, any, string>[] = [];
9
+ private queue?: JobContext<IJob<any, string>, any, string>[] = [];
10
+ private events?: EventEmitter<EventsList> = new EventEmitter<EventsList>();
11
+
12
+ constructor()
13
+ {
14
+ // we need a default error listener or app crashes
15
+ this.events?.addListener('error', e =>
16
+ {
17
+ console.error(e);
18
+ });
19
+ }
20
+
21
+ public enqueue<T> (id: string, job: T, throwOnError?: boolean): T extends IJob<infer TData, infer TState extends string>
22
+ ? Promise<TData>
23
+ : never
24
+ {
25
+ this.disposeSafeguard();
26
+ if (!this.queue || !this.events) throw new Error("Queue disposed");
27
+ const context = new JobContext<any, any, any>(id, this.events, job);
28
+ this.queue.push(context as any);
29
+ this.events?.emit('queued', { id: context.id, job: context });
30
+ this.processQueue();
31
+ return context.promise.promise as any;
32
+ }
33
+
34
+ private processQueue ()
35
+ {
36
+ if (!this.queue) return Promise.resolve();
37
+
38
+ const next = this.queue.filter(j => !j.job.group || !this.activeQueue.some(a => a.job.group === j.job.group)).map((job, i) => ({ i, job }));
39
+
40
+ next.reverse().forEach(({ i }) => this.queue!.splice(i, 1));
41
+
42
+ next.forEach(job =>
43
+ {
44
+ job.job.start();
45
+ this.activeQueue.push(job.job);
46
+ job.job.promise.promise.catch(e => { }).finally(() =>
47
+ {
48
+ const index = this.activeQueue.indexOf(job.job);
49
+ this.activeQueue.splice(index, 1);
50
+ // We need to call it after it has been removed from the queue, so that the has active of type doesn't return true
51
+ this.events?.emit('ended', { id: job.job.id, job: job.job });
52
+ setTimeout(() => this.processQueue(), 0);
53
+ });
54
+ });
55
+ }
56
+
57
+ private disposeSafeguard ()
58
+ {
59
+ if (!this.queue) throw new Error("Queue disposed");
60
+ }
61
+
62
+ public hasActive ()
63
+ {
64
+ return this.activeQueue.length > 0;
65
+ }
66
+
67
+ public hasActiveOfType (type: any)
68
+ {
69
+ for (const entry of this.activeQueue)
70
+ {
71
+ if (entry.job instanceof type)
72
+ {
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+
79
+ public waitForJob (id: string): Promise<void>
80
+ {
81
+ const job = this.queue?.find(j => j.id === id) ?? this.activeQueue?.find(j => j.id === id);
82
+ return job?.promise.promise ?? Promise.resolve();
83
+ }
84
+
85
+ public findJob<T> (
86
+ id: string,
87
+ type: new (...args: any[]) => T
88
+ ): T extends IJob<infer TData, infer TState extends string>
89
+ ? IPublicJob<TData, TState, T> | undefined
90
+ : undefined
91
+ {
92
+ const job = this.queue?.find(j => j.id === id)
93
+ ?? this.activeQueue?.find(j => j.id === id);
94
+
95
+ if (job?.job instanceof type)
96
+ {
97
+ return job as any;
98
+ }
99
+ return undefined as any;
100
+ }
101
+
102
+ public on<E extends keyof EventsList> (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never): () => void
103
+ {
104
+ this.events?.on(event, listener);
105
+ return () => this.events?.removeListener(event, listener);
106
+ }
107
+
108
+ public once<E extends keyof EventsList> (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never)
109
+ {
110
+ this.events?.once(event, listener);
111
+ }
112
+
113
+ public async close ()
114
+ {
115
+ this.queue = [];
116
+ this.activeQueue.forEach(c => c.abort());
117
+ return Promise.all(this.activeQueue.map(c =>
118
+ {
119
+ return new Promise(resolve =>
120
+ {
121
+ c.promise.promise.then(resolve).catch(e =>
122
+ {
123
+ console.error("Error During Task Queue Closing");
124
+ resolve(false);
125
+ });
126
+ setTimeout(resolve, 5000);
127
+ });
128
+ }));
129
+ }
130
+ }
131
+
132
+ export interface EventsList
133
+ {
134
+ started: [e: BaseEvent];
135
+ progress: [e: ProgressEvent];
136
+ abort: [e: AbortEvent];
137
+ /** Called when the job successfully completes */
138
+ completed: [e: CompletedEvent];
139
+ error: [e: ErrorEvent];
140
+ ended: [e: BaseEvent];
141
+ queued: [e: BaseEvent];
142
+ }
143
+
144
+ export interface BaseEvent
145
+ {
146
+ id: string;
147
+ job: IPublicJob<any, string, any>;
148
+ }
149
+
150
+ export interface ErrorEvent extends BaseEvent
151
+ {
152
+ error: unknown;
153
+ }
154
+
155
+ export interface AbortEvent extends BaseEvent
156
+ {
157
+ reason?: any;
158
+ }
159
+
160
+ export interface ProgressEvent extends BaseEvent
161
+ {
162
+ progress: number;
163
+ state?: string;
164
+ }
165
+
166
+ export interface CompletedEvent extends BaseEvent
167
+ {
168
+
169
+ }
170
+
171
+ export interface IJob<TData, TState extends string>
172
+ {
173
+ group?: string;
174
+ start (context: JobContext<IJob<TData, TState>, TData, TState>): Promise<any>;
175
+ exposeData?(): TData;
176
+ }
177
+
178
+ export interface IPublicJob<TData, TState extends string, T extends IJob<TData, TState>>
179
+ {
180
+ progress: number;
181
+ state?: string;
182
+ status: JobStatus;
183
+ job: T;
184
+ abort: (reason?: any) => void;
185
+ }
186
+
187
+ type JobClass = new (...args: any[]) => IJob<any, any>;
188
+ type JobClassWithStatics = JobClass & {
189
+ id: string;
190
+ dataSchema?: any;
191
+ };
192
+ export type JobContextFromClass<C extends JobClassWithStatics> =
193
+ JobContext<
194
+ InstanceType<C>,
195
+ C extends { dataSchema: z.ZodAny; }
196
+ ? z.infer<C['dataSchema']>
197
+ : never,
198
+ C['id']
199
+ >;
200
+
201
+ export class JobContext<T extends IJob<TData, TState>, TData, TState extends string> implements IPublicJob<TData, TState, T>
202
+ {
203
+ private m_id: string;
204
+ private m_progress: number = 0;
205
+ private m_state?: TState;
206
+ private running: boolean = false;
207
+ private aborted: boolean = false;
208
+ private completed: boolean = false;
209
+ private error?: any;
210
+ private events: EventEmitter<EventsList>;
211
+ private abortController: AbortController;
212
+ private m_promise: PromiseWithResolvers<TData | undefined>;
213
+ private readonly m_job: T;
214
+
215
+ constructor(id: string, events: EventEmitter<EventsList>, job: T)
216
+ {
217
+ this.m_id = id;
218
+ this.m_job = job;
219
+ this.abortController = new AbortController();
220
+ this.abortController.signal.addEventListener('abort', () =>
221
+ {
222
+ this.aborted = true;
223
+ this.events.emit('abort', { id: this.m_id, reason: this.abortController.signal.reason, job: this } satisfies AbortEvent);
224
+ });
225
+ this.events = events;
226
+ this.m_promise = Promise.withResolvers();
227
+ }
228
+
229
+ public async start ()
230
+ {
231
+ try
232
+ {
233
+ this.events.emit('started', { id: this.m_id, job: this });
234
+ await this.m_job.start(this);
235
+ if (!this.abortSignal.aborted)
236
+ {
237
+ this.completed = true;
238
+ this.events.emit('completed', { id: this.m_id, job: this });
239
+ this.m_promise.resolve(this.m_job.exposeData?.());
240
+ } else
241
+ {
242
+ this.m_promise.resolve(undefined);
243
+ }
244
+ } catch (error)
245
+ {
246
+ if (error instanceof Event)
247
+ {
248
+ if (error.target instanceof AbortSignal)
249
+ {
250
+ this.m_promise.resolve(undefined);
251
+ } else
252
+ {
253
+ console.error(error);
254
+ this.m_promise.reject(error);
255
+ }
256
+ } else
257
+ {
258
+ this.events.emit('error', { id: this.m_id, job: this, error });
259
+ this.error = error;
260
+ this.m_promise.reject(error);
261
+ }
262
+
263
+ } finally
264
+ {
265
+ this.running = false;
266
+ }
267
+ }
268
+
269
+ public get status (): JobStatus
270
+ {
271
+ if (this.completed) return 'completed';
272
+ if (this.error) return 'error';
273
+ if (this.aborted) return 'aborted';
274
+ if (this.running) return 'running';
275
+ return 'queued';
276
+ }
277
+
278
+ public get id () { return this.m_id; }
279
+
280
+ public get job () { return this.m_job; }
281
+
282
+ public get promise () { return this.m_promise; }
283
+
284
+ public get abortSignal () { return this.abortController.signal; }
285
+
286
+ public get progress () { return this.m_progress; }
287
+
288
+ public get state () { return this.m_state; }
289
+
290
+ /**
291
+ * @param progress The 0 to 100 progress
292
+ * @param state what type of progress is this. Is it really progress. I humanity even advancing.
293
+ */
294
+ public setProgress (progress: number, state?: TState)
295
+ {
296
+ this.m_progress = progress;
297
+ if (state)
298
+ this.m_state = state;
299
+ this.events.emit('progress', { id: this.m_id, progress, state: state ?? this.m_state, job: this });
300
+ }
301
+
302
+ public abort (reason?: any)
303
+ {
304
+ this.error = reason;
305
+ this.abortController.abort(reason);
306
+ }
307
+ }