Compare commits

...

4 Commits

19 changed files with 4294 additions and 2930 deletions

View File

@@ -1,35 +0,0 @@
# Code Style and Conventions for TaskBuffer
## File Structure
- Source code in `ts/` directory
- Tests in `test/` directory
- Compiled output in `dist_ts/`
- All filenames must be lowercase
## Naming Conventions
- **Interfaces**: Prefix with `I` (e.g., `ITaskFunction`)
- **Types**: Prefix with `T` (e.g., `TPreOrAfterTaskFunction`)
- **Classes**: PascalCase (e.g., `TaskManager`)
- **Files**: `taskbuffer.classes.{classname}.ts` pattern
- **Test files**: `test.{number}.{feature}.ts` pattern
## TypeScript Conventions
- Use ES modules (import/export)
- Avoid ENums when possible
- Import dependencies through `plugins.ts`
- Reference with full path: `plugins.myModule.myClass()`
- Use async/await patterns consistently
- Strong typing throughout
## Testing Conventions
- Import expect from `@git.zone/tstest/tapbundle`
- Test files end with `export default tap.start()`
- Use descriptive test names with tap.test()
- Test both browser and node when applicable
## Code Quality
- Make focused, goal-oriented changes
- Preserve necessary complexity
- Keep code elegant and maintainable
- No inline documentation unless requested
- Complete implementations only (no partial work)

View File

@@ -1,27 +0,0 @@
# TaskBuffer Project Overview
## Purpose
@push.rocks/taskbuffer is a powerful TypeScript-first task management library for orchestrating asynchronous operations. It provides flexible task execution patterns including buffered execution, task chains, parallel execution, scheduling, debouncing, and one-time execution.
## Tech Stack
- **Language**: TypeScript (ES modules)
- **Runtime**: Node.js
- **Build Tool**: tsbuild, tsbundle
- **Test Framework**: @git.zone/tstest (tapbundle)
- **Package Manager**: pnpm
- **Module System**: ES modules (type: "module")
## Key Features
- Task: Basic unit of work with async function wrapping
- Taskchain: Sequential task execution with result passing
- Taskparallel: Parallel task execution
- TaskManager: Cron-based task scheduling
- TaskDebounced: Debounced execution pattern
- TaskOnce: Singleton execution pattern
- TaskRunner: Distributed task execution
- BufferRunner: Smart concurrent execution control
## Main Entry Points
- Main export: `ts/index.ts`
- Compiled output: `dist_ts/index.js`
- All classes exported from index for clean API

View File

@@ -1,67 +0,0 @@
# Suggested Commands for TaskBuffer Development
## Build and Test Commands
```bash
# Build the project
pnpm run build
# Run tests
pnpm test
# Type checking
tsbuild check test/**/* --skiplibcheck
# Build documentation
pnpm run buildDocs
```
## Development Workflow
```bash
# Install dependencies
pnpm install
# Add development dependency
pnpm install --save-dev <package>
# Add production dependency
pnpm add <package>
# Run specific test
tstest test/test.some.ts --verbose
# Run tests with logging
tstest test/test.some.ts --logfile
```
## Git Commands
```bash
# View status
git status
# Stage changes
git add .
# Commit with message
git commit -m "message"
# Use git mv for file operations to preserve history
git mv oldfile newfile
```
## System Commands (Linux)
```bash
# List files
ls -la
# Find files
find . -name "*.ts"
# Search in files
rg "pattern" # Use ripgrep instead of grep
# View file
cat filename
# Create directory
mkdir -p path/to/dir
```

View File

@@ -1,37 +0,0 @@
# Task Completion Checklist for TaskBuffer
## Before Marking Any Task Complete
### 1. Code Quality Checks
- [ ] Run build to ensure TypeScript compiles: `pnpm run build`
- [ ] Run tests to ensure nothing breaks: `pnpm test`
- [ ] Type check test files: `tsbuild check test/**/* --skiplibcheck`
### 2. Code Review
- [ ] All changes are focused and purposeful
- [ ] No unnecessary modifications made
- [ ] Code follows project naming conventions
- [ ] Imports use the plugins.ts pattern where applicable
- [ ] All async operations use proper async/await
### 3. Testing
- [ ] New features have corresponding tests
- [ ] Existing tests still pass
- [ ] Test files end with `export default tap.start()`
- [ ] Tests use proper expect from tapbundle
### 4. Documentation
- [ ] readme.md updated if new features added
- [ ] Code is self-explanatory (no comments unless requested)
- [ ] API changes documented
### 5. Git Hygiene
- [ ] Changes are staged appropriately
- [ ] Commit message is clear and focused
- [ ] NO commits made without explicit user approval
## Common Issues to Check
- No uppercase filenames
- No direct npm usage (use pnpm)
- No guessing APIs (always check documentation)
- No partial implementations

View File

@@ -1,68 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "taskbuffer"

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Push.Rocks
Copyright (c) 2016 Task Venture Capital GmbH <hello@task.vc>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,5 +1,28 @@
# Changelog
## 2025-12-04 - 3.5.0 - feat(core)
Add debounced tasks and step-based progress tracking; upgrade deps and improve dashboard and scheduling
- Add TaskDebounced class to coalesce rapid triggers into a single execution (debounce behavior).
- Introduce step tracking and progress reporting on Task via TaskStep, getProgress(), getStepsMetadata(), getMetadata(), resetSteps(), and completeAllSteps().
- Enhance buffered execution flow: BufferRunner and CycleCounter improvements to better coordinate buffered runs and cycle promises.
- Standardize concurrent runner naming (Taskparallel) and update related exports/usages (ts/index.ts, readme examples).
- Enhance TaskManager scheduling/metadata: getScheduledTasks now returns schedule and nextRun, addExecuteRemoveTask collects execution report metadata and cleans up after execution, distributed coordination hooks retained.
- Add/upgrade web dashboard UI, demos and refresh logic to surface task metadata, scheduled tasks and progress.
- Bump runtime and dev dependencies (multiple @push.rocks packages and @git.zone tooling).
- Update tests: reduce iteration threshold and tighten schedule interval in test/test.4.taskmanager.ts.
- Remove several .serena memory files (project overview, style guides and suggested commands) as cleanup.
## 2025-09-07 - 3.4.0 - feat(taskbuffer-dashboard)
Add TaskBuffer dashboard web component, demo and browser tests; add HTML entry and update dependencies
- Introduce a new web component taskbuffer-dashboard for real-time visualization of tasks and schedules (ts_web/taskbuffer-dashboard.ts).
- Add a demo wrapper and interactive UI for the dashboard (ts_web/elements/taskbuffer-dashboard.demo.ts).
- Provide web exports and typings for web usage (ts_web/index.ts) and include an HTML entry (html/index.html).
- Add browser-oriented tests to validate metadata structures for the web component (test/test.10.webcomponent.browser.ts).
- Bump package version to 3.3.0 in package.json as part of this change.
- Update/add dependencies and devDependencies (@design.estate/dees-element added; smartlog, @git.zone/tsbuild and @git.zone/tstest bumped).
## 2025-09-06 - 3.2.0 - feat(core)
Add step-based progress tracking, task metadata and enhanced TaskManager scheduling/metadata APIs

21
html/index.html Normal file
View 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>

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/taskbuffer",
"version": "3.2.0",
"version": "3.5.0",
"private": false,
"description": "A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.",
"main": "dist_ts/index.js",
@@ -34,20 +34,21 @@
},
"homepage": "https://code.foss.global/push.rocks/taskbuffer#readme",
"dependencies": {
"@push.rocks/lik": "^6.0.5",
"@design.estate/dees-element": "^2.1.3",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartlog": "^3.0.3",
"@push.rocks/smartpromise": "^4.0.3",
"@push.rocks/smartrx": "^3.0.6",
"@push.rocks/smarttime": "^4.0.6",
"@push.rocks/smartunique": "^3.0.6"
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartunique": "^3.0.9"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.66",
"@git.zone/tsbundle": "^2.0.8",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^2.3.5",
"@types/node": "^20.8.7"
"@git.zone/tsbuild": "^3.1.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^3.1.3",
"@types/node": "^24.10.1"
},
"files": [
"ts/**/*",

4511
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1389
readme.md

File diff suppressed because it is too large Load Diff

View 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();

View File

@@ -25,7 +25,7 @@ tap.test('should run the task as expected', async () => {
console.log('Task "myTask" executed!');
referenceBoolean = true;
taskRunCounter++;
if (taskRunCounter === 10) {
if (taskRunCounter === 3) {
taskDone.resolve();
}
},
@@ -38,7 +38,7 @@ tap.test('should run the task as expected', async () => {
});
tap.test('should schedule task', async () => {
myTaskManager.scheduleTaskByName('myTask', '*/10 * * * * *');
myTaskManager.scheduleTaskByName('myTask', '*/5 * * * * *');
await taskDone.promise;
myTaskManager.descheduleTaskByName('myTask');
});

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/taskbuffer',
version: '3.2.0',
version: '3.5.0',
description: 'A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.'
}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/taskbuffer',
version: '3.2.0',
version: '3.5.0',
description: 'A flexible task management library supporting TypeScript, allowing for task buffering, scheduling, and execution with dependency management.'
}

View 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
View 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';

View 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>
`;
}
}