import * as plugins from './taskbuffer.plugins.js'; import { BufferRunner } from './taskbuffer.classes.bufferrunner.js'; import { CycleCounter } from './taskbuffer.classes.cyclecounter.js'; import { TaskStep, type ITaskStep } from './taskbuffer.classes.taskstep.js'; import type { ITaskMetadata } from './taskbuffer.interfaces.js'; import { logger } from './taskbuffer.logging.js'; export interface ITaskFunction { (x?: any, setupValue?: T): PromiseLike; } export interface ITaskSetupFunction { (): Promise; } export type TPreOrAfterTaskFunction = () => Task; // Type helper to extract step names from array export type StepNames = T extends ReadonlyArray<{ name: infer N }> ? N : never; export class Task = []> { public static extractTask = []>( preOrAfterTaskArg: Task | TPreOrAfterTaskFunction, ): Task { switch (true) { case !preOrAfterTaskArg: return null; case preOrAfterTaskArg instanceof Task: return preOrAfterTaskArg as Task; case typeof preOrAfterTaskArg === 'function': const taskFunction = preOrAfterTaskArg as TPreOrAfterTaskFunction; return taskFunction() as unknown as Task; default: return null; } } public static emptyTaskFunction: ITaskFunction = function (x) { const done = plugins.smartpromise.defer(); done.resolve(); return done.promise; }; public static isTask = (taskArg: Task): boolean => { if (taskArg instanceof Task && typeof taskArg.taskFunction === 'function') { return true; } else { return false; } }; public static isTaskTouched = []>( taskArg: Task | TPreOrAfterTaskFunction, touchedTasksArray: Task[], ): boolean { const taskToCheck = Task.extractTask(taskArg); let result = false; for (const keyArg in touchedTasksArray) { if (taskToCheck === touchedTasksArray[keyArg]) { result = true; } } return result; } public static runTask = async = []>( taskArg: Task | TPreOrAfterTaskFunction, optionsArg: { x?: any; touchedTasksArray?: Task[] }, ) => { const taskToRun = Task.extractTask(taskArg); const done = plugins.smartpromise.defer(); // Wait for all blocking tasks to finish for (const task of taskToRun.blockingTasks) { await task.finished; } if (!taskToRun.setupValue && taskToRun.taskSetup) { taskToRun.setupValue = await taskToRun.taskSetup(); } if (taskToRun.execDelay) { await plugins.smartdelay.delayFor(taskToRun.execDelay); } taskToRun.running = true; taskToRun.runCount++; taskToRun.lastRun = new Date(); // Reset steps at the beginning of task execution taskToRun.resetSteps(); done.promise.then(async () => { taskToRun.running = false; // Complete all steps when task finishes taskToRun.completeAllSteps(); // When the task has finished running, resolve the finished promise taskToRun.resolveFinished(); // Create a new finished promise for the next run taskToRun.finished = new Promise((resolve) => { taskToRun.resolveFinished = resolve; }); }); const options = { ...{ x: undefined, touchedTasksArray: [] }, ...optionsArg, }; const x = options.x; const touchedTasksArray: Task[] = options.touchedTasksArray; touchedTasksArray.push(taskToRun); const localDeferred = plugins.smartpromise.defer(); localDeferred.promise .then(() => { if ( taskToRun.preTask && !Task.isTaskTouched(taskToRun.preTask, touchedTasksArray) ) { return Task.runTask(taskToRun.preTask, { x, touchedTasksArray }); } else { const done2 = plugins.smartpromise.defer(); done2.resolve(x); return done2.promise; } }) .then(async (x) => { try { return await taskToRun.taskFunction(x, taskToRun.setupValue); } catch (e) { console.log(e); } }) .then((x) => { if ( taskToRun.afterTask && !Task.isTaskTouched(taskToRun.afterTask, touchedTasksArray) ) { return Task.runTask(taskToRun.afterTask, { x: x, touchedTasksArray: touchedTasksArray, }); } else { const done2 = plugins.smartpromise.defer(); done2.resolve(x); return done2.promise; } }) .then((x) => { done.resolve(x); }) .catch((err) => { console.log(err); }); localDeferred.resolve(); return await done.promise; }; public name: string; public version: string; public taskFunction: ITaskFunction; public buffered: boolean; public cronJob: plugins.smarttime.CronJob; public bufferMax: number; public execDelay: number; public timeout: number; public preTask: Task | TPreOrAfterTaskFunction; public afterTask: Task | TPreOrAfterTaskFunction; // Add a list to store the blocking tasks public blockingTasks: Task[] = []; // Add a promise that will resolve when the task has finished private finished: Promise; private resolveFinished: () => void; public running: boolean = false; public bufferRunner = new BufferRunner(this); public cycleCounter = new CycleCounter(this); public lastRun?: Date; public runCount: number = 0; public get idle() { return !this.running; } public taskSetup: ITaskSetupFunction; public setupValue: T; // Step tracking properties private steps = new Map(); private stepProgress = new Map(); public currentStepName?: string; private providedSteps?: TSteps; constructor(optionsArg: { taskFunction: ITaskFunction; preTask?: Task | TPreOrAfterTaskFunction; afterTask?: Task | TPreOrAfterTaskFunction; buffered?: boolean; bufferMax?: number; execDelay?: number; name?: string; taskSetup?: ITaskSetupFunction; steps?: TSteps; }) { this.taskFunction = optionsArg.taskFunction; this.preTask = optionsArg.preTask; this.afterTask = optionsArg.afterTask; this.buffered = optionsArg.buffered; this.bufferMax = optionsArg.bufferMax; this.execDelay = optionsArg.execDelay; this.name = optionsArg.name; this.taskSetup = optionsArg.taskSetup; // Initialize steps if provided if (optionsArg.steps) { this.providedSteps = optionsArg.steps; for (const stepConfig of optionsArg.steps) { const step = new TaskStep({ name: stepConfig.name, description: stepConfig.description, percentage: stepConfig.percentage, }); this.steps.set(stepConfig.name, step); } } // Create the finished promise this.finished = new Promise((resolve) => { this.resolveFinished = resolve; }); } public trigger(x?: any): Promise { if (this.buffered) { return this.triggerBuffered(x); } else { return this.triggerUnBuffered(x); } } public triggerUnBuffered(x?: any): Promise { return Task.runTask(this, { x: x }); } public triggerBuffered(x?: any): Promise { return this.bufferRunner.trigger(x); } // Step notification method with typed step names public notifyStep(stepName: StepNames): void { // Complete previous step if exists if (this.currentStepName) { const prevStep = this.steps.get(this.currentStepName); if (prevStep && prevStep.status === 'active') { prevStep.complete(); this.stepProgress.set(this.currentStepName, prevStep.percentage); } } // Start new step const step = this.steps.get(stepName as string); if (step) { step.start(); this.currentStepName = stepName as string; // Emit event for frontend updates (could be enhanced with event emitter) if (this.name) { logger.log('info', `Task ${this.name}: Starting step "${stepName}" - ${step.description}`); } } } // Get current progress based on completed steps public getProgress(): number { let totalProgress = 0; for (const [stepName, percentage] of this.stepProgress) { totalProgress += percentage; } // Add partial progress of current step if exists if (this.currentStepName) { const currentStep = this.steps.get(this.currentStepName); if (currentStep && currentStep.status === 'active') { // Could add partial progress calculation here if needed // For now, we'll consider active steps as 50% complete totalProgress += currentStep.percentage * 0.5; } } return Math.min(100, Math.round(totalProgress)); } // Get all steps metadata public getStepsMetadata(): ITaskStep[] { return Array.from(this.steps.values()).map(step => step.toJSON()); } // Get task metadata public getMetadata(): ITaskMetadata { return { name: this.name || 'unnamed', version: this.version, status: this.running ? 'running' : 'idle', steps: this.getStepsMetadata(), currentStep: this.currentStepName, currentProgress: this.getProgress(), runCount: this.runCount, buffered: this.buffered, bufferMax: this.bufferMax, timeout: this.timeout, cronSchedule: this.cronJob?.cronExpression, }; } // Reset all steps to pending state public resetSteps(): void { this.steps.forEach(step => step.reset()); this.stepProgress.clear(); this.currentStepName = undefined; } // Complete all remaining steps (useful for cleanup) private completeAllSteps(): void { if (this.currentStepName) { const currentStep = this.steps.get(this.currentStepName); if (currentStep && currentStep.status === 'active') { currentStep.complete(); this.stepProgress.set(this.currentStepName, currentStep.percentage); } } // Mark any pending steps as completed (in case of early task completion) this.steps.forEach((step, name) => { if (step.status === 'pending') { // Don't add their percentage to progress since they weren't actually executed step.status = 'completed'; } }); } }