Files
taskbuffer/ts/taskbuffer.classes.task.ts

350 lines
11 KiB
TypeScript

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<T = undefined> {
(x?: any, setupValue?: T): PromiseLike<any>;
}
export interface ITaskSetupFunction<T = undefined> {
(): Promise<T>;
}
export type TPreOrAfterTaskFunction = () => Task<any>;
// Type helper to extract step names from array
export type StepNames<T> = T extends ReadonlyArray<{ name: infer N }> ? N : never;
export class Task<T = undefined, TSteps extends ReadonlyArray<{ name: string; description: string; percentage: number }> = []> {
public static extractTask<T = undefined, TSteps extends ReadonlyArray<{ name: string; description: string; percentage: number }> = []>(
preOrAfterTaskArg: Task<T, TSteps> | TPreOrAfterTaskFunction,
): Task<T, TSteps> {
switch (true) {
case !preOrAfterTaskArg:
return null;
case preOrAfterTaskArg instanceof Task:
return preOrAfterTaskArg as Task<T, TSteps>;
case typeof preOrAfterTaskArg === 'function':
const taskFunction = preOrAfterTaskArg as TPreOrAfterTaskFunction;
return taskFunction() as unknown as Task<T, TSteps>;
default:
return null;
}
}
public static emptyTaskFunction: ITaskFunction = function (x) {
const done = plugins.smartpromise.defer();
done.resolve();
return done.promise;
};
public static isTask = (taskArg: Task<any>): boolean => {
if (taskArg instanceof Task && typeof taskArg.taskFunction === 'function') {
return true;
} else {
return false;
}
};
public static isTaskTouched<T = undefined, TSteps extends ReadonlyArray<{ name: string; description: string; percentage: number }> = []>(
taskArg: Task<T, TSteps> | TPreOrAfterTaskFunction,
touchedTasksArray: Task<T, TSteps>[],
): 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 <T, TSteps extends ReadonlyArray<{ name: string; description: string; percentage: number }> = []>(
taskArg: Task<T, TSteps> | TPreOrAfterTaskFunction,
optionsArg: { x?: any; touchedTasksArray?: Task<T, TSteps>[] },
) => {
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<T, TSteps>[] = 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<T>;
public buffered: boolean;
public cronJob: plugins.smarttime.CronJob;
public bufferMax: number;
public execDelay: number;
public timeout: number;
public preTask: Task<T, any> | TPreOrAfterTaskFunction;
public afterTask: Task<T, any> | 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<void>;
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<T>;
public setupValue: T;
// Step tracking properties
private steps = new Map<string, TaskStep>();
private stepProgress = new Map<string, number>();
public currentStepName?: string;
private providedSteps?: TSteps;
constructor(optionsArg: {
taskFunction: ITaskFunction<T>;
preTask?: Task<T, any> | TPreOrAfterTaskFunction;
afterTask?: Task<T, any> | TPreOrAfterTaskFunction;
buffered?: boolean;
bufferMax?: number;
execDelay?: number;
name?: string;
taskSetup?: ITaskSetupFunction<T>;
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<any> {
if (this.buffered) {
return this.triggerBuffered(x);
} else {
return this.triggerUnBuffered(x);
}
}
public triggerUnBuffered(x?: any): Promise<any> {
return Task.runTask<T, TSteps>(this, { x: x });
}
public triggerBuffered(x?: any): Promise<any> {
return this.bufferRunner.trigger(x);
}
// Step notification method with typed step names
public notifyStep(stepName: StepNames<TSteps>): 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';
}
});
}
}