Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
2fb605a58e | |||
ed1e9a08b2 | |||
4d23b3dbfe | |||
9784a5eacf | |||
6c9b975029 | |||
b1725cbdf9 | |||
d54012379c | |||
dc47bc3d2a |
@@ -6,8 +6,8 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Install pnpm and npmci
|
- name: Install pnpm and npmci
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
|
|
||||||
- name: Run npm prepare
|
- name: Run npm prepare
|
||||||
run: npmci npm prepare
|
run: npmci npm prepare
|
||||||
|
@@ -6,8 +6,8 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Code quality
|
- name: Code quality
|
||||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,7 +3,6 @@
|
|||||||
# artifacts
|
# artifacts
|
||||||
coverage/
|
coverage/
|
||||||
public/
|
public/
|
||||||
pages/
|
|
||||||
|
|
||||||
# installs
|
# installs
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -17,4 +16,8 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
# custom
|
# AI
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|
||||||
|
#------# custom
|
Binary file not shown.
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
55
changelog.md
55
changelog.md
@@ -1,6 +1,50 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Implement Task.runTask including preTask/afterTask chaining, touched-task cycle prevention and error handling.
|
||||||
|
- Add Task helpers: extractTask, isTask, isTaskTouched and emptyTaskFunction (resolved promise).
|
||||||
|
- Introduce task lifecycle coordination: finished promise, resolveFinished, and blockingTasks to await dependent tasks.
|
||||||
|
- Support taskSetup/setupValue, execDelay handling, and wait-for-blocking-tasks before execution.
|
||||||
|
- Wire up trigger() to choose buffered vs unbuffered execution (triggerBuffered / triggerUnBuffered) and integrate BufferRunner.
|
||||||
|
- Improve logging and safer promise handling (caught errors are logged).
|
||||||
|
- Update README with extended TypeScript generics examples and expanded buffer behavior and strategies documentation.
|
||||||
|
|
||||||
|
## 2025-08-26 - 3.1.9 - fix(tests)
|
||||||
|
Update CI workflows, fix tests and refresh README/package metadata
|
||||||
|
|
||||||
|
- CI: switch Docker image to code.foss.global/host.today/ht-docker-node:npmci and adjust NPMCI_COMPUTED_REPOURL; replace npmci installer package name from @shipzone/npmci to @ship.zone/npmci in Gitea workflows
|
||||||
|
- Tests: update test imports to use @git.zone/tstest/tapbundle and apply small formatting fixes to test files
|
||||||
|
- Package metadata: update bugs URL and homepage to code.foss.global, add a pnpm.overrides placeholder in package.json
|
||||||
|
- .gitignore: add AI/tooling directories (.claude, .serena) and reorganize custom section
|
||||||
|
- Code style/TS fixes: minor formatting changes across ts sources (trailing commas, line breaks, consistent object/argument commas) and small API surface formatting fixes
|
||||||
|
- Documentation: whitespace/formatting cleanups in README and add changelog entry for 3.1.8
|
||||||
|
|
||||||
## 2025-08-26 - 3.1.8 - fix(tests)
|
## 2025-08-26 - 3.1.8 - fix(tests)
|
||||||
|
|
||||||
Update test runner and imports, refresh README and package metadata, add project tooling/config files
|
Update test runner and imports, refresh README and package metadata, add project tooling/config files
|
||||||
|
|
||||||
- Replaced test imports from '@push.rocks/tapbundle' to '@git.zone/tstest/tapbundle' across test files
|
- Replaced test imports from '@push.rocks/tapbundle' to '@git.zone/tstest/tapbundle' across test files
|
||||||
@@ -11,6 +55,7 @@ Update test runner and imports, refresh README and package metadata, add project
|
|||||||
- Added development/project tooling and metadata files (.claude settings, .serena project/memories) to aid local development and CI
|
- Added development/project tooling and metadata files (.claude settings, .serena project/memories) to aid local development and CI
|
||||||
|
|
||||||
## 2024-05-29 - 3.1.7 - maintenance/config
|
## 2024-05-29 - 3.1.7 - maintenance/config
|
||||||
|
|
||||||
Updated package metadata and build configuration.
|
Updated package metadata and build configuration.
|
||||||
|
|
||||||
- Updated package description.
|
- Updated package description.
|
||||||
@@ -18,36 +63,42 @@ Updated package metadata and build configuration.
|
|||||||
- Updated npmextra.json githost entries (changes across 2024-03-30, 2024-04-01, 2024-04-14).
|
- Updated npmextra.json githost entries (changes across 2024-03-30, 2024-04-01, 2024-04-14).
|
||||||
|
|
||||||
## 2023-08-04 - 3.0.15 - feat(Task)
|
## 2023-08-04 - 3.0.15 - feat(Task)
|
||||||
|
|
||||||
Tasks can now be blocked by other tasks.
|
Tasks can now be blocked by other tasks.
|
||||||
|
|
||||||
- Introduced task blocking support in the Task implementation.
|
- Introduced task blocking support in the Task implementation.
|
||||||
- Release contains related maintenance and patch fixes.
|
- Release contains related maintenance and patch fixes.
|
||||||
|
|
||||||
## 2023-01-07 to 2023-10-20 - 3.0.4..3.1.6 - maintenance
|
## 2023-01-07 to 2023-10-20 - 3.0.4..3.1.6 - maintenance
|
||||||
|
|
||||||
Series of patch releases focused on core fixes and stability.
|
Series of patch releases focused on core fixes and stability.
|
||||||
|
|
||||||
- Numerous core fixes and small adjustments across many patch versions.
|
- Numerous core fixes and small adjustments across many patch versions.
|
||||||
- General maintenance: bug fixes, internal updates and stability improvements.
|
- General maintenance: bug fixes, internal updates and stability improvements.
|
||||||
|
|
||||||
## 2022-03-25 - 2.1.17 - BREAKING(core)
|
## 2022-03-25 - 2.1.17 - BREAKING(core)
|
||||||
|
|
||||||
Switched module format to ESM (breaking).
|
Switched module format to ESM (breaking).
|
||||||
|
|
||||||
- BREAKING CHANGE: project now uses ESM module format.
|
- BREAKING CHANGE: project now uses ESM module format.
|
||||||
- Release includes the version bump and migration to ESM.
|
- Release includes the version bump and migration to ESM.
|
||||||
|
|
||||||
## 2019-11-28 - 2.0.16 - feat(taskrunner)
|
## 2019-11-28 - 2.0.16 - feat(taskrunner)
|
||||||
|
|
||||||
Introduce a working task runner.
|
Introduce a working task runner.
|
||||||
|
|
||||||
- Added/activated a working taskrunner implementation.
|
- Added/activated a working taskrunner implementation.
|
||||||
- Improvements to task execution and orchestration.
|
- Improvements to task execution and orchestration.
|
||||||
|
|
||||||
## 2019-09-05 to 2022-11-14 - 2.0.3..2.1.16 - maintenance
|
## 2019-09-05 to 2022-11-14 - 2.0.3..2.1.16 - maintenance
|
||||||
|
|
||||||
Ongoing maintenance and incremental fixes between 2.0.x and 2.1.x series.
|
Ongoing maintenance and incremental fixes between 2.0.x and 2.1.x series.
|
||||||
|
|
||||||
- Multiple fixes labeled as core maintenance updates.
|
- Multiple fixes labeled as core maintenance updates.
|
||||||
- CI, packaging and small doc/test fixes rolled out across these releases.
|
- CI, packaging and small doc/test fixes rolled out across these releases.
|
||||||
|
|
||||||
## 2018-08-04 - 2.0.0 - major
|
## 2018-08-04 - 2.0.0 - major
|
||||||
|
|
||||||
Major release and scope change with CI/test updates.
|
Major release and scope change with CI/test updates.
|
||||||
|
|
||||||
- Released 2.0.0 with updated docs.
|
- Released 2.0.0 with updated docs.
|
||||||
@@ -55,6 +106,7 @@ Major release and scope change with CI/test updates.
|
|||||||
- CI and testing updates (moved to new tstest), package.json adjustments.
|
- CI and testing updates (moved to new tstest), package.json adjustments.
|
||||||
|
|
||||||
## 2017-07-12 - 1.0.21 - enhancements
|
## 2017-07-12 - 1.0.21 - enhancements
|
||||||
|
|
||||||
Feature additions around task utilities and manager.
|
Feature additions around task utilities and manager.
|
||||||
|
|
||||||
- Introduced TaskOnce.
|
- Introduced TaskOnce.
|
||||||
@@ -63,18 +115,21 @@ Feature additions around task utilities and manager.
|
|||||||
- Documentation and test improvements.
|
- Documentation and test improvements.
|
||||||
|
|
||||||
## 2016-08-03 - 1.0.6 - types
|
## 2016-08-03 - 1.0.6 - types
|
||||||
|
|
||||||
Type and promise improvements.
|
Type and promise improvements.
|
||||||
|
|
||||||
- Now returns correct Promise types.
|
- Now returns correct Promise types.
|
||||||
- Dependency and typings updates.
|
- Dependency and typings updates.
|
||||||
|
|
||||||
## 2016-08-01 - 1.0.0 - stable
|
## 2016-08-01 - 1.0.0 - stable
|
||||||
|
|
||||||
First stable 1.0.0 release.
|
First stable 1.0.0 release.
|
||||||
|
|
||||||
- Exported public interfaces.
|
- Exported public interfaces.
|
||||||
- Base API stabilized for 1.x line.
|
- Base API stabilized for 1.x line.
|
||||||
|
|
||||||
## 2016-05-15 to 2016-05-06 - 0.1.0..0.0.5 - initial features
|
## 2016-05-15 to 2016-05-06 - 0.1.0..0.0.5 - initial features
|
||||||
|
|
||||||
Initial implementation of core task primitives and utilities.
|
Initial implementation of core task primitives and utilities.
|
||||||
|
|
||||||
- Added Taskparallel class to execute tasks in parallel.
|
- Added Taskparallel class to execute tasks in parallel.
|
||||||
|
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>
|
20
package.json
20
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/taskbuffer",
|
"name": "@push.rocks/taskbuffer",
|
||||||
"version": "3.1.8",
|
"version": "3.4.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.",
|
"description": "A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
||||||
"build": "(tsbuild --web && tsbundle npm)",
|
"build": "(tsbuild tsfolders)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -30,23 +30,24 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://gitlab.com/pushrocks/taskbuffer/issues"
|
"url": "https://code.foss.global/push.rocks/taskbuffer/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://code.foss.global/push.rocks/taskbuffer",
|
"homepage": "https://code.foss.global/push.rocks/taskbuffer#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@design.estate/dees-element": "^2.1.2",
|
||||||
"@push.rocks/lik": "^6.0.5",
|
"@push.rocks/lik": "^6.0.5",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartlog": "^3.0.3",
|
"@push.rocks/smartlog": "^3.1.9",
|
||||||
"@push.rocks/smartpromise": "^4.0.3",
|
"@push.rocks/smartpromise": "^4.0.3",
|
||||||
"@push.rocks/smartrx": "^3.0.6",
|
"@push.rocks/smartrx": "^3.0.6",
|
||||||
"@push.rocks/smarttime": "^4.0.6",
|
"@push.rocks/smarttime": "^4.0.6",
|
||||||
"@push.rocks/smartunique": "^3.0.6"
|
"@push.rocks/smartunique": "^3.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.66",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsbundle": "^2.0.8",
|
"@git.zone/tsbundle": "^2.0.8",
|
||||||
"@git.zone/tsrun": "^1.2.44",
|
"@git.zone/tsrun": "^1.2.44",
|
||||||
"@git.zone/tstest": "^2.3.5",
|
"@git.zone/tstest": "^2.3.6",
|
||||||
"@types/node": "^20.8.7"
|
"@types/node": "^20.8.7"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -64,5 +65,8 @@
|
|||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
],
|
],
|
||||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
|
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1288
pnpm-lock.yaml
generated
1288
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
778
readme.md
778
readme.md
@@ -1,95 +1,121 @@
|
|||||||
# @push.rocks/taskbuffer 🚀
|
# @push.rocks/taskbuffer 🚀
|
||||||
|
|
||||||
A **powerful**, **flexible**, and **TypeScript-first** task management library for orchestrating asynchronous operations with style. From simple task execution to complex distributed workflows, taskbuffer has got you covered.
|
> **Modern TypeScript task orchestration with smart buffering, scheduling, and real-time progress tracking**
|
||||||
|
|
||||||
## Install 📦
|
[](https://www.npmjs.com/package/@push.rocks/taskbuffer)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
```bash
|
[](https://opensource.org/licenses/MIT)
|
||||||
npm install @push.rocks/taskbuffer --save
|
|
||||||
```
|
## 🌟 Features
|
||||||
|
|
||||||
Or with **pnpm** (recommended):
|
- **🎯 Type-Safe Task Management** - Full TypeScript support with generics and type inference
|
||||||
|
- **📊 Real-Time Progress Tracking** - Step-based progress with percentage weights
|
||||||
|
- **⚡ Smart Buffering** - Intelligent request debouncing and batching
|
||||||
|
- **⏰ Cron Scheduling** - Schedule tasks with cron expressions
|
||||||
|
- **🔄 Task Chains & Parallel Execution** - Sequential and parallel task orchestration
|
||||||
|
- **🎨 Web Component Dashboard** - Real-time visualization of task execution
|
||||||
|
- **📈 Comprehensive Metadata** - Track execution history, duration, and status
|
||||||
|
- **🔒 Thread-Safe Operations** - Concurrency control and execution limits
|
||||||
|
- **🎭 Event-Driven Architecture** - Observable task lifecycle events
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
npm install @push.rocks/taskbuffer
|
||||||
|
# or
|
||||||
pnpm add @push.rocks/taskbuffer
|
pnpm add @push.rocks/taskbuffer
|
||||||
|
# or
|
||||||
|
yarn add @push.rocks/taskbuffer
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why taskbuffer? 🤔
|
## 🚀 Quick Start
|
||||||
|
|
||||||
In the modern JavaScript ecosystem, managing asynchronous tasks efficiently is crucial. Whether you're building a data pipeline, managing API rate limits, or orchestrating complex workflows, **@push.rocks/taskbuffer** provides the tools you need:
|
### Basic Task Creation
|
||||||
|
|
||||||
- **🎯 TypeScript-first**: Built with TypeScript for TypeScript - enjoy complete type safety and excellent IDE support
|
|
||||||
- **⚡ Flexible execution**: From simple tasks to complex parallel workflows with dependencies
|
|
||||||
- **🔄 Smart buffering**: Control concurrent executions with intelligent buffer management
|
|
||||||
- **⏰ Built-in scheduling**: Cron-based task scheduling without additional dependencies
|
|
||||||
- **🎭 Multiple paradigms**: Support for debounced, throttled, and one-time execution patterns
|
|
||||||
- **🔌 Extensible**: Clean architecture that's easy to extend and customize
|
|
||||||
- **🏃 Zero dependencies on external schedulers**: Everything you need is included
|
|
||||||
|
|
||||||
## Core Concepts 🎓
|
|
||||||
|
|
||||||
### Task
|
|
||||||
The fundamental unit of work. A task wraps an asynchronous function and provides powerful execution control.
|
|
||||||
|
|
||||||
### Taskchain
|
|
||||||
Sequential task execution - tasks run one after another, with results passed along the chain.
|
|
||||||
|
|
||||||
### Taskparallel
|
|
||||||
Parallel task execution - multiple tasks run simultaneously for maximum performance.
|
|
||||||
|
|
||||||
### TaskManager
|
|
||||||
Centralized task scheduling and management using cron expressions.
|
|
||||||
|
|
||||||
### TaskDebounced
|
|
||||||
Debounced task execution - prevents rapid repeated executions, only running after a quiet period.
|
|
||||||
|
|
||||||
### TaskOnce
|
|
||||||
Singleton task execution - ensures a task runs exactly once, perfect for initialization routines.
|
|
||||||
|
|
||||||
## Quick Start 🏁
|
|
||||||
|
|
||||||
### Basic Task Execution
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Task } from '@push.rocks/taskbuffer';
|
import { Task, TaskManager } from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
// Create a simple task
|
// Create a simple task
|
||||||
const myTask = new Task({
|
const dataProcessor = new Task({
|
||||||
name: 'DataProcessor',
|
name: 'ProcessData',
|
||||||
taskFunction: async () => {
|
taskFunction: async (data) => {
|
||||||
const data = await fetchData();
|
console.log(`Processing: ${data}`);
|
||||||
return processData(data);
|
// Your async logic here
|
||||||
|
return `Processed: ${data}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Execute the task
|
// Execute the task
|
||||||
const result = await myTask.trigger();
|
const result = await dataProcessor.trigger('my-data');
|
||||||
|
console.log(result); // "Processed: my-data"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Buffered Execution (Rate Limiting)
|
### Tasks with Progress Tracking 📊
|
||||||
|
|
||||||
Perfect for API calls or database operations that need throttling:
|
```typescript
|
||||||
|
const deploymentTask = new Task({
|
||||||
|
name: 'DeployApplication',
|
||||||
|
steps: [
|
||||||
|
{ name: 'build', description: 'Building application', percentage: 30 },
|
||||||
|
{ name: 'test', description: 'Running tests', percentage: 20 },
|
||||||
|
{ name: 'deploy', description: 'Deploying to server', percentage: 40 },
|
||||||
|
{ name: 'verify', description: 'Verifying deployment', percentage: 10 }
|
||||||
|
] as const, // Use 'as const' for type inference
|
||||||
|
taskFunction: async function() {
|
||||||
|
// TypeScript knows these step names!
|
||||||
|
this.notifyStep('build');
|
||||||
|
await buildApplication();
|
||||||
|
|
||||||
|
this.notifyStep('test');
|
||||||
|
await runTests();
|
||||||
|
|
||||||
|
this.notifyStep('deploy');
|
||||||
|
await deployToServer();
|
||||||
|
|
||||||
|
this.notifyStep('verify');
|
||||||
|
await verifyDeployment();
|
||||||
|
|
||||||
|
return 'Deployment successful!';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor progress
|
||||||
|
console.log(deploymentTask.getProgress()); // 0-100
|
||||||
|
console.log(deploymentTask.getStepsMetadata()); // Detailed step info
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Core Concepts
|
||||||
|
|
||||||
|
### Task Buffering - Intelligent Request Management
|
||||||
|
|
||||||
|
TaskBuffer's buffering system prevents overwhelming your system with rapid-fire requests:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const apiTask = new Task({
|
const apiTask = new Task({
|
||||||
name: 'APICall',
|
name: 'APIRequest',
|
||||||
taskFunction: async (endpoint: string) => {
|
taskFunction: async (endpoint) => {
|
||||||
return await fetch(endpoint);
|
return await fetch(endpoint).then(r => r.json());
|
||||||
},
|
},
|
||||||
buffered: true,
|
buffered: true,
|
||||||
bufferMax: 3, // Maximum 3 concurrent executions
|
bufferMax: 5, // Maximum 5 concurrent executions
|
||||||
execDelay: 1000 // Wait 1 second between executions
|
execDelay: 100 // Minimum 100ms between executions
|
||||||
});
|
});
|
||||||
|
|
||||||
// These will be automatically throttled
|
// Rapid fire 100 calls - only 5 will execute concurrently
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
apiTask.trigger(`/api/data/${i}`);
|
apiTask.trigger(`/api/data/${i}`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Buffer Behavior:**
|
||||||
|
- First `bufferMax` calls execute immediately
|
||||||
|
- Additional calls are queued
|
||||||
|
- When buffer is full, new calls overwrite the last queued item
|
||||||
|
- Perfect for real-time data streams where only recent data matters
|
||||||
|
|
||||||
### Task Chains - Sequential Workflows
|
### Task Chains - Sequential Workflows
|
||||||
|
|
||||||
Build complex workflows where each step depends on the previous:
|
Build complex workflows with automatic data flow:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Task, Taskchain } from '@push.rocks/taskbuffer';
|
import { Task, Taskchain } from '@push.rocks/taskbuffer';
|
||||||
@@ -107,7 +133,7 @@ const transformTask = new Task({
|
|||||||
taskFunction: async (data) => {
|
taskFunction: async (data) => {
|
||||||
return data.map(item => ({
|
return data.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
processed: true,
|
transformed: true,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -116,302 +142,328 @@ const transformTask = new Task({
|
|||||||
const saveTask = new Task({
|
const saveTask = new Task({
|
||||||
name: 'SaveData',
|
name: 'SaveData',
|
||||||
taskFunction: async (transformedData) => {
|
taskFunction: async (transformedData) => {
|
||||||
await database.bulkInsert(transformedData);
|
await database.save(transformedData);
|
||||||
return { saved: transformedData.length };
|
return transformedData.length;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflow = new Taskchain({
|
// Create and execute chain
|
||||||
|
const dataChain = new Taskchain({
|
||||||
name: 'DataPipeline',
|
name: 'DataPipeline',
|
||||||
taskArray: [fetchTask, transformTask, saveTask]
|
tasks: [fetchTask, transformTask, saveTask]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Execute the entire chain
|
const savedCount = await dataChain.trigger();
|
||||||
const result = await workflow.trigger();
|
console.log(`Saved ${savedCount} items`);
|
||||||
console.log(`Processed ${result.saved} items`);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parallel Execution - Maximum Performance
|
### Parallel Execution - Concurrent Processing
|
||||||
|
|
||||||
Execute multiple independent tasks simultaneously:
|
Execute multiple tasks simultaneously:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Task, Taskparallel } from '@push.rocks/taskbuffer';
|
import { TaskParallel } from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
const tasks = ['user', 'posts', 'comments'].map(resource =>
|
const parallel = new TaskParallel({
|
||||||
new Task({
|
name: 'ParallelProcessor',
|
||||||
name: `Fetch${resource}`,
|
tasks: [
|
||||||
taskFunction: async () => {
|
emailTask,
|
||||||
const data = await fetch(`/api/${resource}`);
|
smsTask,
|
||||||
return data.json();
|
pushNotificationTask,
|
||||||
|
webhookTask
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// All tasks execute concurrently
|
||||||
|
const results = await parallel.trigger(notificationData);
|
||||||
|
// results = [emailResult, smsResult, pushResult, webhookResult]
|
||||||
|
```
|
||||||
|
|
||||||
|
### TaskManager - Centralized Orchestration
|
||||||
|
|
||||||
|
Manage all your tasks from a single point:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const taskManager = new TaskManager();
|
||||||
|
|
||||||
|
// Add tasks
|
||||||
|
taskManager.addTask(dataProcessor);
|
||||||
|
taskManager.addTask(deploymentTask);
|
||||||
|
|
||||||
|
// Schedule tasks with cron
|
||||||
|
taskManager.addAndScheduleTask(backupTask, '0 2 * * *'); // Daily at 2 AM
|
||||||
|
taskManager.addAndScheduleTask(healthCheck, '*/5 * * * *'); // Every 5 minutes
|
||||||
|
|
||||||
|
// Get task metadata
|
||||||
|
const metadata = taskManager.getTaskMetadata('DeployApplication');
|
||||||
|
console.log(metadata);
|
||||||
|
// {
|
||||||
|
// name: 'DeployApplication',
|
||||||
|
// status: 'idle' | 'running' | 'completed' | 'failed',
|
||||||
|
// steps: [...],
|
||||||
|
// currentProgress: 75,
|
||||||
|
// runCount: 12,
|
||||||
|
// lastRun: Date,
|
||||||
|
// buffered: false,
|
||||||
|
// bufferMax: undefined,
|
||||||
|
// version: '1.0.0',
|
||||||
|
// timeout: 30000
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Get all scheduled tasks
|
||||||
|
const scheduled = taskManager.getScheduledTasks();
|
||||||
|
scheduled.forEach(task => {
|
||||||
|
console.log(`${task.name}: Next run at ${task.nextRun}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute and remove pattern
|
||||||
|
const report = await taskManager.addExecuteRemoveTask(temporaryTask, {
|
||||||
|
trackProgress: true
|
||||||
|
});
|
||||||
|
console.log(report);
|
||||||
|
// {
|
||||||
|
// taskName: 'TempTask',
|
||||||
|
// startTime: Date,
|
||||||
|
// endTime: Date,
|
||||||
|
// duration: 1523,
|
||||||
|
// steps: [...],
|
||||||
|
// stepsCompleted: ['step1', 'step2'],
|
||||||
|
// progress: 100,
|
||||||
|
// result: any,
|
||||||
|
// error?: Error
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Web Component Dashboard
|
||||||
|
|
||||||
|
Visualize your tasks in real-time with the included web component:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="module">
|
||||||
|
import { TaskManager } from '@push.rocks/taskbuffer';
|
||||||
|
import '@push.rocks/taskbuffer/dist_ts_web/taskbuffer-dashboard.js';
|
||||||
|
|
||||||
|
const taskManager = new TaskManager();
|
||||||
|
|
||||||
|
// Attach to dashboard
|
||||||
|
const dashboard = document.querySelector('taskbuffer-dashboard');
|
||||||
|
dashboard.taskManager = taskManager;
|
||||||
|
dashboard.refreshInterval = 500; // Update every 500ms
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<taskbuffer-dashboard></taskbuffer-dashboard>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
The dashboard provides:
|
||||||
|
- 📊 Real-time progress bars with step indicators
|
||||||
|
- 📈 Task execution history
|
||||||
|
- ⏰ Scheduled task information
|
||||||
|
- 🎯 Interactive task controls
|
||||||
|
- 🌓 Light/dark theme support
|
||||||
|
|
||||||
|
## 🧩 Advanced Patterns
|
||||||
|
|
||||||
|
### Dynamic Task Routing
|
||||||
|
|
||||||
|
Route tasks based on conditions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const routerTask = new Task({
|
||||||
|
name: 'Router',
|
||||||
|
taskFunction: async (request) => {
|
||||||
|
if (request.priority === 'high') {
|
||||||
|
return await expressProcessor.trigger(request);
|
||||||
|
} else if (request.size > 1000000) {
|
||||||
|
return await bulkProcessor.trigger(request);
|
||||||
|
} else {
|
||||||
|
return await standardProcessor.trigger(request);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const parallelFetch = new Taskparallel({
|
|
||||||
taskArray: tasks
|
|
||||||
});
|
|
||||||
|
|
||||||
// All tasks execute simultaneously
|
|
||||||
const [users, posts, comments] = await parallelFetch.trigger();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scheduled Tasks with TaskManager
|
|
||||||
|
|
||||||
Run tasks on a schedule using cron expressions:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Task, TaskManager } from '@push.rocks/taskbuffer';
|
|
||||||
|
|
||||||
const backupTask = new Task({
|
|
||||||
name: 'DatabaseBackup',
|
|
||||||
taskFunction: async () => {
|
|
||||||
await performBackup();
|
|
||||||
console.log(`Backup completed at ${new Date().toISOString()}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const manager = new TaskManager();
|
|
||||||
|
|
||||||
// Add and schedule tasks
|
|
||||||
manager.addAndScheduleTask(backupTask, '0 0 * * *'); // Daily at midnight
|
|
||||||
manager.addAndScheduleTask(healthCheck, '*/5 * * * *'); // Every 5 minutes
|
|
||||||
|
|
||||||
// Start the scheduler
|
|
||||||
manager.start();
|
|
||||||
|
|
||||||
// Later... stop if needed
|
|
||||||
manager.stop();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debounced Tasks - Smart Throttling
|
### Task Pools
|
||||||
|
|
||||||
Prevent task spam with intelligent debouncing:
|
Create reusable task pools for load distribution:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { TaskDebounced } from '@push.rocks/taskbuffer';
|
class TaskPool {
|
||||||
|
private tasks: Task[] = [];
|
||||||
|
private currentIndex = 0;
|
||||||
|
|
||||||
const saveTask = new TaskDebounced({
|
constructor(poolSize: number, taskConfig: any) {
|
||||||
name: 'AutoSave',
|
for (let i = 0; i < poolSize; i++) {
|
||||||
taskFunction: async (content: string) => {
|
this.tasks.push(new Task({
|
||||||
await saveToDatabase(content);
|
...taskConfig,
|
||||||
console.log('Content saved');
|
name: `${taskConfig.name}_${i}`
|
||||||
},
|
}));
|
||||||
debounceTimeInMillis: 2000 // Wait 2 seconds of inactivity
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Rapid calls will be debounced
|
async execute(data: any) {
|
||||||
input.addEventListener('input', (e) => {
|
const task = this.tasks[this.currentIndex];
|
||||||
saveTask.trigger(e.target.value);
|
this.currentIndex = (this.currentIndex + 1) % this.tasks.length;
|
||||||
|
return await task.trigger(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processorPool = new TaskPool(5, {
|
||||||
|
name: 'DataProcessor',
|
||||||
|
taskFunction: async (data) => processData(data)
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### One-Time Tasks - Initialize Once
|
### Error Recovery & Retry
|
||||||
|
|
||||||
Ensure initialization code runs exactly once:
|
Implement robust error handling:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { TaskOnce } from '@push.rocks/taskbuffer';
|
const resilientTask = new Task({
|
||||||
|
name: 'ResilientTask',
|
||||||
const initTask = new TaskOnce({
|
taskFunction: async (data, retryCount = 0) => {
|
||||||
name: 'SystemInitialization',
|
try {
|
||||||
taskFunction: async () => {
|
return await riskyOperation(data);
|
||||||
await database.connect();
|
} catch (error) {
|
||||||
await cache.initialize();
|
if (retryCount < 3) {
|
||||||
await loadConfiguration();
|
console.log(`Retry ${retryCount + 1}/3`);
|
||||||
console.log('System initialized');
|
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, retryCount)));
|
||||||
|
return await resilientTask.trigger(data, retryCount + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Safe to call multiple times - only runs once
|
|
||||||
await initTask.trigger();
|
|
||||||
await initTask.trigger(); // This won't run again
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced Features 🔥
|
### Task Composition
|
||||||
|
|
||||||
### Task Dependencies with Pre/Post Hooks
|
Compose complex behaviors from simple tasks:
|
||||||
|
|
||||||
Create sophisticated task relationships:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const validationTask = new Task({
|
const compositeTask = new Task({
|
||||||
name: 'ValidateInput',
|
name: 'CompositeOperation',
|
||||||
taskFunction: async (data) => {
|
steps: [
|
||||||
if (!isValid(data)) {
|
{ name: 'validate', description: 'Validating input', percentage: 20 },
|
||||||
throw new Error('Validation failed');
|
{ name: 'process', description: 'Processing data', percentage: 60 },
|
||||||
}
|
{ name: 'notify', description: 'Sending notifications', percentage: 20 }
|
||||||
return data;
|
] as const,
|
||||||
|
taskFunction: async function(data) {
|
||||||
|
this.notifyStep('validate');
|
||||||
|
const validated = await validationTask.trigger(data);
|
||||||
|
|
||||||
|
this.notifyStep('process');
|
||||||
|
const processed = await processingTask.trigger(validated);
|
||||||
|
|
||||||
|
this.notifyStep('notify');
|
||||||
|
await notificationTask.trigger(processed);
|
||||||
|
|
||||||
|
return processed;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainTask = new Task({
|
|
||||||
name: 'ProcessData',
|
|
||||||
taskFunction: async (data) => {
|
|
||||||
return await complexProcessing(data);
|
|
||||||
},
|
|
||||||
preTask: validationTask, // Runs before main task
|
|
||||||
afterTask: cleanupTask // Runs after main task
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Task Runners - Distributed Execution
|
## 🔧 Configuration
|
||||||
|
|
||||||
The TaskRunner system enables distributed task execution across multiple workers:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { TaskRunner } from '@push.rocks/taskbuffer';
|
|
||||||
|
|
||||||
const runner = new TaskRunner({
|
|
||||||
name: 'WorkerNode1',
|
|
||||||
maxConcurrentTasks: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register tasks this runner can handle
|
|
||||||
runner.registerTask(dataProcessingTask);
|
|
||||||
runner.registerTask(imageResizeTask);
|
|
||||||
|
|
||||||
// Start processing
|
|
||||||
runner.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Buffer Management Strategies
|
|
||||||
|
|
||||||
Fine-tune concurrent execution behavior:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const task = new Task({
|
|
||||||
name: 'ResourceIntensive',
|
|
||||||
taskFunction: async () => { /* ... */ },
|
|
||||||
buffered: true,
|
|
||||||
bufferMax: 5, // Max 5 concurrent
|
|
||||||
execDelay: 100, // 100ms between starts
|
|
||||||
timeout: 30000 // 30 second timeout
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cycle Detection and Prevention
|
|
||||||
|
|
||||||
TaskBuffer automatically detects and prevents circular dependencies:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const taskA = new Task({
|
|
||||||
name: 'TaskA',
|
|
||||||
taskFunction: async () => { /* ... */ },
|
|
||||||
preTask: taskB // This would create a cycle
|
|
||||||
});
|
|
||||||
|
|
||||||
const taskB = new Task({
|
|
||||||
name: 'TaskB',
|
|
||||||
taskFunction: async () => { /* ... */ },
|
|
||||||
preTask: taskA // Circular dependency detected!
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dynamic Task Creation
|
|
||||||
|
|
||||||
Create tasks on-the-fly based on runtime conditions:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const dynamicWorkflow = async (config: Config) => {
|
|
||||||
const tasks = config.steps.map(step =>
|
|
||||||
new Task({
|
|
||||||
name: step.name,
|
|
||||||
taskFunction: async (input) => {
|
|
||||||
return await processStep(step, input);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const chain = new Taskchain({
|
|
||||||
name: 'DynamicWorkflow',
|
|
||||||
taskArray: tasks
|
|
||||||
});
|
|
||||||
|
|
||||||
return await chain.trigger();
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Reference 📚
|
|
||||||
|
|
||||||
### Task Options
|
### Task Options
|
||||||
|
|
||||||
| Option | Type | Description |
|
```typescript
|
||||||
|--------|------|-------------|
|
interface TaskOptions<T = undefined, TSteps = []> {
|
||||||
| `name` | `string` | Unique identifier for the task |
|
name?: string; // Task identifier
|
||||||
| `taskFunction` | `Function` | Async function to execute |
|
taskFunction: Function; // Async function to execute
|
||||||
| `buffered` | `boolean` | Enable buffer management |
|
buffered?: boolean; // Enable buffering
|
||||||
| `bufferMax` | `number` | Maximum concurrent executions |
|
bufferMax?: number; // Max concurrent executions
|
||||||
| `execDelay` | `number` | Delay between executions (ms) |
|
execDelay?: number; // Min delay between executions
|
||||||
| `timeout` | `number` | Task timeout (ms) |
|
timeout?: number; // Task timeout in ms
|
||||||
| `preTask` | `Task` | Task to run before |
|
version?: string; // Task version
|
||||||
| `afterTask` | `Task` | Task to run after |
|
steps?: TSteps; // Progress steps configuration
|
||||||
|
taskSetup?: Function; // One-time setup function
|
||||||
|
beforeTask?: Function; // Runs before each execution
|
||||||
|
afterTask?: Function; // Runs after each execution
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### TaskManager Methods
|
### TaskManager Options
|
||||||
|
|
||||||
| Method | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `addTask(task, cronExpression)` | Add and schedule a task |
|
|
||||||
| `removeTask(taskName)` | Remove a scheduled task |
|
|
||||||
| `start()` | Start the scheduler |
|
|
||||||
| `stop()` | Stop the scheduler |
|
|
||||||
| `getStats()` | Get execution statistics |
|
|
||||||
|
|
||||||
### Taskchain Methods
|
|
||||||
|
|
||||||
| Method | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `addTask(task)` | Add task to chain |
|
|
||||||
| `removeTask(taskName)` | Remove task from chain |
|
|
||||||
| `trigger(initialValue)` | Execute the chain |
|
|
||||||
| `reset()` | Reset chain state |
|
|
||||||
|
|
||||||
## Performance Tips 🏎️
|
|
||||||
|
|
||||||
1. **Use buffering for I/O operations**: Prevents overwhelming external services
|
|
||||||
2. **Leverage parallel execution**: When tasks are independent, run them simultaneously
|
|
||||||
3. **Implement proper error handling**: Use try-catch in task functions
|
|
||||||
4. **Monitor task execution**: Use the built-in stats and logging
|
|
||||||
5. **Set appropriate timeouts**: Prevent hanging tasks from blocking your system
|
|
||||||
|
|
||||||
## Error Handling 🛡️
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const robustTask = new Task({
|
const taskManager = new TaskManager({
|
||||||
name: 'RobustOperation',
|
maxConcurrentTasks: 10, // Global concurrency limit
|
||||||
taskFunction: async (input) => {
|
defaultTimeout: 30000, // Default task timeout
|
||||||
try {
|
logLevel: 'info' // Logging verbosity
|
||||||
return await riskyOperation(input);
|
|
||||||
} catch (error) {
|
|
||||||
// Log error
|
|
||||||
console.error(`Task failed: ${error.message}`);
|
|
||||||
|
|
||||||
// Optionally retry
|
|
||||||
if (error.retryable) {
|
|
||||||
return await riskyOperation(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Or return default value
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
timeout: 5000 // Fail if takes longer than 5 seconds
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Real-World Examples 🌍
|
## 📊 Monitoring & Observability
|
||||||
|
|
||||||
### API Rate Limiting
|
### Task Events
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const apiClient = new Task({
|
task.on('started', () => console.log('Task started'));
|
||||||
name: 'RateLimitedAPI',
|
task.on('completed', (result) => console.log('Task completed:', result));
|
||||||
taskFunction: async (endpoint: string) => {
|
task.on('failed', (error) => console.error('Task failed:', error));
|
||||||
return await fetch(`https://api.example.com${endpoint}`);
|
task.on('stepChange', (step) => console.log('Step:', step.name));
|
||||||
},
|
```
|
||||||
|
|
||||||
|
### Execution Metrics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const metrics = task.getMetrics();
|
||||||
|
console.log({
|
||||||
|
totalRuns: metrics.runCount,
|
||||||
|
averageDuration: metrics.avgDuration,
|
||||||
|
successRate: metrics.successRate,
|
||||||
|
lastError: metrics.lastError
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { expect, tap } from '@git.zone/tstest';
|
||||||
|
import { Task } from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
|
tap.test('Task should execute with progress tracking', async () => {
|
||||||
|
const task = new Task({
|
||||||
|
name: 'TestTask',
|
||||||
|
steps: [
|
||||||
|
{ name: 'step1', description: 'Step 1', percentage: 50 },
|
||||||
|
{ name: 'step2', description: 'Step 2', percentage: 50 }
|
||||||
|
] as const,
|
||||||
|
taskFunction: async function() {
|
||||||
|
this.notifyStep('step1');
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
this.notifyStep('step2');
|
||||||
|
return 'done';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await task.trigger();
|
||||||
|
expect(result).toEqual('done');
|
||||||
|
expect(task.getProgress()).toEqual(100);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Real-World Examples
|
||||||
|
|
||||||
|
### API Rate Limiter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const apiLimiter = new Task({
|
||||||
|
name: 'APIRateLimiter',
|
||||||
buffered: true,
|
buffered: true,
|
||||||
bufferMax: 10, // 10 requests
|
bufferMax: 10, // Max 10 requests per second
|
||||||
execDelay: 100 // Per 100ms = 100 req/s max
|
execDelay: 100, // 100ms between requests
|
||||||
|
taskFunction: async (endpoint, data) => {
|
||||||
|
return await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -420,59 +472,115 @@ const apiClient = new Task({
|
|||||||
```typescript
|
```typescript
|
||||||
const migrationChain = new Taskchain({
|
const migrationChain = new Taskchain({
|
||||||
name: 'DatabaseMigration',
|
name: 'DatabaseMigration',
|
||||||
taskArray: [
|
tasks: [
|
||||||
backupTask,
|
backupDatabaseTask,
|
||||||
schemaUpdateTask,
|
validateSchemaTask,
|
||||||
dataTransformTask,
|
runMigrationsTask,
|
||||||
validationTask,
|
verifyIntegrityTask,
|
||||||
cleanupTask
|
updateIndexesTask
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Execute with rollback on failure
|
||||||
|
try {
|
||||||
|
await migrationChain.trigger();
|
||||||
|
console.log('Migration successful!');
|
||||||
|
} catch (error) {
|
||||||
|
await rollbackTask.trigger();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Microservice Health Monitoring
|
### Distributed Job Queue
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const healthMonitor = new TaskManager();
|
const jobQueue = new TaskManager();
|
||||||
|
|
||||||
services.forEach(service => {
|
// Worker tasks
|
||||||
const healthCheck = new Task({
|
const imageProcessor = new Task({
|
||||||
name: `HealthCheck:${service.name}`,
|
name: 'ImageProcessor',
|
||||||
taskFunction: async () => {
|
buffered: true,
|
||||||
const healthy = await checkHealth(service.url);
|
bufferMax: 5,
|
||||||
if (!healthy) {
|
steps: [
|
||||||
await alertOps(service);
|
{ name: 'download', description: 'Downloading', percentage: 20 },
|
||||||
}
|
{ name: 'resize', description: 'Resizing', percentage: 40 },
|
||||||
|
{ name: 'optimize', description: 'Optimizing', percentage: 30 },
|
||||||
|
{ name: 'upload', description: 'Uploading', percentage: 10 }
|
||||||
|
] as const,
|
||||||
|
taskFunction: async function(job) {
|
||||||
|
this.notifyStep('download');
|
||||||
|
const image = await downloadImage(job.url);
|
||||||
|
|
||||||
|
this.notifyStep('resize');
|
||||||
|
const resized = await resizeImage(image, job.dimensions);
|
||||||
|
|
||||||
|
this.notifyStep('optimize');
|
||||||
|
const optimized = await optimizeImage(resized);
|
||||||
|
|
||||||
|
this.notifyStep('upload');
|
||||||
|
return await uploadToCDN(optimized);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
healthMonitor.addAndScheduleTask(healthCheck, '*/1 * * * *'); // Every minute
|
jobQueue.addTask(imageProcessor);
|
||||||
|
|
||||||
|
// Process incoming jobs
|
||||||
|
messageQueue.on('job', async (job) => {
|
||||||
|
const result = await jobQueue.getTaskByName('ImageProcessor').trigger(job);
|
||||||
|
await messageQueue.ack(job.id, result);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing 🧪
|
## 🚀 Performance Tips
|
||||||
|
|
||||||
|
1. **Use Buffering Wisely** - Enable buffering for I/O-bound tasks
|
||||||
|
2. **Set Appropriate Delays** - Use `execDelay` to prevent API rate limits
|
||||||
|
3. **Leverage Task Pools** - Distribute load across multiple task instances
|
||||||
|
4. **Monitor Progress** - Use step tracking for long-running operations
|
||||||
|
5. **Clean Up** - Use `addExecuteRemoveTask` for one-time operations
|
||||||
|
|
||||||
|
## 🔍 Debugging
|
||||||
|
|
||||||
|
Enable detailed logging:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { expect, tap } from '@git.zone/tstest';
|
import { logger } from '@push.rocks/smartlog';
|
||||||
import { Task } from '@push.rocks/taskbuffer';
|
|
||||||
|
|
||||||
tap.test('should execute task successfully', async () => {
|
logger.enableConsole();
|
||||||
const result = await myTask.trigger();
|
logger.level = 'debug';
|
||||||
expect(result).toEqual(expectedValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
// Tasks will now output detailed execution logs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing 🤝
|
## 📚 API Reference
|
||||||
|
|
||||||
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
### Core Classes
|
||||||
|
|
||||||
## Support 💬
|
- **`Task<T, TSteps>`** - Basic task unit with optional step tracking
|
||||||
|
- **`TaskManager`** - Central orchestrator for task management
|
||||||
|
- **`Taskchain`** - Sequential task executor
|
||||||
|
- **`TaskParallel`** - Concurrent task executor
|
||||||
|
- **`TaskOnce`** - Single-execution task
|
||||||
|
- **`TaskLoop`** - Repeating task with conditions
|
||||||
|
|
||||||
- 📧 Email: [hello@task.vc](mailto:hello@task.vc)
|
### Key Methods
|
||||||
- 🐛 Issues: [GitHub Issues](https://github.com/push-rocks/taskbuffer/issues)
|
|
||||||
- 📖 Docs: [Documentation](https://code.foss.global/push.rocks/taskbuffer)
|
#### Task Methods
|
||||||
|
- `trigger(input?: T): Promise<any>` - Execute the task
|
||||||
|
- `notifyStep(stepName: StepNames<TSteps>): void` - Update current step
|
||||||
|
- `getProgress(): number` - Get progress percentage (0-100)
|
||||||
|
- `getStepsMetadata(): ITaskStep[]` - Get detailed step information
|
||||||
|
- `getMetadata(): ITaskMetadata` - Get complete task metadata
|
||||||
|
|
||||||
|
#### TaskManager Methods
|
||||||
|
- `addTask(task: Task): void` - Register a task
|
||||||
|
- `getTaskByName(name: string): Task | undefined` - Retrieve task by name
|
||||||
|
- `addAndScheduleTask(task: Task, cronExpression: string): void` - Schedule task
|
||||||
|
- `descheduleTaskByName(name: string): void` - Remove scheduling
|
||||||
|
- `getTaskMetadata(name: string): ITaskMetadata | null` - Get task metadata
|
||||||
|
- `getAllTasksMetadata(): ITaskMetadata[]` - Get all tasks metadata
|
||||||
|
- `getScheduledTasks(): IScheduledTaskInfo[]` - List scheduled tasks
|
||||||
|
- `addExecuteRemoveTask(task, options?): Promise<ITaskExecutionReport>` - Execute once
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@@ -49,10 +49,12 @@ tap.test('should execute setup function before the task function', async () => {
|
|||||||
const task2 = new taskbuffer.Task({
|
const task2 = new taskbuffer.Task({
|
||||||
name: 'Task 2',
|
name: 'Task 2',
|
||||||
taskSetup: async () => {
|
taskSetup: async () => {
|
||||||
console.log('this is the setup function for task 2. It should only run once.')
|
console.log(
|
||||||
|
'this is the setup function for task 2. It should only run once.',
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
nice: 'yes',
|
nice: 'yes',
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
taskFunction: async (before, setupArg) => {
|
taskFunction: async (before, setupArg) => {
|
||||||
expect(setupArg).toEqual({ nice: 'yes' });
|
expect(setupArg).toEqual({ nice: 'yes' });
|
||||||
|
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();
|
@@ -29,7 +29,7 @@ tap.test('should run the task as expected', async () => {
|
|||||||
taskDone.resolve();
|
taskDone.resolve();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
myTaskManager.start();
|
myTaskManager.start();
|
||||||
await myTaskManager.triggerTaskByName('myTask');
|
await myTaskManager.triggerTaskByName('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();
|
@@ -16,7 +16,7 @@ tap.test('should execute task when its scheduled', async (tools) => {
|
|||||||
taskFunction: async () => {
|
taskFunction: async () => {
|
||||||
console.log('hi');
|
console.log('hi');
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
testTaskRunner.addTask(
|
testTaskRunner.addTask(
|
||||||
@@ -25,7 +25,7 @@ tap.test('should execute task when its scheduled', async (tools) => {
|
|||||||
console.log('there');
|
console.log('there');
|
||||||
done.resolve();
|
done.resolve();
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await done.promise;
|
await done.promise;
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/taskbuffer',
|
name: '@push.rocks/taskbuffer',
|
||||||
version: '3.1.8',
|
version: '3.4.0',
|
||||||
description: 'A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.'
|
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 { 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 { Taskchain } from './taskbuffer.classes.taskchain.js';
|
||||||
export { Taskparallel } from './taskbuffer.classes.taskparallel.js';
|
export { Taskparallel } from './taskbuffer.classes.taskparallel.js';
|
||||||
export { TaskManager } from './taskbuffer.classes.taskmanager.js';
|
export { TaskManager } from './taskbuffer.classes.taskmanager.js';
|
||||||
export { TaskOnce } from './taskbuffer.classes.taskonce.js';
|
export { TaskOnce } from './taskbuffer.classes.taskonce.js';
|
||||||
export { TaskRunner } from './taskbuffer.classes.taskrunner.js';
|
export { TaskRunner } from './taskbuffer.classes.taskrunner.js';
|
||||||
export { TaskDebounced } from './taskbuffer.classes.taskdebounced.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';
|
import * as distributedCoordination from './taskbuffer.classes.distributedcoordinator.js';
|
||||||
export { distributedCoordination };
|
export { distributedCoordination };
|
||||||
|
@@ -13,9 +13,8 @@ export class BufferRunner {
|
|||||||
if (!(this.bufferCounter >= this.task.bufferMax)) {
|
if (!(this.bufferCounter >= this.task.bufferMax)) {
|
||||||
this.bufferCounter++;
|
this.bufferCounter++;
|
||||||
}
|
}
|
||||||
const returnPromise: Promise<any> = this.task.cycleCounter.getPromiseForCycle(
|
const returnPromise: Promise<any> =
|
||||||
this.bufferCounter
|
this.task.cycleCounter.getPromiseForCycle(this.bufferCounter);
|
||||||
);
|
|
||||||
if (!this.task.running) {
|
if (!this.task.running) {
|
||||||
this._run(x);
|
this._run(x);
|
||||||
}
|
}
|
||||||
|
@@ -26,11 +26,11 @@ export interface IDistributedTaskRequestResult {
|
|||||||
|
|
||||||
export abstract class AbstractDistributedCoordinator {
|
export abstract class AbstractDistributedCoordinator {
|
||||||
public abstract fireDistributedTaskRequest(
|
public abstract fireDistributedTaskRequest(
|
||||||
infoBasis: IDistributedTaskRequest
|
infoBasis: IDistributedTaskRequest,
|
||||||
): Promise<IDistributedTaskRequestResult>;
|
): Promise<IDistributedTaskRequestResult>;
|
||||||
|
|
||||||
public abstract updateDistributedTaskRequest(
|
public abstract updateDistributedTaskRequest(
|
||||||
infoBasis: IDistributedTaskRequest
|
infoBasis: IDistributedTaskRequest,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
public abstract start(): Promise<void>;
|
public abstract start(): Promise<void>;
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import * as plugins from './taskbuffer.plugins.js';
|
import * as plugins from './taskbuffer.plugins.js';
|
||||||
import { BufferRunner } from './taskbuffer.classes.bufferrunner.js';
|
import { BufferRunner } from './taskbuffer.classes.bufferrunner.js';
|
||||||
import { CycleCounter } from './taskbuffer.classes.cyclecounter.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';
|
import { logger } from './taskbuffer.logging.js';
|
||||||
|
|
||||||
@@ -14,18 +16,21 @@ export interface ITaskSetupFunction<T = undefined> {
|
|||||||
|
|
||||||
export type TPreOrAfterTaskFunction = () => Task<any>;
|
export type TPreOrAfterTaskFunction = () => Task<any>;
|
||||||
|
|
||||||
export class Task<T = undefined> {
|
// Type helper to extract step names from array
|
||||||
public static extractTask<T = undefined>(
|
export type StepNames<T> = T extends ReadonlyArray<{ name: infer N }> ? N : never;
|
||||||
preOrAfterTaskArg: Task<T> | TPreOrAfterTaskFunction
|
|
||||||
): Task<T> {
|
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) {
|
switch (true) {
|
||||||
case !preOrAfterTaskArg:
|
case !preOrAfterTaskArg:
|
||||||
return null;
|
return null;
|
||||||
case preOrAfterTaskArg instanceof Task:
|
case preOrAfterTaskArg instanceof Task:
|
||||||
return preOrAfterTaskArg as Task<T>;
|
return preOrAfterTaskArg as Task<T, TSteps>;
|
||||||
case typeof preOrAfterTaskArg === 'function':
|
case typeof preOrAfterTaskArg === 'function':
|
||||||
const taskFunction = preOrAfterTaskArg as TPreOrAfterTaskFunction;
|
const taskFunction = preOrAfterTaskArg as TPreOrAfterTaskFunction;
|
||||||
return taskFunction();
|
return taskFunction() as unknown as Task<T, TSteps>;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -45,9 +50,9 @@ export class Task<T = undefined> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static isTaskTouched<T = undefined>(
|
public static isTaskTouched<T = undefined, TSteps extends ReadonlyArray<{ name: string; description: string; percentage: number }> = []>(
|
||||||
taskArg: Task<T> | TPreOrAfterTaskFunction,
|
taskArg: Task<T, TSteps> | TPreOrAfterTaskFunction,
|
||||||
touchedTasksArray: Task<T>[]
|
touchedTasksArray: Task<T, TSteps>[],
|
||||||
): boolean {
|
): boolean {
|
||||||
const taskToCheck = Task.extractTask(taskArg);
|
const taskToCheck = Task.extractTask(taskArg);
|
||||||
let result = false;
|
let result = false;
|
||||||
@@ -59,9 +64,9 @@ export class Task<T = undefined> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static runTask = async <T>(
|
public static runTask = async <T, TSteps extends ReadonlyArray<{ name: string; description: string; percentage: number }> = []>(
|
||||||
taskArg: Task<T> | TPreOrAfterTaskFunction,
|
taskArg: Task<T, TSteps> | TPreOrAfterTaskFunction,
|
||||||
optionsArg: { x?: any; touchedTasksArray?: Task<T>[] }
|
optionsArg: { x?: any; touchedTasksArray?: Task<T, TSteps>[] },
|
||||||
) => {
|
) => {
|
||||||
const taskToRun = Task.extractTask(taskArg);
|
const taskToRun = Task.extractTask(taskArg);
|
||||||
const done = plugins.smartpromise.defer();
|
const done = plugins.smartpromise.defer();
|
||||||
@@ -80,10 +85,18 @@ export class Task<T = undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
taskToRun.running = true;
|
taskToRun.running = true;
|
||||||
|
taskToRun.runCount++;
|
||||||
|
taskToRun.lastRun = new Date();
|
||||||
|
|
||||||
|
// Reset steps at the beginning of task execution
|
||||||
|
taskToRun.resetSteps();
|
||||||
|
|
||||||
done.promise.then(async () => {
|
done.promise.then(async () => {
|
||||||
taskToRun.running = false;
|
taskToRun.running = false;
|
||||||
|
|
||||||
|
// Complete all steps when task finishes
|
||||||
|
taskToRun.completeAllSteps();
|
||||||
|
|
||||||
// When the task has finished running, resolve the finished promise
|
// When the task has finished running, resolve the finished promise
|
||||||
taskToRun.resolveFinished();
|
taskToRun.resolveFinished();
|
||||||
|
|
||||||
@@ -98,14 +111,17 @@ export class Task<T = undefined> {
|
|||||||
...optionsArg,
|
...optionsArg,
|
||||||
};
|
};
|
||||||
const x = options.x;
|
const x = options.x;
|
||||||
const touchedTasksArray: Task<T>[] = options.touchedTasksArray;
|
const touchedTasksArray: Task<T, TSteps>[] = options.touchedTasksArray;
|
||||||
|
|
||||||
touchedTasksArray.push(taskToRun);
|
touchedTasksArray.push(taskToRun);
|
||||||
|
|
||||||
const localDeferred = plugins.smartpromise.defer();
|
const localDeferred = plugins.smartpromise.defer();
|
||||||
localDeferred.promise
|
localDeferred.promise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (taskToRun.preTask && !Task.isTaskTouched(taskToRun.preTask, touchedTasksArray)) {
|
if (
|
||||||
|
taskToRun.preTask &&
|
||||||
|
!Task.isTaskTouched(taskToRun.preTask, touchedTasksArray)
|
||||||
|
) {
|
||||||
return Task.runTask(taskToRun.preTask, { x, touchedTasksArray });
|
return Task.runTask(taskToRun.preTask, { x, touchedTasksArray });
|
||||||
} else {
|
} else {
|
||||||
const done2 = plugins.smartpromise.defer();
|
const done2 = plugins.smartpromise.defer();
|
||||||
@@ -121,8 +137,14 @@ export class Task<T = undefined> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
if (taskToRun.afterTask && !Task.isTaskTouched(taskToRun.afterTask, touchedTasksArray)) {
|
if (
|
||||||
return Task.runTask(taskToRun.afterTask, { x: x, touchedTasksArray: touchedTasksArray });
|
taskToRun.afterTask &&
|
||||||
|
!Task.isTaskTouched(taskToRun.afterTask, touchedTasksArray)
|
||||||
|
) {
|
||||||
|
return Task.runTask(taskToRun.afterTask, {
|
||||||
|
x: x,
|
||||||
|
touchedTasksArray: touchedTasksArray,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const done2 = plugins.smartpromise.defer();
|
const done2 = plugins.smartpromise.defer();
|
||||||
done2.resolve(x);
|
done2.resolve(x);
|
||||||
@@ -149,8 +171,8 @@ export class Task<T = undefined> {
|
|||||||
public execDelay: number;
|
public execDelay: number;
|
||||||
public timeout: number;
|
public timeout: number;
|
||||||
|
|
||||||
public preTask: Task<T> | TPreOrAfterTaskFunction;
|
public preTask: Task<T, any> | TPreOrAfterTaskFunction;
|
||||||
public afterTask: Task<T> | TPreOrAfterTaskFunction;
|
public afterTask: Task<T, any> | TPreOrAfterTaskFunction;
|
||||||
|
|
||||||
// Add a list to store the blocking tasks
|
// Add a list to store the blocking tasks
|
||||||
public blockingTasks: Task[] = [];
|
public blockingTasks: Task[] = [];
|
||||||
@@ -162,6 +184,8 @@ export class Task<T = undefined> {
|
|||||||
public running: boolean = false;
|
public running: boolean = false;
|
||||||
public bufferRunner = new BufferRunner(this);
|
public bufferRunner = new BufferRunner(this);
|
||||||
public cycleCounter = new CycleCounter(this);
|
public cycleCounter = new CycleCounter(this);
|
||||||
|
public lastRun?: Date;
|
||||||
|
public runCount: number = 0;
|
||||||
|
|
||||||
public get idle() {
|
public get idle() {
|
||||||
return !this.running;
|
return !this.running;
|
||||||
@@ -170,15 +194,22 @@ export class Task<T = undefined> {
|
|||||||
public taskSetup: ITaskSetupFunction<T>;
|
public taskSetup: ITaskSetupFunction<T>;
|
||||||
public setupValue: 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: {
|
constructor(optionsArg: {
|
||||||
taskFunction: ITaskFunction<T>;
|
taskFunction: ITaskFunction<T>;
|
||||||
preTask?: Task<T> | TPreOrAfterTaskFunction;
|
preTask?: Task<T, any> | TPreOrAfterTaskFunction;
|
||||||
afterTask?: Task<T> | TPreOrAfterTaskFunction;
|
afterTask?: Task<T, any> | TPreOrAfterTaskFunction;
|
||||||
buffered?: boolean;
|
buffered?: boolean;
|
||||||
bufferMax?: number;
|
bufferMax?: number;
|
||||||
execDelay?: number;
|
execDelay?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
taskSetup?: ITaskSetupFunction<T>;
|
taskSetup?: ITaskSetupFunction<T>;
|
||||||
|
steps?: TSteps;
|
||||||
}) {
|
}) {
|
||||||
this.taskFunction = optionsArg.taskFunction;
|
this.taskFunction = optionsArg.taskFunction;
|
||||||
this.preTask = optionsArg.preTask;
|
this.preTask = optionsArg.preTask;
|
||||||
@@ -189,6 +220,19 @@ export class Task<T = undefined> {
|
|||||||
this.name = optionsArg.name;
|
this.name = optionsArg.name;
|
||||||
this.taskSetup = optionsArg.taskSetup;
|
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
|
// Create the finished promise
|
||||||
this.finished = new Promise((resolve) => {
|
this.finished = new Promise((resolve) => {
|
||||||
this.resolveFinished = resolve;
|
this.resolveFinished = resolve;
|
||||||
@@ -204,10 +248,102 @@ export class Task<T = undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public triggerUnBuffered(x?: any): Promise<any> {
|
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> {
|
public triggerBuffered(x?: any): Promise<any> {
|
||||||
return this.bufferRunner.trigger(x);
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,14 +27,18 @@ export class Taskchain extends Task {
|
|||||||
let taskCounter = 0; // counter for iterating async over the taskArray
|
let taskCounter = 0; // counter for iterating async over the taskArray
|
||||||
const iterateTasks = (x: any) => {
|
const iterateTasks = (x: any) => {
|
||||||
if (typeof this.taskArray[taskCounter] !== 'undefined') {
|
if (typeof this.taskArray[taskCounter] !== 'undefined') {
|
||||||
console.log(this.name + ' running: Task' + this.taskArray[taskCounter].name);
|
console.log(
|
||||||
|
this.name + ' running: Task' + this.taskArray[taskCounter].name,
|
||||||
|
);
|
||||||
this.taskArray[taskCounter].trigger(x).then((x) => {
|
this.taskArray[taskCounter].trigger(x).then((x) => {
|
||||||
logger.log('info', this.taskArray[taskCounter].name);
|
logger.log('info', this.taskArray[taskCounter].name);
|
||||||
taskCounter++;
|
taskCounter++;
|
||||||
iterateTasks(x);
|
iterateTasks(x);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('Taskchain "' + this.name + '" completed successfully');
|
console.log(
|
||||||
|
'Taskchain "' + this.name + '" completed successfully',
|
||||||
|
);
|
||||||
done.resolve(x);
|
done.resolve(x);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -19,7 +19,9 @@ export class TaskDebounced<T = unknown> extends Task {
|
|||||||
});
|
});
|
||||||
this.taskFunction = optionsArg.taskFunction;
|
this.taskFunction = optionsArg.taskFunction;
|
||||||
this._observableIntake.observable
|
this._observableIntake.observable
|
||||||
.pipe(plugins.smartrx.rxjs.ops.debounceTime(optionsArg.debounceTimeInMillis))
|
.pipe(
|
||||||
|
plugins.smartrx.rxjs.ops.debounceTime(optionsArg.debounceTimeInMillis),
|
||||||
|
)
|
||||||
.subscribe((x) => {
|
.subscribe((x) => {
|
||||||
this.taskFunction(x);
|
this.taskFunction(x);
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import * as plugins from './taskbuffer.plugins.js';
|
import * as plugins from './taskbuffer.plugins.js';
|
||||||
import { Task } from './taskbuffer.classes.task.js';
|
import { Task } from './taskbuffer.classes.task.js';
|
||||||
import { AbstractDistributedCoordinator, type IDistributedTaskRequestResult } from './taskbuffer.classes.distributedcoordinator.js';
|
import {
|
||||||
|
AbstractDistributedCoordinator,
|
||||||
|
type IDistributedTaskRequestResult,
|
||||||
|
} from './taskbuffer.classes.distributedcoordinator.js';
|
||||||
|
import type { ITaskMetadata, ITaskExecutionReport, IScheduledTaskInfo } from './taskbuffer.interfaces.js';
|
||||||
|
|
||||||
export interface ICronJob {
|
export interface ICronJob {
|
||||||
cronString: string;
|
cronString: string;
|
||||||
@@ -14,7 +18,7 @@ export interface ITaskManagerConstructorOptions {
|
|||||||
|
|
||||||
export class TaskManager {
|
export class TaskManager {
|
||||||
public randomId = plugins.smartunique.shortId();
|
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();
|
private cronJobManager = new plugins.smarttime.CronManager();
|
||||||
public options: ITaskManagerConstructorOptions = {
|
public options: ITaskManagerConstructorOptions = {
|
||||||
distributedCoordinator: null,
|
distributedCoordinator: null,
|
||||||
@@ -24,18 +28,18 @@ export class TaskManager {
|
|||||||
this.options = Object.assign(this.options, options);
|
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);
|
return this.taskMap.findSync((task) => task.name === taskName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addTask(task: Task): void {
|
public addTask(task: Task<any, any>): void {
|
||||||
if (!task.name) {
|
if (!task.name) {
|
||||||
throw new Error('Task must have a name to be added to taskManager');
|
throw new Error('Task must have a name to be added to taskManager');
|
||||||
}
|
}
|
||||||
this.taskMap.add(task);
|
this.taskMap.add(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addAndScheduleTask(task: Task, cronString: string) {
|
public addAndScheduleTask(task: Task<any, any>, cronString: string) {
|
||||||
this.addTask(task);
|
this.addTask(task);
|
||||||
this.scheduleTaskByName(task.name, cronString);
|
this.scheduleTaskByName(task.name, cronString);
|
||||||
}
|
}
|
||||||
@@ -48,7 +52,7 @@ export class TaskManager {
|
|||||||
return taskToTrigger.trigger();
|
return taskToTrigger.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async triggerTask(task: Task) {
|
public async triggerTask(task: Task<any, any>) {
|
||||||
return task.trigger();
|
return task.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +64,16 @@ export class TaskManager {
|
|||||||
this.handleTaskScheduling(taskToSchedule, cronString);
|
this.handleTaskScheduling(taskToSchedule, cronString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTaskScheduling(task: Task, cronString: string) {
|
private handleTaskScheduling(task: Task<any, any>, cronString: string) {
|
||||||
const cronJob = this.cronJobManager.addCronjob(
|
const cronJob = this.cronJobManager.addCronjob(
|
||||||
cronString,
|
cronString,
|
||||||
async (triggerTime: number) => {
|
async (triggerTime: number) => {
|
||||||
this.logTaskState(task);
|
this.logTaskState(task);
|
||||||
if (this.options.distributedCoordinator) {
|
if (this.options.distributedCoordinator) {
|
||||||
const announcementResult = await this.performDistributedConsultation(task, triggerTime);
|
const announcementResult = await this.performDistributedConsultation(
|
||||||
|
task,
|
||||||
|
triggerTime,
|
||||||
|
);
|
||||||
if (!announcementResult.shouldTrigger) {
|
if (!announcementResult.shouldTrigger) {
|
||||||
console.log('Distributed coordinator result: NOT EXECUTING');
|
console.log('Distributed coordinator result: NOT EXECUTING');
|
||||||
return;
|
return;
|
||||||
@@ -75,12 +82,12 @@ export class TaskManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await task.trigger();
|
await task.trigger();
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
task.cronJob = cronJob;
|
task.cronJob = cronJob;
|
||||||
}
|
}
|
||||||
|
|
||||||
private logTaskState(task: Task) {
|
private logTaskState(task: Task<any, any>) {
|
||||||
console.log(`Taskbuffer schedule triggered task >>${task.name}<<`);
|
console.log(`Taskbuffer schedule triggered task >>${task.name}<<`);
|
||||||
const bufferState = task.buffered
|
const bufferState = task.buffered
|
||||||
? `buffered with max ${task.bufferMax} buffered calls`
|
? `buffered with max ${task.bufferMax} buffered calls`
|
||||||
@@ -88,7 +95,10 @@ export class TaskManager {
|
|||||||
console.log(`Task >>${task.name}<< is ${bufferState}`);
|
console.log(`Task >>${task.name}<< is ${bufferState}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async performDistributedConsultation(task: Task, triggerTime: number): Promise<IDistributedTaskRequestResult> {
|
private async performDistributedConsultation(
|
||||||
|
task: Task<any, any>,
|
||||||
|
triggerTime: number,
|
||||||
|
): Promise<IDistributedTaskRequestResult> {
|
||||||
console.log('Found a distributed coordinator, performing consultation.');
|
console.log('Found a distributed coordinator, performing consultation.');
|
||||||
|
|
||||||
return this.options.distributedCoordinator.fireDistributedTaskRequest({
|
return this.options.distributedCoordinator.fireDistributedTaskRequest({
|
||||||
@@ -114,7 +124,7 @@ export class TaskManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async descheduleTask(task: Task) {
|
public async descheduleTask(task: Task<any, any>) {
|
||||||
await this.descheduleTaskByName(task.name);
|
await this.descheduleTaskByName(task.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,4 +146,123 @@ export class TaskManager {
|
|||||||
await this.options.distributedCoordinator.stop();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,8 @@ import { Task } from './taskbuffer.classes.task.js';
|
|||||||
export class TaskRunner {
|
export class TaskRunner {
|
||||||
public maxParrallelJobs: number = 1;
|
public maxParrallelJobs: number = 1;
|
||||||
public status: 'stopped' | 'running' = 'stopped';
|
public status: 'stopped' | 'running' = 'stopped';
|
||||||
public runningTasks: plugins.lik.ObjectMap<Task> = new plugins.lik.ObjectMap<Task>();
|
public runningTasks: plugins.lik.ObjectMap<Task> =
|
||||||
|
new plugins.lik.ObjectMap<Task>();
|
||||||
public qeuedTasks: Task[] = [];
|
public qeuedTasks: Task[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
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;
|
||||||
|
}
|
@@ -6,4 +6,12 @@ import * as smartrx from '@push.rocks/smartrx';
|
|||||||
import * as smarttime from '@push.rocks/smarttime';
|
import * as smarttime from '@push.rocks/smarttime';
|
||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
|
|
||||||
export { lik, smartlog, smartpromise, smartdelay, smartrx, smarttime, smartunique };
|
export {
|
||||||
|
lik,
|
||||||
|
smartlog,
|
||||||
|
smartpromise,
|
||||||
|
smartdelay,
|
||||||
|
smartrx,
|
||||||
|
smarttime,
|
||||||
|
smartunique,
|
||||||
|
};
|
||||||
|
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.4.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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@@ -6,9 +6,9 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["dist_*/**/*.d.ts"]
|
||||||
"dist_*/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user