350 lines
11 KiB
TypeScript
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';
|
|
}
|
|
});
|
|
}
|
|
}
|