test: Added download test and made app more testable in general

fix: Store Downloads not properly working on steam deck
fix: Removed linux shortcuts implementation
This commit is contained in:
Simeon Radivoev 2026-03-31 03:11:02 +03:00
parent 58d3c31c56
commit 8a0be8c913
Signed by: simeonradivoev
GPG key ID: C16C2132A7660C8E
26 changed files with 422 additions and 210 deletions

View file

@ -5,39 +5,41 @@ import z from 'zod';
export class TaskQueue
{
private activeQueue: { context: JobContext<IJob<any, string>, any, string>, promise?: Promise<void>; }[] = [];
private queue?: { context: JobContext<IJob<any, string>, any, string>, promise?: Promise<void>; }[] = [];
private activeQueue: JobContext<IJob<any, string>, any, string>[] = [];
private queue?: JobContext<IJob<any, string>, any, string>[] = [];
private events?: EventEmitter<EventsList> = new EventEmitter<EventsList>();
public enqueue<TData, TState extends string, T extends IJob<TData, TState>> (id: string, job: T)
public enqueue<T> (id: string, job: T): T extends IJob<infer TData, infer TState extends string>
? Promise<TData>
: never
{
this.disposeSafeguard();
if (!this.queue || !this.events) throw new Error("Queue disposed");
const context = new JobContext(id, this.events, job);
this.queue.push({ context });
const context = new JobContext<any, any, any>(id, this.events, job);
this.queue.push(context as any);
this.events?.emit('queued', { id: context.id, job: context });
return this.processQueue();
this.processQueue();
return context.promise.promise as any;
}
private processQueue ()
{
if (!this.queue) return Promise.resolve();
const next = this.queue.filter(j => !j.context.job.group || !this.activeQueue.some(a => a.context.job.group === j.context.job.group)).map((job, i) => ({ i, job }));
const next = this.queue.filter(j => !j.job.group || !this.activeQueue.some(a => a.job.group === j.job.group)).map((job, i) => ({ i, job }));
next.reverse().forEach(({ i }) => this.queue!.splice(i, 1));
next.forEach(job =>
{
const promise = job.job.context.start();
job.job.promise = promise;
job.job.start();
this.activeQueue.push(job.job);
promise.finally(() =>
job.job.promise.promise.finally(() =>
{
const index = this.activeQueue.indexOf(job.job);
this.activeQueue.splice(index, 1);
// We need to call it after it has been removed from the queue, so that the has active of type doesn't return true
this.events?.emit('ended', { id: job.job.context.id, job: job.job.context });
this.events?.emit('ended', { id: job.job.id, job: job.job });
setTimeout(() => this.processQueue(), 0);
});
});
@ -57,7 +59,7 @@ export class TaskQueue
{
for (const entry of this.activeQueue)
{
if (entry.context.job instanceof type)
if (entry.job instanceof type)
{
return true;
}
@ -67,19 +69,25 @@ export class TaskQueue
public waitForJob (id: string): Promise<void>
{
const job = this.queue?.find(j => j.context.id === id) ?? this.activeQueue?.find(j => j.context.id === id);
return job?.promise ?? Promise.resolve();
const job = this.queue?.find(j => j.id === id) ?? this.activeQueue?.find(j => j.id === id);
return job?.promise.promise ?? Promise.resolve();
}
public findJob<const TData, const TState extends string, const T extends IJob<TData, TState>> (id: string, type: new (...args: any[]) => T): IPublicJob<TData, TState, T> | undefined
public findJob<T> (
id: string,
type: new (...args: any[]) => T
): T extends IJob<infer TData, infer TState extends string>
? IPublicJob<TData, TState, T> | undefined
: undefined
{
const job = this.queue?.find(j => j.context.id === id) ?? this.activeQueue?.find(j => j.context.id === id);
if (job?.context.job instanceof type)
const job = this.queue?.find(j => j.id === id)
?? this.activeQueue?.find(j => j.id === id);
if (job?.job instanceof type)
{
return job?.context;
return job as any;
}
return undefined;
return undefined as any;
}
public on<E extends keyof EventsList> (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never): () => void
@ -96,7 +104,7 @@ export class TaskQueue
public async close ()
{
this.queue = [];
this.activeQueue.forEach(c => c.context.abort());
this.activeQueue.forEach(c => c.abort());
return Promise.all(this.activeQueue.map(c => c.promise));
}
}
@ -181,6 +189,7 @@ export class JobContext<T extends IJob<TData, TState>, TData, TState extends str
private error?: any;
private events: EventEmitter<EventsList>;
private abortController: AbortController;
private m_promise: PromiseWithResolvers<TData | undefined>;
private readonly m_job: T;
constructor(id: string, events: EventEmitter<EventsList>, job: T)
@ -194,9 +203,10 @@ export class JobContext<T extends IJob<TData, TState>, TData, TState extends str
this.events.emit('abort', { id: this.m_id, reason: this.abortController.signal.reason, job: this } satisfies AbortEvent);
});
this.events = events;
this.m_promise = Promise.withResolvers();
}
public async start (): Promise<void>
public async start ()
{
try
{
@ -204,6 +214,7 @@ export class JobContext<T extends IJob<TData, TState>, TData, TState extends str
await this.m_job.start(this);
this.completed = true;
this.events.emit('completed', { id: this.m_id, job: this });
this.m_promise.resolve(this.m_job.exposeData?.());
} catch (error)
{
@ -214,6 +225,7 @@ export class JobContext<T extends IJob<TData, TState>, TData, TState extends str
this.events.emit('error', { id: this.m_id, job: this, error });
this.error = error;
this.m_promise.reject(error);
} finally
{
this.running = false;
@ -233,6 +245,8 @@ export class JobContext<T extends IJob<TData, TState>, TData, TState extends str
public get job () { return this.m_job; }
public get promise () { return this.m_promise; }
public get abortSignal () { return this.abortController.signal; }
public get progress () { return this.m_progress; }