feat(core): Add step-based progress tracking, task metadata and enhanced TaskManager scheduling/metadata APIs
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
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';
|
||||
|
||||
@@ -14,18 +16,21 @@ export interface ITaskSetupFunction<T = undefined> {
|
||||
|
||||
export type TPreOrAfterTaskFunction = () => Task<any>;
|
||||
|
||||
export class Task<T = undefined> {
|
||||
public static extractTask<T = undefined>(
|
||||
preOrAfterTaskArg: Task<T> | TPreOrAfterTaskFunction,
|
||||
): Task<T> {
|
||||
// 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>;
|
||||
return preOrAfterTaskArg as Task<T, TSteps>;
|
||||
case typeof preOrAfterTaskArg === 'function':
|
||||
const taskFunction = preOrAfterTaskArg as TPreOrAfterTaskFunction;
|
||||
return taskFunction();
|
||||
return taskFunction() as unknown as Task<T, TSteps>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -45,9 +50,9 @@ export class Task<T = undefined> {
|
||||
}
|
||||
};
|
||||
|
||||
public static isTaskTouched<T = undefined>(
|
||||
taskArg: Task<T> | TPreOrAfterTaskFunction,
|
||||
touchedTasksArray: Task<T>[],
|
||||
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;
|
||||
@@ -59,9 +64,9 @@ export class Task<T = undefined> {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static runTask = async <T>(
|
||||
taskArg: Task<T> | TPreOrAfterTaskFunction,
|
||||
optionsArg: { x?: any; touchedTasksArray?: Task<T>[] },
|
||||
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();
|
||||
@@ -80,9 +85,17 @@ export class Task<T = undefined> {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -98,7 +111,7 @@ export class Task<T = undefined> {
|
||||
...optionsArg,
|
||||
};
|
||||
const x = options.x;
|
||||
const touchedTasksArray: Task<T>[] = options.touchedTasksArray;
|
||||
const touchedTasksArray: Task<T, TSteps>[] = options.touchedTasksArray;
|
||||
|
||||
touchedTasksArray.push(taskToRun);
|
||||
|
||||
@@ -158,8 +171,8 @@ export class Task<T = undefined> {
|
||||
public execDelay: number;
|
||||
public timeout: number;
|
||||
|
||||
public preTask: Task<T> | TPreOrAfterTaskFunction;
|
||||
public afterTask: Task<T> | TPreOrAfterTaskFunction;
|
||||
public preTask: Task<T, any> | TPreOrAfterTaskFunction;
|
||||
public afterTask: Task<T, any> | TPreOrAfterTaskFunction;
|
||||
|
||||
// Add a list to store the blocking tasks
|
||||
public blockingTasks: Task[] = [];
|
||||
@@ -171,6 +184,8 @@ export class Task<T = undefined> {
|
||||
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;
|
||||
@@ -179,15 +194,22 @@ export class Task<T = undefined> {
|
||||
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> | TPreOrAfterTaskFunction;
|
||||
afterTask?: Task<T> | TPreOrAfterTaskFunction;
|
||||
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;
|
||||
@@ -198,6 +220,19 @@ export class Task<T = undefined> {
|
||||
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;
|
||||
@@ -213,10 +248,102 @@ export class Task<T = undefined> {
|
||||
}
|
||||
|
||||
public triggerUnBuffered(x?: any): Promise<any> {
|
||||
return Task.runTask<T>(this, { x: x });
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user