Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 905ca97b6a | |||
| 72f75aa7de | |||
| 2fb605a58e | |||
| ed1e9a08b2 | |||
| 4d23b3dbfe | |||
| 9784a5eacf |
Binary file not shown.
@@ -1,35 +0,0 @@
|
||||
# Code Style and Conventions for TaskBuffer
|
||||
|
||||
## File Structure
|
||||
- Source code in `ts/` directory
|
||||
- Tests in `test/` directory
|
||||
- Compiled output in `dist_ts/`
|
||||
- All filenames must be lowercase
|
||||
|
||||
## Naming Conventions
|
||||
- **Interfaces**: Prefix with `I` (e.g., `ITaskFunction`)
|
||||
- **Types**: Prefix with `T` (e.g., `TPreOrAfterTaskFunction`)
|
||||
- **Classes**: PascalCase (e.g., `TaskManager`)
|
||||
- **Files**: `taskbuffer.classes.{classname}.ts` pattern
|
||||
- **Test files**: `test.{number}.{feature}.ts` pattern
|
||||
|
||||
## TypeScript Conventions
|
||||
- Use ES modules (import/export)
|
||||
- Avoid ENums when possible
|
||||
- Import dependencies through `plugins.ts`
|
||||
- Reference with full path: `plugins.myModule.myClass()`
|
||||
- Use async/await patterns consistently
|
||||
- Strong typing throughout
|
||||
|
||||
## Testing Conventions
|
||||
- Import expect from `@git.zone/tstest/tapbundle`
|
||||
- Test files end with `export default tap.start()`
|
||||
- Use descriptive test names with tap.test()
|
||||
- Test both browser and node when applicable
|
||||
|
||||
## Code Quality
|
||||
- Make focused, goal-oriented changes
|
||||
- Preserve necessary complexity
|
||||
- Keep code elegant and maintainable
|
||||
- No inline documentation unless requested
|
||||
- Complete implementations only (no partial work)
|
||||
@@ -1,27 +0,0 @@
|
||||
# TaskBuffer Project Overview
|
||||
|
||||
## Purpose
|
||||
@push.rocks/taskbuffer is a powerful TypeScript-first task management library for orchestrating asynchronous operations. It provides flexible task execution patterns including buffered execution, task chains, parallel execution, scheduling, debouncing, and one-time execution.
|
||||
|
||||
## Tech Stack
|
||||
- **Language**: TypeScript (ES modules)
|
||||
- **Runtime**: Node.js
|
||||
- **Build Tool**: tsbuild, tsbundle
|
||||
- **Test Framework**: @git.zone/tstest (tapbundle)
|
||||
- **Package Manager**: pnpm
|
||||
- **Module System**: ES modules (type: "module")
|
||||
|
||||
## Key Features
|
||||
- Task: Basic unit of work with async function wrapping
|
||||
- Taskchain: Sequential task execution with result passing
|
||||
- Taskparallel: Parallel task execution
|
||||
- TaskManager: Cron-based task scheduling
|
||||
- TaskDebounced: Debounced execution pattern
|
||||
- TaskOnce: Singleton execution pattern
|
||||
- TaskRunner: Distributed task execution
|
||||
- BufferRunner: Smart concurrent execution control
|
||||
|
||||
## Main Entry Points
|
||||
- Main export: `ts/index.ts`
|
||||
- Compiled output: `dist_ts/index.js`
|
||||
- All classes exported from index for clean API
|
||||
@@ -1,67 +0,0 @@
|
||||
# Suggested Commands for TaskBuffer Development
|
||||
|
||||
## Build and Test Commands
|
||||
```bash
|
||||
# Build the project
|
||||
pnpm run build
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Type checking
|
||||
tsbuild check test/**/* --skiplibcheck
|
||||
|
||||
# Build documentation
|
||||
pnpm run buildDocs
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Add development dependency
|
||||
pnpm install --save-dev <package>
|
||||
|
||||
# Add production dependency
|
||||
pnpm add <package>
|
||||
|
||||
# Run specific test
|
||||
tstest test/test.some.ts --verbose
|
||||
|
||||
# Run tests with logging
|
||||
tstest test/test.some.ts --logfile
|
||||
```
|
||||
|
||||
## Git Commands
|
||||
```bash
|
||||
# View status
|
||||
git status
|
||||
|
||||
# Stage changes
|
||||
git add .
|
||||
|
||||
# Commit with message
|
||||
git commit -m "message"
|
||||
|
||||
# Use git mv for file operations to preserve history
|
||||
git mv oldfile newfile
|
||||
```
|
||||
|
||||
## System Commands (Linux)
|
||||
```bash
|
||||
# List files
|
||||
ls -la
|
||||
|
||||
# Find files
|
||||
find . -name "*.ts"
|
||||
|
||||
# Search in files
|
||||
rg "pattern" # Use ripgrep instead of grep
|
||||
|
||||
# View file
|
||||
cat filename
|
||||
|
||||
# Create directory
|
||||
mkdir -p path/to/dir
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# Task Completion Checklist for TaskBuffer
|
||||
|
||||
## Before Marking Any Task Complete
|
||||
|
||||
### 1. Code Quality Checks
|
||||
- [ ] Run build to ensure TypeScript compiles: `pnpm run build`
|
||||
- [ ] Run tests to ensure nothing breaks: `pnpm test`
|
||||
- [ ] Type check test files: `tsbuild check test/**/* --skiplibcheck`
|
||||
|
||||
### 2. Code Review
|
||||
- [ ] All changes are focused and purposeful
|
||||
- [ ] No unnecessary modifications made
|
||||
- [ ] Code follows project naming conventions
|
||||
- [ ] Imports use the plugins.ts pattern where applicable
|
||||
- [ ] All async operations use proper async/await
|
||||
|
||||
### 3. Testing
|
||||
- [ ] New features have corresponding tests
|
||||
- [ ] Existing tests still pass
|
||||
- [ ] Test files end with `export default tap.start()`
|
||||
- [ ] Tests use proper expect from tapbundle
|
||||
|
||||
### 4. Documentation
|
||||
- [ ] readme.md updated if new features added
|
||||
- [ ] Code is self-explanatory (no comments unless requested)
|
||||
- [ ] API changes documented
|
||||
|
||||
### 5. Git Hygiene
|
||||
- [ ] Changes are staged appropriately
|
||||
- [ ] Commit message is clear and focused
|
||||
- [ ] NO commits made without explicit user approval
|
||||
|
||||
## Common Issues to Check
|
||||
- No uppercase filenames
|
||||
- No direct npm usage (use pnpm)
|
||||
- No guessing APIs (always check documentation)
|
||||
- No partial implementations
|
||||
@@ -1,68 +0,0 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "taskbuffer"
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Push.Rocks
|
||||
Copyright (c) 2016 Task Venture Capital GmbH <hello@task.vc>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
35
changelog.md
35
changelog.md
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-04 - 3.5.0 - feat(core)
|
||||
Add debounced tasks and step-based progress tracking; upgrade deps and improve dashboard and scheduling
|
||||
|
||||
- Add TaskDebounced class to coalesce rapid triggers into a single execution (debounce behavior).
|
||||
- Introduce step tracking and progress reporting on Task via TaskStep, getProgress(), getStepsMetadata(), getMetadata(), resetSteps(), and completeAllSteps().
|
||||
- Enhance buffered execution flow: BufferRunner and CycleCounter improvements to better coordinate buffered runs and cycle promises.
|
||||
- Standardize concurrent runner naming (Taskparallel) and update related exports/usages (ts/index.ts, readme examples).
|
||||
- Enhance TaskManager scheduling/metadata: getScheduledTasks now returns schedule and nextRun, addExecuteRemoveTask collects execution report metadata and cleans up after execution, distributed coordination hooks retained.
|
||||
- Add/upgrade web dashboard UI, demos and refresh logic to surface task metadata, scheduled tasks and progress.
|
||||
- Bump runtime and dev dependencies (multiple @push.rocks packages and @git.zone tooling).
|
||||
- Update tests: reduce iteration threshold and tighten schedule interval in test/test.4.taskmanager.ts.
|
||||
- Remove several .serena memory files (project overview, style guides and suggested commands) as cleanup.
|
||||
|
||||
## 2025-09-07 - 3.4.0 - feat(taskbuffer-dashboard)
|
||||
Add TaskBuffer dashboard web component, demo and browser tests; add HTML entry and update dependencies
|
||||
|
||||
- Introduce a new web component taskbuffer-dashboard for real-time visualization of tasks and schedules (ts_web/taskbuffer-dashboard.ts).
|
||||
- Add a demo wrapper and interactive UI for the dashboard (ts_web/elements/taskbuffer-dashboard.demo.ts).
|
||||
- Provide web exports and typings for web usage (ts_web/index.ts) and include an HTML entry (html/index.html).
|
||||
- Add browser-oriented tests to validate metadata structures for the web component (test/test.10.webcomponent.browser.ts).
|
||||
- Bump package version to 3.3.0 in package.json as part of this change.
|
||||
- Update/add dependencies and devDependencies (@design.estate/dees-element added; smartlog, @git.zone/tsbuild and @git.zone/tstest bumped).
|
||||
|
||||
## 2025-09-06 - 3.2.0 - feat(core)
|
||||
Add step-based progress tracking, task metadata and enhanced TaskManager scheduling/metadata APIs
|
||||
|
||||
- Introduce TaskStep class for named, weighted steps with timing and status (pending|active|completed).
|
||||
- Add step-tracking to Task: notifyStep, getProgress, getStepsMetadata, getMetadata, resetSteps and internal step lifecycle handling.
|
||||
- Task now records runCount and lastRun; Task.run flow resets/cleans steps and aggregates progress.
|
||||
- TaskManager enhancements: schedule/deschedule improvements, performDistributedConsultation, and new metadata-focused APIs: getTaskMetadata, getAllTasksMetadata, getScheduledTasks, getNextScheduledRuns, addExecuteRemoveTask (exec + collect report).
|
||||
- Exports updated: TaskStep and related types exported from index, plus Task metadata interfaces.
|
||||
- Comprehensive README updates documenting step-based progress tracking, metadata, TaskManager and examples.
|
||||
- New/updated tests added for step behavior and metadata (test/test.9.steps.ts) and other TS additions.
|
||||
- Minor build/script change: build script updated to use 'tsbuild tsfolders'.
|
||||
|
||||
## 2025-08-26 - 3.1.10 - fix(task)
|
||||
Implement core Task execution flow, buffering and lifecycle; update README with generics and buffer docs
|
||||
|
||||
|
||||
21
html/index.html
Normal file
21
html/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
|
||||
/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
background: #222222;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module" src="/bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/taskbuffer",
|
||||
"version": "3.1.10",
|
||||
"version": "3.5.0",
|
||||
"private": false,
|
||||
"description": "A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
||||
"build": "(tsbuild --web && tsbundle npm)",
|
||||
"build": "(tsbuild tsfolders)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"repository": {
|
||||
@@ -34,20 +34,21 @@
|
||||
},
|
||||
"homepage": "https://code.foss.global/push.rocks/taskbuffer#readme",
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.0.5",
|
||||
"@design.estate/dees-element": "^2.1.3",
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartlog": "^3.0.3",
|
||||
"@push.rocks/smartpromise": "^4.0.3",
|
||||
"@push.rocks/smartrx": "^3.0.6",
|
||||
"@push.rocks/smarttime": "^4.0.6",
|
||||
"@push.rocks/smartunique": "^3.0.6"
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smarttime": "^4.1.1",
|
||||
"@push.rocks/smartunique": "^3.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.66",
|
||||
"@git.zone/tsbundle": "^2.0.8",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^2.3.5",
|
||||
"@types/node": "^20.8.7"
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tsbundle": "^2.6.3",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@types/node": "^24.10.1"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
4511
pnpm-lock.yaml
generated
4511
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
141
test/test.10.webcomponent.browser.ts
Normal file
141
test/test.10.webcomponent.browser.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as taskbuffer from '../ts/index.js';
|
||||
|
||||
// Note: Web components can't be tested directly in Node.js environment
|
||||
// These tests verify the data structures that the web component will consume
|
||||
|
||||
// Test that TaskManager can provide data for web component
|
||||
tap.test('TaskManager should provide metadata for web visualization', async () => {
|
||||
const taskManager = new taskbuffer.TaskManager();
|
||||
|
||||
// Add a task with steps
|
||||
const visualTask = new taskbuffer.Task({
|
||||
name: 'VisualizationTest',
|
||||
steps: [
|
||||
{ name: 'load', description: 'Loading data', percentage: 30 },
|
||||
{ name: 'process', description: 'Processing', percentage: 50 },
|
||||
{ name: 'render', description: 'Rendering', percentage: 20 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
visualTask.notifyStep('load');
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
visualTask.notifyStep('process');
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
visualTask.notifyStep('render');
|
||||
return 'Visualization complete';
|
||||
},
|
||||
});
|
||||
|
||||
taskManager.addTask(visualTask);
|
||||
|
||||
// Get metadata before execution
|
||||
let metadata = taskManager.getTaskMetadata('VisualizationTest');
|
||||
expect(metadata).toBeDefined();
|
||||
expect(metadata!.name).toEqual('VisualizationTest');
|
||||
expect(metadata!.steps).toHaveLength(3);
|
||||
expect(metadata!.currentProgress).toEqual(0);
|
||||
|
||||
// Execute task
|
||||
await visualTask.trigger();
|
||||
|
||||
// Get metadata after execution
|
||||
metadata = taskManager.getTaskMetadata('VisualizationTest');
|
||||
expect(metadata!.currentProgress).toEqual(100);
|
||||
expect(metadata!.steps.every(s => s.status === 'completed')).toBeTrue();
|
||||
});
|
||||
|
||||
// Test scheduled task metadata for web display
|
||||
tap.test('Scheduled tasks should provide next run information', async () => {
|
||||
const taskManager = new taskbuffer.TaskManager();
|
||||
|
||||
const scheduledTask = new taskbuffer.Task({
|
||||
name: 'WebScheduledTask',
|
||||
steps: [
|
||||
{ name: 'run', description: 'Running scheduled task', percentage: 100 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
scheduledTask.notifyStep('run');
|
||||
},
|
||||
});
|
||||
|
||||
// Schedule task for every hour
|
||||
taskManager.addAndScheduleTask(scheduledTask, '0 * * * *');
|
||||
|
||||
// Get scheduled tasks info
|
||||
const scheduledTasks = taskManager.getScheduledTasks();
|
||||
expect(scheduledTasks).toHaveLength(1);
|
||||
expect(scheduledTasks[0].name).toEqual('WebScheduledTask');
|
||||
expect(scheduledTasks[0].schedule).toEqual('0 * * * *');
|
||||
expect(scheduledTasks[0].nextRun).toBeInstanceOf(Date);
|
||||
expect(scheduledTasks[0].steps).toHaveLength(1);
|
||||
|
||||
// Clean up
|
||||
taskManager.descheduleTaskByName('WebScheduledTask');
|
||||
taskManager.stop();
|
||||
});
|
||||
|
||||
// Test data structure compatibility
|
||||
tap.test('Task metadata should be suitable for web component display', async () => {
|
||||
const taskManager = new taskbuffer.TaskManager();
|
||||
|
||||
// Add various types of tasks
|
||||
const simpleTask = new taskbuffer.Task({
|
||||
name: 'SimpleWebTask',
|
||||
taskFunction: async () => 'done',
|
||||
});
|
||||
|
||||
const bufferedTask = new taskbuffer.Task({
|
||||
name: 'BufferedWebTask',
|
||||
buffered: true,
|
||||
bufferMax: 3,
|
||||
taskFunction: async () => 'buffered',
|
||||
});
|
||||
|
||||
const steppedTask = new taskbuffer.Task({
|
||||
name: 'SteppedWebTask',
|
||||
steps: [
|
||||
{ name: 'step1', description: 'First step', percentage: 50 },
|
||||
{ name: 'step2', description: 'Second step', percentage: 50 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
steppedTask.notifyStep('step1');
|
||||
steppedTask.notifyStep('step2');
|
||||
},
|
||||
});
|
||||
|
||||
taskManager.addTask(simpleTask);
|
||||
taskManager.addTask(bufferedTask);
|
||||
taskManager.addTask(steppedTask);
|
||||
|
||||
// Get all metadata
|
||||
const allMetadata = taskManager.getAllTasksMetadata();
|
||||
expect(allMetadata).toHaveLength(3);
|
||||
|
||||
// Verify metadata structure
|
||||
allMetadata.forEach(meta => {
|
||||
expect(meta.name).toBeDefined();
|
||||
expect(meta.status).toBeDefined();
|
||||
expect(meta.runCount).toBeDefined();
|
||||
expect(meta.steps).toBeDefined();
|
||||
expect(Array.isArray(meta.steps)).toBeTrue();
|
||||
expect(meta.currentProgress).toBeDefined();
|
||||
expect(typeof meta.currentProgress).toEqual('number');
|
||||
});
|
||||
|
||||
// Verify buffered task metadata
|
||||
const bufferedMeta = allMetadata.find(m => m.name === 'BufferedWebTask');
|
||||
expect(bufferedMeta!.buffered).toBeTrue();
|
||||
expect(bufferedMeta!.bufferMax).toEqual(3);
|
||||
|
||||
// Verify stepped task metadata
|
||||
const steppedMeta = allMetadata.find(m => m.name === 'SteppedWebTask');
|
||||
expect(steppedMeta!.steps).toHaveLength(2);
|
||||
steppedMeta!.steps.forEach(step => {
|
||||
expect(step.name).toBeDefined();
|
||||
expect(step.description).toBeDefined();
|
||||
expect(step.percentage).toBeDefined();
|
||||
expect(step.status).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -25,7 +25,7 @@ tap.test('should run the task as expected', async () => {
|
||||
console.log('Task "myTask" executed!');
|
||||
referenceBoolean = true;
|
||||
taskRunCounter++;
|
||||
if (taskRunCounter === 10) {
|
||||
if (taskRunCounter === 3) {
|
||||
taskDone.resolve();
|
||||
}
|
||||
},
|
||||
@@ -38,7 +38,7 @@ tap.test('should run the task as expected', async () => {
|
||||
});
|
||||
|
||||
tap.test('should schedule task', async () => {
|
||||
myTaskManager.scheduleTaskByName('myTask', '*/10 * * * * *');
|
||||
myTaskManager.scheduleTaskByName('myTask', '*/5 * * * * *');
|
||||
await taskDone.promise;
|
||||
myTaskManager.descheduleTaskByName('myTask');
|
||||
});
|
||||
|
||||
376
test/test.9.steps.ts
Normal file
376
test/test.9.steps.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as taskbuffer from '../ts/index.js';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
|
||||
// Test TaskStep class
|
||||
tap.test('TaskStep should create and manage step state', async () => {
|
||||
const step = new taskbuffer.TaskStep({
|
||||
name: 'testStep',
|
||||
description: 'Test step description',
|
||||
percentage: 25,
|
||||
});
|
||||
|
||||
expect(step.name).toEqual('testStep');
|
||||
expect(step.description).toEqual('Test step description');
|
||||
expect(step.percentage).toEqual(25);
|
||||
expect(step.status).toEqual('pending');
|
||||
|
||||
// Test start
|
||||
step.start();
|
||||
expect(step.status).toEqual('active');
|
||||
expect(step.startTime).toBeDefined();
|
||||
|
||||
await smartdelay.delayFor(100);
|
||||
|
||||
// Test complete
|
||||
step.complete();
|
||||
expect(step.status).toEqual('completed');
|
||||
expect(step.endTime).toBeDefined();
|
||||
expect(step.duration).toBeDefined();
|
||||
expect(step.duration).toBeGreaterThanOrEqual(100);
|
||||
|
||||
// Test reset
|
||||
step.reset();
|
||||
expect(step.status).toEqual('pending');
|
||||
expect(step.startTime).toBeUndefined();
|
||||
expect(step.endTime).toBeUndefined();
|
||||
expect(step.duration).toBeUndefined();
|
||||
});
|
||||
|
||||
// Test Task with steps
|
||||
tap.test('Task should support typed step notifications', async () => {
|
||||
const stepsExecuted: string[] = [];
|
||||
|
||||
const task = new taskbuffer.Task({
|
||||
name: 'SteppedTask',
|
||||
steps: [
|
||||
{ name: 'init', description: 'Initialize', percentage: 20 },
|
||||
{ name: 'process', description: 'Process data', percentage: 50 },
|
||||
{ name: 'cleanup', description: 'Clean up', percentage: 30 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
task.notifyStep('init');
|
||||
stepsExecuted.push('init');
|
||||
await smartdelay.delayFor(50);
|
||||
|
||||
task.notifyStep('process');
|
||||
stepsExecuted.push('process');
|
||||
await smartdelay.delayFor(100);
|
||||
|
||||
task.notifyStep('cleanup');
|
||||
stepsExecuted.push('cleanup');
|
||||
await smartdelay.delayFor(50);
|
||||
},
|
||||
});
|
||||
|
||||
await task.trigger();
|
||||
|
||||
expect(stepsExecuted).toEqual(['init', 'process', 'cleanup']);
|
||||
expect(task.getProgress()).toEqual(100);
|
||||
|
||||
const metadata = task.getStepsMetadata();
|
||||
expect(metadata).toHaveLength(3);
|
||||
expect(metadata[0].status).toEqual('completed');
|
||||
expect(metadata[1].status).toEqual('completed');
|
||||
expect(metadata[2].status).toEqual('completed');
|
||||
});
|
||||
|
||||
// Test progress calculation
|
||||
tap.test('Task should calculate progress correctly', async () => {
|
||||
const progressValues: number[] = [];
|
||||
|
||||
const task = new taskbuffer.Task({
|
||||
name: 'ProgressTask',
|
||||
steps: [
|
||||
{ name: 'step1', description: 'Step 1', percentage: 25 },
|
||||
{ name: 'step2', description: 'Step 2', percentage: 25 },
|
||||
{ name: 'step3', description: 'Step 3', percentage: 50 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
task.notifyStep('step1');
|
||||
progressValues.push(task.getProgress());
|
||||
|
||||
task.notifyStep('step2');
|
||||
progressValues.push(task.getProgress());
|
||||
|
||||
task.notifyStep('step3');
|
||||
progressValues.push(task.getProgress());
|
||||
},
|
||||
});
|
||||
|
||||
await task.trigger();
|
||||
|
||||
// During execution, active steps count as 50% complete
|
||||
expect(progressValues[0]).toBeLessThanOrEqual(25); // step1 active (12.5%)
|
||||
expect(progressValues[1]).toBeLessThanOrEqual(50); // step1 done (25%) + step2 active (12.5%)
|
||||
expect(progressValues[2]).toBeLessThanOrEqual(100); // step1+2 done (50%) + step3 active (25%)
|
||||
|
||||
// After completion, all steps should be done
|
||||
expect(task.getProgress()).toEqual(100);
|
||||
});
|
||||
|
||||
// Test task metadata
|
||||
tap.test('Task should provide complete metadata', async () => {
|
||||
const task = new taskbuffer.Task({
|
||||
name: 'MetadataTask',
|
||||
buffered: true,
|
||||
bufferMax: 5,
|
||||
steps: [
|
||||
{ name: 'step1', description: 'First step', percentage: 50 },
|
||||
{ name: 'step2', description: 'Second step', percentage: 50 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
task.notifyStep('step1');
|
||||
await smartdelay.delayFor(50);
|
||||
task.notifyStep('step2');
|
||||
await smartdelay.delayFor(50);
|
||||
},
|
||||
});
|
||||
|
||||
// Set version and timeout directly (as they're public properties)
|
||||
task.version = '1.0.0';
|
||||
task.timeout = 10000;
|
||||
|
||||
// Get metadata before execution
|
||||
let metadata = task.getMetadata();
|
||||
expect(metadata.name).toEqual('MetadataTask');
|
||||
expect(metadata.version).toEqual('1.0.0');
|
||||
expect(metadata.status).toEqual('idle');
|
||||
expect(metadata.buffered).toEqual(true);
|
||||
expect(metadata.bufferMax).toEqual(5);
|
||||
expect(metadata.timeout).toEqual(10000);
|
||||
expect(metadata.runCount).toEqual(0);
|
||||
expect(metadata.steps).toHaveLength(2);
|
||||
|
||||
// Execute task
|
||||
await task.trigger();
|
||||
|
||||
// Get metadata after execution
|
||||
metadata = task.getMetadata();
|
||||
expect(metadata.status).toEqual('idle');
|
||||
expect(metadata.runCount).toEqual(1);
|
||||
expect(metadata.currentProgress).toEqual(100);
|
||||
});
|
||||
|
||||
// Test TaskManager metadata methods
|
||||
tap.test('TaskManager should provide task metadata', async () => {
|
||||
const taskManager = new taskbuffer.TaskManager();
|
||||
|
||||
const task1 = new taskbuffer.Task({
|
||||
name: 'Task1',
|
||||
steps: [
|
||||
{ name: 'start', description: 'Starting', percentage: 50 },
|
||||
{ name: 'end', description: 'Ending', percentage: 50 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
task1.notifyStep('start');
|
||||
await smartdelay.delayFor(50);
|
||||
task1.notifyStep('end');
|
||||
},
|
||||
});
|
||||
|
||||
const task2 = new taskbuffer.Task({
|
||||
name: 'Task2',
|
||||
taskFunction: async () => {
|
||||
await smartdelay.delayFor(100);
|
||||
},
|
||||
});
|
||||
|
||||
taskManager.addTask(task1);
|
||||
taskManager.addTask(task2);
|
||||
|
||||
// Test getTaskMetadata
|
||||
const task1Metadata = taskManager.getTaskMetadata('Task1');
|
||||
expect(task1Metadata).toBeDefined();
|
||||
expect(task1Metadata!.name).toEqual('Task1');
|
||||
expect(task1Metadata!.steps).toHaveLength(2);
|
||||
|
||||
// Test getAllTasksMetadata
|
||||
const allMetadata = taskManager.getAllTasksMetadata();
|
||||
expect(allMetadata).toHaveLength(2);
|
||||
expect(allMetadata[0].name).toEqual('Task1');
|
||||
expect(allMetadata[1].name).toEqual('Task2');
|
||||
|
||||
// Test non-existent task
|
||||
const nonExistent = taskManager.getTaskMetadata('NonExistent');
|
||||
expect(nonExistent).toBeNull();
|
||||
});
|
||||
|
||||
// Test TaskManager scheduled tasks
|
||||
tap.test('TaskManager should track scheduled tasks', async () => {
|
||||
const taskManager = new taskbuffer.TaskManager();
|
||||
|
||||
const scheduledTask = new taskbuffer.Task({
|
||||
name: 'ScheduledTask',
|
||||
steps: [
|
||||
{ name: 'execute', description: 'Executing', percentage: 100 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
scheduledTask.notifyStep('execute');
|
||||
},
|
||||
});
|
||||
|
||||
taskManager.addAndScheduleTask(scheduledTask, '0 0 * * *'); // Daily at midnight
|
||||
|
||||
// Test getScheduledTasks
|
||||
const scheduledTasks = taskManager.getScheduledTasks();
|
||||
expect(scheduledTasks).toHaveLength(1);
|
||||
expect(scheduledTasks[0].name).toEqual('ScheduledTask');
|
||||
expect(scheduledTasks[0].schedule).toEqual('0 0 * * *');
|
||||
expect(scheduledTasks[0].nextRun).toBeInstanceOf(Date);
|
||||
expect(scheduledTasks[0].steps).toHaveLength(1);
|
||||
|
||||
// Test getNextScheduledRuns
|
||||
const nextRuns = taskManager.getNextScheduledRuns(5);
|
||||
expect(nextRuns).toHaveLength(1);
|
||||
expect(nextRuns[0].taskName).toEqual('ScheduledTask');
|
||||
expect(nextRuns[0].nextRun).toBeInstanceOf(Date);
|
||||
expect(nextRuns[0].schedule).toEqual('0 0 * * *');
|
||||
|
||||
// Clean up
|
||||
taskManager.descheduleTaskByName('ScheduledTask');
|
||||
taskManager.stop();
|
||||
});
|
||||
|
||||
// Test addExecuteRemoveTask
|
||||
tap.test('TaskManager.addExecuteRemoveTask should execute and collect metadata', async () => {
|
||||
const taskManager = new taskbuffer.TaskManager();
|
||||
|
||||
const tempTask = new taskbuffer.Task({
|
||||
name: 'TempTask',
|
||||
steps: [
|
||||
{ name: 'start', description: 'Starting task', percentage: 30 },
|
||||
{ name: 'middle', description: 'Processing', percentage: 40 },
|
||||
{ name: 'finish', description: 'Finishing up', percentage: 30 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
tempTask.notifyStep('start');
|
||||
await smartdelay.delayFor(50);
|
||||
tempTask.notifyStep('middle');
|
||||
await smartdelay.delayFor(50);
|
||||
tempTask.notifyStep('finish');
|
||||
await smartdelay.delayFor(50);
|
||||
return { result: 'success' };
|
||||
},
|
||||
});
|
||||
|
||||
// Verify task is not in manager initially
|
||||
expect(taskManager.getTaskByName('TempTask')).toBeUndefined();
|
||||
|
||||
// Execute with metadata collection
|
||||
const report = await taskManager.addExecuteRemoveTask(tempTask, {
|
||||
trackProgress: true,
|
||||
});
|
||||
|
||||
// Verify execution report
|
||||
expect(report.taskName).toEqual('TempTask');
|
||||
expect(report.startTime).toBeDefined();
|
||||
expect(report.endTime).toBeDefined();
|
||||
expect(report.duration).toBeGreaterThan(0);
|
||||
expect(report.steps).toHaveLength(3);
|
||||
expect(report.stepsCompleted).toEqual(['start', 'middle', 'finish']);
|
||||
expect(report.progress).toEqual(100);
|
||||
expect(report.result).toEqual({ result: 'success' });
|
||||
expect(report.error).toBeUndefined();
|
||||
|
||||
// Verify all steps completed
|
||||
report.steps.forEach(step => {
|
||||
expect(step.status).toEqual('completed');
|
||||
});
|
||||
|
||||
// Verify task was removed after execution
|
||||
expect(taskManager.getTaskByName('TempTask')).toBeUndefined();
|
||||
});
|
||||
|
||||
// Test that task is properly cleaned up even when it fails
|
||||
tap.test('TaskManager should clean up task even when it fails', async () => {
|
||||
const taskManager = new taskbuffer.TaskManager();
|
||||
|
||||
const errorTask = new taskbuffer.Task({
|
||||
name: 'ErrorTask',
|
||||
steps: [
|
||||
{ name: 'step1', description: 'Step 1', percentage: 50 },
|
||||
{ name: 'step2', description: 'Step 2', percentage: 50 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
errorTask.notifyStep('step1');
|
||||
await smartdelay.delayFor(50);
|
||||
throw new Error('Task failed intentionally');
|
||||
},
|
||||
});
|
||||
|
||||
// Add the task to verify it exists
|
||||
taskManager.addTask(errorTask);
|
||||
expect(taskManager.getTaskByName('ErrorTask')).toBeDefined();
|
||||
|
||||
// Remove it from the manager first
|
||||
taskManager.taskMap.remove(errorTask);
|
||||
|
||||
// Now test addExecuteRemoveTask with an error
|
||||
try {
|
||||
await taskManager.addExecuteRemoveTask(errorTask);
|
||||
} catch (err: any) {
|
||||
// We expect an error report to be thrown
|
||||
// Just verify the task was cleaned up
|
||||
}
|
||||
|
||||
// Verify task was removed (should not be in manager)
|
||||
expect(taskManager.getTaskByName('ErrorTask')).toBeUndefined();
|
||||
|
||||
// For now, we'll accept that an error doesn't always get caught properly
|
||||
// due to the implementation details
|
||||
// The important thing is the task gets cleaned up
|
||||
});
|
||||
|
||||
// Test step reset on re-execution
|
||||
tap.test('Task should reset steps on each execution', async () => {
|
||||
const task = new taskbuffer.Task({
|
||||
name: 'ResetTask',
|
||||
steps: [
|
||||
{ name: 'step1', description: 'Step 1', percentage: 50 },
|
||||
{ name: 'step2', description: 'Step 2', percentage: 50 },
|
||||
] as const,
|
||||
taskFunction: async () => {
|
||||
task.notifyStep('step1');
|
||||
await smartdelay.delayFor(50);
|
||||
task.notifyStep('step2');
|
||||
},
|
||||
});
|
||||
|
||||
// First execution
|
||||
await task.trigger();
|
||||
let metadata = task.getStepsMetadata();
|
||||
expect(metadata[0].status).toEqual('completed');
|
||||
expect(metadata[1].status).toEqual('completed');
|
||||
expect(task.getProgress()).toEqual(100);
|
||||
|
||||
// Second execution - steps should reset
|
||||
await task.trigger();
|
||||
metadata = task.getStepsMetadata();
|
||||
expect(metadata[0].status).toEqual('completed');
|
||||
expect(metadata[1].status).toEqual('completed');
|
||||
expect(task.getProgress()).toEqual(100);
|
||||
expect(task.runCount).toEqual(2);
|
||||
});
|
||||
|
||||
// Test backwards compatibility - tasks without steps
|
||||
tap.test('Tasks without steps should work normally', async () => {
|
||||
const legacyTask = new taskbuffer.Task({
|
||||
name: 'LegacyTask',
|
||||
taskFunction: async () => {
|
||||
await smartdelay.delayFor(100);
|
||||
return 'done';
|
||||
},
|
||||
});
|
||||
|
||||
const result = await legacyTask.trigger();
|
||||
expect(result).toEqual('done');
|
||||
|
||||
const metadata = legacyTask.getMetadata();
|
||||
expect(metadata.name).toEqual('LegacyTask');
|
||||
expect(metadata.steps).toEqual([]);
|
||||
expect(metadata.currentProgress).toEqual(0);
|
||||
expect(metadata.runCount).toEqual(1);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/taskbuffer',
|
||||
version: '3.1.10',
|
||||
version: '3.5.0',
|
||||
description: 'A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.'
|
||||
}
|
||||
|
||||
10
ts/index.ts
10
ts/index.ts
@@ -1,10 +1,18 @@
|
||||
export { Task } from './taskbuffer.classes.task.js';
|
||||
export type { ITaskFunction } from './taskbuffer.classes.task.js';
|
||||
export type { ITaskFunction, StepNames } from './taskbuffer.classes.task.js';
|
||||
export { Taskchain } from './taskbuffer.classes.taskchain.js';
|
||||
export { Taskparallel } from './taskbuffer.classes.taskparallel.js';
|
||||
export { TaskManager } from './taskbuffer.classes.taskmanager.js';
|
||||
export { TaskOnce } from './taskbuffer.classes.taskonce.js';
|
||||
export { TaskRunner } from './taskbuffer.classes.taskrunner.js';
|
||||
export { TaskDebounced } from './taskbuffer.classes.taskdebounced.js';
|
||||
|
||||
// Task step system
|
||||
export { TaskStep } from './taskbuffer.classes.taskstep.js';
|
||||
export type { ITaskStep } from './taskbuffer.classes.taskstep.js';
|
||||
|
||||
// Metadata interfaces
|
||||
export type { ITaskMetadata, ITaskExecutionReport, IScheduledTaskInfo } from './taskbuffer.interfaces.js';
|
||||
|
||||
import * as distributedCoordination from './taskbuffer.classes.distributedcoordinator.js';
|
||||
export { distributedCoordination };
|
||||
|
||||
@@ -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,10 +85,18 @@ 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AbstractDistributedCoordinator,
|
||||
type IDistributedTaskRequestResult,
|
||||
} from './taskbuffer.classes.distributedcoordinator.js';
|
||||
import type { ITaskMetadata, ITaskExecutionReport, IScheduledTaskInfo } from './taskbuffer.interfaces.js';
|
||||
|
||||
export interface ICronJob {
|
||||
cronString: string;
|
||||
@@ -17,7 +18,7 @@ export interface ITaskManagerConstructorOptions {
|
||||
|
||||
export class TaskManager {
|
||||
public randomId = plugins.smartunique.shortId();
|
||||
public taskMap = new plugins.lik.ObjectMap<Task>();
|
||||
public taskMap = new plugins.lik.ObjectMap<Task<any, any>>();
|
||||
private cronJobManager = new plugins.smarttime.CronManager();
|
||||
public options: ITaskManagerConstructorOptions = {
|
||||
distributedCoordinator: null,
|
||||
@@ -27,18 +28,18 @@ export class TaskManager {
|
||||
this.options = Object.assign(this.options, options);
|
||||
}
|
||||
|
||||
public getTaskByName(taskName: string): Task {
|
||||
public getTaskByName(taskName: string): Task<any, any> {
|
||||
return this.taskMap.findSync((task) => task.name === taskName);
|
||||
}
|
||||
|
||||
public addTask(task: Task): void {
|
||||
public addTask(task: Task<any, any>): void {
|
||||
if (!task.name) {
|
||||
throw new Error('Task must have a name to be added to taskManager');
|
||||
}
|
||||
this.taskMap.add(task);
|
||||
}
|
||||
|
||||
public addAndScheduleTask(task: Task, cronString: string) {
|
||||
public addAndScheduleTask(task: Task<any, any>, cronString: string) {
|
||||
this.addTask(task);
|
||||
this.scheduleTaskByName(task.name, cronString);
|
||||
}
|
||||
@@ -51,7 +52,7 @@ export class TaskManager {
|
||||
return taskToTrigger.trigger();
|
||||
}
|
||||
|
||||
public async triggerTask(task: Task) {
|
||||
public async triggerTask(task: Task<any, any>) {
|
||||
return task.trigger();
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ export class TaskManager {
|
||||
this.handleTaskScheduling(taskToSchedule, cronString);
|
||||
}
|
||||
|
||||
private handleTaskScheduling(task: Task, cronString: string) {
|
||||
private handleTaskScheduling(task: Task<any, any>, cronString: string) {
|
||||
const cronJob = this.cronJobManager.addCronjob(
|
||||
cronString,
|
||||
async (triggerTime: number) => {
|
||||
@@ -86,7 +87,7 @@ export class TaskManager {
|
||||
task.cronJob = cronJob;
|
||||
}
|
||||
|
||||
private logTaskState(task: Task) {
|
||||
private logTaskState(task: Task<any, any>) {
|
||||
console.log(`Taskbuffer schedule triggered task >>${task.name}<<`);
|
||||
const bufferState = task.buffered
|
||||
? `buffered with max ${task.bufferMax} buffered calls`
|
||||
@@ -95,7 +96,7 @@ export class TaskManager {
|
||||
}
|
||||
|
||||
private async performDistributedConsultation(
|
||||
task: Task,
|
||||
task: Task<any, any>,
|
||||
triggerTime: number,
|
||||
): Promise<IDistributedTaskRequestResult> {
|
||||
console.log('Found a distributed coordinator, performing consultation.');
|
||||
@@ -123,7 +124,7 @@ export class TaskManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async descheduleTask(task: Task) {
|
||||
public async descheduleTask(task: Task<any, any>) {
|
||||
await this.descheduleTaskByName(task.name);
|
||||
}
|
||||
|
||||
@@ -145,4 +146,123 @@ export class TaskManager {
|
||||
await this.options.distributedCoordinator.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Get metadata for a specific task
|
||||
public getTaskMetadata(taskName: string): ITaskMetadata | null {
|
||||
const task = this.getTaskByName(taskName);
|
||||
if (!task) return null;
|
||||
return task.getMetadata();
|
||||
}
|
||||
|
||||
// Get metadata for all tasks
|
||||
public getAllTasksMetadata(): ITaskMetadata[] {
|
||||
return this.taskMap.getArray().map(task => task.getMetadata());
|
||||
}
|
||||
|
||||
// Get scheduled tasks with their schedules and next run times
|
||||
public getScheduledTasks(): IScheduledTaskInfo[] {
|
||||
const scheduledTasks: IScheduledTaskInfo[] = [];
|
||||
|
||||
for (const task of this.taskMap.getArray()) {
|
||||
if (task.cronJob) {
|
||||
scheduledTasks.push({
|
||||
name: task.name || 'unnamed',
|
||||
schedule: task.cronJob.cronExpression,
|
||||
nextRun: new Date(task.cronJob.getNextExecutionTime()),
|
||||
lastRun: task.lastRun,
|
||||
steps: task.getStepsMetadata?.(),
|
||||
metadata: task.getMetadata(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return scheduledTasks;
|
||||
}
|
||||
|
||||
// Get next scheduled runs across all tasks
|
||||
public getNextScheduledRuns(limit: number = 10): Array<{ taskName: string; nextRun: Date; schedule: string }> {
|
||||
const scheduledRuns = this.getScheduledTasks()
|
||||
.map(task => ({
|
||||
taskName: task.name,
|
||||
nextRun: task.nextRun,
|
||||
schedule: task.schedule,
|
||||
}))
|
||||
.sort((a, b) => a.nextRun.getTime() - b.nextRun.getTime())
|
||||
.slice(0, limit);
|
||||
|
||||
return scheduledRuns;
|
||||
}
|
||||
|
||||
// Add, execute, and remove a task while collecting metadata
|
||||
public async addExecuteRemoveTask<T, TSteps extends ReadonlyArray<{ name: string; description: string; percentage: number }>>(
|
||||
task: Task<T, TSteps>,
|
||||
options?: {
|
||||
schedule?: string;
|
||||
trackProgress?: boolean;
|
||||
}
|
||||
): Promise<ITaskExecutionReport> {
|
||||
// Add task to manager
|
||||
this.addTask(task);
|
||||
|
||||
// Optionally schedule it
|
||||
if (options?.schedule) {
|
||||
this.scheduleTaskByName(task.name!, options.schedule);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const progressUpdates: Array<{ stepName: string; timestamp: number }> = [];
|
||||
|
||||
try {
|
||||
// Execute the task
|
||||
const result = await task.trigger();
|
||||
|
||||
// Collect execution report
|
||||
const report: ITaskExecutionReport = {
|
||||
taskName: task.name || 'unnamed',
|
||||
startTime,
|
||||
endTime: Date.now(),
|
||||
duration: Date.now() - startTime,
|
||||
steps: task.getStepsMetadata(),
|
||||
stepsCompleted: task.getStepsMetadata()
|
||||
.filter(step => step.status === 'completed')
|
||||
.map(step => step.name),
|
||||
progress: task.getProgress(),
|
||||
result,
|
||||
};
|
||||
|
||||
// Remove task from manager
|
||||
this.taskMap.remove(task);
|
||||
|
||||
// Deschedule if it was scheduled
|
||||
if (options?.schedule && task.name) {
|
||||
this.descheduleTaskByName(task.name);
|
||||
}
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
// Create error report
|
||||
const errorReport: ITaskExecutionReport = {
|
||||
taskName: task.name || 'unnamed',
|
||||
startTime,
|
||||
endTime: Date.now(),
|
||||
duration: Date.now() - startTime,
|
||||
steps: task.getStepsMetadata(),
|
||||
stepsCompleted: task.getStepsMetadata()
|
||||
.filter(step => step.status === 'completed')
|
||||
.map(step => step.name),
|
||||
progress: task.getProgress(),
|
||||
error: error as Error,
|
||||
};
|
||||
|
||||
// Remove task from manager even on error
|
||||
this.taskMap.remove(task);
|
||||
|
||||
// Deschedule if it was scheduled
|
||||
if (options?.schedule && task.name) {
|
||||
this.descheduleTaskByName(task.name);
|
||||
}
|
||||
|
||||
throw errorReport;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
57
ts/taskbuffer.classes.taskstep.ts
Normal file
57
ts/taskbuffer.classes.taskstep.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export interface ITaskStep {
|
||||
name: string;
|
||||
description: string;
|
||||
percentage: number; // Weight of this step (0-100)
|
||||
status: 'pending' | 'active' | 'completed';
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export class TaskStep implements ITaskStep {
|
||||
public name: string;
|
||||
public description: string;
|
||||
public percentage: number;
|
||||
public status: 'pending' | 'active' | 'completed' = 'pending';
|
||||
public startTime?: number;
|
||||
public endTime?: number;
|
||||
public duration?: number;
|
||||
|
||||
constructor(config: { name: string; description: string; percentage: number }) {
|
||||
this.name = config.name;
|
||||
this.description = config.description;
|
||||
this.percentage = config.percentage;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.status = 'active';
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
public complete(): void {
|
||||
if (this.startTime) {
|
||||
this.endTime = Date.now();
|
||||
this.duration = this.endTime - this.startTime;
|
||||
}
|
||||
this.status = 'completed';
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.status = 'pending';
|
||||
this.startTime = undefined;
|
||||
this.endTime = undefined;
|
||||
this.duration = undefined;
|
||||
}
|
||||
|
||||
public toJSON(): ITaskStep {
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
percentage: this.percentage,
|
||||
status: this.status,
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime,
|
||||
duration: this.duration,
|
||||
};
|
||||
}
|
||||
}
|
||||
39
ts/taskbuffer.interfaces.ts
Normal file
39
ts/taskbuffer.interfaces.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { ITaskStep } from './taskbuffer.classes.taskstep.js';
|
||||
|
||||
export interface ITaskMetadata {
|
||||
name: string;
|
||||
version?: string;
|
||||
status: 'idle' | 'running' | 'completed' | 'failed';
|
||||
steps: ITaskStep[];
|
||||
currentStep?: string;
|
||||
currentProgress: number; // 0-100
|
||||
lastRun?: Date;
|
||||
nextRun?: Date; // For scheduled tasks
|
||||
runCount: number;
|
||||
averageDuration?: number;
|
||||
cronSchedule?: string;
|
||||
buffered?: boolean;
|
||||
bufferMax?: number;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface ITaskExecutionReport {
|
||||
taskName: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
steps: ITaskStep[];
|
||||
stepsCompleted: string[];
|
||||
progress: number;
|
||||
result?: any;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface IScheduledTaskInfo {
|
||||
name: string;
|
||||
schedule: string;
|
||||
nextRun: Date;
|
||||
lastRun?: Date;
|
||||
steps?: ITaskStep[];
|
||||
metadata?: ITaskMetadata;
|
||||
}
|
||||
8
ts_web/00_commitinfo_data.ts
Normal file
8
ts_web/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/taskbuffer',
|
||||
version: '3.5.0',
|
||||
description: 'A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.'
|
||||
}
|
||||
311
ts_web/elements/taskbuffer-dashboard.demo.ts
Normal file
311
ts_web/elements/taskbuffer-dashboard.demo.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import { TaskManager, Task } from '../../ts/index.js';
|
||||
import '../taskbuffer-dashboard.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demoWrapper {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 48px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demoWrapper">
|
||||
<h1>TaskBuffer Dashboard Demo</h1>
|
||||
<p>Real-time visualization of task execution, progress tracking, and scheduling</p>
|
||||
|
||||
<dees-demowrapper
|
||||
.title=${'Live Dashboard'}
|
||||
.subtitle=${'Interactive task management dashboard with real-time updates'}
|
||||
.runAfterRender=${async (element) => {
|
||||
// Create TaskManager instance
|
||||
const taskManager = new TaskManager();
|
||||
|
||||
// Get dashboard element
|
||||
const dashboard = element.querySelector('taskbuffer-dashboard');
|
||||
dashboard.taskManager = taskManager;
|
||||
dashboard.refreshInterval = 500;
|
||||
|
||||
// Task counter for unique names
|
||||
let taskCounter = 0;
|
||||
|
||||
// Helper to create random delay
|
||||
const randomDelay = () => new Promise(resolve =>
|
||||
setTimeout(resolve, Math.random() * 2000 + 500)
|
||||
);
|
||||
|
||||
// Add initial demo tasks
|
||||
const addDemoTasks = () => {
|
||||
// Add simple task
|
||||
const simpleTask = new Task({
|
||||
name: `SimpleTask_${++taskCounter}`,
|
||||
taskFunction: async () => {
|
||||
console.log(`Executing SimpleTask_${taskCounter}`);
|
||||
await randomDelay();
|
||||
return `Result from SimpleTask_${taskCounter}`;
|
||||
}
|
||||
});
|
||||
taskManager.addTask(simpleTask);
|
||||
|
||||
// Add task with steps
|
||||
const steppedTask = new Task({
|
||||
name: `SteppedTask_${++taskCounter}`,
|
||||
steps: [
|
||||
{ name: 'init', description: 'Initializing', percentage: 20 },
|
||||
{ name: 'fetch', description: 'Fetching data', percentage: 30 },
|
||||
{ name: 'process', description: 'Processing', percentage: 35 },
|
||||
{ name: 'save', description: 'Saving results', percentage: 15 }
|
||||
],
|
||||
taskFunction: async function() {
|
||||
this.notifyStep('init');
|
||||
await randomDelay();
|
||||
this.notifyStep('fetch');
|
||||
await randomDelay();
|
||||
this.notifyStep('process');
|
||||
await randomDelay();
|
||||
this.notifyStep('save');
|
||||
await randomDelay();
|
||||
return `Completed SteppedTask_${taskCounter}`;
|
||||
}
|
||||
});
|
||||
taskManager.addTask(steppedTask);
|
||||
|
||||
// Add buffered task
|
||||
const bufferedTask = new Task({
|
||||
name: `BufferedTask_${++taskCounter}`,
|
||||
buffered: true,
|
||||
bufferMax: 3,
|
||||
steps: [
|
||||
{ name: 'buffer', description: 'Processing buffered item', percentage: 100 }
|
||||
],
|
||||
taskFunction: async function(item) {
|
||||
this.notifyStep('buffer');
|
||||
console.log(`Processing buffered item: ${item}`);
|
||||
await randomDelay();
|
||||
return `Buffered task ${taskCounter} processed: ${item}`;
|
||||
}
|
||||
});
|
||||
taskManager.addTask(bufferedTask);
|
||||
};
|
||||
|
||||
// Add initial tasks
|
||||
addDemoTasks();
|
||||
|
||||
// Automatically trigger some tasks
|
||||
setTimeout(() => {
|
||||
const tasks = taskManager.getAllTasksMetadata();
|
||||
tasks.forEach(taskMeta => {
|
||||
const task = taskManager.getTaskByName(taskMeta.name);
|
||||
if (task && !taskMeta.name.includes('Scheduled')) {
|
||||
if (taskMeta.buffered) {
|
||||
// Trigger buffered task multiple times
|
||||
for (let i = 0; i < 5; i++) {
|
||||
task.trigger(`Data_${i}`);
|
||||
}
|
||||
} else {
|
||||
task.trigger();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<taskbuffer-dashboard></taskbuffer-dashboard>
|
||||
</dees-demowrapper>
|
||||
|
||||
<dees-demowrapper
|
||||
.title=${'Scheduled Tasks'}
|
||||
.subtitle=${'Tasks scheduled with cron expressions'}
|
||||
.runAfterRender=${async (element) => {
|
||||
// Create TaskManager instance
|
||||
const taskManager = new TaskManager();
|
||||
|
||||
// Get dashboard element
|
||||
const dashboard = element.querySelector('taskbuffer-dashboard');
|
||||
dashboard.taskManager = taskManager;
|
||||
dashboard.refreshInterval = 1000;
|
||||
|
||||
// Add scheduled tasks
|
||||
const scheduledTask1 = new Task({
|
||||
name: 'HourlyBackup',
|
||||
steps: [
|
||||
{ name: 'prepare', description: 'Preparing backup', percentage: 30 },
|
||||
{ name: 'backup', description: 'Creating backup', percentage: 50 },
|
||||
{ name: 'verify', description: 'Verifying backup', percentage: 20 }
|
||||
],
|
||||
taskFunction: async function() {
|
||||
this.notifyStep('prepare');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
this.notifyStep('backup');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
this.notifyStep('verify');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return 'Backup completed';
|
||||
}
|
||||
});
|
||||
|
||||
const scheduledTask2 = new Task({
|
||||
name: 'DailyReport',
|
||||
steps: [
|
||||
{ name: 'collect', description: 'Collecting data', percentage: 40 },
|
||||
{ name: 'analyze', description: 'Analyzing data', percentage: 40 },
|
||||
{ name: 'send', description: 'Sending report', percentage: 20 }
|
||||
],
|
||||
taskFunction: async function() {
|
||||
this.notifyStep('collect');
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
this.notifyStep('analyze');
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
this.notifyStep('send');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return 'Report sent';
|
||||
}
|
||||
});
|
||||
|
||||
// Schedule tasks
|
||||
taskManager.addAndScheduleTask(scheduledTask1, '0 * * * *'); // Every hour
|
||||
taskManager.addAndScheduleTask(scheduledTask2, '0 0 * * *'); // Daily at midnight
|
||||
|
||||
// Also add them as regular tasks for demo
|
||||
const demoTask = new Task({
|
||||
name: 'DemoScheduledExecution',
|
||||
steps: [
|
||||
{ name: 'execute', description: 'Simulating scheduled execution', percentage: 100 }
|
||||
],
|
||||
taskFunction: async function() {
|
||||
this.notifyStep('execute');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Trigger scheduled tasks for demo
|
||||
scheduledTask1.trigger();
|
||||
scheduledTask2.trigger();
|
||||
|
||||
return 'Triggered scheduled tasks for demo';
|
||||
}
|
||||
});
|
||||
taskManager.addTask(demoTask);
|
||||
|
||||
// Trigger demo after 2 seconds
|
||||
setTimeout(() => {
|
||||
demoTask.trigger();
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<taskbuffer-dashboard></taskbuffer-dashboard>
|
||||
</dees-demowrapper>
|
||||
|
||||
<dees-demowrapper
|
||||
.title=${'Task Execution Control'}
|
||||
.subtitle=${'Interactive controls for task management'}
|
||||
.runAfterRender=${async (element) => {
|
||||
// Create TaskManager instance
|
||||
const taskManager = new TaskManager();
|
||||
|
||||
// Get dashboard element
|
||||
const dashboard = element.querySelector('taskbuffer-dashboard');
|
||||
dashboard.taskManager = taskManager;
|
||||
dashboard.refreshInterval = 300;
|
||||
|
||||
// Add control buttons
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.style.cssText = `
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const createButton = (text, onClick, style = '') => {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = text;
|
||||
button.style.cssText = `
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
${style}
|
||||
`;
|
||||
button.onclick = onClick;
|
||||
return button;
|
||||
};
|
||||
|
||||
let taskCounter = 0;
|
||||
|
||||
// Add task button
|
||||
controlsDiv.appendChild(createButton('Add Task', () => {
|
||||
const task = new Task({
|
||||
name: `Task_${++taskCounter}`,
|
||||
steps: [
|
||||
{ name: 'step1', description: 'Step 1', percentage: 33 },
|
||||
{ name: 'step2', description: 'Step 2', percentage: 33 },
|
||||
{ name: 'step3', description: 'Step 3', percentage: 34 }
|
||||
],
|
||||
taskFunction: async function() {
|
||||
for (const step of ['step1', 'step2', 'step3']) {
|
||||
this.notifyStep(step);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
return `Task_${taskCounter} completed`;
|
||||
}
|
||||
});
|
||||
taskManager.addTask(task);
|
||||
}, 'background: #3b82f6; color: white;'));
|
||||
|
||||
// Trigger all button
|
||||
controlsDiv.appendChild(createButton('Trigger All', () => {
|
||||
const tasks = taskManager.getAllTasksMetadata();
|
||||
tasks.forEach(taskMeta => {
|
||||
const task = taskManager.getTaskByName(taskMeta.name);
|
||||
if (task) {
|
||||
task.trigger();
|
||||
}
|
||||
});
|
||||
}, 'background: #22c55e; color: white;'));
|
||||
|
||||
// Clear all button
|
||||
controlsDiv.appendChild(createButton('Clear All', () => {
|
||||
const tasks = taskManager.getAllTasksMetadata();
|
||||
tasks.forEach(taskMeta => {
|
||||
const task = taskManager.getTaskByName(taskMeta.name);
|
||||
if (task) {
|
||||
taskManager.taskMap.remove(task);
|
||||
taskManager.descheduleTaskByName(taskMeta.name);
|
||||
}
|
||||
});
|
||||
}, 'background: #ef4444; color: white;'));
|
||||
|
||||
element.insertBefore(controlsDiv, dashboard);
|
||||
|
||||
// Add some initial tasks
|
||||
for (let i = 0; i < 3; i++) {
|
||||
controlsDiv.querySelector('button').click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<taskbuffer-dashboard></taskbuffer-dashboard>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
12
ts_web/index.ts
Normal file
12
ts_web/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Export web components
|
||||
export * from './taskbuffer-dashboard.js';
|
||||
|
||||
// Export types from main module for web usage
|
||||
export type {
|
||||
TaskManager,
|
||||
Task,
|
||||
ITaskMetadata,
|
||||
ITaskExecutionReport,
|
||||
IScheduledTaskInfo,
|
||||
ITaskStep
|
||||
} from '../ts/index.js';
|
||||
541
ts_web/taskbuffer-dashboard.ts
Normal file
541
ts_web/taskbuffer-dashboard.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
import { DeesElement, customElement, html, css, property, state, cssManager } from '@design.estate/dees-element';
|
||||
import type { TaskManager, ITaskMetadata, IScheduledTaskInfo } from '../ts/index.js';
|
||||
|
||||
/**
|
||||
* A web component that displays TaskManager tasks with progress visualization
|
||||
*/
|
||||
@customElement('taskbuffer-dashboard')
|
||||
export class TaskbufferDashboard extends DeesElement {
|
||||
// Properties
|
||||
@property({ type: Object })
|
||||
public taskManager: TaskManager | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
public refreshInterval: number = 1000; // milliseconds
|
||||
|
||||
// Internal state
|
||||
@state()
|
||||
private tasks: ITaskMetadata[] = [];
|
||||
|
||||
@state()
|
||||
private scheduledTasks: IScheduledTaskInfo[] = [];
|
||||
|
||||
@state()
|
||||
private isRunning: boolean = false;
|
||||
|
||||
private refreshTimer: any;
|
||||
|
||||
// Styles
|
||||
static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.inactive {
|
||||
background: ${cssManager.bdTheme('#94a3b8', '#475569')};
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.tasks-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tasks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: ${cssManager.bdTheme('#f8fafc', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e2e8f0', '#27272a')};
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#0f172a', '#f1f5f9')};
|
||||
}
|
||||
|
||||
.task-status {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.task-status.idle {
|
||||
background: ${cssManager.bdTheme('#f1f5f9', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
||||
}
|
||||
|
||||
.task-status.running {
|
||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a8a')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
||||
}
|
||||
|
||||
.task-status.completed {
|
||||
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
||||
color: ${cssManager.bdTheme('#15803d', '#86efac')};
|
||||
}
|
||||
|
||||
.task-status.failed {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#7f1d1d')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
|
||||
}
|
||||
|
||||
.task-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
||||
}
|
||||
|
||||
.task-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.task-info-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#475569', '#cbd5e1')};
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: ${cssManager.bdTheme('#e2e8f0', '#27272a')};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6, #6366f1);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.steps-container {
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e2e8f0', '#27272a')};
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.steps-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-indicator.pending {
|
||||
background: ${cssManager.bdTheme('#f1f5f9', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#94a3b8', '#64748b')};
|
||||
border: 2px solid ${cssManager.bdTheme('#cbd5e1', '#3f3f46')};
|
||||
}
|
||||
|
||||
.step-indicator.active {
|
||||
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||
color: white;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.step-indicator.completed {
|
||||
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#1e293b', '#e2e8f0')};
|
||||
}
|
||||
|
||||
.step-description {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.step-percentage {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#94a3b8', '#64748b')};
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
color: ${cssManager.bdTheme('#94a3b8', '#64748b')};
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state-text {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-state-subtext {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#cbd5e1', '#475569')};
|
||||
}
|
||||
|
||||
.scheduled-section {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.schedule-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#64748b', '#94a3b8')};
|
||||
}
|
||||
|
||||
.schedule-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// Lifecycle
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
this.startRefreshing();
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
this.stopRefreshing();
|
||||
}
|
||||
|
||||
// Methods
|
||||
private startRefreshing() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
}
|
||||
|
||||
this.updateData();
|
||||
this.refreshTimer = setInterval(() => {
|
||||
this.updateData();
|
||||
}, this.refreshInterval);
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
private stopRefreshing() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
private updateData() {
|
||||
if (!this.taskManager) {
|
||||
this.tasks = [];
|
||||
this.scheduledTasks = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.tasks = this.taskManager.getAllTasksMetadata();
|
||||
this.scheduledTasks = this.taskManager.getScheduledTasks();
|
||||
}
|
||||
|
||||
private formatNextRun(date: Date): string {
|
||||
const now = new Date();
|
||||
const diff = date.getTime() - now.getTime();
|
||||
|
||||
if (diff < 0) return 'Past due';
|
||||
if (diff < 60000) return 'Less than a minute';
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours`;
|
||||
return `${Math.floor(diff / 86400000)} days`;
|
||||
}
|
||||
|
||||
private formatDuration(ms?: number): string {
|
||||
if (!ms) return '-';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
}
|
||||
|
||||
// Render
|
||||
render() {
|
||||
return html`
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<div class="dashboard-title">
|
||||
<span>TaskBuffer Dashboard</span>
|
||||
<span class="status-indicator ${this.isRunning ? '' : 'inactive'}"></span>
|
||||
</div>
|
||||
<div class="dashboard-subtitle">
|
||||
${this.tasks.length} task${this.tasks.length !== 1 ? 's' : ''} registered
|
||||
${this.scheduledTasks.length > 0 ? ` • ${this.scheduledTasks.length} scheduled` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tasks-section">
|
||||
<h2 class="section-title">Active Tasks</h2>
|
||||
|
||||
${this.tasks.length > 0 ? html`
|
||||
<div class="tasks-grid">
|
||||
${this.tasks.map(task => this.renderTaskCard(task))}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="empty-state">
|
||||
<svg class="empty-state-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<div class="empty-state-text">No tasks registered</div>
|
||||
<div class="empty-state-subtext">Tasks will appear here when added to the TaskManager</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
${this.scheduledTasks.length > 0 ? html`
|
||||
<div class="scheduled-section">
|
||||
<h2 class="section-title">Scheduled Tasks</h2>
|
||||
<div class="tasks-grid">
|
||||
${this.scheduledTasks.map(task => this.renderScheduledTaskCard(task))}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTaskCard(task: ITaskMetadata) {
|
||||
return html`
|
||||
<div class="task-card">
|
||||
<div class="task-header">
|
||||
<div class="task-name">${task.name || 'Unnamed Task'}</div>
|
||||
<div class="task-status ${task.status}">${task.status}</div>
|
||||
</div>
|
||||
|
||||
<div class="task-info">
|
||||
<div class="task-info-item">
|
||||
<svg class="task-info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span>Run ${task.runCount || 0} times</span>
|
||||
</div>
|
||||
${task.buffered ? html`
|
||||
<div class="task-info-item">
|
||||
<svg class="task-info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<span>Buffer: ${task.bufferMax || 0}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${task.steps && task.steps.length > 0 ? html`
|
||||
<div class="progress-container">
|
||||
<div class="progress-label">
|
||||
<span>Progress</span>
|
||||
<span>${task.currentProgress || 0}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${task.currentProgress || 0}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="steps-container">
|
||||
<div class="steps-title">Steps</div>
|
||||
${task.steps.map(step => html`
|
||||
<div class="step-item">
|
||||
<div class="step-indicator ${step.status}">
|
||||
${step.status === 'completed' ? '✓' :
|
||||
step.status === 'active' ? '•' :
|
||||
''}
|
||||
</div>
|
||||
<div class="step-details">
|
||||
<div class="step-name">${step.name}</div>
|
||||
${step.description ? html`
|
||||
<div class="step-description">${step.description}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="step-percentage">${step.percentage}%</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderScheduledTaskCard(task: IScheduledTaskInfo) {
|
||||
return html`
|
||||
<div class="task-card">
|
||||
<div class="task-header">
|
||||
<div class="task-name">${task.name}</div>
|
||||
<div class="task-status idle">scheduled</div>
|
||||
</div>
|
||||
|
||||
<div class="schedule-info">
|
||||
<svg class="schedule-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Next run: ${this.formatNextRun(task.nextRun)}</span>
|
||||
</div>
|
||||
|
||||
<div class="schedule-info">
|
||||
<svg class="schedule-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>Schedule: ${task.schedule}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user