@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/README.md +15 -0
- package/build.ts +27 -0
- package/hooks/app.ts +12 -0
- package/hooks/auth.ts +9 -0
- package/hooks/emulators.ts +39 -0
- package/hooks/games.ts +174 -0
- package/hooks/store.ts +10 -0
- package/index.ts +91 -0
- package/package.json +49 -52
- package/sdk.tsconfig.json +22 -0
- package/shared.ts +623 -0
- package/task-queue.ts +307 -0
- package/index.d.ts +0 -1219
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
|
+
}
|