S12 - Async feedback for scheduling
All checks were successful
Build, test and publish / verify (push) Successful in 7m25s
All checks were successful
Build, test and publish / verify (push) Successful in 7m25s
This commit is contained in:
107
agiledancers-fe/src/app/core/scheduling-status.ts
Normal file
107
agiledancers-fe/src/app/core/scheduling-status.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
||||
|
||||
export interface SchedulingRun {
|
||||
runId: string;
|
||||
schoolId: number;
|
||||
showId: number;
|
||||
showName: string;
|
||||
startedAt: string;
|
||||
finishedAt: string | null;
|
||||
state: string;
|
||||
progress: { phase: string; feasible: boolean };
|
||||
message: string;
|
||||
summary: {
|
||||
attendanceExcess: number;
|
||||
performanceSpread: number;
|
||||
varietyPenalty: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SchedulingStatus {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
readonly runs = signal<SchedulingRun[]>([]);
|
||||
readonly connected = signal(false);
|
||||
readonly notifications = signal<SchedulingRun[]>([]);
|
||||
readonly now = signal(Date.now());
|
||||
private source?: EventSource;
|
||||
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
||||
private initialized = false;
|
||||
private readonly seen = new Set<string>();
|
||||
|
||||
connect(): void {
|
||||
if (this.source) return;
|
||||
this.open();
|
||||
const timer = setInterval(() => this.now.set(Date.now()), 1000);
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.source?.close();
|
||||
clearTimeout(this.reconnectTimer);
|
||||
clearInterval(timer);
|
||||
});
|
||||
}
|
||||
|
||||
private open(): void {
|
||||
const source = new EventSource('/api/scheduling/events');
|
||||
this.source = source;
|
||||
source.onmessage = (event: MessageEvent<string>) => {
|
||||
const eventData = JSON.parse(event.data) as { runs: SchedulingRun[] | null };
|
||||
if (eventData.runs !== null) this.accept(eventData.runs);
|
||||
this.connected.set(true);
|
||||
};
|
||||
source.onerror = () => {
|
||||
this.connected.set(false);
|
||||
// Retry also when a proxy error makes native EventSource reconnection stop permanently.
|
||||
source.close();
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = setTimeout(() => this.open(), 3000);
|
||||
};
|
||||
}
|
||||
|
||||
private accept(runs: SchedulingRun[]): void {
|
||||
for (const run of runs) {
|
||||
if (run.state === 'RUNNING' || this.seen.has(run.runId)) continue;
|
||||
this.seen.add(run.runId);
|
||||
if (this.initialized) this.notifications.update((items) => [...items, run]);
|
||||
}
|
||||
this.initialized = true;
|
||||
this.runs.set(runs);
|
||||
}
|
||||
|
||||
run(schoolId: number, showId: number): SchedulingRun | undefined {
|
||||
return this.runs().find((run) => run.schoolId === schoolId && run.showId === showId);
|
||||
}
|
||||
|
||||
started(run: SchedulingRun): void {
|
||||
// The SSE completion can arrive before the POST response. Never overwrite it with RUNNING.
|
||||
if (this.runs().some((item) => item.runId === run.runId)) return;
|
||||
this.runs.update((items) => [...items.filter((item) => item.showId !== run.showId), run]);
|
||||
}
|
||||
|
||||
elapsed(run: SchedulingRun): string {
|
||||
const seconds = Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
((run.finishedAt ? Date.parse(run.finishedAt) : this.now()) - Date.parse(run.startedAt)) /
|
||||
1000,
|
||||
),
|
||||
);
|
||||
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
||||
}
|
||||
|
||||
phase(run: SchedulingRun): string {
|
||||
return (
|
||||
(
|
||||
{
|
||||
PREPARING: 'Voorbereiden',
|
||||
ATTENDANCE: 'Aanwezigheid',
|
||||
SPREAD: 'Spreiding',
|
||||
VARIETY: 'Variatie',
|
||||
} as Record<string, string>
|
||||
)[run.progress.phase] ?? run.progress.phase
|
||||
);
|
||||
}
|
||||
|
||||
dismiss(): void {
|
||||
this.notifications.update((items) => items.slice(1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user