import { Subscription, timer } from 'rxjs'; import { DyNTS_SingletonServiceBase } from '../../_services/base/singleton.service-base'; import { DyNTS_ScheduledJob, DyNTS_ScheduledJobRun, } from './_models/scheduler-job.interface'; /** Belso reprezentacio egy regisztralt job-rol + aktiv timer-rol. */ interface DyNTS_RegisteredJob { job: DyNTS_ScheduledJob; subscription?: Subscription; running: boolean; } /** * Altalanos, idozitett-job utemzo a DyNTS szerverekhez. * * - **Regisztralhato**: `registerJob({ key, intervalMs, handler })`. * - **Idempotens / skip-if-running**: egy job-bol egyszerre csak egy futas megy * (a lassu futas nem torlodik fel). * - **Restart-biztos**: a scheduler NEM perzisztal futasi allapotot — a helyesseget * a handler igazsagforras-timestampjei adjak (lasd DyNTS_ScheduledJob doc). * - **Naplo**: az utolso N futas in-memory ring bufferben (`getRuns()`), hiba eseten * rich hibauzenettel (sosem `[object Object]`). * * Hasznalat (szerver indulasakor): * const scheduler = DyNTS_Scheduler_Service.getInstance(); * scheduler.registerJob({ key: 'sp-auto-accept', intervalMs: 24 * 60 * 60 * 1000, handler }); * scheduler.start(); */ export class DyNTS_Scheduler_Service extends DyNTS_SingletonServiceBase { static getInstance(): DyNTS_Scheduler_Service { return DyNTS_Scheduler_Service.getSingletonInstance() as DyNTS_Scheduler_Service; } private readonly jobs: Map = new Map(); private readonly runs: DyNTS_ScheduledJobRun[] = []; private readonly maxRuns: number = 500; private started: boolean = false; /** * Job regisztralasa. Ha a scheduler mar el (`start()` lefutott), a job timer-e * azonnal elindul. Duplikalt kulcs / hibas intervalMs eseten hibat dob. */ registerJob(job: DyNTS_ScheduledJob): void { if (!job || !job.key) { throw new Error('DyNTS_Scheduler_Service.registerJob: job.key kotelezo'); } if (this.jobs.has(job.key)) { throw new Error(`DyNTS_Scheduler_Service.registerJob: a(z) "${job.key}" job mar regisztralva van`); } if (typeof job.intervalMs !== 'number' || job.intervalMs <= 0) { throw new Error(`DyNTS_Scheduler_Service.registerJob: a(z) "${job.key}" job-hoz pozitiv intervalMs kell`); } const registered: DyNTS_RegisteredJob = { job: job, running: false }; this.jobs.set(job.key, registered); if (this.started) { this.startJob(registered); } } /** Job leiratkoztatasa + eltavolitasa. Ismeretlen kulcs eseten no-op. */ unregisterJob(key: string): void { const registered: DyNTS_RegisteredJob | undefined = this.jobs.get(key); if (!registered) { return; } registered.subscription?.unsubscribe(); this.jobs.delete(key); } /** Minden regisztralt job timer-enek inditasa. Idempotens. */ start(): void { if (this.started) { return; } this.started = true; this.jobs.forEach((registered: DyNTS_RegisteredJob) => this.startJob(registered)); } /** Minden job timer-enek leallitasa (a regisztraciok megmaradnak). */ stop(): void { this.jobs.forEach((registered: DyNTS_RegisteredJob) => { registered.subscription?.unsubscribe(); registered.subscription = undefined; }); this.started = false; } /** Egy job azonnali, kezi futtatasa (a skip-if-running lock-ot tiszteletben tartva). */ async runNow(key: string): Promise { const registered: DyNTS_RegisteredJob | undefined = this.jobs.get(key); if (!registered) { throw new Error(`DyNTS_Scheduler_Service.runNow: a(z) "${key}" job nincs regisztralva`); } await this.executeJob(registered); } /** El van-e inditva a scheduler. */ isStarted(): boolean { return this.started; } /** Eppen fut-e az adott job. */ isRunning(key: string): boolean { return this.jobs.get(key)?.running ?? false; } /** A regisztralt job-kulcsok. */ getJobKeys(): string[] { return Array.from(this.jobs.keys()); } /** Az utolso N futas, legujabb elol. */ getRuns(limit: number = 100): DyNTS_ScheduledJobRun[] { const safeLimit: number = Math.max(0, Math.min(limit, this.runs.length)); return this.runs.slice(this.runs.length - safeLimit).reverse(); } /** Teszt-segedlet: minden timer leall + minden regisztracio/naplo torlodik. */ _resetForTesting(): void { this.stop(); this.jobs.clear(); this.runs.length = 0; } private startJob(registered: DyNTS_RegisteredJob): void { if (registered.subscription) { return; } const initialDelay: number = registered.job.initialDelayMs ?? registered.job.intervalMs; registered.subscription = timer(initialDelay, registered.job.intervalMs) .subscribe(() => { void this.executeJob(registered); }); } private async executeJob(registered: DyNTS_RegisteredJob): Promise { if (registered.running) { return; } registered.running = true; const run: DyNTS_ScheduledJobRun = { jobKey: registered.job.key, startedAt: new Date().toISOString(), status: 'running', }; this.pushRun(run); try { await registered.job.handler(); run.status = 'success'; } catch (error: unknown) { run.status = 'failed'; run.error = this.toErrorMessage(error); this.logError(registered.job.key, error); } finally { run.finishedAt = new Date().toISOString(); registered.running = false; } } private pushRun(run: DyNTS_ScheduledJobRun): void { this.runs.push(run); if (this.runs.length > this.maxRuns) { this.runs.shift(); } } private toErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } try { return JSON.stringify(error); } catch { return String(error); } } private logError(jobKey: string, error: unknown): void { // A console.error-t a DyNTS_Logs_Service (ha telepitve van) automatikusan bufferli. // TODO (kovetkezo iteracio): bekotni a DyNTS_GlobalService.globalErrorHandler-be. // eslint-disable-next-line no-console console.error(` ! Scheduler job "${jobKey}" failed:`, error); } }